/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: * Copyright © 2014-2018 Red Hat, Inc * Copyright © 2024 GNOME Foundation, Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: * Alexander Larsson * Georges Basile Stavracas Neto * Hubert Figuière */ #include "config.h" #include "flatpak-context-private.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libglnx.h" #include "flatpak-error.h" #include "flatpak-metadata-private.h" #include "flatpak-usb-private.h" #include "flatpak-utils-base-private.h" #include "flatpak-utils-private.h" /* Same order as enum */ const char *flatpak_context_shares[] = { "network", "ipc", NULL }; /* Same order as enum */ const char *flatpak_context_sockets[] = { "x11", "wayland", "pulseaudio", "session-bus", "system-bus", "fallback-x11", "ssh-auth", "pcsc", "cups", "gpg-agent", "inherit-wayland-socket", NULL }; const char *flatpak_context_devices[] = { "dri", "all", "kvm", "shm", "input", "usb", NULL }; const char *flatpak_context_features[] = { "devel", "multiarch", "bluetooth", "canbus", "per-app-dev-shm", NULL }; const char *flatpak_context_special_filesystems[] = { "home", "host", "host-etc", "host-os", "host-reset", "host-root", NULL }; const char *flatpak_context_conditions[] = { "true", "false", "has-input-device", "has-usb-device", "has-wayland", "has-usb-portal", NULL }; FlatpakContextConditions flatpak_context_true_conditions = FLATPAK_CONTEXT_CONDITION_TRUE | FLATPAK_CONTEXT_CONDITION_HAS_INPUT_DEV | FLATPAK_CONTEXT_CONDITION_HAS_USB_DEV; static const char *parse_negated (const char *option, gboolean *negated); static guint32 flatpak_context_bitmask_from_string (const char *name, const char **names); typedef struct FlatpakPermission FlatpakPermission; struct FlatpakPermission { /* Is the permission unconditionally allowed */ gboolean allowed; /* When layering, reset all permissions below */ gboolean reset; /* Assumes allowed is false */ GPtrArray *conditionals; /* Only used during deserialization */ gboolean disallow_if_conditional; gboolean disallow_if_conditional_original_reset; GPtrArray *disallow_if_conditional_original_conditionals; }; static FlatpakPermission * flatpak_permission_new (void) { FlatpakPermission *permission; permission = g_slice_new0 (FlatpakPermission); permission->conditionals = g_ptr_array_new_with_free_func (g_free); permission->disallow_if_conditional_original_conditionals = g_ptr_array_new_with_free_func (g_free); return permission; }; static void flatpak_permission_free (FlatpakPermission *permission) { g_ptr_array_free (permission->conditionals, TRUE); g_ptr_array_free (permission->disallow_if_conditional_original_conditionals, TRUE); g_slice_free (FlatpakPermission, permission); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakPermission, flatpak_permission_free) static FlatpakPermission * flatpak_permission_dup (FlatpakPermission *permission) { FlatpakPermission *copy = NULL; copy = flatpak_permission_new (); if (!permission) return copy; copy->allowed = permission->allowed; copy->reset = permission->reset; for (size_t i = 0; i < permission->conditionals->len; i++) { const char *condition = permission->conditionals->pdata[i]; g_ptr_array_add (copy->conditionals, g_strdup (condition)); } return copy; } static void flatpak_permission_set_not_allowed (FlatpakPermission *permission) { permission->allowed = FALSE; permission->reset = TRUE; g_ptr_array_set_size (permission->conditionals, 0); } static void flatpak_permission_set_allowed (FlatpakPermission *permission) { permission->allowed = TRUE; /* We reset even when allowed, because lower layer conditionals being added * at merge would make this non-conditional layer conditional. */ permission->reset = TRUE; g_ptr_array_set_size (permission->conditionals, 0); } static void flatpak_permission_set_allowed_if (FlatpakPermission *permission, const char *condition) { /* If we are already unconditionally allowed, don't add useless conditionals */ if (permission->allowed) return; /* Check if its already there */ if (g_ptr_array_find_with_equal_func (permission->conditionals, condition, g_str_equal, NULL)) return; g_ptr_array_add (permission->conditionals, g_strdup (condition)); g_ptr_array_sort (permission->conditionals, flatpak_strcmp0_ptr); } static void flatpak_permission_serialize (FlatpakPermission *permission, const char *name, GPtrArray *res, gboolean flatten) { if (permission->allowed) { /* Completely allowed */ g_ptr_array_add (res, g_strdup (name)); g_assert (permission->conditionals->len == 0); /* A non-conditional add always implies reset, so no need to serialize that */ } else if (permission->conditionals->len > 0) { /* Partially allowed */ if (permission->reset && !flatten) g_ptr_array_add (res, g_strdup_printf ("!%s", name)); /* As backwards compat for pre-conditional flatpaks we unconditionally * add this first. New versions will ignore this if there are * any conditionals. * Note: This may result in both "!foo" and "foo", but that * is fine as the "foo" is last and wins for older flatpaks. */ g_ptr_array_add (res, g_strdup (name)); for (size_t i = 0; i < permission->conditionals->len; i++) { const char *conditional = permission->conditionals->pdata[i]; g_ptr_array_add (res, g_strdup_printf ("if:%s:%s", name, conditional)); } } else { /* Completely disallowed */ if (!flatten) g_ptr_array_add (res, g_strdup_printf ("!%s", name)); } } static void flatpak_permission_to_args (FlatpakPermission *permission, const char *argname, const char *noargname, const char *name, GPtrArray *args) { if (permission->allowed) { /* Completely allowed */ g_ptr_array_add (args, g_strdup_printf ("--%s=%s", argname, name)); } else if (permission->conditionals->len > 0) { /* Partially allowed */ if (permission->reset) g_ptr_array_add (args, g_strdup_printf ("--%s=%s", noargname, name)); for (size_t i = 0; i < permission->conditionals->len; i++) { const char *conditional = permission->conditionals->pdata[i]; g_ptr_array_add (args, g_strdup_printf ("--%s-if=%s:%s", argname, name, conditional)); } } else { /* Completely disallowed */ g_ptr_array_add (args, g_strdup_printf ("--%s=%s", noargname, name)); } } static void flatpak_permission_deserialize (FlatpakPermission *permission, gboolean negated, const char *maybe_condition) { /* This can't use the flatpak_permission_set_ helpers, because we * have to be wary of the backward compat non-conditional permission * in case conditionals are used. */ if (maybe_condition == NULL) { /* Non-conditional option, these are always before conditionals, * but if non-negated could be backwards compat for later conditional. */ if (negated) { permission->allowed = FALSE; permission->reset = TRUE; } else { GPtrArray *tmp; /* Allow us to revert this if it is a backwards compat */ permission->disallow_if_conditional = TRUE; permission->disallow_if_conditional_original_reset = permission->reset; tmp = permission->conditionals; permission->conditionals = permission->disallow_if_conditional_original_conditionals; permission->disallow_if_conditional_original_conditionals = tmp; permission->allowed = TRUE; permission->reset = TRUE; } } else { /* Conditional option */ if (permission->disallow_if_conditional) { GPtrArray *tmp; /* Previous allow was a backward compat, revert it */ permission->allowed = FALSE; permission->reset = permission->disallow_if_conditional_original_reset; permission->disallow_if_conditional = FALSE; tmp = permission->disallow_if_conditional_original_conditionals; permission->disallow_if_conditional_original_conditionals = permission->conditionals; permission->conditionals = tmp; g_ptr_array_set_size ( permission->disallow_if_conditional_original_conditionals, 0); } g_ptr_array_add (permission->conditionals, g_strdup (maybe_condition)); g_ptr_array_sort (permission->conditionals, flatpak_strcmp0_ptr); } } static void flatpak_permission_merge (FlatpakPermission *permission, FlatpakPermission *other_permission) { if (other_permission->reset) { permission->reset = TRUE; g_ptr_array_set_size (permission->conditionals, 0); } permission->allowed = other_permission->allowed; for (size_t i = 0; i < other_permission->conditionals->len; i++) { const char *conditional = other_permission->conditionals->pdata[i]; /* Check if its already there */ if (g_ptr_array_find_with_equal_func (permission->conditionals, conditional, g_str_equal, NULL)) continue; g_ptr_array_add (permission->conditionals, g_strdup (conditional)); } g_ptr_array_sort (permission->conditionals, flatpak_strcmp0_ptr); /* Internal consistency check */ if (permission->allowed) g_assert (permission->conditionals->len == 0); } static gboolean flatpak_permission_compute_allowed (FlatpakPermission *permission, FlatpakContextConditionEvaluator evaluator) { if (permission->allowed) return TRUE; for (size_t i = 0; i < permission->conditionals->len; i++) { const char *conditional = permission->conditionals->pdata[i]; gboolean negated; const char *condition_str; guint32 condition; condition_str = parse_negated (conditional, &negated); condition = flatpak_context_bitmask_from_string (condition_str, flatpak_context_conditions); /* If condition is 0 it means this version of flatpak doesn't know * about the condition and it cannot be satisfied. */ if (condition == 0) continue; /* Conditions which are always true in this version of flatpak */ if (condition & flatpak_context_true_conditions) { if (!negated) return TRUE; continue; } /* Conditions which need runtime evaluation */ if (evaluator && evaluator (condition) == !negated) return TRUE; } /* No condition evaluated to TRUE, so disable the thing */ return FALSE; } static gboolean flatpak_permission_adds_permissions (FlatpakPermission *old, FlatpakPermission *new) { size_t i = 0, j = 0; /* Already unconditionally allowed, can't add more */ if (old->allowed) return FALSE; /* Upgrading from conditional to unconditional */ if (new->allowed) return TRUE; /* More conditionals means more ways to grant the permission (OR) */ if (new->conditionals->len > old->conditionals->len) return TRUE; while (i < old->conditionals->len && j < new->conditionals->len) { const char *old_cond = old->conditionals->pdata[i]; const char *new_cond = new->conditionals->pdata[j]; int res; res = strcmp (old_cond, new_cond); if (res == 0) /* Same conditional */ { i++; j++; } else if (res < 0) /* Old conditional was removed */ { i++; } else /* New conditional not in old — adds permissions */ { return TRUE; } } /* New conditionals remain that weren't matched in old */ return j < new->conditionals->len; } static GHashTable * flatpak_permissions_new (void) { return g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) flatpak_permission_free); } static GHashTable * flatpak_permissions_dup (GHashTable *old) { GHashTable *new; const char *name; FlatpakPermission *old_permission; GHashTableIter iter; new = flatpak_permissions_new (); g_hash_table_iter_init (&iter, old); while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer *) &old_permission)) { g_hash_table_insert (new, g_strdup (name), flatpak_permission_dup (old_permission)); } return new; } static FlatpakPermission * flatpak_permissions_ensure (GHashTable *permissions, const char *name) { FlatpakPermission *permission = g_hash_table_lookup (permissions, name); if (permission == NULL) { permission = flatpak_permission_new (); g_hash_table_insert (permissions, g_strdup (name), permission); } return permission; } static void flatpak_permissions_set_not_allowed (GHashTable *permissions, const char *name) { flatpak_permission_set_not_allowed (flatpak_permissions_ensure (permissions, name)); } static void flatpak_permissions_set_allowed (GHashTable *permissions, const char *name) { flatpak_permission_set_allowed (flatpak_permissions_ensure (permissions, name)); } static void flatpak_permissions_set_allowed_if (GHashTable *permissions, const char *name, const char *condition) { flatpak_permission_set_allowed_if (flatpak_permissions_ensure (permissions, name), condition); } static gboolean flatpak_permissions_allows_unconditionally (GHashTable *permissions, const char *name) { FlatpakPermission *permission = g_hash_table_lookup (permissions, name); if (permission) return permission->allowed; return FALSE; } static void flatpak_permissions_to_args (GHashTable *permissions, const char *argname, const char *noargname, GPtrArray *args) { g_autoptr(GList) ordered_keys = NULL; ordered_keys = g_hash_table_get_keys (permissions); ordered_keys = g_list_sort (ordered_keys, (GCompareFunc) strcmp); for (GList *l = ordered_keys; l != NULL; l = l->next) { const char *name = l->data; FlatpakPermission *permission = g_hash_table_lookup (permissions, name); flatpak_permission_to_args (permission, argname, noargname, name, args); } } static char ** flatpak_permissions_to_strv (GHashTable *permissions, gboolean flatten) { g_autoptr(GList) ordered_keys = NULL; g_autoptr(GPtrArray) res = g_ptr_array_new (); ordered_keys = g_hash_table_get_keys (permissions); ordered_keys = g_list_sort (ordered_keys, (GCompareFunc) strcmp); for (GList *l = ordered_keys; l != NULL; l = l->next) { const char *name = l->data; FlatpakPermission *permission = g_hash_table_lookup (permissions, name); flatpak_permission_serialize (permission, name, res, flatten); } g_ptr_array_add (res, NULL); return (char **)g_ptr_array_free (g_steal_pointer (&res), FALSE); } static guint32 flatpak_permissions_compute_allowed (GHashTable *permissions, const char **names, FlatpakContextConditionEvaluator evaluator) { guint32 bitmask = 0; for (size_t i = 0; names[i] != NULL; i++) { const char *name = names[i]; FlatpakPermission *permission = g_hash_table_lookup (permissions, name); if (permission && flatpak_permission_compute_allowed (permission, evaluator)) bitmask |= 1 << i; } return bitmask; } static gboolean flatpak_permissions_from_strv (GHashTable *permissions, const char **strv, GError **error) { for (size_t i = 0; strv[i] != NULL; i++) { g_auto(GStrv) tokens = g_strsplit (strv[i], ":", 3); const char *name = NULL; gboolean negated = FALSE; const char *condition = NULL; FlatpakPermission *permission; if (strcmp (tokens[0], "if") == 0) { if (g_strv_length (tokens) != 3) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Invalid permission syntax: %s"), strv[i]); return FALSE; } name = tokens[1]; condition = tokens[2]; } else { if (g_strv_length (tokens) != 1) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Invalid permission syntax: %s"), strv[i]); return FALSE; } name = parse_negated (tokens[0], &negated); } permission = flatpak_permissions_ensure (permissions, name); flatpak_permission_deserialize (permission, negated, condition); } return TRUE; } static void flatpak_permissions_merge (GHashTable *permissions, GHashTable *other) { const char *name; FlatpakPermission *other_permission; GHashTableIter iter; FlatpakPermission *x11; /* If we reset the x11 conditionals, the fallback-x11 permission also must go * because it is a conditional. */ x11 = g_hash_table_lookup (other, "x11"); if (x11 && x11->reset) g_hash_table_remove (permissions, "fallback-x11"); g_hash_table_iter_init (&iter, other); while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer *) &other_permission)) { FlatpakPermission *permission = g_hash_table_lookup (permissions, name); if (permission) { flatpak_permission_merge (permission, other_permission); } else { g_hash_table_insert (permissions, g_strdup (name), flatpak_permission_dup (other_permission)); } } } static gboolean flatpak_permissions_adds_permissions (GHashTable *old, GHashTable *new) { const char *name; FlatpakPermission *new_permission; GHashTableIter iter; g_hash_table_iter_init (&iter, new); while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer *) &new_permission)) { FlatpakPermission *old_permission = g_hash_table_lookup (old, name); if (old_permission) { if (flatpak_permission_adds_permissions (old_permission, new_permission)) return TRUE; } else { if (new_permission->allowed || new_permission->conditionals->len > 0) return TRUE; /* new is completely new permission */ } } return FALSE; } #ifdef INCLUDE_INTERNAL_TESTS static void flatpak_permissions_test_basic (void) { /* This is in canonical form, so must be kept sorted by name */ const char *perms_strv[] = { /* Regular unconditional allowed (resets) */ "allowed", /* conditional allowed with two conditions (doesn't reset) */ "cond1", /* backwards compat */ "if:cond1:check1", "if:cond1:check2", /* conditional allowed with one conditions (doesn't reset) */ "cond2", /* backwards compat */ "if:cond2:check3", /* conditional allowed (resets) */ "!cond3", /* reset */ "cond3", /* backwards compat */ "if:cond3:check3", /* Regular unconditional disallowed (resets) */ "!disallowed", NULL, }; const char *perms_args[] = { "--socket=allowed", "--socket-if=cond1:check1", "--socket-if=cond1:check2", "--socket-if=cond2:check3", /* conditional allowed (resets) */ "--nosocket=cond3", "--socket-if=cond3:check3", /* Regular unconditional disallowed (resets) */ "--nosocket=disallowed", NULL, }; GError *error = NULL; /* Test parsing */ g_autoptr(GHashTable) perms = flatpak_permissions_new (); gboolean ok = flatpak_permissions_from_strv (perms, perms_strv, &error); g_assert_true(ok); g_assert_no_error(error); g_assert_nonnull(perms); g_assert_cmpint(g_hash_table_size (perms), ==, 5); FlatpakPermission *allowed = g_hash_table_lookup (perms, "allowed"); g_assert_nonnull(allowed); g_assert_true(allowed->allowed); g_assert_true(allowed->reset); g_assert(allowed->conditionals->len == 0); FlatpakPermission *disallowed = g_hash_table_lookup (perms, "disallowed"); g_assert_nonnull(disallowed); g_assert_false(disallowed->allowed); g_assert_true(disallowed->reset); g_assert(disallowed->conditionals->len == 0); FlatpakPermission *cond1 = g_hash_table_lookup (perms, "cond1"); g_assert_nonnull(cond1); g_assert_false(cond1->allowed); g_assert_false(cond1->reset); g_assert(cond1->conditionals->len == 2); g_assert_cmpstr(cond1->conditionals->pdata[0], ==, "check1"); g_assert_cmpstr(cond1->conditionals->pdata[1], ==, "check2"); FlatpakPermission *cond2 = g_hash_table_lookup (perms, "cond2"); g_assert_nonnull(cond2); g_assert_false(cond2->allowed); g_assert_false(cond2->reset); g_assert(cond2->conditionals->len == 1); g_assert_cmpstr(cond2->conditionals->pdata[0], ==, "check3"); FlatpakPermission *cond3 = g_hash_table_lookup (perms, "cond3"); g_assert_nonnull(cond3); g_assert_false(cond3->allowed); g_assert_true(cond3->reset); g_assert(cond3->conditionals->len == 1); g_assert_cmpstr(cond3->conditionals->pdata[0], ==, "check3"); /* Test roundtrip */ g_auto(GStrv) new_strv = flatpak_permissions_to_strv (perms, FALSE); g_assert_cmpstrv (perms_strv, new_strv); g_autoptr(GPtrArray) args = g_ptr_array_new_with_free_func (g_free); flatpak_permissions_to_args (perms, "socket", "nosocket", args); g_ptr_array_add(args, NULL); g_assert_cmpstrv (perms_args, args->pdata); /* Test copy */ g_autoptr(FlatpakPermission) cond1_copy = flatpak_permission_dup(cond1); g_assert_nonnull(cond1_copy); g_assert_false(cond1_copy->allowed); g_assert_false(cond1_copy->reset); g_assert(cond1_copy->conditionals->len == 2); g_assert_cmpstr(cond1_copy->conditionals->pdata[0], ==, "check1"); g_assert_cmpstr(cond1_copy->conditionals->pdata[1], ==, "check2"); /* Test setters: */ { g_autoptr(FlatpakPermission) copy = flatpak_permission_dup(cond1); flatpak_permission_set_allowed (copy); g_assert_true (copy->allowed); g_assert_true (copy->reset); g_assert(copy->conditionals->len == 0); } { g_autoptr(FlatpakPermission) copy = flatpak_permission_dup(cond1); flatpak_permission_set_not_allowed (copy); g_assert_false (copy->allowed); g_assert_true (copy->reset); g_assert(copy->conditionals->len == 0); } { g_autoptr(FlatpakPermission) copy = flatpak_permission_dup(cond1); flatpak_permission_set_allowed_if (copy, "check0"); g_assert_false (copy->allowed); g_assert_false (copy->reset); g_assert(copy->conditionals->len == 3); g_assert_cmpstr(copy->conditionals->pdata[0], ==, "check0"); g_assert_cmpstr(copy->conditionals->pdata[1], ==, "check1"); g_assert_cmpstr(copy->conditionals->pdata[2], ==, "check2"); } /* Test merge */ { g_autoptr(FlatpakPermission) copy = flatpak_permission_dup(cond1); flatpak_permission_merge (copy, allowed); g_assert_true (copy->allowed); g_assert_true (copy->reset); g_assert(copy->conditionals->len == 0); } { g_autoptr(FlatpakPermission) copy = flatpak_permission_dup(cond1); flatpak_permission_merge (copy, disallowed); g_assert_false (copy->allowed); g_assert_true (copy->reset); g_assert(copy->conditionals->len == 0); } { /* Merge from non-reset conditional */ g_autoptr(FlatpakPermission) copy = flatpak_permission_dup(cond1); flatpak_permission_merge (copy, cond2); g_assert_false (copy->allowed); g_assert_false (copy->reset); g_assert(copy->conditionals->len == 3); } { /* Merge from reset conditional */ g_autoptr(FlatpakPermission) copy = flatpak_permission_dup(cond1); flatpak_permission_merge (copy, cond3); g_assert_false (copy->allowed); g_assert_true (copy->reset); g_assert(copy->conditionals->len == 1); } } static void flatpak_permissions_test_backwards_compat (void) { { /* Deserialize if:wayland:foo;wayland * The last wayland makes it unconditional. */ g_autoptr(FlatpakPermission) perm = flatpak_permission_new (); flatpak_permission_deserialize (perm, FALSE, "foo"); flatpak_permission_deserialize (perm, FALSE, NULL); g_assert_true (perm->allowed); g_assert_true (perm->reset); g_assert_cmpint (perm->conditionals->len, ==, 0); } { /* Deserialize wayland;if:wayland:foo;wayland * Should be the same as the one above. The first wayland is just for * backwards compat. */ g_autoptr(FlatpakPermission) perm = flatpak_permission_new (); flatpak_permission_deserialize (perm, FALSE, NULL); flatpak_permission_deserialize (perm, FALSE, "foo"); flatpak_permission_deserialize (perm, FALSE, NULL); g_assert_true (perm->allowed); g_assert_true (perm->reset); g_assert_cmpint (perm->conditionals->len, ==, 0); } { /* Deserialize if:wayland:foo;wayland;if:wayland:bar * Now the wayland is before a conditional, so it acts as backwards * compat. */ g_autoptr(FlatpakPermission) perm = flatpak_permission_new (); flatpak_permission_deserialize (perm, FALSE, "foo"); flatpak_permission_deserialize (perm, FALSE, NULL); flatpak_permission_deserialize (perm, FALSE, "bar"); g_assert_false (perm->allowed); g_assert_false (perm->reset); g_assert_cmpint (perm->conditionals->len, ==, 2); g_assert_cmpstr (perm->conditionals->pdata[0], ==, "bar"); g_assert_cmpstr (perm->conditionals->pdata[1], ==, "foo"); } } static void flatpak_permissions_test_fallback_x11 (void) { g_autoptr(GHashTable) perms = NULL; g_autoptr(GHashTable) res_perms = NULL; { FlatpakPermission *fallback_x11; FlatpakPermission *wayland; g_autoptr(GError) error = NULL; gboolean ok; perms = flatpak_permissions_new (); ok = flatpak_permissions_from_strv (perms, (const char * []) { "fallback-x11", "wayland", NULL, }, &error); g_assert_true (ok); g_assert_no_error (error); g_assert_nonnull (perms); g_assert_cmpint (g_hash_table_size (perms), ==, 2); fallback_x11 = g_hash_table_lookup (perms, "fallback-x11"); g_assert_nonnull (fallback_x11); g_assert_true (fallback_x11->allowed); wayland = g_hash_table_lookup (perms, "wayland"); g_assert_nonnull (wayland); g_assert_true (wayland->allowed); } /* If we only add a conditional, we don't reset fallback-x11 */ { g_autoptr(GHashTable) perms2 = NULL; FlatpakPermission *x11; FlatpakPermission *fallback_x11; FlatpakPermission *wayland; g_autoptr(GError) error = NULL; gboolean ok; res_perms = flatpak_permissions_dup (perms); perms2 = flatpak_permissions_new (); ok = flatpak_permissions_from_strv (perms2, (const char * []) { "if:x11:foo", NULL, }, &error); g_assert_true (ok); g_assert_no_error (error); g_assert_nonnull (perms2); g_assert_cmpint (g_hash_table_size (perms2), ==, 1); x11 = g_hash_table_lookup (perms2, "x11"); g_assert_nonnull (x11); g_assert_false (x11->allowed); g_assert_false (x11->reset); g_assert_cmpint (x11->conditionals->len, ==, 1); g_assert_cmpstr (x11->conditionals->pdata[0], ==, "foo"); flatpak_permissions_merge (res_perms, perms2); g_assert_cmpint (g_hash_table_size (res_perms), ==, 3); x11 = g_hash_table_lookup (res_perms, "x11"); g_assert_nonnull (x11); g_assert_false (x11->allowed); g_assert_false (x11->reset); g_assert_cmpint (x11->conditionals->len, ==, 1); g_assert_cmpstr (x11->conditionals->pdata[0], ==, "foo"); fallback_x11 = g_hash_table_lookup (res_perms, "fallback-x11"); g_assert_nonnull (fallback_x11); g_assert_true (fallback_x11->allowed); wayland = g_hash_table_lookup (res_perms, "wayland"); g_assert_nonnull (wayland); g_assert_true (wayland->allowed); } /* If we set x11, we reset conditionals *and* fallback-x11 */ { g_autoptr(GHashTable) perms2 = NULL; FlatpakPermission *x11; FlatpakPermission *wayland; g_autoptr(GError) error = NULL; gboolean ok; res_perms = flatpak_permissions_dup (perms); perms2 = flatpak_permissions_new (); ok = flatpak_permissions_from_strv (perms2, (const char * []) { "x11", NULL, }, &error); g_assert_true (ok); g_assert_no_error (error); g_assert_nonnull (perms2); g_assert_cmpint (g_hash_table_size (perms2), ==, 1); x11 = g_hash_table_lookup (perms2, "x11"); g_assert_nonnull (x11); g_assert_true (x11->allowed); g_assert_true (x11->reset); flatpak_permissions_merge (res_perms, perms2); g_assert_cmpint (g_hash_table_size (res_perms), ==, 2); x11 = g_hash_table_lookup (res_perms, "x11"); g_assert_nonnull (x11); g_assert_true (x11->allowed); wayland = g_hash_table_lookup (res_perms, "wayland"); g_assert_nonnull (wayland); g_assert_true (wayland->allowed); } /* Only add a conditional and nosocket fallback-x11, gives nosocket fallback-x11 */ { g_autoptr(GHashTable) perms2 = NULL; FlatpakPermission *x11; FlatpakPermission *fallback_x11; FlatpakPermission *wayland; g_autoptr(GError) error = NULL; gboolean ok; res_perms = flatpak_permissions_dup (perms); perms2 = flatpak_permissions_new (); ok = flatpak_permissions_from_strv (perms2, (const char * []) { "if:x11:foo", "!fallback-x11", NULL, }, &error); g_assert_true (ok); g_assert_no_error (error); g_assert_nonnull (perms2); g_assert_cmpint (g_hash_table_size (perms2), ==, 2); x11 = g_hash_table_lookup (perms2, "x11"); g_assert_nonnull (x11); g_assert_false (x11->allowed); g_assert_false (x11->reset); g_assert_cmpint (x11->conditionals->len, ==, 1); g_assert_cmpstr (x11->conditionals->pdata[0], ==, "foo"); fallback_x11 = g_hash_table_lookup (perms2, "fallback-x11"); g_assert_nonnull (fallback_x11); g_assert_false (fallback_x11->allowed); flatpak_permissions_merge (res_perms, perms2); g_assert_cmpint (g_hash_table_size (res_perms), ==, 3); x11 = g_hash_table_lookup (res_perms, "x11"); g_assert_nonnull (x11); g_assert_false (x11->allowed); g_assert_false (x11->reset); g_assert_cmpint (x11->conditionals->len, ==, 1); g_assert_cmpstr (x11->conditionals->pdata[0], ==, "foo"); fallback_x11 = g_hash_table_lookup (res_perms, "fallback-x11"); g_assert_nonnull (fallback_x11); g_assert_false (fallback_x11->allowed); wayland = g_hash_table_lookup (res_perms, "wayland"); g_assert_nonnull (wayland); g_assert_true (wayland->allowed); } } static void flatpak_permissions_test_negated_args (void) { static const struct { const char *perm; const char *argname; const char *noargname; const char *expected_arg; } cases[] = { { "!network", "share", "unshare", "--unshare=network" }, { "!x11", "socket", "nosocket", "--nosocket=x11" }, { "!dri", "device", "nodevice", "--nodevice=dri" }, { "!bluetooth", "allow", "disallow", "--disallow=bluetooth" }, }; for (size_t i = 0; i < G_N_ELEMENTS (cases); i++) { g_autoptr(GError) error = NULL; g_autoptr(GHashTable) perms = flatpak_permissions_new (); g_autoptr(GPtrArray) args = g_ptr_array_new_with_free_func (g_free); gboolean ok; const char *perms_strv[] = { cases[i].perm, NULL }; ok = flatpak_permissions_from_strv (perms, perms_strv, &error); g_assert_true (ok); g_assert_no_error (error); flatpak_permissions_to_args (perms, cases[i].argname, cases[i].noargname, args); g_ptr_array_add (args, NULL); g_assert_cmpuint (args->len - 1, ==, 1); g_assert_cmpstr (args->pdata[0], ==, cases[i].expected_arg); } } FLATPAK_INTERNAL_TEST("/context/permissions/basic", flatpak_permissions_test_basic); FLATPAK_INTERNAL_TEST("/context/permissions/backwards-compat", flatpak_permissions_test_backwards_compat); FLATPAK_INTERNAL_TEST("/context/permissions/fallback-x11", flatpak_permissions_test_fallback_x11); FLATPAK_INTERNAL_TEST("/context/permissions/negated-args", flatpak_permissions_test_negated_args); #endif /* INCLUDE_INTERNAL_TESTS */ FlatpakContext * flatpak_context_new (void) { FlatpakContext *context; context = g_slice_new0 (FlatpakContext); context->env_vars = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); context->persistent = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); /* filename or special filesystem name => FlatpakFilesystemMode */ context->filesystems = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); context->session_bus_policy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); context->system_bus_policy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); context->a11y_bus_policy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); context->generic_policy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_strfreev); context->enumerable_usb_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) flatpak_usb_query_free); context->hidden_usb_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) flatpak_usb_query_free); context->shares_permissions = flatpak_permissions_new (); context->socket_permissions = flatpak_permissions_new (); context->device_permissions = flatpak_permissions_new (); context->features_permissions = flatpak_permissions_new (); return context; } void flatpak_context_free (FlatpakContext *context) { g_hash_table_destroy (context->env_vars); g_hash_table_destroy (context->persistent); g_hash_table_destroy (context->filesystems); g_hash_table_destroy (context->session_bus_policy); g_hash_table_destroy (context->system_bus_policy); g_hash_table_destroy (context->a11y_bus_policy); g_hash_table_destroy (context->generic_policy); g_hash_table_destroy (context->enumerable_usb_devices); g_hash_table_destroy (context->hidden_usb_devices); g_hash_table_destroy (context->shares_permissions); g_hash_table_destroy (context->device_permissions); g_hash_table_destroy (context->socket_permissions); g_hash_table_destroy (context->features_permissions); g_slice_free (FlatpakContext, context); } static guint32 flatpak_context_bitmask_from_string (const char *name, const char **names) { guint32 i; for (i = 0; names[i] != NULL; i++) { if (strcmp (names[i], name) == 0) return 1 << i; } return 0; } static FlatpakContextShares flatpak_context_share_from_string (const char *string, GError **error) { FlatpakContextShares shares = flatpak_context_bitmask_from_string (string, flatpak_context_shares); if (shares == 0) { g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_shares); g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown share type %s, valid types are: %s"), string, values); } return shares; } static FlatpakPolicy flatpak_policy_from_string (const char *string, GError **error) { const char *policies[] = { "none", "see", "talk", "own", NULL }; int i; g_autofree char *values = NULL; for (i = 0; policies[i]; i++) { if (strcmp (string, policies[i]) == 0) return i; } values = g_strjoinv (", ", (char **) policies); g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown policy type %s, valid types are: %s"), string, values); return -1; } static const char * flatpak_policy_to_string (FlatpakPolicy policy) { if (policy == FLATPAK_POLICY_SEE) return "see"; if (policy == FLATPAK_POLICY_TALK) return "talk"; if (policy == FLATPAK_POLICY_OWN) return "own"; return "none"; } /* Returns the command-line option that gives a name the given @policy on * the session bus. The system bus equivalent is the same option with a * "system-" prefix. There is no option for FLATPAK_POLICY_SEE, which like * FLATPAK_POLICY_NONE does not let the app talk to the name. */ static const char * flatpak_policy_to_name_option (FlatpakPolicy policy) { switch (policy) { case FLATPAK_POLICY_TALK: return "talk-name"; case FLATPAK_POLICY_OWN: return "own-name"; case FLATPAK_POLICY_NONE: case FLATPAK_POLICY_SEE: return "no-talk-name"; default: g_return_val_if_reached ("no-talk-name"); } } static gboolean flatpak_verify_dbus_name (const char *name, GError **error) { const char *name_part; g_autofree char *tmp = NULL; if (g_str_has_suffix (name, ".*")) { tmp = g_strndup (name, strlen (name) - 2); name_part = tmp; } else { name_part = name; } if (g_dbus_is_name (name_part) && !g_dbus_is_unique_name (name_part)) return TRUE; g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Invalid dbus name %s"), name); return FALSE; } static FlatpakContextSockets flatpak_context_socket_from_string (const char *string, GError **error) { FlatpakContextSockets sockets = flatpak_context_bitmask_from_string (string, flatpak_context_sockets); if (sockets == 0) { g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_sockets); g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown socket type %s, valid types are: %s"), string, values); } return sockets; } static FlatpakContextDevices flatpak_context_device_from_string (const char *string, GError **error) { FlatpakContextDevices devices = flatpak_context_bitmask_from_string (string, flatpak_context_devices); if (devices == 0) { g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_devices); g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown device type %s, valid types are: %s"), string, values); } return devices; } static FlatpakContextFeatures flatpak_context_feature_from_string (const char *string, GError **error) { FlatpakContextFeatures feature = flatpak_context_bitmask_from_string (string, flatpak_context_features); if (feature == 0) { g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_features); g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown feature type %s, valid types are: %s"), string, values); } return feature; } static void flatpak_context_set_env_var (FlatpakContext *context, const char *name, const char *value) { g_hash_table_insert (context->env_vars, g_strdup (name), g_strdup (value)); } void flatpak_context_set_session_bus_policy (FlatpakContext *context, const char *name, FlatpakPolicy policy) { g_hash_table_insert (context->session_bus_policy, g_strdup (name), GINT_TO_POINTER (policy)); } void flatpak_context_set_a11y_bus_policy (FlatpakContext *context, const char *name, FlatpakPolicy policy) { g_hash_table_insert (context->a11y_bus_policy, g_strdup (name), GINT_TO_POINTER (policy)); } GStrv flatpak_context_get_session_bus_policy_allowed_own_names (FlatpakContext *context) { GHashTableIter iter; gpointer key, value; g_autoptr(GPtrArray) names = g_ptr_array_new_with_free_func (g_free); g_hash_table_iter_init (&iter, context->session_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) if (GPOINTER_TO_INT (value) == FLATPAK_POLICY_OWN) g_ptr_array_add (names, g_strdup (key)); g_ptr_array_add (names, NULL); return (GStrv) g_ptr_array_free (g_steal_pointer (&names), FALSE); } void flatpak_context_set_system_bus_policy (FlatpakContext *context, const char *name, FlatpakPolicy policy) { g_hash_table_insert (context->system_bus_policy, g_strdup (name), GINT_TO_POINTER (policy)); } static void flatpak_context_apply_generic_policy (FlatpakContext *context, const char *key, const char *value) { GPtrArray *new = g_ptr_array_new (); const char **old_v; int i; g_assert (strchr (key, '.') != NULL); old_v = g_hash_table_lookup (context->generic_policy, key); for (i = 0; old_v != NULL && old_v[i] != NULL; i++) { const char *old = old_v[i]; const char *cmp1 = old; const char *cmp2 = value; if (*cmp1 == '!') cmp1++; if (*cmp2 == '!') cmp2++; if (strcmp (cmp1, cmp2) != 0) g_ptr_array_add (new, g_strdup (old)); } g_ptr_array_add (new, g_strdup (value)); g_ptr_array_add (new, NULL); g_hash_table_insert (context->generic_policy, g_strdup (key), g_ptr_array_free (new, FALSE)); } static void flatpak_context_add_query_to (GHashTable *queries, const FlatpakUsbQuery *usb_query) { g_autoptr(FlatpakUsbQuery) copy = NULL; g_autoptr(GString) string = NULL; g_assert (queries != NULL); g_assert (usb_query != NULL && usb_query->rules != NULL); copy = flatpak_usb_query_copy (usb_query); string = g_string_new (NULL); flatpak_usb_query_print (usb_query, string); g_hash_table_insert (queries, g_strdup (string->str), g_steal_pointer (©)); } static void flatpak_context_add_usb_query (FlatpakContext *context, const FlatpakUsbQuery *usb_query) { flatpak_context_add_query_to (context->enumerable_usb_devices, usb_query); } static void flatpak_context_add_nousb_query (FlatpakContext *context, const FlatpakUsbQuery *usb_query) { flatpak_context_add_query_to (context->hidden_usb_devices, usb_query); } static gboolean flatpak_context_add_usb_list (FlatpakContext *context, const char *list, GError **error) { return flatpak_usb_parse_usb_list (list, context->enumerable_usb_devices, context->hidden_usb_devices, error); } static gboolean flatpak_context_add_usb_list_from_file (FlatpakContext *context, const char *path, GError **error) { g_autofree char *contents = NULL; if (!flatpak_validate_path_characters (path, error)) return FALSE; if (!g_file_get_contents (path, &contents, NULL, error)) return FALSE; return flatpak_usb_parse_usb_list (contents, context->enumerable_usb_devices, context->hidden_usb_devices, error); } static gboolean flatpak_context_set_persistent (FlatpakContext *context, const char *path, GError **error) { if (!flatpak_validate_path_characters (path, error)) return FALSE; g_hash_table_insert (context->persistent, g_strdup (path), GINT_TO_POINTER (1)); return TRUE; } static gboolean get_xdg_dir_from_prefix (const char *prefix, const char **where, const char **dir) { if (strcmp (prefix, "xdg-data") == 0) { if (where) *where = "data"; if (dir) *dir = g_get_user_data_dir (); return TRUE; } if (strcmp (prefix, "xdg-cache") == 0) { if (where) *where = "cache"; if (dir) *dir = g_get_user_cache_dir (); return TRUE; } if (strcmp (prefix, "xdg-config") == 0) { if (where) *where = "config"; if (dir) *dir = g_get_user_config_dir (); return TRUE; } return FALSE; } /* This looks only in the xdg dirs (config, cache, data), not the user definable ones */ static char * get_xdg_dir_from_string (const char *filesystem, const char **suffix, const char **where) { const char *slash; const char *rest; g_autofree char *prefix = NULL; const char *dir = NULL; gsize len; slash = strchr (filesystem, '/'); if (slash) len = slash - filesystem; else len = strlen (filesystem); rest = filesystem + len; while (*rest == '/') rest++; if (suffix != NULL) *suffix = rest; prefix = g_strndup (filesystem, len); if (get_xdg_dir_from_prefix (prefix, where, &dir)) return g_build_filename (dir, rest, NULL); return NULL; } static gboolean get_xdg_user_dir_from_string (const char *filesystem, const char **config_key, const char **suffix, char **dir) { const char *slash; const char *rest; g_autofree char *prefix = NULL; gsize len; const char *dir_out = NULL; slash = strchr (filesystem, '/'); if (slash) len = slash - filesystem; else len = strlen (filesystem); rest = filesystem + len; while (*rest == '/') rest++; if (suffix) *suffix = rest; prefix = g_strndup (filesystem, len); if (strcmp (prefix, "xdg-desktop") == 0) { if (config_key) *config_key = "XDG_DESKTOP_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_DESKTOP)); return TRUE; } if (strcmp (prefix, "xdg-documents") == 0) { if (config_key) *config_key = "XDG_DOCUMENTS_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS)); return TRUE; } if (strcmp (prefix, "xdg-download") == 0) { if (config_key) *config_key = "XDG_DOWNLOAD_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_DOWNLOAD)); return TRUE; } if (strcmp (prefix, "xdg-music") == 0) { if (config_key) *config_key = "XDG_MUSIC_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_MUSIC)); return TRUE; } if (strcmp (prefix, "xdg-pictures") == 0) { if (config_key) *config_key = "XDG_PICTURES_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_PICTURES)); return TRUE; } if (strcmp (prefix, "xdg-public-share") == 0) { if (config_key) *config_key = "XDG_PUBLICSHARE_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_PUBLIC_SHARE)); return TRUE; } if (strcmp (prefix, "xdg-templates") == 0) { if (config_key) *config_key = "XDG_TEMPLATES_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_TEMPLATES)); return TRUE; } if (strcmp (prefix, "xdg-videos") == 0) { if (config_key) *config_key = "XDG_VIDEOS_DIR"; if (dir) *dir = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_VIDEOS)); return TRUE; } if (get_xdg_dir_from_prefix (prefix, NULL, &dir_out)) { if (config_key) *config_key = NULL; if (dir) *dir = g_strdup (dir_out); return TRUE; } /* Don't support xdg-run without suffix, because that doesn't work */ if (strcmp (prefix, "xdg-run") == 0 && *rest != 0) { if (config_key) *config_key = NULL; if (dir) *dir = flatpak_get_real_xdg_runtime_dir (); return TRUE; } return FALSE; } static char * unparse_filesystem_flags (const char *path, FlatpakFilesystemMode mode) { g_autoptr(GString) s = g_string_new (""); const char *p; for (p = path; *p != 0; p++) { if (*p == ':') g_string_append (s, "\\:"); else if (*p == '\\') g_string_append (s, "\\\\"); else g_string_append_c (s, *p); } switch (mode) { case FLATPAK_FILESYSTEM_MODE_READ_ONLY: g_string_append (s, ":ro"); break; case FLATPAK_FILESYSTEM_MODE_CREATE: g_string_append (s, ":create"); break; case FLATPAK_FILESYSTEM_MODE_READ_WRITE: break; case FLATPAK_FILESYSTEM_MODE_NONE: g_string_insert_c (s, 0, '!'); if (g_str_has_suffix (s->str, "-reset")) { g_string_truncate (s, s->len - 6); g_string_append (s, ":reset"); } break; default: g_warning ("Unexpected filesystem mode %d", mode); break; } return g_string_free (g_steal_pointer (&s), FALSE); } static char * parse_filesystem_flags (const char *filesystem, gboolean negated, FlatpakFilesystemMode *mode_out, GError **error) { g_autoptr(GString) s = g_string_new (""); const char *p, *suffix; FlatpakFilesystemMode mode; gboolean reset = FALSE; p = filesystem; while (*p != 0 && *p != ':') { if (*p == '\\') { p++; if (*p != 0) g_string_append_c (s, *p++); } else g_string_append_c (s, *p++); } if (negated) mode = FLATPAK_FILESYSTEM_MODE_NONE; else mode = FLATPAK_FILESYSTEM_MODE_READ_WRITE; if (g_str_equal (s->str, "host-reset")) { reset = TRUE; if (!negated) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "Filesystem token \"%s\" is only applicable for --nofilesystem", s->str); return NULL; } if (*p != '\0') { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "Filesystem token \"%s\" cannot be used with a suffix", s->str); return NULL; } } if (*p == ':') { suffix = p + 1; if (strcmp (suffix, "ro") == 0) mode = FLATPAK_FILESYSTEM_MODE_READ_ONLY; else if (strcmp (suffix, "rw") == 0) mode = FLATPAK_FILESYSTEM_MODE_READ_WRITE; else if (strcmp (suffix, "create") == 0) mode = FLATPAK_FILESYSTEM_MODE_CREATE; else if (strcmp (suffix, "reset") == 0) reset = TRUE; else if (*suffix != 0) g_warning ("Unexpected filesystem suffix %s, ignoring", suffix); if (negated && mode != FLATPAK_FILESYSTEM_MODE_NONE) { g_warning ("Filesystem suffix \"%s\" is not applicable for --nofilesystem", suffix); mode = FLATPAK_FILESYSTEM_MODE_NONE; } if (reset) { if (!negated) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "Filesystem suffix \"%s\" only applies to --nofilesystem", suffix); return NULL; } if (!g_str_equal (s->str, "host")) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "Filesystem suffix \"%s\" can only be applied to " "--nofilesystem=host", suffix); return NULL; } /* We internally handle host:reset (etc) as host-reset, only exposing it as a flag in the public part to allow it to be ignored (with a warning) for old flatpak versions */ g_string_append (s, "-reset"); } } /* Postcondition check: the code above should make some results * impossible */ if (negated) { g_assert (mode == FLATPAK_FILESYSTEM_MODE_NONE); } else { g_assert (mode > FLATPAK_FILESYSTEM_MODE_NONE); /* This flag is only applicable to --nofilesystem */ g_assert (!reset); } /* Postcondition check: filesystem token is host-reset iff reset flag * was found */ if (reset) g_assert (g_str_equal (s->str, "host-reset")); else g_assert (!g_str_equal (s->str, "host-reset")); if (mode_out) *mode_out = mode; return g_string_free (g_steal_pointer (&s), FALSE); } gboolean flatpak_context_parse_filesystem (const char *filesystem_and_mode, gboolean negated, char **filesystem_out, FlatpakFilesystemMode *mode_out, GError **error) { g_autofree char *filesystem = NULL; char *slash; if (!flatpak_validate_path_characters (filesystem_and_mode, error)) return FALSE; filesystem = parse_filesystem_flags (filesystem_and_mode, negated, mode_out, error); if (filesystem == NULL) return FALSE; /* Forbid /../ in paths */ if (g_str_has_suffix (filesystem, "/..") || strstr (filesystem, "/../") != NULL) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("Filesystem location \"%s\" contains \"..\""), filesystem); return FALSE; } slash = strchr (filesystem, '/'); if (slash != NULL) { /* Convert "//" and "/./" to "/" */ for (; slash != NULL; slash = strchr (slash + 1, '/')) { while (TRUE) { if (slash[1] == '/') memmove (slash + 1, slash + 2, strlen (slash + 2) + 1); else if (slash[1] == '.' && slash[2] == '/') memmove (slash + 1, slash + 3, strlen (slash + 3) + 1); else break; } } /* Eliminate trailing "/." or "/". */ while (TRUE) { slash = strrchr (filesystem, '/'); if (slash != NULL && ((slash != filesystem && slash[1] == '\0') || (slash[1] == '.' && slash[2] == '\0'))) *slash = '\0'; else break; } if (filesystem[0] == '/' && filesystem[1] == '\0') { /* We don't allow --filesystem=/ as equivalent to host, because * it doesn't do what you'd think: --filesystem=host mounts some * host directories in /run/host, not in the root. */ g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("--filesystem=/ is not available, " "use --filesystem=host for a similar result")); return FALSE; } } if (g_strv_contains (flatpak_context_special_filesystems, filesystem) || get_xdg_user_dir_from_string (filesystem, NULL, NULL, NULL) || g_str_has_prefix (filesystem, "~/") || g_str_has_prefix (filesystem, "/")) { if (filesystem_out != NULL) *filesystem_out = g_steal_pointer (&filesystem); return TRUE; } if (strcmp (filesystem, "~") == 0) { if (filesystem_out != NULL) *filesystem_out = g_strdup ("home"); return TRUE; } if (g_str_has_prefix (filesystem, "home/")) { if (filesystem_out != NULL) *filesystem_out = g_strconcat ("~/", filesystem + 5, NULL); return TRUE; } g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown filesystem location %s, valid locations are: host, host-os, host-etc, host-root, home, xdg-*[/…], ~/dir, /dir"), filesystem); return FALSE; } static void flatpak_context_take_filesystem (FlatpakContext *context, char *fs, FlatpakFilesystemMode mode) { /* Special case: --nofilesystem=host-reset implies --nofilesystem=host. * --filesystem=host-reset (or host:reset) is not allowed. */ if (g_str_equal (fs, "host-reset")) { g_return_if_fail (mode == FLATPAK_FILESYSTEM_MODE_NONE); g_hash_table_insert (context->filesystems, g_strdup ("host"), GINT_TO_POINTER (mode)); } g_hash_table_insert (context->filesystems, fs, GINT_TO_POINTER (mode)); } void flatpak_context_merge (FlatpakContext *context, FlatpakContext *other) { GHashTableIter iter; gpointer key, value; flatpak_permissions_merge (context->shares_permissions, other->shares_permissions); flatpak_permissions_merge (context->socket_permissions, other->socket_permissions); flatpak_permissions_merge (context->device_permissions, other->device_permissions); flatpak_permissions_merge (context->features_permissions, other->features_permissions); g_hash_table_iter_init (&iter, other->env_vars); while (g_hash_table_iter_next (&iter, &key, &value)) g_hash_table_insert (context->env_vars, g_strdup (key), g_strdup (value)); g_hash_table_iter_init (&iter, other->persistent); while (g_hash_table_iter_next (&iter, &key, &value)) g_hash_table_insert (context->persistent, g_strdup (key), value); /* We first handle host:reset, as it overrides all other keys from the parent */ if (g_hash_table_lookup_extended (other->filesystems, "host-reset", NULL, &value)) { g_warn_if_fail (GPOINTER_TO_INT (value) == FLATPAK_FILESYSTEM_MODE_NONE); g_hash_table_remove_all (context->filesystems); } /* Then set the new ones, which includes propagating host:reset. */ g_hash_table_iter_init (&iter, other->filesystems); while (g_hash_table_iter_next (&iter, &key, &value)) g_hash_table_insert (context->filesystems, g_strdup (key), value); g_hash_table_iter_init (&iter, other->session_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) g_hash_table_insert (context->session_bus_policy, g_strdup (key), value); g_hash_table_iter_init (&iter, other->system_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) g_hash_table_insert (context->system_bus_policy, g_strdup (key), value); g_hash_table_iter_init (&iter, other->a11y_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) g_hash_table_insert (context->a11y_bus_policy, g_strdup (key), value); g_hash_table_iter_init (&iter, other->generic_policy); while (g_hash_table_iter_next (&iter, &key, &value)) { const char **policy_values = (const char **) value; int i; for (i = 0; policy_values[i] != NULL; i++) flatpak_context_apply_generic_policy (context, (char *) key, policy_values[i]); } g_hash_table_iter_init (&iter, other->enumerable_usb_devices); while (g_hash_table_iter_next (&iter, NULL, &value)) flatpak_context_add_usb_query (context, value); g_hash_table_iter_init (&iter, other->hidden_usb_devices); while (g_hash_table_iter_next (&iter, NULL, &value)) flatpak_context_add_nousb_query (context, value); } static gboolean parse_if_option (const char *option_name, const char *value, char **name_out, char **condition_out, GError **error) { g_auto(GStrv) tokens = g_strsplit (value, ":", 2); if (g_strv_length (tokens) != 2) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Invalid syntax for %s: %s"), option_name, value); return FALSE; } *name_out = g_strdup (tokens[0]); *condition_out = g_strdup (tokens[1]); return TRUE; } static gboolean option_share_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextShares share; share = flatpak_context_share_from_string (value, error); if (share == 0) return FALSE; flatpak_permissions_set_allowed (context->shares_permissions, value); return TRUE; } static gboolean option_unshare_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextShares share; share = flatpak_context_share_from_string (value, error); if (share == 0) return FALSE; flatpak_permissions_set_not_allowed (context->shares_permissions, value); return TRUE; } static gboolean option_share_if_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; g_autofree char *name = NULL; g_autofree char *condition = NULL; FlatpakContextShares share; if (!parse_if_option (option_name, value, &name, &condition, error)) return FALSE; share = flatpak_context_share_from_string (name, error); if (share == 0) return FALSE; flatpak_permissions_set_allowed_if (context->shares_permissions, name, condition); return TRUE; } static gboolean option_socket_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextSockets socket; socket = flatpak_context_socket_from_string (value, error); if (socket == 0) return FALSE; flatpak_permissions_set_allowed (context->socket_permissions, value); return TRUE; } static gboolean option_nosocket_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextSockets socket; socket = flatpak_context_socket_from_string (value, error); if (socket == 0) return FALSE; flatpak_permissions_set_not_allowed (context->socket_permissions, value); return TRUE; } static gboolean option_socket_if_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; g_autofree char *name = NULL; g_autofree char *condition = NULL; FlatpakContextSockets socket; if (!parse_if_option (option_name, value, &name, &condition, error)) return FALSE; socket = flatpak_context_socket_from_string (name, error); if (socket == 0) return FALSE; if (socket == FLATPAK_CONTEXT_SOCKET_FALLBACK_X11) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("fallback-x11 can not be conditional")); return FALSE; } flatpak_permissions_set_allowed_if (context->socket_permissions, name, condition); return TRUE; } static gboolean option_device_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextDevices device; device = flatpak_context_device_from_string (value, error); if (device == 0) return FALSE; flatpak_permissions_set_allowed (context->device_permissions, value); return TRUE; } static gboolean option_nodevice_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextDevices device; device = flatpak_context_device_from_string (value, error); if (device == 0) return FALSE; flatpak_permissions_set_not_allowed (context->device_permissions, value); return TRUE; } static gboolean option_device_if_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; g_autofree char *name = NULL; g_autofree char *condition = NULL; FlatpakContextDevices device; if (!parse_if_option (option_name, value, &name, &condition, error)) return FALSE; device = flatpak_context_device_from_string (name, error); if (device == 0) return FALSE; flatpak_permissions_set_allowed_if (context->device_permissions, name, condition); return TRUE; } static gboolean option_allow_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextFeatures feature; feature = flatpak_context_feature_from_string (value, error); if (feature == 0) return FALSE; flatpak_permissions_set_allowed (context->features_permissions, value); return TRUE; } static gboolean option_disallow_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; FlatpakContextFeatures feature; feature = flatpak_context_feature_from_string (value, error); if (feature == 0) return FALSE; flatpak_permissions_set_not_allowed (context->features_permissions, value); return TRUE; } static gboolean option_allow_if_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; g_autofree char *name = NULL; g_autofree char *condition = NULL; FlatpakContextFeatures feature; if (!parse_if_option (option_name, value, &name, &condition, error)) return FALSE; feature = flatpak_context_feature_from_string (name, error); if (feature == 0) return FALSE; flatpak_permissions_set_allowed_if (context->features_permissions, name, condition); return TRUE; } static gboolean option_filesystem_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; g_autofree char *fs = NULL; FlatpakFilesystemMode mode; if (!flatpak_context_parse_filesystem (value, FALSE, &fs, &mode, error)) return FALSE; flatpak_context_take_filesystem (context, g_steal_pointer (&fs), mode); return TRUE; } static gboolean option_nofilesystem_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; g_autofree char *fs = NULL; FlatpakFilesystemMode mode; if (!flatpak_context_parse_filesystem (value, TRUE, &fs, &mode, error)) return FALSE; flatpak_context_take_filesystem (context, g_steal_pointer (&fs), FLATPAK_FILESYSTEM_MODE_NONE); return TRUE; } static gboolean option_env_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; g_auto(GStrv) split = g_strsplit (value, "=", 2); if (split == NULL || split[0] == NULL || split[0][0] == 0 || split[1] == NULL) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Invalid env format %s"), value); return FALSE; } flatpak_context_set_env_var (context, split[0], split[1]); return TRUE; } gboolean flatpak_context_parse_env_block (FlatpakContext *context, const char *data, gsize length, GError **error) { g_auto(GStrv) env_vars = NULL; int i; env_vars = flatpak_parse_env_block (data, length, error); if (env_vars == NULL) return FALSE; for (i = 0; env_vars[i] != NULL; i++) { g_auto(GStrv) split = g_strsplit (env_vars[i], "=", 2); g_assert (g_strv_length (split) == 2); g_assert (split[0][0] != '\0'); flatpak_context_set_env_var (context, split[0], split[1]); } return TRUE; } gboolean flatpak_context_parse_env_fd (FlatpakContext *context, int fd, GError **error) { g_autoptr(GBytes) env_block = NULL; const char *data; gsize len; env_block = glnx_fd_readall_bytes (fd, NULL, error); if (env_block == NULL) return FALSE; data = g_bytes_get_data (env_block, &len); return flatpak_context_parse_env_block (context, data, len, error); } static gboolean option_env_fd_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; glnx_autofd int fd = -1; fd = flatpak_accept_fd_argument (option_name, value, error); if (fd < 0) return FALSE; return flatpak_context_parse_env_fd (context, fd, error); } static gboolean option_unset_env_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (strchr (value, '=') != NULL) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Environment variable name must not contain '=': %s"), value); return FALSE; } flatpak_context_set_env_var (context, value, NULL); return TRUE; } static gboolean option_own_name_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (!flatpak_verify_dbus_name (value, error)) return FALSE; flatpak_context_set_session_bus_policy (context, value, FLATPAK_POLICY_OWN); return TRUE; } static gboolean option_a11y_own_name_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (!flatpak_verify_dbus_name (value, error)) return FALSE; flatpak_context_set_a11y_bus_policy (context, value, FLATPAK_POLICY_OWN); return TRUE; } static gboolean option_talk_name_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (!flatpak_verify_dbus_name (value, error)) return FALSE; flatpak_context_set_session_bus_policy (context, value, FLATPAK_POLICY_TALK); return TRUE; } static gboolean option_no_talk_name_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (!flatpak_verify_dbus_name (value, error)) return FALSE; flatpak_context_set_session_bus_policy (context, value, FLATPAK_POLICY_NONE); return TRUE; } static gboolean option_system_own_name_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (!flatpak_verify_dbus_name (value, error)) return FALSE; flatpak_context_set_system_bus_policy (context, value, FLATPAK_POLICY_OWN); return TRUE; } static gboolean option_system_talk_name_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (!flatpak_verify_dbus_name (value, error)) return FALSE; flatpak_context_set_system_bus_policy (context, value, FLATPAK_POLICY_TALK); return TRUE; } static gboolean option_system_no_talk_name_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; if (!flatpak_verify_dbus_name (value, error)) return FALSE; flatpak_context_set_system_bus_policy (context, value, FLATPAK_POLICY_NONE); return TRUE; } static gboolean option_add_generic_policy_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; const char *t; g_autofree char *key = NULL; const char *policy_value; t = strchr (value, '='); if (t == NULL) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("--add-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); return FALSE; } policy_value = t + 1; key = g_strndup (value, t - value); if (strchr (key, '.') == NULL) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("--add-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); return FALSE; } if (policy_value[0] == '!') { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("--add-policy values can't start with \"!\"")); return FALSE; } flatpak_context_apply_generic_policy (context, key, policy_value); return TRUE; } static gboolean option_remove_generic_policy_cb (const gchar *option_name, const gchar *value, gpointer data, GError **error) { FlatpakContext *context = data; const char *t; g_autofree char *key = NULL; const char *policy_value; g_autofree char *extended_value = NULL; t = strchr (value, '='); if (t == NULL) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("--remove-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); return FALSE; } policy_value = t + 1; key = g_strndup (value, t - value); if (strchr (key, '.') == NULL) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("--remove-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); return FALSE; } if (policy_value[0] == '!') { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, _("--remove-policy values can't start with \"!\"")); return FALSE; } extended_value = g_strconcat ("!", policy_value, NULL); flatpak_context_apply_generic_policy (context, key, extended_value); return TRUE; } static gboolean option_usb_cb (const char *option_name, const char *value, gpointer data, GError **error) { g_autoptr(FlatpakUsbQuery) usb_query = NULL; FlatpakContext *context = data; if (!flatpak_usb_parse_usb (value, &usb_query, error)) return FALSE; flatpak_context_add_usb_query (context, usb_query); return TRUE; } static gboolean option_nousb_cb (const char *option_name, const char *value, gpointer data, GError **error) { g_autoptr(FlatpakUsbQuery) usb_query = NULL; FlatpakContext *context = data; if (!flatpak_usb_parse_usb (value, &usb_query, error)) return FALSE; flatpak_context_add_nousb_query (context, usb_query); return TRUE; } static gboolean option_usb_list_file_cb (const char *option_name, const char *value, gpointer data, GError **error) { return flatpak_context_add_usb_list_from_file (data, value, error); } static gboolean option_usb_list_cb (const char *option_name, const char *value, gpointer data, GError **error) { return flatpak_context_add_usb_list (data, value, error); } static gboolean option_persist_cb (const char *option_name, const char *value, gpointer data, GError **error) { FlatpakContext *context = data; return flatpak_context_set_persistent (context, value, error); } static gboolean option_no_desktop_deprecated; static GOptionEntry context_options[] = { { "share", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_share_cb, N_("Share with host"), N_("SHARE") }, { "unshare", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_unshare_cb, N_("Unshare with host"), N_("SHARE") }, { "share-if", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_share_if_cb, N_("Require conditions to be met for a subsystem to get shared"), N_("SHARE:CONDITION") }, { "socket", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_socket_cb, N_("Expose socket to app"), N_("SOCKET") }, { "nosocket", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_nosocket_cb, N_("Don't expose socket to app"), N_("SOCKET") }, { "socket-if", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_socket_if_cb, N_("Require conditions to be met for a socket to get exposed"), N_("SOCKET:CONDITION") }, { "device", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_device_cb, N_("Expose device to app"), N_("DEVICE") }, { "nodevice", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_nodevice_cb, N_("Don't expose device to app"), N_("DEVICE") }, { "device-if", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_device_if_cb, N_("Require conditions to be met for a device to get exposed"), N_("DEVICE:CONDITION") }, { "allow", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_allow_cb, N_("Allow feature"), N_("FEATURE") }, { "disallow", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_disallow_cb, N_("Don't allow feature"), N_("FEATURE") }, { "allow-if", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_allow_if_cb, N_("Require conditions to be met for a feature to get allowed"), N_("FEATURE:CONDITION") }, { "filesystem", 0, G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, &option_filesystem_cb, N_("Expose filesystem to app (:ro for read-only)"), N_("FILESYSTEM[:ro]") }, { "nofilesystem", 0, G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, &option_nofilesystem_cb, N_("Don't expose filesystem to app"), N_("FILESYSTEM") }, { "env", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_env_cb, N_("Set environment variable"), N_("VAR=VALUE") }, { "env-fd", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_env_fd_cb, N_("Read environment variables in env -0 format from FD"), N_("FD") }, { "unset-env", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_unset_env_cb, N_("Remove variable from environment"), N_("VAR") }, { "own-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_own_name_cb, N_("Allow app to own name on the session bus"), N_("DBUS_NAME") }, { "talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_talk_name_cb, N_("Allow app to talk to name on the session bus"), N_("DBUS_NAME") }, { "no-talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_no_talk_name_cb, N_("Don't allow app to talk to name on the session bus"), N_("DBUS_NAME") }, { "system-own-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_system_own_name_cb, N_("Allow app to own name on the system bus"), N_("DBUS_NAME") }, { "system-talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_system_talk_name_cb, N_("Allow app to talk to name on the system bus"), N_("DBUS_NAME") }, { "system-no-talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_system_no_talk_name_cb, N_("Don't allow app to talk to name on the system bus"), N_("DBUS_NAME") }, { "a11y-own-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_a11y_own_name_cb, N_("Allow app to own name on the a11y bus"), N_("DBUS_NAME") }, { "add-policy", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_add_generic_policy_cb, N_("Add generic policy option"), N_("SUBSYSTEM.KEY=VALUE") }, { "remove-policy", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_remove_generic_policy_cb, N_("Remove generic policy option"), N_("SUBSYSTEM.KEY=VALUE") }, { "usb", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_usb_cb, N_("Add USB device to enumerables"), N_("VENDOR_ID:PRODUCT_ID") }, { "nousb", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_nousb_cb, N_("Add USB device to hidden list"), N_("VENDOR_ID:PRODUCT_ID") }, { "usb-list", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_usb_list_cb, N_("A list of USB devices that are enumerable"), N_("LIST") }, { "usb-list-file", 0, G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, &option_usb_list_file_cb, N_("File containing a list of USB devices to make enumerable"), N_("FILENAME") }, { "persist", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_persist_cb, N_("Persist home directory subpath"), N_("FILENAME") }, /* This is not needed/used anymore, so hidden, but we accept it for backwards compat */ { "no-desktop", 0, G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &option_no_desktop_deprecated, N_("Don't require a running session (no cgroups creation)"), NULL }, { NULL } }; GOptionEntry * flatpak_context_get_option_entries (void) { return context_options; } GOptionGroup * flatpak_context_get_options (FlatpakContext *context) { GOptionGroup *group; group = g_option_group_new ("environment", "Runtime Environment", "Runtime Environment", context, NULL); g_option_group_set_translation_domain (group, GETTEXT_PACKAGE); g_option_group_add_entries (group, context_options); return group; } static const char * parse_negated (const char *option, gboolean *negated) { if (option[0] == '!') { option++; *negated = TRUE; } else { *negated = FALSE; } return option; } /* * Merge the FLATPAK_METADATA_GROUP_CONTEXT, * FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, * FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY and * FLATPAK_METADATA_GROUP_ENVIRONMENT groups, and all groups starting * with FLATPAK_METADATA_GROUP_PREFIX_POLICY, from metakey into context. * * This is a merge, not a replace! */ gboolean flatpak_context_load_metadata (FlatpakContext *context, GKeyFile *metakey, GError **error) { gboolean remove; g_auto(GStrv) groups = NULL; gsize i; if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SHARED, NULL)) { g_auto(GStrv) shares = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SHARED, NULL, error); if (shares == NULL) return FALSE; if (!flatpak_permissions_from_strv (context->shares_permissions, (const char **)shares, error)) return FALSE; } if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SOCKETS, NULL)) { g_auto(GStrv) sockets = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SOCKETS, NULL, error); if (sockets == NULL) return FALSE; if (!flatpak_permissions_from_strv (context->socket_permissions, (const char **)sockets, error)) return FALSE; } if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_DEVICES, NULL)) { g_auto(GStrv) devices = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_DEVICES, NULL, error); if (devices == NULL) return FALSE; if (!flatpak_permissions_from_strv (context->device_permissions, (const char **)devices, error)) return FALSE; } if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FEATURES, NULL)) { g_auto(GStrv) features = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FEATURES, NULL, error); if (features == NULL) return FALSE; if (!flatpak_permissions_from_strv (context->features_permissions, (const char **)features, error)) return FALSE; } if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FILESYSTEMS, NULL)) { g_auto(GStrv) filesystems = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FILESYSTEMS, NULL, error); if (filesystems == NULL) return FALSE; for (i = 0; filesystems[i] != NULL; i++) { const char *fs = parse_negated (filesystems[i], &remove); g_autofree char *filesystem = NULL; g_autoptr(GError) local_error = NULL; FlatpakFilesystemMode mode; if (!flatpak_context_parse_filesystem (fs, remove, &filesystem, &mode, &local_error)) { if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA)) { /* Invalid characters, so just hard-fail. */ g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; } else { g_info ("Unknown filesystem type %s", filesystems[i]); g_clear_error (&local_error); } } else { g_assert (mode == FLATPAK_FILESYSTEM_MODE_NONE || !remove); flatpak_context_take_filesystem (context, g_steal_pointer (&filesystem), mode); } } } if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_PERSISTENT, NULL)) { g_auto(GStrv) persistent = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_PERSISTENT, NULL, error); if (persistent == NULL) return FALSE; for (i = 0; persistent[i] != NULL; i++) if (!flatpak_context_set_persistent (context, persistent[i], error)) return FALSE; } if (g_key_file_has_group (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY)) { g_auto(GStrv) keys = NULL; gsize keys_count; keys = g_key_file_get_keys (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, &keys_count, NULL); for (i = 0; i < keys_count; i++) { const char *key = keys[i]; g_autofree char *value = g_key_file_get_string (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, key, NULL); FlatpakPolicy policy; if (!flatpak_verify_dbus_name (key, error)) return FALSE; policy = flatpak_policy_from_string (value, NULL); if ((int) policy != -1) flatpak_context_set_session_bus_policy (context, key, policy); } } if (g_key_file_has_group (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY)) { g_auto(GStrv) keys = NULL; gsize keys_count; keys = g_key_file_get_keys (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, &keys_count, NULL); for (i = 0; i < keys_count; i++) { const char *key = keys[i]; g_autofree char *value = g_key_file_get_string (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, key, NULL); FlatpakPolicy policy; if (!flatpak_verify_dbus_name (key, error)) return FALSE; policy = flatpak_policy_from_string (value, NULL); if ((int) policy != -1) flatpak_context_set_system_bus_policy (context, key, policy); } } if (g_key_file_has_group (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT)) { g_auto(GStrv) keys = NULL; gsize keys_count; keys = g_key_file_get_keys (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, &keys_count, NULL); for (i = 0; i < keys_count; i++) { const char *key = keys[i]; g_autofree char *value = g_key_file_get_string (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, key, NULL); flatpak_context_set_env_var (context, key, value); } } /* unset-environment is higher precedence than Environment, so that * we can put unset keys in both places. Old versions of Flatpak will * interpret the empty string as unset; new versions will obey * unset-environment. */ if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_UNSET_ENVIRONMENT, NULL)) { g_auto(GStrv) vars = NULL; gsize vars_count; vars = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_UNSET_ENVIRONMENT, &vars_count, error); if (vars == NULL) return FALSE; for (i = 0; i < vars_count; i++) { const char *var = vars[i]; flatpak_context_set_env_var (context, var, NULL); } } groups = g_key_file_get_groups (metakey, NULL); for (i = 0; groups[i] != NULL; i++) { const char *group = groups[i]; const char *subsystem; int j; if (g_str_has_prefix (group, FLATPAK_METADATA_GROUP_PREFIX_POLICY)) { g_auto(GStrv) keys = NULL; subsystem = group + strlen (FLATPAK_METADATA_GROUP_PREFIX_POLICY); keys = g_key_file_get_keys (metakey, group, NULL, NULL); for (j = 0; keys != NULL && keys[j] != NULL; j++) { const char *key = keys[j]; g_autofree char *policy_key = g_strdup_printf ("%s.%s", subsystem, key); g_auto(GStrv) values = NULL; int k; values = g_key_file_get_string_list (metakey, group, key, NULL, NULL); for (k = 0; values != NULL && values[k] != NULL; k++) flatpak_context_apply_generic_policy (context, policy_key, values[k]); } } } if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_USB_DEVICES, FLATPAK_METADATA_KEY_USB_ENUMERABLE_DEVICES, NULL)) { g_auto(GStrv) values = NULL; size_t count; values = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_USB_DEVICES, FLATPAK_METADATA_KEY_USB_ENUMERABLE_DEVICES, &count, error); if (!values) return FALSE; for (i = 0; i < count; i++) { g_autoptr(FlatpakUsbQuery) usb_query = NULL; if (!flatpak_usb_parse_usb (values[i], &usb_query, error)) return FALSE; flatpak_context_add_usb_query (context, usb_query); } } if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_USB_DEVICES, FLATPAK_METADATA_KEY_USB_HIDDEN_DEVICES, NULL)) { g_auto(GStrv) values = NULL; size_t count; values = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_USB_DEVICES, FLATPAK_METADATA_KEY_USB_HIDDEN_DEVICES, &count, error); if (!values) return FALSE; for (i = 0; i < count; i++) { g_autoptr(FlatpakUsbQuery) usb_query = NULL; if (!flatpak_usb_parse_usb (values[i], &usb_query, error)) return FALSE; flatpak_context_add_nousb_query (context, usb_query); } } return TRUE; } static void flatpak_context_save_usb_devices (GHashTable *devices, GKeyFile *keyfile, const char *key) { GHashTableIter iter; gpointer value; if (g_hash_table_size (devices) > 0) { g_autoptr(GPtrArray) usb_devices = g_ptr_array_new (); g_hash_table_iter_init (&iter, devices); while (g_hash_table_iter_next (&iter, &value, NULL)) g_ptr_array_add (usb_devices, (char *) value); if (usb_devices->len > 0) { g_key_file_set_string_list (keyfile, FLATPAK_METADATA_GROUP_USB_DEVICES, key, (const char * const *) usb_devices->pdata, usb_devices->len); } } } /* * Save the FLATPAK_METADATA_GROUP_CONTEXT, * FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, * FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY and * FLATPAK_METADATA_GROUP_ENVIRONMENT groups, and all groups starting * with FLATPAK_METADATA_GROUP_PREFIX_POLICY, into metakey */ void flatpak_context_save_metadata (FlatpakContext *context, gboolean flatten, GKeyFile *metakey) { g_auto(GStrv) shared = NULL; g_auto(GStrv) sockets = NULL; g_auto(GStrv) devices = NULL; g_auto(GStrv) features = NULL; g_autoptr(GPtrArray) unset_env = NULL; GHashTableIter iter; gpointer key, value; g_auto(GStrv) groups = NULL; int i; shared = flatpak_permissions_to_strv (context->shares_permissions, flatten); sockets = flatpak_permissions_to_strv (context->socket_permissions, flatten); devices = flatpak_permissions_to_strv (context->device_permissions, flatten); features = flatpak_permissions_to_strv (context->features_permissions, flatten); if (shared[0] != NULL) { g_key_file_set_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SHARED, (const char * const *) shared, g_strv_length (shared)); } else { g_key_file_remove_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SHARED, NULL); } if (sockets[0] != NULL) { g_key_file_set_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SOCKETS, (const char * const *) sockets, g_strv_length (sockets)); } else { g_key_file_remove_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SOCKETS, NULL); } if (devices[0] != NULL) { g_key_file_set_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_DEVICES, (const char * const *) devices, g_strv_length (devices)); } else { g_key_file_remove_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_DEVICES, NULL); } if (features[0] != NULL) { g_key_file_set_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FEATURES, (const char * const *) features, g_strv_length (features)); } else { g_key_file_remove_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FEATURES, NULL); } if (g_hash_table_size (context->filesystems) > 0) { g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func (g_free); /* Serialize host-reset first, because order can matter in * corner cases. */ if (g_hash_table_lookup_extended (context->filesystems, "host-reset", NULL, &value)) { g_warn_if_fail (GPOINTER_TO_INT (value) == FLATPAK_FILESYSTEM_MODE_NONE); if (!flatten) g_ptr_array_add (array, g_strdup ("!host:reset")); } g_hash_table_iter_init (&iter, context->filesystems); while (g_hash_table_iter_next (&iter, &key, &value)) { FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); if (flatten && mode == FLATPAK_FILESYSTEM_MODE_NONE) continue; /* We already did this */ if (g_str_equal (key, "host-reset")) continue; g_ptr_array_add (array, unparse_filesystem_flags (key, mode)); } g_key_file_set_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FILESYSTEMS, (const char * const *) array->pdata, array->len); } else { g_key_file_remove_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FILESYSTEMS, NULL); } if (g_hash_table_size (context->persistent) > 0) { g_autofree char **keys = (char **) g_hash_table_get_keys_as_array (context->persistent, NULL); g_key_file_set_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_PERSISTENT, (const char * const *) keys, g_strv_length (keys)); } else { g_key_file_remove_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_PERSISTENT, NULL); } g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, NULL); g_hash_table_iter_init (&iter, context->session_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) { FlatpakPolicy policy = GPOINTER_TO_INT (value); if (flatten && (policy == 0)) continue; g_key_file_set_string (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, (char *) key, flatpak_policy_to_string (policy)); } g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, NULL); g_hash_table_iter_init (&iter, context->system_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) { FlatpakPolicy policy = GPOINTER_TO_INT (value); if (flatten && (policy == 0)) continue; g_key_file_set_string (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, (char *) key, flatpak_policy_to_string (policy)); } g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_A11Y_BUS_POLICY, NULL); g_hash_table_iter_init (&iter, context->a11y_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) { FlatpakPolicy policy = GPOINTER_TO_INT (value); if (flatten && (policy == 0)) continue; g_key_file_set_string (metakey, FLATPAK_METADATA_GROUP_A11Y_BUS_POLICY, (char *) key, flatpak_policy_to_string (policy)); } /* Elements are borrowed from context->env_vars */ unset_env = g_ptr_array_new (); g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, NULL); g_hash_table_iter_init (&iter, context->env_vars); while (g_hash_table_iter_next (&iter, &key, &value)) { if (value != NULL) { g_key_file_set_string (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, (char *) key, (char *) value); } else { /* In older versions of Flatpak, [Environment] FOO= * was interpreted as unsetting - so let's do both. */ g_key_file_set_string (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, (char *) key, ""); g_ptr_array_add (unset_env, key); } } if (unset_env->len > 0) { g_ptr_array_add (unset_env, NULL); g_key_file_set_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_UNSET_ENVIRONMENT, (const char * const *) unset_env->pdata, unset_env->len - 1); } else { g_key_file_remove_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_UNSET_ENVIRONMENT, NULL); } groups = g_key_file_get_groups (metakey, NULL); for (i = 0; groups[i] != NULL; i++) { const char *group = groups[i]; if (g_str_has_prefix (group, FLATPAK_METADATA_GROUP_PREFIX_POLICY)) g_key_file_remove_group (metakey, group, NULL); } g_hash_table_iter_init (&iter, context->generic_policy); while (g_hash_table_iter_next (&iter, &key, &value)) { g_auto(GStrv) parts = g_strsplit ((const char *) key, ".", 2); g_autofree char *group = NULL; g_assert (parts[1] != NULL); const char **policy_values = (const char **) value; g_autoptr(GPtrArray) new = g_ptr_array_new (); for (i = 0; policy_values[i] != NULL; i++) { const char *policy_value = policy_values[i]; if (!flatten || policy_value[0] != '!') g_ptr_array_add (new, (char *) policy_value); } if (new->len > 0) { group = g_strconcat (FLATPAK_METADATA_GROUP_PREFIX_POLICY, parts[0], NULL); g_key_file_set_string_list (metakey, group, parts[1], (const char * const *) new->pdata, new->len); } } g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_USB_DEVICES, NULL); flatpak_context_save_usb_devices (context->enumerable_usb_devices, metakey, FLATPAK_METADATA_KEY_USB_ENUMERABLE_DEVICES); flatpak_context_save_usb_devices (context->hidden_usb_devices, metakey, FLATPAK_METADATA_KEY_USB_HIDDEN_DEVICES); } gboolean flatpak_context_get_needs_session_bus_proxy (FlatpakContext *context) { return g_hash_table_size (context->session_bus_policy) > 0; } gboolean flatpak_context_get_needs_system_bus_proxy (FlatpakContext *context) { return g_hash_table_size (context->system_bus_policy) > 0; } static gboolean adds_bus_policy (GHashTable *old, GHashTable *new) { GLNX_HASH_TABLE_FOREACH_KV (new, const char *, name, gpointer, _new_policy) { int new_policy = GPOINTER_TO_INT (_new_policy); int old_policy = GPOINTER_TO_INT (g_hash_table_lookup (old, name)); if (new_policy > old_policy) return TRUE; } return FALSE; } static gboolean adds_generic_policy (GHashTable *old, GHashTable *new) { GLNX_HASH_TABLE_FOREACH_KV (new, const char *, key, GPtrArray *, new_values) { GPtrArray *old_values = g_hash_table_lookup (old, key); int i; if (new_values == NULL || new_values->len == 0) continue; if (old_values == NULL || old_values->len == 0) return TRUE; for (i = 0; i < new_values->len; i++) { const char *new_value = g_ptr_array_index (new_values, i); if (!flatpak_g_ptr_array_contains_string (old_values, new_value)) return TRUE; } } return FALSE; } static gboolean adds_filesystem_access (GHashTable *old, GHashTable *new) { FlatpakFilesystemMode old_host_mode = GPOINTER_TO_INT (g_hash_table_lookup (old, "host")); GLNX_HASH_TABLE_FOREACH_KV (new, const char *, location, gpointer, _new_mode) { FlatpakFilesystemMode new_mode = GPOINTER_TO_INT (_new_mode); FlatpakFilesystemMode old_mode = GPOINTER_TO_INT (g_hash_table_lookup (old, location)); /* Allow more limited access to the same thing */ if (new_mode <= old_mode) continue; /* Allow more limited access if we used to have access to everything */ if (new_mode <= old_host_mode) continue; /* For the remainder we have to be pessimistic, for instance even if we have home access we can't allow adding access to ~/foo, because foo might be a symlink outside home which didn't work before but would work with an explicit access to that particular file. */ return TRUE; } return FALSE; } static gboolean adds_usb_device (FlatpakContext *old, FlatpakContext *new) { GHashTableIter iter; gpointer value; /* Does it add new devices to the allowlist? */ g_hash_table_iter_init (&iter, new->enumerable_usb_devices); while (g_hash_table_iter_next (&iter, &value, NULL)) { if (!g_hash_table_contains (old->enumerable_usb_devices, value)) return TRUE; } /* Does it remove devices from the blocklist? */ g_hash_table_iter_init (&iter, old->hidden_usb_devices); while (g_hash_table_iter_next (&iter, &value, NULL)) { if (!g_hash_table_contains (new->hidden_usb_devices, value)) return TRUE; } return FALSE; } gboolean flatpak_context_adds_permissions (FlatpakContext *old, FlatpakContext *new) { g_autoptr(GHashTable) old_features_permissions = NULL; g_autoptr(GHashTable) old_socket_permissions = NULL; old_features_permissions = flatpak_permissions_dup (old->features_permissions); /* We allow upgrade to multiarch, that is really not a huge problem. * Similarly, having sensible semantics for /dev/shm is * not a security concern. */ flatpak_permissions_set_allowed (old_features_permissions, "multiarch"); flatpak_permissions_set_allowed (old_features_permissions, "per-app-dev-shm"); old_socket_permissions = flatpak_permissions_dup (old->socket_permissions); /* If we used to allow X11, also allow new fallback X11, as that is actually less permissions */ if (flatpak_permissions_allows_unconditionally (old_socket_permissions, "x11")) flatpak_permissions_set_allowed (old_socket_permissions, "fallback-x11"); if (flatpak_permissions_adds_permissions (old->shares_permissions, new->shares_permissions)) return TRUE; if (flatpak_permissions_adds_permissions (old_socket_permissions, new->socket_permissions)) return TRUE; if (flatpak_permissions_adds_permissions (old->device_permissions, new->device_permissions)) return TRUE; if (flatpak_permissions_adds_permissions (old_features_permissions, new->features_permissions)) return TRUE; if (adds_bus_policy (old->session_bus_policy, new->session_bus_policy)) return TRUE; if (adds_bus_policy (old->system_bus_policy, new->system_bus_policy)) return TRUE; if (adds_bus_policy (old->a11y_bus_policy, new->a11y_bus_policy)) return TRUE; if (adds_generic_policy (old->generic_policy, new->generic_policy)) return TRUE; if (adds_filesystem_access (old->filesystems, new->filesystems)) return TRUE; if (adds_usb_device (old, new)) return TRUE; /* env_vars and persistent are intentionally not checked here. * They only affect the sandbox-internal environment and layout, * not the app's access to host resources. */ return FALSE; } char * flatpak_context_devices_to_usb_list (GHashTable *devices, gboolean hidden) { GString *list = g_string_new (NULL); GHashTableIter iter; gpointer value; g_hash_table_iter_init (&iter, devices); while (g_hash_table_iter_next (&iter, &value, NULL)) { if (hidden) g_string_append_printf (list, "!%s;", (const char *) value); else g_string_append_printf (list, "%s;", (const char *) value); } return g_string_free (list, FALSE); } void flatpak_context_to_args (FlatpakContext *context, GPtrArray *args) { GHashTableIter iter; gpointer key, value; char *usb_list = NULL; flatpak_permissions_to_args (context->features_permissions, "allow", "disallow", args); flatpak_permissions_to_args (context->shares_permissions, "share", "unshare", args); flatpak_permissions_to_args (context->device_permissions, "device", "nodevice", args); flatpak_permissions_to_args (context->socket_permissions, "socket", "nosocket", args); g_hash_table_iter_init (&iter, context->env_vars); while (g_hash_table_iter_next (&iter, &key, &value)) { if (value != NULL) g_ptr_array_add (args, g_strdup_printf ("--env=%s=%s", (char *) key, (char *) value)); else g_ptr_array_add (args, g_strdup_printf ("--unset-env=%s", (char *) key)); } g_hash_table_iter_init (&iter, context->persistent); while (g_hash_table_iter_next (&iter, &key, &value)) g_ptr_array_add (args, g_strdup_printf ("--persist=%s", (char *) key)); g_hash_table_iter_init (&iter, context->session_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) { const char *name = key; FlatpakPolicy policy = GPOINTER_TO_INT (value); const char *option = flatpak_policy_to_name_option (policy); g_ptr_array_add (args, g_strdup_printf ("--%s=%s", option, name)); } g_hash_table_iter_init (&iter, context->system_bus_policy); while (g_hash_table_iter_next (&iter, &key, &value)) { const char *name = key; FlatpakPolicy policy = GPOINTER_TO_INT (value); const char *option = flatpak_policy_to_name_option (policy); g_ptr_array_add (args, g_strdup_printf ("--system-%s=%s", option, name)); } /* Serialize host-reset first, because order can matter in * corner cases. */ if (g_hash_table_lookup_extended (context->filesystems, "host-reset", NULL, &value)) { g_warn_if_fail (GPOINTER_TO_INT (value) == FLATPAK_FILESYSTEM_MODE_NONE); g_ptr_array_add (args, g_strdup ("--nofilesystem=host:reset")); } g_hash_table_iter_init (&iter, context->filesystems); while (g_hash_table_iter_next (&iter, &key, &value)) { g_autofree char *fs = NULL; FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); /* We already did this */ if (g_str_equal (key, "host-reset")) continue; fs = unparse_filesystem_flags (key, mode); if (mode != FLATPAK_FILESYSTEM_MODE_NONE) { g_ptr_array_add (args, g_strdup_printf ("--filesystem=%s", fs)); } else { g_assert (fs[0] == '!'); g_ptr_array_add (args, g_strdup_printf ("--nofilesystem=%s", &fs[1])); } } usb_list = flatpak_context_devices_to_usb_list (context->enumerable_usb_devices, FALSE); g_ptr_array_add (args, g_strdup_printf ("--usb-list=%s", usb_list)); g_free (usb_list); usb_list = flatpak_context_devices_to_usb_list (context->hidden_usb_devices, TRUE); g_ptr_array_add (args, g_strdup_printf ("--usb-list=%s", usb_list)); g_free (usb_list); } void flatpak_context_add_bus_filters (FlatpakContext *context, const char *app_id, FlatpakBus bus, gboolean sandboxed, FlatpakBwrap *bwrap) { GHashTable *ht; GHashTableIter iter; gpointer key, value; flatpak_bwrap_add_arg (bwrap, "--filter"); switch (bus) { case FLATPAK_SESSION_BUS: if (app_id) { if (!sandboxed) { flatpak_bwrap_add_arg_printf (bwrap, "--own=%s.*", app_id); flatpak_bwrap_add_arg_printf (bwrap, "--own=org.mpris.MediaPlayer2.%s.*", app_id); } else { flatpak_bwrap_add_arg_printf (bwrap, "--own=%s.Sandboxed.*", app_id); flatpak_bwrap_add_arg_printf (bwrap, "--own=org.mpris.MediaPlayer2.%s.Sandboxed.*", app_id); } } ht = context->session_bus_policy; break; case FLATPAK_SYSTEM_BUS: ht = context->system_bus_policy; break; case FLATPAK_A11Y_BUS: ht = context->a11y_bus_policy; break; default: g_assert_not_reached (); } g_hash_table_iter_init (&iter, ht); while (g_hash_table_iter_next (&iter, &key, &value)) { FlatpakPolicy policy = GPOINTER_TO_INT (value); if (policy > 0) flatpak_bwrap_add_arg_printf (bwrap, "--%s=%s", flatpak_policy_to_string (policy), (char *) key); } } void flatpak_context_reset_non_permissions (FlatpakContext *context) { g_hash_table_remove_all (context->env_vars); } void flatpak_context_reset_permissions (FlatpakContext *context) { g_hash_table_remove_all (context->shares_permissions); g_hash_table_remove_all (context->socket_permissions); g_hash_table_remove_all (context->device_permissions); g_hash_table_remove_all (context->features_permissions); g_hash_table_remove_all (context->persistent); g_hash_table_remove_all (context->filesystems); g_hash_table_remove_all (context->session_bus_policy); g_hash_table_remove_all (context->system_bus_policy); g_hash_table_remove_all (context->a11y_bus_policy); g_hash_table_remove_all (context->generic_policy); } void flatpak_context_make_sandboxed (FlatpakContext *context) { /* We drop almost everything from the app permission, except * multiarch which is inherited, to make sure app code keeps * running. */ g_autoptr(FlatpakPermission) multiarch = flatpak_permission_dup (g_hash_table_lookup (context->features_permissions, "multiarch")); g_hash_table_remove_all (context->shares_permissions); g_hash_table_remove_all (context->socket_permissions); g_hash_table_remove_all (context->device_permissions); g_hash_table_remove_all (context->features_permissions); if (multiarch) { g_hash_table_insert (context->features_permissions, g_strdup ("multiarch"), g_steal_pointer (&multiarch)); } g_hash_table_remove_all (context->persistent); g_hash_table_remove_all (context->filesystems); g_hash_table_remove_all (context->session_bus_policy); g_hash_table_remove_all (context->system_bus_policy); g_hash_table_remove_all (context->a11y_bus_policy); g_hash_table_remove_all (context->generic_policy); } const char *dont_mount_in_root[] = { ".", "..", "app", "bin", "boot", "dev", "efi", "etc", "lib", "lib32", "lib64", "proc", "root", "run", "sbin", "sys", "tmp", "usr", "var", NULL }; static void log_cannot_export_error (FlatpakFilesystemMode mode, const char *path, const GError *error) { GLogLevelFlags level = G_LOG_LEVEL_MESSAGE; /* By default we don't show a log message if the reason we are not sharing * something with the sandbox is simply "it doesn't exist" (or something * very close): otherwise it would be very noisy to launch apps that * opportunistically share things they might benefit from, like Steam * having access to $XDG_RUNTIME_DIR/app/com.discordapp.Discord if it * happens to exist. */ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) level = G_LOG_LEVEL_INFO; /* Some callers specifically suppress warnings for particular errors * by setting this code. */ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED)) level = G_LOG_LEVEL_INFO; switch (mode) { case FLATPAK_FILESYSTEM_MODE_NONE: g_log (G_LOG_DOMAIN, level, _("Not replacing \"%s\" with tmpfs: %s"), path, error->message); break; case FLATPAK_FILESYSTEM_MODE_CREATE: case FLATPAK_FILESYSTEM_MODE_READ_ONLY: case FLATPAK_FILESYSTEM_MODE_READ_WRITE: g_log (G_LOG_DOMAIN, level, _("Not sharing \"%s\" with sandbox: %s"), path, error->message); break; } } static void flatpak_context_export (FlatpakContext *context, FlatpakExports *exports, GFile *app_id_dir, GPtrArray *extra_app_id_dirs, gboolean do_create, gchar **xdg_dirs_conf_out, gboolean *home_access_out) { gboolean home_access = FALSE; g_autoptr(GString) xdg_dirs_conf = NULL; FlatpakFilesystemMode fs_mode, os_mode, etc_mode, root_mode, home_mode; GHashTableIter iter; gpointer key, value; g_autoptr(GError) local_error = NULL; if (xdg_dirs_conf_out != NULL) xdg_dirs_conf = g_string_new (""); fs_mode = GPOINTER_TO_INT (g_hash_table_lookup (context->filesystems, "host")); if (fs_mode != FLATPAK_FILESYSTEM_MODE_NONE) { DIR *dir; struct dirent *dirent; g_info ("Allowing host-fs access"); home_access = TRUE; /* Bind mount most dirs in / into the new root */ dir = opendir ("/"); if (dir != NULL) { while ((dirent = readdir (dir))) { g_autofree char *path = NULL; if (g_strv_contains (dont_mount_in_root, dirent->d_name)) continue; path = g_build_filename ("/", dirent->d_name, NULL); if (!flatpak_exports_add_path_expose (exports, fs_mode, path, &local_error)) { /* Failure to share something like /lib32 because it's * actually a symlink to /usr/lib32 is less of a problem * here than it would be for an explicit * --filesystem=/lib32, so the warning that would normally * be produced in that situation is downgraded to a * debug message. */ if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTABLE_FILE)) local_error->code = G_IO_ERROR_FAILED_HANDLED; log_cannot_export_error (fs_mode, path, local_error); g_clear_error (&local_error); } } closedir (dir); } if (!flatpak_exports_add_path_expose (exports, fs_mode, "/run/media", &local_error)) { log_cannot_export_error (fs_mode, "/run/media", local_error); g_clear_error (&local_error); } } os_mode = MAX (GPOINTER_TO_INT (g_hash_table_lookup (context->filesystems, "host-os")), fs_mode); if (os_mode != FLATPAK_FILESYSTEM_MODE_NONE) flatpak_exports_add_host_os_expose (exports, os_mode); etc_mode = MAX (GPOINTER_TO_INT (g_hash_table_lookup (context->filesystems, "host-etc")), fs_mode); if (etc_mode != FLATPAK_FILESYSTEM_MODE_NONE) flatpak_exports_add_host_etc_expose (exports, etc_mode); root_mode = MAX (GPOINTER_TO_INT (g_hash_table_lookup (context->filesystems, "host-root")), fs_mode); if (root_mode != FLATPAK_FILESYSTEM_MODE_NONE) flatpak_exports_add_host_root_expose (exports, root_mode); home_mode = GPOINTER_TO_INT (g_hash_table_lookup (context->filesystems, "home")); if (home_mode != FLATPAK_FILESYSTEM_MODE_NONE) { g_info ("Allowing homedir access"); home_access = TRUE; if (!flatpak_exports_add_path_expose (exports, MAX (home_mode, fs_mode), g_get_home_dir (), &local_error)) { /* Even if the error is one that we would normally silence, like * the path not existing, it seems reasonable to make more of a fuss * about the home directory not existing or otherwise being unusable, * so this is intentionally not using cannot_export() */ g_warning (_("Not allowing home directory access: %s"), local_error->message); g_clear_error (&local_error); } } g_hash_table_iter_init (&iter, context->filesystems); while (g_hash_table_iter_next (&iter, &key, &value)) { const char *filesystem = key; FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); if (g_strv_contains (flatpak_context_special_filesystems, filesystem)) continue; if (g_str_has_prefix (filesystem, "xdg-")) { g_autofree char *path = NULL; const char *rest = NULL; const char *config_key = NULL; g_autofree char *subpath = NULL; g_autofree char *canonical_path = NULL; g_autofree char *canonical_home = NULL; if (!get_xdg_user_dir_from_string (filesystem, &config_key, &rest, &path)) { g_warning ("Unsupported xdg dir %s", filesystem); continue; } if (path == NULL) continue; /* Unconfigured, ignore */ canonical_path = flatpak_canonicalize_filename (path); canonical_home = flatpak_canonicalize_filename (g_get_home_dir ()); if (strcmp (canonical_path, canonical_home) == 0) { /* xdg-user-dirs sets disabled dirs to $HOME, and its in general not a good idea to set full access to $HOME other than explicitly, so we ignore these */ g_info ("Xdg dir %s is $HOME (i.e. disabled), ignoring", filesystem); continue; } subpath = g_build_filename (path, rest, NULL); if (mode == FLATPAK_FILESYSTEM_MODE_CREATE && do_create) { if (g_mkdir_with_parents (subpath, 0755) != 0) g_info ("Unable to create directory %s", subpath); } if (g_file_test (subpath, G_FILE_TEST_EXISTS)) { if (config_key && xdg_dirs_conf) g_string_append_printf (xdg_dirs_conf, "%s=\"%s\"\n", config_key, path); if (!flatpak_exports_add_path_expose_or_hide (exports, mode, subpath, &local_error)) { log_cannot_export_error (mode, subpath, local_error); g_clear_error (&local_error); } } } else if (g_str_has_prefix (filesystem, "~/")) { g_autofree char *path = NULL; path = g_build_filename (g_get_home_dir (), filesystem + 2, NULL); if (mode == FLATPAK_FILESYSTEM_MODE_CREATE && do_create) { if (g_mkdir_with_parents (path, 0755) != 0) g_info ("Unable to create directory %s", path); } if (!flatpak_exports_add_path_expose_or_hide (exports, mode, path, &local_error)) { log_cannot_export_error (mode, path, local_error); g_clear_error (&local_error); } } else if (g_str_has_prefix (filesystem, "/")) { if (mode == FLATPAK_FILESYSTEM_MODE_CREATE && do_create) { if (g_mkdir_with_parents (filesystem, 0755) != 0) g_info ("Unable to create directory %s", filesystem); } if (!flatpak_exports_add_path_expose_or_hide (exports, mode, filesystem, &local_error)) { log_cannot_export_error (mode, filesystem, local_error); g_clear_error (&local_error); } } else { g_warning ("Unexpected filesystem arg %s", filesystem); } } if (app_id_dir) { g_autoptr(GFile) apps_dir = g_file_get_parent (app_id_dir); int i; /* Hide the .var/app dir by default (unless explicitly made visible) */ if (!flatpak_exports_add_path_tmpfs (exports, flatpak_file_get_path_cached (apps_dir), &local_error)) { log_cannot_export_error (FLATPAK_FILESYSTEM_MODE_NONE, flatpak_file_get_path_cached (apps_dir), local_error); g_clear_error (&local_error); } /* But let the app write to the per-app dir in it */ if (!flatpak_exports_add_path_expose (exports, FLATPAK_FILESYSTEM_MODE_READ_WRITE, flatpak_file_get_path_cached (app_id_dir), &local_error)) { log_cannot_export_error (FLATPAK_FILESYSTEM_MODE_READ_WRITE, flatpak_file_get_path_cached (apps_dir), local_error); g_clear_error (&local_error); } if (extra_app_id_dirs != NULL) { for (i = 0; i < extra_app_id_dirs->len; i++) { GFile *extra_app_id_dir = g_ptr_array_index (extra_app_id_dirs, i); if (!flatpak_exports_add_path_expose (exports, FLATPAK_FILESYSTEM_MODE_READ_WRITE, flatpak_file_get_path_cached (extra_app_id_dir), &local_error)) { log_cannot_export_error (FLATPAK_FILESYSTEM_MODE_READ_WRITE, flatpak_file_get_path_cached (extra_app_id_dir), local_error); g_clear_error (&local_error); } } } } if (home_access_out != NULL) *home_access_out = home_access; if (xdg_dirs_conf_out != NULL) { g_assert (xdg_dirs_conf != NULL); *xdg_dirs_conf_out = g_string_free (g_steal_pointer (&xdg_dirs_conf), FALSE); } } GFile * flatpak_get_data_dir (const char *app_id) { g_autoptr(GFile) home = g_file_new_for_path (g_get_home_dir ()); g_autoptr(GFile) var_app = g_file_resolve_relative_path (home, ".var/app"); return g_file_get_child (var_app, app_id); } FlatpakExports * flatpak_context_get_exports (FlatpakContext *context, const char *app_id) { g_autoptr(GFile) app_id_dir = flatpak_get_data_dir (app_id); return flatpak_context_get_exports_full (context, app_id_dir, NULL, FALSE, FALSE, NULL, NULL); } FlatpakRunFlags flatpak_context_features_to_run_flags (FlatpakContextFeatures features) { FlatpakRunFlags flags = 0; if (features & FLATPAK_CONTEXT_FEATURE_DEVEL) flags |= FLATPAK_RUN_FLAG_DEVEL; if (features & FLATPAK_CONTEXT_FEATURE_MULTIARCH) flags |= FLATPAK_RUN_FLAG_MULTIARCH; if (features & FLATPAK_CONTEXT_FEATURE_BLUETOOTH) flags |= FLATPAK_RUN_FLAG_BLUETOOTH; if (features & FLATPAK_CONTEXT_FEATURE_CANBUS) flags |= FLATPAK_RUN_FLAG_CANBUS; return flags; } FlatpakExports * flatpak_context_get_exports_full (FlatpakContext *context, GFile *app_id_dir, GPtrArray *extra_app_id_dirs, gboolean do_create, gboolean include_default_dirs, gchar **xdg_dirs_conf_out, gboolean *home_access_out) { g_autoptr(FlatpakExports) exports = flatpak_exports_new (); flatpak_context_export (context, exports, app_id_dir, extra_app_id_dirs, do_create, xdg_dirs_conf_out, home_access_out); if (include_default_dirs) { g_autoptr(GFile) user_flatpak_dir = NULL; g_autoptr(GError) local_error = NULL; /* Hide the flatpak dir by default (unless explicitly made visible) */ user_flatpak_dir = flatpak_get_user_base_dir_location (); if (!flatpak_exports_add_path_tmpfs (exports, flatpak_file_get_path_cached (user_flatpak_dir), &local_error)) { log_cannot_export_error (FLATPAK_FILESYSTEM_MODE_NONE, flatpak_file_get_path_cached (user_flatpak_dir), local_error); g_clear_error (&local_error); } /* Ensure we always have a homedir */ if (!flatpak_exports_add_path_dir (exports, g_get_home_dir (), &local_error)) { g_warning (_("Unable to provide a temporary home directory in the sandbox: %s"), local_error->message); g_clear_error (&local_error); } } return g_steal_pointer (&exports); } static void flatpak_context_apply_env_appid (FlatpakBwrap *bwrap, GFile *app_dir) { g_autoptr(GFile) app_dir_data = NULL; g_autoptr(GFile) app_dir_config = NULL; g_autoptr(GFile) app_dir_cache = NULL; g_autoptr(GFile) app_dir_state = NULL; app_dir_data = g_file_get_child (app_dir, "data"); app_dir_config = g_file_get_child (app_dir, "config"); app_dir_cache = g_file_get_child (app_dir, "cache"); /* Yes, this is inconsistent with data, config and cache. However, using * this path lets apps provide backwards-compatibility with older Flatpak * versions by using `--persist=.local/state --unset-env=XDG_STATE_DIR`. */ app_dir_state = g_file_get_child (app_dir, ".local/state"); flatpak_bwrap_set_env (bwrap, "XDG_DATA_HOME", flatpak_file_get_path_cached (app_dir_data), TRUE); flatpak_bwrap_set_env (bwrap, "XDG_CONFIG_HOME", flatpak_file_get_path_cached (app_dir_config), TRUE); flatpak_bwrap_set_env (bwrap, "XDG_CACHE_HOME", flatpak_file_get_path_cached (app_dir_cache), TRUE); flatpak_bwrap_set_env (bwrap, "XDG_STATE_HOME", flatpak_file_get_path_cached (app_dir_state), TRUE); if (g_getenv ("XDG_DATA_HOME")) flatpak_bwrap_set_env (bwrap, "HOST_XDG_DATA_HOME", g_getenv ("XDG_DATA_HOME"), TRUE); if (g_getenv ("XDG_CONFIG_HOME")) flatpak_bwrap_set_env (bwrap, "HOST_XDG_CONFIG_HOME", g_getenv ("XDG_CONFIG_HOME"), TRUE); if (g_getenv ("XDG_CACHE_HOME")) flatpak_bwrap_set_env (bwrap, "HOST_XDG_CACHE_HOME", g_getenv ("XDG_CACHE_HOME"), TRUE); if (g_getenv ("XDG_STATE_HOME")) flatpak_bwrap_set_env (bwrap, "HOST_XDG_STATE_HOME", g_getenv ("XDG_STATE_HOME"), TRUE); } /* This creates zero or more directories unders base_fd+basedir, each * being guaranteed to either exist and be a directory (no symlinks) * or be created as a directory. The last directory is opened * and the fd is returned. */ static gboolean mkdir_p_open_nofollow_at (int base_fd, const char *basedir, int mode, const char *subdir, int *out_fd, GError **error) { glnx_autofd int parent_fd = -1; if (g_path_is_absolute (subdir)) { const char *skipped_prefix = subdir; while (*skipped_prefix == '/') skipped_prefix++; g_warning ("--persist=\"%s\" is deprecated, treating it as --persist=\"%s\"", subdir, skipped_prefix); subdir = skipped_prefix; } g_autofree char *subdir_dirname = g_path_get_dirname (subdir); if (strcmp (subdir_dirname, ".") == 0) { /* It is ok to open basedir with follow=true */ if (!glnx_opendirat (base_fd, basedir, TRUE, &parent_fd, error)) return FALSE; } else if (strcmp (subdir_dirname, "..") == 0) { return glnx_throw (error, "'..' not supported in --persist paths"); } else { if (!mkdir_p_open_nofollow_at (base_fd, basedir, mode, subdir_dirname, &parent_fd, error)) return FALSE; } g_autofree char *subdir_basename = g_path_get_basename (subdir); if (strcmp (subdir_basename, ".") == 0) { *out_fd = glnx_steal_fd (&parent_fd); return TRUE; } else if (strcmp (subdir_basename, "..") == 0) { return glnx_throw (error, "'..' not supported in --persist paths"); } if (!glnx_shutil_mkdir_p_at (parent_fd, subdir_basename, mode, NULL, error)) return FALSE; int fd = openat (parent_fd, subdir_basename, O_PATH | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW); if (fd == -1) { int saved_errno = errno; struct stat stat_buf; /* If it's a symbolic link, that could be a user trying to offload * large data to another filesystem, but it could equally well be * a malicious or compromised app trying to exploit GHSA-7hgv-f2j8-xw87. * Produce a clearer error message in this case. * Unfortunately the errno we get in this case is ENOTDIR, so we have * to ask again to find out whether it's really a symlink. */ if (saved_errno == ENOTDIR && fstatat (parent_fd, subdir_basename, &stat_buf, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK (stat_buf.st_mode)) return glnx_throw (error, "Symbolic link \"%s\" not allowed to avoid sandbox escape", subdir_basename); return glnx_throw_errno_prefix (error, "openat(%s)", subdir_basename); } *out_fd = fd; return TRUE; } void flatpak_context_append_bwrap_filesystem (FlatpakContext *context, FlatpakBwrap *bwrap, const char *app_id, GFile *app_id_dir, FlatpakExports *exports, const char *xdg_dirs_conf, gboolean home_access) { GHashTableIter iter; gpointer key, value; if (app_id_dir != NULL) flatpak_context_apply_env_appid (bwrap, app_id_dir); if (!home_access) { /* Enable persistent mapping only if no access to real home dir */ g_hash_table_iter_init (&iter, context->persistent); while (g_hash_table_iter_next (&iter, &key, NULL)) { const char *persist = key; g_autofree char *appdir = g_build_filename (g_get_home_dir (), ".var/app", app_id, NULL); g_autofree char *dest = g_build_filename (g_get_home_dir (), persist, NULL); g_autoptr(GError) local_error = NULL; if (g_mkdir_with_parents (appdir, 0755) != 0) { g_warning ("Unable to create directory %s", appdir); continue; } /* Don't follow symlinks from the persist directory, as it is under user control */ glnx_autofd int src_fd = -1; if (!mkdir_p_open_nofollow_at (AT_FDCWD, appdir, 0755, persist, &src_fd, &local_error)) { g_warning ("Failed to create persist path %s: %s", persist, local_error->message); continue; } g_autofree char *src_via_proc = g_strdup_printf ("%d", src_fd); flatpak_bwrap_add_fd (bwrap, g_steal_fd (&src_fd)); flatpak_bwrap_add_bind_arg (bwrap, "--bind-fd", src_via_proc, dest); } } if (app_id_dir != NULL) { g_autofree char *user_runtime_dir = flatpak_get_real_xdg_runtime_dir (); g_autofree char *run_user_app_dst = g_strdup_printf ("/run/flatpak/app/%s", app_id); g_autofree char *run_user_app_src = g_build_filename (user_runtime_dir, "app", app_id, NULL); if (glnx_shutil_mkdir_p_at (AT_FDCWD, run_user_app_src, 0700, NULL, NULL)) flatpak_bwrap_add_args (bwrap, "--bind", run_user_app_src, run_user_app_dst, NULL); /* Later, we'll make $XDG_RUNTIME_DIR/app a symlink to /run/flatpak/app */ flatpak_bwrap_add_runtime_dir_member (bwrap, "app"); } /* This actually outputs the args for the hide/expose operations * in the exports */ flatpak_exports_append_bwrap_args (exports, bwrap); /* Special case subdirectories of the cache, config and data xdg * dirs. If these are accessible explicitly, then we bind-mount * these in the app-id dir. This allows applications to explicitly * opt out of keeping some config/cache/data in the app-specific * directory. */ if (app_id_dir) { g_hash_table_iter_init (&iter, context->filesystems); while (g_hash_table_iter_next (&iter, &key, &value)) { const char *filesystem = key; FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); g_autofree char *xdg_path = NULL; const char *rest, *where; xdg_path = get_xdg_dir_from_string (filesystem, &rest, &where); if (xdg_path != NULL && *rest != 0 && mode >= FLATPAK_FILESYSTEM_MODE_READ_ONLY) { g_autoptr(GFile) app_version = g_file_get_child (app_id_dir, where); g_autoptr(GFile) app_version_subdir = g_file_resolve_relative_path (app_version, rest); if (g_file_test (xdg_path, G_FILE_TEST_IS_DIR) || g_file_test (xdg_path, G_FILE_TEST_IS_REGULAR)) { g_autofree char *xdg_path_in_app = g_file_get_path (app_version_subdir); flatpak_bwrap_add_bind_arg (bwrap, mode == FLATPAK_FILESYSTEM_MODE_READ_ONLY ? "--ro-bind" : "--bind", xdg_path, xdg_path_in_app); } } } } if (home_access && app_id_dir != NULL) { g_autofree char *src_path = g_build_filename (g_get_user_config_dir (), "user-dirs.dirs", NULL); g_autofree char *path = g_build_filename (flatpak_file_get_path_cached (app_id_dir), "config/user-dirs.dirs", NULL); if (g_file_test (src_path, G_FILE_TEST_EXISTS)) flatpak_bwrap_add_bind_arg (bwrap, "--ro-bind", src_path, path); } else if (xdg_dirs_conf != NULL && xdg_dirs_conf[0] != '\0' && app_id_dir != NULL) { g_autofree char *path = g_build_filename (flatpak_file_get_path_cached (app_id_dir), "config/user-dirs.dirs", NULL); flatpak_bwrap_add_args_data (bwrap, "xdg-config-dirs", xdg_dirs_conf, strlen (xdg_dirs_conf), path, NULL); } } gboolean flatpak_context_get_allowed_exports (FlatpakContext *context, const char *source_path, const char *app_id, char ***allowed_extensions_out, char ***allowed_prefixes_out, gboolean *require_exact_match_out) { g_autoptr(GPtrArray) allowed_extensions = g_ptr_array_new_with_free_func (g_free); g_autoptr(GPtrArray) allowed_prefixes = g_ptr_array_new_with_free_func (g_free); gboolean require_exact_match = FALSE; g_ptr_array_add (allowed_prefixes, g_strdup_printf ("%s.*", app_id)); if (strcmp (source_path, "share/applications") == 0) { g_ptr_array_add (allowed_extensions, g_strdup (".desktop")); } else if (flatpak_has_path_prefix (source_path, "share/icons")) { g_ptr_array_add (allowed_extensions, g_strdup (".svgz")); g_ptr_array_add (allowed_extensions, g_strdup (".png")); g_ptr_array_add (allowed_extensions, g_strdup (".svg")); g_ptr_array_add (allowed_extensions, g_strdup (".ico")); } else if (strcmp (source_path, "share/dbus-1/services") == 0) { g_auto(GStrv) owned_dbus_names = flatpak_context_get_session_bus_policy_allowed_own_names (context); g_ptr_array_add (allowed_extensions, g_strdup (".service")); for (GStrv iter = owned_dbus_names; *iter != NULL; ++iter) g_ptr_array_add (allowed_prefixes, g_strdup (*iter)); /* We need an exact match with no extra garbage, because the filename refers to busnames * and we can *only* match exactly these */ require_exact_match = TRUE; } else if (strcmp (source_path, "share/gnome-shell/search-providers") == 0) { g_ptr_array_add (allowed_extensions, g_strdup (".ini")); } else if (strcmp (source_path, "share/krunner/dbusplugins") == 0) { g_ptr_array_add (allowed_extensions, g_strdup (".desktop")); } else if (strcmp (source_path, "share/mime/packages") == 0) { g_ptr_array_add (allowed_extensions, g_strdup (".xml")); } else if (strcmp (source_path, "share/metainfo") == 0 || strcmp (source_path, "share/appdata") == 0) { g_ptr_array_add (allowed_extensions, g_strdup (".xml")); } else if (strcmp (source_path, "share/metainfo/releases") == 0) { g_ptr_array_add (allowed_extensions, g_strdup (".releases.xml")); } else return FALSE; g_ptr_array_add (allowed_extensions, NULL); g_ptr_array_add (allowed_prefixes, NULL); if (allowed_extensions_out) *allowed_extensions_out = (char **) g_ptr_array_free (g_steal_pointer (&allowed_extensions), FALSE); if (allowed_prefixes_out) *allowed_prefixes_out = (char **) g_ptr_array_free (g_steal_pointer (&allowed_prefixes), FALSE); if (require_exact_match_out) *require_exact_match_out = require_exact_match; return TRUE; } void flatpak_context_dump (FlatpakContext *context, const char *title) { if (flatpak_is_debugging ()) { g_autoptr(GError) local_error = NULL; g_autoptr(GKeyFile) metakey = NULL; g_autofree char *data = NULL; char *saveptr = NULL; const char *line; metakey = g_key_file_new (); flatpak_context_save_metadata (context, FALSE, metakey); data = g_key_file_to_data (metakey, NULL, &local_error); if (data == NULL) { g_debug ("%s: (unable to serialize: %s)", title, local_error->message); return; } g_debug ("%s:", title); for (line = strtok_r (data, "\n", &saveptr); line != NULL; line = strtok_r (NULL, "\n", &saveptr)) g_debug ("\t%s", line); g_debug ("\t#"); } } FlatpakContextShares flatpak_context_compute_allowed_shares (FlatpakContext *context, FlatpakContextConditionEvaluator evaluator) { return flatpak_permissions_compute_allowed (context->shares_permissions, flatpak_context_shares, evaluator); } FlatpakContextSockets flatpak_context_compute_allowed_sockets (FlatpakContext *context, FlatpakContextConditionEvaluator evaluator) { g_autoptr(GHashTable) permissions = flatpak_permissions_new (); GHashTableIter iter; gpointer key, value; FlatpakPermission *fallback_x11; g_hash_table_iter_init (&iter, context->socket_permissions); while (g_hash_table_iter_next (&iter, &key, &value)) g_hash_table_insert (permissions, g_strdup (key), flatpak_permission_dup (value)); fallback_x11 = g_hash_table_lookup (context->socket_permissions, "fallback-x11"); if (fallback_x11 && fallback_x11->allowed) { FlatpakPermission *x11 = flatpak_permissions_ensure (permissions, "x11"); x11->allowed = FALSE; flatpak_permission_set_allowed_if (x11, "!has-wayland"); } g_hash_table_remove (permissions, "fallback-x11"); return flatpak_permissions_compute_allowed (permissions, flatpak_context_sockets, evaluator); } FlatpakContextDevices flatpak_context_compute_allowed_devices (FlatpakContext *context, FlatpakContextConditionEvaluator evaluator) { return flatpak_permissions_compute_allowed (context->device_permissions, flatpak_context_devices, evaluator); } FlatpakContextFeatures flatpak_context_compute_allowed_features (FlatpakContext *context, FlatpakContextConditionEvaluator evaluator) { return flatpak_permissions_compute_allowed (context->features_permissions, flatpak_context_features, evaluator); }