diff -Nru flatpak-xdg-utils-1.0.4/debian/changelog flatpak-xdg-utils-1.0.5/debian/changelog --- flatpak-xdg-utils-1.0.4/debian/changelog 2020-10-09 07:52:05.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/debian/changelog 2022-02-11 10:18:54.000000000 +0000 @@ -1,3 +1,17 @@ +flatpak-xdg-utils (1.0.5-1) unstable; urgency=medium + + * New upstream release + - Fix xdg-email attachment passing + - Fix leaks + - New flatpak-spawn features: --app-path, --usr-path, --unset-env, + --env-fd, --share-pid + - flatpak-spawn --sandbox-expose-path now tries to normalize paths + to work better with portals + * Standards-Version: 4.6.0 (no changes required) + * Use debhelper compat level 13, drop dh_missing override + + -- Simon McVittie Fri, 11 Feb 2022 10:18:54 +0000 + flatpak-xdg-utils (1.0.4-1) unstable; urgency=medium * New upstream release diff -Nru flatpak-xdg-utils-1.0.4/debian/control flatpak-xdg-utils-1.0.5/debian/control --- flatpak-xdg-utils-1.0.4/debian/control 2020-10-09 07:52:05.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/debian/control 2022-02-11 10:18:54.000000000 +0000 @@ -6,11 +6,11 @@ Simon McVittie , Build-Depends: dbus , - debhelper-compat (= 12), + debhelper-compat (= 13), libglib2.0-dev, meson (>= 0.46.0), Rules-Requires-Root: no -Standards-Version: 4.5.0 +Standards-Version: 4.6.0 Homepage: https://github.com/flatpak/flatpak-xdg-utils Vcs-Git: https://salsa.debian.org/debian/flatpak-xdg-utils.git Vcs-Browser: https://salsa.debian.org/debian/flatpak-xdg-utils diff -Nru flatpak-xdg-utils-1.0.4/debian/rules flatpak-xdg-utils-1.0.5/debian/rules --- flatpak-xdg-utils-1.0.4/debian/rules 2020-10-09 07:52:05.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/debian/rules 2022-02-11 10:18:54.000000000 +0000 @@ -14,6 +14,3 @@ --bindir=/usr/libexec/flatpak-xdg-utils \ -Dinstalled_tests=true \ $(NULL) - -override_dh_missing: - dh_missing --fail-missing diff -Nru flatpak-xdg-utils-1.0.4/.github/workflows/block-autosquash-commits.yml flatpak-xdg-utils-1.0.5/.github/workflows/block-autosquash-commits.yml --- flatpak-xdg-utils-1.0.4/.github/workflows/block-autosquash-commits.yml 1970-01-01 00:00:00.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/.github/workflows/block-autosquash-commits.yml 2022-02-08 09:14:08.903771200 +0000 @@ -0,0 +1,15 @@ +on: pull_request + +name: Pull Requests + +jobs: + message-check: + name: Block Autosquash Commits + + runs-on: ubuntu-latest + + steps: + - name: Block Autosquash Commits + uses: xt0rted/block-autosquash-commits-action@v2.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff -Nru flatpak-xdg-utils-1.0.4/.github/workflows/check.yml flatpak-xdg-utils-1.0.5/.github/workflows/check.yml --- flatpak-xdg-utils-1.0.4/.github/workflows/check.yml 1970-01-01 00:00:00.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/.github/workflows/check.yml 2022-02-08 09:14:08.903771200 +0000 @@ -0,0 +1,31 @@ +name: CI + +on: + - push + - pull_request + +jobs: + check: + name: Build with gcc and test + runs-on: ubuntu-20.04 + steps: + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y dbus libglib2.0-dev meson + - name: Check out + uses: actions/checkout@v1 + - name: Configure + run: meson -Db_sanitize=address,undefined _build + - name: Build + run: ninja -C _build + - name: Test + run: meson test -C _build -v + env: + ASAN_OPTIONS: detect_leaks=0 # Right now we're not fully clean, but this gets us use-after-free etc + - name: Upload test logs + uses: actions/upload-artifact@v1 + if: failure() || cancelled() + with: + name: logs + path: _build/meson-logs diff -Nru flatpak-xdg-utils-1.0.4/meson.build flatpak-xdg-utils-1.0.5/meson.build --- flatpak-xdg-utils-1.0.4/meson.build 2020-10-08 08:11:28.995254300 +0000 +++ flatpak-xdg-utils-1.0.5/meson.build 2022-02-08 09:14:08.905771300 +0000 @@ -1,6 +1,6 @@ project( 'flatpak-xdg-utils', 'c', - version: '1.0.4', + version: '1.0.5', license: 'LGPL-2.1+', default_options: [ 'buildtype=debugoptimized', diff -Nru flatpak-xdg-utils-1.0.4/NEWS flatpak-xdg-utils-1.0.5/NEWS --- flatpak-xdg-utils-1.0.4/NEWS 2020-10-08 08:11:28.994254000 +0000 +++ flatpak-xdg-utils-1.0.5/NEWS 2022-02-08 09:14:08.905771300 +0000 @@ -1,3 +1,15 @@ +Changes in 1.0.5 +================ + + +flatpak-spawn supports --app-path and --usr-path +flatpak-spawn --sandbox-expose-path now tries to normalize paths to work better with portals. +flatpak-spawn now supports --unset-env and --env-fd +flatpak-spwan now supports --share-pid (if supported by portal) +xdg-email now passes attachments correctly +Added tests +Fixed leaks + Changes in 1.0.4 ================ diff -Nru flatpak-xdg-utils-1.0.4/src/flatpak-portal.h flatpak-xdg-utils-1.0.5/src/flatpak-portal.h --- flatpak-xdg-utils-1.0.4/src/flatpak-portal.h 1970-01-01 00:00:00.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/src/flatpak-portal.h 2022-02-08 09:14:08.906771200 +0000 @@ -0,0 +1,77 @@ +/* + * Copyright © 2018 Red Hat, 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 + */ + +#ifndef __FLATPAK_PORTAL_H__ +#define __FLATPAK_PORTAL_H__ + +#define FLATPAK_PORTAL_BUS_NAME "org.freedesktop.portal.Flatpak" +#define FLATPAK_PORTAL_PATH "/org/freedesktop/portal/Flatpak" +#define FLATPAK_PORTAL_INTERFACE FLATPAK_PORTAL_BUS_NAME +#define FLATPAK_PORTAL_INTERFACE_UPDATE_MONITOR FLATPAK_PORTAL_BUS_NAME ".UpdateMonitor" + +typedef enum { + FLATPAK_SPAWN_FLAGS_CLEAR_ENV = 1 << 0, + FLATPAK_SPAWN_FLAGS_LATEST_VERSION = 1 << 1, + FLATPAK_SPAWN_FLAGS_SANDBOX = 1 << 2, + FLATPAK_SPAWN_FLAGS_NO_NETWORK = 1 << 3, + FLATPAK_SPAWN_FLAGS_WATCH_BUS = 1 << 4, + FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS = 1 << 5, + FLATPAK_SPAWN_FLAGS_NOTIFY_START = 1 << 6, + FLATPAK_SPAWN_FLAGS_SHARE_PIDS = 1 << 7, + FLATPAK_SPAWN_FLAGS_EMPTY_APP = 1 << 8, + FLATPAK_SPAWN_FLAGS_NONE = 0 +} FlatpakSpawnFlags; + +typedef enum { + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY = 1 << 0, + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND = 1 << 1, + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU = 1 << 2, + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS = 1 << 3, + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y = 1 << 4, + FLATPAK_SPAWN_SANDBOX_FLAGS_NONE = 0 +} FlatpakSpawnSandboxFlags; + + +typedef enum { + FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS = 1 << 0, + FLATPAK_SPAWN_SUPPORT_FLAGS_NONE = 0 +} FlatpakSpawnSupportFlags; + +/* The same flag is reused: this feature is available under the same + * circumstances */ +#define FLATPAK_SPAWN_SUPPORT_FLAGS_SHARE_PIDS FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS + +#define FLATPAK_SPAWN_FLAGS_ALL (FLATPAK_SPAWN_FLAGS_CLEAR_ENV | \ + FLATPAK_SPAWN_FLAGS_LATEST_VERSION | \ + FLATPAK_SPAWN_FLAGS_SANDBOX | \ + FLATPAK_SPAWN_FLAGS_NO_NETWORK | \ + FLATPAK_SPAWN_FLAGS_WATCH_BUS | \ + FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS | \ + FLATPAK_SPAWN_FLAGS_NOTIFY_START | \ + FLATPAK_SPAWN_FLAGS_SHARE_PIDS | \ + FLATPAK_SPAWN_FLAGS_EMPTY_APP) + +#define FLATPAK_SPAWN_SANDBOX_FLAGS_ALL (FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY | \ + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND | \ + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU | \ + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS | \ + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y) + +#endif /* __FLATPAK_PORTAL_H__ */ diff -Nru flatpak-xdg-utils-1.0.4/src/flatpak-session-helper.h flatpak-xdg-utils-1.0.5/src/flatpak-session-helper.h --- flatpak-xdg-utils-1.0.4/src/flatpak-session-helper.h 1970-01-01 00:00:00.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/src/flatpak-session-helper.h 2022-02-08 09:14:08.906771200 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright © 2021 Collabora Ltd. + * + * 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 . + */ + +#ifndef __FLATPAK_SESSION_HELPER_H__ +#define __FLATPAK_SESSION_HELPER_H__ + +#define FLATPAK_SESSION_HELPER_BUS_NAME "org.freedesktop.Flatpak" + +#define FLATPAK_SESSION_HELPER_PATH "/org/freedesktop/Flatpak/SessionHelper" +#define FLATPAK_SESSION_HELPER_INTERFACE "org.freedesktop.Flatpak.SessionHelper" + +#define FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT "/org/freedesktop/Flatpak/Development" +#define FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT "org.freedesktop.Flatpak.Development" + +typedef enum { + FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV = 1 << 0, + FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS = 1 << 1, + FLATPAK_HOST_COMMAND_FLAGS_NONE = 0 +} FlatpakHostCommandFlags; + +#endif diff -Nru flatpak-xdg-utils-1.0.4/src/flatpak-spawn.c flatpak-xdg-utils-1.0.5/src/flatpak-spawn.c --- flatpak-xdg-utils-1.0.4/src/flatpak-spawn.c 2020-10-08 08:11:28.997254100 +0000 +++ flatpak-xdg-utils-1.0.5/src/flatpak-spawn.c 2022-02-08 09:14:08.907771000 +0000 @@ -27,11 +27,15 @@ #include #include #include +#include + #include #include #include #include "backport-autoptr.h" +#include "flatpak-portal.h" +#include "flatpak-session-helper.h" /* Change to #if 1 to check backwards-compatibility code paths */ #if 0 @@ -39,34 +43,6 @@ #define GLIB_CHECK_VERSION(x, y, z) (0) #endif -typedef enum { - FLATPAK_SPAWN_FLAGS_CLEAR_ENV = 1 << 0, - FLATPAK_SPAWN_FLAGS_LATEST_VERSION = 1 << 1, - FLATPAK_SPAWN_FLAGS_SANDBOX = 1 << 2, - FLATPAK_SPAWN_FLAGS_NO_NETWORK = 1 << 3, - FLATPAK_SPAWN_FLAGS_WATCH_BUS = 1 << 4, /* Since 1.2 */ - FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS = 1 << 5, /* Since 1.6, optional */ -} FlatpakSpawnFlags; - -typedef enum { - FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV = 1 << 0, - FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS = 1 << 1, /* Since 1.2 */ -} FlatpakHostCommandFlags; - -/* Since 1.6 */ -typedef enum { - FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY = 1 << 0, - FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND = 1 << 1, - FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU = 1 << 2, - FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS = 1 << 3, - FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y = 1 << 4, -} FlatpakSpawnSandboxFlags; - -/* Since 1.6 */ -typedef enum { - FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS = 1 << 0, -} FlatpakSpawnSupportFlags; - static GDBusConnection *session_bus = NULL; guint child_pid = 0; @@ -478,6 +454,17 @@ return supports; } +#define NOT_SETUID_ROOT_MESSAGE \ +"This feature requires Flatpak to be using a bubblewrap (bwrap) executable\n" \ +"that is not setuid root.\n" \ +"\n" \ +"The non-setuid version of bubblewrap requires a kernel that allows\n" \ +"unprivileged users to create new user namespaces.\n" \ +"\n" \ +"For more details please see:\n" \ +"https://github.com/flatpak/flatpak/wiki/User-namespace-requirements\n" \ +"\n" + static void check_portal_supports (const char *option, guint32 supports_needed) { @@ -486,12 +473,143 @@ if ((supports & supports_needed) != supports_needed) { g_printerr ("--%s not supported by host portal\n", option); + + if (supports_needed == FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS) + g_printerr ("\n%s", NOT_SETUID_ROOT_MESSAGE); + exit (1); } } +/* + * @str: A path + * @prefix: A possible prefix + * + * The same as flatpak_has_path_prefix(), but instead of a boolean, + * return the part of @str after @prefix (non-%NULL but possibly empty) + * if @str has prefix @prefix, or %NULL if it does not. + * + * Returns: (nullable) (transfer none): the part of @str after @prefix, + * or %NULL if @str is not below @prefix + */ +static const char * +get_path_after (const char *str, + const char *prefix) +{ + while (TRUE) + { + /* Skip consecutive slashes to reach next path + element */ + while (*str == '/') + str++; + while (*prefix == '/') + prefix++; + + /* No more prefix path elements? Done! */ + if (*prefix == 0) + return str; + + /* Compare path element */ + while (*prefix != 0 && *prefix != '/') + { + if (*str != *prefix) + return NULL; + str++; + prefix++; + } + + /* Matched prefix path element, + must be entire str path element */ + if (*str != '/' && *str != 0) + return NULL; + } +} + +static gint32 +path_to_handle (GUnixFDList *fd_list, + const char *path, + const char *home_realpath, + const char *flatpak_id, + GError **error) +{ + int path_fd = open (path, O_PATH|O_CLOEXEC|O_NOFOLLOW|O_RDONLY); + int saved_errno; + gint32 handle; + + if (path_fd < 0) + { + saved_errno = errno; + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno), + "Failed to open %s to expose in sandbox: %s", + path, g_strerror (saved_errno)); + return -1; + } + + if (home_realpath != NULL && flatpak_id != NULL) + { + g_autofree char *real = NULL; + const char *after = NULL; + + real = realpath (path, NULL); + + if (real != NULL) + after = get_path_after (real, home_realpath); + + if (after != NULL) + { + g_autofree char *var_path = NULL; + int var_fd = -1; + struct stat path_buf; + struct stat var_buf; + + /* @after is possibly "", but that's OK: if @path is exactly $HOME, + * we want to check whether it's the same file as + * ~/.var/app/$FLATPAK_ID, with no suffix + */ + var_path = g_build_filename (home_realpath, ".var", "app", flatpak_id, + after, NULL); + + var_fd = open (var_path, O_PATH|O_CLOEXEC|O_NOFOLLOW|O_RDONLY); + + if (var_fd >= 0 && + fstat (path_fd, &path_buf) == 0 && + fstat (var_fd, &var_buf) == 0 && + path_buf.st_dev == var_buf.st_dev && + path_buf.st_ino == var_buf.st_ino) + { + close (path_fd); + path_fd = var_fd; + var_fd = -1; + } + else + { + close (var_fd); + } + } + } + + + handle = g_unix_fd_list_append (fd_list, path_fd, error); + + if (handle < 0) + { + g_prefix_error (error, "Failed to add fd to list for %s: ", path); + close (path_fd); + return -1; + } + + /* The GUnixFdList keeps a duplicate, so we should release the original */ + close (path_fd); + return handle; +} + static gboolean -add_paths_to_variant (GVariantBuilder *builder, GUnixFDList *fd_list, const GStrv paths, gboolean ignore_errors) +add_paths_to_variant (GVariantBuilder *builder, + GUnixFDList *fd_list, + const GStrv paths, + const char *home_realpath, + const char *flatpak_id, + gboolean ignore_errors) { g_autoptr(GError) error = NULL; @@ -500,31 +618,142 @@ for (gsize i = 0; paths[i] != NULL; i++) { - gint handle = -1; - int path_fd = open (paths[i], O_PATH|O_CLOEXEC|O_NOFOLLOW|O_RDONLY); - if (path_fd == -1) + gint32 handle = path_to_handle (fd_list, paths[i], home_realpath, + flatpak_id, + ignore_errors ? NULL : &error); + + if (handle < 0) { if (ignore_errors) continue; - g_printerr ("Failed to open %s to expose in sandbox\n", paths[i]); + g_printerr ("%s\n", error->message); return FALSE; } - handle = g_unix_fd_list_append (fd_list, path_fd, &error); - if (handle == -1) - { - if (ignore_errors) - continue; + g_variant_builder_add (builder, "h", handle); + } + + return TRUE; +} + +static GHashTable *opt_env = NULL; +static GHashTable *opt_unsetenv = NULL; + +static gboolean +opt_env_cb (G_GNUC_UNUSED const char *option_name, + const gchar *value, + G_GNUC_UNUSED gpointer data, + GError **error) +{ + g_auto(GStrv) split = g_strsplit (value, "=", 2); + + g_assert (opt_env != NULL); + g_assert (opt_unsetenv != NULL); + + if (split == NULL || + split[0] == NULL || + split[0][0] == 0 || + split[1] == NULL) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Invalid env format %s", value); + return FALSE; + } + + g_hash_table_remove (opt_unsetenv, split[0]); + g_hash_table_replace (opt_env, + g_steal_pointer (&split[0]), + g_steal_pointer (&split[1])); + return TRUE; +} + +static gboolean +opt_unset_env_cb (G_GNUC_UNUSED const char *option_name, + const gchar *value, + G_GNUC_UNUSED gpointer data, + G_GNUC_UNUSED GError **error) +{ + g_assert (opt_env != NULL); + g_assert (opt_unsetenv != NULL); + + g_hash_table_remove (opt_env, value); + g_hash_table_add (opt_unsetenv, g_strdup (value)); + return TRUE; +} + +static gboolean +option_env_fd_cb (G_GNUC_UNUSED const gchar *option_name, + const gchar *value, + G_GNUC_UNUSED gpointer data, + GError **error) +{ + g_autofree gchar *proc_filename = NULL; + g_autofree gchar *env_block = NULL; + gsize remaining; + const char *p; + guint64 fd; + gchar *endptr; + + g_assert (opt_env != NULL); + g_assert (opt_unsetenv != NULL); + + fd = g_ascii_strtoull (value, &endptr, 10); + + if (endptr == NULL || *endptr != '\0' || fd > G_MAXINT) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Not a valid file descriptor: %s", value); + return FALSE; + } + + proc_filename = g_strdup_printf ("/proc/self/fd/%d", (int) fd); + + if (!g_file_get_contents (proc_filename, &env_block, &remaining, error)) + return FALSE; + + p = env_block; + + while (remaining > 0) + { + g_autofree gchar *var = NULL; + g_autofree gchar *val = NULL; + size_t len = strnlen (p, remaining); + const char *equals; + + g_assert (len <= remaining); + + equals = memchr (p, '=', len); - g_printerr ("Failed to add fd to list for %s: %s\n", paths[i], error->message); + if (equals == NULL || equals == p) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Environment variable must be given in the form VARIABLE=VALUE, not %.*s", + (int) len, p); return FALSE; } - /* The GUnixFdList keeps a duplicate, so we should release the original */ - close (path_fd); - g_variant_builder_add (builder, "h", handle); + + var = g_strndup (p, equals - p); + val = g_strndup (equals + 1, len - (equals - p) - 1); + g_hash_table_remove (opt_unsetenv, var); + g_hash_table_replace (opt_env, + g_steal_pointer (&var), + g_steal_pointer (&val)); + + p += len; + remaining -= len; + + if (remaining > 0) + { + g_assert (*p == '\0'); + p += 1; + remaining -= 1; + } } + if (fd >= 3) + close (fd); + return TRUE; } @@ -539,11 +768,11 @@ int i, opt_argc; gboolean verbose = FALSE; char **forward_fds = NULL; - char **opt_envs = NULL; guint spawn_flags; gboolean opt_clear_env = FALSE; gboolean opt_watch_bus = FALSE; gboolean opt_expose_pids = FALSE; + gboolean opt_share_pids = FALSE; gboolean opt_latest_version = FALSE; gboolean opt_sandbox = FALSE; gboolean opt_no_network = FALSE; @@ -554,7 +783,11 @@ char **opt_sandbox_expose_path_try = NULL; char **opt_sandbox_expose_path_ro_try = NULL; char *opt_directory = NULL; + char *opt_app_path = NULL; + char *opt_usr_path = NULL; g_autofree char *cwd = NULL; + g_autofree char *home_realpath = NULL; + const char *flatpak_id = NULL; GVariantBuilder options_builder; const GOptionEntry options[] = { { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Enable debug output", NULL }, @@ -562,7 +795,10 @@ { "clear-env", 0, 0, G_OPTION_ARG_NONE, &opt_clear_env, "Run with clean environment", NULL }, { "watch-bus", 0, 0, G_OPTION_ARG_NONE, &opt_watch_bus, "Make the spawned command exit if we do", NULL }, { "expose-pids", 0, 0, G_OPTION_ARG_NONE, &opt_expose_pids, "Expose sandbox pid in calling sandbox", NULL }, - { "env", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_envs, "Set environment variable", "VAR=VALUE" }, + { "share-pids", 0, 0, G_OPTION_ARG_NONE, &opt_share_pids, "Use same pid namespace as calling sandbox", NULL }, + { "env", 0, 0, G_OPTION_ARG_CALLBACK, &opt_env_cb, "Set environment variable", "VAR=VALUE" }, + { "unset-env", 0, 0, G_OPTION_ARG_CALLBACK, &opt_unset_env_cb, "Unset environment variable", "VAR=VALUE" }, + { "env-fd", 0, 0, G_OPTION_ARG_CALLBACK, &option_env_fd_cb, "Read environment variables in env -0 format from FD", "FD" }, { "latest-version", 0, 0, G_OPTION_ARG_NONE, &opt_latest_version, "Run latest version", NULL }, { "sandbox", 0, 0, G_OPTION_ARG_NONE, &opt_sandbox, "Run sandboxed", NULL }, { "no-network", 0, 0, G_OPTION_ARG_NONE, &opt_no_network, "Run without network access", NULL }, @@ -575,9 +811,13 @@ { "sandbox-flag", 0, 0, G_OPTION_ARG_CALLBACK, sandbox_flag_callback, "Enable sandbox flag", "FLAG" }, { "host", 0, 0, G_OPTION_ARG_NONE, &opt_host, "Start the command on the host", NULL }, { "directory", 0, 0, G_OPTION_ARG_FILENAME, &opt_directory, "Working directory in which to run the command", "DIR" }, + { "app-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_app_path, "Replace runtime's /app with DIR or empty", "DIR|\"\"" }, + { "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, "Replace runtime's /usr with DIR", "DIR" }, { NULL } }; guint signal_source = 0; + GHashTableIter iter; + gpointer key, value; setlocale (LC_ALL, ""); @@ -594,6 +834,8 @@ i++; opt_argc = i; + opt_env = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + opt_unsetenv = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); while (i < argc) { @@ -632,6 +874,11 @@ if (signal_source == 0) return 1; + flatpak_id = g_getenv ("FLATPAK_ID"); + + if (flatpak_id != NULL) + home_realpath = realpath (g_get_home_dir (), NULL); + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); if (session_bus == NULL) { @@ -641,15 +888,15 @@ if (opt_host) { - service_iface = "org.freedesktop.Flatpak.Development"; - service_obj_path = "/org/freedesktop/Flatpak/Development"; - service_bus_name = "org.freedesktop.Flatpak"; + service_iface = FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT; + service_obj_path = FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT; + service_bus_name = FLATPAK_SESSION_HELPER_BUS_NAME; } else { - service_iface = "org.freedesktop.portal.Flatpak"; - service_obj_path = "/org/freedesktop/portal/Flatpak"; - service_bus_name = "org.freedesktop.portal.Flatpak"; + service_iface = FLATPAK_PORTAL_INTERFACE; + service_obj_path = FLATPAK_PORTAL_PATH; + service_bus_name = FLATPAK_PORTAL_BUS_NAME; } g_dbus_connection_signal_subscribe (session_bus, @@ -718,18 +965,12 @@ g_variant_builder_add (fd_builder, "{uh}", fd, handle); } - for (i = 0; opt_envs != NULL && opt_envs[i] != NULL; i++) - { - const char *opt_env = opt_envs[i]; - g_auto(GStrv) split = g_strsplit (opt_env, "=", 2); + g_hash_table_iter_init (&iter, opt_env); - if (split == NULL || split[0] == NULL || split[0][0] == 0 || split[1] == NULL) - { - g_printerr ("Invalid env format %s\n", opt_env); - return 1; - } - g_variant_builder_add (env_builder, "{ss}", split[0], split[1]); - } + while (g_hash_table_iter_next (&iter, &key, &value)) + g_variant_builder_add (env_builder, "{ss}", key, value); + + g_clear_pointer (&opt_env, g_hash_table_unref); spawn_flags = 0; @@ -739,11 +980,24 @@ if (opt_watch_bus) spawn_flags |= opt_host ? FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS : FLATPAK_SPAWN_FLAGS_WATCH_BUS; - if (opt_expose_pids) + if (opt_share_pids) { if (opt_host) { - g_printerr ("--host not compatible with --expose-pids"); + g_printerr ("--host not compatible with --share-pids\n"); + return 1; + } + + check_portal_version ("share-pids", 5); + check_portal_supports ("share-pids", FLATPAK_SPAWN_SUPPORT_FLAGS_SHARE_PIDS); + + spawn_flags |= FLATPAK_SPAWN_FLAGS_SHARE_PIDS; + } + else if (opt_expose_pids) + { + if (opt_host) + { + g_printerr ("--host not compatible with --expose-pids\n"); return 1; } @@ -757,7 +1011,7 @@ { if (opt_host) { - g_printerr ("--host not compatible with --latest-version"); + g_printerr ("--host not compatible with --latest-version\n"); return 1; } spawn_flags |= FLATPAK_SPAWN_FLAGS_LATEST_VERSION; @@ -767,7 +1021,7 @@ { if (opt_host) { - g_printerr ("--host not compatible with --sandbox"); + g_printerr ("--host not compatible with --sandbox\n"); return 1; } spawn_flags |= FLATPAK_SPAWN_FLAGS_SANDBOX; @@ -777,7 +1031,7 @@ { if (opt_host) { - g_printerr ("--host not compatible with --no-network"); + g_printerr ("--host not compatible with --no-network\n"); return 1; } spawn_flags |= FLATPAK_SPAWN_FLAGS_NO_NETWORK; @@ -785,11 +1039,63 @@ g_variant_builder_init (&options_builder, G_VARIANT_TYPE ("a{sv}")); + if (g_hash_table_size (opt_unsetenv) > 0) + { + g_hash_table_iter_init (&iter, opt_unsetenv); + + /* The host portal doesn't support options, so we always have to do + * this the hard way. The subsandbox portal supports unset-env in + * versions >= 5. */ + if (opt_host ? FALSE : (get_portal_version () >= 5)) + { + GVariantBuilder strv_builder; + + g_variant_builder_init (&strv_builder, G_VARIANT_TYPE_STRING_ARRAY); + + while (g_hash_table_iter_next (&iter, &key, NULL)) + g_variant_builder_add (&strv_builder, "s", key); + + g_variant_builder_add (&options_builder, "{s@v}", "unset-env", + g_variant_new_variant (g_variant_builder_end (&strv_builder))); + } + else + { + /* env(1) will do the wrong thing if argv[0] contains an equals + * sign, so we might need to prepend this incantation - and + * because we're prepending, we need to do it backwards. + * More legibly, we're replacing MY=COMMAND ARGS with: + * + * /usr/bin/env -u VAR -u VAR2 /bin/sh -euc 'exec "$@"' sh MY=COMMAND ARGS + * + * This is a standard trick for dealing with env(1). */ + g_assert (child_argv->len >= 1); + + if (strchr (g_ptr_array_index (child_argv, 0), '=') != NULL) + { + g_ptr_array_insert (child_argv, 0, g_strdup ("sh")); /* argv[0] */ + g_ptr_array_insert (child_argv, 0, g_strdup ("exec \"$@\"")); + g_ptr_array_insert (child_argv, 0, g_strdup ("-euc")); + g_ptr_array_insert (child_argv, 0, g_strdup ("/bin/sh")); + } + + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + /* Again, yes, this is backwards: we're prepending. */ + g_ptr_array_insert (child_argv, 0, g_strdup (key)); + g_ptr_array_insert (child_argv, 0, g_strdup ("-u")); + } + + g_ptr_array_insert (child_argv, 0, g_strdup ("/usr/bin/env")); + } + } + + g_clear_pointer (&opt_unsetenv, g_hash_table_unref); + if (opt_sandbox_expose) { if (opt_host) { - g_printerr ("--host not compatible with --sandbox-expose"); + g_printerr ("--host not compatible with --sandbox-expose\n"); return 1; } g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose", @@ -800,7 +1106,7 @@ { if (opt_host) { - g_printerr ("--host not compatible with --sandbox-expose-ro"); + g_printerr ("--host not compatible with --sandbox-expose-ro\n"); return 1; } g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose-ro", @@ -833,8 +1139,8 @@ check_portal_version ("sandbox-expose-path", 3); - if (!add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path, FALSE) - || !add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_try, TRUE)) + if (!add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path, home_realpath, flatpak_id, FALSE) + || !add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_try, home_realpath, flatpak_id, TRUE)) return 1; g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose-fd", @@ -853,14 +1159,76 @@ check_portal_version ("sandbox-expose-path-ro", 3); - if (!add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_ro, FALSE) - || !add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_ro_try, TRUE)) + if (!add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_ro, home_realpath, flatpak_id, FALSE) + || !add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_ro_try, home_realpath, flatpak_id, TRUE)) return 1; g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose-fd-ro", g_variant_new_variant (g_variant_builder_end (g_steal_pointer (&expose_fd_builder)))); } + if (opt_app_path != NULL) + { + gint32 handle; + + g_debug ("Using \"%s\" as /app instead of runtime", opt_app_path); + + if (opt_host) + { + g_printerr ("--host not compatible with --app-path\n"); + return 1; + } + + check_portal_version ("app-path", 6); + + if (opt_app_path[0] == '\0') + { + /* Empty path is special-cased to mean an empty directory */ + spawn_flags |= FLATPAK_SPAWN_FLAGS_EMPTY_APP; + } + else + { + handle = path_to_handle (fd_list, opt_app_path, home_realpath, + flatpak_id, &error); + + if (handle < 0) + { + g_printerr ("%s\n", error->message); + return 1; + } + + g_variant_builder_add (&options_builder, "{s@v}", "app-fd", + g_variant_new_variant (g_variant_new_handle (handle))); + } + } + + if (opt_usr_path != NULL) + { + gint32 handle; + + g_debug ("Using %s as /usr instead of runtime", opt_usr_path); + + if (opt_host) + { + g_printerr ("--host not compatible with --usr-path\n"); + return 1; + } + + check_portal_version ("usr-path", 6); + + handle = path_to_handle (fd_list, opt_usr_path, home_realpath, + flatpak_id, &error); + + if (handle < 0) + { + g_printerr ("%s\n", error->message); + return 1; + } + + g_variant_builder_add (&options_builder, "{s@v}", "usr-fd", + g_variant_new_variant (g_variant_new_handle (handle))); + } + if (!opt_directory) { opt_directory = cwd; diff -Nru flatpak-xdg-utils-1.0.4/src/xdg-email.c flatpak-xdg-utils-1.0.5/src/xdg-email.c --- flatpak-xdg-utils-1.0.4/src/xdg-email.c 2020-10-08 08:11:28.997254100 +0000 +++ flatpak-xdg-utils-1.0.5/src/xdg-email.c 2022-02-08 09:14:08.907771000 +0000 @@ -385,7 +385,7 @@ g_variant_builder_add (&opt_builder, "{sv}", - "attachments", g_variant_new_parsed ("[0]")); + "attachment_fds", g_variant_new_parsed ("@ah [0]")); } parameters = g_variant_new ("(s@a{sv})", diff -Nru flatpak-xdg-utils-1.0.4/src/xdg-open.c flatpak-xdg-utils-1.0.5/src/xdg-open.c --- flatpak-xdg-utils-1.0.4/src/xdg-open.c 2020-10-08 08:11:28.997254100 +0000 +++ flatpak-xdg-utils-1.0.5/src/xdg-open.c 2022-02-08 09:14:08.907771000 +0000 @@ -26,6 +26,8 @@ #include #include +#include "backport-autoptr.h" + #define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" #define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" #define PORTAL_IFACE_NAME "org.freedesktop.portal.OpenURI" @@ -46,11 +48,12 @@ int main (int argc, char *argv[]) { - GOptionContext *context; - GError *error = NULL; - GDBusConnection *connection; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GDBusConnection) connection = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GVariant) reply = NULL; GVariantBuilder opt_builder; - GFile *file; context = g_option_context_new ("{ file | URL }"); @@ -63,7 +66,6 @@ g_printerr ("\n"); g_printerr ("Try \"%s --help\" for more information.\n", g_get_prgname ()); - g_error_free (error); return 1; } @@ -76,10 +78,9 @@ if (show_help || uris == NULL || g_strv_length (uris) > 1) { - char *help = g_option_context_get_help (context, TRUE, NULL); + g_autofree gchar *help = g_option_context_get_help (context, TRUE, NULL); g_print ("%s\n", help); - g_free (help); return 0; } @@ -92,7 +93,6 @@ else g_printerr ("Failed to connect to session bus"); - g_clear_pointer (&error, g_error_free); return 3; } @@ -101,69 +101,61 @@ file = g_file_new_for_commandline_arg (uris[0]); if (g_file_is_native (file)) { - char *path; + g_autofree gchar *path = NULL; int fd; - GUnixFDList *fd_list; + g_autoptr(GUnixFDList) fd_list = NULL; path = g_file_get_path (file); fd = open (path, O_RDONLY | O_CLOEXEC); if (fd == -1) { g_printerr ("Failed to open '%s': %s", path, g_strerror (errno)); + g_variant_unref (g_variant_builder_end (&opt_builder)); return 5; } fd_list = g_unix_fd_list_new_from_array (&fd, 1); fd = -1; - g_dbus_connection_call_with_unix_fd_list_sync (connection, - PORTAL_BUS_NAME, - PORTAL_OBJECT_PATH, - PORTAL_IFACE_NAME, - "OpenFile", - g_variant_new ("(sh@a{sv})", - "", 0, - g_variant_builder_end (&opt_builder)), - NULL, - G_DBUS_CALL_FLAGS_NONE, - -1, - fd_list, - NULL, - NULL, - &error); - - g_object_unref (fd_list); - g_free (path); + reply = g_dbus_connection_call_with_unix_fd_list_sync (connection, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + PORTAL_IFACE_NAME, + "OpenFile", + g_variant_new ("(sh@a{sv})", + "", 0, + g_variant_builder_end (&opt_builder)), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + fd_list, + NULL, + NULL, + &error); } else { - g_dbus_connection_call_sync (connection, - PORTAL_BUS_NAME, - PORTAL_OBJECT_PATH, - PORTAL_IFACE_NAME, - "OpenURI", - g_variant_new ("(ss@a{sv})", - "", uris[0], - g_variant_builder_end (&opt_builder)), - NULL, - G_DBUS_CALL_FLAGS_NONE, - -1, - NULL, - &error); + reply = g_dbus_connection_call_sync (connection, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + PORTAL_IFACE_NAME, + "OpenURI", + g_variant_new ("(ss@a{sv})", + "", uris[0], + g_variant_builder_end (&opt_builder)), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); } if (error) { g_printerr ("Failed to call portal: %s\n", error->message); - g_object_unref (connection); - g_error_free (error); - return 4; } - g_object_unref (connection); - g_object_unref (file); - return 0; } diff -Nru flatpak-xdg-utils-1.0.4/tests/common.c flatpak-xdg-utils-1.0.5/tests/common.c --- flatpak-xdg-utils-1.0.4/tests/common.c 2020-10-08 08:11:28.999254200 +0000 +++ flatpak-xdg-utils-1.0.5/tests/common.c 2022-02-08 09:14:08.907771000 +0000 @@ -97,3 +97,17 @@ g_variant_get (variant, "(u)", &result); g_assert_cmpuint (result, ==, DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER); } + +/* A GAsyncReadyCallback that stores @res via a `GAsyncResult **`. */ +void +store_result_cb (G_GNUC_UNUSED GObject *source, + GAsyncResult *res, + gpointer data) +{ + GAsyncResult **res_p = data; + + g_assert_nonnull (res_p); + g_assert_null (*res_p); + g_assert_true (G_IS_ASYNC_RESULT (res)); + *res_p = g_object_ref (res); +} diff -Nru flatpak-xdg-utils-1.0.4/tests/common.h flatpak-xdg-utils-1.0.5/tests/common.h --- flatpak-xdg-utils-1.0.4/tests/common.h 2020-10-08 08:11:28.999254200 +0000 +++ flatpak-xdg-utils-1.0.5/tests/common.h 2022-02-08 09:14:08.907771000 +0000 @@ -35,3 +35,9 @@ gchar **dbus_address); void own_name_sync (GDBusConnection *conn, const char *name); +void store_result_cb (GObject *source, GAsyncResult *res, gpointer data); + +#ifndef g_assert_no_errno +#define g_assert_no_errno(expr) \ + g_assert_cmpstr ((expr) >= 0 ? NULL : g_strerror (errno), ==, NULL) +#endif diff -Nru flatpak-xdg-utils-1.0.4/tests/meson.build flatpak-xdg-utils-1.0.5/tests/meson.build --- flatpak-xdg-utils-1.0.4/tests/meson.build 2020-10-08 08:11:28.999254200 +0000 +++ flatpak-xdg-utils-1.0.5/tests/meson.build 2022-02-08 09:14:08.907771000 +0000 @@ -3,6 +3,7 @@ test_env.set('G_TEST_SRCDIR', meson.current_source_dir()) test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) test_env.set('GIO_USE_VFS', 'local') +test_env.set('FLATPAK_SPAWN', flatpak_spawn.full_path()) test_env.set('XDG_EMAIL', xdg_email.full_path()) test_env.set('XDG_OPEN', xdg_open.full_path()) test_env.set('MALLOC_CHECK_', '2') @@ -10,6 +11,7 @@ tests = [ 'test-email', 'test-open', + 'test-spawn', ] test_timeout = 60 diff -Nru flatpak-xdg-utils-1.0.4/tests/test-spawn.c flatpak-xdg-utils-1.0.5/tests/test-spawn.c --- flatpak-xdg-utils-1.0.4/tests/test-spawn.c 1970-01-01 00:00:00.000000000 +0000 +++ flatpak-xdg-utils-1.0.5/tests/test-spawn.c 2022-02-08 09:14:08.908771300 +0000 @@ -0,0 +1,1279 @@ +/* + * Copyright © 2018-2019 Collabora Ltd. + * Copyright © 2021 Simon McVittie + * + * 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 . + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "backport-autoptr.h" +#include "common.h" + +typedef enum +{ + FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV = 1 << 0, + FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS = 1 << 1, +} FlatpakHostCommandFlags; + +#define FLATPAK_SESSION_HELPER_BUS_NAME "org.freedesktop.Flatpak" +#define FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT "/org/freedesktop/Flatpak/Development" +#define FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT "org.freedesktop.Flatpak.Development" + +typedef enum { + FLATPAK_SPAWN_FLAGS_CLEAR_ENV = 1 << 0, + FLATPAK_SPAWN_FLAGS_LATEST_VERSION = 1 << 1, + FLATPAK_SPAWN_FLAGS_SANDBOX = 1 << 2, + FLATPAK_SPAWN_FLAGS_NO_NETWORK = 1 << 3, + FLATPAK_SPAWN_FLAGS_WATCH_BUS = 1 << 4, + FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS = 1 << 5, + FLATPAK_SPAWN_FLAGS_NOTIFY_START = 1 << 6, + FLATPAK_SPAWN_FLAGS_SHARE_PIDS = 1 << 7, + FLATPAK_SPAWN_FLAGS_EMPTY_APP = 1 << 8, +} FlatpakSpawnFlags; + +typedef enum { + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY = 1 << 0, + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND = 1 << 1, + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU = 1 << 2, + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS = 1 << 3, + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y = 1 << 4, +} FlatpakSpawnSandboxFlags; +#define FLATPAK_SPAWN_SANDBOX_FLAGS_FUTURE (1 << 23) + +typedef enum { + FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS = 1 << 0, +} FlatpakSpawnSupportFlags; + +#define FLATPAK_PORTAL_BUS_NAME "org.freedesktop.portal.Flatpak" +#define FLATPAK_PORTAL_PATH "/org/freedesktop/portal/Flatpak" +#define FLATPAK_PORTAL_INTERFACE FLATPAK_PORTAL_BUS_NAME + +typedef struct +{ + const char *extra_arg; + FlatpakHostCommandFlags host_flags; + FlatpakSpawnFlags subsandbox_flags; + FlatpakSpawnSandboxFlags subsandbox_sandbox_flags; + FlatpakSpawnSupportFlags portal_supports; + const char *app_path; + const char *usr_path; + int fails_immediately; + int fails_after_version_check; + gboolean awkward_command_name; + gboolean dbus_call_fails; + gboolean extra; + gboolean host; + gboolean no_command; + gboolean no_session_bus; + gboolean sandbox_complex; +} Config; + +typedef struct +{ + const Config *config; + GSubprocess *dbus_daemon; + gchar *dbus_address; + GSubprocess *flatpak_spawn; + gchar *flatpak_spawn_path; + GDBusConnection *mock_development_conn; + GDBusConnection *mock_portal_conn; + guint mock_development_object; + guint mock_portal_object; + GQueue invocations; + guint32 mock_development_version; + guint32 mock_portal_version; + guint32 mock_portal_supports; +} Fixture; + +static const Config default_config = {}; + +static void +mock_method_call (GDBusConnection *conn G_GNUC_UNUSED, + const gchar *sender G_GNUC_UNUSED, + const gchar *object_path G_GNUC_UNUSED, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters G_GNUC_UNUSED, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + Fixture *f = user_data; + g_autofree gchar *params = NULL; + + params = g_variant_print (parameters, TRUE); + + g_test_message ("Method called: %s.%s%s", interface_name, method_name, + params); + + g_queue_push_tail (&f->invocations, g_object_ref (invocation)); + + if (f->config->dbus_call_fails) + { + g_dbus_method_invocation_return_dbus_error (invocation, + "com.example.No", + "Mock portal failed"); + return; + } + + if (strcmp (method_name, "HostCommand") == 0 || + strcmp (method_name, "Spawn") == 0) + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(u)", 12345)); + else /* HostCommandSignal or SpawnSignal */ + g_dbus_method_invocation_return_value (invocation, NULL); +} + +static GVariant * +mock_get_property (GDBusConnection *conn G_GNUC_UNUSED, + const gchar *sender G_GNUC_UNUSED, + const gchar *object_path G_GNUC_UNUSED, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + Fixture *f = user_data; + + g_test_message ("Property retrieved: %s.%s", interface_name, property_name); + + if (strcmp (interface_name, FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT) == 0) + { + if (strcmp (property_name, "version") == 0) + return g_variant_new_uint32 (f->mock_development_version); + } + + if (strcmp (interface_name, FLATPAK_PORTAL_INTERFACE) == 0) + { + if (strcmp (property_name, "supports") == 0) + return g_variant_new_uint32 (f->mock_portal_supports); + + if (strcmp (property_name, "version") == 0) + return g_variant_new_uint32 (f->mock_portal_version); + } + + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, + "Unknown interface or property %s.%s", + interface_name, property_name); + return NULL; +} + +static GDBusArgInfo arg_cwd_path = +{ + -1, + "cwd_path", + "ay", + NULL /* annotations */ +}; + +static GDBusArgInfo arg_argv = +{ + -1, + "argv", + "aay", + NULL /* annotations */ +}; + +static GDBusArgInfo arg_fds = +{ + -1, + "fds", + "a{uh}", + NULL /* annotations */ +}; + +static GDBusArgInfo arg_envs = +{ + -1, + "envs", + "a{ss}", + NULL /* annotations */ +}; + +static GDBusArgInfo arg_flags = +{ + -1, + "flags", + "u", + NULL /* annotations */ +}; + +static GDBusArgInfo arg_out_pid = +{ + -1, + "pid", + "u", + NULL /* annotations */ +}; + +static GDBusArgInfo *host_command_in_args[] = +{ + &arg_cwd_path, + &arg_argv, + &arg_fds, + &arg_envs, + &arg_flags, + NULL +}; + +static GDBusArgInfo *host_command_out_args[] = +{ + &arg_out_pid, + NULL +}; + +static GDBusMethodInfo host_command_info = +{ + -1, + "HostCommand", + host_command_in_args, + host_command_out_args, + NULL /* annotations */ +}; + +static GDBusArgInfo arg_pid = +{ + -1, + "pid", + "u", + NULL /* annotations */ +}; + +static GDBusArgInfo arg_signal = +{ + -1, + "signal", + "u", + NULL /* annotations */ +}; + +static GDBusArgInfo arg_to_process_group = +{ + -1, + "to_process_group", + "b", + NULL /* annotations */ +}; + +static GDBusArgInfo *host_command_signal_in_args[] = +{ + &arg_pid, + &arg_signal, + &arg_to_process_group, + NULL +}; + +static GDBusMethodInfo host_command_signal_info = +{ + -1, + "HostCommandSignal", + host_command_signal_in_args, + NULL, /* out args */ + NULL /* annotations */ +}; + +static GDBusMethodInfo *development_method_info[] = +{ + &host_command_info, + &host_command_signal_info, + NULL +}; + +static GDBusPropertyInfo version_property_info = +{ + -1, + "version", + "u", + G_DBUS_PROPERTY_INFO_FLAGS_READABLE, + NULL /* annotations */ +}; + +static GDBusPropertyInfo *version_properties_info[] = +{ + &version_property_info, + NULL +}; + +static GDBusInterfaceInfo development_iface_info = +{ + -1, + FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT, + development_method_info, + NULL, /* signals */ + version_properties_info, + NULL /* annotations */ +}; + +static GDBusArgInfo arg_options = +{ + -1, + "options", + "a{sv}", + NULL /* annotations */ +}; + +static GDBusArgInfo *spawn_in_args[] = +{ + &arg_cwd_path, + &arg_argv, + &arg_fds, + &arg_envs, + &arg_flags, + &arg_options, + NULL +}; + +static GDBusArgInfo *spawn_out_args[] = +{ + &arg_out_pid, + NULL +}; + +static GDBusMethodInfo spawn_info = +{ + -1, + "Spawn", + spawn_in_args, + spawn_out_args, + NULL /* annotations */ +}; + +static GDBusMethodInfo spawn_signal_info = +{ + -1, + "SpawnSignal", + host_command_signal_in_args, /* they're the same */ + NULL, /* out args */ + NULL /* annotations */ +}; + +static GDBusMethodInfo *portal_method_info[] = +{ + &spawn_info, + &spawn_signal_info, + NULL +}; + +static GDBusPropertyInfo supports_property_info = +{ + -1, + "supports", + "u", + G_DBUS_PROPERTY_INFO_FLAGS_READABLE, + NULL /* annotations */ +}; + +static GDBusPropertyInfo *portal_properties_info[] = +{ + &version_property_info, + &supports_property_info, + NULL +}; + +static GDBusInterfaceInfo portal_iface_info = +{ + -1, + FLATPAK_PORTAL_INTERFACE, + portal_method_info, + NULL, /* signals */ + portal_properties_info, + NULL /* annotations */ +}; + +static const GDBusInterfaceVTable vtable = +{ + mock_method_call, + mock_get_property, + NULL /* set */ +}; + +static void +setup (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + + if (context == NULL) + f->config = &default_config; + else + f->config = context; + + f->mock_development_version = 1; + f->mock_portal_version = 6; + f->mock_portal_supports = f->config->portal_supports; + + g_queue_init (&f->invocations); + + setup_dbus_daemon (&f->dbus_daemon, &f->dbus_address); + + f->flatpak_spawn_path = g_strdup (g_getenv ("FLATPAK_SPAWN")); + + if (f->flatpak_spawn_path == NULL) + f->flatpak_spawn_path = g_strdup (BINDIR "/flatpak-spawn"); + + f->mock_development_conn = g_dbus_connection_new_for_address_sync (f->dbus_address, + (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), + NULL, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (f->mock_development_conn); + + f->mock_portal_conn = g_dbus_connection_new_for_address_sync (f->dbus_address, + (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), + NULL, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (f->mock_portal_conn); + + f->mock_development_object = g_dbus_connection_register_object (f->mock_development_conn, + FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT, + &development_iface_info, + &vtable, + f, + NULL, + &error); + g_assert_no_error (error); + g_assert_cmpuint (f->mock_development_object, !=, 0); + + f->mock_portal_object = g_dbus_connection_register_object (f->mock_portal_conn, + FLATPAK_PORTAL_PATH, + &portal_iface_info, + &vtable, + f, + NULL, + &error); + g_assert_no_error (error); + g_assert_cmpuint (f->mock_portal_object, !=, 0); + + own_name_sync (f->mock_development_conn, FLATPAK_SESSION_HELPER_BUS_NAME); + own_name_sync (f->mock_portal_conn, FLATPAK_PORTAL_BUS_NAME); +} + +static void +test_help (Fixture *f, + gconstpointer context G_GNUC_UNUSED) +{ + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autofree gchar *stdout_buf; + g_autofree gchar *stderr_buf; + g_autoptr(GError) error = NULL; + + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | + G_SUBPROCESS_FLAGS_STDERR_PIPE); + g_subprocess_launcher_setenv (launcher, + "DBUS_SESSION_BUS_ADDRESS", + f->dbus_address, + TRUE); + + f->flatpak_spawn = g_subprocess_launcher_spawn (launcher, &error, + f->flatpak_spawn_path, + "--help", + NULL); + g_assert_no_error (error); + g_assert_nonnull (f->flatpak_spawn); + + g_subprocess_communicate_utf8 (f->flatpak_spawn, NULL, NULL, &stdout_buf, + &stderr_buf, &error); + g_assert_cmpstr (stderr_buf, ==, ""); + g_assert_nonnull (stdout_buf); + g_test_message ("flatpak-spawn --help: %s", stdout_buf); + g_assert_true (strstr (stdout_buf, "--latest-version") != NULL); + + g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); + g_assert_no_error (error); +} + +static void +test_command (Fixture *f, + gconstpointer context) +{ + const Config *config = context; + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GDBusMethodInvocation) invocation = NULL; + g_autoptr(GPtrArray) command = NULL; + GVariant *parameters; + const char *cwd; + g_autofree const char **argv = NULL; + g_autoptr(GVariant) fds_variant = NULL; + g_autoptr(GVariant) envs_variant = NULL; + guint32 flags; + g_autoptr(GVariant) options_variant = NULL; + GDBusMessage *message; + GUnixFDList *fd_list; + const int *fds; + const char *s; + gsize i; + guint n_fds_for_options = 0; + + g_test_timer_start (); + + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); + g_subprocess_launcher_set_cwd (launcher, "/"); + + if (config->no_session_bus) + g_subprocess_launcher_setenv (launcher, + "DBUS_SESSION_BUS_ADDRESS", + "nope:", + TRUE); + else + g_subprocess_launcher_setenv (launcher, + "DBUS_SESSION_BUS_ADDRESS", + f->dbus_address, + TRUE); + + command = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (command, g_strdup (f->flatpak_spawn_path)); + + if (config->host) + { + g_assert_cmpint (config->subsandbox_flags, ==, 0); + + g_ptr_array_add (command, g_strdup ("--host")); + + if (config->host_flags & FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV) + g_ptr_array_add (command, g_strdup ("--clear-env")); + + if (config->host_flags & FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS) + g_ptr_array_add (command, g_strdup ("--watch-bus")); + } + else + { + g_assert_cmpint (config->host_flags, ==, 0); + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_CLEAR_ENV) + g_ptr_array_add (command, g_strdup ("--clear-env")); + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_LATEST_VERSION) + g_ptr_array_add (command, g_strdup ("--latest-version")); + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_SANDBOX) + { + g_ptr_array_add (command, g_strdup ("--sandbox")); + + if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY) + g_ptr_array_add (command, g_strdup ("--sandbox-flag=share-display")); + + if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND) + g_ptr_array_add (command, g_strdup ("--sandbox-flag=share-sound")); + + if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU) + g_ptr_array_add (command, g_strdup ("--sandbox-flag=share-gpu")); + + if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS) + g_ptr_array_add (command, g_strdup ("--sandbox-flag=allow-dbus")); + + if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y) + g_ptr_array_add (command, g_strdup ("--sandbox-flag=allow-a11y")); + + if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_FUTURE) + g_ptr_array_add (command, g_strdup ("--sandbox-flag=8388608")); + + if (config->sandbox_complex) + { + g_ptr_array_add (command, g_strdup ("--sandbox-expose=/foo")); + g_ptr_array_add (command, g_strdup ("--sandbox-expose=/bar")); + g_ptr_array_add (command, g_strdup ("--sandbox-expose-ro=/proc")); + g_ptr_array_add (command, g_strdup ("--sandbox-expose-ro=/sys")); + g_ptr_array_add (command, g_strdup ("--sandbox-expose-path=/")); + g_ptr_array_add (command, g_strdup ("--sandbox-expose-path-ro=/dev")); + n_fds_for_options += 2; + } + } + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_NO_NETWORK) + g_ptr_array_add (command, g_strdup ("--no-network")); + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_WATCH_BUS) + g_ptr_array_add (command, g_strdup ("--watch-bus")); + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS) + g_ptr_array_add (command, g_strdup ("--expose-pids")); + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_NOTIFY_START) + g_assert_not_reached (); /* TODO: unimplemented */ + + if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_SHARE_PIDS) + g_ptr_array_add (command, g_strdup ("--share-pids")); + } + + if (config->app_path != NULL) + { + g_ptr_array_add (command, g_strdup_printf ("--app-path=%s", config->app_path)); + + if (config->app_path[0] != '\0') + n_fds_for_options++; + } + + if (config->usr_path != NULL) + { + g_ptr_array_add (command, g_strdup_printf ("--usr-path=%s", config->usr_path)); + n_fds_for_options++; + } + + /* Generic "extra complexity" options */ + if (config->extra) + { + g_ptr_array_add (command, g_strdup ("--directory=/dev")); + g_ptr_array_add (command, g_strdup ("--env=FOO=bar")); + g_ptr_array_add (command, g_strdup ("--forward-fd=2")); + g_subprocess_launcher_take_fd (launcher, open ("/dev/null", O_RDWR|O_CLOEXEC), 4); + g_ptr_array_add (command, g_strdup ("--forward-fd=4")); + g_ptr_array_add (command, g_strdup ("--unset-env=NOPE")); + g_ptr_array_add (command, g_strdup ("--verbose")); + } + + if (config->extra_arg != NULL) + g_ptr_array_add (command, g_strdup (config->extra_arg)); + + if (config->awkward_command_name) + g_ptr_array_add (command, g_strdup ("some=command")); + else if (!config->no_command) + g_ptr_array_add (command, g_strdup ("some-command")); + + if (config->extra) + { + g_ptr_array_add (command, g_strdup ("--arg1")); + g_ptr_array_add (command, g_strdup ("arg2")); + } + + g_ptr_array_add (command, NULL); + + f->flatpak_spawn = g_subprocess_launcher_spawnv (launcher, + (const char * const *) command->pdata, + &error); + + g_assert_no_error (error); + g_assert_nonnull (f->flatpak_spawn); + + if (config->fails_immediately) + { + g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); + g_assert_error (error, G_SPAWN_EXIT_ERROR, config->fails_immediately); + + /* Make sure we didn't wait for the entire 25 second D-Bus timeout */ + g_assert_cmpfloat (g_test_timer_elapsed (), <=, 20); + + g_test_minimized_result (g_test_timer_elapsed (), + "time to fail immediately: %.1f", + g_test_timer_elapsed ()); + return; + } + + if (config->fails_after_version_check) + { + g_autoptr(GAsyncResult) result = NULL; + + g_subprocess_wait_check_async (f->flatpak_spawn, NULL, + store_result_cb, &result); + + while (result == NULL) + g_main_context_iteration (NULL, TRUE); + + g_subprocess_wait_check_finish (f->flatpak_spawn, result, &error); + g_assert_error (error, G_SPAWN_EXIT_ERROR, config->fails_after_version_check); + + /* Make sure we didn't wait for the entire 25 second D-Bus timeout */ + g_assert_cmpfloat (g_test_timer_elapsed (), <=, 20); + + g_test_minimized_result (g_test_timer_elapsed (), + "time to fail after version check: %.1f", + g_test_timer_elapsed ()); + + g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 0); + return; + } + + while (g_queue_get_length (&f->invocations) < 1) + g_main_context_iteration (NULL, TRUE); + + g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); + invocation = g_queue_pop_head (&f->invocations); + message = g_dbus_method_invocation_get_message (invocation); + fd_list = g_dbus_message_get_unix_fd_list (message); + fds = g_unix_fd_list_peek_fds (fd_list, NULL); + + if (config->host) + { + g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), + ==, FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT); + g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), + ==, "HostCommand"); + } + else + { + g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), + ==, FLATPAK_PORTAL_INTERFACE); + g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), + ==, "Spawn"); + } + + parameters = g_dbus_method_invocation_get_parameters (invocation); + + if (config->host) + { + g_assert_cmpstr (g_variant_get_type_string (parameters), ==, + "(ayaaya{uh}a{ss}u)"); + g_variant_get (parameters, "(^&ay^a&ay@a{uh}@a{ss}u)", + &cwd, &argv, &fds_variant, &envs_variant, &flags); + } + else + { + g_assert_cmpstr (g_variant_get_type_string (parameters), ==, + "(ayaaya{uh}a{ss}ua{sv})"); + g_variant_get (parameters, "(^&ay^a&ay@a{uh}@a{ss}u@a{sv})", + &cwd, &argv, &fds_variant, &envs_variant, &flags, + &options_variant); + } + + if (config->extra) + g_assert_cmpstr (cwd, ==, "/dev"); + else + g_assert_cmpstr (cwd, ==, "/"); + + g_assert_nonnull (argv); + i = 0; + + if (config->extra && config->host) + { + g_assert_cmpstr (argv[i++], ==, "/usr/bin/env"); + g_assert_cmpstr (argv[i++], ==, "-u"); + g_assert_cmpstr (argv[i++], ==, "NOPE"); + + if (config->awkward_command_name) + { + g_assert_cmpstr (argv[i++], ==, "/bin/sh"); + g_assert_cmpstr (argv[i++], ==, "-euc"); + g_assert_cmpstr (argv[i++], ==, "exec \"$@\""); + g_assert_cmpstr (argv[i++], ==, "sh"); /* sh's argv[0] */ + } + } + + if (config->awkward_command_name) + { + g_assert_cmpstr (argv[i++], ==, "some=command"); + } + else + { + g_assert_cmpstr (argv[i++], ==, "some-command"); + } + + if (config->extra) + { + g_assert_cmpstr (argv[i++], ==, "--arg1"); + g_assert_cmpstr (argv[i++], ==, "arg2"); + } + + g_assert_cmpstr (argv[i++], ==, NULL); + + g_assert_cmpstr (g_variant_get_type_string (fds_variant), ==, "a{uh}"); + + /* it carries stdin, stdout, stderr, and maybe fd 4 */ + if (config->extra) + g_assert_cmpuint (g_variant_n_children (fds_variant), ==, 4); + else + g_assert_cmpuint (g_variant_n_children (fds_variant), ==, 3); + + g_assert_cmpstr (g_variant_get_type_string (envs_variant), ==, "a{ss}"); + + if (config->extra) + { + g_assert_cmpuint (g_variant_n_children (envs_variant), ==, 1); + g_assert_true (g_variant_lookup (envs_variant, "FOO", "&s", &s)); + g_assert_cmpstr (s, ==, "bar"); + } + else + { + g_assert_cmpuint (g_variant_n_children (envs_variant), ==, 0); + } + + if (config->host) + { + g_assert_cmpuint (flags, ==, config->host_flags); + } + else + { + guint options_handled = 0; + + g_assert_cmpuint (flags, ==, config->subsandbox_flags); + g_assert_cmpstr (g_variant_get_type_string (options_variant), ==, "a{sv}"); + + if (config->sandbox_complex) + { + g_autofree const char **expose = NULL; + g_autofree const char **ro = NULL; + GVariantIter *handles_iter; + + g_assert_true (g_variant_lookup (options_variant, "sandbox-expose", "^a&s", &expose)); + g_assert_nonnull (expose); + i = 0; + g_assert_cmpstr (expose[i++], ==, "/foo"); + g_assert_cmpstr (expose[i++], ==, "/bar"); + g_assert_cmpstr (expose[i++], ==, NULL); + options_handled++; + + g_assert_true (g_variant_lookup (options_variant, "sandbox-expose-ro", "^a&s", &ro)); + g_assert_nonnull (ro); + i = 0; + g_assert_cmpstr (ro[i++], ==, "/proc"); + g_assert_cmpstr (ro[i++], ==, "/sys"); + g_assert_cmpstr (ro[i++], ==, NULL); + options_handled++; + + g_assert_true (g_variant_lookup (options_variant, "sandbox-flags", "u", &flags)); + g_assert_cmpuint (flags, ==, config->subsandbox_sandbox_flags); + options_handled++; + + g_assert_true (g_variant_lookup (options_variant, "sandbox-expose-fd", "ah", &handles_iter)); + g_assert_nonnull (handles_iter); + g_variant_iter_free (handles_iter); + options_handled++; + + g_assert_true (g_variant_lookup (options_variant, "sandbox-expose-fd-ro", "ah", &handles_iter)); + g_assert_nonnull (handles_iter); + g_variant_iter_free (handles_iter); + options_handled++; + } + + if (config->extra) + { + g_autofree const char **unset = NULL; + + g_assert_true (g_variant_lookup (options_variant, "unset-env", "^a&s", &unset)); + g_assert_nonnull (unset); + i = 0; + g_assert_cmpstr (unset[i++], ==, "NOPE"); + g_assert_cmpstr (unset[i++], ==, NULL); + options_handled++; + } + + if (config->app_path != NULL && config->app_path[0] != '\0') + { + struct stat expected, got; + gint32 handle; + + g_assert_no_errno (stat (config->app_path, &expected)); + g_assert_true (g_variant_lookup (options_variant, "app-fd", "h", &handle)); + g_assert_cmpint (handle, >=, 0); + g_assert_cmpint (handle, <, g_unix_fd_list_get_length (fd_list)); + g_assert_no_errno (fstat (fds[handle], &got)); + g_assert_cmpint (expected.st_dev, ==, got.st_dev); + g_assert_cmpint (expected.st_ino, ==, got.st_ino); + options_handled++; + } + + if (config->usr_path != NULL) + { + struct stat expected, got; + gint32 handle; + + g_assert_no_errno (stat (config->usr_path, &expected)); + g_assert_true (g_variant_lookup (options_variant, "usr-fd", "h", &handle)); + g_assert_cmpint (handle, >=, 0); + g_assert_cmpint (handle, <, g_unix_fd_list_get_length (fd_list)); + g_assert_no_errno (fstat (fds[handle], &got)); + g_assert_cmpint (expected.st_dev, ==, got.st_dev); + g_assert_cmpint (expected.st_ino, ==, got.st_ino); + options_handled++; + } + + g_assert_cmpuint (g_variant_n_children (options_variant), ==, options_handled); + } + + /* it carries stdin, stdout, stderr, and maybe fd 4 */ + g_assert_cmpuint (g_dbus_message_get_num_unix_fds (message), ==, + g_variant_n_children (fds_variant) + + n_fds_for_options); + for (i = 0; i < g_variant_n_children (fds_variant) + n_fds_for_options; i++) + g_assert_cmpint (fds[i], >=, 0); + g_assert_cmpint (fds[i], ==, -1); + + if (f->config->dbus_call_fails) + { + g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); + g_assert_error (error, G_SPAWN_EXIT_ERROR, 1); + g_clear_error (&error); + g_test_minimized_result (g_test_timer_elapsed (), + "time to fail: %.1f", + g_test_timer_elapsed ()); + return; + } + + if (config->host) + { + if (config->extra) + { + /* Pretend the command was killed by SIGSEGV and dumped core */ + g_dbus_connection_emit_signal (f->mock_development_conn, + NULL, + FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT, + FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT, + "HostCommandExited", + g_variant_new ("(uu)", + 12345, + SIGSEGV | 0x80), + &error); + g_assert_no_error (error); + + g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); + g_assert_error (error, G_SPAWN_EXIT_ERROR, 128 + SIGSEGV); + g_clear_error (&error); + } + else + { + /* Pretend the command exited with status 0 */ + g_dbus_connection_emit_signal (f->mock_development_conn, + NULL, + FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT, + FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT, + "HostCommandExited", + g_variant_new ("(uu)", 12345, 0), + &error); + g_assert_no_error (error); + + g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); + g_assert_no_error (error); + } + } + else + { + if (config->extra) + { + /* Pretend the command exited with status 23 */ + g_dbus_connection_emit_signal (f->mock_portal_conn, + NULL, + FLATPAK_PORTAL_PATH, + FLATPAK_PORTAL_INTERFACE, + "SpawnExited", + g_variant_new ("(uu)", 12345, (23 << 8)), + &error); + g_assert_no_error (error); + + g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); + g_assert_error (error, G_SPAWN_EXIT_ERROR, 23); + g_clear_error (&error); + } + else + { + /* Pretend the command exited with status 0 */ + g_dbus_connection_emit_signal (f->mock_portal_conn, + NULL, + FLATPAK_PORTAL_PATH, + FLATPAK_PORTAL_INTERFACE, + "SpawnExited", + g_variant_new ("(uu)", 12345, 0), + &error); + g_assert_no_error (error); + + g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); + g_assert_no_error (error); + } + } + + g_test_minimized_result (g_test_timer_elapsed (), + "time to succeed: %.1f", + g_test_timer_elapsed ()); +} + +static void +teardown (Fixture *f, + gconstpointer context G_GNUC_UNUSED) +{ + g_autoptr(GError) error = NULL; + gpointer free_me; + + for (free_me = g_queue_pop_head (&f->invocations); + free_me != NULL; + free_me = g_queue_pop_head (&f->invocations)) + g_object_unref (free_me); + + if (f->mock_development_object != 0) + g_dbus_connection_unregister_object (f->mock_development_conn, + f->mock_development_object); + + if (f->mock_portal_object != 0) + g_dbus_connection_unregister_object (f->mock_portal_conn, + f->mock_portal_object); + + if (f->dbus_daemon != NULL) + { + g_subprocess_send_signal (f->dbus_daemon, SIGTERM); + g_subprocess_wait (f->dbus_daemon, NULL, &error); + g_assert_no_error (error); + } + + g_clear_object (&f->dbus_daemon); + g_clear_object (&f->flatpak_spawn); + g_clear_object (&f->mock_development_conn); + g_clear_object (&f->mock_portal_conn); + g_free (f->dbus_address); + g_free (f->flatpak_spawn_path); + alarm (0); +} + +static const Config subsandbox_fails = +{ + .dbus_call_fails = TRUE, +}; + +static const Config subsandbox_complex = +{ + .awkward_command_name = TRUE, + .extra = TRUE, + /* This is obviously not a realistic thing to put at /app, but it needs + * to be something that will certainly exist on the host system! */ + .app_path = "/dev", + /* Similar */ + .usr_path = "/", +}; + +static const Config subsandbox_empty_app = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_EMPTY_APP, + .app_path = "", +}; + +static const Config subsandbox_clear_env = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_CLEAR_ENV, +}; + +static const Config subsandbox_latest = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_LATEST_VERSION, +}; + +static const Config subsandbox_no_net = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_NO_NETWORK, +}; + +static const Config subsandbox_watch_bus = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_WATCH_BUS, +}; + +static const Config subsandbox_expose_pids = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS, + .portal_supports = FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS, +}; + +static const Config subsandbox_share_pids = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_SHARE_PIDS, + .portal_supports = FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS, +}; + +static const Config subsandbox_sandbox_simple = +{ + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_SANDBOX, +}; + +static const Config subsandbox_sandbox_complex = +{ + .sandbox_complex = TRUE, + .subsandbox_flags = FLATPAK_SPAWN_FLAGS_SANDBOX, + /* TODO: Exercise these separately */ + .subsandbox_sandbox_flags = (FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY | + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND | + FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU | + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS | + FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y | + FLATPAK_SPAWN_SANDBOX_FLAGS_FUTURE), +}; + +static const Config host_simple = +{ + .host = TRUE, +}; + +static const Config host_fails = +{ + .dbus_call_fails = TRUE, + .host = TRUE, +}; + +static const Config host_complex1 = +{ + .extra = TRUE, + .host = TRUE, + .host_flags = FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV, +}; + +static const Config host_complex2 = +{ + .awkward_command_name = TRUE, + .extra = TRUE, + .host = TRUE, + .host_flags = FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS, +}; + +static const Config fail_invalid_env = +{ + .fails_immediately = 1, + .extra_arg = "--env=", +}; + +static const Config fail_invalid_env2 = +{ + .fails_immediately = 1, + .extra_arg = "--env=NOPE", +}; + +static const Config fail_invalid_fd = +{ + .fails_immediately = 1, + .extra_arg = "--forward-fd=", +}; + +static const Config fail_invalid_fd2 = +{ + .fails_immediately = 1, + .extra_arg = "--forward-fd=yesplease", +}; + +/* TODO: These should fail, but succeed */ +#if 0 +static const Config fail_invalid_fd3 = +{ + .fails_immediately = 1, + .extra_arg = "--forward-fd=1a", +}; + +static const Config fail_invalid_fd4 = +{ + .fails_immediately = 1, + .extra_arg = "--forward-fd=-1", +}; +#endif + +static const Config fail_invalid_sandbox_flag = +{ + .fails_immediately = 1, + .extra_arg = "--sandbox-flag=tricolore", +}; + +static const Config fail_invalid_sandbox_flag2 = +{ + .fails_immediately = 1, + .extra_arg = "--sandbox-flag=1e6", +}; + +static const Config fail_no_command = +{ + .fails_immediately = 1, + .no_command = TRUE, +}; + +static const Config fail_no_session_bus = +{ + .fails_immediately = 1, + .no_session_bus = TRUE, +}; + +static const Config fail_no_usr_path = +{ + .fails_after_version_check = 1, + .usr_path = "", +}; + +static const Config fail_nonexistent_app_path = +{ + .fails_after_version_check = 1, + .app_path = "/nonexistent", +}; + +static const Config fail_nonexistent_usr_path = +{ + .fails_after_version_check = 1, + .usr_path = "/nonexistent", +}; + +static const Config host_cannot[] = +{ + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--expose-pids" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--latest-version" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--no-network" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose=/" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose-path=/" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose-path-ro=/" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose-ro=/" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-flag=1" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--share-pids" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--app-path=" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--app-path=/" }, + { .fails_immediately = 1, .host = TRUE, .extra_arg = "--usr-path=/" }, +}; + +int +main (int argc, + char **argv) +{ + gsize i; + + g_test_init (&argc, &argv, NULL); + + g_test_add ("/help", Fixture, NULL, setup, test_help, teardown); + + g_test_add ("/host/simple", Fixture, &host_simple, setup, test_command, teardown); + g_test_add ("/host/complex1", Fixture, &host_complex1, setup, test_command, teardown); + g_test_add ("/host/complex2", Fixture, &host_complex2, setup, test_command, teardown); + g_test_add ("/host/fails", Fixture, &host_fails, setup, test_command, teardown); + + g_test_add ("/subsandbox/simple", Fixture, &default_config, setup, test_command, teardown); + g_test_add ("/subsandbox/clear-env", Fixture, &subsandbox_clear_env, setup, test_command, teardown); + g_test_add ("/subsandbox/complex", Fixture, &subsandbox_complex, setup, test_command, teardown); + g_test_add ("/subsandbox/empty_app", Fixture, &subsandbox_empty_app, setup, test_command, teardown); + g_test_add ("/subsandbox/expose-pids", Fixture, &subsandbox_expose_pids, setup, test_command, teardown); + g_test_add ("/subsandbox/fails", Fixture, &subsandbox_fails, setup, test_command, teardown); + g_test_add ("/subsandbox/latest", Fixture, &subsandbox_latest, setup, test_command, teardown); + g_test_add ("/subsandbox/no-net", Fixture, &subsandbox_no_net, setup, test_command, teardown); + g_test_add ("/subsandbox/sandbox/simple", Fixture, &subsandbox_sandbox_simple, setup, test_command, teardown); + g_test_add ("/subsandbox/sandbox/complex", Fixture, &subsandbox_sandbox_complex, setup, test_command, teardown); + g_test_add ("/subsandbox/share-pids", Fixture, &subsandbox_share_pids, setup, test_command, teardown); + g_test_add ("/subsandbox/watch-bus", Fixture, &subsandbox_watch_bus, setup, test_command, teardown); + + g_test_add ("/fail/invalid-env", Fixture, &fail_invalid_env, setup, test_command, teardown); + g_test_add ("/fail/invalid-env2", Fixture, &fail_invalid_env2, setup, test_command, teardown); + g_test_add ("/fail/invalid-fd", Fixture, &fail_invalid_fd, setup, test_command, teardown); + g_test_add ("/fail/invalid-fd2", Fixture, &fail_invalid_fd2, setup, test_command, teardown); + g_test_add ("/fail/invalid-sandbox-flag", Fixture, &fail_invalid_sandbox_flag, setup, test_command, teardown); + g_test_add ("/fail/invalid-sandbox-flag2", Fixture, &fail_invalid_sandbox_flag2, setup, test_command, teardown); + g_test_add ("/fail/no-command", Fixture, &fail_no_command, setup, test_command, teardown); + g_test_add ("/fail/no-session-bus", Fixture, &fail_no_session_bus, setup, test_command, teardown); + g_test_add ("/fail/no-usr-path", Fixture, &fail_no_usr_path, setup, test_command, teardown); + g_test_add ("/fail/nonexistent-app-path", Fixture, &fail_nonexistent_app_path, setup, test_command, teardown); + g_test_add ("/fail/nonexistent-usr-path", Fixture, &fail_nonexistent_usr_path, setup, test_command, teardown); + + for (i = 0; i < G_N_ELEMENTS (host_cannot); i++) + { + g_autofree gchar *name = g_strdup_printf ("/fail/host-cannot/%s", + host_cannot[i].extra_arg); + + g_strdelimit (name + strlen ("/fail/host-cannot/"), "/", 'x'); + + g_test_add (name, Fixture, &host_cannot[i], setup, test_command, teardown); + } + + return g_test_run (); +}