diff --git a/debian/control b/debian/control index 4bba83d208fcee7028c7483315419f4497de9b25..8ec09e31f18c7df95060300338f527d40e5c2894 100644 --- a/debian/control +++ b/debian/control @@ -6,6 +6,7 @@ Standards-Version: 4.4.0 Build-Depends: debhelper (>= 9), g++ (>= 4:4.8) | g++-4.8, + libelf-dev, libglib2.0-dev, libsteam-runtime-tools-0-dev (>= 0.20200331.1~), libxau-dev, diff --git a/meson.build b/meson.build index e29eb930e14ac0cce9bd53674cbff1aaca85127f..78edcb1e5b1db960329fca5efdaaaff8b3ff4da4 100644 --- a/meson.build +++ b/meson.build @@ -150,6 +150,11 @@ glib_tap_support = dependency( required : false, ) xau = dependency('xau', required : true) +libelf = dependency('libelf', required : false) + +if not libelf.found() + libelf = declare_dependency(link_args : ['-lelf']) +endif scripts = [ 'pressure-vessel-locale-gen', diff --git a/src/elf-utils.c b/src/elf-utils.c new file mode 100644 index 0000000000000000000000000000000000000000..5fcb790bc29f69b7998fd6283334d7dfabdff4a0 --- /dev/null +++ b/src/elf-utils.c @@ -0,0 +1,158 @@ +/* + * Copyright © 2017-2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "elf-utils.h" + +#include <glib.h> +#include <glib/gstdio.h> +#include <gio/gio.h> +#include <libelf.h> + +#include "libglnx/libglnx.h" + +#include "glib-backports.h" + +#define throw_elf_error(error, format, ...) \ + glnx_null_throw (error, format ": %s", ##__VA_ARGS__, elf_errmsg (elf_errno ())) + +/* + * pv_elf_open_fd: + * @fd: An open file descriptor + * + * Returns: (transfer full): A libelf object representing the library + */ +Elf * +pv_elf_open_fd (int fd, + GError **error) +{ + g_autoptr(Elf) elf = NULL; + + g_return_val_if_fail (fd >= 0, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + if (elf_version (EV_CURRENT) == EV_NONE) + return throw_elf_error (error, "elf_version(EV_CURRENT)"); + + elf = elf_begin (fd, ELF_C_READ, NULL); + + if (elf == NULL) + return throw_elf_error (error, "elf_begin"); + + return g_steal_pointer (&elf); +} + +/* + * pv_elf_get_soname: + * @elf: A libelf object + * + * Return the `DT_SONAME` header, or %NULL on error + */ +gchar * +pv_elf_get_soname (Elf *elf, + GError **error) +{ + GElf_Ehdr ehdr; + size_t phdr_count = 0; + size_t i; + GElf_Phdr phdr_mem; + GElf_Phdr *phdr = NULL; + GElf_Dyn *dynamic_header = NULL; + GElf_Dyn dynamic_header_mem; + Elf_Scn *dynamic_section = NULL; + GElf_Shdr dynamic_section_header_mem; + GElf_Shdr *dynamic_section_header = NULL; + Elf_Scn *string_table_section = NULL; + GElf_Shdr string_table_header_mem; + GElf_Shdr *string_table_header = NULL; + Elf_Data *data = NULL; + size_t size_per_dyn; + const char *soname = NULL; + + g_return_val_if_fail (elf != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + if (elf_kind (elf) != ELF_K_ELF) + return glnx_null_throw (error, "elf_kind %d, expected ELF_K_ELF=%d", + elf_kind (elf), ELF_K_ELF); + + if (gelf_getehdr (elf, &ehdr) == NULL) + return throw_elf_error (error, "elf_getehdr"); + + if (ehdr.e_type != ET_DYN) + return glnx_null_throw (error, "ehdr.e_type %d, expected ET_DYN=%d", + ehdr.e_type, ET_DYN); + + if (elf_getphdrnum (elf, &phdr_count) != 0) + return throw_elf_error (error, "elf_getphdrnum"); + + for (i = 0; i < phdr_count; i++) + { + phdr = gelf_getphdr (elf, i, &phdr_mem); + + if (phdr != NULL && phdr->p_type == PT_DYNAMIC) + { + dynamic_section = gelf_offscn (elf, phdr->p_offset); + dynamic_section_header = gelf_getshdr (dynamic_section, + &dynamic_section_header_mem); + break; + } + } + + if (dynamic_section == NULL || dynamic_section_header == NULL) + return glnx_null_throw (error, "Unable to find dynamic section header"); + + string_table_section = elf_getscn (elf, dynamic_section_header->sh_link); + string_table_header = gelf_getshdr (string_table_section, + &string_table_header_mem); + + if (string_table_section == NULL || string_table_header == NULL) + return glnx_null_throw (error, "Unable to find linked string table"); + + data = elf_getdata (dynamic_section, NULL); + + if (data == NULL) + return throw_elf_error (error, "elf_getdata(dynamic_section)"); + + size_per_dyn = gelf_fsize (elf, ELF_T_DYN, 1, EV_CURRENT); + + for (i = 0; i < dynamic_section_header->sh_size / size_per_dyn; i++) + { + dynamic_header = gelf_getdyn (data, i, &dynamic_header_mem); + + if (dynamic_header == NULL) + break; + + if (dynamic_header->d_tag == DT_SONAME) + { + soname = elf_strptr (elf, dynamic_section_header->sh_link, + dynamic_header->d_un.d_val); + + if (soname == NULL) + return glnx_null_throw (error, "Unable to read DT_SONAME"); + + + } + } + + if (soname == NULL) + return glnx_null_throw (error, "Unable to find DT_SONAME"); + + return g_strdup (soname); +} + diff --git a/src/elf-utils.h b/src/elf-utils.h new file mode 100644 index 0000000000000000000000000000000000000000..fa89652e727feb5ca26b5ced048203434fceffb5 --- /dev/null +++ b/src/elf-utils.h @@ -0,0 +1,36 @@ +/* + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <gelf.h> +#include <glib.h> +#include <libelf.h> + +#include "libglnx/libglnx.h" + +#include "glib-backports.h" + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (Elf, elf_end); + +Elf *pv_elf_open_fd (int fd, + GError **error); + +gchar *pv_elf_get_soname (Elf *elf, + GError **error); diff --git a/src/meson.build b/src/meson.build index 9d9e816a184bf100c1c91729cddb2b5a4782f337..220d1d30be08deec869384d4fab3bf2573979338 100644 --- a/src/meson.build +++ b/src/meson.build @@ -23,6 +23,7 @@ # Headers to scan for enum/flags types. headers = [ + 'resolve-in-sysroot.h', 'runtime.h', ] @@ -36,17 +37,22 @@ pressure_vessel_utils = static_library( sources : [ 'bwrap-lock.c', 'bwrap-lock.h', + 'elf-utils.c', + 'elf-utils.h', 'flatpak-utils-base.c', 'flatpak-utils-base-private.h', 'flatpak-utils.c', 'flatpak-utils-private.h', 'glib-backports.c', 'glib-backports.h', + 'resolve-in-sysroot.c', + 'resolve-in-sysroot.h', 'utils.c', 'utils.h', ], dependencies : [ gio_unix, + libelf, libglnx.get_variable('libglnx_dep'), ], include_directories : project_include_dirs, diff --git a/src/resolve-in-sysroot.c b/src/resolve-in-sysroot.c new file mode 100644 index 0000000000000000000000000000000000000000..8fe6cadb3b1a16f02f4e24442e8c6825b302811e --- /dev/null +++ b/src/resolve-in-sysroot.c @@ -0,0 +1,327 @@ +/* + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "resolve-in-sysroot.h" + +#include <glib.h> +#include <glib/gstdio.h> +#include <gio/gio.h> + +#include "libglnx/libglnx.h" + +#include "glib-backports.h" + +/* Enabling debug logging for this is rather too verbose, so only + * enable it when actively debugging this module */ +#if 0 +#define trace(...) g_debug (__VA_ARGS__) +#else +#define trace(...) do { } while (0) +#endif + +/* + * clear_fd: + * @p: A pointer into the data array underlying a #GArray. + * + * Close the fd pointed to by @p, and set `*(int *) p` to -1. + * + * This wraps glnx_close_fd() with the signature required by + * g_array_set_clear_func(). + */ +static void +clear_fd (void *p) +{ + glnx_close_fd (p); +} + +/* + * Steal `*fdp` and append it to @fds. + * + * We can't just use g_array_append_val (fds, glnx_steal_fd (&fd)) + * because g_array_append_val is a macro that takes a pointer to its + * argument. + */ +static inline void +fd_array_take (GArray *fds, + int *fdp) +{ + int fd = glnx_steal_fd (fdp); + + g_array_append_val (fds, fd); +} + +/* + * pv_resolve_in_sysroot: + * @sysroot: (transfer none): A file descriptor representing the root + * @descendant: (type filename): A path below the root directory, either + * absolute or relative (to the root) + * @flags: Flags affecting how we resolve the path + * @real_path_out: (optional) (out) (type filename): If not %NULL, used to + * return the path to @descendant below @sysroot + * @error: Used to raise an error on failure + * + * Open @descendant as though @sysroot was the root directory. + * + * If %PV_RESOLVE_FLAGS_MKDIR_P is in @flags, each path segment in + * @descendant must be a directory, a symbolic link to a directory, + * or nonexistent (in which case a directory will be created, currently + * with hard-coded 0700 permissions). + * + * Returns: An `O_PATH` file descriptor pointing to @descendant, + * or -1 on error + */ +int +pv_resolve_in_sysroot (int sysroot, + const char *descendant, + PvResolveFlags flags, + gchar **real_path_out, + GError **error) +{ + g_autoptr(GString) current_path = g_string_new (""); + /* Array of fds pointing to directories beneath @sysroot. + * The 0'th element is sysroot itself, the 1st element is a direct + * child of sysroot and so on. The last element can be a + * non-directory. */ + g_autoptr(GArray) fds = NULL; + /* @buffer contains parts of @descendant. We edit it in-place to replace + * each directory separator we have dealt with by \0. */ + g_autofree gchar *buffer = g_strdup (descendant); + /* @remaining points to the remaining path to traverse. For example, + * if we are trying to resolve a/b/c/d, we have already opened a, and we + * will open b next, then @buffer contains "a\0b/c" and remaining + * points to b. */ + gchar *remaining; + + g_return_val_if_fail (sysroot > 0, -1); + g_return_val_if_fail (descendant != NULL, -1); + g_return_val_if_fail (real_path_out == NULL || *real_path_out == NULL, -1); + g_return_val_if_fail (error == NULL || *error == NULL, -1); + + { + glnx_autofd int fd = -1; + + fd = TEMP_FAILURE_RETRY (fcntl (sysroot, F_DUPFD_CLOEXEC, 0)); + + if (fd < 0) + { + glnx_throw_errno_prefix (error, "Unable to duplicate fd \"%d\"", + sysroot); + return -1; + } + + fds = g_array_new (FALSE, FALSE, sizeof (int)); + g_array_set_clear_func (fds, clear_fd); + fd_array_take (fds, &fd); + } + + remaining = buffer; + + while (remaining != NULL) + { + g_autofree gchar *target = NULL; + glnx_autofd int fd = -1; + const gchar *next; + gchar *slash; /* Points into @buffer */ + int open_flags; + + /* Ignore excess slashes */ + while (remaining[0] == '/') + remaining++; + + next = remaining; + + if (next[0] == '\0') + break; + + slash = strchr (remaining, '/'); + + if (slash == NULL) + { + trace ("Done so far: \"%s\"; next: \"%s\"; remaining: nothing", + current_path->str, next); + remaining = NULL; + } + else + { + *slash = '\0'; + remaining = slash + 1; + trace ("Done so far: \"%s\"; next: \"%s\"; remaining: \"%s\"", + current_path->str, next, remaining); + } + + /* Ignore ./ path segments */ + if (strcmp (next, ".") == 0) + continue; + + /* Implement ../ by going up a level - unless we would escape + * from the sysroot, in which case do nothing */ + if (strcmp (next, "..") == 0) + { + const gchar *last_slash; + + if (fds->len >= 2) + g_array_set_size (fds, fds->len - 1); + /* else silently ignore ../ when already at the root, the same + * as the kernel would */ + + last_slash = strrchr (current_path->str, '/'); + + if (last_slash != NULL) + g_string_truncate (current_path, last_slash - current_path->str); + else + g_string_truncate (current_path, 0); + + continue; + } + + /* Open @next with O_NOFOLLOW, so that if it's a symbolic link, + * we open the symbolic link itself and not whatever it points to */ + open_flags = O_CLOEXEC | O_NOFOLLOW | O_PATH; + fd = TEMP_FAILURE_RETRY (openat (g_array_index (fds, int, fds->len - 1), + next, open_flags)); + + if (fd < 0 && errno == ENOENT && (flags & PV_RESOLVE_FLAGS_MKDIR_P) != 0) + { + if (TEMP_FAILURE_RETRY (mkdirat (g_array_index (fds, int, fds->len - 1), + next, 0700)) != 0) + { + glnx_throw_errno_prefix (error, "Unable to create \"%s/%s\"", + current_path->str, next); + return -1; + } + + g_debug ("Created \"%s/%s\" in /proc/self/fd/%d", + current_path->str, next, sysroot); + + fd = TEMP_FAILURE_RETRY (openat (g_array_index (fds, int, fds->len - 1), + next, open_flags | O_DIRECTORY)); + } + + if (fd < 0) + { + glnx_throw_errno_prefix (error, "Unable to open \"%s/%s\"", + current_path->str, next); + return -1; + } + + /* Maybe it's a symlink? */ + target = glnx_readlinkat_malloc (fd, "", NULL, NULL); + + if (target != NULL) /* Yes, it's a symlink */ + { + if (flags & PV_RESOLVE_FLAGS_REJECT_SYMLINKS) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS, + "\"%s/%s\" is a symlink", current_path->str, next); + return -1; + } + else if ((flags & PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK) != 0 && + remaining == NULL) + { + /* Treat as though not a symlink. */ + g_clear_pointer (&target, g_free); + } + } + + if (target != NULL) + { + /* This isn't g_autofree because clang would warn about it + * being unused. We need to keep it alive until we change + * @remaining to point into the new buffer. */ + gchar *old_buffer = NULL; + + if (target[0] == '/') + { + /* For example if we were asked to resolve foo/bar/a/b, + * but bar is a symlink to /x/y, we restart from the beginning as though + * we had been asked to resolve x/y/a/b */ + trace ("Absolute symlink to \"%s\"", target); + g_string_set_size (current_path, 0); + g_array_set_size (fds, 1); + } + else + { + /* For example if we were asked to resolve foo/bar/a/b, + * but bar is a symlink to ../x/y, we continue as though + * we had been asked to resolve foo/../x/y/baz */ + trace ("Relative symlink to \"%s\"/\"%s\"", + current_path->str, target); + } + + old_buffer = g_steal_pointer (&buffer); + buffer = g_build_filename (target, remaining, NULL); + remaining = buffer; + g_free (old_buffer); + } + else /* Not a symlink, or a symlink but we are returning it anyway. */ + { + /* If we are emulating mkdir -p, or if we will go on to open + * a member of @fd, then it had better be a directory. */ + if ((flags & PV_RESOLVE_FLAGS_MKDIR_P) != 0 || + remaining != NULL) + { + struct stat stat_buf; + + if (!glnx_fstatat (fd, "", &stat_buf, AT_EMPTY_PATH, error)) + { + g_prefix_error (error, + "Unable to determine whether \"%s\" " + "is a directory", + current_path->str); + return -1; + } + + if (!S_ISDIR (stat_buf.st_mode)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY, + "\"%s\" is not a directory", + current_path->str); + } + } + + if (current_path->len != 0) + g_string_append_c (current_path, '/'); + + g_string_append (current_path, next); + fd_array_take (fds, &fd); + } + } + + if (real_path_out != NULL) + *real_path_out = g_string_free (g_steal_pointer (¤t_path), FALSE); + + if (flags & PV_RESOLVE_FLAGS_READABLE) + { + g_autofree char *proc_fd_name = g_strdup_printf ("/proc/self/fd/%d", + g_array_index (fds, int, + fds->len - 1)); + glnx_autofd int fd = -1; + + if (!glnx_openat_rdonly (-1, proc_fd_name, TRUE, &fd, error)) + return -1; + + return glnx_steal_fd (&fd); + } + + /* Taking the address might look like nonsense here, but it's + * documented to work: g_array_index expands to fds->data[some_offset]. + * We need to steal ownership of the fd back from @fds so it won't be + * closed with the rest of them when @fds is freed. */ + return glnx_steal_fd (&g_array_index (fds, int, fds->len - 1)); +} diff --git a/src/resolve-in-sysroot.h b/src/resolve-in-sysroot.h new file mode 100644 index 0000000000000000000000000000000000000000..2941030c63d2328301b0e1df4fe80541d3bcf17a --- /dev/null +++ b/src/resolve-in-sysroot.h @@ -0,0 +1,52 @@ +/* + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> + +/* + * PvResolveFlags: + * @PV_RESOLVE_FLAGS_MKDIR_P: Create the filename to be resolved and + * all of its ancestors as directories. If any already exist, they + * must be directories or symlinks to directories. + * @PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK: If the last component of + * the path is a symlink, return a fd pointing to the symlink itself. + * @PV_RESOLVE_FLAGS_REJECT_SYMLINKS: If any component of + * the path is a symlink, fail with %G_IO_ERROR_TOO_MANY_LINKS. + * @PV_RESOLVE_FLAGS_READABLE: Open the last component of the path + * for reading, instead of just as `O_PATH`. + * @PV_RESOLVE_FLAGS_NONE: No special behaviour. + * + * Flags affecting how pv_resolve_in_sysroot() behaves. + */ +typedef enum +{ + PV_RESOLVE_FLAGS_MKDIR_P = (1 << 0), + PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK = (1 << 1), + PV_RESOLVE_FLAGS_REJECT_SYMLINKS = (1 << 2), + PV_RESOLVE_FLAGS_READABLE = (1 << 3), + PV_RESOLVE_FLAGS_NONE = 0 +} PvResolveFlags; + +int pv_resolve_in_sysroot (int sysroot, + const char *descendant, + PvResolveFlags flags, + gchar **real_path_out, + GError **error) G_GNUC_WARN_UNUSED_RESULT; diff --git a/src/runtime.c b/src/runtime.c index b4def4e1de4e7f1808360b230de1ef201f902c4c..71a62fc7b4f8c210bb1006f7f3184f8c3241a0a6 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -61,6 +61,7 @@ struct _PvRuntime PvRuntimeFlags flags; gboolean any_libc_from_host; gboolean all_libc_from_host; + gboolean runtime_is_just_usr; }; struct _PvRuntimeClass @@ -337,9 +338,14 @@ pv_runtime_initable_init (GInitable *initable, self->runtime_usr = g_build_filename (self->source_files, "usr", NULL); - if (!g_file_test (self->runtime_usr, G_FILE_TEST_IS_DIR)) + if (g_file_test (self->runtime_usr, G_FILE_TEST_IS_DIR)) + { + self->runtime_is_just_usr = FALSE; + } + else { /* source_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); } @@ -516,22 +522,65 @@ static gboolean pv_runtime_provide_container_access (PvRuntime *self, GError **error) { - /* TODO: Avoid using bwrap if we don't need to: when run from inside - * a Flatpak, it won't work. - * - * If we are working with a non-merged-/usr runtime, we can just - * set self->container_access to its path. - * - * Similarly, if we are working with a writeable copy of a runtime - * that we are editing in-place, we can set self->container_access to - * that. */ - - if (self->container_access_adverb == NULL) + if (self->container_access_adverb != NULL) + return TRUE; + + if (!self->runtime_is_just_usr) { - self->container_access = g_build_filename (self->tmpdir, "mnt", NULL); + static const char * const need_top_level[] = + { + "bin", + "etc", + "lib", + "sbin", + }; + gsize i; + + /* If we are working with a runtime that has a root directory containing + * /etc and /usr, we can just access it via its path - that's "the same + * 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. */ + 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); + + /* 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, + * but that doesn't guarantee that it has /bin, /lib, /sbin (either + * in the form of real directories or symlinks into /usr) and /etc + * (for at least /etc/alternatives and /etc/ld.so.cache). + * + * This check is not intended to be exhaustive, merely something + * that will catch obvious mistakes like completely forgetting to + * add the merged-/usr symlinks. + * + * In practice we also need /lib64 for 64-bit-capable runtimes, + * but a pure 32-bit runtime would legitimately not have that, + * 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, + need_top_level[i], + NULL); + if (!g_file_test (path, G_FILE_TEST_IS_DIR)) + g_warning ("%s does not exist, this probably won't work", + path); + } + } + else + { + /* Otherwise, will we need to use bwrap to build a directory hierarchy + * that is the same shape as the final system. */ + g_debug ("%s: Using bwrap to set up runtime that is just /usr", + G_STRFUNC); + + self->container_access = g_build_filename (self->tmpdir, "mnt", NULL); g_mkdir (self->container_access, 0700); - self->container_access = self->container_access; self->container_access_adverb = flatpak_bwrap_new (NULL); flatpak_bwrap_add_args (self->container_access_adverb, @@ -564,9 +613,10 @@ try_bind_dri (PvRuntime *self, { g_autoptr(FlatpakBwrap) temp_bwrap = NULL; g_autofree gchar *expr = NULL; - g_autofree gchar *host_dri = NULL; - g_autofree gchar *dest_dri = NULL; + g_autoptr(GDir) dir = NULL; + const char *member; + g_debug ("Collecting dependencies of DRI drivers in \"%s\"...", dri); expr = g_strdup_printf ("only-dependencies:if-exists:path-match:%s/dri/*.so", libdir); @@ -589,28 +639,34 @@ try_bind_dri (PvRuntime *self, g_clear_pointer (&temp_bwrap, flatpak_bwrap_free); - /* TODO: If we're already in a container, rely on /run/host - * already being mounted, so we don't need to re-enter a container - * here. */ - host_dri = g_build_filename ("/run/host", libdir, "dri", NULL); - dest_dri = g_build_filename (arch->libdir_on_host, "dri", NULL); - temp_bwrap = flatpak_bwrap_new (NULL); - flatpak_bwrap_add_args (temp_bwrap, - self->bubblewrap, - "--ro-bind", "/", "/", - "--tmpfs", "/run", - "--ro-bind", "/", "/run/host", - "--bind", self->overrides, self->overrides, - "sh", "-c", - "ln -fns \"$1\"/* \"$2\"", - "sh", /* $0 */ - host_dri, - dest_dri, - NULL); - flatpak_bwrap_finish (temp_bwrap); + dir = g_dir_open (dri, 0, error); - if (!pv_bwrap_run_sync (temp_bwrap, NULL, error)) + if (dir == NULL) return FALSE; + + for (member = g_dir_read_name (dir); + member != NULL; + member = g_dir_read_name (dir)) + { + g_autofree gchar *target = g_build_filename ("/run/host", dri, + member, NULL); + g_autofree gchar *dest = g_build_filename (arch->libdir_on_host, + "dri", member, NULL); + + g_debug ("Creating symbolic link \"%s\" -> \"%s\" for \"%s\" DRI driver", + dest, target, arch->tuple); + + /* Delete an existing symlink if any, like ln -f */ + if (unlink (dest) != 0 && errno != ENOENT) + return glnx_throw_errno_prefix (error, + "Unable to remove \"%s\"", + dest); + + if (symlink (target, dest) != 0) + return glnx_throw_errno_prefix (error, + "Unable to create symlink \"%s\" -> \"%s\"", + dest, target); + } } if (g_file_test (s2tc, G_FILE_TEST_EXISTS)) @@ -618,6 +674,7 @@ try_bind_dri (PvRuntime *self, g_autoptr(FlatpakBwrap) temp_bwrap = NULL; g_autofree gchar *expr = NULL; + g_debug ("Collecting s2tc \"%s\" and its dependencies...", s2tc); expr = g_strdup_printf ("path-match:%s", s2tc); if (!pv_runtime_provide_container_access (self, error)) @@ -674,16 +731,11 @@ ensure_locales (PvRuntime *self, locale_gen = g_build_filename (self->tools_dir, "pressure-vessel-locale-gen", NULL); - - flatpak_bwrap_add_args (run_locale_gen, - self->bubblewrap, - "--ro-bind", "/", "/", - NULL); - pv_bwrap_add_api_filesystems (run_locale_gen); + /* We don't actually need to use bwrap when we're just running on + * the host system. */ flatpak_bwrap_add_args (run_locale_gen, - "--bind", locales, locales, - "--chdir", locales, locale_gen, + "--output-dir", locales, "--verbose", NULL); } @@ -707,8 +759,8 @@ ensure_locales (PvRuntime *self, flatpak_bwrap_add_args (run_locale_gen, "--ro-bind", self->tools_dir, "/run/host/tools", "--bind", locales, "/overrides/locales", - "--chdir", "/overrides/locales", locale_gen, + "--output-dir", "/overrides/locales", "--verbose", NULL); } @@ -1284,6 +1336,7 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, return FALSE; flatpak_bwrap_add_args (temp_bwrap, + "env", "PATH=/usr/bin:/bin", "readlink", "-e", arch->ld_so, NULL); flatpak_bwrap_finish (temp_bwrap); @@ -1469,7 +1522,13 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, g_debug ("Making host ld.so visible in container"); - ld_so_in_host = flatpak_canonicalize_filename (arch->ld_so); + 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, @@ -1596,13 +1655,18 @@ pv_runtime_use_host_graphics_stack (PvRuntime *self, all_libdrm_from_host = FALSE; } + /* Order matters: drivers from a later entry will overwrite + * drivers from an earlier entry. Because we don't know whether + * /lib and /usr/lib are 32- or 64-bit, we need to prioritize + * libQUAL higher. Prioritize Debian-style multiarch higher + * still, because it's completely unambiguous. */ dirs = g_new0 (gchar *, 7); - dirs[0] = g_build_filename ("/lib", arch->tuple, NULL); - dirs[1] = g_build_filename ("/usr", "lib", arch->tuple, NULL); - dirs[2] = g_strdup ("/lib"); - dirs[3] = g_strdup ("/usr/lib"); - dirs[4] = g_build_filename ("/", arch->libqual, NULL); - dirs[5] = g_build_filename ("/usr", arch->libqual, NULL); + dirs[0] = g_strdup ("/lib"); + dirs[1] = g_strdup ("/usr/lib"); + dirs[2] = g_build_filename ("/", arch->libqual, NULL); + dirs[3] = g_build_filename ("/usr", arch->libqual, NULL); + dirs[4] = g_build_filename ("/lib", arch->tuple, NULL); + dirs[5] = g_build_filename ("/usr", "lib", arch->tuple, NULL); for (j = 0; j < 6; j++) { @@ -2078,16 +2142,8 @@ pv_runtime_set_search_paths (PvRuntime *self, FlatpakBwrap *bwrap) { g_autoptr(GString) ld_library_path = g_string_new (""); - g_autoptr(GString) bin_path = g_string_new (""); gsize i; - pv_search_path_append (bin_path, "/overrides/bin"); - pv_search_path_append (bin_path, g_getenv ("PATH")); - flatpak_bwrap_add_args (bwrap, - "--setenv", "PATH", - bin_path->str, - NULL); - /* TODO: Adapt the use_ld_so_cache code from Flatpak instead * of setting LD_LIBRARY_PATH, for better robustness against * games that set their own LD_LIBRARY_PATH ignoring what they @@ -2107,6 +2163,11 @@ pv_runtime_set_search_paths (PvRuntime *self, /* This would be filtered out by a setuid bwrap, so we have to go * via --setenv. */ flatpak_bwrap_add_args (bwrap, + /* The PATH from outside the container doesn't + * really make sense inside the container: + * in principle the layout could be totally + * different. */ + "--setenv", "PATH", "/overrides/bin:/usr/bin:/bin", "--setenv", "LD_LIBRARY_PATH", ld_library_path->str, NULL); diff --git a/sysroot/run-in-sysroot.py b/sysroot/run-in-sysroot.py index eb875f4270adecabf7cd09f2b69f7751fe9137d0..64a712de7772d4c5eb051a92f8169dca181e274e 100755 --- a/sysroot/run-in-sysroot.py +++ b/sysroot/run-in-sysroot.py @@ -46,6 +46,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--srcdir', default='.') parser.add_argument('--builddir', default='_build') + parser.add_argument('--rw', action='append', default=[]) parser.add_argument('--sysroot', default=None) parser.add_argument('--tarball', default=None) parser.add_argument('command') @@ -94,27 +95,48 @@ def main(): os.makedirs( os.path.join(abs_sysroot, './' + abs_builddir), exist_ok=True) - os.execvp( + argv = [ 'bwrap', - [ - 'bwrap', - '--ro-bind', abs_sysroot, '/', - '--bind', - os.path.join(abs_sysroot, 'var', 'lib', 'apt'), - '/var/lib/apt', - '--dev-bind', '/dev', '/dev', - '--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf', - '--proc', '/proc', - '--tmpfs', '/tmp', - '--tmpfs', '/var/tmp', - '--tmpfs', '/home', - '--bind', abs_srcdir, abs_srcdir, - '--bind', abs_builddir, abs_builddir, - '--chdir', os.getcwd(), - '--setenv', 'LC_ALL', 'C.UTF-8', - args.command, - ] + args.args, - ) + '--ro-bind', abs_sysroot, '/', + '--bind', + os.path.join(abs_sysroot, 'var', 'lib', 'apt'), + '/var/lib/apt', + '--dev-bind', '/dev', '/dev', + '--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf', + '--proc', '/proc', + '--tmpfs', '/tmp', + '--tmpfs', '/var/tmp', + '--tmpfs', '/home', + '--bind', abs_srcdir, abs_srcdir, + '--bind', abs_builddir, abs_builddir, + ] + + for var in ( + 'AUTOPKGTEST_ARTIFACTS', + 'DESTDIR', + 'G_TEST_BUILDDIR', + 'G_TEST_SRCDIR', + 'PRESSURE_VESSEL_TEST_CONTAINERS', + ): + if var in os.environ: + val = os.environ[var] + argv.extend([ + '--bind', val, val, + ]) + + for rw in args.rw: + argv.extend([ + '--bind', rw, rw, + ]) + + argv.extend([ + '--chdir', os.getcwd(), + '--setenv', 'LC_ALL', 'C.UTF-8', + args.command, + ]) + argv.extend(args.args) + + os.execvp('bwrap', argv) if __name__ == '__main__': diff --git a/tests/containers.py b/tests/containers.py index 7be681096170991204666dce2e9c774117e04f29..ae474cfc45dbd9a589ed7489afbc1dc96c07e952 100755 --- a/tests/containers.py +++ b/tests/containers.py @@ -117,7 +117,7 @@ class TestContainers(BaseTest): bwrap = os.environ.get('BWRAP', shutil.which('bwrap')) - if bwrap is not None and subprocess.run( + if bwrap is not None and run_subprocess( [bwrap, '--dev-bind', '/', '/', 'sh', '-c', 'true'], stdout=2, stderr=2, @@ -256,7 +256,7 @@ class TestContainers(BaseTest): os.path.join(cls.artifacts, 'host-srsi.json'), 'w', ) as writer: - subprocess.run( + run_subprocess( [ host_srsi, '--verbose', @@ -369,7 +369,7 @@ class TestContainers(BaseTest): with tee_file_and_stderr( os.path.join(artifacts, 'inside-scout.log') ) as tee: - completed = subprocess.run( + completed = self.run_subprocess( argv, cwd=self.artifacts, stdout=tee.stdin, diff --git a/tests/elf-get-soname.c b/tests/elf-get-soname.c new file mode 100644 index 0000000000000000000000000000000000000000..218623ed8cdd2bf5ab04c2504c9a54041b876120 --- /dev/null +++ b/tests/elf-get-soname.c @@ -0,0 +1,114 @@ +/* + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include "subprojects/libglnx/config.h" + +#include <locale.h> +#include <sysexits.h> + +#include "libglnx/libglnx.h" + +#include "glib-backports.h" + +#include "elf-utils.h" +#include "utils.h" + +static GOptionEntry options[] = +{ + { NULL } +}; + +int +main (int argc, + char *argv[]) +{ + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + int ret = EX_USAGE; + int i; + + setlocale (LC_ALL, ""); + pv_avoid_gvfs (); + + context = g_option_context_new ("LIBRARY..."); + g_option_context_add_main_entries (context, options, NULL); + + if (!g_option_context_parse (context, &argc, &argv, error)) + goto out; + + if (argc >= 2 && strcmp (argv[1], "--") == 0) + { + argv++; + argc--; + } + + if (argc < 2) + { + g_printerr ("A library to open is required\n"); + goto out; + } + + ret = 0; + + for (i = 1; i < argc; i++) + { + int fd = open (argv[i], O_RDONLY | O_CLOEXEC); + g_autoptr(Elf) elf = NULL; + g_autofree gchar *soname = NULL; + + if (fd < 0) + { + g_printerr ("Cannot open %s: %s\n", argv[i], g_strerror (errno)); + ret = 1; + continue; + } + + elf = pv_elf_open_fd (fd, error); + + if (elf == NULL) + { + g_printerr ("Unable to open %s: %s\n", + argv[i], local_error->message); + g_clear_error (error); + ret = 1; + continue; + } + + soname = pv_elf_get_soname (elf, error); + + if (soname == NULL) + { + g_printerr ("Unable to get SONAME of %s: %s\n", + argv[i], local_error->message); + g_clear_error (error); + ret = 1; + continue; + } + + g_print ("%s DT_SONAME: %s\n", argv[i], soname); + } + +out: + if (local_error != NULL) + g_warning ("%s", local_error->message); + + return ret; +} diff --git a/tests/inside-scout.py b/tests/inside-scout.py index 3b9f4885358c7de050a1a3d4eefaffaac4e41d63..b7e19585ee2db2c8ae6fbc3dbe9f68ad5733f211 100755 --- a/tests/inside-scout.py +++ b/tests/inside-scout.py @@ -363,13 +363,14 @@ class TestInsideScout(BaseTest): arch=multiarch, ): self.assertTrue(arch_info['can-run']) - self.assertEqual([], arch_info['library-issues-summary']) # Graphics driver support depends on the host system, so we # don't assert that everything is fine, only that we have # the information. self.assertIn('graphics-details', arch_info) self.assertIn('glx/gl', arch_info['graphics-details']) + expect_library_issues = set() + for soname, details in arch_info['library-details'].items(): with self.catch( 'per-library information', @@ -377,6 +378,15 @@ class TestInsideScout(BaseTest): arch=multiarch, soname=soname, ): + if soname == 'libldap-2.4.so.2': + # On Debian, libldap-2.4.so.2 is really an alias + # for libldap_r-2.4.so.2; but on Arch Linux they + # are separate libraries, and this causes trouble + # for our library-loading. Ignore failure to load + # the former. + expect_library_issues |= set(details.get('issues', [])) + continue + self.assertIn('path', details) self.assertEqual( [], @@ -521,6 +531,16 @@ class TestInsideScout(BaseTest): details.get(key), ) + with self.catch( + 'per-architecture information', + diagnostic=arch_info, + arch=multiarch, + ): + self.assertEqual( + expect_library_issues, + set(arch_info['library-issues-summary']), + ) + if __name__ == '__main__': assert sys.version_info >= (3, 5), 'Python 3.5+ is required' diff --git a/tests/meson.build b/tests/meson.build index 8b2bc8d6f9905cd7ba3f4998550998c3ed42d82c..b122e5e4850b367b3d7c400aa9bbfcd103eb9e03 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -43,6 +43,7 @@ tests = [ compiled_tests = [ 'bwrap-lock', + 'resolve-in-sysroot', 'utils', ] @@ -101,20 +102,28 @@ foreach test_name : tests endif endforeach -executable( - 'test-cheap-copy', - sources : [ - 'cheap-copy.c', - ], - dependencies : [ - gio_unix, - libglnx.get_variable('libglnx_dep'), - ], - link_with : [ - pressure_vessel_utils, - ], - include_directories : project_include_dirs, - install : false, -) +# Helper programs and manual tests +helpers = [ + 'cheap-copy', + 'elf-get-soname', +] + +foreach helper : helpers + executable( + 'test-' + helper, + sources : [ + helper + '.c', + ], + dependencies : [ + gio_unix, + libglnx.get_variable('libglnx_dep'), + ], + link_with : [ + pressure_vessel_utils, + ], + include_directories : project_include_dirs, + install : false, + ) +endforeach # vim:set sw=2 sts=2 et: diff --git a/tests/resolve-in-sysroot.c b/tests/resolve-in-sysroot.c new file mode 100644 index 0000000000000000000000000000000000000000..7256764fc52b179c53d23805120401b020017add --- /dev/null +++ b/tests/resolve-in-sysroot.c @@ -0,0 +1,273 @@ +/* + * Copyright © 2019-2020 Collabora Ltd. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gstdio.h> + +#include "glib-backports.h" +#include "libglnx/libglnx.h" + +#include "resolve-in-sysroot.h" +#include "test-utils.h" +#include "utils.h" + +typedef struct +{ + TestsOpenFdSet old_fds; +} Fixture; + +static void +setup (Fixture *f, + gconstpointer context) +{ + f->old_fds = tests_check_fd_leaks_enter (); +} + +static void +teardown (Fixture *f, + gconstpointer context) +{ + tests_check_fd_leaks_leave (f->old_fds); +} + +static gboolean +fd_same_as_rel_path_nofollow (int fd, + int dfd, + const gchar *path) +{ + GStatBuf fd_buffer, path_buffer; + + return (fstat (fd, &fd_buffer) == 0 + && fstatat (dfd, path, &path_buffer, AT_SYMLINK_NOFOLLOW) == 0 + && fd_buffer.st_dev == path_buffer.st_dev + && fd_buffer.st_ino == path_buffer.st_ino); +} + +typedef struct +{ + const char *name; + const char *target; +} Symlink; + +typedef enum +{ + RESOLVE_ERROR_DOMAIN_NONE, + RESOLVE_ERROR_DOMAIN_GIO, +} ResolveErrorDomain; + +typedef enum +{ + RESOLVE_CALL_FLAGS_IGNORE_PATH = (1 << 0), + RESOLVE_CALL_FLAGS_NONE = 0 +} ResolveCallFlags; + +typedef struct +{ + struct + { + const char *path; + PvResolveFlags flags; + ResolveCallFlags test_flags; + } call; + struct + { + const char *path; + int code; + } expect; +} ResolveTest; + +static void +test_resolve_in_sysroot (Fixture *f, + gconstpointer context) +{ + static const char * const prepare_dirs[] = + { + "a/b/c/d/e", + "a/b2/c2/d2/e2", + }; + static const Symlink prepare_symlinks[] = + { + { "a/b/symlink_to_c", "c" }, + { "a/b/symlink_to_b2", "../b2" }, + { "a/b/symlink_to_c2", "../../a/b2/c2" }, + { "a/b/abs_symlink_to_run", "/run" }, + { "a/b/long_symlink_to_dev", "../../../../../../../../../../../dev" }, + { "x", "create_me" }, + }; + static const ResolveTest tests[] = + { + { { "a/b/c/d" }, { "a/b/c/d" } }, + { + { "a/b/c/d", PV_RESOLVE_FLAGS_NONE, RESOLVE_CALL_FLAGS_IGNORE_PATH }, + { "a/b/c/d" }, + }, + { { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d" } }, + { + { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P, RESOLVE_CALL_FLAGS_IGNORE_PATH }, + { "a/b/c/d" }, + }, + { { "create_me" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { + { "create_me", PV_RESOLVE_FLAGS_NONE, RESOLVE_CALL_FLAGS_IGNORE_PATH }, + { NULL, G_IO_ERROR_NOT_FOUND } + }, + { { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d" } }, + { { "a/b///////.////./././///././c/d" }, { "a/b/c/d" } }, + { { "/a/b///////.////././../b2////././c2/d2" }, { "a/b2/c2/d2" } }, + { { "a/b/c/d/e/f" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { { "a/b/c/d/e/f", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d/e/f" } }, + { { "a/b/c/d/e/f" }, { "a/b/c/d/e/f" } }, + { { "a/b/c/d/e/f", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d/e/f" } }, + { { "a3/b3/c3" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { { "a3/b3/c3", PV_RESOLVE_FLAGS_MKDIR_P }, { "a3/b3/c3" } }, + { { "a/b/symlink_to_c" }, { "a/b/c" } }, + { { "a/b/symlink_to_c/d" }, { "a/b/c/d" } }, + { + { "a/b/symlink_to_c/d", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK }, + { "a/b/c/d" } + }, + { + { "a/b/symlink_to_c/d", PV_RESOLVE_FLAGS_REJECT_SYMLINKS }, + { NULL, G_IO_ERROR_TOO_MANY_LINKS } + }, + { { "a/b/symlink_to_b2" }, { "a/b2" } }, + { { "a/b/symlink_to_c2" }, { "a/b2/c2" } }, + { { "a/b/abs_symlink_to_run" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { + { "a/b/abs_symlink_to_run", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK }, + { "a/b/abs_symlink_to_run" } + }, + { { "run" }, { NULL, G_IO_ERROR_NOT_FOUND } }, /* Wasn't created yet */ + { { "a/b/abs_symlink_to_run", PV_RESOLVE_FLAGS_MKDIR_P }, { "run" } }, + { { "a/b/abs_symlink_to_run/host" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { { "a/b/abs_symlink_to_run/host", PV_RESOLVE_FLAGS_MKDIR_P }, { "run/host" } }, + { { "a/b/long_symlink_to_dev" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { { "a/b/long_symlink_to_dev/shm" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { { "a/b/long_symlink_to_dev/shm", PV_RESOLVE_FLAGS_MKDIR_P }, { "dev/shm" } }, + { { "a/b/../b2/c2/../c3", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b2/c3" } }, + { { "x" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { { "x", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK }, { "x" } }, + /* This is a bit odd: unlike mkdir -p, we create targets for dangling + * symlinks. It's easier to do this than not, and for pressure-vessel's + * use-case it probably even makes more sense than not. */ + { { "x/y" }, { NULL, G_IO_ERROR_NOT_FOUND } }, + { { "x/y", PV_RESOLVE_FLAGS_MKDIR_P }, { "create_me/y" } }, + }; + g_autoptr(GError) error = NULL; + g_auto(GLnxTmpDir) tmpdir = { FALSE }; + gsize i; + + glnx_mkdtemp ("test-XXXXXX", 0700, &tmpdir, &error); + g_assert_no_error (error); + + for (i = 0; i < G_N_ELEMENTS (prepare_dirs); i++) + { + const char *it = prepare_dirs[i]; + + glnx_shutil_mkdir_p_at (tmpdir.fd, it, 0700, NULL, &error); + g_assert_no_error (error); + } + + for (i = 0; i < G_N_ELEMENTS (prepare_symlinks); i++) + { + const Symlink *it = &prepare_symlinks[i]; + + if (symlinkat (it->target, tmpdir.fd, it->name) != 0) + g_error ("symlinkat %s: %s", it->name, g_strerror (errno)); + } + + for (i = 0; i < G_N_ELEMENTS (tests); i++) + { + const ResolveTest *it = &tests[i]; + glnx_autofd int fd = -1; + g_autofree gchar *path = NULL; + gchar **out_path; + g_autoptr(GString) description = g_string_new (""); + TestsOpenFdSet old_fds; + + old_fds = tests_check_fd_leaks_enter (); + + if (it->call.flags & PV_RESOLVE_FLAGS_MKDIR_P) + g_string_append (description, " (creating directories)"); + + if (it->call.flags & PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK) + g_string_append (description, " (not following final symlink)"); + + if (it->call.flags & PV_RESOLVE_FLAGS_REJECT_SYMLINKS) + g_string_append (description, " (not following any symlink)"); + + g_test_message ("%" G_GSIZE_FORMAT ": Resolving %s%s", + i, it->call.path, description->str); + + if (it->call.test_flags & RESOLVE_CALL_FLAGS_IGNORE_PATH) + out_path = NULL; + else + out_path = &path; + + fd = pv_resolve_in_sysroot (tmpdir.fd, it->call.path, + it->call.flags, out_path, &error); + + if (it->expect.path != NULL) + { + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + + if (out_path != NULL) + g_assert_cmpstr (*out_path, ==, it->expect.path); + + g_assert_true (fd_same_as_rel_path_nofollow (fd, tmpdir.fd, + it->expect.path)); + } + else + { + g_assert_error (error, G_IO_ERROR, it->expect.code); + g_assert_cmpint (fd, ==, -1); + + if (out_path != NULL) + g_assert_cmpstr (*out_path, ==, NULL); + + g_clear_error (&error); + } + + glnx_close_fd (&fd); + tests_check_fd_leaks_leave (old_fds); + } +} + +int +main (int argc, + char **argv) +{ + pv_avoid_gvfs (); + + g_test_init (&argc, &argv, NULL); + g_test_add ("/resolve-in-sysroot", Fixture, NULL, + setup, test_resolve_in_sysroot, teardown); + + return g_test_run (); +}