From nobody Thu Sep 03 02:16:51 2026 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hb3BW2BMKz6qMV7 for ; Thu, 03 Sep 2026 02:16:51 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hb3BW1Qtyz3TD7 for ; Thu, 03 Sep 2026 02:16:51 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1788401811; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=imnpa/bV2Ar79SqP+pfx4DNvLPRYYcwN+42lEfIaOq0=; b=FGwXRsSk4l+iluQfjQFFxZvJptsJsxaGayLUwBQ10wvvvB9hCkjug0g3dFVDejB6V8Q9NH NSGtGNypn7LFIgPy36S/6CUfPLVJ5vx2a72IqCYhuTjydR7+WUeMYxcHcUKbGO54pwcZuQ 2CNjXAoeYA61mG7HKqnAq10Z6UBBH8sYYVMtk6KhKq0CLJZHi9sBOln8Kd8nZbEKuComkN rdnrGysVeS0uO4a/H3kA/imozDLLct1jWQMtr33QL29zq9OJF4skiXUSN7S83z+xwUEx1n t41PTeE83gdFThOcZ9wjfB7kpPIdCf4nWk+UYXf7PqXjEfb1JEATfOb0Hh+/4g== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1788401811; a=rsa-sha256; cv=none; b=IgDCXfRknfc4SBH8hlswXP7q8hYX6T4u/vL4lI1B9gsBJ2S7fOqoTkMLseDu6gpkbtkwJZ td5EKj4vi9ALzZvH76V9WK0R5CgVJ+JQFT69L13itj7ET9CgP8Z5XJZ9Y6EbiKdXa9heh4 +oHqm+++8g709QgUnNZnyt/ZntF3cp+AIqx0VI99M8gOP6Cbqnr1aF/ZbKhHzgD+fQmCWc +2RCUtyCJStsL4iGo2XK4S5Cvl9GG4WVb2z38C3rriQGRItkEhRyb/C90k1vnpFxECho78 8KMIp4VOdSf+7H+CL5DyF29co2wxAwlFNNcdo5gtqj9lmGny31oN6pwSjinFxA== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1788401811; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=imnpa/bV2Ar79SqP+pfx4DNvLPRYYcwN+42lEfIaOq0=; b=tk/8itAlrQcr3vk0sW5jlknyXbiXXAivD+2P90VEnLRy6zl3GS6rYFSUy/TCJ25Aq8XSRx soSECeX0tW6W7D54RNlQ1I5w3dylC6ZoWEgJxIoRYCtpKvd7YLXE96PuzGISK+FKIIw+mj o2MeUIJKii1J9Jc/WGGMQjE+TumAOz0xmqJpns4K/0CkWGP72IZaqzzXm0XrqHNF4Hxgvx lSb/Emhtllx3toB29Oy/XStuIuu1TbWQO2KWeIoEsGfF4VULoRJ0dWleRGeTLZURt3LjJY EJ+3aN0PY9YjyLlkHtJk/bceyI9XxUyVZ20zf7OMW7l7tzfzfg9YdOqlEqr6hw== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hb3BW0S0Qz18cs for ; Thu, 03 Sep 2026 02:16:51 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 26900 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Thu, 03 Sep 2026 02:16:51 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Xin LI Subject: git: bbaf254293f7 - main - fsck_msdosfs: add tests for lost cluster chain repair accounting List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: delphij X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: bbaf254293f7e19fa7b0f9ed1da21c7a43126d79 Auto-Submitted: auto-generated Date: Thu, 03 Sep 2026 02:16:51 +0000 Message-Id: <6a98d893.26900.76780dae@gitrepo.freebsd.org> The branch main has been updated by delphij: URL: https://cgit.FreeBSD.org/src/commit/?id=bbaf254293f7e19fa7b0f9ed1da21c7a43126d79 commit bbaf254293f7e19fa7b0f9ed1da21c7a43126d79 Author: Xin LI AuthorDate: 2026-09-03 02:15:05 +0000 Commit: Xin LI CommitDate: 2026-09-03 02:16:22 +0000 fsck_msdosfs: add tests for lost cluster chain repair accounting Add an ATF test suite covering Phase 3 ("Checking for Lost Files") error accounting. Test images are created using newfs_msdos(8), and lost cluster chains are injected directly into FAT copies at offsets derived from the BPB. The LOST.DIR directory required by reconnect() is constructed similarly: a root directory entry with ATTR_DIRECTORY set and its first cluster pointing to a zero-filled cluster containing "." and ".." entries. The lost_chain_cleared and corrupted_lost_chain_reconnected test cases provide regression coverage for the preceding commit: - lost_chain_cleared verifies that clearing a lost chain (the fallback taken when LOST.DIR is absent) exits with status 0 rather than 8 (unrecovered error). - corrupted_lost_chain_reconnected verifies that FAT modifications from a chain truncated by checkchain() prior to reconnection are written back to disk, requiring "Update FATs? yes" and ensuring a clean second pass. Additionally, lost_chain_left_alone, lost_chain_preen, and corrupted_lost_chain_left_alone cover scenarios that must continue to report unrecovered errors: read-only mode (-n), which performs no repairs and leaves the image byte-for-byte unchanged, and preen mode (-p), which attempts reconnection but does not clear lost chains. MFC after: 1 week --- etc/mtree/BSD.tests.dist | 2 + sbin/fsck_msdosfs/Makefile | 5 + sbin/fsck_msdosfs/tests/Makefile | 5 + sbin/fsck_msdosfs/tests/fsck_msdosfs_test.sh | 261 +++++++++++++++++++++++++++ 4 files changed, 273 insertions(+) diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist index 9cc9fad8222b..3843ac9254b5 100644 --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -491,6 +491,8 @@ .. dhclient .. + fsck_msdosfs + .. growfs .. ifconfig diff --git a/sbin/fsck_msdosfs/Makefile b/sbin/fsck_msdosfs/Makefile index d5fe24b220b7..2651f5c2b755 100644 --- a/sbin/fsck_msdosfs/Makefile +++ b/sbin/fsck_msdosfs/Makefile @@ -1,5 +1,7 @@ # $NetBSD: Makefile,v 1.6 1997/05/08 21:11:11 gwr Exp $ +.include + FSCK= ${.CURDIR:H}/fsck .PATH: ${FSCK} @@ -11,4 +13,7 @@ SRCS= main.c check.c boot.c fat.c dir.c fsutil.c CFLAGS+= -I${FSCK} -DHAVE_LIBUTIL_H LIBADD= util +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + .include diff --git a/sbin/fsck_msdosfs/tests/Makefile b/sbin/fsck_msdosfs/tests/Makefile new file mode 100644 index 000000000000..a36439f91bce --- /dev/null +++ b/sbin/fsck_msdosfs/tests/Makefile @@ -0,0 +1,5 @@ +PACKAGE= tests + +ATF_TESTS_SH= fsck_msdosfs_test + +.include diff --git a/sbin/fsck_msdosfs/tests/fsck_msdosfs_test.sh b/sbin/fsck_msdosfs/tests/fsck_msdosfs_test.sh new file mode 100644 index 000000000000..1c5dd2cff887 --- /dev/null +++ b/sbin/fsck_msdosfs/tests/fsck_msdosfs_test.sh @@ -0,0 +1,261 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 The FreeBSD Foundation +# + +# Tests for fsck_msdosfs(8) phase 3 ("Checking for Lost Files") repair +# accounting: a lost cluster chain that fsck_msdosfs(8) has repaired must +# not be reported as an unrecovered error, and one that it has left alone +# must be. + +IMG=fat16.img + +# Read an unsigned little-endian integer of $3 bytes at offset $2 of $1. +bpb_read() +{ + od -An -v -tu1 -j "$2" -N "$3" "$1" | awk ' + { for (i = 1; i <= NF; i++) b[n++] = $i } + END { v = 0; for (i = n - 1; i >= 0; i--) v = v * 256 + b[i] + print v }' +} + +# Write the unsigned 16 bit little-endian value $3 at offset $2 of $1. +poke16() +{ + printf "$(printf '\\%03o\\%03o' $(($3 & 255)) $((($3 >> 8) & 255)))" | + dd of="$1" bs=1 seek="$2" conv=notrunc status=none +} + +# Write the unsigned 8 bit value $3 at offset $2 of $1. +poke8() +{ + printf "$(printf '\\%03o' $(($3 & 255)))" | + dd of="$1" bs=1 seek="$2" conv=notrunc status=none +} + +# Write the ASCII string $3 at offset $2 of $1. +poke_str() +{ + printf '%s' "$3" | dd of="$1" bs=1 seek="$2" conv=notrunc status=none +} + +# Create a 4 MiB FAT16 file system in $IMG. One sector per cluster keeps +# the cluster numbers used below well inside the data area. +make_image() +{ + atf_check -s exit:0 -o ignore -e ignore \ + newfs_msdos -C 4m -F 16 -c 1 -S 512 ./${IMG} + # A freshly created file system must be clean. + atf_check -s exit:0 -o ignore -e ignore fsck_msdosfs -y ./${IMG} +} + +# Mark clusters 300, 301 and 302 of $IMG as an allocated chain in every +# copy of the FAT. No directory entry refers to them, so fsck_msdosfs(8) +# has to find them as a lost chain in phase 3. These cluster numbers are +# used because neither byte of their little-endian FAT16 encoding is NUL. +inject_lost_chain() +{ + local bps rsvd nfats fatsz i base + + bps=$(bpb_read ${IMG} 11 2) + rsvd=$(bpb_read ${IMG} 14 2) + nfats=$(bpb_read ${IMG} 16 1) + fatsz=$(bpb_read ${IMG} 22 2) + + i=0 + while [ "${i}" -lt "${nfats}" ]; do + base=$((rsvd * bps + i * fatsz * bps)) + poke16 ${IMG} $((base + 300 * 2)) 301 + poke16 ${IMG} $((base + 301 * 2)) 302 + poke16 ${IMG} $((base + 302 * 2)) 65535 + i=$((i + 1)) + done +} + +# Mark clusters 300 and 301 of $IMG as an allocated lost chain where 300 points +# to 301 and 301 points to CLUST_FREE (0). No directory entry refers to them, so +# checklost() finds cluster 300 as a lost chain head, but checkchain() fails +# because the chain ends unexpectedly with a free cluster. +inject_corrupted_lost_chain() +{ + local bps rsvd nfats fatsz i base + + bps=$(bpb_read ${IMG} 11 2) + rsvd=$(bpb_read ${IMG} 14 2) + nfats=$(bpb_read ${IMG} 16 1) + fatsz=$(bpb_read ${IMG} 22 2) + + i=0 + while [ "${i}" -lt "${nfats}" ]; do + base=$((rsvd * bps + i * fatsz * bps)) + poke16 ${IMG} $((base + 300 * 2)) 301 + poke16 ${IMG} $((base + 301 * 2)) 0 + i=$((i + 1)) + done +} + +# Create an empty LOST.DIR in the root directory of $IMG, with cluster 400 +# holding its contents, so that reconnect() has somewhere to link a lost +# chain to. Everything not written here is already zero in a freshly +# created file system, which is what these fields need to be. +create_lost_dir() +{ + local bps spc rsvd nfats rootent fatsz i base rootoff dataoff dir + + bps=$(bpb_read ${IMG} 11 2) + spc=$(bpb_read ${IMG} 13 1) + rsvd=$(bpb_read ${IMG} 14 2) + nfats=$(bpb_read ${IMG} 16 1) + rootent=$(bpb_read ${IMG} 17 2) + fatsz=$(bpb_read ${IMG} 22 2) + + i=0 + while [ "${i}" -lt "${nfats}" ]; do + base=$((rsvd * bps + i * fatsz * bps)) + poke16 ${IMG} $((base + 400 * 2)) 65535 + i=$((i + 1)) + done + + rootoff=$(((rsvd + nfats * fatsz) * bps)) + dataoff=$((rootoff + rootent * 32)) + dir=$((dataoff + (400 - 2) * spc * bps)) + + # The entry in the root directory. 16 is ATTR_DIRECTORY. + poke_str ${IMG} ${rootoff} 'LOST DIR' + poke8 ${IMG} $((rootoff + 11)) 16 + poke16 ${IMG} $((rootoff + 26)) 400 + + # Its "." and ".." entries. The remainder of the cluster stays + # zero, which reads as SLOT_EMPTY, so reconnect() has free slots. + poke_str ${IMG} ${dir} '. ' + poke8 ${IMG} $((dir + 11)) 16 + poke16 ${IMG} $((dir + 26)) 400 + poke_str ${IMG} $((dir + 32)) '.. ' + poke8 ${IMG} $((dir + 43)) 16 +} + +atf_test_case lost_chain_cleared +lost_chain_cleared_head() +{ + atf_set "descr" "A lost chain that was cleared is not an error" + atf_set "require.progs" "newfs_msdos fsck_msdosfs" +} +lost_chain_cleared_body() +{ + make_image + inject_lost_chain + + # There is no LOST.DIR, so reconnect() fails and fsck_msdosfs(8) + # falls back to clearing the chain. That repairs the file system, + # so the exit status must be 0 and not 8 (unrecovered error). + atf_check -s exit:0 \ + -o match:'Lost cluster chain at cluster 300' \ + -o match:'3 Cluster\(s\) lost' \ + -o match:'No LOST.DIR directory' \ + -o match:'Clear\? yes' \ + -e ignore \ + fsck_msdosfs -y ./${IMG} + + # The repair has to be durable: a second pass must find nothing. + atf_check -s exit:0 -o not-match:'Lost cluster chain' -e ignore \ + fsck_msdosfs -y ./${IMG} +} + +atf_test_case lost_chain_left_alone +lost_chain_left_alone_head() +{ + atf_set "descr" "A lost chain that was not repaired is an error" + atf_set "require.progs" "newfs_msdos fsck_msdosfs" +} +lost_chain_left_alone_body() +{ + make_image + inject_lost_chain + cp ${IMG} ${IMG}.save + + # In -n mode nothing is repaired, so the lost chain must still be + # reported as an unrecovered error and the image must not change. + atf_check -s exit:8 -o match:'Lost cluster chain at cluster 300' \ + -e ignore fsck_msdosfs -n ./${IMG} + atf_check cmp ${IMG}.save ${IMG} +} + +atf_test_case lost_chain_preen +lost_chain_preen_head() +{ + atf_set "descr" "Preen mode reports a lost chain it cannot reconnect" + atf_set "require.progs" "newfs_msdos fsck_msdosfs" +} +lost_chain_preen_body() +{ + make_image + inject_lost_chain + + # Preen mode attempts the reconnect but never clears, so with no + # LOST.DIR the chain stays lost and has to be reported. + atf_check -s exit:8 -o match:'Lost cluster chain at cluster 300' \ + -o match:'No LOST.DIR directory' -e ignore \ + fsck_msdosfs -p -f ./${IMG} +} + +atf_test_case corrupted_lost_chain_left_alone +corrupted_lost_chain_left_alone_head() +{ + atf_set "descr" "A corrupted lost chain that checkchain fails on is an error if left alone" + atf_set "require.progs" "newfs_msdos fsck_msdosfs" +} +corrupted_lost_chain_left_alone_body() +{ + make_image + inject_corrupted_lost_chain + cp ${IMG} ${IMG}.save + + # In -n mode nothing is repaired, so checkchain() returns FSERROR when + # the chain ends unexpectedly with CLUST_FREE. checklost() must not + # swallow this FSERROR, so fsck_msdosfs must exit 8 and leave the image unchanged. + atf_check -s exit:8 \ + -o match:'Cluster chain starting at 300 ends with cluster marked free' \ + -e ignore fsck_msdosfs -n ./${IMG} + atf_check cmp ${IMG}.save ${IMG} +} + +atf_test_case corrupted_lost_chain_reconnected +corrupted_lost_chain_reconnected_head() +{ + atf_set "descr" "Truncating a lost chain before reconnecting it is written out" + atf_set "require.progs" "newfs_msdos fsck_msdosfs" +} +corrupted_lost_chain_reconnected_body() +{ + make_image + create_lost_dir + # Adding LOST.DIR by hand must not have damaged anything. + atf_check -s exit:0 -o ignore -e ignore fsck_msdosfs -y ./${IMG} + inject_corrupted_lost_chain + + # checkchain() truncates the chain (FSFATMOD) and reconnect() then + # links it into LOST.DIR (FSDIRMOD). Both results have to reach + # mod: without the FSFATMOD, checkfilesys() never writes the FATs + # back and the truncation is silently discarded. + atf_check -s exit:0 \ + -o match:'Cluster chain starting at 300 ends with cluster marked free' \ + -o match:'Truncate\? yes' \ + -o match:'Lost cluster chain at cluster 300' \ + -o match:'Update FATs\? yes' \ + -e ignore \ + fsck_msdosfs -y ./${IMG} + + # The truncation has to be durable: a second pass must find nothing. + atf_check -s exit:0 -o not-match:'ends with cluster marked free' \ + -e ignore fsck_msdosfs -y ./${IMG} +} + +atf_init_test_cases() +{ + atf_add_test_case lost_chain_cleared + atf_add_test_case lost_chain_left_alone + atf_add_test_case lost_chain_preen + atf_add_test_case corrupted_lost_chain_left_alone + atf_add_test_case corrupted_lost_chain_reconnected +}