From nobody Thu Sep 03 07:57:38 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 4hbBlk5tBMz6qr59 for ; Thu, 03 Sep 2026 07:57:38 +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 4hbBlk4y82z3W0S for ; Thu, 03 Sep 2026 07:57:38 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1788422258; 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=CztNaHjVxfsaZW3VC00F1uaN9wiFBLEJzapptKcfao8=; b=yXw+RV5Q0kWU1por1jjyxVyMakWztYYN6hWhixaWsv7d6IiK22pf6uFx1fChH2+X4RBanP XDBK6FP0d12kM2fqpyvRiOSRPwYyibMi6qbj7DPzdquKBIRaLtjXqhd+MHm5L9pIKz2rCv FF8MrlUB6fwtAXgzC7G4t2ElBx6gCfLRsg+ECFVL27N7uy2IdDHT4sUmK2r7oiD7l1O1v0 8m8dCcl4bWA+9t/rM88Kwf9Gtq3fPM5bUrJfgJ4K93EYMn3q/6J4SYWRgMIQeDzToDoqv0 zzfgQeXDm0h3pTvoJpm32w8LAVrSxD9PC7XWwoT0LkmSWpi6KBBzdHZu3sjuVw== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1788422258; a=rsa-sha256; cv=none; b=McAF+JNo4NIiB5IlymXy1VMuBIwlAHtlsjsz5RbeRI4/xNF925xwj5ivjDv9beObbcXm8E 3Hphx8QmsokA1nu2+SiZUu+evjDBWjn2Dx2gzJVaFZhlRbTWsfbOW1dMwqkhNFKnta08Id KPK4jJtwkOXrP4eBW4Uwk4SOijvTwZUYntCwotgn9JYmcJ8HUd5COsT274O5xtZ9dnKOLZ xO+ef/aggAg32sN3uMQ8pMH+ClLaEecHTCNH4DBgIKjfuY8dvahSd4uWGM74xfexDO6jMY dDXWWw1ZfBFjwnLpRC8pbq0D4MuBkzt00xpIcHQzhSZ2YcjJz+23QvwsUlWp5g== 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=1788422258; 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=CztNaHjVxfsaZW3VC00F1uaN9wiFBLEJzapptKcfao8=; b=ceefDpMuFKgw2o1y47+8vUNiJFFbPtli5DqNjSC7JcZwkv41TPamXF4TTqzRKNbKoPrT+p 24bi3c67Ux4KIbR+gBezhLPRgZqAARcxYpl8JAAwUOm1Uqpi3po7lnqJUy+4CbaV+3xdJV 3vt2aWzdFnJOTwjbWt5SzG5Pf8qUTz2xbMAVP7DhAG+smkhRGuotjvoqMihhrIRmbS2W4T aGXsfkEhS2ajj/dzDPWI+v7beatq12wnf16PDmFUNv2Z8ebyxPsAVK4TSWEH4WqMTrWgdy 1N/KM88h/aQ1fN3wTI1ma1rKqazxE3Bk2BQjcMCx8WG1etO6L8QuxcD6QTKzOQ== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hbBlk3bxSz1KLX for ; Thu, 03 Sep 2026 07:57:38 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 22532 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Thu, 03 Sep 2026 07:57:38 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Xin LI Subject: git: 37aec55d0a71 - main - fsck_msdosfs: add a test for reconnecting on volumes larger than 4 GiB 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: 37aec55d0a7165960f686e5277f030ab3d44cf45 Auto-Submitted: auto-generated Date: Thu, 03 Sep 2026 07:57:38 +0000 Message-Id: <6a992872.22532.7d4e309e@gitrepo.freebsd.org> The branch main has been updated by delphij: URL: https://cgit.FreeBSD.org/src/commit/?id=37aec55d0a7165960f686e5277f030ab3d44cf45 commit 37aec55d0a7165960f686e5277f030ab3d44cf45 Author: Xin LI AuthorDate: 2026-09-03 07:33:31 +0000 Commit: Xin LI CommitDate: 2026-09-03 07:56:24 +0000 fsck_msdosfs: add a test for reconnecting on volumes larger than 4 GiB Build a 4.5 GiB FAT32 image whose LOST.DIR cluster sits exactly 4 GiB above the single cluster of a PAYLOAD.BIN, so that truncating the offset of the former to 32 bits yields the offset of the latter, then inject a lost cluster chain and let fsck_msdosfs(8) reconnect it. The test asserts both halves of the bug fixed in the previous commit: that PAYLOAD.BIN's cluster is unchanged, and that a second pass no longer reports the chain as lost, which it only stops doing once the directory entry reaches the real LOST.DIR. newfs_msdos(8) -C only calls ftruncate(2) and nothing outside the reserved area, the FATs and a handful of clusters is ever written, so the image stays sparse and costs about 2 MiB on disk. The geometry is read back out of the BPB rather than assumed, so newfs_msdos(8) stays free to lay the file system out differently; the test fails with a clear message if the volume ever becomes too small to hold a cluster a full 4 GiB beyond the data area. MFC after: 1 week --- sbin/fsck_msdosfs/tests/Makefile | 3 +- sbin/fsck_msdosfs/tests/fsck_msdosfs_large_test.sh | 241 +++++++++++++++++++++ 2 files changed, 243 insertions(+), 1 deletion(-) diff --git a/sbin/fsck_msdosfs/tests/Makefile b/sbin/fsck_msdosfs/tests/Makefile index c8963837b7b0..33b0ae8310e8 100644 --- a/sbin/fsck_msdosfs/tests/Makefile +++ b/sbin/fsck_msdosfs/tests/Makefile @@ -1,6 +1,7 @@ PACKAGE= tests ATF_TESTS_SH= fsck_msdosfs_test \ - fsck_msdosfs_boot_test + fsck_msdosfs_boot_test \ + fsck_msdosfs_large_test .include diff --git a/sbin/fsck_msdosfs/tests/fsck_msdosfs_large_test.sh b/sbin/fsck_msdosfs/tests/fsck_msdosfs_large_test.sh new file mode 100644 index 000000000000..3b96e239b9c1 --- /dev/null +++ b/sbin/fsck_msdosfs/tests/fsck_msdosfs_large_test.sh @@ -0,0 +1,241 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 The FreeBSD Foundation +# + +# Tests for fsck_msdosfs(8) on FAT32 volumes larger than 4 GiB, where a +# cluster's byte offset no longer fits in 32 bits. reconnect() used to +# compute the offset of the LOST.DIR cluster in 32 bit arithmetic, so it +# read, modified and wrote back the cluster 4 GiB below the intended one, +# silently corrupting whatever user data lived there while leaving the lost +# chain unreferenced. + +IMG=fat32.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 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 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 32 bit little-endian value $3 at offset $2 of $1. +poke32() +{ + printf "$(printf '\\%03o\\%03o\\%03o\\%03o' $(($3 & 255)) \ + $((($3 >> 8) & 255)) $((($3 >> 16) & 255)) \ + $((($3 >> 24) & 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 +} + +# Read the file system geometry of $IMG out of its BPB and derive the two +# cluster numbers the tests below use. Everything is taken from the image +# rather than assumed, so newfs_msdos(8) remains free to pick a different +# layout than the one requested. +# +# victimcl is an ordinary data cluster near the start of the volume and +# lostcl is exactly 4 GiB further into the volume, so that truncating the +# offset of lostcl to 32 bits yields the offset of victimcl. +fat32_geom() +{ + local totsec + + bps=$(bpb_read ${IMG} 11 2) + spc=$(bpb_read ${IMG} 13 1) + rsvd=$(bpb_read ${IMG} 14 2) + nfats=$(bpb_read ${IMG} 16 1) + totsec=$(bpb_read ${IMG} 32 4) + fatsz=$(bpb_read ${IMG} 36 4) + rootcl=$(bpb_read ${IMG} 44 4) + + clsz=$((spc * bps)) + fatoff=$((rsvd * bps)) + dataoff=$(((rsvd + nfats * fatsz) * bps)) + numclust=$(((totsec - rsvd - nfats * fatsz) / spc)) + + victimcl=64 + lostcl=$((victimcl + 4294967296 / clsz)) + + if [ "${lostcl}" -ge "${numclust}" ]; then + atf_fail "image holds ${numclust} clusters, need ${lostcl}" + fi +} + +# Print the byte offset of cluster $1. +cloff() +{ + echo $((dataoff + ($1 - 2) * clsz)) +} + +# Set the FAT32 entry for cluster $2 to $3 in every copy of the FAT of $1. +fat_set() +{ + local i + + i=0 + while [ "${i}" -lt "${nfats}" ]; do + poke32 "$1" $((fatoff + i * fatsz * bps + $2 * 4)) "$3" + i=$((i + 1)) + done +} + +# Write the 8.3 directory entry $2 at offset $1 of $IMG, with attribute $3, +# start cluster $4 and size $5. Everything not written here is already zero +# in a freshly created file system, which is what the remaining fields need +# to be. +dirent() +{ + poke_str ${IMG} "$1" "$2" + poke8 ${IMG} $(($1 + 11)) "$3" + poke16 ${IMG} $(($1 + 20)) $(($4 >> 16)) + poke16 ${IMG} $(($1 + 26)) $(($4 & 65535)) + poke32 ${IMG} $(($1 + 28)) "$5" +} + +# Create a 4.5 GiB FAT32 file system in $IMG. newfs_msdos(8) -C only calls +# ftruncate(2), and nothing outside the reserved area, the FATs and a +# handful of clusters is ever written, so the image stays sparse. +# +# The volume has to be large enough that a cluster can sit a full 4 GiB +# beyond an ordinary data cluster, which 4.5 GiB satisfies for every cluster +# size newfs_msdos(8) may choose here. +make_image() +{ + atf_check -s exit:0 -o ignore -e ignore \ + newfs_msdos -C 4608m -F 32 -c 64 -S 512 ./${IMG} + fat32_geom + # A freshly created file system must be clean. + atf_check -s exit:0 -o ignore -e ignore fsck_msdosfs -y ./${IMG} +} + +# Create an empty LOST.DIR in the root directory of $IMG, with cluster +# $lostcl holding its contents, so that reconnect() has somewhere to link a +# lost chain to. That cluster begins more than 4 GiB into the volume. +create_lost_dir() +{ + local root dir + + fat_set ${IMG} ${lostcl} 268435455 + + root=$(cloff ${rootcl}) + dir=$(cloff ${lostcl}) + + # The entry in the root directory. 16 is ATTR_DIRECTORY. + dirent ${root} 'LOST DIR' 16 ${lostcl} 0 + + # Its "." and ".." entries. The remainder of the cluster stays + # zero, which reads as SLOT_EMPTY, so reconnect() has free slots. + dirent ${dir} '. ' 16 ${lostcl} 0 + dirent $((dir + 32)) '.. ' 16 0 0 +} + +# Create PAYLOAD.BIN in the root directory of $IMG, occupying the single +# cluster $victimcl, which is exactly 4 GiB below the LOST.DIR cluster. +# +# The first 32 bytes are left zero on purpose: a truncated offset makes +# reconnect() search this cluster for a free directory slot, and a leading +# NUL reads as SLOT_EMPTY, so the bogus entry lands at a known place. The +# rest carries a marker so the region is recognisable in a corrupted image. +create_payload() +{ + local data + + fat_set ${IMG} ${victimcl} 268435455 + + data=$(cloff ${victimcl}) + poke_str ${IMG} $((data + 32)) 'PAYLOAD.BIN DATA - MUST NOT BE TOUCHED' + + # 32 is ATTR_ARCHIVE. + dirent $(($(cloff ${rootcl}) + 32)) 'PAYLOAD BIN' 32 ${victimcl} ${clsz} +} + +# 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 and reconnect them into LOST.DIR. +inject_lost_chain() +{ + fat_set ${IMG} 300 301 + fat_set ${IMG} 301 302 + fat_set ${IMG} 302 268435455 +} + +# Copy the cluster $1 of $IMG into the file $2. The size is checked so that +# a short read cannot turn the comparison below into a vacuous success. +save_cluster() +{ + dd if=${IMG} of="$2" bs=${bps} skip=$(($(cloff "$1") / bps)) \ + count=${spc} status=none + if [ "$(stat -f %z "$2")" -ne "${clsz}" ]; then + atf_fail "could not read cluster $1 of ${IMG}" + fi +} + +atf_test_case reconnect_above_4g +reconnect_above_4g_head() +{ + atf_set "descr" "Reconnecting into a LOST.DIR past 4 GiB does not corrupt user data" + atf_set "require.progs" "newfs_msdos fsck_msdosfs" +} +reconnect_above_4g_body() +{ + make_image + create_lost_dir + create_payload + + # Adding the entries by hand must not have damaged anything. This + # also brings the free cluster count in the FSInfo block back in + # line with the FAT, so the run below does not have to fix it. + atf_check -s exit:0 -o ignore -e ignore fsck_msdosfs -y ./${IMG} + + inject_lost_chain + save_cluster ${victimcl} victim.before + + # reconnect() has to link the lost chain into the LOST.DIR cluster + # more than 4 GiB into the volume. Computing that offset in 32 bit + # arithmetic instead lands on PAYLOAD.BIN's cluster. + atf_check -s exit:0 \ + -o match:'Lost cluster chain at cluster 300' \ + -o match:'3 Cluster\(s\) lost' \ + -o match:'Reconnect\? yes' \ + -e ignore \ + fsck_msdosfs -y ./${IMG} + + # PAYLOAD.BIN is 4 GiB below LOST.DIR, so a truncated offset + # rewrites its cluster with a directory entry in the first slot. + save_cluster ${victimcl} victim.after + atf_check cmp victim.before victim.after + + # The reconnect has to be durable: the chain is only referenced if + # the entry reached the real LOST.DIR, so a second pass that still + # reports it means the first one wrote somewhere else. + atf_check -s exit:0 -o not-match:'Lost cluster chain' -e ignore \ + fsck_msdosfs -y ./${IMG} +} + +atf_init_test_cases() +{ + atf_add_test_case reconnect_above_4g +}