libzfs: don't truncate a resolved vdev path in zpool_vdev_name()

zpool_vdev_name() copied the result of realpath() into a 64 byte stack
buffer shared with the short formatted names, so a vdev whose resolved
path was longer than 63 bytes was reported cut short by zpool status -L
and anything else asking for VDEV_NAME_FOLLOW_LINKS.  The cut is made at
a byte boundary, so a multi-byte character straddling it is left as
invalid UTF-8, which also makes zpool status -j -L emit JSON a parser
rejects.

Resolve straight into a buffer of the right size.  realpath() fills a
caller supplied buffer of at least PATH_MAX, as it is used elsewhere in
libzutil, which also removes the intermediate allocation.

The other three users of that buffer format a guid, a raidz name, or a
draid name, and none of them can exceed its length.

Reviewed-by: Brian Behlendorf <[email protected]>
Signed-off-by: Michael Heller <[email protected]>
Closes #18851
Closes #18871
This commit is contained in:
mkhllr authored and GitHub committed 2026-07-31 13:06:11 -07:00
1 parent be62c53850
commit 74e76f2d80
4 files changed
+94 -6

No files matched your search

+3 -6
View File
@@ -4740,6 +4740,7 @@ zpool_vdev_name(libzfs_handle_t *hdl, zpool_handle_t *zhp, nvlist_t *nv,
uint64_t value;
char buf[PATH_BUF_LEN];
char tmpbuf[PATH_BUF_LEN * 2];
char rpath[MAXPATHLEN];
/*
* vdev_name will be "root"/"root-0" for the root vdev, but it is the
@@ -4765,12 +4766,8 @@ zpool_vdev_name(libzfs_handle_t *hdl, zpool_handle_t *zhp, nvlist_t *nv,
path = tpath;
if (name_flags & VDEV_NAME_FOLLOW_LINKS) {
char *rp = realpath(path, NULL);
if (rp) {
strlcpy(buf, rp, sizeof (buf));
path = buf;
free(rp);
}
if (realpath(path, rpath) != NULL)
path = rpath;
}
/*
+1
View File
@@ -594,6 +594,7 @@ tests = ['zpool_status_001_pos', 'zpool_status_002_pos',
'zpool_status_003_pos', 'zpool_status_004_pos',
'zpool_status_005_pos', 'zpool_status_006_pos',
'zpool_status_007_pos', 'zpool_status_008_pos',
'zpool_status_009_pos',
'zpool_status_features_001_pos']
tags = ['functional', 'cli_root', 'zpool_status']
+1
View File
@@ -1386,6 +1386,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/cli_root/zpool_status/zpool_status_006_pos.ksh \
functional/cli_root/zpool_status/zpool_status_007_pos.ksh \
functional/cli_root/zpool_status/zpool_status_008_pos.ksh \
functional/cli_root/zpool_status/zpool_status_009_pos.ksh \
functional/cli_root/zpool_status/zpool_status_features_001_pos.ksh \
functional/cli_root/zpool_sync/cleanup.ksh \
functional/cli_root/zpool_sync/setup.ksh \
@@ -0,0 +1,89 @@
#!/bin/ksh -p
# SPDX-License-Identifier: CDDL-1.0
#
# CDDL HEADER START
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source. A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#
# CDDL HEADER END
#
#
# Copyright (c) 2026 by Michael Heller.
#
. $STF_SUITE/include/libtest.shlib
#
# DESCRIPTION:
# Verify 'zpool status -L' does not truncate a long vdev path.
#
# STRATEGY:
# 1. Create a pool on a file vdev whose path is longer than the buffer the
# resolved path used to be copied into.
# 2. Verify 'zpool status -L' reports the whole path.
# 3. Verify 'zpool status' without -L reports it too, which it always did.
#
verify_runnable "global"
function cleanup
{
poolexists $TESTPOOL2 && destroy_pool $TESTPOOL2
log_must rm -rf $longdir
}
log_assert "'zpool status -L' does not truncate a long vdev path"
log_onexit cleanup
#
# The resolved path has to exceed 63 bytes to reach the case this covers, so
# the name is padded out and the length checked rather than assumed.
#
longdir=$TESTDIR/$(printf 'd%.0s' {1..40})
longvdev=$longdir/$(printf 'v%.0s' {1..40})
typeset -i pathlen=${#longvdev}
if (( pathlen <= 63 )); then
log_fail "vdev path is only $pathlen bytes, too short to truncate"
fi
log_note "vdev path is $pathlen bytes"
log_must mkdir -p $longdir
log_must truncate -s $MINVDEVSIZE $longvdev
log_must zpool create -f $TESTPOOL2 $longvdev
#
# -L resolves symlinks in the path. A file vdev resolves to itself, so the
# whole path must come back unchanged. Report whatever name was printed
# rather than only whether it matched, so a failure shows how far it was cut.
#
function vdev_name_from_status # args
{
zpool status "$@" $TESTPOOL2 |
awk -v d="$TESTDIR" 'index($1, d) == 1 {print $1; exit}'
}
typeset name
name=$(vdev_name_from_status -L)
if [[ "$name" != "$longvdev" ]]; then
log_fail "'zpool status -L' reported a ${#name} byte name," \
"expected $pathlen bytes: '$name'"
fi
name=$(vdev_name_from_status)
if [[ "$name" != "$longvdev" ]]; then
log_fail "'zpool status' reported a ${#name} byte name," \
"expected $pathlen bytes: '$name'"
fi
log_pass "'zpool status -L' does not truncate a long vdev path"