diff --git a/src/meson.build b/src/meson.build index 74e52fa9..29946896 100644 --- a/src/meson.build +++ b/src/meson.build @@ -33,7 +33,7 @@ config_data.set_quoted('DEBUGEDIT', debugedit.full_path()) config_data.set_quoted('GETTEXT_PACKAGE', meson.project_name()) config_data.set_quoted('PACKAGE_VERSION', meson.project_version()) config_data.set_quoted('PACKAGE_STRING', '@0@-@1@'.format(meson.project_name(), meson.project_version())) -config_data.set('GLIB_VERSION_MAX_ALLOWED', 'GLIB_VERSION_2_70') +config_data.set('GLIB_VERSION_MAX_ALLOWED', 'GLIB_VERSION_2_76') config_data.set('GLIB_VERSION_MIN_REQUIRED', 'GLIB_VERSION_2_66') # We support targeting specific fuse versions to optimize performance. diff --git a/subprojects/libglnx/.gitlab-ci.yml b/subprojects/libglnx/.gitlab-ci.yml index d9e465aa..2bb7eb03 100644 --- a/subprojects/libglnx/.gitlab-ci.yml +++ b/subprojects/libglnx/.gitlab-ci.yml @@ -1,28 +1,72 @@ # Copyright 2019 Endless OS Foundation LLC # SPDX-License-Identifier: LGPL-2.0-or-later -image: registry.fedoraproject.org/fedora:30 +variables: + SETUP_ARGS: "-Db_sanitize=address,undefined" + CAN_MESON_DIST: "yes" stages: - build -before_script: - - dnf install -y gcc git meson ninja-build "pkgconfig(gio-2.0)" "pkgconfig(gio-unix-2.0)" "pkgconfig(glib-2.0)" xz +.deps_fedora: &deps_fedora + - | + dnf install -y \ + "pkgconfig(gio-2.0)" \ + "pkgconfig(gio-unix-2.0)" \ + "pkgconfig(glib-2.0)" \ + gcc \ + git \ + libasan \ + libubsan \ + meson \ + ninja-build \ + xz \ + ${NULL+} -build: +.deps_debian: &deps_debian + - apt-get update + - apt-get install -y build-essential git meson ninja-build pkg-config libglib2.0-dev xz-utils + +.deps_ubuntu_oldlts: &deps_ubuntu_oldlts + - apt-get update + - | + apt-get install -y \ + build-essential \ + git \ + libglib2.0-dev \ + ninja-build \ + pkg-config \ + python3 \ + python3-pip \ + python3-setuptools \ + python3-wheel \ + xz-utils \ + ${NULL+} + # Modern versions of Meson need a modern version of Python, + # and we still aim to support building with 0.49 + - pip3 install meson==0.49 + +.build: stage: build + before_script: *deps_fedora script: - - meson _build . + - meson setup _build . $SETUP_ARGS - ninja -C _build - meson test -C _build # Run it again! This previously did not work. - meson test -C _build # Ensure that we can build as a subproject - rm -fr _build/meson-dist - - meson dist -C _build + - | + if [ "$CAN_MESON_DIST" = yes ]; then + meson dist -C _build + else + mkdir -p _build/meson-dist + git archive --prefix=libglnx/ HEAD | xz > _build/meson-dist/libglnx.tar.xz + fi - mkdir -p tests/use-as-subproject/subprojects/libglnx - tar --strip-components=1 -C tests/use-as-subproject/subprojects/libglnx -xf _build/meson-dist/*.tar.xz - - meson tests/use-as-subproject/_build tests/use-as-subproject + - meson setup tests/use-as-subproject/_build tests/use-as-subproject - ninja -C tests/use-as-subproject/_build - meson test -C tests/use-as-subproject/_build artifacts: @@ -31,6 +75,57 @@ build: paths: - "${CI_PROJECT_DIR}/_build/meson-logs" +build:fedora-latest: + extends: .build + image: registry.fedoraproject.org/fedora:latest + +build:fedora-rawhide: + extends: .build + image: registry.fedoraproject.org/fedora:rawhide + +# Linux 3.2 headers, glibc 2.15, Python 3.5 backport, GLib 2.58 backport +build:steam-runtime1: + extends: .build + image: registry.gitlab.steamos.cloud/steamrt/scout/sdk:latest + before_script: *deps_debian + variables: + # gcc-4.8 is too old to have useful sanitizers + SETUP_ARGS: "" + +# Linux 4.19 headers, glibc 2.28, GLib 2.58, Python 3.7, Meson 0.49 +build:debian10: + extends: .build + image: debian/eol:buster + before_script: *deps_debian + variables: + CAN_MESON_DIST: no + +build:debian-stable: + extends: .build + image: debian:stable + before_script: *deps_debian + +build:debian-testing: + extends: .build + image: debian:testing + before_script: *deps_debian + +# Linux 4.4 headers, glibc 2.23, GLib 2.48, Python 3.5, Meson 0.49 from pip +build:ubuntu1604: + extends: .build + image: ubuntu:xenial + before_script: *deps_ubuntu_oldlts + variables: + CAN_MESON_DIST: no + +# Linux 4.15 headers, glibc 2.27, GLib 2.56, Python 3.6, Meson 0.49 from pip +build:ubuntu1804: + extends: .build + image: ubuntu:bionic + before_script: *deps_ubuntu_oldlts + variables: + CAN_MESON_DIST: no + reuse: stage: build image: diff --git a/subprojects/libglnx/Makefile-libglnx.am b/subprojects/libglnx/Makefile-libglnx.am index 5934a2b6..2c6ad447 100644 --- a/subprojects/libglnx/Makefile-libglnx.am +++ b/subprojects/libglnx/Makefile-libglnx.am @@ -37,6 +37,8 @@ libglnx_la_SOURCES = \ $(libglnx_srcpath)/glnx-backport-testutils.c \ $(libglnx_srcpath)/glnx-backports.h \ $(libglnx_srcpath)/glnx-backports.c \ + $(libglnx_srcpath)/glnx-chase.h \ + $(libglnx_srcpath)/glnx-chase.c \ $(libglnx_srcpath)/glnx-local-alloc.h \ $(libglnx_srcpath)/glnx-local-alloc.c \ $(libglnx_srcpath)/glnx-errors.h \ diff --git a/subprojects/libglnx/glnx-backports.h b/subprojects/libglnx/glnx-backports.h index 9ec11e4c..aabf8ddb 100644 --- a/subprojects/libglnx/glnx-backports.h +++ b/subprojects/libglnx/glnx-backports.h @@ -80,6 +80,40 @@ g_clear_fd (int *fd_ptr, return _glnx_close (fd, error); G_GNUC_END_IGNORE_DEPRECATIONS } + +#else + +static inline gboolean +_glnx_clear_fd (int *fd_ptr, + GError **error) +{ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + return g_clear_fd (fd_ptr, error); + G_GNUC_END_IGNORE_DEPRECATIONS +} +#define g_clear_fd _glnx_clear_fd + +#endif + +/* This is part of the backport of g_autofd, but we define it + * unconditionally because it's also used to implement glnx_close_fd() */ +static inline void +_glnx_clear_fd_ignore_error (int *fd_ptr) +{ + /* Don't overwrite thread-local errno if closing the fd fails */ + int errsv = errno; + + if (!g_clear_fd (fd_ptr, NULL)) + { + /* Do nothing: we ignore all errors, except for EBADF which + * is a programming error, checked for by g_close(). */ + } + + errno = errsv; +} + +#if !GLIB_CHECK_VERSION(2, 76, 0) +#define g_autofd __attribute__((cleanup(_glnx_clear_fd_ignore_error))) #endif #if !GLIB_CHECK_VERSION(2, 40, 0) diff --git a/subprojects/libglnx/glnx-chase.c b/subprojects/libglnx/glnx-chase.c index 9ad2fe3b..2c061fe3 100644 --- a/subprojects/libglnx/glnx-chase.c +++ b/subprojects/libglnx/glnx-chase.c @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,13 @@ #define GLNX_CHASE_ALL_FLAGS \ (GLNX_CHASE_ALL_DEBUG_FLAGS | GLNX_CHASE_ALL_REGULAR_FLAGS) +typedef int (* GlnxChaseCallback)(int next_fd, + int current_fd, + const char *segment, + int open_tree_flags, + gpointer user_data, + GError **error); + typedef GQueue GlnxStatxQueue; static void @@ -223,9 +231,9 @@ chase_open_tree (int dirfd, if (errno != ENOTDIR) return glnx_fd_throw_errno_prefix (error, "openat(O_DIRECTORY) in autofs mount open_tree fallback"); - /* The automount is a directory, so let's try to open the file, - * which can fail because we are missing permissions, but that's - * okay, we only need to trigger automount. */ + /* The automount is not a directory, so let's try to open the + * file, which can fail because we are missing permissions, but + * that's okay, we only need to trigger automount. */ new_fd = openat (fd, ".", (openat_flags & ~O_PATH) | O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); glnx_close_fd (&new_fd); @@ -328,10 +336,12 @@ extract_next_segment (const char **remaining, * we're in full control over the resolving. */ static int -chase_manual (int dirfd, - const char *path, - GlnxChaseFlags flags, - GError **error) +chase_manual (int dirfd, + const char *path, + GlnxChaseFlags flags, + GlnxChaseCallback callback, + gpointer user_data, + GError **error) { gboolean is_absolute; g_autofree char *buffer = NULL; @@ -349,6 +359,7 @@ chase_manual (int dirfd, * - none of the resolve flags are set (they would require work here) * - NO_AUTOMOUNT is set (chase_open_tree only triggers the automount for * last component in some cases) + * - there is no callback * * TODO: if we have a guarantee that the open_tree syscall works, we can * shortcut even without GLNX_CHASE_NO_AUTOMOUNT @@ -356,9 +367,11 @@ chase_manual (int dirfd, if ((flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_RESOLVE_BENEATH | GLNX_CHASE_RESOLVE_IN_ROOT | - GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT) + GLNX_CHASE_RESOLVE_NO_SYMLINKS)) == GLNX_CHASE_NO_AUTOMOUNT && + callback == NULL) { GlnxChaseFlags open_tree_flags = + GLNX_CHASE_NO_AUTOMOUNT | (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS)); return chase_open_tree (dirfd, path, open_tree_flags, error); @@ -466,10 +479,25 @@ chase_manual (int dirfd, GlnxChaseFlags open_tree_flags = GLNX_CHASE_NOFOLLOW | (flags & (GLNX_CHASE_NO_AUTOMOUNT | GLNX_CHASE_ALL_DEBUG_FLAGS)); + g_autoptr(GError) local_error = NULL; + + next_fd = chase_open_tree (fd, segment, open_tree_flags, &local_error); + + /* Note that the callback can be called with next_fd < 0. + * If so, the error is already set, but may be cleared by + * the callback if it can recover from an error that already + * occurred. */ + if (callback) + { + next_fd = callback (next_fd, fd, segment, open_tree_flags, + user_data, &local_error); + } - next_fd = chase_open_tree (fd, segment, open_tree_flags, error); if (next_fd < 0) - return -1; + { + g_propagate_error (error, g_steal_pointer (&local_error)); + return -1; + } } if (!glnx_chase_statx (next_fd, no_automount, &st, error)) @@ -602,30 +630,13 @@ chase_manual (int dirfd, return g_steal_fd (&owned_fd); } -/** - * glnx_chaseat: - * @dirfd: a directory file descriptor - * @path: a path - * @flags: combination of GlnxChaseFlags flags - * @error: a #GError - * - * Behaves similar to openat, but with a number of differences: - * - * - All file descriptors which get returned are O_PATH and O_CLOEXEC. If you - * want to actually open the file for reading or writing, use glnx_fd_reopen, - * openat, or other at-style functions. - * - By default, automounts get triggered and the O_PATH fd will point to inodes - * in the newly mounted filesystem if an automount is encountered. This can be - * turned off with GLNX_CHASE_NO_AUTOMOUNT. - * - The GLNX_CHASE_RESOLVE_ flags can be used to safely deal with symlinks. - * - * Returns: the chased file, or -1 with @error set on error - */ -int -glnx_chaseat (int dirfd, - const char *path, - GlnxChaseFlags flags, - GError **error) +static int +glnx_chaseat_full (int dirfd, + const char *path, + GlnxChaseFlags flags, + GlnxChaseCallback callback, + gpointer user_data, + GError **error) { static gboolean can_openat2 = TRUE; glnx_autofd int fd = -1; @@ -643,15 +654,14 @@ glnx_chaseat (int dirfd, g_return_val_if_fail ((must_flags & (must_flags - 1)) == 0, -1); } - /* TODO: Add a callback which is called for every resolved path segment, to - * allow users to verify and expand the functionality safely. */ - /* We need the manual impl for NO_AUTOMOUNT, and we can skip this, if we don't * have openat2 at all. * Technically racy (static, not synced), but both paths work fine so it * doesn't matter. */ - if (can_openat2 && (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 && - (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0) + if (can_openat2 && + (flags & GLNX_CHASE_NO_AUTOMOUNT) == 0 && + (flags & GLNX_CHASE_DEBUG_NO_OPENAT2) == 0 && + callback == NULL) { uint64_t openat2_flags = 0; uint64_t openat2_resolve = 0; @@ -675,7 +685,10 @@ glnx_chaseat (int dirfd, .resolve = openat2_resolve, }; - fd = openat2 (dirfd, path, &how, sizeof (how)); + do + fd = openat2 (dirfd, path, &how, sizeof (how)); + while (fd < 0 && errno == EAGAIN); + if (fd < 0) { /* If the syscall is not implemented (ENOSYS) or blocked by @@ -690,7 +703,7 @@ glnx_chaseat (int dirfd, if (fd < 0) { - fd = chase_manual (dirfd, path, flags, error); + fd = chase_manual (dirfd, path, flags, callback, user_data, error); if (fd < 0) return -1; } @@ -744,6 +757,34 @@ glnx_chaseat (int dirfd, return g_steal_fd (&fd); } +/** + * glnx_chaseat: + * @dirfd: a directory file descriptor + * @path: a path + * @flags: combination of GlnxChaseFlags flags + * @error: a #GError + * + * Behaves similar to openat, but with a number of differences: + * + * - All file descriptors which get returned are O_PATH and O_CLOEXEC. If you + * want to actually open the file for reading or writing, use glnx_fd_reopen, + * openat, or other at-style functions. + * - By default, automounts get triggered and the O_PATH fd will point to inodes + * in the newly mounted filesystem if an automount is encountered. This can be + * turned off with GLNX_CHASE_NO_AUTOMOUNT. + * - The GLNX_CHASE_RESOLVE_ flags can be used to safely deal with symlinks. + * + * Returns: the chased file, or -1 with @error set on error + */ +int +glnx_chaseat (int dirfd, + const char *path, + GlnxChaseFlags flags, + GError **error) +{ + return glnx_chaseat_full (dirfd, path, flags, NULL, NULL, error); +} + /** * glnx_chase_and_statxat: * @dirfd: a directory file descriptor @@ -787,3 +828,89 @@ glnx_chase_and_statxat (int dirfd, return g_steal_fd (&fd); } + +static int +chase_and_mkdir (int next_fd, + int current_fd, + const char *segment, + G_GNUC_UNUSED int open_tree_flags, + gpointer user_data, + GError **error) +{ + mode_t mode = GPOINTER_TO_INT (user_data); + glnx_autofd int new_fd = -1; + + /* if chase managed to get the next segment, we already got our answer */ + if (next_fd >= 0) + return next_fd; + + /* if the problem isn't that the file doesn't exist, we propagate the error */ + g_assert (*error != NULL); + if (!g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return -1; + g_clear_error (error); + + /* create the directory with the specified mode */ + if (!glnx_ensure_dir (current_fd, segment, mode, error)) + return -1; + + /* Get a fd to the created dir. + * This is racy, meaning another process can modify the filesystem and we + * might open a directory that we did not create and might have a different + * mode. There isn't a kernel API which returns a fd or an inode which makes + * this race unavoidable. This is fine though because the semantics of + * `glnx_chase_and_mkdirat` accept arbitrary directories (even with different + * modes) when chasing the path. + */ + new_fd = openat (current_fd, segment, + O_CLOEXEC | O_PATH | O_DIRECTORY | O_NOFOLLOW); + if (new_fd < 0) + return glnx_fd_throw_errno_prefix (error, "opening created dir failed"); + + return g_steal_fd (&new_fd); +} + +/** + * glnx_chase_and_mkdirat: + * @dirfd: a directory file descriptor + * @path: a path + * @flags: restricted combination of GlnxChaseFlags flags + * @mode: the mode for new directories + * @error: a #GError + * + * Same as glnx_chase with `GLNX_CHASE_MUST_BE_DIRECTORY`, but when a path + * segment does not exist, a directory is created for the segment with the mode + * @mode. + * This essentially implement a fd-relative equivalent of g_mkdir_with_parents() + * or `mkdir -p`. + * Note, that directories in the path which already exist can have arbitrary + * modes. + * + * See glnx_chaseat for the meaning of @dirfd and @path. + * + * The @flags argument is the same as in glnx_chaseat, but setting + * `GLNX_CHASE_MUST_BE_REGULAR`, `GLNX_CHASE_MUST_BE_SOCKET`, or + * `GLNX_CHASE_MUST_BE_DIRECTORY` is an error. + * + * Returns: the chased file, or -1 with @error set on error + */ +int +glnx_chase_and_mkdirat (int dirfd, + const char *path, + GlnxChaseFlags flags, + mode_t mode, + GError **error) +{ + g_return_val_if_fail ((flags & ~(GLNX_CHASE_ALL_FLAGS)) == 0, -1); + g_return_val_if_fail ((flags & (GLNX_CHASE_MUST_BE_REGULAR | + GLNX_CHASE_MUST_BE_SOCKET | + GLNX_CHASE_MUST_BE_DIRECTORY)) == 0, -1); + g_return_val_if_fail ((mode & ~((mode_t) 07777)) == 0, -1); + + /* This function always implies GLNX_CHASE_MUST_BE_DIRECTORY */ + flags |= GLNX_CHASE_MUST_BE_DIRECTORY; + + return glnx_chaseat_full (dirfd, path, flags, + chase_and_mkdir, GINT_TO_POINTER (mode), + error); +} diff --git a/subprojects/libglnx/glnx-chase.h b/subprojects/libglnx/glnx-chase.h index 05dac12d..a4cb43d6 100644 --- a/subprojects/libglnx/glnx-chase.h +++ b/subprojects/libglnx/glnx-chase.h @@ -8,6 +8,8 @@ #include +#include + typedef enum _GlnxChaseFlags { /* Default */ GLNX_CHASE_DEFAULT = 0, @@ -48,4 +50,10 @@ int glnx_chase_and_statxat (int dirfd, struct glnx_statx *statbuf, GError **error); +int glnx_chase_and_mkdirat (int dirfd, + const char *path, + GlnxChaseFlags flags, + mode_t mode, + GError **error); + G_END_DECLS diff --git a/subprojects/libglnx/glnx-fdio.c b/subprojects/libglnx/glnx-fdio.c index 59261668..93ed45de 100644 --- a/subprojects/libglnx/glnx-fdio.c +++ b/subprojects/libglnx/glnx-fdio.c @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,18 @@ #include #include +/* From systemd mountpoint-util.c at d2b27a7: + * This is the original MAX_HANDLE_SZ definition from the kernel, when the API + * was introduced. We use that in place of any more currently defined value to + * future-proof things: if the size is increased in the API headers, and our code + * is recompiled then it would cease working on old kernels, as those refuse any + * sizes larger than this value with EINVAL right-away. Hence, let's disconnect + * ourselves from any such API changes, and stick to the original definition + * from when it was introduced. We use it as a start value only anyway (see + * below), and hence should be able to deal with large file handles anyway. + */ +#define ORIGINAL_MAX_HANDLE_SZ 128 + /* The standardized version of BTRFS_IOC_CLONE */ #ifndef FICLONE #define FICLONE _IOW(0x94, 9, int) @@ -697,15 +710,36 @@ copy_symlink_at (int src_dfd, GCancellable *cancellable, GError **error) { - g_autofree char *buf = glnx_readlinkat_malloc (src_dfd, src_subpath, cancellable, error); + g_autofree char *buf = NULL; + glnx_autofd int fd = -1; + g_autofree char *target = NULL; + + buf = glnx_readlinkat_malloc (src_dfd, src_subpath, cancellable, error); if (!buf) return FALSE; if (TEMP_FAILURE_RETRY (symlinkat (buf, dest_dfd, dest_subpath)) != 0) return glnx_throw_errno_prefix (error, "symlinkat"); + fd = TEMP_FAILURE_RETRY (openat (dest_dfd, dest_subpath, + O_PATH | O_NOFOLLOW | O_CLOEXEC)); + if (fd < 0) + return glnx_throw_errno_prefix (error, "openat(O_PATH)"); + + target = glnx_readlinkat_malloc (fd, "", cancellable, error); + if (!target) + return FALSE; + + if (strcmp (buf, target) != 0) + return glnx_throw (error, "Symlink target changed during copy"); + if (!(copyflags & GLNX_FILE_COPY_NOXATTRS)) { + /* The only way to do set xattrs on a symlink is via lsetxattr. An O_PATH + * is just not enough proof to modify the file (such as setting the + * xattrs) but it's the only non-racy reference to a symlink. So setting + * the xattrs on a symlink will be racy. + */ g_autoptr(GVariant) xattrs = NULL; if (!glnx_dfd_name_get_all_xattrs (src_dfd, src_subpath, &xattrs, @@ -717,10 +751,12 @@ copy_symlink_at (int src_dfd, return FALSE; } - if (TEMP_FAILURE_RETRY (fchownat (dest_dfd, dest_subpath, - src_stbuf->st_uid, src_stbuf->st_gid, - AT_SYMLINK_NOFOLLOW)) != 0) - return glnx_throw_errno_prefix (error, "fchownat"); + if (!(copyflags & GLNX_FILE_COPY_NOCHOWN)) + { + if (TEMP_FAILURE_RETRY (fchownat (fd, "", src_stbuf->st_uid, src_stbuf->st_gid, + AT_EMPTY_PATH)) != 0) + return glnx_throw_errno_prefix (error, "fchownat"); + } return TRUE; } @@ -1279,3 +1315,169 @@ glnx_fd_reopen (int fd, return g_steal_fd (&new_fd); } + +static gboolean +glnx_name_to_handle_at_internal (int fd, + int flags, + struct file_handle **handle_out, + uint64_t *mnt_id_out, + GError **error) +{ + size_t handle_bytes = ORIGINAL_MAX_HANDLE_SZ; + + for (;;) + { + g_autofree struct file_handle *handle = NULL; + uint64_t mnt_id_unique = 0; + unsigned int mnt_id = 0; + int *mnt_id_ptr; + int r; + + /* The kernel ABI involves an int * for backward compatibility, + * but with AT_HANDLE_MNT_ID_UNIQUE it's really expecting a + * uint64_t and will write a full 64-bit ID into it. */ + if ((flags & AT_HANDLE_MNT_ID_UNIQUE)) + mnt_id_ptr = (int *) &mnt_id_unique; + else + mnt_id_ptr = (int *) &mnt_id; + + handle = g_malloc0 (offsetof (struct file_handle, f_handle) + handle_bytes); + handle->handle_bytes = handle_bytes; + r = name_to_handle_at (fd, "", + handle, + mnt_id_ptr, + flags | AT_EMPTY_PATH); + + if (r < 0) + { + if (errno != EOVERFLOW) + return glnx_throw_errno (error); + + if (handle->handle_bytes <= handle_bytes) + return glnx_throw (error, "No file handle available"); + } + + if (r >= 0) + { + if (handle_out) + *handle_out = g_steal_pointer (&handle); + if (mnt_id_out) + *mnt_id_out = (flags & AT_HANDLE_MNT_ID_UNIQUE) ? mnt_id_unique : mnt_id; + return TRUE; + } + + handle_bytes = handle->handle_bytes; + } +} + +/** + * glnx_name_to_handle_at: + * @dfd: Directory FD to stat beneath + * @path: Path to get the handle to beneath @dfd + * @flags: Flags + * @handle_out: (out) (transfer full): Return location for the `struct file_handle` + * @mnt_id_out: (out caller-allocates): Return location for the mount id + * @error: Return location for a #GError, or %NULL + * + * Wrapper around name_to_handle_at() which adds #GError support, takes care of + * allocating the right size, and falls back to glnx_statx() for + * AT_HANDLE_MNT_ID_UNIQUE. + * + * The @mnt_id_out is pointer to a 64 bit location, but can contain either a + * traditional 32 bit mount id or a 64 bit unique mount id if + * AT_HANDLE_MNT_ID_UNIQUE is set. + * + * The @flags must be a combination of AT_SYMLINK_FOLLOW, AT_EMPTY_PATH, + * AT_HANDLE_FID, AT_HANDLE_MNT_ID_UNIQUE. + * + * Returns: %TRUE on success, or %FALSE setting both @error and `errno` + * Since: UNRELEASED + */ +gboolean +glnx_name_to_handle_at (int dfd, + const char *path, + int flags, + struct file_handle **handle_out, + uint64_t *mnt_id_out, + GError **error) +{ + int fd = -1; + glnx_autofd int fd_owned = -1; + uint64_t mnt_id; + g_autoptr(GError) local_error = NULL; + + g_return_val_if_fail (dfd >= 0 || dfd == AT_FDCWD, FALSE); + g_return_val_if_fail (path != NULL, FALSE); + g_return_val_if_fail ((flags & ~(AT_SYMLINK_FOLLOW | + AT_EMPTY_PATH | + AT_HANDLE_FID | + AT_HANDLE_MNT_ID_UNIQUE)) == 0, FALSE); + + if ((flags & AT_EMPTY_PATH) && path[0] == '\0') + { + fd = dfd; + } + else + { + int chase_flags = GLNX_CHASE_NO_AUTOMOUNT; + + if ((flags & AT_SYMLINK_FOLLOW) == 0) + chase_flags |= GLNX_CHASE_NOFOLLOW; + + fd = fd_owned = glnx_chaseat (dfd, path, chase_flags, error); + if (fd < 0) + return FALSE; + } + + if (glnx_name_to_handle_at_internal (fd, + flags, + handle_out, mnt_id_out, + &local_error)) + return TRUE; + + if (errno != EINVAL || (flags & AT_HANDLE_MNT_ID_UNIQUE) == 0) + { + g_propagate_prefixed_error (error, g_steal_pointer (&local_error), + "name_to_handle_at: "); + return FALSE; + } + g_clear_error (&local_error); + + { + struct glnx_statx stx; + int statx_flags = AT_EMPTY_PATH; + + if ((flags & AT_SYMLINK_FOLLOW) == 0) + statx_flags |= AT_SYMLINK_NOFOLLOW; + + if (!glnx_statx (fd, "", statx_flags, GLNX_STATX_MNT_ID_UNIQUE, &stx, error)) + { + g_prefix_error (error, "statx: "); + return FALSE; + } + + if ((stx.stx_mask & GLNX_STATX_MNT_ID_UNIQUE) == 0) + { + errno = ENODATA; + return glnx_throw_errno_prefix (error, + "unique mount ID not in statx result"); + } + + mnt_id = stx.stx_mnt_id; + } + + if (!glnx_name_to_handle_at_internal (fd, + flags & (~AT_HANDLE_MNT_ID_UNIQUE), + handle_out, NULL, + &local_error)) + { + g_propagate_prefixed_error (error, g_steal_pointer (&local_error), + "name_to_handle_at: "); + return FALSE; + } + + if (mnt_id_out) + *mnt_id_out = mnt_id; + + return TRUE; +} diff --git a/subprojects/libglnx/glnx-fdio.h b/subprojects/libglnx/glnx-fdio.h index acd546df..902013b7 100644 --- a/subprojects/libglnx/glnx-fdio.h +++ b/subprojects/libglnx/glnx-fdio.h @@ -39,6 +39,8 @@ G_BEGIN_DECLS +struct file_handle; + /* Irritatingly, g_basename() which is what we want * is deprecated. */ @@ -419,4 +421,11 @@ int glnx_fd_reopen (int fd, int flags, GError **error); +gboolean glnx_name_to_handle_at (int dfd, + const char *path, + int flags, + struct file_handle **handle_out, + uint64_t *mnt_id_out, + GError **error); + G_END_DECLS diff --git a/subprojects/libglnx/glnx-local-alloc.h b/subprojects/libglnx/glnx-local-alloc.h index a6e0e9b4..9ac8a027 100644 --- a/subprojects/libglnx/glnx-local-alloc.h +++ b/subprojects/libglnx/glnx-local-alloc.h @@ -51,38 +51,23 @@ glnx_local_obj_unref (void *v) * glnx_close_fd: * @fdp: Pointer to fd * - * Effectively `close (g_steal_fd (&fd))`. Also - * asserts that `close()` did not raise `EBADF` - encountering - * that error is usually a critical bug in the program. + * Same as `g_clear_fd()`, but ignoring the error (if any) and making sure + * not to alter `errno`. As a result, this function can be used for cleanup + * in contexts where `errno` needs to be preserved. */ -static inline void -glnx_close_fd (int *fdp) -{ - int errsv; - - g_assert (fdp); - - int fd = g_steal_fd (fdp); - if (fd >= 0) - { - errsv = errno; - if (close (fd) < 0) - g_assert (errno != EBADF); - errno = errsv; - } -} +#define glnx_close_fd _glnx_clear_fd_ignore_error /** * glnx_fd_close: * - * Deprecated in favor of `glnx_autofd`. + * Deprecated in favor of `g_autofd`. */ -#define glnx_fd_close __attribute__((cleanup(glnx_close_fd))) +#define glnx_fd_close g_autofd /** * glnx_autofd: * - * Call close() on a variable location when it goes out of scope. + * Deprecated in favor of `g_autofd`. */ -#define glnx_autofd __attribute__((cleanup(glnx_close_fd))) +#define glnx_autofd g_autofd G_END_DECLS diff --git a/subprojects/libglnx/glnx-missing-syscall.h b/subprojects/libglnx/glnx-missing-syscall.h index f3bdb3ba..2f102640 100644 --- a/subprojects/libglnx/glnx-missing-syscall.h +++ b/subprojects/libglnx/glnx-missing-syscall.h @@ -589,3 +589,102 @@ inline_openat2 (int dfd, #define openat2 inline_openat2 #define HAVE_OPENAT2 #endif + +#ifndef __IGNORE_name_to_handle_at +# if defined(__aarch64__) +# define systemd_NR_name_to_handle_at 264 +# elif defined(__alpha__) +# define systemd_NR_name_to_handle_at 497 +# elif defined(__arc__) || defined(__tilegx__) +# define systemd_NR_name_to_handle_at 264 +# elif defined(__arm__) +# define systemd_NR_name_to_handle_at 370 +# elif defined(__i386__) +# define systemd_NR_name_to_handle_at 341 +# elif defined(__ia64__) +# define systemd_NR_name_to_handle_at 1326 +# elif defined(__loongarch_lp64) +# define systemd_NR_name_to_handle_at 264 +# elif defined(__m68k__) +# define systemd_NR_name_to_handle_at 340 +# elif defined(_MIPS_SIM) +# if _MIPS_SIM == _MIPS_SIM_ABI32 +# define systemd_NR_name_to_handle_at 4339 +# elif _MIPS_SIM == _MIPS_SIM_NABI32 +# define systemd_NR_name_to_handle_at 6303 +# elif _MIPS_SIM == _MIPS_SIM_ABI64 +# define systemd_NR_name_to_handle_at 5298 +# else +# error "Unknown MIPS ABI" +# endif +# elif defined(__hppa__) +# define systemd_NR_name_to_handle_at 325 +# elif defined(__powerpc__) +# define systemd_NR_name_to_handle_at 345 +# elif defined(__riscv) +# if __riscv_xlen == 32 +# define systemd_NR_name_to_handle_at 264 +# elif __riscv_xlen == 64 +# define systemd_NR_name_to_handle_at 264 +# else +# error "Unknown RISC-V ABI" +# endif +# elif defined(__s390__) +# define systemd_NR_name_to_handle_at 335 +# elif defined(__sparc__) +# define systemd_NR_name_to_handle_at 332 +# elif defined(__x86_64__) +# if defined(__ILP32__) +# define systemd_NR_name_to_handle_at (303 | /* __X32_SYSCALL_BIT */ 0x40000000) +# else +# define systemd_NR_name_to_handle_at 303 +# endif +# elif !defined(missing_arch_template) +# warning "name_to_handle_at() syscall number is unknown for your architecture" +# endif + +/* may be an (invalid) negative number due to libseccomp, see PR 13319 */ +# if defined __NR_name_to_handle_at && __NR_name_to_handle_at >= 0 +# if defined systemd_NR_name_to_handle_at +G_STATIC_ASSERT (__NR_name_to_handle_at == systemd_NR_name_to_handle_at); +# endif +# else +# if defined __NR_name_to_handle_at +# undef __NR_name_to_handle_at +# endif +# if defined systemd_NR_name_to_handle_at && systemd_NR_name_to_handle_at >= 0 +# define __NR_name_to_handle_at systemd_NR_name_to_handle_at +# endif +# endif +#endif + +#ifndef AT_HANDLE_FID +#define AT_HANDLE_FID 0x200 +#endif + +#ifndef AT_HANDLE_MNT_ID_UNIQUE +#define AT_HANDLE_MNT_ID_UNIQUE 0x001 +#endif + +#ifndef AT_HANDLE_CONNECTABLE +#define AT_HANDLE_CONNECTABLE 0x002 +#endif + +#if !HAVE_DECL_NAME_TO_HANDLE_AT && defined(__NR_name_to_handle_at) +struct file_handle { + unsigned int handle_bytes; + int handle_type; + unsigned char f_handle[0]; +}; + +static inline int +name_to_handle_at (int fd, + const char *name, + struct file_handle *handle, + int *mnt_id, + int flags) +{ + return syscall(__NR_name_to_handle_at, fd, name, handle, mnt_id, flags); +} + +#endif diff --git a/subprojects/libglnx/meson.build b/subprojects/libglnx/meson.build index 0fff39d2..eecb8ccb 100644 --- a/subprojects/libglnx/meson.build +++ b/subprojects/libglnx/meson.build @@ -12,6 +12,7 @@ project( ) add_project_arguments('-D_GNU_SOURCE', language: 'c') +add_project_arguments('-Wno-missing-field-initializers', language: 'c') add_project_arguments('-Wno-unused-local-typedefs', language: 'c') # We are intentionally using non-ISO features in this (sub)project, @@ -26,6 +27,7 @@ check_functions = [ 'renameat2', 'memfd_create', 'copy_file_range', + 'name_to_handle_at', ] conf = configuration_data() foreach check_function : check_functions @@ -110,6 +112,9 @@ libglnx_dep = declare_dependency( dependencies : libglnx_deps, include_directories : libglnx_inc, link_with : libglnx) +if meson.version().version_compare('>= 0.54.0') + meson.override_dependency('libglnx', libglnx_dep) +endif subdir('tests') diff --git a/subprojects/libglnx/tests/libglnx-testlib.c b/subprojects/libglnx/tests/libglnx-testlib.c index 3eb2ba14..22c38981 100644 --- a/subprojects/libglnx/tests/libglnx-testlib.c +++ b/subprojects/libglnx/tests/libglnx-testlib.c @@ -23,6 +23,7 @@ #include "libglnx-testlib.h" #include +#include #include @@ -72,3 +73,21 @@ _glnx_test_auto_temp_dir_leave (_GLnxTestAutoTempDir *dir) g_free (dir->old_cwd); g_free (dir); } + +void +_glnx_test_assert_fd_was_closed (int fd) +{ + /* We can't tell a fd was really closed without behaving as though it + * was still valid */ + if (g_test_undefined ()) + { + int result; + int errsv; + + errno = 0; + result = fcntl (fd, F_GETFD, 0); + errsv = errno; + g_assert_cmpint (result, <, 0); + g_assert_cmpint (errsv, ==, EBADF); + } +} diff --git a/subprojects/libglnx/tests/libglnx-testlib.h b/subprojects/libglnx/tests/libglnx-testlib.h index dccc7e55..009de74b 100644 --- a/subprojects/libglnx/tests/libglnx-testlib.h +++ b/subprojects/libglnx/tests/libglnx-testlib.h @@ -1,7 +1,7 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2017 Red Hat, Inc. - * Copyright 2019 Collabora Ltd. + * Copyright 2019-2022 Collabora Ltd. * SPDX-License-Identifier: LGPL-2.0-or-later * * This library is free software; you can redistribute it and/or @@ -23,6 +23,7 @@ #pragma once #include +#include #include "glnx-backport-autoptr.h" @@ -47,3 +48,5 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(_GLnxTestAutoTempDir, _glnx_test_auto_temp_dir_lea #define _GLNX_TEST_SCOPED_TEMP_DIR \ G_GNUC_UNUSED g_autoptr(_GLnxTestAutoTempDir) temp_dir = _glnx_test_auto_temp_dir_enter () + +void _glnx_test_assert_fd_was_closed (int fd); diff --git a/subprojects/libglnx/tests/test-libglnx-chase.c b/subprojects/libglnx/tests/test-libglnx-chase.c index b0ce1b41..0a7a6551 100644 --- a/subprojects/libglnx/tests/test-libglnx-chase.c +++ b/subprojects/libglnx/tests/test-libglnx-chase.c @@ -50,6 +50,18 @@ path_get_ino (const char *path) return st.st_ino; } +static mode_t +get_mode (int fd) +{ + int r; + struct stat st; + + r = fstatat (fd, "", &st, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + g_assert_cmpint (r, >=, 0); + + return st.st_mode & 0777; +} + static char * get_abspath (int dfd, const char *path) @@ -586,6 +598,62 @@ test_chase_and_statxat_permissions (void) g_clear_fd (&chase_fd, NULL); } +static void +test_chase_and_mkdir (void) +{ + g_autoptr(GError) error = NULL; + glnx_autofd int dfd = -1; + glnx_autofd int file_fd = -1; + glnx_autofd int chase_fd = -1; + ino_t expected_ino; + + g_assert_true (glnx_shutil_mkdir_p_at_open (AT_FDCWD, "d1", 0755, + &dfd, + NULL, &error)); + g_assert_no_error (error); + + chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d2/d3", + GLNX_CHASE_DEFAULT, + 0755, + &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + + file_fd = openat (AT_FDCWD, "d1/d2/d3", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + expected_ino = get_ino (file_fd); + + g_assert_cmpint (get_ino (chase_fd), ==, expected_ino); + + g_assert_cmpint (get_mode (dfd), ==, 0755); + g_assert_cmpint (get_mode (chase_fd), ==, 0755); + + g_clear_fd (&chase_fd, NULL); + g_clear_fd (&file_fd, NULL); + + chase_fd = glnx_chase_and_mkdirat (AT_FDCWD, "d1/d4/d5", + GLNX_CHASE_DEFAULT, + 0700, + &error); + g_assert_cmpint (chase_fd, >=, 0); + g_assert_no_error (error); + + file_fd = openat (AT_FDCWD, "d1", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + g_assert_cmpint (get_mode (file_fd), ==, 0755); + g_clear_fd (&file_fd, NULL); + + file_fd = openat (AT_FDCWD, "d1/d4", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + g_assert_cmpint (get_mode (file_fd), ==, 0700); + g_clear_fd (&file_fd, NULL); + + file_fd = openat (AT_FDCWD, "d1/d4/d5", O_PATH | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint (file_fd, >=, 0); + g_assert_cmpint (get_mode (file_fd), ==, 0700); + g_clear_fd (&file_fd, NULL); +} + int main (int argc, char **argv) { _GLNX_TEST_SCOPED_TEMP_DIR; @@ -602,6 +670,7 @@ int main (int argc, char **argv) g_test_add_func ("/chase-and-statxat-basic", test_chase_and_statxat_basic); g_test_add_func ("/chase-and-statxat-symlink", test_chase_and_statxat_symlink); g_test_add_func ("/chase-and-statxat-permissions", test_chase_and_statxat_permissions); + g_test_add_func ("/chase-and-mkdir", test_chase_and_mkdir); ret = g_test_run(); diff --git a/subprojects/libglnx/tests/test-libglnx-fdio.c b/subprojects/libglnx/tests/test-libglnx-fdio.c index cfa5b648..e1324823 100644 --- a/subprojects/libglnx/tests/test-libglnx-fdio.c +++ b/subprojects/libglnx/tests/test-libglnx-fdio.c @@ -29,6 +29,125 @@ #include "libglnx-testlib.h" +static void +test_close (void) +{ + g_autoptr(GError) error = NULL; + int errsv; + int fd = -2; + int fd_borrowed; + + g_test_summary ("Exercise glnx_close_fd"); + + /* Closing a non-fd is a no-op, and preserves errno. + * EILSEQ is an arbitrary valid value of errno that is unlikely + * to be set accidentally as a side-effect of I/O. */ + g_test_message ("Closing a non-fd is a no-op and preserves errno..."); + errno = EILSEQ; + glnx_close_fd (&fd); + errsv = errno; + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errsv, ==, EILSEQ); + + /* Closing a valid fd really closes it, and preserves errno. */ + g_test_message ("Closing a valid fd preserves errno..."); + glnx_opendirat (AT_FDCWD, "/", TRUE, &fd, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + fd_borrowed = fd; + errno = EILSEQ; + glnx_close_fd (&fd); + errsv = errno; + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errsv, ==, EILSEQ); + _glnx_test_assert_fd_was_closed (fd_borrowed); +} + +/* Exercise glnx_close_fd in the case where close() fails. + * The only convenient way we can arrange for this to happen is to use + * an invalid fd, which is a programming error. + * + * This function is only run under g_test_undefined(), and assumes the + * implementation detail that GLib responds to that programming error + * with a critical warning rather than a fatal error. */ +static void +test_close_ebadf_subprocess (void) +{ + g_autoptr(GError) error = NULL; + int errsv; + int fd = -2; + int non_fd; + + /* Preparation: Open a fd, and close it, leaving non_fd set to the + * file descriptor number. */ + glnx_opendirat (AT_FDCWD, "/", TRUE, &fd, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + close (fd); + non_fd = fd; + _glnx_test_assert_fd_was_closed (non_fd); + + /* "Closing" the non-fd provokes a critical warning. */ + g_log_set_always_fatal (G_LOG_FATAL_MASK); + g_log_set_fatal_mask ("GLib", G_LOG_FATAL_MASK); + + errno = EILSEQ; + glnx_close_fd (&non_fd); + errsv = errno; + g_assert_cmpint (non_fd, ==, -1); + + /* We preserved errno. */ + g_assert_cmpint (errsv, ==, EILSEQ); + g_print ("Closing invalid fd preserved errno\n"); +} + +static void +test_close_ebadf (void) +{ + g_test_summary ("Exercise glnx_close_fd when close() fails"); + + /* If close() fails, it still preserves errno. + * The only convenient way to make close() fail on-demand is EBADF. */ + + if (g_test_subprocess ()) + { + test_close_ebadf_subprocess (); + return; + } + + if (g_test_undefined ()) + { + g_test_message ("Closing invalid fd preserves errno..."); + +#if GLIB_CHECK_VERSION (2, 38, 0) + g_test_trap_subprocess (NULL, 0, G_TEST_SUBPROCESS_DEFAULT); +#else + if (g_test_trap_fork (0, 0)) + { + test_close_ebadf_subprocess (); + _exit (0); + } +#endif + + g_test_trap_assert_passed (); + g_test_trap_assert_stdout ("*Closing invalid fd preserved errno*"); +#if !GLIB_CHECK_VERSION(2, 76, 0) + /* We can assert that our backport emits this message */ + g_test_trap_assert_stderr ("*_glnx_close(fd:*) failed with EBADF*"); +#else + /* We can't assert anything this specific about GLib's, + * but as an implementation detail, it's currently very similar */ +# if 0 + g_test_trap_assert_stderr ("*g_close(fd:*) failed with EBADF*"); +# endif +#endif + } + else + { + g_test_skip ("Can't test this without provoking undefined behaviour"); + } +} + static gboolean renameat_test_setup (int *out_srcfd, int *out_destfd, GError **error) @@ -236,6 +355,181 @@ test_filecopy (void) g_assert (S_ISREG (stbuf.st_mode)); } +/* uid/gid 5 is normally 'tty', e.g. , + * which is a reasonably harmless one to use */ +const uid_t not_root_uid = 5; +const gid_t not_root_gid = 5; + +static void +test_copy_symlink (void) +{ + _GLNX_TEST_DECLARE_ERROR(local_error, error); + struct stat stbuf; + g_autofree char *expected_target = NULL; + g_autofree char *target = NULL; + + if (symlinkat ("sometarget", AT_FDCWD, "srclink") < 0) + return (void) glnx_throw_errno_prefix (error, "symlinkat"); + + if (!glnx_file_copy_at (AT_FDCWD, "srclink", NULL, AT_FDCWD, "dstlink", + GLNX_FILE_COPY_NOXATTRS, NULL, error)) + return; + + if (!glnx_fstatat (AT_FDCWD, "dstlink", &stbuf, AT_SYMLINK_NOFOLLOW, error)) + return; + g_assert (S_ISLNK (stbuf.st_mode)); + + target = glnx_readlinkat_malloc (AT_FDCWD, "dstlink", NULL, error); + if (!target) + return; + g_assert_cmpstr (target, ==, "sometarget"); + + /* /dev/stderr is a convenient example of a symlink that will often exist, + * and is often owned by root. This means that unprivileged users will + * not be able to set the copy's ownership to equal the original's. */ + expected_target = glnx_readlinkat_malloc (AT_FDCWD, "/dev/stderr", NULL, error); + + if (expected_target != NULL) + { + g_autofree char *actual_target = NULL; + + if (!glnx_file_copy_at (AT_FDCWD, "/dev/stderr", NULL, + AT_FDCWD, "stderr", + GLNX_FILE_COPY_NOCHOWN | GLNX_FILE_COPY_NOXATTRS, + NULL, error)) + return; + + actual_target = glnx_readlinkat_malloc (AT_FDCWD, "stderr", NULL, error); + + if (!actual_target) + return; + + g_assert_cmpstr (actual_target, ==, expected_target); + } + else + { + g_test_message ("Not testing /dev/stderr: %s", local_error->message); + g_clear_error (&local_error); + } + + /* If we're running the test as root, we expect that copying /dev/stderr + * would have succeeded even if we incorrectly changed its ownership. + * However, if we're root, we can construct a symlink owned by someone else + * on-demand, and use that. */ + if (symlinkat ("sometarget", AT_FDCWD, "owned-by-other") < 0) + return (void) glnx_throw_errno_prefix (error, "symlinkat"); + + if (geteuid () != 0) + { + g_test_message ("Not testing symlink owned by another user: not root"); + } + else if (lchown ("owned-by-other", not_root_uid, not_root_gid) < 0) + { + g_test_message ("Not testing symlink owned by another user: %s", + g_strerror (errno)); + } + else + { + g_autofree char *actual_target = NULL; + + if (!glnx_fstatat (AT_FDCWD, "owned-by-other", &stbuf, + AT_SYMLINK_NOFOLLOW, error)) + return; + + g_assert_cmpint (stbuf.st_uid, ==, not_root_uid); + g_assert_cmpint (stbuf.st_gid, ==, not_root_gid); + + if (!glnx_file_copy_at (AT_FDCWD, "owned-by-other", NULL, + AT_FDCWD, "owned-by-other-copy", + GLNX_FILE_COPY_NOCHOWN | GLNX_FILE_COPY_NOXATTRS, + NULL, error)) + return; + + if (!glnx_fstatat (AT_FDCWD, "owned-by-other-copy", &stbuf, + AT_SYMLINK_NOFOLLOW, error)) + return; + + g_assert_true (S_ISLNK (stbuf.st_mode)); + g_assert_cmpint (stbuf.st_uid, !=, not_root_uid); + g_assert_cmpint (stbuf.st_gid, !=, not_root_gid); + + actual_target = glnx_readlinkat_malloc (AT_FDCWD, "owned-by-other-copy", + NULL, error); + + if (!actual_target) + return; + + g_assert_cmpstr (actual_target, ==, "sometarget"); + } +} + +static void +test_copy_symlink_xattrs (void) +{ + /* Intentionally not UTF-8 or a valid bytestring */ + static const char value[] = { '\xff', '\x00', '\x55', '\xaa' }; + char *buf[16]; + _GLNX_TEST_DECLARE_ERROR(local_error, error); + struct stat stbuf; + g_autofree char *target = NULL; + g_autofree char *tmpdir_path = NULL; + g_autofree char *srclink_path = NULL; + g_autofree char *dstlink_path = NULL; + g_auto(GLnxTmpDir) tmpdir = { 0, }; + gssize len; + + tmpdir_path = g_strdup_printf ("%s/libglnx-xattrs-XXXXXX", + getenv ("TMPDIR") ?: "/var/tmp"); + + if (!glnx_mkdtempat (AT_FDCWD, tmpdir_path, 0700, &tmpdir, error)) + return; + + g_assert_no_errno (symlinkat ("sometarget", tmpdir.fd, "srclink")); + srclink_path = g_strdup_printf ("/proc/self/fd/%d/srclink", tmpdir.fd); + + /* A limitation of xattrs on Linux is that symlinks cannot have user. + * extended attributes, only trusted., system. or security., + * so we can only test this if we are root. */ + if (lsetxattr (srclink_path, "trusted.test", value, sizeof (value), 0) < 0) + { + g_test_skip_printf ("could not set xattr trusted.test on symlink: %s", + g_strerror (errno)); + return; + } + + g_test_message ("Copying srclink to dstlink..."); + + if (!glnx_file_copy_at (tmpdir.fd, "srclink", NULL, tmpdir.fd, "dstlink", + 0, /* note absence of GLNX_FILE_COPY_NOXATTRS */ + NULL, error)) + return; + + g_test_message ("Checking results..."); + + if (!glnx_fstatat (tmpdir.fd, "dstlink", &stbuf, AT_SYMLINK_NOFOLLOW, error)) + return; + + g_assert_true (S_ISLNK (stbuf.st_mode)); + + target = glnx_readlinkat_malloc (tmpdir.fd, "dstlink", NULL, error); + + if (!target) + return; + + g_assert_cmpstr (target, ==, "sometarget"); + + dstlink_path = g_strdup_printf ("/proc/self/fd/%d/dstlink", tmpdir.fd); + len = lgetxattr (dstlink_path, "trusted.test", buf, sizeof (buf)); + + if (len < 0) + { + glnx_throw_errno_prefix (error, "lgetxattr(dstlink)"); + return; + } + + g_assert_cmpmem (buf, len, value, sizeof (value)); +} + static void test_filecopy_procfs (void) { @@ -286,6 +580,111 @@ test_filecopy_procfs (void) } } +static void +test_name_to_handle_at (void) +{ + g_autoptr(GError) error = NULL; + g_autofree struct file_handle *handle1 = NULL; + g_autofree struct file_handle *handle2 = NULL; + g_autofree struct file_handle *handle3 = NULL; + uint64_t mnt_id1 = 0; + uint64_t mnt_id2 = 0; + uint64_t mnt_id3 = 0; + glnx_autofd int dfd = -1; + gboolean ok; + + /* Create a test directory and file */ + ok = glnx_shutil_mkdir_p_at_open (AT_FDCWD, "handle_test", 0755, &dfd, NULL, &error); + g_assert_no_error (error); + g_assert_true (ok); + + ok = glnx_file_replace_contents_at (dfd, "testfile", + (const guint8 *)"test", 4, + GLNX_FILE_REPLACE_NODATASYNC, NULL, &error); + g_assert_no_error (error); + g_assert_true (ok); + + ok = glnx_name_to_handle_at (dfd, "testfile", 0, &handle1, &mnt_id1, &error); + + /* Skip the test if the syscall is not supported */ + if (!ok && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) + return g_test_skip ("name_to_handle_at not supported"); + + /* Test 1: Get handle for a regular file */ + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + g_assert_cmpuint (mnt_id1, >, 0); + + /* Test 2: Get handle for the same file again - should have same handle and mount ID */ + ok = glnx_name_to_handle_at (dfd, "testfile", 0, &handle2, &mnt_id2, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle2); + g_assert_cmpuint (mnt_id1, ==, mnt_id2); + g_assert_cmpuint (handle1->handle_bytes, ==, handle2->handle_bytes); + g_assert_cmpuint (handle1->handle_type, ==, handle2->handle_type); + g_assert_cmpmem (handle1->f_handle, handle1->handle_bytes, + handle2->f_handle, handle2->handle_bytes); + + g_clear_pointer (&handle1, g_free); + g_clear_pointer (&handle2, g_free); + + /* Test 3: Get handle for a directory */ + ok = glnx_name_to_handle_at (AT_FDCWD, "handle_test", 0, &handle1, &mnt_id1, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + g_assert_cmpuint (mnt_id1, >, 0); + + g_clear_pointer (&handle1, g_free); + + /* Test 4: Test with AT_EMPTY_PATH */ + ok = glnx_name_to_handle_at (dfd, "", AT_EMPTY_PATH, &handle1, &mnt_id1, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + g_assert_cmpuint (mnt_id1, >, 0); + + g_clear_pointer (&handle1, g_free); + + /* Test 5: Create symlink and test AT_SYMLINK_FOLLOW */ + g_assert_no_errno (symlinkat ("testfile", dfd, "testlink")); + + ok = glnx_name_to_handle_at (dfd, "testlink", 0, &handle1, &mnt_id1, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle1); + + ok = glnx_name_to_handle_at (dfd, "testlink", AT_SYMLINK_FOLLOW, &handle2, &mnt_id2, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_nonnull (handle2); + + /* files are on the same mount, so we should get the same kind of handle */ + g_assert_true (handle1->handle_bytes == handle2->handle_bytes); + g_assert_true (handle1->handle_type == handle2->handle_type); + /* handle1 != handle2 */ + g_assert_false (memcmp (handle1->f_handle, handle2->f_handle, handle1->handle_bytes) == 0); + + /* Following the symlink should give us the same handle as the target */ + ok = glnx_name_to_handle_at (dfd, "testfile", 0, &handle3, &mnt_id3, &error); + g_assert_no_error (error); + g_assert_true (ok); + g_assert_cmpmem (handle2->f_handle, handle2->handle_bytes, + handle3->f_handle, handle3->handle_bytes); + + g_clear_pointer (&handle1, g_free); + g_clear_pointer (&handle2, g_free); + g_clear_pointer (&handle3, g_free); + + /* Test 6: Error case - non-existent file */ + ok = glnx_name_to_handle_at (dfd, "nosuchfile", 0, &handle1, &mnt_id1, &error); + g_assert_false (ok); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_clear_error (&error); +} + static void test_fd_reopen (void) { @@ -445,13 +844,18 @@ int main (int argc, char **argv) g_test_init (&argc, &argv, NULL); + g_test_add_func ("/close", test_close); + g_test_add_func ("/close/ebadf", test_close_ebadf); g_test_add_func ("/tmpfile", test_tmpfile); g_test_add_func ("/stdio-file", test_stdio_file); g_test_add_func ("/filecopy", test_filecopy); + g_test_add_func ("/copy-symlink", test_copy_symlink); + g_test_add_func ("/copy-symlink/xattrs", test_copy_symlink_xattrs); g_test_add_func ("/filecopy-procfs", test_filecopy_procfs); g_test_add_func ("/renameat2-noreplace", test_renameat2_noreplace); g_test_add_func ("/renameat2-exchange", test_renameat2_exchange); g_test_add_func ("/fstat", test_fstatat); + g_test_add_func ("/name-to-handle-at", test_name_to_handle_at); g_test_add_func ("/fd-reopen", test_fd_reopen); ret = g_test_run();