diff --git a/README.md b/README.md index 5ad22fcadd01cdfd4cf9250fdf194a9ccaae00a9..14670054f8b55c1f6dd42304c819f2a07dc6c1dd 100644 --- a/README.md +++ b/README.md @@ -446,6 +446,32 @@ configured. The interactive shell's current working directory matches the game's. As with the xterm, run `"$@"` in the interactive shell to run the game. +* To get a modifiable copy of the runtime, use something like: + + mkdir -p $HOME/.steam/steam/steamapps/common/SteamLinuxRuntime/var + /opt/pressure-vessel/bin/pressure-vessel-unruntime \ + --runtime=$HOME/.steam/steam/steamapps/common/SteamLinuxRuntime/scout/files \ + --copy-runtime-into=$HOME/.steam/steam/steamapps/common/SteamLinuxRuntime/var \ + -- %command% + + You will find a temporary copy of the runtime in the directory you + chose, in this example something like + `.../SteamLinuxRuntime/var/tmp-1234567`. You can modify it in-place. + For example, if you want to replace `libdbus-1.so.3` with an + instrumented version, you can do something like this: + + cd .../SteamLinuxRuntime/var/tmp-1234567 + rm -f overrides/lib/x86_64-linux-gnu/libdbus-1.so.3* + rm -f usr/lib/x86_64-linux-gnu/libdbus-1.so.3* + cp --dereference ~/dbus/dbus/.libs/libdbus-1.so.3 \ + overrides/lib/x86_64-linux-gnu/libdbus-1.so.3 + + To avoid temporary copies building up forever, they will be deleted + *next* time you run a game in a container (assuming there is no longer + anything running in the previous container), unless you create a file + like `.../var/tmp-1234567/keep` to flag this root directory to be kept + for future reference. + ### More options Use `pressure-vessel-unruntime` or `pressure-vessel-unruntime-test-ui` diff --git a/src/locking.md b/src/locking.md new file mode 100644 index 0000000000000000000000000000000000000000..adbd4caa8f3d9b608d4571b01aa65fe439a345a8 --- /dev/null +++ b/src/locking.md @@ -0,0 +1,55 @@ +pressure-vessel file locking model +================================== + +pressure-vessel uses fcntl locks to guard concurrent access to runtimes. +They are `F_OFD_SETLK` locks if possible, falling back to POSIX +process-oriented `F_SETLK` locks on older kernels. See `src/bwrap-lock.c` +for full details. + +Do not use `flock(2)`, `flock(1)` or `lockf(3)` to interact with +pressure-vessel. `bwrap --lock-file` or `pressure-vessel-with-lock` +can be used. + +Runtimes +-------- + +When pressure-vessel-wrap is told to use a runtime, for example +`scout/files`, it takes out a shared (read) lock on the file `.ref` +at top level, for example `scout/files/.ref`. + +If told to make a mutable copy of the runtime, this lock is only held +for as long as it takes to carry out setup. There is a separate lock on +the mutable copy (see below). + +If the runtime is used directly, the lock file may appear as either +`/usr/.ref` or `/.ref` inside the container. If the runtime is a merged +/usr rather than a complete root filesystem, the lock file will be +`/usr/.ref` with a symbolic link `/.ref -> usr/.ref`. If the runtime is +a complete root filesystem, the lock file will be `/.ref`. Processes +inside the container wishing to take out the lock must use `/.ref`. + +Processes wishing to clean up obsolete runtimes should take out an +exclusive (write) lock on the runtime's `/.ref`, for example +`scout/files/.ref`, to prevent deletion of a runtime that is in use. + +Mutable copies +-------------- + +pressure-vessel-wrap can be told to make a mutable copy of the runtime +with `--runtime-mutable-parent=${parent}`. + +While setting this up, it takes out a shared (read) lock on +`${parent}/.ref`. During setup, it additionally takes out a shared (read) +lock on `${parent}/tmp-${random}/.ref`, which continues to be held after +setup has finished and the lock on the parent directory has been released. +(Implementation detail: that's actually a symbolic link to `usr/.ref`.) + +Processes wishing to clean up the mutable copies must take an +exclusive (write) lock on `${parent}/.ref`, to prevent +pressure-vessel-wrap from creating new mutable copies during cleanup. + +A process that is holding the lock on the parent may detect whether each +mutable runtime `${parent}/tmp-${random}/` is in use by attempting to +take out an exclusive (write) lock on `${parent}/tmp-${random}/.ref` +(to avoid deadlocks, this is best done in non-blocking mode). If it +successfully takes the lock, it may delete the runtime. diff --git a/src/runtime.c b/src/runtime.c index 71a62fc7b4f8c210bb1006f7f3184f8c3241a0a6..cd61d80e0a835067de4f2718aad83384f08eaaa2 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -32,8 +32,10 @@ #include "bwrap.h" #include "bwrap-lock.h" +#include "elf-utils.h" #include "enumtypes.h" #include "flatpak-run-private.h" +#include "resolve-in-sysroot.h" #include "utils.h" /* @@ -51,14 +53,20 @@ struct _PvRuntime gchar *tools_dir; PvBwrapLock *runtime_lock; + gchar *mutable_parent; + gchar *mutable_sysroot; gchar *tmpdir; gchar *overrides; - gchar *overrides_bin; + const gchar *overrides_in_container; gchar *container_access; FlatpakBwrap *container_access_adverb; - gchar *runtime_usr; /* either source_files or that + "/usr" */ + const gchar *runtime_files; /* either source_files or mutable_sysroot */ + gchar *runtime_usr; /* either runtime_files or that + "/usr" */ + const gchar *with_lock_in_container; PvRuntimeFlags flags; + int mutable_parent_fd; + int mutable_sysroot_fd; gboolean any_libc_from_host; gboolean all_libc_from_host; gboolean runtime_is_just_usr; @@ -73,6 +81,7 @@ enum { PROP_0, PROP_BUBBLEWRAP, PROP_FLAGS, + PROP_MUTABLE_PARENT, PROP_SOURCE_FILES, PROP_TOOLS_DIRECTORY, N_PROPERTIES @@ -87,6 +96,38 @@ G_DEFINE_TYPE_WITH_CODE (PvRuntime, pv_runtime, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, pv_runtime_initable_iface_init)) +/* + * Return whether @path is likely to be visible in /run/host. + * This needs to be kept approximately in sync with pv_bwrap_bind_usr() + * and Flatpak's --filesystem=host-os special keyword. + * + * This doesn't currently handle /etc: we make the pessimistic assumption + * that /etc/ld.so.cache, etc., are not shared. + */ +static gboolean +path_visible_in_run_host (const char *path) +{ + while (path[0] == '/') + path++; + + if (g_str_has_prefix (path, "usr") && + (path[3] == '\0' || path[3] == '/')) + return TRUE; + + if (g_str_has_prefix (path, "lib")) + return TRUE; + + if (g_str_has_prefix (path, "bin") && + (path[3] == '\0' || path[3] == '/')) + return TRUE; + + if (g_str_has_prefix (path, "sbin") || + (path[4] == '\0' || path[4] == '/')) + return TRUE; + + return FALSE; +} + /* * Supported Debian-style multiarch tuples */ @@ -143,8 +184,8 @@ runtime_architecture_init (RuntimeArchitecture *self, NULL); self->libdir_on_host = g_build_filename (runtime->overrides, "lib", self->tuple, NULL); - self->libdir_in_container = g_build_filename ("/overrides", "lib", - self->tuple, NULL); + self->libdir_in_container = g_build_filename (runtime->overrides_in_container, + "lib", self->tuple, NULL); /* This has the side-effect of testing whether we can run binaries * for this architecture on the host system. */ @@ -201,6 +242,8 @@ pv_runtime_init (PvRuntime *self) { self->any_libc_from_host = FALSE; self->all_libc_from_host = FALSE; + self->mutable_parent_fd = -1; + self->mutable_sysroot_fd = -1; } static void @@ -221,6 +264,10 @@ pv_runtime_get_property (GObject *object, g_value_set_flags (value, self->flags); break; + case PROP_MUTABLE_PARENT: + g_value_set_string (value, self->mutable_parent); + break; + case PROP_SOURCE_FILES: g_value_set_string (value, self->source_files); break; @@ -241,6 +288,7 @@ pv_runtime_set_property (GObject *object, GParamSpec *pspec) { PvRuntime *self = PV_RUNTIME (object); + const char *path; switch (prop_id) { @@ -254,10 +302,42 @@ pv_runtime_set_property (GObject *object, self->flags = g_value_get_flags (value); break; + case PROP_MUTABLE_PARENT: + /* Construct-only */ + g_return_if_fail (self->mutable_parent == NULL); + path = g_value_get_string (value); + + if (path != NULL) + { + self->mutable_parent = realpath (path, NULL); + + if (self->mutable_parent == NULL) + { + /* It doesn't exist. Keep the non-canonical path so we + * can warn about it later */ + self->mutable_parent = g_strdup (path); + } + } + + break; + case PROP_SOURCE_FILES: /* Construct-only */ g_return_if_fail (self->source_files == NULL); - self->source_files = g_value_dup_string (value); + path = g_value_get_string (value); + + if (path != NULL) + { + self->source_files = realpath (path, NULL); + + if (self->source_files == NULL) + { + /* It doesn't exist. Keep the non-canonical path so we + * can warn about it later */ + self->source_files = g_strdup (path); + } + } + break; case PROP_TOOLS_DIRECTORY: @@ -283,6 +363,292 @@ pv_runtime_constructed (GObject *object) g_return_if_fail (self->tools_dir != NULL); } +static gboolean +pv_runtime_garbage_collect (PvRuntime *self, + PvBwrapLock *mutable_parent_lock, + GError **error) +{ + g_auto(GLnxDirFdIterator) iter = { FALSE }; + + g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE); + g_return_val_if_fail (self->mutable_parent != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + /* We don't actually *use* this: it just acts as an assertion that + * we are holding the lock on the parent directory. */ + g_return_val_if_fail (mutable_parent_lock != NULL, FALSE); + + if (!glnx_dirfd_iterator_init_at (AT_FDCWD, self->mutable_parent, + TRUE, &iter, error)) + return FALSE; + + while (TRUE) + { + g_autoptr(GError) local_error = NULL; + g_autoptr(PvBwrapLock) temp_lock = NULL; + g_autofree gchar *keep = NULL; + g_autofree gchar *ref = NULL; + struct dirent *dent; + struct stat ignore; + + if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iter, &dent, + NULL, error)) + return FALSE; + + if (dent == NULL) + break; + + switch (dent->d_type) + { + case DT_DIR: + break; + + case DT_BLK: + case DT_CHR: + case DT_FIFO: + case DT_LNK: + case DT_REG: + case DT_SOCK: + case DT_UNKNOWN: + default: + g_debug ("Ignoring %s/%s: not a directory", + self->mutable_parent, dent->d_name); + continue; + } + + if (!g_str_has_prefix (dent->d_name, "tmp-")) + { + g_debug ("Ignoring %s/%s: not tmp-*", + self->mutable_parent, dent->d_name); + continue; + } + + g_debug ("Found temporary runtime %s/%s, considering whether to " + "delete it...", + self->mutable_parent, dent->d_name); + + keep = g_build_filename (dent->d_name, "keep", NULL); + + if (glnx_fstatat (self->mutable_parent_fd, keep, &ignore, + AT_SYMLINK_NOFOLLOW, &local_error)) + { + g_debug ("Not deleting \"%s/%s\": ./keep exists", + self->mutable_parent, dent->d_name); + continue; + } + else if (!g_error_matches (local_error, G_IO_ERROR, + G_IO_ERROR_NOT_FOUND)) + { + /* EACCES or something? Give it the benefit of the doubt */ + g_warning ("Not deleting \"%s/%s\": unable to stat ./keep: %s", + self->mutable_parent, dent->d_name, local_error->message); + g_clear_error (&local_error); + continue; + } + + g_clear_error (&local_error); + + ref = g_build_filename (dent->d_name, ".ref", NULL); + temp_lock = pv_bwrap_lock_new (self->mutable_parent_fd, ref, + (PV_BWRAP_LOCK_FLAGS_CREATE | + PV_BWRAP_LOCK_FLAGS_WRITE), + &local_error); + + if (temp_lock == NULL) + { + g_debug ("Ignoring \"%s/%s\": unable to get lock: %s", + self->mutable_parent, dent->d_name, + local_error->message); + g_clear_error (&local_error); + continue; + } + + g_debug ("Deleting \"%s/%s\"...", self->mutable_parent, dent->d_name); + + /* We have the lock, which would not have happened if someone was + * still using the runtime, so we can safely delete it. */ + if (!glnx_shutil_rm_rf_at (self->mutable_parent_fd, dent->d_name, + NULL, &local_error)) + { + g_debug ("Unable to delete %s/%s: %s", + self->mutable_parent, dent->d_name, local_error->message); + g_clear_error (&local_error); + continue; + } + } + + return TRUE; +} + +static gboolean +pv_runtime_init_mutable (PvRuntime *self, + GError **error) +{ + g_autofree gchar *dest_usr = NULL; + g_autofree gchar *source_usr_subdir = NULL; + g_autofree gchar *temp_dir = NULL; + g_autoptr(GDir) dir = NULL; + g_autoptr(PvBwrapLock) copy_lock = NULL; + g_autoptr(PvBwrapLock) mutable_lock = NULL; + g_autoptr(PvBwrapLock) source_lock = NULL; + const char *member; + const char *source_usr; + glnx_autofd int temp_dir_fd = -1; + gboolean is_just_usr; + + /* Nothing to do in this case */ + if (self->mutable_parent == NULL) + return TRUE; + + if (g_mkdir_with_parents (self->mutable_parent, 0700) != 0) + return glnx_throw_errno_prefix (error, "Unable to create %s", + self->mutable_parent); + + if (!glnx_opendirat (AT_FDCWD, self->mutable_parent, TRUE, + &self->mutable_parent_fd, error)) + return FALSE; + + /* Lock the parent directory. Anything that directly manipulates the + * temporary runtimes is expected to do the same, so that + * it cannot be deleting temporary runtimes at the same time we're + * creating them. + * + * This is a read-mode lock: it's OK to create more than one temporary + * runtime in parallel, as long as nothing is deleting them + * concurrently. */ + mutable_lock = pv_bwrap_lock_new (self->mutable_parent_fd, ".ref", + PV_BWRAP_LOCK_FLAGS_CREATE, + error); + + if (mutable_lock == NULL) + return glnx_prefix_error (error, "Unable to lock \"%s/%s\"", + self->mutable_parent, ".ref"); + + /* GC old runtimes (if they have become unused) before we create a + * new one. This means we should only ever have one temporary runtime + * copy per game that is run concurrently. */ + if ((self->flags & PV_RUNTIME_FLAGS_GC_RUNTIMES) != 0 && + !pv_runtime_garbage_collect (self, mutable_lock, error)) + return FALSE; + + temp_dir = g_build_filename (self->mutable_parent, "tmp-XXXXXX", NULL); + + if (g_mkdtemp (temp_dir) == NULL) + return glnx_throw_errno_prefix (error, + "Cannot create temporary directory \"%s\"", + temp_dir); + + source_usr_subdir = g_build_filename (self->source_files, "usr", NULL); + dest_usr = g_build_filename (temp_dir, "usr", NULL); + + is_just_usr = !g_file_test (source_usr_subdir, G_FILE_TEST_IS_DIR); + + if (is_just_usr) + { + /* ${source_files}/usr does not exist, so assume it's a merged /usr, + * for example ./scout/files. Copy ${source_files}/bin to + * ${temp_dir}/usr/bin, etc. */ + source_usr = self->source_files; + + if (!pv_cheap_tree_copy (self->source_files, dest_usr, error)) + return FALSE; + } + else + { + /* ${source_files}/usr exists, so assume it's a complete sysroot. + * Copy ${source_files}/bin to ${temp_dir}/bin, etc. */ + source_usr = source_usr_subdir; + + if (!pv_cheap_tree_copy (self->source_files, temp_dir, error)) + return FALSE; + } + + if (!glnx_opendirat (-1, temp_dir, FALSE, &temp_dir_fd, error)) + return FALSE; + + /* Create the copy in a pre-locked state. After the lock on the parent + * directory is released, the copy continues to have a read lock, + * preventing it from being modified or deleted while in use (even if + * a cleanup process successfully obtains a write lock on the parent). + * + * Because we control the structure of the runtime in this case, we + * actually lock /usr/.ref instead of /.ref, and ensure that /.ref + * is a symlink to it. This might become important if we pass the + * runtime's /usr to Flatpak, which normally takes out a lock on + * /usr/.ref (obviously this will only work if the runtime happens + * to be merged-/usr). */ + copy_lock = pv_bwrap_lock_new (temp_dir_fd, "usr/.ref", + PV_BWRAP_LOCK_FLAGS_CREATE, + error); + + if (copy_lock == NULL) + return glnx_prefix_error (error, + "Unable to lock \"%s/.ref\" in temporary runtime", + dest_usr); + + if (is_just_usr) + { + g_autofree gchar *in_root = g_build_filename (temp_dir, ".ref", NULL); + + if (unlink (in_root) != 0 && errno != ENOENT) + return glnx_throw_errno_prefix (error, + "Cannot remove \"%s\"", in_root); + + if (symlink ("usr/.ref", in_root) != 0) + return glnx_throw_errno_prefix (error, + "Cannot create symlink \"%s\" -> usr/.ref", + in_root); + } + + dir = g_dir_open (source_usr, 0, error); + + if (dir == NULL) + return FALSE; + + for (member = g_dir_read_name (dir); + member != NULL; + member = g_dir_read_name (dir)) + { + /* Create symlinks ${temp_dir}/bin -> usr/bin, etc. if missing. + * + * Also make ${temp_dir}/etc, ${temp_dir}/var symlinks to etc + * and var, for the benefit of tools like capsule-capture-libs + * accessing /etc/ld.so.cache in the incomplete container (for the + * final container command-line they get merged by bind_runtime() + * instead). */ + if (g_str_equal (member, "bin") || + g_str_equal (member, "etc") || + (g_str_has_prefix (member, "lib") && + !g_str_equal (member, "libexec")) || + g_str_equal (member, "sbin") || + g_str_equal (member, "var")) + { + g_autofree gchar *dest = g_build_filename (temp_dir, member, NULL); + g_autofree gchar *target = g_build_filename ("usr", member, NULL); + + if (symlink (target, dest) != 0) + { + /* Ignore EEXIST in the case where it was not just /usr: + * it's fine if the runtime we copied from source_files + * already had either directories or symlinks in its root + * directory */ + if (is_just_usr || errno != EEXIST) + return glnx_throw_errno_prefix (error, + "Cannot create symlink \"%s\" -> %s", + dest, target); + } + } + } + + /* Hand over from holding a lock on the source to just holding a lock + * on the copy. We'll release source_lock when we leave this scope */ + source_lock = g_steal_pointer (&self->runtime_lock); + self->runtime_lock = g_steal_pointer (©_lock); + self->mutable_sysroot = g_steal_pointer (&temp_dir); + self->mutable_sysroot_fd = glnx_steal_fd (&temp_dir_fd); + + return TRUE; +} + static gboolean pv_runtime_initable_init (GInitable *initable, GCancellable *cancellable G_GNUC_UNUSED, @@ -300,6 +666,13 @@ pv_runtime_initable_init (GInitable *initable, self->bubblewrap); } + if (self->mutable_parent != NULL + && !g_file_test (self->mutable_parent, G_FILE_TEST_IS_DIR)) + { + return glnx_throw (error, "\"%s\" is not a directory", + self->mutable_parent); + } + if (!g_file_test (self->source_files, G_FILE_TEST_IS_DIR)) { return glnx_throw (error, "\"%s\" is not a directory", @@ -313,7 +686,12 @@ pv_runtime_initable_init (GInitable *initable, } /* Take a lock on the runtime until we're finished with setup, - * to make sure it doesn't get deleted. */ + * to make sure it doesn't get deleted. + * + * If the runtime is mounted read-only in the container, it will + * continue to be locked until all processes in the container exit. + * If we make a temporary mutable copy, we only hold this lock until + * setup has finished. */ files_ref = g_build_filename (self->source_files, ".ref", NULL); self->runtime_lock = pv_bwrap_lock_new (AT_FDCWD, files_ref, PV_BWRAP_LOCK_FLAGS_CREATE, @@ -323,20 +701,33 @@ pv_runtime_initable_init (GInitable *initable, if (self->runtime_lock == NULL) return FALSE; - g_debug ("Creating temporary directories..."); + if (!pv_runtime_init_mutable (self, error)) + return FALSE; - /* Using a runtime requires a temporary directory */ - self->tmpdir = g_dir_make_tmp ("pressure-vessel-wrap.XXXXXX", error); + if (self->mutable_sysroot != NULL) + { + self->overrides_in_container = "/usr/lib/pressure-vessel/overrides"; + self->overrides = g_build_filename (self->mutable_sysroot, + self->overrides_in_container, NULL); + self->runtime_files = self->mutable_sysroot; + } + else + { + /* We currently only need a temporary directory if we don't have + * a mutable sysroot to work with. */ + self->tmpdir = g_dir_make_tmp ("pressure-vessel-wrap.XXXXXX", error); - if (self->tmpdir == NULL) - return FALSE; + if (self->tmpdir == NULL) + return FALSE; + + self->overrides = g_build_filename (self->tmpdir, "overrides", NULL); + self->overrides_in_container = "/overrides"; + self->runtime_files = self->source_files; + } - self->overrides = g_build_filename (self->tmpdir, "overrides", NULL); g_mkdir (self->overrides, 0700); - self->overrides_bin = g_build_filename (self->overrides, "bin", NULL); - g_mkdir (self->overrides_bin, 0700); - self->runtime_usr = g_build_filename (self->source_files, "usr", NULL); + self->runtime_usr = g_build_filename (self->runtime_files, "usr", NULL); if (g_file_test (self->runtime_usr, G_FILE_TEST_IS_DIR)) { @@ -344,10 +735,10 @@ pv_runtime_initable_init (GInitable *initable, } else { - /* source_files is just a merged /usr. */ + /* runtime_files is just a merged /usr. */ self->runtime_is_just_usr = TRUE; g_free (self->runtime_usr); - self->runtime_usr = g_strdup (self->source_files); + self->runtime_usr = g_strdup (self->runtime_files); } return TRUE; @@ -367,7 +758,6 @@ pv_runtime_cleanup (PvRuntime *self) local_error->message); } - g_clear_pointer (&self->overrides_bin, g_free); g_clear_pointer (&self->overrides, g_free); g_clear_pointer (&self->container_access, g_free); g_clear_pointer (&self->container_access_adverb, flatpak_bwrap_free); @@ -381,6 +771,10 @@ pv_runtime_finalize (GObject *object) pv_runtime_cleanup (self); g_free (self->bubblewrap); + glnx_close_fd (&self->mutable_parent_fd); + g_free (self->mutable_parent); + glnx_close_fd (&self->mutable_sysroot_fd); + g_free (self->mutable_sysroot); g_free (self->runtime_usr); g_free (self->source_files); g_free (self->tools_dir); @@ -415,6 +809,14 @@ pv_runtime_class_init (PvRuntimeClass *cls) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + properties[PROP_MUTABLE_PARENT] = + g_param_spec_string ("mutable-parent", "Mutable parent", + ("Path to a directory in which to create a " + "mutable copy of source-files, or NULL"), + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + properties[PROP_SOURCE_FILES] = g_param_spec_string ("source-files", "Source files", ("Path to read-only runtime files (merged-/usr " @@ -435,6 +837,7 @@ pv_runtime_class_init (PvRuntimeClass *cls) PvRuntime * pv_runtime_new (const char *source_files, + const char *mutable_parent, const char *bubblewrap, const char *tools_dir, PvRuntimeFlags flags, @@ -449,6 +852,7 @@ pv_runtime_new (const char *source_files, NULL, error, "bubblewrap", bubblewrap, + "mutable-parent", mutable_parent, "source-files", source_files, "tools-directory", tools_dir, "flags", flags, @@ -467,9 +871,11 @@ pv_runtime_append_lock_adverb (PvRuntime *self, { g_return_if_fail (PV_IS_RUNTIME (self)); g_return_if_fail (!pv_bwrap_was_finished (bwrap)); + /* This will be true if pv_runtime_bind() was successfully called. */ + g_return_if_fail (self->with_lock_in_container != NULL); flatpak_bwrap_add_args (bwrap, - "/run/pressure-vessel/bin/pressure-vessel-with-lock", + self->with_lock_in_container, "--subreaper", NULL); @@ -541,12 +947,11 @@ pv_runtime_provide_container_access (PvRuntime *self, * shape" that the final system is going to be. * * In particular, if we are working with a writeable copy of a runtime - * that we are editing in-place, we can arrange that it's always - * like that. */ + * that we are editing in-place, it's always like that. */ g_debug ("%s: Setting up runtime without using bwrap", G_STRFUNC); self->container_access_adverb = flatpak_bwrap_new (NULL); - self->container_access = g_strdup (self->source_files); + self->container_access = g_strdup (self->runtime_files); /* This is going to go poorly for us if the runtime is not complete. * !self->runtime_is_just_usr means we know it has a /usr subdirectory, @@ -563,7 +968,7 @@ pv_runtime_provide_container_access (PvRuntime *self, * so we don't check for it. */ for (i = 0; i < G_N_ELEMENTS (need_top_level); i++) { - g_autofree gchar *path = g_build_filename (self->source_files, + g_autofree gchar *path = g_build_filename (self->runtime_files, need_top_level[i], NULL); @@ -579,6 +984,11 @@ pv_runtime_provide_container_access (PvRuntime *self, g_debug ("%s: Using bwrap to set up runtime that is just /usr", G_STRFUNC); + /* By design, writeable copies of the runtime never need this: + * the writeable copy is a complete sysroot, not just a merged /usr. */ + g_assert (self->mutable_sysroot == NULL); + g_assert (self->tmpdir != NULL); + self->container_access = g_build_filename (self->tmpdir, "mnt", NULL); g_mkdir (self->container_access, 0700); @@ -590,7 +1000,7 @@ pv_runtime_provide_container_access (PvRuntime *self, "--tmpfs", self->container_access, NULL); if (!pv_bwrap_bind_usr (self->container_access_adverb, - self->source_files, + self->runtime_files, self->container_access, error)) return FALSE; @@ -715,6 +1125,8 @@ ensure_locales (PvRuntime *self, g_autoptr(FlatpakBwrap) run_locale_gen = NULL; g_autofree gchar *locale_gen = NULL; g_autofree gchar *locales = g_build_filename (self->overrides, "locales", NULL); + g_autofree gchar *locales_in_container = g_build_filename (self->overrides_in_container, + "locales", NULL); g_autoptr(GDir) dir = NULL; int exit_status; @@ -746,7 +1158,10 @@ ensure_locales (PvRuntime *self, NULL); flatpak_bwrap_append_bwrap (run_locale_gen, bwrap); - pv_bwrap_copy_tree (run_locale_gen, self->overrides, "/overrides"); + flatpak_bwrap_add_args (bwrap, + "--ro-bind", self->overrides, + self->overrides_in_container, + NULL); if (!flatpak_bwrap_bundle_args (run_locale_gen, 1, -1, FALSE, &local_error)) @@ -758,9 +1173,9 @@ ensure_locales (PvRuntime *self, flatpak_bwrap_add_args (run_locale_gen, "--ro-bind", self->tools_dir, "/run/host/tools", - "--bind", locales, "/overrides/locales", + "--bind", locales, locales_in_container, locale_gen, - "--output-dir", "/overrides/locales", + "--output-dir", locales_in_container, "--verbose", NULL); } @@ -792,7 +1207,7 @@ ensure_locales (PvRuntime *self, g_debug ("%s is non-empty", locales); - locpath = g_string_new ("/overrides/locales"); + locpath = g_string_new (locales_in_container); pv_search_path_append (locpath, g_getenv ("LOCPATH")); flatpak_bwrap_add_args (bwrap, "--setenv", "LOCPATH", locpath->str, @@ -1032,9 +1447,34 @@ bind_runtime (PvRuntime *self, g_return_val_if_fail (!pv_bwrap_was_finished (bwrap), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); - if (!pv_bwrap_bind_usr (bwrap, self->source_files, "/", error)) + if (!pv_bwrap_bind_usr (bwrap, self->runtime_files, "/", error)) return FALSE; + /* In the case where we have a mutable sysroot, we mount the overrides + * as part of /usr. Make /overrides a symbolic link, to be nice to + * older steam-runtime-tools versions. */ + + if (self->mutable_sysroot != NULL) + { + g_assert (self->overrides_in_container[0] == '/'); + g_assert (g_strcmp0 (self->overrides_in_container, "/overrides") != 0); + flatpak_bwrap_add_args (bwrap, + "--symlink", + &self->overrides_in_container[1], + "/overrides", + NULL); + + /* Also make a matching symbolic link on disk, to make it easier + * to inspect the sysroot. */ + if (TEMP_FAILURE_RETRY (symlinkat (&self->overrides_in_container[1], + self->mutable_sysroot_fd, + "overrides")) != 0) + return glnx_throw_errno_prefix (error, + "Unable to create symlink \"%s/overrides\" -> \"%s\"", + self->mutable_sysroot, + &self->overrides_in_container[1]); + } + flatpak_bwrap_add_args (bwrap, "--setenv", "XDG_RUNTIME_DIR", xrd, "--tmpfs", "/run", @@ -1048,7 +1488,7 @@ bind_runtime (PvRuntime *self, for (i = 0; i < G_N_ELEMENTS (bind_mutable); i++) { - g_autofree gchar *path = g_build_filename (self->source_files, + g_autofree gchar *path = g_build_filename (self->runtime_files, bind_mutable[i], NULL); g_autoptr(GDir) dir = NULL; @@ -1070,7 +1510,7 @@ bind_runtime (PvRuntime *self, if (g_strv_contains (dont_bind, dest)) continue; - full = g_build_filename (self->source_files, + full = g_build_filename (self->runtime_files, bind_mutable[i], member, NULL); @@ -1148,7 +1588,16 @@ bind_runtime (PvRuntime *self, flatpak_run_add_pulseaudio_args (bwrap); flatpak_run_add_session_dbus_args (bwrap); flatpak_run_add_system_dbus_args (bwrap); - pv_bwrap_copy_tree (bwrap, self->overrides, "/overrides"); + + if (self->mutable_sysroot == NULL) + { + /* self->overrides is in a temporary directory that will be + * cleaned up before we enter the container, so we need to convert + * it into a series of --dir and --symlink instructions. + * + * We have to do this late, because it adds data fds. */ + pv_bwrap_copy_tree (bwrap, self->overrides, self->overrides_in_container); + } /* /etc/localtime and /etc/resolv.conf can not exist (or be symlinks to * non-existing targets), in which case we don't want to attempt to create @@ -1197,6 +1646,425 @@ bind_runtime (PvRuntime *self, return TRUE; } +typedef enum +{ + TAKE_FROM_HOST_FLAGS_IF_DIR = (1 << 0), + TAKE_FROM_HOST_FLAGS_IF_EXISTS = (1 << 1), + TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE = (1 << 2), + TAKE_FROM_HOST_FLAGS_COPY_FALLBACK = (1 << 3), + TAKE_FROM_HOST_FLAGS_NONE = 0 +} TakeFromHostFlags; + +static gboolean +pv_runtime_take_from_host (PvRuntime *self, + FlatpakBwrap *bwrap, + const char *source_in_host, + const char *dest_in_container, + TakeFromHostFlags flags, + GError **error) +{ + g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE); + g_return_val_if_fail (!pv_bwrap_was_finished (bwrap), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + if (flags & TAKE_FROM_HOST_FLAGS_IF_DIR) + { + if (!g_file_test (source_in_host, G_FILE_TEST_IS_DIR)) + return TRUE; + } + + if (flags & TAKE_FROM_HOST_FLAGS_IF_EXISTS) + { + if (!g_file_test (source_in_host, G_FILE_TEST_EXISTS)) + return TRUE; + } + + if (self->mutable_sysroot != NULL) + { + /* Replace ${mutable_sysroot}/usr/lib/locale with a symlink to + * /run/host/usr/lib/locale, or similar */ + g_autofree gchar *parent_in_container = NULL; + g_autofree gchar *target = NULL; + const char *base; + glnx_autofd int parent_dirfd = -1; + + parent_in_container = g_path_get_dirname (dest_in_container); + parent_dirfd = pv_resolve_in_sysroot (self->mutable_sysroot_fd, + parent_in_container, + PV_RESOLVE_FLAGS_MKDIR_P, + NULL, error); + + if (parent_dirfd < 0) + return FALSE; + + base = glnx_basename (dest_in_container); + + if (!glnx_shutil_rm_rf_at (parent_dirfd, base, NULL, error)) + return FALSE; + + /* If it isn't in /usr, /lib, etc., then the symlink will be + * dangling and this probably isn't going to work. */ + if (!path_visible_in_run_host (source_in_host)) + { + if (flags & TAKE_FROM_HOST_FLAGS_COPY_FALLBACK) + { + return glnx_file_copy_at (AT_FDCWD, source_in_host, NULL, + parent_dirfd, base, + GLNX_FILE_COPY_OVERWRITE, + NULL, error); + } + else + { + g_warning ("\"%s\" is unlikely to appear in /run/host", + source_in_host); + /* ... but try it anyway, it can't hurt */ + } + } + + target = g_build_filename ("/run/host", source_in_host, NULL); + + if (TEMP_FAILURE_RETRY (symlinkat (target, parent_dirfd, base)) != 0) + return glnx_throw_errno_prefix (error, + "Unable to create symlink \"%s/%s\" -> \"%s\"", + self->mutable_sysroot, + dest_in_container, target); + } + else + { + /* We can't edit the runtime in-place, so tell bubblewrap to mount + * a new version over the top */ + + if (flags & TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE) + { + g_autofree gchar *dest = NULL; + + if (g_str_has_prefix (dest_in_container, "/usr/")) + dest = g_build_filename (self->runtime_usr, + dest_in_container + strlen ("/usr/"), + NULL); + else + dest = g_build_filename (self->runtime_files, + dest_in_container, + NULL); + + if (g_file_test (source_in_host, G_FILE_TEST_IS_DIR)) + { + if (!g_file_test (dest, G_FILE_TEST_IS_DIR)) + { + g_warning ("Not mounting \"%s\" over non-directory file or " + "nonexistent path \"%s\"", + source_in_host, dest); + return TRUE; + } + } + else + { + if (!(g_file_test (dest, G_FILE_TEST_EXISTS) && + g_file_test (dest, G_FILE_TEST_IS_DIR))) + { + g_warning ("Not mounting \"%s\" over directory or " + "nonexistent path \"%s\"", + source_in_host, dest); + return TRUE; + } + } + } + + flatpak_bwrap_add_args (bwrap, + "--ro-bind", source_in_host, dest_in_container, + NULL); + } + + return TRUE; +} + +static gboolean +pv_runtime_remove_overridden_libraries (PvRuntime *self, + RuntimeArchitecture *arch, + GError **error) +{ + static const char * const libdirs[] = { "lib", "usr/lib", "usr/lib/mesa" }; + GHashTable *delete[G_N_ELEMENTS (libdirs)] = { NULL }; + GLnxDirFdIterator iters[G_N_ELEMENTS (libdirs)] = { { FALSE } }; + gchar *multiarch_libdirs[G_N_ELEMENTS (libdirs)] = { NULL }; + gboolean ret = FALSE; + gsize i; + + g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE); + g_return_val_if_fail (arch != NULL, FALSE); + g_return_val_if_fail (arch->ld_so != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* Not applicable/possible if we don't have a mutable sysroot */ + g_return_val_if_fail (self->mutable_sysroot != NULL, FALSE); + + /* We have to figure out what we want to delete before we delete anything, + * because we can't tell whether a symlink points to a library of a + * particular SONAME if we already deleted the library. */ + for (i = 0; i < G_N_ELEMENTS (libdirs); i++) + { + glnx_autofd int libdir_fd = -1; + struct dirent *dent; + + multiarch_libdirs[i] = g_build_filename (libdirs[i], arch->tuple, NULL); + + /* Mostly ignore error: if the library directory cannot be opened, + * presumably we don't need to do anything with it... */ + { + g_autoptr(GError) local_error = NULL; + + libdir_fd = pv_resolve_in_sysroot (self->mutable_sysroot_fd, + multiarch_libdirs[i], + PV_RESOLVE_FLAGS_READABLE, + NULL, &local_error); + + if (libdir_fd < 0) + { + g_debug ("Cannot resolve \"%s\" in \"%s\", so no need to delete " + "libraries from it: %s", + multiarch_libdirs[i], self->mutable_sysroot, + local_error->message); + g_clear_error (&local_error); + continue; + } + } + + g_debug ("Removing overridden %s libraries from \"%s\" in \"%s\"...", + arch->tuple, multiarch_libdirs[i], self->mutable_sysroot); + + if (!glnx_dirfd_iterator_init_take_fd (&libdir_fd, &iters[i], error)) + { + glnx_prefix_error (error, "Unable to start iterating \"%s/%s\"", + self->mutable_sysroot, + multiarch_libdirs[i]); + goto out; + } + + delete[i] = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_free); + + while (TRUE) + { + g_autoptr(Elf) elf = NULL; + g_autoptr(GError) local_error = NULL; + glnx_autofd int libfd = -1; + g_autofree gchar *path = NULL; + g_autofree gchar *soname = NULL; + g_autofree gchar *target = NULL; + + if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iters[i], &dent, + NULL, error)) + return glnx_prefix_error (error, "Unable to iterate over \"%s/%s\"", + self->mutable_sysroot, + multiarch_libdirs[i]); + + if (dent == NULL) + break; + + switch (dent->d_type) + { + case DT_REG: + case DT_LNK: + break; + + case DT_BLK: + case DT_CHR: + case DT_DIR: + case DT_FIFO: + case DT_SOCK: + case DT_UNKNOWN: + default: + continue; + } + + if (!g_str_has_prefix (dent->d_name, "lib")) + continue; + + if (!g_str_has_suffix (dent->d_name, ".so") && + strstr (dent->d_name, ".so.") == NULL) + continue; + + path = g_build_filename (multiarch_libdirs[i], dent->d_name, NULL); + + /* scope for soname_link */ + { + g_autofree gchar *soname_link = NULL; + + soname_link = g_build_filename (arch->libdir_on_host, + dent->d_name, NULL); + + /* If we found libfoo.so.1 in the container, and libfoo.so.1 + * also exists among the overrides, delete it. */ + if (g_file_test (soname_link, G_FILE_TEST_IS_SYMLINK)) + { + g_hash_table_replace (delete[i], + g_strdup (dent->d_name), + g_steal_pointer (&soname_link)); + continue; + } + } + + target = glnx_readlinkat_malloc (iters[i].fd, dent->d_name, + NULL, NULL); + + if (target != NULL) + { + g_autofree gchar *soname_link = NULL; + + soname_link = g_build_filename (arch->libdir_on_host, + glnx_basename (target), + NULL); + + /* If the symlink in the container points to + * /foo/bar/libfoo.so.1, and libfoo.so.1 also exists among + * the overrides, delete it. */ + if (g_file_test (soname_link, G_FILE_TEST_IS_SYMLINK)) + { + g_hash_table_replace (delete[i], + g_strdup (dent->d_name), + g_steal_pointer (&soname_link)); + continue; + } + } + + libfd = pv_resolve_in_sysroot (self->mutable_sysroot_fd, path, + PV_RESOLVE_FLAGS_READABLE, NULL, + &local_error); + + if (libfd < 0) + { + g_warning ("Unable to open %s/%s for reading: %s", + self->mutable_sysroot, path, local_error->message); + g_clear_error (&local_error); + continue; + } + + elf = pv_elf_open_fd (libfd, &local_error); + + if (elf != NULL) + soname = pv_elf_get_soname (elf, &local_error); + + if (soname == NULL) + { + g_warning ("Unable to get SONAME of %s/%s: %s", + self->mutable_sysroot, path, local_error->message); + g_clear_error (&local_error); + continue; + } + + /* If we found a library with SONAME libfoo.so.1 in the + * container, and libfoo.so.1 also exists among the overrides, + * delete it. */ + { + g_autofree gchar *soname_link = NULL; + + soname_link = g_build_filename (arch->libdir_on_host, soname, + NULL); + + if (g_file_test (soname_link, G_FILE_TEST_IS_SYMLINK)) + { + g_hash_table_replace (delete[i], + g_strdup (dent->d_name), + g_steal_pointer (&soname_link)); + continue; + } + } + } + } + + for (i = 0; i < G_N_ELEMENTS (libdirs); i++) + { + if (delete[i] == NULL) + continue; + + g_assert (iters[i].initialized); + g_assert (iters[i].fd >= 0); + + GLNX_HASH_TABLE_FOREACH_KV (delete[i], + const char *, name, + const char *, reason) + { + g_autoptr(GError) local_error = NULL; + + g_debug ("Deleting %s/%s/%s because %s replaces it", + self->mutable_sysroot, multiarch_libdirs[i], name, reason); + + if (!glnx_unlinkat (iters[i].fd, name, 0, &local_error)) + { + g_warning ("Unable to delete %s/%s/%s: %s", + self->mutable_sysroot, multiarch_libdirs[i], + name, local_error->message); + g_clear_error (&local_error); + } + } + } + + ret = TRUE; + +out: + for (i = 0; i < G_N_ELEMENTS (libdirs); i++) + { + g_clear_pointer (&delete[i], g_hash_table_unref); + glnx_dirfd_iterator_clear (&iters[i]); + g_free (multiarch_libdirs[i]); + } + + return ret; +} + +static gboolean +pv_runtime_take_ld_so_from_host (PvRuntime *self, + RuntimeArchitecture *arch, + const gchar *ld_so_in_runtime, + FlatpakBwrap *bwrap, + GError **error) +{ + g_autofree gchar *ld_so_in_host = NULL; + + g_debug ("Making host ld.so visible in container"); + + ld_so_in_host = realpath (arch->ld_so, NULL); + + if (ld_so_in_host == NULL) + return glnx_throw_errno_prefix (error, + "Unable to determine host path to %s", + arch->ld_so); + + g_debug ("Host path: %s -> %s", arch->ld_so, ld_so_in_host); + /* Might be either absolute, or relative to the root */ + g_debug ("Container path: %s -> %s", arch->ld_so, ld_so_in_runtime); + + /* If we have a mutable sysroot, we can delete the interoperable path + * and replace it with a symlink to what we want. + * For example, overwrite /lib/ld-linux.so.2 with a symlink to + * /run/host/lib/i386-linux-gnu/ld-2.30.so, or similar. This avoids + * having to dereference a long chain of symlinks every time we run + * an executable. */ + if (self->mutable_sysroot != NULL && + !pv_runtime_take_from_host (self, bwrap, ld_so_in_host, + arch->ld_so, TAKE_FROM_HOST_FLAGS_NONE, + error)) + return FALSE; + + /* If we don't have a mutable sysroot, we cannot replace symlinks, + * and we also cannot mount onto symlinks (they get dereferenced), + * so our only choice is to bind-mount + * /lib/i386-linux-gnu/ld-2.30.so onto + * /lib/i386-linux-gnu/ld-2.15.so and so on. + * + * In the mutable sysroot case, we don't strictly need to + * overwrite /lib/i386-linux-gnu/ld-2.15.so with a symlink to + * /run/host/lib/i386-linux-gnu/ld-2.30.so, but we might as well do + * it anyway, for extra robustness: if we ever run a ld.so that + * doesn't match the libc we are using (perhaps via an OS-specific, + * non-standard path), that's pretty much a disaster, because it will + * just crash. However, all of those (chains of) non-standard symlinks + * will end up pointing to ld_so_in_runtime. */ + return pv_runtime_take_from_host (self, bwrap, ld_so_in_host, + ld_so_in_runtime, + TAKE_FROM_HOST_FLAGS_NONE, error); +} + static gboolean pv_runtime_use_host_graphics_stack (PvRuntime *self, FlatpakBwrap *bwrap, @@ -1226,7 +2094,6 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, g_free, NULL); g_autoptr(GHashTable) gconv_from_host = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); - g_autofree gchar *libdrm_data_in_runtime = NULL; g_autofree gchar *best_libdrm_data_from_host = NULL; GHashTableIter iter; const gchar *gconv_path; @@ -1319,32 +2186,55 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, g_autofree gchar *this_dri_path_in_container = g_build_filename (arch->libdir_in_container, "dri", NULL); g_autofree gchar *libc = NULL; + /* Can either be relative to the sysroot, or absolute */ g_autofree gchar *ld_so_in_runtime = NULL; g_autofree gchar *libdrm = NULL; g_autoptr(SrtObjectList) vdpau_drivers = NULL; g_autoptr(SrtObjectList) va_api_drivers = NULL; - temp_bwrap = flatpak_bwrap_new (NULL); - flatpak_bwrap_add_args (temp_bwrap, - self->bubblewrap, - NULL); + if (self->mutable_sysroot != NULL) + { + glnx_autofd int fd = -1; - if (!pv_bwrap_bind_usr (temp_bwrap, - self->source_files, - "/", - error)) - return FALSE; + fd = pv_resolve_in_sysroot (self->mutable_sysroot_fd, + arch->ld_so, + PV_RESOLVE_FLAGS_NONE, + &ld_so_in_runtime, + error); - flatpak_bwrap_add_args (temp_bwrap, - "env", "PATH=/usr/bin:/bin", - "readlink", "-e", arch->ld_so, - NULL); - flatpak_bwrap_finish (temp_bwrap); + if (fd < 0) + return FALSE; + } + else + { + /* Do it the hard way, by asking a process running in the + * container (or at least a container resembling the one we + * are going to use) to resolve it for us */ + temp_bwrap = flatpak_bwrap_new (NULL); + flatpak_bwrap_add_args (temp_bwrap, + self->bubblewrap, + NULL); - ld_so_in_runtime = pv_capture_output ((const char * const *) temp_bwrap->argv->pdata, - NULL); + if (!pv_bwrap_bind_usr (temp_bwrap, + self->runtime_files, + "/", + error)) + return FALSE; - g_clear_pointer (&temp_bwrap, flatpak_bwrap_free); + if (!pv_bwrap_bind_usr (temp_bwrap, "/", "/run/host", error)) + return FALSE; + + flatpak_bwrap_add_args (temp_bwrap, + "env", "PATH=/usr/bin:/bin", + "readlink", "-e", arch->ld_so, + NULL); + flatpak_bwrap_finish (temp_bwrap); + + ld_so_in_runtime = pv_capture_output ( + (const char * const *) temp_bwrap->argv->pdata, NULL); + + g_clear_pointer (&temp_bwrap, flatpak_bwrap_free); + } if (ld_so_in_runtime == NULL) { @@ -1511,30 +2401,22 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, g_ptr_array_add (va_api_icd_details, g_steal_pointer (&details)); } + if (self->mutable_sysroot != NULL && + !pv_runtime_remove_overridden_libraries (self, arch, error)) + return FALSE; + libc = g_build_filename (arch->libdir_on_host, "libc.so.6", NULL); /* If we are going to use the host system's libc6 (likely) * then we have to use its ld.so too. */ if (g_file_test (libc, G_FILE_TEST_IS_SYMLINK)) { - g_autofree gchar *ld_so_in_host = NULL; g_autofree char *libc_target = NULL; - g_debug ("Making host ld.so visible in container"); - - ld_so_in_host = realpath (arch->ld_so, NULL); - - if (ld_so_in_host == NULL) - return glnx_throw_errno_prefix (error, - "Unable to determine host path to %s", - arch->ld_so); - - g_debug ("Host path: %s -> %s", arch->ld_so, ld_so_in_host); - g_debug ("Container path: %s -> %s", arch->ld_so, ld_so_in_runtime); - flatpak_bwrap_add_args (bwrap, - "--ro-bind", ld_so_in_host, - ld_so_in_runtime, - NULL); + if (!pv_runtime_take_ld_so_from_host (self, arch, + ld_so_in_runtime, + bwrap, error)) + return FALSE; /* Collect miscellaneous libraries that libc might dlopen. * At the moment this is just libidn2. */ @@ -1720,17 +2602,19 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, { g_debug ("Making host locale data visible in container"); - if (g_file_test ("/usr/lib/locale", G_FILE_TEST_EXISTS)) - flatpak_bwrap_add_args (bwrap, - "--ro-bind", "/usr/lib/locale", - "/usr/lib/locale", - NULL); + if (!pv_runtime_take_from_host (self, bwrap, + "/usr/lib/locale", + "/usr/lib/locale", + TAKE_FROM_HOST_FLAGS_IF_EXISTS, + error)) + return FALSE; - if (g_file_test ("/usr/share/i18n", G_FILE_TEST_EXISTS)) - flatpak_bwrap_add_args (bwrap, - "--ro-bind", "/usr/share/i18n", - "/usr/share/i18n", - NULL); + if (!pv_runtime_take_from_host (self, bwrap, + "/usr/share/i18n", + "/usr/share/i18n", + TAKE_FROM_HOST_FLAGS_IF_EXISTS, + error)) + return FALSE; localedef = g_find_program_in_path ("localedef"); @@ -1738,15 +2622,12 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, { g_warning ("Cannot find localedef in PATH"); } - else + else if (!pv_runtime_take_from_host (self, bwrap, localedef, + "/usr/bin/localedef", + TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE, + error)) { - g_autofree gchar *target = g_build_filename ("/run/host", - localedef, NULL); - - flatpak_bwrap_add_args (bwrap, - "--symlink", target, - "/overrides/bin/localedef", - NULL); + return FALSE; } locale = g_find_program_in_path ("locale"); @@ -1755,15 +2636,12 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, { g_warning ("Cannot find locale in PATH"); } - else + else if (!pv_runtime_take_from_host (self, bwrap, locale, + "/usr/bin/locale", + TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE, + error)) { - g_autofree gchar *target = g_build_filename ("/run/host", - locale, NULL); - - flatpak_bwrap_add_args (bwrap, - "--symlink", target, - "/overrides/bin/locale", - NULL); + return FALSE; } ldconfig = g_find_program_in_path ("ldconfig"); @@ -1780,11 +2658,13 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, { g_warning ("Cannot find ldconfig in PATH, /sbin or /usr/sbin"); } - else + else if (!pv_runtime_take_from_host (self, bwrap, + ldconfig, + "/sbin/ldconfig", + TAKE_FROM_HOST_FLAGS_NONE, + error)) { - flatpak_bwrap_add_args (bwrap, - "--ro-bind", ldconfig, "/sbin/ldconfig", - NULL); + return FALSE; } g_debug ("Making host gconv modules visible in container"); @@ -1792,19 +2672,12 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, g_hash_table_iter_init (&iter, gconv_from_host); while (g_hash_table_iter_next (&iter, (gpointer *)&gconv_path, NULL)) { - g_autofree gchar *gconv_in_runtime = NULL; - - if (g_str_has_prefix (gconv_path, "/usr/")) - gconv_in_runtime = g_build_filename (self->runtime_usr, gconv_path + strlen ("/usr"), NULL); - else - gconv_in_runtime = g_build_filename (self->runtime_usr, gconv_path, NULL); - - if (g_file_test (gconv_in_runtime, G_FILE_TEST_IS_DIR)) - { - flatpak_bwrap_add_args (bwrap, - "--ro-bind", gconv_path, gconv_path, - NULL); - } + if (!pv_runtime_take_from_host (self, bwrap, + gconv_path, + gconv_path, + TAKE_FROM_HOST_FLAGS_IF_DIR, + error)) + return FALSE; } } else @@ -1836,14 +2709,14 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, best_libdrm_data_from_host = g_strdup (pv_hash_table_get_arbitrary_key (libdrm_data_from_host)); } - libdrm_data_in_runtime = g_build_filename (self->runtime_usr, "share", "libdrm", NULL); - - if (best_libdrm_data_from_host != NULL && - g_file_test (libdrm_data_in_runtime, G_FILE_TEST_IS_DIR)) + if (best_libdrm_data_from_host != NULL) { - flatpak_bwrap_add_args (bwrap, - "--ro-bind", best_libdrm_data_from_host, "/usr/share/libdrm", - NULL); + if (!pv_runtime_take_from_host (self, bwrap, + best_libdrm_data_from_host, + "/usr/share/libdrm", + TAKE_FROM_HOST_FLAGS_IF_CONTAINER_COMPATIBLE, + error)) + return FALSE; } g_debug ("Setting up EGL ICD JSON..."); @@ -1883,8 +2756,9 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, json_base = g_strdup_printf ("%" G_GSIZE_FORMAT "-%s.json", j, multiarch_tuples[i]); json_on_host = g_build_filename (dir_on_host, json_base, NULL); - json_in_container = g_build_filename ("/overrides", "share", - "glvnd", "egl_vendor.d", + json_in_container = g_build_filename (self->overrides_in_container, + "share", "glvnd", + "egl_vendor.d", json_base, NULL); replacement = srt_egl_icd_new_replace_library_path (icd, @@ -1909,13 +2783,18 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, const char *json_on_host = srt_egl_icd_get_json_path (icd); json_base = g_strdup_printf ("%" G_GSIZE_FORMAT ".json", j); - json_in_container = g_build_filename ("/overrides", "share", - "glvnd", "egl_vendor.d", + json_in_container = g_build_filename (self->overrides_in_container, + "share", "glvnd", + "egl_vendor.d", json_base, NULL); - flatpak_bwrap_add_args (bwrap, - "--ro-bind", json_on_host, json_in_container, - NULL); + if (!pv_runtime_take_from_host (self, bwrap, + json_on_host, + json_in_container, + TAKE_FROM_HOST_FLAGS_COPY_FALLBACK, + error)) + return FALSE; + pv_search_path_append (egl_path, json_in_container); } } @@ -1957,8 +2836,8 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, json_base = g_strdup_printf ("%" G_GSIZE_FORMAT "-%s.json", j, multiarch_tuples[i]); json_on_host = g_build_filename (dir_on_host, json_base, NULL); - json_in_container = g_build_filename ("/overrides", "share", - "vulkan", "icd.d", + json_in_container = g_build_filename (self->overrides_in_container, + "share", "vulkan", "icd.d", json_base, NULL); replacement = srt_vulkan_icd_new_replace_library_path (icd, @@ -1983,13 +2862,17 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, const char *json_on_host = srt_vulkan_icd_get_json_path (icd); json_base = g_strdup_printf ("%" G_GSIZE_FORMAT ".json", j); - json_in_container = g_build_filename ("/overrides", "share", - "vulkan", "icd.d", + json_in_container = g_build_filename (self->overrides_in_container, + "share", "vulkan", "icd.d", json_base, NULL); - flatpak_bwrap_add_args (bwrap, - "--ro-bind", json_on_host, json_in_container, - NULL); + if (!pv_runtime_take_from_host (self, bwrap, + json_on_host, + json_in_container, + TAKE_FROM_HOST_FLAGS_COPY_FALLBACK, + error)) + return FALSE; + pv_search_path_append (vulkan_path, json_in_container); } } @@ -2069,8 +2952,10 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, * already. */ flatpak_bwrap_add_args (bwrap, "--setenv", "VDPAU_DRIVER_PATH", - "/overrides/lib/${PLATFORM}-linux-gnu/vdpau", NULL); + flatpak_bwrap_add_arg_printf (bwrap, + "%s/lib/${PLATFORM}-linux-gnu/vdpau", + self->overrides_in_container); static const char * const extra_multiarch_tuples[] = { @@ -2123,15 +3008,49 @@ pv_runtime_bind (PvRuntime *self, if (!bind_runtime (self, bwrap, error)) return FALSE; - pressure_vessel_prefix = g_path_get_dirname (self->tools_dir); - - /* Make sure pressure-vessel itself is visible there. */ + /* steam-runtime-system-info uses this to detect pressure-vessel, so we + * need to create it even if it will be empty */ flatpak_bwrap_add_args (bwrap, - "--ro-bind", - pressure_vessel_prefix, + "--dir", "/run/pressure-vessel", NULL); + pressure_vessel_prefix = g_path_get_dirname (self->tools_dir); + + /* Make sure pressure-vessel itself is visible there. */ + if (self->mutable_sysroot != NULL) + { + g_autofree gchar *dest = NULL; + glnx_autofd int parent_dirfd = -1; + + parent_dirfd = pv_resolve_in_sysroot (self->mutable_sysroot_fd, + "/usr/lib/pressure-vessel", + PV_RESOLVE_FLAGS_MKDIR_P, + NULL, error); + + if (parent_dirfd < 0) + return FALSE; + + if (!glnx_shutil_rm_rf_at (parent_dirfd, "from-host", NULL, error)) + return FALSE; + + dest = glnx_fdrel_abspath (parent_dirfd, "from-host"); + + if (!pv_cheap_tree_copy (pressure_vessel_prefix, dest, error)) + return FALSE; + + self->with_lock_in_container = "/usr/lib/pressure-vessel/from-host/bin/pressure-vessel-with-lock"; + } + else + { + flatpak_bwrap_add_args (bwrap, + "--ro-bind", + pressure_vessel_prefix, + "/run/pressure-vessel/pv-from-host", + NULL); + self->with_lock_in_container = "/run/pressure-vessel/pv-from-host/bin/pressure-vessel-with-lock"; + } + pv_runtime_set_search_paths (self, bwrap); return TRUE; @@ -2154,8 +3073,8 @@ pv_runtime_set_search_paths (PvRuntime *self, { g_autofree gchar *ld_path = NULL; - ld_path = g_build_filename ("/overrides", "lib", - multiarch_tuples[i], NULL); + ld_path = g_build_filename (self->overrides_in_container, "lib", + multiarch_tuples[i], NULL); pv_search_path_append (ld_library_path, ld_path); } @@ -2167,7 +3086,7 @@ pv_runtime_set_search_paths (PvRuntime *self, * really make sense inside the container: * in principle the layout could be totally * different. */ - "--setenv", "PATH", "/overrides/bin:/usr/bin:/bin", + "--setenv", "PATH", "/usr/bin:/bin", "--setenv", "LD_LIBRARY_PATH", ld_library_path->str, NULL); diff --git a/src/runtime.h b/src/runtime.h index eb3a16f24376922fe89028e2f8ddd99aa75c9dde..3c004c72254b4782baff9fc056e327efc110cc6f 100644 --- a/src/runtime.h +++ b/src/runtime.h @@ -33,6 +33,7 @@ * PvRuntimeFlags: * @PV_RUNTIME_FLAGS_HOST_GRAPHICS_STACK: Use host system graphics stack * @PV_RUNTIME_FLAGS_GENERATE_LOCALES: Generate missing locales + * @PV_RUNTIME_FLAGS_GC_RUNTIMES: Garbage-collect old temporary runtimes * @PV_RUNTIME_FLAGS_NONE: None of the above * * Flags affecting how we set up the runtime. @@ -41,12 +42,14 @@ typedef enum { PV_RUNTIME_FLAGS_HOST_GRAPHICS_STACK = (1 << 0), PV_RUNTIME_FLAGS_GENERATE_LOCALES = (1 << 1), + PV_RUNTIME_FLAGS_GC_RUNTIMES = (1 << 2), PV_RUNTIME_FLAGS_NONE = 0 } PvRuntimeFlags; #define PV_RUNTIME_FLAGS_MASK \ (PV_RUNTIME_FLAGS_HOST_GRAPHICS_STACK \ | PV_RUNTIME_FLAGS_GENERATE_LOCALES \ + | PV_RUNTIME_FLAGS_GC_RUNTIMES \ ) typedef struct _PvRuntime PvRuntime; @@ -61,6 +64,7 @@ typedef struct _PvRuntimeClass PvRuntimeClass; GType pv_runtime_get_type (void); PvRuntime *pv_runtime_new (const char *source_files, + const char *mutable_parent, const char *bubblewrap, const char *tools_dir, PvRuntimeFlags flags, diff --git a/src/wrap.c b/src/wrap.c index 56126d873d66f6d13d3baa09bfb7bff8467fe58e..2791c167fc7134bbd0d2d2528c3a1ea117f61627 100644 --- a/src/wrap.c +++ b/src/wrap.c @@ -100,7 +100,8 @@ find_bwrap (const char *tools_dir) } static gchar * -check_bwrap (const char *tools_dir) +check_bwrap (const char *tools_dir, + gboolean only_prepare) { g_autoptr(GError) local_error = NULL; GError **error = &local_error; @@ -119,6 +120,14 @@ check_bwrap (const char *tools_dir) { g_warning ("Cannot find bwrap"); } + else if (only_prepare) + { + /* With --only-prepare we don't necessarily expect to be able to run + * it anyway (we are probably in a Docker container that doesn't allow + * creation of nested user namespaces), so just assume that it's the + * right one. */ + return g_steal_pointer (&bwrap_executable); + } else { int wait_status; @@ -316,14 +325,17 @@ typedef enum TRISTATE_MAYBE } Tristate; +static char *opt_copy_runtime_into = NULL; static char **opt_env_if_host = NULL; static char *opt_fake_home = NULL; static char *opt_freedesktop_app_id = NULL; static char *opt_steam_app_id = NULL; +static gboolean opt_gc_runtimes = TRUE; static gboolean opt_generate_locales = TRUE; static char *opt_home = NULL; static gboolean opt_host_fallback = FALSE; static gboolean opt_host_graphics = TRUE; +static gboolean opt_only_prepare = FALSE; static gboolean opt_remove_game_overlay = FALSE; static PvShell opt_shell = PV_SHELL_NONE; static GPtrArray *opt_ld_preload = NULL; @@ -495,6 +507,11 @@ opt_share_home_cb (const gchar *option_name, static GOptionEntry options[] = { + { "copy-runtime-into", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_copy_runtime_into, + "If a --runtime is used, copy it into DIR and edit the copy in-place. " + "[Default: $PRESSURE_VESSEL_COPY_RUNTIME_INTO or empty]", + "DIR" }, { "env-if-host", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &opt_env_if_host, "Set VAR=VAL if COMMAND is run with /usr from the host system, " @@ -509,6 +526,15 @@ static GOptionEntry options[] = G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_steam_app_id, "Make --unshare-home use ~/.var/app/com.steampowered.AppN " "as home directory. [Default: $SteamAppId]", "N" }, + { "gc-runtimes", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_gc_runtimes, + "If using --copy-runtime-into, garbage-collect old temporary " + "runtimes. [Default, unless $PRESSURE_VESSEL_GC_RUNTIMES is 0]", + NULL }, + { "no-gc-runtimes", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_gc_runtimes, + "If using --copy-runtime-into, don't garbage-collect old " + "temporary runtimes.", NULL }, { "generate-locales", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_generate_locales, "If using --runtime, attempt to generate any missing locales. " @@ -626,6 +652,9 @@ static GOptionEntry options[] = { "test", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_test, "Smoke test pressure-vessel-wrap and exit.", NULL }, + { "only-prepare", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_only_prepare, + "Prepare runtime, but do not actually run anything.", NULL }, { NULL } }; @@ -734,6 +763,7 @@ main (int argc, opt_remove_game_overlay = boolean_environment ("PRESSURE_VESSEL_REMOVE_GAME_OVERLAY", FALSE); opt_share_home = tristate_environment ("PRESSURE_VESSEL_SHARE_HOME"); + opt_gc_runtimes = boolean_environment ("PRESSURE_VESSEL_GC_RUNTIMES", TRUE); opt_generate_locales = boolean_environment ("PRESSURE_VESSEL_GENERATE_LOCALES", TRUE); opt_host_graphics = boolean_environment ("PRESSURE_VESSEL_HOST_GRAPHICS", TRUE); @@ -773,6 +803,13 @@ main (int argc, opt_runtime = g_build_filename (opt_runtime_base, tmp, NULL); } + if (opt_copy_runtime_into == NULL) + opt_copy_runtime_into = g_strdup (g_getenv ("PRESSURE_VESSEL_COPY_RUNTIME_INTO")); + + if (opt_copy_runtime_into != NULL + && opt_copy_runtime_into[0] == '\0') + opt_copy_runtime_into = NULL; + if (opt_version_only) { g_print ("%s\n", VERSION); @@ -790,7 +827,7 @@ main (int argc, goto out; } - if (argc < 2 && !opt_test) + if (argc < 2 && !opt_test && !opt_only_prepare) { g_printerr ("%s: An executable to run is required\n", g_get_prgname ()); @@ -875,6 +912,13 @@ main (int argc, } } + if (opt_only_prepare && opt_test) + { + g_printerr ("%s: --only-prepare and --test are mutually exclusive", + g_get_prgname ()); + goto out; + } + /* Finished parsing arguments, so any subsequent failures will make * us exit 1. */ ret = 1; @@ -993,7 +1037,7 @@ main (int argc, flatpak_bwrap_append_argsv (wrapped_command, &argv[1], argc - 1); g_debug ("Checking for bwrap..."); - bwrap_executable = check_bwrap (tools_dir); + bwrap_executable = check_bwrap (tools_dir, opt_only_prepare); if (opt_test) { @@ -1070,6 +1114,9 @@ main (int argc, { PvRuntimeFlags flags = PV_RUNTIME_FLAGS_NONE; + if (opt_gc_runtimes) + flags |= PV_RUNTIME_FLAGS_GC_RUNTIMES; + if (opt_generate_locales) flags |= PV_RUNTIME_FLAGS_GENERATE_LOCALES; @@ -1079,6 +1126,7 @@ main (int argc, g_debug ("Configuring runtime %s...", opt_runtime); runtime = pv_runtime_new (opt_runtime, + opt_copy_runtime_into, bwrap_executable, tools_dir, flags, @@ -1308,7 +1356,11 @@ main (int argc, pv_runtime_cleanup (runtime); flatpak_bwrap_finish (bwrap); - pv_bwrap_execve (bwrap, error); + + if (opt_only_prepare) + ret = 0; + else + pv_bwrap_execve (bwrap, error); out: if (local_error != NULL) diff --git a/tests/containers.py b/tests/containers.py index ae474cfc45dbd9a589ed7489afbc1dc96c07e952..e437ae73a1784baf4cde8554544e9b734cd58dbe 100755 --- a/tests/containers.py +++ b/tests/containers.py @@ -78,11 +78,16 @@ SteamOS 2 'brewmaster', Debian 8 'jessie', Ubuntu 14.04 'trusty'. """ import contextlib +import fcntl +import glob +import json import logging import os import shutil +import struct import subprocess import sys +import tempfile import unittest try: @@ -105,6 +110,7 @@ logger = logging.getLogger('test-containers') class TestContainers(BaseTest): bwrap = None # type: typing.Optional[str] containers_dir = '' + host_srsi_parsed = {} # type: typing.Dict[str, typing.Any] pv_dir = '' pv_wrap = '' @@ -122,9 +128,8 @@ class TestContainers(BaseTest): stdout=2, stderr=2, ).returncode != 0: - # Right now this means we will skip all the tests, but in - # future we will be able to do some tests that just do the - # setup and do not actually go as far as running the container. + # We can only do tests that just do setup and don't actually + # try to run the container. cls.bwrap = None else: cls.bwrap = bwrap @@ -270,8 +275,15 @@ class TestContainers(BaseTest): os.environ['HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON'] = os.path.join( cls.artifacts, 'host-srsi.json', ) + + with open( + os.path.join(cls.artifacts, 'host-srsi.json'), + 'r', + ) as reader: + cls.host_srsi_parsed = json.load(reader) else: os.environ.pop('HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON', None) + cls.host_srsi_parsed = {} try: os.environ['HOST_LD_LINUX_SO_REALPATH'] = os.path.realpath( @@ -292,6 +304,7 @@ class TestContainers(BaseTest): cls = self.__class__ self.bwrap = cls.bwrap self.containers_dir = cls.containers_dir + self.host_srsi_parsed = cls.host_srsi_parsed self.pv_dir = cls.pv_dir self.pv_wrap = cls.pv_wrap @@ -306,6 +319,21 @@ class TestContainers(BaseTest): os.path.join(cls.artifacts, 'tmp', f), ) + # This parsing is sufficiently "cheap" that we repeat it for + # each test-case rather than introducing more class variables. + + self.host_os_release = cls.host_srsi_parsed.get('os-release', {}) + + if self.host_os_release.get('id') == 'debian': + logger.info('Host OS is Debian') + self.host_is_debian_derived = True + elif 'debian' in self.host_os_release.get('id_like', []): + logger.info('Host OS is Debian-derived') + self.host_is_debian_derived = True + else: + logger.info('Host OS is not Debian-derived') + self.host_is_debian_derived = False + def tearDown(self) -> None: with contextlib.suppress(FileNotFoundError): shutil.rmtree(os.path.join(self.artifacts, 'tmp')) @@ -334,9 +362,12 @@ class TestContainers(BaseTest): test_name: str, scout: str, *, - locales: bool = False + copy: bool = False, + gc: bool = True, + locales: bool = False, + only_prepare: bool = False ) -> None: - if self.bwrap is None: + if self.bwrap is None and not only_prepare: self.skipTest('Unable to run bwrap (in a container?)') if not os.path.isdir(scout): @@ -354,30 +385,442 @@ class TestContainers(BaseTest): '--verbose', ] + var = os.path.join(self.containers_dir, 'var') + os.makedirs(var, exist_ok=True) + if not locales: argv.append('--no-generate-locales') - argv.extend([ - '--', - 'env', - 'TEST_INSIDE_SCOUT_ARTIFACTS=' + artifacts, - 'TEST_INSIDE_SCOUT_LOCALES=' + ('1' if locales else ''), - 'python3.5', - os.path.join(self.artifacts, 'tmp', 'inside-scout.py'), - ]) - - with tee_file_and_stderr( - os.path.join(artifacts, 'inside-scout.log') - ) as tee: - completed = self.run_subprocess( - argv, - cwd=self.artifacts, - stdout=tee.stdin, - stderr=tee.stdin, - universal_newlines=True, + with tempfile.TemporaryDirectory(prefix='test-', dir=var) as temp: + if copy: + argv.extend(['--copy-runtime-into', temp]) + + if not gc: + argv.append('--no-gc-runtimes') + + if only_prepare: + argv.append('--only-prepare') + else: + argv.extend([ + '--', + 'env', + 'TEST_INSIDE_SCOUT_ARTIFACTS=' + artifacts, + 'TEST_INSIDE_SCOUT_IS_COPY=' + ('1' if copy else ''), + 'TEST_INSIDE_SCOUT_LOCALES=' + ('1' if locales else ''), + 'python3.5', + os.path.join(self.artifacts, 'tmp', 'inside-scout.py'), + ]) + + # Create directories representing previous runs of + # pressure-vessel-wrap, so that we can assert that they are + # GC'd (or not) as desired. + + # Do not delete because its name does not start with tmp- + os.makedirs(os.path.join(temp, 'donotdelete'), exist_ok=True) + # Delete + os.makedirs(os.path.join(temp, 'tmp-deleteme'), exist_ok=True) + # Delete, and assert that it is recursive + os.makedirs( + os.path.join(temp, 'tmp-deleteme2', 'usr', 'lib'), + exist_ok=True, ) + # Do not delete because it has ./keep + os.makedirs(os.path.join(temp, 'tmp-keep', 'keep'), exist_ok=True) + # Do not delete because we will read-lock .ref + os.makedirs(os.path.join(temp, 'tmp-rlock'), exist_ok=True) + # Do not delete because we will write-lock .ref + os.makedirs(os.path.join(temp, 'tmp-wlock'), exist_ok=True) - self.assertEqual(completed.returncode, 0) + with open( + os.path.join(temp, 'tmp-rlock', '.ref'), 'w+' + ) as rlock_writer, open( + os.path.join(temp, 'tmp-wlock', '.ref'), 'w' + ) as wlock_writer, tee_file_and_stderr( + os.path.join(artifacts, 'inside-scout.log') + ) as tee: + lockdata = struct.pack('hhlli', fcntl.F_RDLCK, 0, 0, 0, 0) + fcntl.fcntl(rlock_writer.fileno(), fcntl.F_SETLKW, lockdata) + lockdata = struct.pack('hhlli', fcntl.F_WRLCK, 0, 0, 0, 0) + fcntl.fcntl(wlock_writer.fileno(), fcntl.F_SETLKW, lockdata) + + # Put this in a subtest so that if it fails, we still get + # to inspect the copied sysroot + with self.subTest('run', copy=copy, scout=scout): + completed = self.run_subprocess( + argv, + cwd=self.artifacts, + stdout=tee.stdin, + stderr=tee.stdin, + universal_newlines=True, + ) + self.assertEqual(completed.returncode, 0) + + if copy: + members = set(os.listdir(temp)) + + self.assertIn('.ref', members) + self.assertIn('donotdelete', members) + self.assertIn('tmp-keep', members) + self.assertIn('tmp-rlock', members) + self.assertIn('tmp-wlock', members) + if gc: + self.assertNotIn('tmp-deleteme', members) + self.assertNotIn('tmp-deleteme2', members) + else: + # These would have been deleted if not for --no-gc-runtimes + self.assertIn('tmp-deleteme', members) + self.assertIn('tmp-deleteme2', members) + + members.discard('.ref') + members.discard('donotdelete') + members.discard('tmp-deleteme') + members.discard('tmp-deleteme2') + members.discard('tmp-keep') + members.discard('tmp-rlock') + members.discard('tmp-wlock') + # After discarding those, there should be exactly one left: + # the one we just created + self.assertEqual(len(members), 1) + tree = os.path.join(temp, members.pop()) + + with self.subTest('mutable sysroot'): + self._assert_mutable_sysroot( + tree, + artifacts, + is_scout=True, + ) + + def _assert_mutable_sysroot( + self, + tree: str, + artifacts: str, + *, + is_scout: bool = True + ) -> None: + with open( + os.path.join(artifacts, 'contents.txt'), + 'w', + ) as writer: + self.run_subprocess([ + 'find', + '.', + '-ls', + ], cwd=tree, stderr=2, stdout=writer) + + self.assertTrue(os.path.isdir(os.path.join(tree, 'bin'))) + self.assertTrue(os.path.isdir(os.path.join(tree, 'etc'))) + self.assertTrue(os.path.isdir(os.path.join(tree, 'lib'))) + self.assertTrue(os.path.isdir(os.path.join(tree, 'usr'))) + self.assertFalse( + os.path.isdir(os.path.join(tree, 'usr', 'usr')) + ) + self.assertTrue(os.path.isdir(os.path.join(tree, 'sbin'))) + + target = os.readlink(os.path.join(tree, 'overrides')) + self.assertEqual(target, 'usr/lib/pressure-vessel/overrides') + self.assertTrue( + os.path.isdir(os.path.join(tree, 'overrides', 'lib')), + ) + self.assertTrue( + os.path.isfile( + os.path.join( + tree, 'usr', 'lib', 'pressure-vessel', 'from-host', + 'bin', 'pressure-vessel-with-lock', + ) + ) + ) + self.assertTrue( + os.path.isdir( + os.path.join( + tree, 'usr', 'lib', 'pressure-vessel', 'overrides', 'lib', + ) + ) + ) + + for multiarch, arch_info in self.host_srsi_parsed.get( + 'architectures', {} + ).items(): + overrides_libdir = os.path.join( + tree, 'overrides', 'lib', multiarch, + ) + root_libdir = os.path.join( + tree, 'lib', multiarch, + ) + usr_libdir = os.path.join( + tree, 'usr', 'lib', multiarch, + ) + host_root_libdir = os.path.join( + '/', 'lib', multiarch, + ) + host_usr_libdir = os.path.join( + '/', 'usr', 'lib', multiarch, + ) + with self.subTest(arch=multiarch): + + self.assertTrue(os.path.isdir(overrides_libdir)) + self.assertTrue(os.path.isdir(root_libdir)) + self.assertTrue(os.path.isdir(usr_libdir)) + + if is_scout: + for soname in ( + 'libBrokenLocale.so.1', + 'libanl.so.1', + 'libc.so.6', + 'libcrypt.so.1', + 'libdl.so.2', + 'libm.so.6', + 'libnsl.so.1', + 'libpthread.so.0', + 'libresolv.so.2', + 'librt.so.1', + 'libutil.so.1', + ): + # These are from glibc, which is depended on by + # Mesa, and is at least as new as scout's version + # in every supported version of the Steam Runtime. + with self.subTest(soname=soname): + target = os.readlink( + os.path.join(overrides_libdir, soname) + ) + self.assertRegex(target, r'^/run/host/') + + devlib = soname.split('.so.', 1)[0] + '.so' + + # It was deleted from /lib, if present... + with self.assertRaises(FileNotFoundError): + print( + '#', + os.path.join(root_libdir, soname), + '->', + os.readlink( + os.path.join(root_libdir, soname) + ), + ) + # ... and /usr/lib, if present + with self.assertRaises(FileNotFoundError): + print( + '#', + os.path.join(usr_libdir, soname), + '->', + os.readlink( + os.path.join(usr_libdir, soname) + ), + ) + + # In most cases we expect the development + # symlink to be removed, too - but some of + # them are actually linker scripts or other + # non-ELF things + if soname not in ( + 'libc.so.6', + 'libpthread.so.0', + ): + with self.assertRaises(FileNotFoundError): + print( + '#', + os.path.join(usr_libdir, devlib), + '->', + os.readlink( + os.path.join(usr_libdir, devlib) + ), + ) + with self.assertRaises(FileNotFoundError): + print( + '#', + os.path.join(root_libdir, devlib), + '->', + os.readlink( + os.path.join(root_libdir, devlib) + ), + ) + + for soname in ( + # These are some examples of libraries in the + # graphics stack that we don't upgrade, so we + # expect the host version to be newer (or possibly + # absent if the host graphics stack doesn't use + # them, for example static linking or something). + 'libdrm.so.2', + 'libudev.so.0', + ): + with self.subTest(soname=soname): + if ( + not os.path.exists( + os.path.join(host_usr_libdir, soname) + ) + and not os.path.exists( + os.path.join(host_root_libdir, soname) + ) + ): + self.assertTrue( + os.path.exists( + os.path.join(usr_libdir, soname) + ) + or os.path.exists( + os.path.join(root_libdir, soname) + ) + ) + continue + + devlib = soname.split('.so.', 1)[0] + '.so' + pattern = soname + '.*' + + target = os.readlink( + os.path.join(overrides_libdir, soname) + ) + self.assertRegex(target, r'^/run/host/') + + # It was deleted from /lib, if present... + with self.assertRaises(FileNotFoundError): + os.readlink( + os.path.join(root_libdir, soname) + ) + with self.assertRaises(FileNotFoundError): + os.readlink( + os.path.join(root_libdir, devlib) + ) + self.assertEqual( + glob.glob(os.path.join(root_libdir, pattern)), + [], + ) + + # ... and /usr/lib, if present + with self.assertRaises(FileNotFoundError): + os.readlink( + os.path.join(usr_libdir, soname) + ) + with self.assertRaises(FileNotFoundError): + os.readlink( + os.path.join(usr_libdir, devlib) + ) + self.assertEqual( + glob.glob(os.path.join(usr_libdir, pattern)), + [], + ) + + for soname in ( + 'libSDL-1.2.so.0', + 'libfltk.so.1.1', + ): + with self.subTest(soname=soname): + with self.assertRaises(FileNotFoundError): + os.readlink( + os.path.join(overrides_libdir, soname), + ) + + # Keys are the basename of a DRI driver. + # Values are lists of paths to DRI drivers of that name + # on the host. We assert that for each name, an arbitrary + # one of the DRI drivers of that name appears in the + # container. + expect_symlinks = { + } # type: typing.Dict[str, typing.List[str]] + + for dri in arch_info.get('dri_drivers', ()): + path = dri['library_path'] + + if path.startswith(( # any of: + '/usr/lib/dri/', + '/usr/lib32/dri/', + '/usr/lib64/dri/', + '/usr/lib/{}/dri/'.format(multiarch), + )): + # We don't make any assertion about the search + # order here. + + # Take the realpath() on non-Debian-derived hosts, + # because on Arch Linux, we find drivers in + # /usr/lib64 that are physically in /usr/lib. + # Be more strict on Debian because we know more + # about the canonical paths there. + if not self.host_is_debian_derived: + with contextlib.suppress(OSError): + path = os.path.realpath(path) + + expect_symlinks.setdefault( + os.path.basename(path), [] + ).append(path) + + for k, vs in expect_symlinks.items(): + with self.subTest(dri_symlink=k): + link = os.path.join(overrides_libdir, 'dri', k) + target = os.readlink(link) + self.assertEqual(target[:10], '/run/host/') + target = target[9:] # includes the / after host/ + + # Again, take the realpath() on non-Debian-derived + # hosts, but be more strict on Debian. + if not self.host_is_debian_derived: + with contextlib.suppress(OSError): + target = os.path.realpath(target) + + self.assertIn(target, vs) + + if is_scout: + if self.host_is_debian_derived: + link = os.path.join( + tree, 'usr', 'lib', multiarch, 'gconv', + ) + target = os.readlink(link) + self.assertEqual( + target, + '/run/host/usr/lib/{}/gconv'.format(multiarch), + ) + + if os.path.isdir('/usr/share/libdrm'): + link = os.path.join( + tree, 'usr', 'share', 'libdrm', + ) + target = os.readlink(link) + self.assertEqual(target, '/run/host/usr/share/libdrm') + + if is_scout: + if os.path.isdir('/usr/lib/locale'): + link = os.path.join(tree, 'usr', 'lib', 'locale') + target = os.readlink(link) + self.assertEqual(target, '/run/host/usr/lib/locale') + + if os.path.isdir('/usr/share/i18n'): + link = os.path.join(tree, 'usr', 'share', 'i18n') + target = os.readlink(link) + self.assertEqual(target, '/run/host/usr/share/i18n') + + link = os.path.join(tree, 'sbin', 'ldconfig') + target = os.readlink(link) + # Might not be /sbin/ldconfig, for example on non-Debian hosts + self.assertRegex(target, r'^/run/host/') + + if os.path.isfile('/usr/bin/locale'): + link = os.path.join(tree, 'usr', 'bin', 'locale') + target = os.readlink(link) + self.assertEqual(target, '/run/host/usr/bin/locale') + + if os.path.isfile('/usr/bin/localedef'): + link = os.path.join(tree, 'usr', 'bin', 'localedef') + target = os.readlink(link) + self.assertEqual(target, '/run/host/usr/bin/localedef') + + for ldso, scout_impl in ( + ( + '/lib/ld-linux.so.2', + '/lib/i386-linux-gnu/ld-2.15.so', + ), + ( + '/lib64/ld-linux-x86-64.so.2', + '/lib/x86_64-linux-gnu/ld-2.15.so', + ), + ): + try: + host_path = os.path.realpath(ldso) + except OSError: + pass + else: + link = os.path.join(tree, './' + ldso) + target = os.readlink(link) + self.assertEqual(target, '/run/host' + host_path) + link = os.path.join(tree, './' + scout_impl) + target = os.readlink(link) + self.assertEqual(target, '/run/host' + host_path) def test_scout_sysroot(self) -> None: scout = os.path.join(self.containers_dir, 'scout_sysroot') @@ -385,12 +828,29 @@ class TestContainers(BaseTest): if os.path.isdir(os.path.join(scout, 'files')): scout = os.path.join(scout, 'files') - self._test_scout('scout_sysroot', scout, locales=True) + with self.subTest('only-prepare'): + self._test_scout( + 'scout_sysroot_prep', scout, + copy=True, only_prepare=True, + ) + + with self.subTest('copy'): + self._test_scout('scout_sysroot_copy', scout, copy=True, gc=False) + + with self.subTest('transient'): + self._test_scout('scout_sysroot', scout, locales=True) def test_scout_usr(self) -> None: scout = os.path.join(self.containers_dir, 'scout', 'files') - self._test_scout('scout', scout, locales=True) + with self.subTest('only-prepare'): + self._test_scout('scout_prep', scout, copy=True, only_prepare=True) + + with self.subTest('copy'): + self._test_scout('scout_copy', scout, copy=True, locales=True) + + with self.subTest('transient'): + self._test_scout('scout', scout) if __name__ == '__main__': diff --git a/tests/inside-scout.py b/tests/inside-scout.py index b7e19585ee2db2c8ae6fbc3dbe9f68ad5733f211..58e402ff8b7cc4cf3b9e6298412b52f5a2127ce2 100755 --- a/tests/inside-scout.py +++ b/tests/inside-scout.py @@ -5,6 +5,7 @@ import contextlib import ctypes +import errno import json import logging import os @@ -146,6 +147,14 @@ class TestInsideScout(BaseTest): ) # No actual *tests* here just yet - we just log what's there. + def test_overrides(self) -> None: + if os.getenv('TEST_INSIDE_SCOUT_IS_COPY'): + target = os.readlink('/overrides') + self.assertEqual(target, 'usr/lib/pressure-vessel/overrides') + + self.assertTrue(Path('/overrides').is_dir()) + self.assertTrue(Path('/overrides/lib').is_dir()) + def test_glibc(self) -> None: """ Assert that we took the glibc version from the host OS. @@ -155,6 +164,8 @@ class TestInsideScout(BaseTest): and in cases where our glibc is the same version as the glibc of the host OS, we prefer the host. """ + overrides = Path('/overrides').resolve() + glibc = ctypes.cdll.LoadLibrary('libc.so.6') gnu_get_libc_version = glibc.gnu_get_libc_version gnu_get_libc_version.restype = ctypes.c_char_p @@ -167,7 +178,10 @@ class TestInsideScout(BaseTest): # closely enough. On x86_64 it does, and on i386 we have # symlinks /overrides/lib/i[456]86-linux-gnu. host_glibc = ctypes.cdll.LoadLibrary( - '/overrides/lib/{}-linux-gnu/libc.so.6'.format(os.uname().machine), + '/{}/lib/{}-linux-gnu/libc.so.6'.format( + overrides, + os.uname().machine, + ), ) gnu_get_libc_version = host_glibc.gnu_get_libc_version gnu_get_libc_version.restype = ctypes.c_char_p @@ -214,6 +228,8 @@ class TestInsideScout(BaseTest): self.assertEqual(really_stat.st_ino, expected_stat.st_ino) def test_srsi(self) -> None: + overrides = Path('/overrides').resolve() + if 'HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON' in os.environ: with open( os.environ['HOST_STEAM_RUNTIME_SYSTEM_INFO_JSON'], @@ -416,7 +432,7 @@ class TestInsideScout(BaseTest): # version of the Steam Runtime. self.assertEqual( arch_info['library-details'][soname]['path'], - '/overrides/lib/{}/{}'.format(multiarch, soname), + '{}/lib/{}/{}'.format(overrides, multiarch, soname), ) for soname in ( @@ -461,7 +477,9 @@ class TestInsideScout(BaseTest): for k, vs in expect_symlinks.items(): with self.subTest(dri_symlink=k): - link = '/overrides/lib/{}/dri/{}'.format(multiarch, k) + link = '{}/lib/{}/dri/{}'.format( + overrides, multiarch, k, + ) logger.info('Target of %s should be in %s', link, vs) target = os.readlink(link) @@ -541,6 +559,46 @@ class TestInsideScout(BaseTest): set(arch_info['library-issues-summary']), ) + def test_read_only(self) -> None: + for read_only_place in ( + '/bin', + '/etc/ld.so.conf.d', + '/lib', + '/overrides', + '/overrides/lib', + '/run/host/bin', + '/run/host/lib', + '/run/host/usr', + '/run/host/usr/bin', + '/run/host/usr/lib', + '/run/pressure-vessel/pv-from-host', + '/run/pressure-vessel/pv-from-host/bin', + '/sbin', + '/usr', + '/usr/lib', + '/usr/lib/pressure-vessel/from-host/bin', + '/usr/lib/pressure-vessel/overrides', + '/usr/lib/pressure-vessel/overrides/lib', + ): + with self.subTest(read_only_place): + if ( + read_only_place.startswith('/overrides') + and not os.getenv('TEST_INSIDE_SCOUT_IS_COPY') + ): + # If we aren't working from a temporary copy of the + # runtime, /overrides is on a tmpfs + continue + + with self.assertRaises(OSError) as raised: + open(os.path.join(read_only_place, 'hello'), 'w') + + if isinstance(raised.exception, FileNotFoundError): + # Some of these paths don't exist under all + # circumstances + continue + + self.assertEqual(raised.exception.errno, errno.EROFS) + if __name__ == '__main__': assert sys.version_info >= (3, 5), 'Python 3.5+ is required' diff --git a/tests/invocation.py b/tests/invocation.py index 44248abddb33dfd7d54fe0f415e5c828bb5ec078..7d8a7f737981c1f7879274d4200f224e427d0a83 100644 --- a/tests/invocation.py +++ b/tests/invocation.py @@ -64,6 +64,14 @@ class TestInvocation(BaseTest): ) self.assertEqual(completed.returncode, 2) + def test_only_prepare_exclusive(self) -> None: + completed = run_subprocess( + [self.pv_wrap, '--only-prepare', '--test'], + stdout=2, + stderr=2, + ) + self.assertEqual(completed.returncode, 2) + if __name__ == '__main__': assert sys.version_info >= (3, 4), \