From nobody Thu Sep 10 02:32:08 2026 X-Original-To: dev-commits-src-branches@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 4hgMBw40N7z6rsRt for ; Thu, 10 Sep 2026 02:32:08 +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 4hgMBw3KnWz3KrG for ; Thu, 10 Sep 2026 02:32:08 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1789007528; 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=8onZD6K4bXJbtYrDKaAm9rMuVFDYlTrrfMdD1x2mkBM=; b=C3sj9UMwUtUKdBlQQjGneD9nDzSKcjPLQcRusWCvgu2ozv4KEsIO0uZjnKRLX5SMVkvlg0 ZrNgZL2jCi6mdh/vuXUEvtviqfRj+eSU608A9bNlo0I8VRolJqyXpdR1j8m5RNFJaQw2Uo xZ0L5L2wQ852jiUTGP28SLLDJz6jQR8zEouFUr4FopWIDw/KBoaM03EzXKl6Hn1p4n95t9 aHwjxVqnVlal8XMIXQtsE77joa/nx68KPg2N47/ube87031/Gxs303MXzeLFPINTamNdWV r1YG/iHJdCBXF1DkMeYdwQFlek8PdMBnl8piV0lB8pidUmBEN4yGZZy8ZHgdhw== ARC-Seal: i=1; a=rsa-sha256; d=freebsd.org; s=dkim; cv=none; t=1789007528; b=thVICNeZFIxp/d3Wx74Lvi3w8gHBrUxk6J/hiVLCzLKoY1mtEavmwhnhATDBJqzKj0GL7V T90SLXC1d2yMCwDd3gxoa3JrBClQ5H42HOVIMrT1c+PpEeduIKTij6p8lRnT0+I0wY6gIp JnoW75uDduQepVdTe7tr+q++FEH9BvWlMJuMSTWiXPl6Ui2+gg/aRs3xO7gpEGEKq/1wQv dFk/721syuNhOzvZHYwEjNS77/oWi/vMnTYzaWYPsAl16ZikTsEzU0QZ1ygAqU3bzvpuWf bR61eVj+xmxR+gaZIJGlSS0sQqwFCH0Jz2EIw6lOjQm6coP9VL2fm0sAD8ooxQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1789007528; 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=8onZD6K4bXJbtYrDKaAm9rMuVFDYlTrrfMdD1x2mkBM=; b=mWJXY6DvPd9YM4vutnSyp0VOdtFC2IGvnwN9GZKrVhFfhWSB5T44e5jrU5LsZrwmuTc07H nuV/QWpi+ba6ea2nZZU7jEy56h9XGahRFH88UdTrmg8O4K8YLd1gOz5ESWnwWz4v9iyiYs 39Dw9rIbTD8b37JnMgYjeflmSBnC2rRiSJQQSsNJ9zWA99xjmbqP54oKOL9BUXpxE4M3i+ ERFUwGCJ/jgv2Dutpb5zCgc/9nFFDmgXvHX6I06mHVJ7iwTSQsHxvrxrLmEqpRo/VLY20R GsV+dDv508Sz5hVU57J/XujWvfqGJMcU/My2lWuujnUiVyrDRJu1RbNTzrUAsw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hgMBw2N6Wz1MgX for ; Thu, 10 Sep 2026 02:32:08 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 202ac by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Thu, 10 Sep 2026 02:32:08 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org From: Xin LI Subject: git: 9af3236ece36 - stable/14 - MFC: fsck_msdosfs: add tests for lost cluster chain repair accounting List-Id: Commits to the stable branches of the FreeBSD src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-branches List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-branches@freebsd.org Sender: owner-dev-commits-src-branches@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/stable/14 X-Git-Reftype: branch X-Git-Commit: 9af3236ece36e244bb709839b120d7daf6f99113 Auto-Submitted: auto-generated Date: Thu, 10 Sep 2026 02:32:08 +0000 Message-Id: <6aa216a8.202ac.426e2328@gitrepo.freebsd.org> The branch stable/14 has been updated by delphij: URL: https://cgit.FreeBSD.org/src/commit/?id=9af3236ece36e244bb709839b120d7daf6f99113 commit 9af3236ece36e244bb709839b120d7daf6f99113 Author: Xin LI AuthorDate: 2026-09-03 02:15:05 +0000 Commit: Xin LI CommitDate: 2026-09-10 02:30:58 +0000 MFC: fsck_msdosfs: add tests for lost cluster chain repair accounting (cherry picked from commit bbaf254293f7e19fa7b0f9ed1da21c7a43126d79) --- etc/mtree/BSD.tests.dist | 4 +- sbin/fsck_msdosfs/Makefile | 5 + sbin/fsck_msdosfs/tests/Makefile | 5 + sbin/fsck_msdosfs/tests/fsck_msdosfs_test.sh | 261 +++++++++++++++++++++++++++ 4 files changed, 274 insertions(+), 1 deletion(-) diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist index 41944b275fcc..fde3b619b67b 100644 --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -455,9 +455,11 @@ sbin bectl .. + devd + .. dhclient .. - devd + fsck_msdosfs .. growfs .. 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 +}