From 4acb62930b72e4ee5675b58aaecd6101351b324e Mon Sep 17 00:00:00 2001 From: Prakash Surya Date: Tue, 28 Apr 2026 09:24:24 -0700 Subject: libspl/mnttab: follow symlinks when resolving path via statx (#18469) When the path argument to "zfs list -Ho name " (or any caller of zfs_path_to_zhandle()) is a symlink that crosses a mount boundary, the wrong dataset is returned. Instead of returning the dataset that owns the symlink's target, getextmntent() matches the dataset containing the symlink itself. For example, given two ZFS datasets "tank/ds1" and "tank/ds2", and a symlink "/tank/ds1/link" pointing into "/tank/ds2": $ sudo zfs list -Ho name /tank/ds1/link tank/ds1 The expected (and previous) behavior is to return "tank/ds2", since the symlink's target resides in that dataset. The problem is in getextmntent(), in lib/libspl/os/linux/mnttab.c. That function calls statx() on the caller-supplied path to obtain its mnt_id (used to match against the mnt_id of each entry in /proc/self/mounts), and it passes AT_SYMLINK_NOFOLLOW to that statx() call. As a result, the mnt_id returned reflects the symlink's location rather than the symlink target's mount, and the wrong /proc/self/mounts entry is matched. The same function also calls stat64() on the caller-supplied path (used as a fallback when STATX_MNT_ID is not available, and to populate the statbuf out-parameter). stat64() always follows symlinks, so the statx() and stat64() calls were inconsistent: one resolved the symlink, the other didn't. The AT_SYMLINK_NOFOLLOW behavior may be appropriate when statx() is called on a mount entry from /proc/self/mounts (which is always a real directory), but it is wrong for caller-supplied paths, which may be symlinks. This bug was introduced by 523d9d6007 ("Validate mountpoint on path-based unmount using statx"), which added the STATX_MNT_ID code path. However, the bug was latent: config/user-statx.m4 omitted "#define _GNU_SOURCE" when checking for STATX_MNT_ID in , so HAVE_STATX_MNT_ID was never defined, and the buggy statx() path was never compiled in. getextmntent() always fell back to the dev_t comparison via stat64(), which correctly follows symlinks. The fix to that autoconf check, in 2b930f63f8 ("config: fix STATX_MNT_ID detection"), caused HAVE_STATX_MNT_ID to be properly defined on kernels that support it, activating the broken AT_SYMLINK_NOFOLLOW path for the first time and exposing the regression. The fix is to drop AT_SYMLINK_NOFOLLOW from the statx() call so that symlinks are followed, matching the behavior of stat64() on the same path. Verified with a minimal reproducer: created two ZFS datasets, placed a symlink inside the first pointing into the second, and confirmed that "zfs list -Ho name " returns the dataset containing the symlink's target rather than the dataset containing the symlink. Signed-off-by: Prakash Surya Reviewed-by: Ameer Hamza Reviewed-by: Mark Maybee Reviewed-by: Alexander Motin --- lib/libspl/os/linux/mnttab.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'lib/libspl') diff --git a/lib/libspl/os/linux/mnttab.c b/lib/libspl/os/linux/mnttab.c index 25fa132ac6fc..f51219e898e6 100644 --- a/lib/libspl/os/linux/mnttab.c +++ b/lib/libspl/os/linux/mnttab.c @@ -125,7 +125,14 @@ getextmntent(const char *path, struct mnttab *entry, struct stat64 *statbuf) } #ifdef HAVE_STATX_MNT_ID - if (statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT | AT_SYMLINK_NOFOLLOW, + /* + * Use AT_STATX_SYNC_AS_STAT without AT_SYMLINK_NOFOLLOW so that + * symlinks are followed, matching the behavior of stat64() above. + * Without this, if path is a symlink crossing a mount boundary, + * statx() returns the mnt_id of the symlink's location rather + * than the symlink target's mount. + */ + if (statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, STATX_MNT_ID, &stx) == 0 && (stx.stx_mask & STATX_MNT_ID)) { have_mnt_id = B_TRUE; target_mnt_id = stx.stx_mnt_id; -- cgit v1.3