Skip to content

Add optional read-only FAT32 and ext4 support to the disk loader - #872

Open
dgarske wants to merge 4 commits into
wolfSSL:masterfrom
dgarske:disk_fs
Open

Add optional read-only FAT32 and ext4 support to the disk loader#872
dgarske wants to merge 4 commits into
wolfSSL:masterfrom
dgarske:disk_fs

Conversation

@dgarske

@dgarske dgarske commented Aug 24, 2026

Copy link
Copy Markdown
Member

Description

Adds optional read-only FAT32 and ext4 support to the disk loader, so a boot slot can name a file (BOOT_FILE_A / BOOT_FILE_B) instead of requiring the signed image at raw offset 0 of a partition. Enabled with DISK_FS=fat32|ext4|both; off by default, and raw partitions behave exactly as before. Partitions can be selected by index or by label (GPT partition name, or the FAT32/ext4 volume label, so labels also work on MBR disks).

The file is an ordinary wolfBoot-signed image -- header at offset 0 of the file, payload after it -- so signing, A/B version selection and anti-rollback are unchanged.

Also tightens WOLFBOOT_RAMBOOT_MAX_SIZE in versal_vmk180_sdcard.config from 0x80000000 to 0x70000000. That value bounds what the on-disk header may claim before the payload is authenticated; with WOLFBOOT_LOAD_ADDRESS=0x10000000 the old value allowed a claimed size to reach 0x90000000, past the end of the board's 2 GB DDR-low aperture.

Also in this PR

Big-endian correctness for the whole disk path. src/gpt.c and src/disk.c read the MBR table, the GPT header and the GPT entries by casting the sector to packed structures and dereferencing them, which is only correct on a little-endian host. They now read every on-disk field byte-wise and convert at the parse boundary, so struct guid_ptable and struct gpt_part_info come back in host order and downstream callers are unchanged. The GPT header CRC is now computed over the raw on-disk bytes with the CRC field zeroed, which is both what the spec says and what makes it endian-neutral. This was pre-existing and previously unreachable, since no big-endian target enables disk boot, but the filesystem parsers were already endian-neutral and it would have been the only thing blocking a big-endian disk target.

A host-side test harness, tools/fs-test. It links the real parsers against a file-backed block device, so any disk image or read-only block device can be exercised exactly as the bootloader reads it. make fixtures builds real volumes with mkfs.vfat + mtools and mke2fs -d, needing no root and no loopback mounts. It also dumps files out of an existing filesystem, which is useful for checking media by hand.

A big-endian CI job. .github/workflows/test-endian.yml cross-builds the harness as big-endian PowerPC and reads the same fixtures under qemu-user, so any future change that reintroduces a native multi-byte read of on-disk data fails CI instead of going unnoticed.

Limitations

  • Read-only. FAT32 only (no FAT12/FAT16); ext4 extent-mapped files only (no ext2/ext3 indirect block maps).
  • No symlinks, inline_data, ext4 encryption/compression, or uninitialized extents. Each is refused with a clear error rather than misread.
  • Sparse files are read correctly: a hole reads as zeros, which is ext4's definition of a hole and therefore the file's true content, so the hash still covers exactly what the file contains. This matters in practice because mke2fs -d and cp punch holes wherever the source has a run of zeros, and a signed image's header padding always does.
  • 512-byte logical sectors only, matching the existing GPT/MBR layer.
  • ext4 volumes larger than 16 TiB are refused. 64bit, metadata_csum and flex_bg are supported, so a volume from a plain mkfs.ext4 is readable as-is.
  • A dirty (unreplayed-journal) ext4 volume is refused unless WOLFBOOT_FS_EXT4_ALLOW_DIRTY is set.
  • One signed file per slot. Separately-signed kernel/initrd/dtb files are not supported yet.
  • tegra234 (Orin) is build coverage only; SDMMC1 bring-up is still open.

Testing

  • Unit tests: 160 new checks across six suites, all passing and clean under ASAN=1 (218 checks total across the disk/FS area, including the pre-existing raw-path suites).
    • unit-fs-probe (35): filesystem probing, raw passthrough, metadata cache, path splitting, partition accessors.
    • unit-fat32 (18): interop against a volume from the host's real mkfs.vfat + mcopy.
    • unit-ext4 (17): interop against two mke2fs volumes, one minimal and one with the stock 64bit,metadata_csum,flex_bg feature set.
    • unit-fs-malicious (58): hostile input, with a negative case for every validation rule and every loop bound. Hand-built metadata, no external tools, so it never skips.
    • unit-update-disk-fs (16): loader integration, mocking only the raw block layer so the real gpt/disk/disk_fs/fat32/ext4 stack runs underneath wolfBoot_start(). Covers slot selection across two different filesystems, the raw fallback, partition selection by volume label and by GPT partition name (including the precedence between them), rejection of a file larger than the load region, and the fail-closed defaults.
    • unit-update-disk-fs-enc (16): the same source built with disk encryption. The mock cipher's keystream depends on the byte's position in the image, so it pins the fact that a file's byte stream lines up with the CTR counter exactly as a raw partition's does; shifting the keystream by one byte fails 7 of the 13 tests, so the check is not vacuous.
  • Full suite green: make -C tools/unit-tests run passes, 129 suites at 100%. unit-disk, unit-update-disk, unit-update-disk-oob and unit-update-disk-fit are unmodified and still pass, which is the no-regression proof for the raw path.
  • End-to-end disk image verified: make_hd.sh FS=1 builds a GPT image whose FAT32 and ext4 partitions are produced by the real mkfs.vfat and mke2fs; the parsers mount both, match their volume labels, and read back byte-identical signed images.
  • Build coverage: DISK_FS=both builds clean on ZynqMP, Zynq-7000, Versal VMK180 and tegra234, plus fat32-only and ext4-only variants, and in combination with ENCRYPT_WITH_AES256, ENCRYPT_WITH_CHACHA and FIT_RAMDISK. New CI jobs cover all of these. versal_vmk180_sdcard.config had no CI job at all before this branch; it now has both a raw and a DISK_FS=both job. Cost of the feature on that target is about 17 KB of .text (129760 -> 147320).
  • New QEMU CI job boots the x86 FSP/AHCI target from a file on a filesystem; the existing raw-boot job is untouched.
  • Size: 15,590 bytes of .text for all three objects at -Os (aarch64), well inside the bootloader partition on every target that enables it. A dedicated size/complexity pass removed duplicated node-header validation in the extent walk, merged two near-identical extent accessors, replaced six copies of the FAT cluster-range test with one helper, shared is_pow2 between the backends, and deleted dead code (fs_le64, the write-only inode_no field).
  • cppcheck clean on all new and modified sources, using the repo's own make cppcheck flags verbatim (-f --enable=warning --enable=portability --check-level=exhaustive --std=c89 --error-exitcode=89): exit 0, zero findings.
  • Security review: two five-lens Skoll scans (review-security,bugs,review,defaults,cicd), the second after a size/complexity pass. Latest: 0 critical, 0 high, 0 medium, 2 low, 3 info. Four are fixed; one is declined with evidence (see below).
  • Hardware: validated on six targets, each contributing a different part of the matrix. In every case the same signed image was read from a file and produced the same Boot partition: line as the raw path, then Firmware Valid., then the application banner.
Board Arch / table Signing What it proves
x86 FSP (QEMU, AHCI) x86 / GPT ECC256 / SHA256 p2: fat32 and p3: ext4 in one boot, both volumes from a real mkfs
ZCU102 (ZynqMP) aarch64 / MBR RSA4096 / SHA3-384 FAT32 on p0 and ext4 on p3 (1K blocks, 3739 groups) mounted in one boot; the ext4 slot correctly won on version
PolarFire MPFS250 RISC-V / GPT ECC384 / SHA384 GPT partition-label selection: BOOT_PART_A/B were set to bogus indices 7/8 and resolved to 0 and 2 purely from the labels "boot" and "root". FAT32 (129022 clusters) + ext4 (4K blocks, 238 groups)
ZC702 (Zynq-7000) ARMv7-A / MBR ECC256 / SHA256 The slow-controller case: Arasan SDHCI capped at 6 MHz, so it is the worst case for filesystem metadata reads. p0: fat32 + p1: ext4 (4K blocks) in one boot, ext4 winning on version
Versal VMK180 aarch64 / MBR ECC384 / SHA384 The PIO case -- arch.mk sets -DSDHCI_SDMA_DISABLED on this target, so every filesystem metadata read is programmed I/O. p0: fat32 + p1: ext4, and the ext4 file was sparse (extents (0), (16-49), blocks 1-15 a hole), so this exercised the hole-reads-as-zeros path on real hardware. Reached the app banner and Application running successfully!
NXP QorIQ T1040 RDB PowerPC e5500 (big endian) / MBR n/a (host-run parsers) The only big-endian target available. wolfBoot has no eSDHC block driver for T10xx yet, so the disk stack (gpt.c, disk.c, fat32.c, ext4.c) was run natively on the board's Linux via tools/fs-test built as 32-bit MSB PowerPC. MBR parsed, FAT32 and ext4 both read back byte-identical, negative cases refused. This is what validates the endian fix on real silicon rather than under emulation

Between them these cover both partition-table formats, both label mechanisms (GPT partition name and volume-label fallback), both ext4 block sizes, both SDHCI transfer modes (SDMA and PIO), four signature algorithms and four architectures (x86, aarch64, ARMv7-A, RISC-V).

  • Note on the two in-place-ELF targets (ZCU102 and Versal): the filesystem path validated fully there -- both volumes mounted, image read from file, Firmware Valid. -- but the application did not start until the unrelated ELF entry-point fix in PR elf: read the ELF entry point before loading segments over it #871 was also applied, because those configs load the ELF over its own header. That is a pre-existing master-only regression, not something this branch introduces; with elf: read the ELF entry point before loading segments over it #871 applied the Versal boots straight through to its banner.
  • The ZC702 run also exercised the truncation guard for real: a hand-signed image built with a 256-byte header against a IMAGE_HEADER_SIZE=1024 build was rejected with Image larger than file for P:B rather than being read short. That is the intended behaviour of the fw_size + IMAGE_HEADER_SIZE > file_size check, and it named the real cause instead of surfacing a confusing I/O error.
  • Two real defects were caught by the QEMU run that none of the 212 unit checks did, both now fixed and covered: test_qemu.sh signed with --ecc384 while the config declared SIGN=ECC256, so that new CI job could never have built; and ext4 refused sparse files, which every real signed image is. The unit fixtures had used /dev/urandom, which never compresses to holes, so nothing caught it -- the interop fixture is now deliberately sparse.

@dgarske dgarske self-assigned this Aug 24, 2026
@dgarske
dgarske requested review from danielinux and a lite review from Copilot August 24, 2026 23:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds an optional read-only filesystem layer to the disk boot path so slots can load a wolfBoot-signed image from a file on FAT32 or ext4 (with raw-partition fallback preserved), and expands CI/unit-test coverage for the new paths.

Changes:

  • Add DISK_FS=fat32|ext4|both build option and integrate filesystem-backed slot reading into src/update_disk.c.
  • Introduce read-only filesystem layer (disk_fs.c/.h) plus FAT32/ext4 backends.
  • Add extensive unit/integration tests, update QEMU scripts + workflows, and refresh documentation/config examples.

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tools/unit-tests/unit-update-disk-fs.c New integration tests booting from FAT32/ext4 files (and raw fallback), including encrypted-disk variant.
tools/unit-tests/unit-fs-probe.c New unit tests for backend-agnostic FS probing, cache, raw passthrough, and path splitting.
tools/unit-tests/unit-fat32.c FAT32 interop tests against host-generated images (mkfs.vfat + mtools).
tools/unit-tests/unit-ext4.c ext4 interop tests against host-generated images (mke2fs).
tools/unit-tests/Makefile Wire new test binaries + fixture generation; conditionally include interop tests based on tool availability.
tools/scripts/x86_fsp/qemu/test_qemu.sh Add -f path to test filesystem boot config; adjust keygen/TPM steps accordingly.
tools/scripts/x86_fsp/qemu/make_hd.sh Add FS=1 mode to create FAT32/ext4 partitions populated with signed images for QEMU.
src/update_disk.c Add slot abstraction + filesystem-backed reads; add label-based partition selection; enforce file truncation checks.
src/fat32.c New read-only FAT32 backend with VFAT LFN support and bounded parsing.
src/ext4.c New read-only ext4 backend (extent-mapped only) with bounded parsing and sparse-file hole handling.
src/disk_fs.c New backend-independent filesystem layer: probing, dispatch, metadata cache, bounded reads, path splitting.
src/disk.c Add disk_part_size() and disk_part_count() helpers used by filesystem layer.
options.mk Add DISK_FS build knob to include FS objects/flags only when requested.
include/disk_fs.h Public/internal API for filesystem layer and security bounds configuration.
include/disk.h Export new partition helper APIs.
docs/compile.md Document DISK_FS usage, supported/refused features, and security/performance notes.
docs/Targets.md Update target docs to reflect optional filesystem boot path and link compile docs.
config/examples/zynqmp_sdcard.config Add commented example for DISK_FS + boot file/label options.
config/examples/zynq7000_sdcard.config Add commented example for DISK_FS + boot file/label options.
config/examples/x86_fsp_qemu_fs.config New QEMU config enabling DISK_FS boot from file.
config/examples/versal_vmk180_sdcard.config Tighten WOLFBOOT_RAMBOOT_MAX_SIZE; add DISK_FS example block.
config/examples/tegra234-sdcard.config Add commented DISK_FS example block.
config/examples/polarfire_mpfs250.config Add commented DISK_FS example block.
Makefile Set WOLFBOOT_TARGET_BUILD=1 to scope DISK_FS checks to real target builds.
.gitignore Ignore new unit-test binaries and generated filesystem fixture images.
.github/workflows/test-x86-fsp-qemu.yml Add new QEMU CI job for filesystem boot path.
.github/workflows/test-configs.yml Add build-matrix jobs compiling FAT32/ext4/both variants and new config coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/update_disk.c
Comment thread tools/unit-tests/unit-ext4.c Outdated
Comment thread tools/unit-tests/unit-fat32.c Outdated
@dgarske
dgarske force-pushed the disk_fs branch 2 times, most recently from 3bfb897 to 5b877c7 Compare August 25, 2026 19:24
@dgarske
dgarske requested a lite review from Copilot August 25, 2026 22:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 39 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

tools/unit-tests/unit-disk.c:248

  • This test fixture writes the MBR boot signature via a uint16_t* assignment, which is host-endian. On a big-endian host this writes bytes AA 55 instead of the on-disk little-endian 55 AA, breaking the intent of the new endian-neutral GPT/MBR parsing tests.

Comment thread tools/fs-test/fs-test.c Outdated
Comment thread tools/fs-test/fs-test.c
Comment thread tools/fs-test/fs-test.c Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants