From nobody Thu Sep 03 07:04:14 2026 X-Original-To: dev-commits-src-all@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 4hb9Z66yHGz6qmG6 for ; Thu, 03 Sep 2026 07:04:14 +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 4hb9Z62LpHz3Mx8 for ; Thu, 03 Sep 2026 07:04:14 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1788419054; 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=hCOHSQ9brLIs3NPYAmDfchE+YSv20A3weP4j3b9E+n8=; b=AUPGlQfILaQADq5NyLMViZtDnKLYs6HCjiGShx4KsUwAQ6iNpp2iDh4YlI0IFRiOBfp+lZ OT1yO/trZxi5r6oWKeSNy8Az6zkRg/1KMcUPK/Ta6Da5MDH09rZ39kEDUD5/owgM0JUE/Y aUjwZoyy87X0tBO6OsIzeJnTGFQjzLu9EAyw39eL4f1J265JIqhDV/lntMyDEcxzg5Yymw v8GO4J7kB1adlL96efNyU0wi3C9N/wXe229r4//pkHxGgPbFLOVvxhY4iv6FfgIUWjsWkx 3o0xD5krwj5dagO5QYNN3HDAoCItt4nwTGmbdy8Wlb7QwAITGEsBmrKOHVCQPA== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1788419054; a=rsa-sha256; cv=none; b=wRHHa8Ov/CG5NgJTKYcwae2GZLdE1+qZJSknr+DD7Jga4C0muULV3DpjfOPxaiQC6hq+dA yS9VhpNPXU1e5WejswLFgdVMuggahsdbX1eLD62mAzYSVwTX77UjpW1vvy7XlISVvmTeuK vhftId+86irH6oc1Qk31twIFT3oSLVJwaCSigrs2yK6F8xQL9/eQNP86hygbJ6SsQvnjgr BgV0SqERYbZDYccDeMaCNL7aaiAC3VUdf40QZ28+42aoBDbtjo2bxWwmlSn/2CMFeZVl03 Z5M/YRA8yu2juIgpEh9wRRT2w4LDUUcXuTssvR4Glwq28lGLKq3YBfuPgEp/6g== 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=1788419054; 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=hCOHSQ9brLIs3NPYAmDfchE+YSv20A3weP4j3b9E+n8=; b=Nrv64A9Yzx2nzl32w82phAjlZPEjMVBBHT83aedBOuFxh2UPEbNlmepzZNP6uQyD7RtS47 17/xAfd/VPxG0y3+Y/QUdw1B6bQTPKt/a1K2a+MlrBeQQ1XlGp4A3nzdnQNm7CfYVLdcaD oJMUxYLyrd5FLaAP+HTCWQhuBqkXPBa9+LlwtrYt6ogRJzBsdbZIG7Q0G7buvqrI5tpA6z jSpjW0f0S6ScjzmSnNmT/qyh91eo+rswSwvNTGtFAjpIv6Ud4GUJyrXO+agRC0Uf4gGTNG YTtHTWkYNLSdpNGlvUllcsY5ph00Ep40K+llt9Kmix9g2r7H86FllW2F2KQhcA== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hb9Z61M92z1HQ2 for ; Thu, 03 Sep 2026 07:04:14 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 1a0ea by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Thu, 03 Sep 2026 07:04:14 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Xin LI Subject: git: c4f458da4411 - main - fsck_msdosfs: add tests for the 32-bit boot block field decoding List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@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: c4f458da4411872df4968e02ca292389df462b7b Auto-Submitted: auto-generated Date: Thu, 03 Sep 2026 07:04:14 +0000 Message-Id: <6a991bee.1a0ea.21077122@gitrepo.freebsd.org> The branch main has been updated by delphij: URL: https://cgit.FreeBSD.org/src/commit/?id=c4f458da4411872df4968e02ca292389df462b7b commit c4f458da4411872df4968e02ca292389df462b7b Author: Xin LI AuthorDate: 2026-09-03 05:23:53 +0000 Commit: Xin LI CommitDate: 2026-09-03 06:51:36 +0000 fsck_msdosfs: add tests for the 32-bit boot block field decoding Exercise each of the 32-bit BIOS Parameter Block and FSInfo fields that readboot() decodes, using values whose most significant byte has its high bit set. Each case checks two things: that fsck_msdosfs(8) reports the full unsigned 32-bit value back on stdout, and that nothing writes a sanitizer runtime error to stderr. The second check is what catches a byte-at-a-time decode. Shifting such a byte left by 24 is undefined, but every compiler we use wraps it into the same bit pattern, so the decoded value alone cannot tell a correct decode from an overflowing one. In a WITH_UBSAN build bsd.sanitizer.mk compiles with -fsanitize=undefined and -fsanitize-recover=undefined, so the shift is reported on stderr and execution continues, which the test can then assert on. Against the byte-at-a-time decode these cases fail in a WITH_UBSAN build and pass without it. Note that the stderr check also fails on unrelated undefined behavior that these images reach anywhere in fsck_msdosfs(8), which is intended. MFC after: 1 week --- sbin/fsck_msdosfs/tests/Makefile | 3 +- sbin/fsck_msdosfs/tests/fsck_msdosfs_boot_test.sh | 214 ++++++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) diff --git a/sbin/fsck_msdosfs/tests/Makefile b/sbin/fsck_msdosfs/tests/Makefile index a36439f91bce..c8963837b7b0 100644 --- a/sbin/fsck_msdosfs/tests/Makefile +++ b/sbin/fsck_msdosfs/tests/Makefile @@ -1,5 +1,6 @@ PACKAGE= tests -ATF_TESTS_SH= fsck_msdosfs_test +ATF_TESTS_SH= fsck_msdosfs_test \ + fsck_msdosfs_boot_test .include diff --git a/sbin/fsck_msdosfs/tests/fsck_msdosfs_boot_test.sh b/sbin/fsck_msdosfs/tests/fsck_msdosfs_boot_test.sh new file mode 100644 index 000000000000..51333982a96b --- /dev/null +++ b/sbin/fsck_msdosfs/tests/fsck_msdosfs_boot_test.sh @@ -0,0 +1,214 @@ +# +# 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 +}