git: 81b5b2265d77 - main - makefs/zfs: count "." in the link count and size of dataset roots

From: Renato Botelho <garga_at_FreeBSD.org>
Date: Fri, 09 Oct 2026 14:50:22 UTC
The branch main has been updated by garga:

URL: https://cgit.FreeBSD.org/src/commit/?id=81b5b2265d7734f9c4d4603031b3e8254c7e23d8

commit 81b5b2265d7734f9c4d4603031b3e8254c7e23d8
Author:     Renato Botelho <garga@FreeBSD.org>
AuthorDate: 2026-10-07 11:32:45 +0000
Commit:     Renato Botelho <garga@FreeBSD.org>
CommitDate: 2026-10-09 14:49:32 +0000

    makefs/zfs: count "." in the link count and size of dataset roots
    
    The root directory of a dataset is populated from an fsnode that is the
    "." entry of the staging directory, and its children are the siblings of
    that node, so the loop that computes the link count and the size of the
    directory (which expects "." among the children, as the nodes of the
    other directories have) never counts it.  The link count of the root of
    every dataset is one less than it should be (1 for an empty dataset
    instead of 2, 19 instead of 20 for a root with 18 subdirectories), and
    so is its size.
    
    fts(3) trusts the link count of the directories of ZFS when called with
    FTS_NOSTAT and FTS_PHYSICAL: it stops looking for subdirectories once it
    has seen as many as the link count says, and does not descend into the
    ones it did not look at.  As a result find(1), and rm(1) when it does
    not need to stat the entries, silently skip directories of a pool made
    by makefs.  On a root filesystem made by makefs, find / does not list
    /etc at all.
    
    Start the loop at the "." node itself for the roots of datasets, so that
    it is counted the same way as in every other directory.  Add a test that
    checks the link counts and the sizes of the dataset roots and of the
    other directories, with nested datasets and a dataset whose mountpoint
    is not in the staging tree, and that find(1) sees the whole tree.
    
    Fixes:          240afd8c1fc ("makefs: Add ZFS support")
    MFC after:      1 week
    Approved by:    markj
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
    Differential Revision:  https://reviews.freebsd.org/D60439
---
 usr.sbin/makefs/tests/makefs_zfs_tests.sh | 54 +++++++++++++++++++++++++++++++
 usr.sbin/makefs/zfs/fs.c                  |  7 ++--
 2 files changed, 58 insertions(+), 3 deletions(-)

diff --git a/usr.sbin/makefs/tests/makefs_zfs_tests.sh b/usr.sbin/makefs/tests/makefs_zfs_tests.sh
index fe27c62ab765..b8595a98a2ab 100644
--- a/usr.sbin/makefs/tests/makefs_zfs_tests.sh
+++ b/usr.sbin/makefs/tests/makefs_zfs_tests.sh
@@ -277,6 +277,59 @@ compression_cleanup()
 	common_cleanup
 }
 
+#
+# Make sure that the link count and the size of the directories which are the
+# roots of datasets count "." and the directories which are the mountpoints
+# of other datasets.  fts(3) trusts the link count of the directories of ZFS
+# and skips the subdirectories that it does not account for.
+#
+atf_test_case dataset_link_counts cleanup
+dataset_link_counts_body()
+{
+	local dir entries subdirs
+
+	create_test_dirs
+	cd $TEST_INPUTS_DIR
+
+	mkdir -p dir1/sub1 dir1/sub2 dir1/dir2/sub3 dir1/dir2/dir3 dir4
+	echo a > dir1/a
+	echo b > dir1/dir2/b
+	echo c > dir1/sub1/c
+	echo d > dir1/sub2/d
+	echo e > dir1/dir2/sub3/e
+
+	cd -
+
+	atf_check $MAKEFS -s 1g -o rootpath=/ -o poolname=$ZFS_POOL_NAME \
+	    -o fs=${ZFS_POOL_NAME}/dir1 -o fs=${ZFS_POOL_NAME}/dir1/dir2 \
+	    -o fs=${ZFS_POOL_NAME}/dir1/dir2/dir3 -o fs=${ZFS_POOL_NAME}/dir4 \
+	    -o fs=${ZFS_POOL_NAME}/dir5 \
+	    $TEST_IMAGE $TEST_INPUTS_DIR
+
+	import_image
+
+	# find(1) relies on the link counts to decide which entries may be
+	# directories, so it must see the whole tree.
+	(cd $TEST_INPUTS_DIR && find dir1 dir4 | sort) > ./expected
+	atf_check -o file:./expected -x \
+	    "cd $TEST_MOUNT_DIR && find dir1 dir4 | sort"
+
+	# The link count is the number of directories in the directory plus
+	# "." and "..", and the size is the number of entries plus the same.
+	for dir in . dir1 dir1/sub1 dir1/dir2 dir1/dir2/dir3 dir4 dir5; do
+		subdirs=$(ls -d $TEST_MOUNT_DIR/$dir/*/ 2>/dev/null | wc -l)
+		entries=$(ls -A $TEST_MOUNT_DIR/$dir | wc -l)
+		atf_check -o inline:$((subdirs + 2))\\n \
+		    stat -f %l $TEST_MOUNT_DIR/$dir
+		atf_check -o inline:$((entries + 2))\\n \
+		    stat -f %z $TEST_MOUNT_DIR/$dir
+	done
+}
+dataset_link_counts_cleanup()
+{
+	common_cleanup
+}
+
 #
 # Try destroying a dataset that was created by makefs.
 #
@@ -1171,6 +1224,7 @@ atf_init_test_cases()
 	atf_add_test_case autotrim
 	atf_add_test_case basic
 	atf_add_test_case compression
+	atf_add_test_case dataset_link_counts
 	atf_add_test_case dataset_removal
 	atf_add_test_case devfs
 	atf_add_test_case empty_dir
diff --git a/usr.sbin/makefs/zfs/fs.c b/usr.sbin/makefs/zfs/fs.c
index 75f6e30e1500..4e15b4a79f14 100644
--- a/usr.sbin/makefs/zfs/fs.c
+++ b/usr.sbin/makefs/zfs/fs.c
@@ -389,10 +389,11 @@ fs_populate_sattrs(struct fs_populate_arg *arg, const fsnode *cur,
 			 * The size of a ZPL directory is the number of entries
 			 * (including "." and ".."), and the link count is the
 			 * number of entries which are directories
-			 * (including "." and "..").
+			 * (including "." and "..").  The root of a dataset is
+			 * the "." node itself, followed by the other entries.
 			 */
-			for (fsnode *c =
-			    fsnode_isroot(cur) ? cur->next : cur->child;
+			for (const fsnode *c =
+			    fsnode_isroot(cur) ? cur : cur->child;
 			    c != NULL; c = c->next) {
 				switch (c->type) {
 				case S_IFDIR: