/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details: * * Copyright (C) 2021-2022 Aleksander Morgado */ #include #include #include #include "mm-errors-types.h" #include "mm-utils.h" #include "mm-log-object.h" #include "mm-dispatcher.h" static void log_object_iface_init (MMLogObjectInterface *iface); G_DEFINE_TYPE_EXTENDED (MMDispatcher, mm_dispatcher, G_TYPE_OBJECT, 0, G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init)) enum { PROP_0, PROP_OPERATION_DESCRIPTION, PROP_LAST }; static GParamSpec *properties[PROP_LAST]; struct _MMDispatcherPrivate { gchar *operation_description; GSubprocessLauncher *launcher; }; /*****************************************************************************/ static gchar * log_object_build_id (MMLogObject *_self) { MMDispatcher *self = MM_DISPATCHER (_self); return g_strdup_printf ("%s dispatcher", self->priv->operation_description); } /*****************************************************************************/ static gboolean validate_file (const gchar *path, GError **error) { g_autoptr(GFile) file = NULL; g_autoptr(GFileInfo) file_info = NULL; guint32 file_mode; guint32 file_uid; file = g_file_new_for_path (path); file_info = g_file_query_info (file, (G_FILE_ATTRIBUTE_STANDARD_SIZE "," G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET "," G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK "," G_FILE_ATTRIBUTE_UNIX_MODE "," G_FILE_ATTRIBUTE_UNIX_UID), G_FILE_QUERY_INFO_NONE, NULL, error); if (!file_info) return FALSE; if (g_file_info_get_is_symlink (file_info)) { const gchar *link_target; link_target = g_file_info_get_symlink_target (file_info); if (g_strcmp0 (link_target, "/dev/null") == 0) { g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, "Link '%s' to /dev/null is not executable", path); return FALSE; } } if (g_file_info_get_size (file_info) == 0) { g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, "File '%s' is empty", path); return FALSE; } file_uid = g_file_info_get_attribute_uint32 (file_info, G_FILE_ATTRIBUTE_UNIX_UID); if (file_uid != 0) { g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, "File '%s' not owned by root", path); return FALSE; } file_mode = g_file_info_get_attribute_uint32 (file_info, G_FILE_ATTRIBUTE_UNIX_MODE); if (!S_ISREG (file_mode)) { g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, "File '%s' is not regular", path); return FALSE; } if (file_mode & (S_IWGRP | S_IWOTH)) { g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, "File '%s' is writable by group or other", path); return FALSE; } if (file_mode & S_ISUID) { g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, "File '%s' is set-UID", path); return FALSE; } if (!(file_mode & S_IXUSR)) { g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, "File '%s' is not executable by the owner", path); return FALSE; } return TRUE; } /*****************************************************************************/ typedef struct { GSubprocess *subprocess; guint timeout_id; } RunContext; static void run_context_free (RunContext *ctx) { g_assert (!ctx->timeout_id); g_clear_object (&ctx->subprocess); g_slice_free (RunContext, ctx); } gboolean mm_dispatcher_run_finish (MMDispatcher *self, GAsyncResult *res, GError **error) { return g_task_propagate_boolean (G_TASK (res), error); } static gboolean subprocess_wait_timed_out (GTask *task) { MMDispatcher *self; RunContext *ctx; self = g_task_get_source_object (task); ctx = g_task_get_task_data (task); mm_obj_warn (self, "forcing exit on %s operation", self->priv->operation_description); g_subprocess_force_exit (ctx->subprocess); ctx->timeout_id = 0; return G_SOURCE_REMOVE; } static void subprocess_wait_ready (GSubprocess *subprocess, GAsyncResult *res, GTask *task) { GError *error = NULL; MMDispatcher *self; RunContext *ctx; self = g_task_get_source_object (task); ctx = g_task_get_task_data (task); /* cleanup timeout before any return */ if (ctx->timeout_id) { g_source_remove (ctx->timeout_id); ctx->timeout_id = 0; } if (!g_subprocess_wait_finish (subprocess, res, &error)) { g_prefix_error (&error, "%s operation wait failed: ", self->priv->operation_description); g_task_return_error (task, error); } else if (!g_subprocess_get_successful (subprocess)) { if (g_subprocess_get_if_signaled (subprocess)) g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "%s operation aborted with signal %d", self->priv->operation_description, g_subprocess_get_term_sig (subprocess)); else if (g_subprocess_get_if_exited (subprocess)) g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "%s operation finished with status %d", self->priv->operation_description, g_subprocess_get_exit_status (subprocess)); else g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "%s operation failed", self->priv->operation_description); } else g_task_return_boolean (task, TRUE); g_object_unref (task); } void mm_dispatcher_run (MMDispatcher *self, const GStrv argv, guint timeout_secs, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GTask *task; RunContext *ctx; GError *error = NULL; g_assert (g_strv_length (argv) >= 1); task = g_task_new (self, cancellable, callback, user_data); ctx = g_slice_new0 (RunContext); g_task_set_task_data (task, ctx, (GDestroyNotify) run_context_free); /* Validation checks to see if we should run it or not */ if (!validate_file (argv[0], &error)) { g_prefix_error (&error, "Cannot run %s operation from %s: ", self->priv->operation_description, argv[0]); g_task_return_error (task, error); g_object_unref (task); return; } /* create and launch subprocess */ ctx->subprocess = g_subprocess_launcher_spawnv (self->priv->launcher, (const gchar * const *)argv, &error); if (!ctx->subprocess) { g_prefix_error (&error, "%s operation launch from %s failed: ", self->priv->operation_description, argv[0]); g_task_return_error (task, error); g_object_unref (task); return; } /* setup timeout */ ctx->timeout_id = g_timeout_add_seconds (timeout_secs, (GSourceFunc)subprocess_wait_timed_out, task); /* wait for subprocess exit */ g_subprocess_wait_async (ctx->subprocess, cancellable, (GAsyncReadyCallback)subprocess_wait_ready, task); } /*****************************************************************************/ static void mm_dispatcher_init (MMDispatcher *self) { /* Initialize private data */ self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_DISPATCHER, MMDispatcherPrivate); /* Create launcher and inherit parent's environment */ self->priv->launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE | G_SUBPROCESS_FLAGS_STDERR_SILENCE); g_subprocess_launcher_set_environ (self->priv->launcher, NULL); } static void set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { MMDispatcher *self = MM_DISPATCHER (object); switch (prop_id) { case PROP_OPERATION_DESCRIPTION: /* construct only */ self->priv->operation_description = g_value_dup_string (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { MMDispatcher *self = MM_DISPATCHER (object); switch (prop_id) { case PROP_OPERATION_DESCRIPTION: g_value_set_string (value, self->priv->operation_description); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void dispose (GObject *object) { MMDispatcher *self = MM_DISPATCHER (object); g_clear_object (&self->priv->launcher); G_OBJECT_CLASS (mm_dispatcher_parent_class)->dispose (object); } static void finalize (GObject *object) { MMDispatcher *self = MM_DISPATCHER (object); g_free (self->priv->operation_description); G_OBJECT_CLASS (mm_dispatcher_parent_class)->finalize (object); } static void log_object_iface_init (MMLogObjectInterface *iface) { iface->build_id = log_object_build_id; } static void mm_dispatcher_class_init (MMDispatcherClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); g_type_class_add_private (object_class, sizeof (MMDispatcherPrivate)); object_class->get_property = get_property; object_class->set_property = set_property; object_class->dispose = dispose; object_class->finalize = finalize; properties[PROP_OPERATION_DESCRIPTION] = g_param_spec_string (MM_DISPATCHER_OPERATION_DESCRIPTION, "Operation description", "String describing the operation, to be used in logging", NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); g_object_class_install_property (object_class, PROP_OPERATION_DESCRIPTION, properties[PROP_OPERATION_DESCRIPTION]); }