xref: /freebsd/sbin/fsck_msdosfs/tests/fsck_msdosfs_boot_test.sh (revision c4f458da4411872df4968e02ca292389df462b7b)
1#
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2026 The FreeBSD Foundation
5#
6
7# Tests for the 32 bit BIOS Parameter Block and FSInfo fields decoded by
8# readboot() in sbin/fsck_msdosfs/boot.c.  Each case sets one field to a
9# value whose most significant byte has its high bit set, which is the
10# range that a byte-by-byte "b[3] << 24" decode has to shift into the
11# sign bit of an int.
12#
13# Each case makes two assertions:
14#
15# 1. On stdout, that fsck_msdosfs(8) reports the full unsigned 32 bit
16#    value back.  This covers the decoding itself in an ordinary build.
17#
18# 2. On stderr, that nothing reports a runtime error.  A byte-by-byte
19#    decode of these values is undefined behavior, but every compiler we
20#    use wraps it into the same bit pattern, so it cannot be caught by
21#    the value alone.  In a WITH_UBSAN build it is caught here, because
22#    bsd.sanitizer.mk builds with -fsanitize=undefined and
23#    -fsanitize-recover=undefined, so the shift is reported on stderr and
24#    execution continues.  In a build without the sanitizer these
25#    assertions are trivially true.
26#
27# Note that assertion 2 also fails on unrelated undefined behavior that
28# these images reach anywhere in fsck_msdosfs(8), which is intentional.
29
30IMG=fat32.img
31
32# The high bit of the most significant byte is set in all of these.
33HIGH=2147483648		# 0x80000000
34HIGH1=2147483649	# 0x80000001
35
36# A UBSan report, as produced by a WITH_UBSAN build.  It never appears
37# in a build without the sanitizer.
38UB='runtime error'
39
40# Read an unsigned little-endian integer of $3 bytes at offset $2 of $1.
41bpb_read()
42{
43	od -An -v -tu1 -j "$2" -N "$3" "$1" | awk '
44	    { for (i = 1; i <= NF; i++) b[n++] = $i }
45	    END { v = 0; for (i = n - 1; i >= 0; i--) v = v * 256 + b[i]
46		  print v }'
47}
48
49# Write the unsigned 32 bit little-endian value $3 at offset $2 of $1.
50poke32()
51{
52	printf "$(printf '\\%03o\\%03o\\%03o\\%03o' $(($3 & 255)) \
53	    $((($3 >> 8) & 255)) $((($3 >> 16) & 255)) \
54	    $((($3 >> 24) & 255)))" |
55	    dd of="$1" bs=1 seek="$2" conv=notrunc status=none
56}
57
58# Byte offset of the FSInfo sector of $IMG.
59fsinfo_off()
60{
61	echo $(($(bpb_read ${IMG} 48 2) * $(bpb_read ${IMG} 11 2)))
62}
63
64# Create a 40 MiB FAT32 file system in $IMG.  One sector per cluster
65# keeps it comfortably above the 65525 cluster FAT32 minimum.
66make_image()
67{
68	atf_check -s exit:0 -o ignore -e ignore \
69	    newfs_msdos -C 40m -F 32 -c 1 -S 512 ./${IMG}
70	# A freshly created file system must be clean, and must not have
71	# tripped the sanitizer on its way through readboot().
72	atf_check -s exit:0 -o ignore -e not-match:"${UB}" \
73	    fsck_msdosfs -y ./${IMG}
74}
75
76atf_test_case hidden_secs_high_bit
77hidden_secs_high_bit_head()
78{
79	atf_set "descr" "Hidden sector count with the high bit set"
80	atf_set "require.progs" "newfs_msdos fsck_msdosfs"
81}
82hidden_secs_high_bit_body()
83{
84	make_image
85	poke32 ${IMG} 28 ${HIGH}
86
87	# readboot() decodes bpbHiddenSecs but nothing uses it, so the
88	# only thing to check is that the file system still comes out
89	# clean and that decoding it was well defined.
90	atf_check -s exit:0 -o not-match:'Invalid' -e not-match:"${UB}" \
91	    fsck_msdosfs -n ./${IMG}
92}
93
94atf_test_case fsinfo_free_high_bit
95fsinfo_free_high_bit_head()
96{
97	atf_set "descr" "FSInfo free cluster count with the high bit set"
98	atf_set "require.progs" "newfs_msdos fsck_msdosfs"
99}
100fsinfo_free_high_bit_body()
101{
102	make_image
103	poke32 ${IMG} $(($(fsinfo_off) + 0x1e8)) ${HIGH}
104
105	# The count is far larger than the number of clusters, so it has
106	# to be reported as wrong, with the decoded value spelled out.
107	atf_check -s exit:0 \
108	    -o match:"Free space in FSInfo block \(${HIGH}\) not correct" \
109	    -e not-match:"${UB}" fsck_msdosfs -n ./${IMG}
110
111	# Once repaired the bogus count must be gone for good.
112	atf_check -s exit:0 -o ignore -e not-match:"${UB}" \
113	    fsck_msdosfs -y ./${IMG}
114	atf_check -s exit:0 -o not-match:'Free space in FSInfo block' \
115	    -e not-match:"${UB}" fsck_msdosfs -n ./${IMG}
116}
117
118atf_test_case fsinfo_next_high_bit
119fsinfo_next_high_bit_head()
120{
121	atf_set "descr" "FSInfo next free cluster with the high bit set"
122	atf_set "require.progs" "newfs_msdos fsck_msdosfs"
123}
124fsinfo_next_high_bit_body()
125{
126	make_image
127	poke32 ${IMG} $(($(fsinfo_off) + 0x1ec)) ${HIGH1}
128
129	atf_check -s exit:0 \
130	    -o match:"Next free cluster in FSInfo block \(${HIGH1}\) invalid" \
131	    -e not-match:"${UB}" fsck_msdosfs -n ./${IMG}
132
133	atf_check -s exit:0 -o ignore -e not-match:"${UB}" \
134	    fsck_msdosfs -y ./${IMG}
135	atf_check -s exit:0 -o not-match:'Next free cluster in FSInfo block' \
136	    -e not-match:"${UB}" fsck_msdosfs -n ./${IMG}
137}
138
139atf_test_case root_cluster_high_bit
140root_cluster_high_bit_head()
141{
142	atf_set "descr" "FAT32 root directory cluster with the high bit set"
143	atf_set "require.progs" "newfs_msdos fsck_msdosfs"
144}
145root_cluster_high_bit_body()
146{
147	make_image
148	poke32 ${IMG} 44 ${HIGH}
149
150	# Out of range, so readboot() gives up before any phase runs.
151	atf_check -s exit:8 \
152	    -o match:"Root directory starts with cluster out of range\(${HIGH}\)" \
153	    -e not-match:"${UB}" fsck_msdosfs -n ./${IMG}
154}
155
156atf_test_case fatsecs_high_bit
157fatsecs_high_bit_head()
158{
159	atf_set "descr" "FAT32 sectors per FAT with the high bit set"
160	atf_set "require.progs" "newfs_msdos fsck_msdosfs"
161}
162fatsecs_high_bit_body()
163{
164	local nfats
165
166	make_image
167	nfats=$(bpb_read ${IMG} 16 1)
168	poke32 ${IMG} 36 ${HIGH}
169
170	# ${HIGH} FAT sectors times ${nfats} FATs overflows 32 bits.
171	atf_check -s exit:8 \
172	    -o match:"Invalid FATs\(${nfats}\) with FATsecs\(${HIGH}\)" \
173	    -e not-match:"${UB}" fsck_msdosfs -n ./${IMG}
174}
175
176atf_test_case huge_sectors_high_bit
177huge_sectors_high_bit_head()
178{
179	atf_set "descr" "32 bit total sector count with the high bit set"
180	atf_set "require.progs" "newfs_msdos fsck_msdosfs"
181}
182huge_sectors_high_bit_body()
183{
184	local bps spc rsvd nfats fatsz first clusters
185
186	make_image
187	bps=$(bpb_read ${IMG} 11 2)
188	spc=$(bpb_read ${IMG} 13 1)
189	rsvd=$(bpb_read ${IMG} 14 2)
190	nfats=$(bpb_read ${IMG} 16 1)
191	fatsz=$(bpb_read ${IMG} 36 4)
192	poke32 ${IMG} 32 ${HIGH}
193
194	# The root directory has no fixed entries on FAT32, so the data
195	# area starts right after the reserved sectors and the FATs.
196	first=$((rsvd + nfats * fatsz))
197	clusters=$(((HIGH - first) / spc))
198
199	# Too many clusters for FAT32; the count in the message is
200	# derived from the decoded sector count.
201	atf_check -s exit:8 \
202	    -o match:"Filesystem too big \(${clusters} clusters\) for FAT32" \
203	    -e not-match:"${UB}" fsck_msdosfs -n ./${IMG}
204}
205
206atf_init_test_cases()
207{
208	atf_add_test_case hidden_secs_high_bit
209	atf_add_test_case fsinfo_free_high_bit
210	atf_add_test_case fsinfo_next_high_bit
211	atf_add_test_case root_cluster_high_bit
212	atf_add_test_case fatsecs_high_bit
213	atf_add_test_case huge_sectors_high_bit
214}
215