diff --git a/subprojects/libglnx/glnx-backports.h b/subprojects/libglnx/glnx-backports.h index 9ec11e4cf..aabf8ddb9 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 9ad2fe3b3..2251cb71f 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 @@ -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,7 +367,8 @@ 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 = (flags & (GLNX_CHASE_NOFOLLOW | GLNX_CHASE_ALL_DEBUG_FLAGS)); @@ -466,10 +478,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 +629,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 +653,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; @@ -690,7 +699,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 +753,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 +824,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 05dac12da..a4cb43d61 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 59261668a..62b0dcae8 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) @@ -1279,3 +1292,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 acd546dfb..1a5c4ee99 100644 --- a/subprojects/libglnx/glnx-fdio.h +++ b/subprojects/libglnx/glnx-fdio.h @@ -419,4 +419,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 a6e0e9b4f..9ac8a027d 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 f3bdb3baf..2f1026406 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 0fff39d21..6afcb8ff4 100644 --- a/subprojects/libglnx/meson.build +++ b/subprojects/libglnx/meson.build @@ -26,6 +26,7 @@ check_functions = [ 'renameat2', 'memfd_create', 'copy_file_range', + 'name_to_handle_at', ] conf = configuration_data() foreach check_function : check_functions @@ -110,6 +111,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 3eb2ba143..22c38981d 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 dccc7e558..009de74b8 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 b0ce1b414..0a7a6551b 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 cfa5b648a..26db23c3c 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) @@ -286,6 +405,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,6 +669,8 @@ 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); @@ -452,6 +678,7 @@ int main (int argc, char **argv) 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();