mirror of
https://git.proxmox.com/git/mirror_zfs.git
synced 2026-05-23 19:04:45 +03:00
0b58f1db89
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 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 <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 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 <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>
196 lines
4.7 KiB
C
196 lines
4.7 KiB
C
// SPDX-License-Identifier: CDDL-1.0
|
|
/*
|
|
* CDDL HEADER START
|
|
*
|
|
* The contents of this file are subject to the terms of the
|
|
* Common Development and Distribution License, Version 1.0 only
|
|
* (the "License"). You may not use this file except in compliance
|
|
* with the License.
|
|
*
|
|
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
|
|
* or https://opensource.org/licenses/CDDL-1.0.
|
|
* See the License for the specific language governing permissions
|
|
* and limitations under the License.
|
|
*
|
|
* When distributing Covered Code, include this CDDL HEADER in each
|
|
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
|
|
* If applicable, add the following below this CDDL HEADER, with the
|
|
* fields enclosed by brackets "[]" replaced with your own identifying
|
|
* information: Portions Copyright [yyyy] [name of copyright owner]
|
|
*
|
|
* CDDL HEADER END
|
|
*/
|
|
/*
|
|
* Copyright 2005 Sun Microsystems, Inc. All rights reserved.
|
|
* Copyright 2006 Ricardo Correia. All rights reserved.
|
|
* Use is subject to license terms.
|
|
*/
|
|
|
|
/* Copyright (c) 1988 AT&T */
|
|
/* All Rights Reserved */
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <mntent.h>
|
|
#include <sys/errno.h>
|
|
#include <sys/mnttab.h>
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/sysmacros.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <libzutil.h>
|
|
|
|
#define BUFSIZE (MNT_LINE_MAX + 2)
|
|
|
|
static __thread char buf[BUFSIZE];
|
|
|
|
#define DIFF(xx) ( \
|
|
(mrefp->xx != NULL) && \
|
|
(mgetp->xx == NULL || strcmp(mrefp->xx, mgetp->xx) != 0))
|
|
|
|
int
|
|
getmntany(FILE *fp, struct mnttab *mgetp, struct mnttab *mrefp)
|
|
{
|
|
int ret;
|
|
|
|
while (
|
|
((ret = _sol_getmntent(fp, mgetp)) == 0) && (
|
|
DIFF(mnt_special) || DIFF(mnt_mountp) ||
|
|
DIFF(mnt_fstype) || DIFF(mnt_mntopts))) { }
|
|
|
|
return (ret);
|
|
}
|
|
|
|
int
|
|
_sol_getmntent(FILE *fp, struct mnttab *mgetp)
|
|
{
|
|
struct mntent mntbuf;
|
|
struct mntent *ret;
|
|
|
|
ret = getmntent_r(fp, &mntbuf, buf, BUFSIZE);
|
|
|
|
if (ret != NULL) {
|
|
mgetp->mnt_special = mntbuf.mnt_fsname;
|
|
mgetp->mnt_mountp = mntbuf.mnt_dir;
|
|
mgetp->mnt_fstype = mntbuf.mnt_type;
|
|
mgetp->mnt_mntopts = mntbuf.mnt_opts;
|
|
return (0);
|
|
}
|
|
|
|
if (feof(fp))
|
|
return (-1);
|
|
|
|
return (MNT_TOOLONG);
|
|
}
|
|
|
|
static int
|
|
getextmntent_impl(FILE *fp, struct extmnttab *mp, uint64_t *mnt_id)
|
|
{
|
|
int ret;
|
|
struct stat64 st;
|
|
|
|
*mnt_id = 0;
|
|
ret = _sol_getmntent(fp, (struct mnttab *)mp);
|
|
if (ret == 0) {
|
|
#ifdef HAVE_STATX_MNT_ID
|
|
struct statx stx;
|
|
if (statx(AT_FDCWD, mp->mnt_mountp,
|
|
AT_STATX_SYNC_AS_STAT | AT_SYMLINK_NOFOLLOW,
|
|
STATX_MNT_ID, &stx) == 0 && (stx.stx_mask & STATX_MNT_ID))
|
|
*mnt_id = stx.stx_mnt_id;
|
|
#endif
|
|
if (stat64(mp->mnt_mountp, &st) != 0) {
|
|
mp->mnt_major = 0;
|
|
mp->mnt_minor = 0;
|
|
return (ret);
|
|
}
|
|
mp->mnt_major = major(st.st_dev);
|
|
mp->mnt_minor = minor(st.st_dev);
|
|
}
|
|
|
|
return (ret);
|
|
}
|
|
|
|
int
|
|
getextmntent(const char *path, struct extmnttab *entry, struct stat64 *statbuf)
|
|
{
|
|
struct stat64 st;
|
|
FILE *fp;
|
|
int match;
|
|
boolean_t have_mnt_id = B_FALSE;
|
|
uint64_t target_mnt_id = 0;
|
|
uint64_t entry_mnt_id;
|
|
#ifdef HAVE_STATX_MNT_ID
|
|
struct statx stx;
|
|
#endif
|
|
|
|
if (strlen(path) >= MAXPATHLEN) {
|
|
(void) fprintf(stderr, "invalid object; pathname too long\n");
|
|
return (-1);
|
|
}
|
|
|
|
/*
|
|
* Search for the path in /proc/self/mounts. Rather than looking for the
|
|
* specific path, which can be fooled by non-standard paths (i.e. ".."
|
|
* or "//"), we stat() the path and search for the corresponding
|
|
* (major,minor) device pair.
|
|
*/
|
|
if (stat64(path, statbuf) != 0) {
|
|
(void) fprintf(stderr, "cannot open '%s': %s\n",
|
|
path, zfs_strerror(errno));
|
|
return (-1);
|
|
}
|
|
|
|
#ifdef HAVE_STATX_MNT_ID
|
|
/*
|
|
* 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;
|
|
}
|
|
#endif
|
|
|
|
if ((fp = fopen(MNTTAB, "re")) == NULL) {
|
|
(void) fprintf(stderr, "cannot open %s\n", MNTTAB);
|
|
return (-1);
|
|
}
|
|
|
|
/*
|
|
* Search for the given (major,minor) pair in the mount table.
|
|
*/
|
|
|
|
match = 0;
|
|
while (getextmntent_impl(fp, entry, &entry_mnt_id) == 0) {
|
|
if (have_mnt_id) {
|
|
match = (entry_mnt_id == target_mnt_id);
|
|
} else {
|
|
match = makedev(entry->mnt_major, entry->mnt_minor) ==
|
|
statbuf->st_dev;
|
|
}
|
|
if (match)
|
|
break;
|
|
}
|
|
(void) fclose(fp);
|
|
|
|
if (!match) {
|
|
(void) fprintf(stderr, "cannot find mountpoint for '%s'\n",
|
|
path);
|
|
return (-1);
|
|
}
|
|
|
|
if (stat64(entry->mnt_mountp, &st) != 0) {
|
|
entry->mnt_major = 0;
|
|
entry->mnt_minor = 0;
|
|
return (-1);
|
|
}
|
|
|
|
return (0);
|
|
}
|