mountpoint-util: rework name_to_handle_at() unique mount id handling

name_to_handle_at_try_unique_mntid_fid() in its current form is
ill-designed for various reasons:

* AT_HANDLE_FID requires file system support, while unique mount id
  is a VFS concept hence is always available if supported. Hence
  the fallback for AT_HANDLE_MNT_ID_UNIQUE should be independent
  of fid.
* The request for AT_HANDLE_MNT_ID_UNIQUE can be identified via
  specifying ret_unique_mnt_id, no need for opening up the control
  to caller (and currently the function simply doesn't handle
  mismatch between ret params and flags).
* The caller cannot realistically differentiate whether the returned
  mount id is actually unique.
* The path_get_unique_mnt_id() fallback did not handle AT_SYMLINK_FOLLOW.

Let's instead move the statx() fallback into name_to_handle_at_loop()
directly, and revamp interaction of ret_mnt_id/ret_unique_mnt_id:
if both are set, it indicates that the caller can handle both, hence
set what we have and return 0/1 for whether we managed to acquire
the unique one.

The !ret_handle && ret_mnt_id logic is removed. Let's not rely on
undocumented bizaare behavior and it's unused anyways.
path_get_mnt_id_at() exists for a reason...
This commit is contained in:
Mike Yuan
2026-02-05 14:14:40 +01:00
parent 56adac1193
commit 5817c73391
6 changed files with 97 additions and 97 deletions
+61 -71
View File
@@ -53,27 +53,29 @@ int name_to_handle_at_loop(
uint64_t *ret_unique_mnt_id,
int flags) {
size_t n = ORIGINAL_MAX_HANDLE_SZ;
int r;
assert(fd >= 0 || fd == AT_FDCWD);
assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH|AT_HANDLE_FID|AT_HANDLE_MNT_ID_UNIQUE)) == 0);
assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH|AT_HANDLE_FID)) == 0);
/* We need to invoke name_to_handle_at() in a loop, given that it might return EOVERFLOW when the specified
* buffer is too small. Note that in contrast to what the docs might suggest, MAX_HANDLE_SZ is only good as a
* start value, it is not an upper bound on the buffer size required.
*
* This improves on raw name_to_handle_at() also in one other regard: ret_handle and ret_mnt_id can be passed
* as NULL if there's no interest in either. */
* as NULL if there's no interest in either.
*
* If unique mount id is requested via ret_unique_mnt_id, try AT_HANDLE_MNT_ID_UNIQUE flag first
* (needs kernel v6.12), and fall back to statx() if not supported. If neither worked, and caller
* also specifies ret_mnt_id, then the old-style mount id is returned, -EUNATCH otherwise. */
if (isempty(path)) {
flags |= AT_EMPTY_PATH;
path = "";
}
for (;;) {
for (size_t n = ORIGINAL_MAX_HANDLE_SZ;;) {
_cleanup_free_ struct file_handle *h = NULL;
int mnt_id = -1, r;
uint64_t unique_mnt_id = 0;
h = malloc0(offsetof(struct file_handle, f_handle) + n);
if (!h)
@@ -81,18 +83,61 @@ int name_to_handle_at_loop(
h->handle_bytes = n;
if (FLAGS_SET(flags, AT_HANDLE_MNT_ID_UNIQUE))
if (ret_unique_mnt_id) {
uint64_t mnt_id;
/* The kernel will still use this as uint64_t pointer */
r = name_to_handle_at(fd, path, h, (int *) &unique_mnt_id, flags);
else
r = name_to_handle_at(fd, path, h, &mnt_id, flags);
r = name_to_handle_at(fd, path, h, (int *) &mnt_id, flags|AT_HANDLE_MNT_ID_UNIQUE);
if (r >= 0) {
if (ret_handle)
*ret_handle = TAKE_PTR(h);
*ret_unique_mnt_id = mnt_id;
if (ret_mnt_id)
*ret_mnt_id = -1;
return 1;
}
if (errno == EOVERFLOW)
goto grow;
if (errno != EINVAL)
return -errno;
}
int mnt_id;
r = name_to_handle_at(fd, path, h, &mnt_id, flags);
if (r >= 0) {
if (ret_unique_mnt_id) {
/* Hmm, AT_HANDLE_MNT_ID_UNIQUE is not supported? Let's try to acquire
* the unique mount id from statx() then, which has a slightly lower
* kernel version requirement (6.8 vs 6.12). */
struct statx sx;
r = xstatx(fd, path,
at_flags_normalize_nofollow(flags & (AT_SYMLINK_FOLLOW|AT_EMPTY_PATH))|AT_STATX_DONT_SYNC,
STATX_MNT_ID_UNIQUE,
&sx);
if (r >= 0) {
if (ret_handle)
*ret_handle = TAKE_PTR(h);
*ret_unique_mnt_id = sx.stx_mnt_id;
if (ret_mnt_id)
*ret_mnt_id = -1;
return 1;
}
if (r != -EUNATCH || !ret_mnt_id)
return r;
*ret_unique_mnt_id = 0;
}
if (ret_handle)
*ret_handle = TAKE_PTR(h);
if (ret_unique_mnt_id)
*ret_unique_mnt_id = unique_mnt_id;
if (ret_mnt_id)
*ret_mnt_id = mnt_id;
@@ -101,19 +146,7 @@ int name_to_handle_at_loop(
if (errno != EOVERFLOW)
return -errno;
if (!ret_handle && ((ret_mnt_id && mnt_id >= 0) || (ret_unique_mnt_id && unique_mnt_id > 0))) {
/* As it appears, name_to_handle_at() fills in mnt_id even when it returns EOVERFLOW when the
* buffer is too small, but that's undocumented. Hence, let's make use of this if it appears to
* be filled in, and the caller was interested in only the mount ID an nothing else. */
if (ret_unique_mnt_id)
*ret_unique_mnt_id = unique_mnt_id;
if (ret_mnt_id)
*ret_mnt_id = mnt_id;
return 0;
}
grow:
/* If name_to_handle_at() didn't increase the byte size, then this EOVERFLOW is caused by
* something else (apparently EOVERFLOW is returned for untriggered nfs4 autofs mounts
* sometimes), not by the too small buffer. In that case propagate EOVERFLOW */
@@ -134,6 +167,7 @@ int name_to_handle_at_try_fid(
const char *path,
struct file_handle **ret_handle,
int *ret_mnt_id,
uint64_t *ret_unique_mnt_id,
int flags) {
int r;
@@ -144,55 +178,11 @@ int name_to_handle_at_try_fid(
* we'll try without the flag, in order to support older kernels that didn't have AT_HANDLE_FID
* (i.e. older than Linux 6.5). */
r = name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, /* ret_unique_mnt_id= */ NULL, flags | AT_HANDLE_FID);
r = name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, ret_unique_mnt_id, flags | AT_HANDLE_FID);
if (r >= 0 || is_name_to_handle_at_fatal_error(r))
return r;
return name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, /* ret_unique_mnt_id= */ NULL, flags & ~AT_HANDLE_FID);
}
int name_to_handle_at_try_unique_mntid_fid(
int fd,
const char *path,
struct file_handle **ret_handle,
uint64_t *ret_mnt_id,
int flags) {
int mnt_id = -1, r;
assert(fd >= 0 || fd == AT_FDCWD);
/* First issues name_to_handle_at() with AT_HANDLE_MNT_ID_UNIQUE and AT_HANDLE_FID.
* If this fails and this is not a fatal error we'll try without the
* AT_HANDLE_MNT_ID_UNIQUE flag because it's only available from Linux 6.12 onwards. */
r = name_to_handle_at_loop(fd, path, ret_handle, /* ret_mnt_id= */ NULL, ret_mnt_id, flags | AT_HANDLE_MNT_ID_UNIQUE | AT_HANDLE_FID);
if (r >= 0 || is_name_to_handle_at_fatal_error(r))
return r;
flags &= ~AT_HANDLE_MNT_ID_UNIQUE;
/* Then issues name_to_handle_at() with AT_HANDLE_FID. If this fails and this is not a fatal error
* we'll try without the flag, in order to support older kernels that didn't have AT_HANDLE_FID
* (i.e. older than Linux 6.5). */
r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, /* ret_unique_mnt_id= */ NULL, flags | AT_HANDLE_FID);
if (r < 0 && is_name_to_handle_at_fatal_error(r))
return r;
if (r >= 0) {
if (ret_mnt_id && mnt_id >= 0) {
/* See if we can do better because statx can do unique mount IDs since Linux 6.8
* and only if this doesn't work we use the non-unique mnt_id as returned. */
if (path_get_unique_mnt_id_at(fd, path, ret_mnt_id) < 0)
*ret_mnt_id = mnt_id;
}
return r;
}
r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, /* ret_unique_mnt_id= */ NULL, flags & ~AT_HANDLE_FID);
if (ret_mnt_id && mnt_id >= 0)
*ret_mnt_id = mnt_id;
return r;
return name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, ret_unique_mnt_id, flags & ~AT_HANDLE_FID);
}
int name_to_handle_at_u64(int fd, const char *path, uint64_t *ret) {
+1 -2
View File
@@ -35,8 +35,7 @@
bool is_name_to_handle_at_fatal_error(int err);
int name_to_handle_at_loop(int fd, const char *path, struct file_handle **ret_handle, int *ret_mnt_id, uint64_t *ret_unique_mnt_id, int flags);
int name_to_handle_at_try_fid(int fd, const char *path, struct file_handle **ret_handle, int *ret_mnt_id, int flags);
int name_to_handle_at_try_unique_mntid_fid(int fd, const char *path, struct file_handle **ret_handle, uint64_t *ret_mnt_id, int flags);
int name_to_handle_at_try_fid(int fd, const char *path, struct file_handle **ret_handle, int *ret_mnt_id, uint64_t *ret_unique_mnt_id, int flags);
int name_to_handle_at_u64(int fd, const char *path, uint64_t *ret);
static inline int path_to_handle_u64(const char *path, uint64_t *ret) {
return name_to_handle_at_u64(AT_FDCWD, path, ret);
+9 -2
View File
@@ -431,12 +431,14 @@ int inode_same_at(int fda, const char *filea, int fdb, const char *fileb, int fl
int ntha_flags = at_flags_normalize_follow(flags) & (AT_EMPTY_PATH|AT_SYMLINK_FOLLOW);
_cleanup_free_ struct file_handle *ha = NULL, *hb = NULL;
int mntida = -1, mntidb = -1;
uint64_t mntida, mntidb;
int _mntida, _mntidb;
r = name_to_handle_at_try_fid(
fda,
filea,
&ha,
&_mntida,
&mntida,
ntha_flags);
if (r < 0) {
@@ -445,12 +447,15 @@ int inode_same_at(int fda, const char *filea, int fdb, const char *fileb, int fl
goto fallback;
}
if (r == 0)
mntida = _mntida;
r = name_to_handle_at_try_fid(
fdb,
fileb,
&hb,
&mntidb,
r > 0 ? NULL : &_mntidb, /* if we managed to get unique mnt id for a, insist on that for b */
r > 0 ? &mntidb : NULL,
ntha_flags);
if (r < 0) {
if (is_name_to_handle_at_fatal_error(r))
@@ -458,6 +463,8 @@ int inode_same_at(int fda, const char *filea, int fdb, const char *fileb, int fl
goto fallback;
}
if (r == 0)
mntidb = _mntidb;
/* Now compare the two file handles */
if (!file_handle_equal(ha, hb))
+1 -1
View File
@@ -685,7 +685,7 @@ int json_variant_new_fd_info(sd_json_variant **ret, int fd) {
if (r < 0)
return r;
r = name_to_handle_at_try_fid(fd, "", &fid, &mntid, AT_EMPTY_PATH);
r = name_to_handle_at_try_fid(fd, "", &fid, &mntid, /* ret_unique_mnt_id = */ NULL, AT_EMPTY_PATH);
if (r < 0 && is_name_to_handle_at_fatal_error(r))
return r;
+7 -19
View File
@@ -451,27 +451,15 @@ static int image_make(
path_startswith(path, "/usr") ||
(faccessat(fd, "", W_OK, AT_EACCESS|AT_EMPTY_PATH) < 0 && errno == EROFS);
uint64_t on_mount_id = 0;
_cleanup_free_ struct file_handle *fh = NULL;
uint64_t on_mount_id;
int _mnt_id;
r = name_to_handle_at_try_unique_mntid_fid(fd, /* path= */ NULL, &fh, &on_mount_id, /* flags= */ 0);
if (r < 0) {
if (is_name_to_handle_at_fatal_error(r))
return r;
r = path_get_unique_mnt_id_at(fd, /* path= */ NULL, &on_mount_id);
if (r < 0) {
if (!ERRNO_IS_NEG_NOT_SUPPORTED(r))
return r;
int on_mount_id_fallback = -1;
r = path_get_mnt_id_at(fd, /* path= */ NULL, &on_mount_id_fallback);
if (r < 0)
return r;
on_mount_id = on_mount_id_fallback;
}
}
r = name_to_handle_at_try_fid(fd, /* path= */ NULL, &fh, &_mnt_id, &on_mount_id, AT_EMPTY_PATH);
if (r < 0)
return r;
if (r == 0)
on_mount_id = _mnt_id;
if (S_ISDIR(st->st_mode)) {
unsigned file_attr = 0;
+18 -2
View File
@@ -1015,8 +1015,10 @@ static int make_tmpfs(void) {
struct make_archive_data {
struct archive *archive;
TarFlags flags;
int hardlink_db_fd;
char *hardlink_db_path;
int have_unique_mount_id;
};
static int hardlink_lookup(
@@ -1042,16 +1044,29 @@ static int hardlink_lookup(
if (FLAGS_SET(sx->stx_mask, STATX_TYPE) && !inode_type_can_hardlink(sx->stx_mode))
goto bypass;
uint64_t unique_mnt_id;
int mnt_id;
r = name_to_handle_at_try_fid(inode_fd, /* path= */ NULL, &handle, &mnt_id, /* flags= */ AT_EMPTY_PATH);
r = name_to_handle_at_try_fid(inode_fd, /* path= */ NULL,
&handle,
d->have_unique_mount_id <= 0 ? &mnt_id : NULL,
d->have_unique_mount_id != 0 ? &unique_mnt_id : NULL,
/* flags= */ AT_EMPTY_PATH);
if (r < 0)
return log_error_errno(r, "Failed to get file handle of file: %m");
if (d->have_unique_mount_id < 0)
d->have_unique_mount_id = r > 0;
else
assert(d->have_unique_mount_id == (r > 0));
m = hexmem(SHA256_DIRECT(handle->f_handle, handle->handle_bytes), SHA256_DIGEST_SIZE);
if (!m)
return log_oom();
if (asprintf(&n, "%i:%i:%s", mnt_id, handle->handle_type, m) < 0)
if (d->have_unique_mount_id)
r = asprintf(&n, "%" PRIu64 ":%i:%s", unique_mnt_id, handle->handle_type, m);
else
r = asprintf(&n, "%i:%i:%s", mnt_id, handle->handle_type, m);
if (r < 0)
return log_oom();
if (d->hardlink_db_fd < 0) {
@@ -1467,6 +1482,7 @@ int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) {
.archive = a,
.flags = flags,
.hardlink_db_fd = -EBADF,
.have_unique_mount_id = -1,
};
r = recurse_dir(tree_fd,