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