Repository navigation
libspl/mnttab: follow symlinks when resolving path via statx - #18469
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes incorrect dataset resolution for symlink paths on Linux by ensuring getextmntent() follows symlinks when using statx(..., STATX_MNT_ID, ...), aligning statx() behavior with the existing stat64() call and preventing mismatched /proc/self/mounts lookups.
Changes:
- Remove
AT_SYMLINK_NOFOLLOWfrom thestatx()call ingetextmntent()so symlinks are resolved when determiningmnt_id. - Add a regression test validating that
zfs list -Ho name <symlink>returns the dataset owning the symlink target when the symlink crosses a mount boundary. - Register the new test in the test packaging (
Makefile.am) and test runfile (common.run).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
lib/libspl/os/linux/mnttab.c |
Makes statx() follow symlinks in getextmntent() to avoid selecting the wrong mount entry when a symlink crosses mount boundaries. |
tests/zfs-tests/tests/functional/cli_user/zfs_list/zfs_list_009_pos.ksh |
Adds coverage for path-to-dataset resolution through a cross-mount symlink. |
tests/zfs-tests/tests/Makefile.am |
Ensures the new test script is distributed/installed with the test suite. |
tests/runfiles/common.run |
Adds the new test to the cli_user/zfs_list test group so it runs in CI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ixhamza
left a comment
There was a problem hiding this comment.
Thanks for running this down, LGTM. I believe the new test should be moved to cli_root/ from cli_user/ as it does privileged operations (zfs create/destroy).
When the path argument to "zfs list -Ho name <path>" (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 523d9d6 ("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 <sys/stat.h>,
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 2b930f6 ("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 <symlink>" returns the dataset containing the
symlink's target rather than the dataset containing the symlink.
Signed-off-by: Prakash Surya <prakash.surya@perforce.com>
3c67eed to
6c80768
Compare
|
@ixhamza thank you! applied your suggestion. |
amotin
left a comment
There was a problem hiding this comment.
I was wondering whether the symlinks should be fallowed there in general, but consistency with stat64() case is definitely an argument.
|
@satmandu I'll try to pull this into the 2.3.7 and 2.4.2 patchsets. |
…#18469) When the path argument to "zfs list -Ho name <path>" (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 523d9d6 ("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 <sys/stat.h>, 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 2b930f6 ("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 <symlink>" returns the dataset containing the symlink's target rather than the dataset containing the symlink. Signed-off-by: Prakash Surya <prakash.surya@perforce.com> Reviewed-by: Ameer Hamza <ahamza@ixsystems.com> Reviewed-by: Mark Maybee <mark.maybee@delphix.com> Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
…#18469) When the path argument to "zfs list -Ho name <path>" (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 523d9d6 ("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 <sys/stat.h>, 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 2b930f6 ("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 <symlink>" returns the dataset containing the symlink's target rather than the dataset containing the symlink. Signed-off-by: Prakash Surya <prakash.surya@perforce.com> Reviewed-by: Ameer Hamza <ahamza@ixsystems.com> Reviewed-by: Mark Maybee <mark.maybee@delphix.com> Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
…#18469) When the path argument to "zfs list -Ho name <path>" (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 523d9d6 ("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 <sys/stat.h>, 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 2b930f6 ("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 <symlink>" returns the dataset containing the symlink's target rather than the dataset containing the symlink. Signed-off-by: Prakash Surya <prakash.surya@perforce.com> Reviewed-by: Ameer Hamza <ahamza@ixsystems.com> Reviewed-by: Mark Maybee <mark.maybee@delphix.com> Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
…#18469) When the path argument to "zfs list -Ho name <path>" (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 523d9d6 ("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 <sys/stat.h>, 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 2b930f6 ("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 <symlink>" returns the dataset containing the symlink's target rather than the dataset containing the symlink. Signed-off-by: Prakash Surya <prakash.surya@perforce.com> Reviewed-by: Ameer Hamza <ahamza@ixsystems.com> Reviewed-by: Mark Maybee <mark.maybee@delphix.com> Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
…#18469) When the path argument to "zfs list -Ho name <path>" (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 523d9d6 ("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 <sys/stat.h>, 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 2b930f6 ("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 <symlink>" returns the dataset containing the symlink's target rather than the dataset containing the symlink. Signed-off-by: Prakash Surya <prakash.surya@perforce.com> Reviewed-by: Ameer Hamza <ahamza@ixsystems.com> Reviewed-by: Mark Maybee <mark.maybee@delphix.com> Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
…#18469) When the path argument to "zfs list -Ho name <path>" (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 523d9d6 ("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 <sys/stat.h>, 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 2b930f6 ("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 <symlink>" returns the dataset containing the symlink's target rather than the dataset containing the symlink. Signed-off-by: Prakash Surya <prakash.surya@perforce.com> Reviewed-by: Ameer Hamza <ahamza@ixsystems.com> Reviewed-by: Mark Maybee <mark.maybee@delphix.com> Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
Motivation and Context
getextmntent()inlib/libspl/os/linux/mnttab.cusesstatx(2)withAT_SYMLINK_NOFOLLOWto obtain themnt_idof the caller-supplied path. Whenthat path is a symbolic link crossing a mount boundary,
AT_SYMLINK_NOFOLLOWprevents symlink resolution, causing
statxto return themnt_idof themount containing the symlink itself rather than the mount the symlink points
into. The subsequent search through
/proc/self/mountsthen matches the wrongentry, so
zfs list -Ho name <symlink-path>(and anything that callszfs_path_to_zhandle()) reports the wrong dataset.The same function calls
stat64()on the same path immediately beforehand, andstat64()always follows symlinks — so the two calls were inconsistent when thepath was a symlink.
The bug was introduced by 523d9d6 ("Validate mountpoint on path-based unmount
using statx") but was latent because the autoconf check in
config/user-statx.m4omitted
#define _GNU_SOURCEwhen testing forSTATX_MNT_ID, soHAVE_STATX_MNT_IDwas never defined and thestatx()path was dead code.It was exposed by 2b930f6 ("config: fix STATX_MNT_ID detection"), which
fixed the autoconf check and activated the broken path for the first time.
Description
Remove
AT_SYMLINK_NOFOLLOWfrom thestatx()call on the caller-suppliedpathingetextmntent(), so symlinks are followed — consistent with thestat64()call on the same path in the same function.The
AT_SYMLINK_NOFOLLOWflag is retained ingetextmntent_impl(), wherestatx()is called onmp->mnt_mountp(a path read from/proc/self/mounts,always a real directory).
A regression test is added in
tests/functional/cli_user/zfs_list/zfs_list_009_pos.ksh.How Has This Been Tested?
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 <symlink>returns the dataset containing the symlink'starget rather than the dataset containing the symlink.
The regression test
zfs_list_009_poswas also run viazfs-tests.shon bothan unpatched build (FAIL) and with this fix applied (PASS).
Types of changes
Checklist:
Signed-off-by.