mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-11-07 04:36:52 -10:00
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
This commit is contained in:
1 parent
d3c5464fee
commit
37aec55d0a
2 files changed
+243
-1
No files matched your search
@@ -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 <bsd.test.mk>
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user