Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/libzutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ _LIBZUTIL_H char *zfs_strip_partition(const char *);
_LIBZUTIL_H const char *zfs_strip_path(const char *);

_LIBZUTIL_H int zfs_strcmp_pathname(const char *, const char *, int);
_LIBZUTIL_H int zfs_flatten_path(char *, size_t, char **);

_LIBZUTIL_H boolean_t zfs_dev_is_dm(const char *);
_LIBZUTIL_H boolean_t zfs_dev_is_whole_disk(const char *);
Expand Down
12 changes: 12 additions & 0 deletions lib/libzfs/libzfs.abi
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@
<elf-symbol name='zfs_device_get_physical' type='func-type' binding='global-binding' visibility='default-visibility' is-defined='yes'/>
<elf-symbol name='zfs_dirnamelen' type='func-type' binding='global-binding' visibility='default-visibility' is-defined='yes'/>
<elf-symbol name='zfs_expand_proplist' type='func-type' binding='global-binding' visibility='default-visibility' is-defined='yes'/>
<elf-symbol name='zfs_flatten_path' type='func-type' binding='global-binding' visibility='default-visibility' is-defined='yes'/>
<elf-symbol name='zfs_foreach_mountpoint' type='func-type' binding='global-binding' visibility='default-visibility' is-defined='yes'/>
<elf-symbol name='zfs_get_all_props' type='func-type' binding='global-binding' visibility='default-visibility' is-defined='yes'/>
<elf-symbol name='zfs_get_clones_nvl' type='func-type' binding='global-binding' visibility='default-visibility' is-defined='yes'/>
Expand Down Expand Up @@ -5957,6 +5958,12 @@
<parameter type-id='80f4b756'/>
<return type-id='48b5725f'/>
</function-decl>
<function-decl name='zfs_flatten_path' mangled-name='zfs_flatten_path' visibility='default' binding='global' size-in-bits='64' elf-symbol-id='zfs_flatten_path'>
<parameter type-id='26a90f95'/>
<parameter type-id='b59d7dce'/>
<parameter type-id='9b23c9ad'/>
<return type-id='95e97e5e'/>
</function-decl>
<function-decl name='mkdirp' mangled-name='mkdirp' visibility='default' binding='global' size-in-bits='64' elf-symbol-id='mkdirp'>
<parameter type-id='80f4b756'/>
<parameter type-id='d50d396c'/>
Expand Down Expand Up @@ -6554,6 +6561,11 @@
<pointer-type-def type-id='35acf840' size-in-bits='64' id='17f3480d'/>
<pointer-type-def type-id='688c495b' size-in-bits='64' id='cec6f2e4'/>
<pointer-type-def type-id='d11b7617' size-in-bits='64' id='23432aaa'/>
<function-decl name='dump_nvlist' visibility='default' binding='global' size-in-bits='64'>
<parameter type-id='5ce45b60'/>
<parameter type-id='95e97e5e'/>
<return type-id='48b5725f'/>
</function-decl>
<function-decl name='zpool_get_handle' mangled-name='zpool_get_handle' visibility='default' binding='global' size-in-bits='64' elf-symbol-id='zpool_get_handle'>
<parameter type-id='4c81de99'/>
<return type-id='b0382bb3'/>
Expand Down
9 changes: 9 additions & 0 deletions lib/libzfs/libzfs_mount.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* Copyright 2017 RackTop Systems.
* Copyright (c) 2018 Datto Inc.
* Copyright 2018 OmniOS Community Edition (OmniOSce) Association.
* Copyright (c) 2026, TrueNAS.
*/

/*
Expand Down Expand Up @@ -269,6 +270,7 @@ zfs_is_mountable(zfs_handle_t *zhp, char *buf, size_t buflen,
if (source)
*source = sourcetype;

VERIFY0(zfs_flatten_path(buf, buflen, NULL));
return (B_TRUE);
}

Expand Down Expand Up @@ -998,11 +1000,13 @@ mountpoint_cmp(const void *arga, const void *argb)
if (gota) {
verify(zfs_prop_get(za, ZFS_PROP_MOUNTPOINT, mounta,
sizeof (mounta), NULL, NULL, 0, B_FALSE) == 0);
VERIFY0(zfs_flatten_path(mounta, sizeof (mounta), NULL));
}
gotb = (zfs_get_type(zb) == ZFS_TYPE_FILESYSTEM);
if (gotb) {
verify(zfs_prop_get(zb, ZFS_PROP_MOUNTPOINT, mountb,
sizeof (mountb), NULL, NULL, 0, B_FALSE) == 0);
VERIFY0(zfs_flatten_path(mountb, sizeof (mountb), NULL));
}

if (gota && gotb) {
Expand Down Expand Up @@ -1061,10 +1065,13 @@ non_descendant_idx(zfs_handle_t **handles, size_t num_handles, int idx)

verify(zfs_prop_get(handles[idx], ZFS_PROP_MOUNTPOINT, parent,
sizeof (parent), NULL, NULL, 0, B_FALSE) == 0);
VERIFY0(zfs_flatten_path(parent, sizeof (parent), NULL));

for (i = idx + 1; i < num_handles; i++) {
verify(zfs_prop_get(handles[i], ZFS_PROP_MOUNTPOINT, child,
sizeof (child), NULL, NULL, 0, B_FALSE) == 0);
VERIFY0(zfs_flatten_path(child, sizeof (child), NULL));

if (!libzfs_path_contains(parent, child))
break;
}
Expand Down Expand Up @@ -1170,6 +1177,7 @@ zfs_mount_task(void *arg)

verify(zfs_prop_get(handles[idx], ZFS_PROP_MOUNTPOINT, mountpoint,
sizeof (mountpoint), NULL, NULL, 0, B_FALSE) == 0);
VERIFY0(zfs_flatten_path(mountpoint, sizeof (mountpoint), NULL));

if (mp->mnt_func(handles[idx], mp->mnt_data) != 0)
goto out;
Expand All @@ -1187,6 +1195,7 @@ zfs_mount_task(void *arg)
char child[ZFS_MAXPROPLEN];
verify(zfs_prop_get(handles[i], ZFS_PROP_MOUNTPOINT,
child, sizeof (child), NULL, NULL, 0, B_FALSE) == 0);
VERIFY0(zfs_flatten_path(child, sizeof (child), NULL));

if (!libzfs_path_contains(mountpoint, child))
break; /* not a descendant, return */
Expand Down
74 changes: 74 additions & 0 deletions lib/libzutil/zutil_device_path.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

/*
* Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2026, Rob Norris <robn@despairlabs.com>
*/

#include <errno.h>
Expand Down Expand Up @@ -203,3 +204,76 @@ zfs_strcmp_pathname(const char *name, const char *cmp, int wholedisk)

return (0);
}

/*
* "Flatten" a UNIX-style path by:
* - collapsing multiple '/' chars to just one
* - removing trailing '/' chars (one or more followed by '\0')
*
* args:
* - src: source buffer
* - srclen: size of source buffer (>= strlen(in)+1)
* - dstp: pointer to destination buffer:
* - if NULL, source buffer is modified in place
* - if set and *dstp is set, output written to *dstp
* - if set and *dstp is NULL, *dstp set to malloc(srclen)
*
* returns:
* - 0 on success. caller must free *dstp is it was allocated (above)
* - ENOMEM: alloc failed
*/
int
zfs_flatten_path(char *src, size_t srclen, char **dstp)
{
char *s, *d, *dstbuf = NULL;

s = src;
if (dstp == NULL)
/* Modify in-place */
d = src;
else if (*dstp != NULL)
/* Use provided dest buffer */
d = *dstp;
else {
/* Allocate dest buffer */
dstbuf = malloc(srclen);
if (dstbuf == NULL)
return (ENOMEM);
d = dstbuf;
}

while (*s != '\0') {
/* Copy char unless in-place and no change */
if (s != d)
*d = *s;

/* Found a slash */
if (*s == '/') {
/* Move past additional slashes */
while (*s == '/')
s++;

/*
* Found null after slash, abort. d still points
* at the slash copied above, which will then be
* nulled after the loop.
*/
if (*s == '\0')
break;
} else
/* Non-slash char copied, move source forward. */
s++;

/* Copied char ok, move dest forward for next iteration */
d++;
}

/* End of path, set dest terminator unless in-place and no move. */
if (s != d)
*d = '\0';

if (dstp != NULL && *dstp == NULL)
*dstp = dstbuf;

return (0);
}
2 changes: 1 addition & 1 deletion tests/runfiles/common.run
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ tests = ['zfs_mount_001_pos', 'zfs_mount_002_pos', 'zfs_mount_003_pos',
'zfs_mount_009_neg', 'zfs_mount_010_neg', 'zfs_mount_011_neg',
'zfs_mount_012_pos', 'zfs_mount_all_001_pos', 'zfs_mount_encrypted',
'zfs_mount_remount', 'zfs_mount_all_fail', 'zfs_mount_all_mountpoints',
'zfs_mount_test_race', 'zfs_mount_recursive']
'zfs_mount_test_race', 'zfs_mount_recursive', 'zfs_mount_slashes']
tags = ['functional', 'cli_root', 'zfs_mount']

[tests/functional/cli_root/zfs_program]
Expand Down
1 change: 1 addition & 0 deletions tests/zfs-tests/tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/cli_root/zfs_mount/zfs_mount_all_fail.ksh \
functional/cli_root/zfs_mount/zfs_mount_all_mountpoints.ksh \
functional/cli_root/zfs_mount/zfs_mount_encrypted.ksh \
functional/cli_root/zfs_mount/zfs_mount_slashes.ksh \
functional/cli_root/zfs_mount/zfs_mount_recursive.ksh \
functional/cli_root/zfs_mount/zfs_mount_remount.ksh \
functional/cli_root/zfs_mount/zfs_mount_test_race.ksh \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/ksh -p
# 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 (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 (c) 2026, TrueNAS.
#

. $STF_SUITE/include/libtest.shlib

#
# Ensure that datasets are mounted in the correct order even when their
# mountpoint= parameters have extra/redundant slashes.
#

verify_runnable "both"

function cleanup
{
zfs unmount -a
zfs destroy -R $TESTPOOL/aaa
zfs destroy -R $TESTPOOL/zzz
rm -rf /mnt/slashes
zfs mount -a
}
log_onexit cleanup

log_assert "Mountpoints with redundant slashes still mount in the correct order."

# We create the following set of filesystems:
#
# dataset mountpoint
# testpool/zzz /mnt/tree/
# testpool/zzz/a /mnt/tree//a (inherited)
# testpool/zzz/a/b /mnt/tree//a/b (inherited)
# testpool/aaa /mnt/tree/a/b//c
# testpool/aaa/d /mnt/tree/a/b//c/d (inherited)
#
# The dataset names and the creation order are deliberately set such that if
# for whatever reason mountpoint_cmp() falls back to comparing dataset names
# or relying on creation order, the mountpoint order will be wrong. The only
# way that the mountpoints can be ordered correctly is if mountpoint_cmp()
# understands what to do with extra interior and trailing slashes.
#
# If the mounts are performed in the wrong order, they will often still
# succeed, just not be visible in the filesystem. To handle this, we also add a
# file to the root of each filesystem with its expected position. If the mounts
# are out-of-order, these will be hidden "under" the top filesystem, and we
# will know that the order was wrong.

log_must zfs unmount -a

log_must zfs create -o mountpoint=/mnt/tree/a/b//c $TESTPOOL/aaa
log_must touch /mnt/tree/a/b/c/ident-c
log_must zfs create $TESTPOOL/aaa/d
log_must touch /mnt/tree/a/b/c/d/ident-d
log_must zfs unmount -a

log_must zfs create -o mountpoint=/mnt/tree/ $TESTPOOL/zzz
log_must touch /mnt/tree/ident-tree
log_must zfs create $TESTPOOL/zzz/a
log_must touch /mnt/tree/a/ident-a
log_must zfs create $TESTPOOL/zzz/a/b
log_must touch /mnt/tree/a/b/ident-b
log_must zfs unmount -a

# Mount everything, forcing mountpoint order to be resolved
log_must zfs mount -a

# Ensure all mounted, and that the expected filesystem appears at each point.
# `mount` and `df` can produce different results for reasons, check them both.
log_must test $(df | grep /mnt/tree | wc -l) == 5
log_must test $(mount | grep /mnt/tree | wc -l) == 5

# Finally, test that all the files are visible.
log_must test -f /mnt/tree/ident-tree
log_must test -f /mnt/tree/a/ident-a
log_must test -f /mnt/tree/a/b/ident-b
log_must test -f /mnt/tree/a/b/c/ident-c
log_must test -f /mnt/tree/a/b/c/d/ident-d

log_pass "Mountpoints with redundant slashes still mount in the correct order."
Loading