fdio: Add support for name_to_handle_at

This commit is contained in:
Sebastian Wick
2026-03-26 23:12:14 +01:00
parent ccea836b79
commit 015ec9c63e
6 changed files with 394 additions and 0 deletions

View File

@@ -8,6 +8,8 @@
#include <glib.h>
#include <glnx-missing.h>
typedef enum _GlnxChaseFlags {
/* Default */
GLNX_CHASE_DEFAULT = 0,

View File

@@ -33,6 +33,7 @@
#include <sys/sendfile.h>
#include <errno.h>
#include <glnx-chase.h>
#include <glnx-fdio.h>
#include <glnx-dirfd.h>
#include <glnx-errors.h>
@@ -42,6 +43,18 @@
#include <glnx-local-alloc.h>
#include <glnx-missing.h>
/* 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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -26,6 +26,7 @@ check_functions = [
'renameat2',
'memfd_create',
'copy_file_range',
'name_to_handle_at',
]
conf = configuration_data()
foreach check_function : check_functions

View File

@@ -286,6 +286,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)
{
@@ -452,6 +557,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();