diff --git a/src/meson.build b/src/meson.build index 9d9e816a184bf100c1c91729cddb2b5a4782f337..789e181cf8c8272ecc98181d793e29df225abeca 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', ] @@ -42,6 +43,8 @@ pressure_vessel_utils = static_library( 'flatpak-utils-private.h', 'glib-backports.c', 'glib-backports.h', + 'resolve-in-sysroot.c', + 'resolve-in-sysroot.h', 'utils.c', 'utils.h', ], 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;