# # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2026 The FreeBSD Foundation # # Tests for the 32 bit BIOS Parameter Block and FSInfo fields decoded by # readboot() in sbin/fsck_msdosfs/boot.c. Each case sets one field to a # value whose most significant byte has its high bit set, which is the # range that a byte-by-byte "b[3] << 24" decode has to shift into the # sign bit of an int. # # Each case makes two assertions: # # 1. On stdout, that fsck_msdosfs(8) reports the full unsigned 32 bit # value back. This covers the decoding itself in an ordinary build. # # 2. On stderr, that nothing reports a runtime error. A byte-by-byte # decode of these values is undefined behavior, but every compiler we # use wraps it into the same bit pattern, so it cannot be caught by # the value alone. In a WITH_UBSAN build it is caught here, because # bsd.sanitizer.mk builds with -fsanitize=undefined and # -fsanitize-recover=undefined, so the shift is reported on stderr and # execution continues. In a build without the sanitizer these # assertions are trivially true. # # Note that assertion 2 also fails on unrelated undefined behavior that # these images reach anywhere in fsck_msdosfs(8), which is intentional. IMG=fat32.img # The high bit of the most significant byte is set in all of these. HIGH=2147483648 # 0x80000000 HIGH1=2147483649 # 0x80000001 # A UBSan report, as produced by a WITH_UBSAN build. It never appears # in a build without the sanitizer. UB='runtime error' # 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 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 } # Byte offset of the FSInfo sector of $IMG. fsinfo_off() { echo $(($(bpb_read ${IMG} 48 2) * $(bpb_read ${IMG} 11 2))) } # Create a 40 MiB FAT32 file system in $IMG. One sector per cluster # keeps it comfortably above the 65525 cluster FAT32 minimum. make_image() { atf_check -s exit:0 -o ignore -e ignore \ newfs_msdos -C 40m -F 32 -c 1 -S 512 ./${IMG} # A freshly created file system must be clean, and must not have # tripped the sanitizer on its way through readboot(). atf_check -s exit:0 -o ignore -e not-match:"${UB}" \ fsck_msdosfs -y ./${IMG} } atf_test_case hidden_secs_high_bit hidden_secs_high_bit_head() { atf_set "descr" "Hidden sector count with the high bit set" atf_set "require.progs" "newfs_msdos fsck_msdosfs" } hidden_secs_high_bit_body() { make_image poke32 ${IMG} 28 ${HIGH} # readboot() decodes bpbHiddenSecs but nothing uses it, so the # only thing to check is that the file system still comes out # clean and that decoding it was well defined. atf_check -s exit:0 -o not-match:'Invalid' -e not-match:"${UB}" \ fsck_msdosfs -n ./${IMG} } atf_test_case fsinfo_free_high_bit fsinfo_free_high_bit_head() { atf_set "descr" "FSInfo free cluster count with the high bit set" atf_set "require.progs" "newfs_msdos fsck_msdosfs" } fsinfo_free_high_bit_body() { make_image poke32 ${IMG} $(($(fsinfo_off) + 0x1e8)) ${HIGH} # The count is far larger than the number of clusters, so it has # to be reported as wrong, with the decoded value spelled out. atf_check -s exit:0 \ -o match:"Free space in FSInfo block \(${HIGH}\) not correct" \ -e not-match:"${UB}" fsck_msdosfs -n ./${IMG} # Once repaired the bogus count must be gone for good. atf_check -s exit:0 -o ignore -e not-match:"${UB}" \ fsck_msdosfs -y ./${IMG} atf_check -s exit:0 -o not-match:'Free space in FSInfo block' \ -e not-match:"${UB}" fsck_msdosfs -n ./${IMG} } atf_test_case fsinfo_next_high_bit fsinfo_next_high_bit_head() { atf_set "descr" "FSInfo next free cluster with the high bit set" atf_set "require.progs" "newfs_msdos fsck_msdosfs" } fsinfo_next_high_bit_body() { make_image poke32 ${IMG} $(($(fsinfo_off) + 0x1ec)) ${HIGH1} atf_check -s exit:0 \ -o match:"Next free cluster in FSInfo block \(${HIGH1}\) invalid" \ -e not-match:"${UB}" fsck_msdosfs -n ./${IMG} atf_check -s exit:0 -o ignore -e not-match:"${UB}" \ fsck_msdosfs -y ./${IMG} atf_check -s exit:0 -o not-match:'Next free cluster in FSInfo block' \ -e not-match:"${UB}" fsck_msdosfs -n ./${IMG} } atf_test_case root_cluster_high_bit root_cluster_high_bit_head() { atf_set "descr" "FAT32 root directory cluster with the high bit set" atf_set "require.progs" "newfs_msdos fsck_msdosfs" } root_cluster_high_bit_body() { make_image poke32 ${IMG} 44 ${HIGH} # Out of range, so readboot() gives up before any phase runs. atf_check -s exit:8 \ -o match:"Root directory starts with cluster out of range\(${HIGH}\)" \ -e not-match:"${UB}" fsck_msdosfs -n ./${IMG} } atf_test_case fatsecs_high_bit fatsecs_high_bit_head() { atf_set "descr" "FAT32 sectors per FAT with the high bit set" atf_set "require.progs" "newfs_msdos fsck_msdosfs" } fatsecs_high_bit_body() { local nfats make_image nfats=$(bpb_read ${IMG} 16 1) poke32 ${IMG} 36 ${HIGH} # ${HIGH} FAT sectors times ${nfats} FATs overflows 32 bits. atf_check -s exit:8 \ -o match:"Invalid FATs\(${nfats}\) with FATsecs\(${HIGH}\)" \ -e not-match:"${UB}" fsck_msdosfs -n ./${IMG} } atf_test_case huge_sectors_high_bit huge_sectors_high_bit_head() { atf_set "descr" "32 bit total sector count with the high bit set" atf_set "require.progs" "newfs_msdos fsck_msdosfs" } huge_sectors_high_bit_body() { local bps spc rsvd nfats fatsz first clusters make_image bps=$(bpb_read ${IMG} 11 2) spc=$(bpb_read ${IMG} 13 1) rsvd=$(bpb_read ${IMG} 14 2) nfats=$(bpb_read ${IMG} 16 1) fatsz=$(bpb_read ${IMG} 36 4) poke32 ${IMG} 32 ${HIGH} # The root directory has no fixed entries on FAT32, so the data # area starts right after the reserved sectors and the FATs. first=$((rsvd + nfats * fatsz)) clusters=$(((HIGH - first) / spc)) # Too many clusters for FAT32; the count in the message is # derived from the decoded sector count. atf_check -s exit:8 \ -o match:"Filesystem too big \(${clusters} clusters\) for FAT32" \ -e not-match:"${UB}" fsck_msdosfs -n ./${IMG} } atf_init_test_cases() { atf_add_test_case hidden_secs_high_bit atf_add_test_case fsinfo_free_high_bit atf_add_test_case fsinfo_next_high_bit atf_add_test_case root_cluster_high_bit atf_add_test_case fatsecs_high_bit atf_add_test_case huge_sectors_high_bit }