disk_fs: add read-only FAT32 filesystem support for disk boot

pull/884/head
David Garske 2026-08-22 09:39:49 -07:00 committed by Daniele Lacamera
parent 336ee7fd56
commit f06f081a20
11 changed files with 4083 additions and 1 deletions

7
.gitignore vendored
View File

@ -144,6 +144,13 @@ tools/unit-tests/unit-aes256
tools/unit-tests/unit-chacha20
tools/unit-tests/unit-delta
tools/unit-tests/unit-disk
tools/unit-tests/unit-fs-probe
tools/unit-tests/unit-fat32
tools/unit-tests/unit-fs-malicious
tools/unit-tests/fat32-test.img
tools/unit-tests/fat32-payload.bin
tools/unit-tests/fat32-nested.bin
tools/unit-tests/fat32-short.bin
tools/unit-tests/unit-enc-nvm
tools/unit-tests/unit-enc-nvm-flagshome
tools/unit-tests/unit-extflash

View File

@ -70,6 +70,8 @@ int disk_open(int drv);
int disk_part_read(int drv, int part, uint64_t off, uint64_t sz, uint8_t *buf);
int disk_part_write(int drv, int part, uint64_t off, uint64_t sz, const uint8_t *buf);
int disk_find_partition_by_label(int drv, const char *label);
int disk_part_size(int drv, int part, uint64_t *size);
int disk_part_count(int drv);
#endif /* _WOLFBOOT_DISK_H */

238
include/disk_fs.h 100644
View File

@ -0,0 +1,238 @@
/* disk_fs.h
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
/* Read-only filesystem layer for wolfBoot disk boot.
*
* Sits between the partition layer (disk_part_read(), src/disk.c) and the
* disk loader (src/update_disk.c), so a boot slot can name a file instead
* of requiring the signed image at raw offset 0 of a partition.
*
* Supported: FAT32 (with VFAT long filenames) and ext4 (extent-mapped
* files only). A partition holding neither is presented as a single file
* spanning the whole partition (FS_TYPE_RAW), which is exactly the
* pre-existing behaviour.
*
* SECURITY: every byte this layer parses is attacker-controlled and is
* read BEFORE the image signature is verified. All metadata is validated
* before use, and every loop that follows on-media links is bounded. See
* the WOLFBOOT_FS_MAX_* knobs below.
*
* Compile with DISK_FS=fat32|ext4|both.
*/
#ifndef WOLFBOOT_DISK_FS_H
#define WOLFBOOT_DISK_FS_H
#include <stdint.h>
#include "gpt.h"
/* Per-filesystem on-disk layout and backend interfaces. Only the parsers
* selected by DISK_FS are compiled, so each is included on its own. */
#ifdef WOLFBOOT_FAT32
#include "fat32.h"
#endif
#ifdef WOLFBOOT_EXT4
#include "ext4.h"
#endif
/* error codes */
#define WOLFBOOT_FS_OK 0
#define WOLFBOOT_FS_E_PARAM -1 /* bad argument / path too long */
#define WOLFBOOT_FS_E_IO -2 /* disk read failed or came up short */
#define WOLFBOOT_FS_E_NOFS -3 /* nothing recognizable: use raw */
#define WOLFBOOT_FS_E_FORMAT -4 /* metadata failed validation */
#define WOLFBOOT_FS_E_UNSUPPORTED -5 /* valid filesystem, feature refused */
#define WOLFBOOT_FS_E_NOENT -6 /* path component not found */
#define WOLFBOOT_FS_E_NOTFILE -7 /* found, but not a regular file */
#define WOLFBOOT_FS_E_TOOBIG -8 /* size exceeds the caller's max */
#define WOLFBOOT_FS_E_CORRUPT -9 /* a security bound tripped */
#define WOLFBOOT_FS_E_RANGE -10 /* offset arithmetic out of range */
/* filesystem types */
#define FS_TYPE_NONE 0
#define FS_TYPE_RAW 1
#define FS_TYPE_FAT32 2
#define FS_TYPE_EXT4 3
/* Metadata cache size. Holds ONLY metadata (FAT entries, directory
* sectors, superblocks, group descriptors, inodes, extent nodes); bulk file
* data streams straight to the caller's buffer. Raising it cuts disk reads
* on a heavily fragmented file. */
#ifndef WOLFBOOT_FS_CACHE_SIZE
#define WOLFBOOT_FS_CACHE_SIZE 512
#endif
#if (WOLFBOOT_FS_CACHE_SIZE < 512) || ((WOLFBOOT_FS_CACHE_SIZE % 512) != 0)
#error "WOLFBOOT_FS_CACHE_SIZE must be a multiple of 512, minimum 512"
#endif
/* Security bounds. Each of these exists to make an on-media structure that
* can reference other on-media structures terminate in bounded time even
* when it is crafted. Each has a dedicated negative unit test. */
/* Longest absolute path accepted by fs_open(). */
#ifndef WOLFBOOT_FS_MAX_PATH
#define WOLFBOOT_FS_MAX_PATH 128
#endif
/* Maximum number of '/'-separated components in a path. */
#ifndef WOLFBOOT_FS_MAX_PATH_DEPTH
#define WOLFBOOT_FS_MAX_PATH_DEPTH 8
#endif
/* Longest single filename accepted. Names longer than this can never match
* a configured path component, so they are skipped rather than assembled. */
#ifndef WOLFBOOT_FS_MAX_NAME
#define WOLFBOOT_FS_MAX_NAME 64
#endif
/* Maximum directory entries scanned before declaring the directory bad. */
#ifndef WOLFBOOT_FS_MAX_DIR_ENTRIES
#define WOLFBOOT_FS_MAX_DIR_ENTRIES 4096
#endif
/* Maximum blocks/clusters walked while scanning one directory. */
#ifndef WOLFBOOT_FS_MAX_DIR_BLOCKS
#define WOLFBOOT_FS_MAX_DIR_BLOCKS 1024
#endif
/* Absolute stop for a FAT cluster chain walk, independent of file size and
* of volume geometry. */
#ifndef WOLFBOOT_FS_MAX_CHAIN
#define WOLFBOOT_FS_MAX_CHAIN 1048576
#endif
/* Maximum ext4 extent tree depth. This is ext4's own architectural limit;
* the walk additionally requires depth to decrease by exactly one at every
* level, which is what makes the descent provably finite. */
#ifndef WOLFBOOT_FS_EXT4_MAX_DEPTH
#define WOLFBOOT_FS_EXT4_MAX_DEPTH 5
#endif
/* A mounted partition. FS_TYPE_RAW is the identity filesystem: the whole
* partition is one file and fs_read() is a direct disk_part_read(), which is
* how a DISK_FS build stays byte-identical to one without it on a partition
* holding no filesystem. */
struct fs_volume {
uint64_t part_sz; /* partition size in bytes; bounds all arithmetic */
int drv;
int part;
int type; /* FS_TYPE_* */
union {
#ifdef WOLFBOOT_FAT32
struct fat32_volume fat;
#endif
#ifdef WOLFBOOT_EXT4
struct ext4_volume ext;
#endif
/* Keeps the union non-empty for a RAW-only build. */
uint8_t raw;
} u;
};
/* An open file. The forward-only cursor makes a sequential load one linear
* metadata walk rather than one walk per read. */
struct fs_file {
struct fs_volume *vol;
uint64_t size; /* validated <= partition size and <= max_size */
uint64_t cur_off; /* file offset at which the cursor position begins */
uint32_t first_clus; /* FAT32: first cluster of the file */
uint32_t cur_clus; /* FAT32: cluster covering cur_off */
uint8_t iblock[60]; /* ext4: raw i_block[], the extent tree root node */
};
/**
* @brief Probe a partition and populate a volume descriptor.
*
* Order: wolfBoot magic at offset 0 (short-circuits to RAW), FAT32, ext4,
* else RAW. An UNRECOGNIZED partition is not an error - it becomes RAW, so
* raw boot behaviour is preserved. A partition that IS a filesystem we
* refuse (FAT12/16, ext2/3, an unsupported ext4 feature) returns
* WOLFBOOT_FS_E_UNSUPPORTED. The distinction is deliberate: falling back to
* raw on a real filesystem would parse a boot sector as an image header.
*/
int fs_mount(struct fs_volume *vol, int drv, int part);
/**
* @brief Resolve an absolute path to a regular file. RAW ignores the path
* and opens the whole partition.
*
* @param max_size Upper bound on file size. The filesystem-reported size is
* attacker-controlled and is checked against this before it
* drives any I/O.
*/
int fs_open(struct fs_volume *vol, struct fs_file *f, const char *path,
uint64_t max_size);
/* Read from an open file. Matches disk_part_read()'s contract - returns
* bytes read (may be short at EOF), or negative WOLFBOOT_FS_E_* - so callers
* can switch between a file and a raw partition unchanged. */
int fs_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf);
/* Size in bytes of an open file. */
uint64_t fs_size(const struct fs_file *f);
/* Human-readable volume type, for boot logs. */
const char *fs_type_name(const struct fs_volume *vol);
/* Compare a volume's label (FAT32 BS_VolLab / ext4 s_volume_name) against
* an ASCII string, so a partition can be selected by name on MBR disks where
* GPT names do not exist. Case-insensitive for FAT32, case-sensitive for
* ext4, matching each filesystem's convention.
* Returns 1 match, 0 no match, negative WOLFBOOT_FS_E_* on error. */
int fs_label_eq(struct fs_volume *vol, const char *label);
/* ---------------------------------------------------------------------
* Internal API. Shared between src/disk_fs.c and the filesystem backends
* (src/fat32.c, src/ext4.c). Not intended for use outside this layer.
* --------------------------------------------------------------------- */
/* Byte-wise little-endian accessors. Used in preference to casting packed
* structs onto a sector buffer, which is only correct on little-endian
* targets and trips -Wextra on packed-member address-taking. */
int fs_is_pow2(uint32_t v);
/* Deliberately out-of-line, unlike the inline readers in gpt.h. The parsers
* call these about fifty times between them, and inlining every site costs
* more code than the calls do. gpt.c and disk.c have only a handful of sites
* each, so the inline form is the right trade there. */
uint16_t fs_le16(const uint8_t *p);
uint32_t fs_le32(const uint8_t *p);
/* Read exactly len bytes at a partition-relative offset, or fail.
* disk_part_read() silently CLAMPS to the partition end and returns the
* clamped count; treating that as success would turn a truncation into a
* silent wrong read, so every read in this layer goes through here. */
int fs_disk_read_exact(struct fs_volume *v, uint64_t off, uint32_t len,
uint8_t *buf);
/* Read metadata through the shared cache, handling straddled windows so
* callers never deal with alignment. Bulk file data must NOT come through
* here: it would evict the metadata window and gain nothing. */
int fs_meta_read(struct fs_volume *v, uint64_t off, uint32_t len,
uint8_t *buf);
/* Drop the metadata cache. Called on mount, and whenever the underlying
* media may have changed. */
void fs_cache_invalidate(void);
/* Split the next component off an absolute path: advances *p, writes the
* component NUL-terminated to out (WOLFBOOT_FS_MAX_NAME + 1 bytes), bumps
* *depth. Rejects "." and ".." outright - neither is needed to name a boot
* image, and ".." is the only way a crafted tree could build a traversal
* cycle. Returns 1 for a component, 0 at end of path, negative on error. */
int fs_path_next(const char **p, char *out, uint32_t *out_len, int *depth);
#endif /* WOLFBOOT_DISK_FS_H */

126
include/fat32.h 100644
View File

@ -0,0 +1,126 @@
/* fat32.h
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
/* On-disk FAT32 layout constants and the FAT32 backend interface.
*/
#ifndef WOLFBOOT_FAT32_H
#define WOLFBOOT_FAT32_H
#include <stdint.h>
/* The partition layer works in 512-byte sectors throughout (GPT_SECTOR_SIZE
* in include/gpt.h, and the lba*512 arithmetic in src/disk.c), so a volume
* with any other logical sector size is refused rather than misread. */
#define FAT_SECTOR_SIZE 512U
/* BIOS Parameter Block field offsets within the boot sector */
#define FAT_BPB_SIZE 64U
#define FAT_BPB_MEDIA 0x15U
#define FAT_BPB_BYTS_PER_SEC 0x0BU
#define FAT_BPB_SEC_PER_CLUS 0x0DU
#define FAT_BPB_RSVD_SEC_CNT 0x0EU
#define FAT_BPB_NUM_FATS 0x10U
#define FAT_BPB_ROOT_ENT_CNT 0x11U
#define FAT_BPB_TOT_SEC16 0x13U
#define FAT_BPB_FAT_SZ16 0x16U
#define FAT_BPB_TOT_SEC32 0x20U
#define FAT_BPB_FAT_SZ32 0x24U
#define FAT_BPB_EXT_FLAGS 0x28U
#define FAT_BPB_FS_VER 0x2AU
#define FAT_BPB_ROOT_CLUS 0x2CU
#define FAT_BS_VOL_LAB 0x47U
#define FAT_BS_VOL_LAB_LEN 11U
#define FAT_BOOT_SIG_OFF 0x1FEU
#define FAT_BOOT_SIG_VALUE 0xAA55U
/* BPB_ExtFlags: bit 7 clear means all FATs are mirrored, and the low
* nibble selects the active FAT when it is set. */
#define FAT_EXT_NO_MIRROR 0x0080U
#define FAT_EXT_ACTIVE_MASK 0x000FU
/* FAT entry values, after masking off the reserved high nibble */
#define FAT_CLUS_MASK 0x0FFFFFFFU
#define FAT_CLUS_MAX 0x0FFFFFF5U /* highest usable cluster number */
#define FAT_CLUS_BAD 0x0FFFFFF7U
#define FAT_CLUS_EOC_MIN 0x0FFFFFF8U
/* A volume with fewer data clusters than this is FAT16 or FAT12 by
* definition, whatever its BPB claims. */
#define FAT_MIN_CLUSTERS 65525U
#define FAT_MAX_CLUS_SIZE 65536U
/* Directory entry layout */
#define FAT_DIR_ENTRY_SIZE 32U
#define FAT_DIR_ATTR 11U
#define FAT_DIR_FST_CLUS_HI 20U
#define FAT_DIR_FST_CLUS_LO 26U
#define FAT_DIR_FILE_SIZE 28U
#define FAT_DIR_FREE 0xE5U
#define FAT_DIR_END 0x00U
#define FAT_DIR_KANJI_E5 0x05U
#define FAT_ATTR_VOLUME_ID 0x08U
#define FAT_ATTR_DIRECTORY 0x10U
#define FAT_ATTR_LFN 0x0FU
#define FAT_ATTR_LFN_MASK 0x3FU
/* Long filename entries */
#define FAT_LFN_LAST 0x40U
#define FAT_LFN_ORD_MASK 0x3FU
#define FAT_LFN_MAX_ORD 20U /* 20 * 13 == 260, the FAT maximum */
#define FAT_LFN_CHARS 13U
#define FAT_LFN_CKSUM_OFF 13U
/* An 8.3 short name occupies 11 bytes ON DISK: 8 name + 3 extension, space
* padded, with no stored '.'. FAT_SFN_MAX is the different, larger number:
* the length of the same name once rendered as "NAME.EXT" for comparison. */
#define FAT_SFN_NAME_LEN 8U
#define FAT_SFN_EXT_LEN 3U
#define FAT_SFN_LEN (FAT_SFN_NAME_LEN + FAT_SFN_EXT_LEN) /* 11 */
#define FAT_SFN_MAX (FAT_SFN_LEN + 1U) /* 12 */
/* FAT32 geometry, validated at mount. Offsets are partition-relative so
* every read inherits disk_part_read()'s bounds checking. */
struct fat32_volume {
uint64_t fat_off; /* byte offset of FAT #0 within the partition */
uint64_t data_off; /* byte offset of cluster 2 */
uint64_t fat_sz; /* bytes in one FAT */
uint32_t clus_sz; /* bytes per cluster */
uint32_t clus_count; /* data clusters; highest valid is clus_count + 1 */
uint32_t root_clus; /* first cluster of the root directory */
};
struct fs_volume;
struct fs_file;
/* Backend entry points. fat32_mount() returns WOLFBOOT_FS_E_NOFS when the
* partition simply is not FAT (so probing continues), and
* WOLFBOOT_FS_E_UNSUPPORTED when it IS FAT but uses something this
* implementation refuses to read, such as FAT12 or FAT16. */
int fat32_mount(struct fs_volume *vol);
int fat32_open(struct fs_volume *vol, struct fs_file *f, const char *path,
uint64_t max_size);
int fat32_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf);
int fat32_label_eq(struct fs_volume *vol, const char *label);
#endif /* WOLFBOOT_FAT32_H */

View File

@ -436,4 +436,49 @@ int disk_find_partition_by_label(int drv, const char *label)
return -1;
}
/**
* @brief Get the size in bytes of a disk partition.
*
* The partition table stores the first and last valid byte offsets, so the
* size is the inclusive difference plus one. Used by the read-only
* filesystem layer to bound every offset it computes from on-media
* metadata.
*
* @param[in] drv The drive number (0 to `MAX_DISKS - 1`).
* @param[in] part The partition number (0 to `MAX_PARTITIONS - 1`).
* @param[out] size The partition size in bytes.
*
* @return 0 on success, or -1 if the partition is not accessible.
*/
int disk_part_size(int drv, int part, uint64_t *size)
{
struct disk_partition *p = open_part(drv, part);
if ((p == NULL) || (size == NULL)) {
return -1;
}
if (p->end < p->start) {
return -1;
}
*size = (p->end - p->start) + 1;
return 0;
}
/**
* @brief Get the number of partitions found on a drive.
*
* @param[in] drv The drive number (0 to `MAX_DISKS - 1`).
*
* @return The number of partitions, or -1 if the drive is not open.
*/
int disk_part_count(int drv)
{
if ((drv < 0) || (drv >= MAX_DISKS)) {
return -1;
}
if (Drives[drv].is_open == 0) {
return -1;
}
return Drives[drv].n_parts;
}
#endif /* _WOLFBOOT_DISK_C_ */

465
src/disk_fs.c 100644
View File

@ -0,0 +1,465 @@
/* disk_fs.c
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
/* Read-only filesystem layer for wolfBoot disk boot.
*
* Holds what is common to every backend: probe order, type dispatch, the
* RAW (no filesystem) backend, the shared metadata cache, the little-endian
* accessors and the path splitter. The parsers live in src/fat32.c and
* src/ext4.c.
*
* Dispatch is a switch on vol->type, not a function-pointer table: taking
* the address of a backend entry point defeats --gc-sections, so an
* ext4-only build would still link the FAT32 code. With a switch the
* unselected arm is preprocessed away and the linker drops it.
*/
#ifndef _WOLFBOOT_DISK_FS_C_
#define _WOLFBOOT_DISK_FS_C_
#ifdef WOLFBOOT_DISK_FS
#include <stdint.h>
#include <string.h>
#include "disk_fs.h"
#include "disk.h"
#include "printf.h"
#include "wolfboot/wolfboot.h"
#ifdef DEBUG_FS
#define FS_DBG(_f_, ...) wolfBoot_printf(_f_, ##__VA_ARGS__)
#else
#define FS_DBG(_f_, ...) do{}while(0)
#endif
/* wolfBoot image header magic as it appears on media. Compared byte-wise,
* not as a word: WOLFBOOT_MAGIC has the opposite byte order on big-endian
* builds, so a word compare would only be right on one of them. */
static const uint8_t fs_wolfboot_magic[4] = { 'W', 'O', 'L', 'F' };
/* ---------------------------------------------------------------------
* Metadata cache: one statically allocated window (several disk targets
* build with WOLFBOOT_SMALL_STACK=1, so the stack is not an option). Keyed
* by drive and partition as well as offset - slot A and slot B can be
* independent partitions, and an offset-only key would hand slot B's reads
* slot A's cached bytes.
* --------------------------------------------------------------------- */
static uint8_t fs_cache[WOLFBOOT_FS_CACHE_SIZE] XALIGNED(4);
static uint64_t fs_cache_off;
static uint32_t fs_cache_len;
static int fs_cache_drv;
static int fs_cache_part;
static int fs_cache_valid;
void fs_cache_invalidate(void)
{
fs_cache_valid = 0;
fs_cache_off = 0;
fs_cache_len = 0;
fs_cache_drv = -1;
fs_cache_part = -1;
}
/* ---------------------------------------------------------------------
* Little-endian accessors
* --------------------------------------------------------------------- */
int fs_is_pow2(uint32_t v)
{
return ((v != 0U) && ((v & (v - 1U)) == 0U)) ? 1 : 0;
}
/* ---------------------------------------------------------------------
* Bounded disk access
* --------------------------------------------------------------------- */
uint16_t fs_le16(const uint8_t *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
uint32_t fs_le32(const uint8_t *p)
{
return ((uint32_t)p[0]) |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24);
}
int fs_disk_read_exact(struct fs_volume *v, uint64_t off, uint32_t len,
uint8_t *buf)
{
int ret;
if ((v == NULL) || (buf == NULL)) {
return WOLFBOOT_FS_E_PARAM;
}
if (len == 0U) {
return WOLFBOOT_FS_OK;
}
/* Bound against the partition before issuing the read, so an offset
* derived from crafted metadata is rejected here rather than being
* quietly truncated by disk_part_read(). */
if ((off > v->part_sz) || ((uint64_t)len > (v->part_sz - off))) {
FS_DBG("fs: read out of range off %x_%x len %u part %x_%x\r\n",
(uint32_t)(off >> 32), (uint32_t)off, len,
(uint32_t)(v->part_sz >> 32), (uint32_t)v->part_sz);
return WOLFBOOT_FS_E_RANGE;
}
ret = disk_part_read(v->drv, v->part, off, (uint64_t)len, buf);
if (ret < 0) {
return WOLFBOOT_FS_E_IO;
}
/* disk_part_read() clamps to the partition end and reports the clamped
* count. Anything short of what was asked for is an I/O error here. */
if ((uint32_t)ret != len) {
FS_DBG("fs: short read %d of %u\r\n", ret, len);
return WOLFBOOT_FS_E_IO;
}
return WOLFBOOT_FS_OK;
}
int fs_meta_read(struct fs_volume *v, uint64_t off, uint32_t len,
uint8_t *buf)
{
uint64_t pos, win, avail;
uint32_t in_win, chunk, rdlen, done;
int ret;
if ((v == NULL) || (buf == NULL)) {
return WOLFBOOT_FS_E_PARAM;
}
if (len == 0U) {
return WOLFBOOT_FS_OK;
}
if ((off > v->part_sz) || ((uint64_t)len > (v->part_sz - off))) {
return WOLFBOOT_FS_E_RANGE;
}
done = 0U;
while (done < len) {
pos = off + (uint64_t)done;
win = pos - (pos % (uint64_t)WOLFBOOT_FS_CACHE_SIZE);
if ((fs_cache_valid == 0) || (fs_cache_off != win) ||
(fs_cache_drv != v->drv) || (fs_cache_part != v->part)) {
avail = v->part_sz - win;
rdlen = WOLFBOOT_FS_CACHE_SIZE;
if ((uint64_t)rdlen > avail) {
rdlen = (uint32_t)avail;
}
fs_cache_valid = 0;
ret = fs_disk_read_exact(v, win, rdlen, fs_cache);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
fs_cache_off = win;
fs_cache_len = rdlen;
fs_cache_drv = v->drv;
fs_cache_part = v->part;
fs_cache_valid = 1;
}
in_win = (uint32_t)(pos - win);
if (in_win >= fs_cache_len) {
return WOLFBOOT_FS_E_RANGE;
}
chunk = fs_cache_len - in_win;
if (chunk > (len - done)) {
chunk = len - done;
}
memcpy(buf + done, fs_cache + in_win, chunk);
done += chunk;
}
return WOLFBOOT_FS_OK;
}
/* ---------------------------------------------------------------------
* Path handling
* --------------------------------------------------------------------- */
int fs_path_next(const char **p, char *out, uint32_t *out_len, int *depth)
{
const char *s;
uint32_t n;
if ((p == NULL) || (*p == NULL) || (out == NULL) || (out_len == NULL) ||
(depth == NULL)) {
return WOLFBOOT_FS_E_PARAM;
}
s = *p;
/* Skip separators. Repeated slashes are harmless and are collapsed. */
while (*s == '/') {
s++;
}
if (*s == '\0') {
*p = s;
return 0;
}
if (*depth >= WOLFBOOT_FS_MAX_PATH_DEPTH) {
FS_DBG("fs: path too deep\r\n");
return WOLFBOOT_FS_E_PARAM;
}
n = 0U;
while ((*s != '/') && (*s != '\0')) {
/* A backslash is an ordinary filename character on both FAT and
* ext4. Treating it as a separator here would let a crafted name
* be read as two components. */
if (n >= WOLFBOOT_FS_MAX_NAME) {
FS_DBG("fs: path component too long\r\n");
return WOLFBOOT_FS_E_PARAM;
}
out[n] = *s;
n++;
s++;
}
out[n] = '\0';
/* "." and ".." are never needed to name a boot image, and ".." is the
* only way a crafted directory tree could build a traversal cycle. */
if ((n == 1U) && (out[0] == '.')) {
return WOLFBOOT_FS_E_PARAM;
}
if ((n == 2U) && (out[0] == '.') && (out[1] == '.')) {
return WOLFBOOT_FS_E_PARAM;
}
*out_len = n;
*p = s;
(*depth)++;
return 1;
}
/* ---------------------------------------------------------------------
* Public API
* --------------------------------------------------------------------- */
int fs_mount(struct fs_volume *vol, int drv, int part)
{
uint8_t magic[4];
uint64_t sz = 0;
int ret;
if (vol == NULL) {
return WOLFBOOT_FS_E_PARAM;
}
memset(vol, 0, sizeof(*vol));
vol->drv = drv;
vol->part = part;
vol->type = FS_TYPE_NONE;
if (disk_part_size(drv, part, &sz) != 0) {
return WOLFBOOT_FS_E_IO;
}
if (sz == 0U) {
return WOLFBOOT_FS_E_IO;
}
vol->part_sz = sz;
fs_cache_invalidate();
/* A partition that starts with a wolfBoot image header is a raw slot.
* Short-circuiting here means an existing raw layout is never handed
* to a filesystem probe at all. */
if (sz >= 4U) {
ret = fs_meta_read(vol, 0, 4, magic);
if ((ret == WOLFBOOT_FS_OK) &&
(memcmp(magic, fs_wolfboot_magic, 4) == 0)) {
vol->type = FS_TYPE_RAW;
FS_DBG("fs: drv %d part %d: wolfBoot image at offset 0\r\n",
drv, part);
return WOLFBOOT_FS_OK;
}
}
#ifdef WOLFBOOT_FAT32
ret = fat32_mount(vol);
if (ret == WOLFBOOT_FS_OK) {
vol->type = FS_TYPE_FAT32;
return WOLFBOOT_FS_OK;
}
/* Anything other than "this is not FAT" stops the probe. A volume we
* recognize but refuse must not fall through to raw, where its boot
* sector would be parsed as an image header. */
if (ret != WOLFBOOT_FS_E_NOFS) {
return ret;
}
#endif
#ifdef WOLFBOOT_EXT4
ret = ext4_mount(vol);
if (ret == WOLFBOOT_FS_OK) {
vol->type = FS_TYPE_EXT4;
return WOLFBOOT_FS_OK;
}
if (ret != WOLFBOOT_FS_E_NOFS) {
return ret;
}
#endif
vol->type = FS_TYPE_RAW;
FS_DBG("fs: drv %d part %d: no filesystem, using raw\r\n", drv, part);
return WOLFBOOT_FS_OK;
}
int fs_open(struct fs_volume *vol, struct fs_file *f, const char *path,
uint64_t max_size)
{
#if defined(WOLFBOOT_FAT32) || defined(WOLFBOOT_EXT4)
size_t plen;
#endif
if ((vol == NULL) || (f == NULL)) {
return WOLFBOOT_FS_E_PARAM;
}
memset(f, 0, sizeof(*f));
f->vol = vol;
if (vol->type == FS_TYPE_RAW) {
/* The whole partition is the file. No max_size check here: the raw
* path has always read a header first and bounded the payload from
* it, and changing that would alter existing behaviour. */
(void)path;
(void)max_size;
f->size = vol->part_sz;
return WOLFBOOT_FS_OK;
}
#if defined(WOLFBOOT_FAT32) || defined(WOLFBOOT_EXT4)
if (path == NULL) {
return WOLFBOOT_FS_E_PARAM;
}
if (path[0] != '/') {
FS_DBG("fs: path must be absolute\r\n");
return WOLFBOOT_FS_E_PARAM;
}
plen = strlen(path);
if (plen > (size_t)WOLFBOOT_FS_MAX_PATH) {
FS_DBG("fs: path too long\r\n");
return WOLFBOOT_FS_E_PARAM;
}
#endif
#ifdef WOLFBOOT_FAT32
if (vol->type == FS_TYPE_FAT32) {
return fat32_open(vol, f, path, max_size);
}
#endif
#ifdef WOLFBOOT_EXT4
if (vol->type == FS_TYPE_EXT4) {
return ext4_open(vol, f, path, max_size);
}
#endif
return WOLFBOOT_FS_E_PARAM;
}
int fs_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf)
{
int ret;
if ((f == NULL) || (f->vol == NULL) || (buf == NULL)) {
return WOLFBOOT_FS_E_PARAM;
}
if (len == 0U) {
return 0;
}
if (off >= f->size) {
return 0;
}
if (len > (f->size - off)) {
len = f->size - off;
}
if (len > (uint64_t)DISK_IO_MAX_SIZE) {
len = (uint64_t)DISK_IO_MAX_SIZE;
}
switch (f->vol->type) {
case FS_TYPE_RAW:
ret = disk_part_read(f->vol->drv, f->vol->part, off, len, buf);
break;
#ifdef WOLFBOOT_FAT32
case FS_TYPE_FAT32:
ret = fat32_read(f, off, len, buf);
break;
#endif
#ifdef WOLFBOOT_EXT4
case FS_TYPE_EXT4:
ret = ext4_read(f, off, len, buf);
break;
#endif
default:
ret = WOLFBOOT_FS_E_PARAM;
break;
}
return ret;
}
uint64_t fs_size(const struct fs_file *f)
{
if (f == NULL) {
return 0;
}
return f->size;
}
const char *fs_type_name(const struct fs_volume *vol)
{
if (vol == NULL) {
return "none";
}
switch (vol->type) {
case FS_TYPE_RAW:
return "raw";
case FS_TYPE_FAT32:
return "fat32";
case FS_TYPE_EXT4:
return "ext4";
default:
return "none";
}
}
int fs_label_eq(struct fs_volume *vol, const char *label)
{
if ((vol == NULL) || (label == NULL)) {
return WOLFBOOT_FS_E_PARAM;
}
switch (vol->type) {
#ifdef WOLFBOOT_FAT32
case FS_TYPE_FAT32:
return fat32_label_eq(vol, label);
#endif
#ifdef WOLFBOOT_EXT4
case FS_TYPE_EXT4:
return ext4_label_eq(vol, label);
#endif
default:
/* A raw partition has no filesystem label. */
return 0;
}
}
#endif /* WOLFBOOT_DISK_FS */
#endif /* _WOLFBOOT_DISK_FS_C_ */

810
src/fat32.c 100644
View File

@ -0,0 +1,810 @@
/* fat32.c
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
/* Read-only FAT32 backend for the wolfBoot disk filesystem layer.
*
* Supports FAT32 only, with VFAT long filename reads. FAT12 and FAT16 are
* refused rather than misread, as are volumes with a logical sector size
* other than 512 (the partition layer assumes 512 throughout).
*
* SECURITY: the boot sector, the FAT and the directory entries are all
* attacker-controlled and are parsed before the image signature is
* checked. Every geometry field is validated at mount time so that later
* cluster-to-byte arithmetic cannot leave the partition, and every walk
* over an on-media link is bounded so a crafted cycle terminates.
*/
#ifndef _WOLFBOOT_FAT32_C_
#define _WOLFBOOT_FAT32_C_
#if defined(WOLFBOOT_DISK_FS) && defined(WOLFBOOT_FAT32)
#include <stdint.h>
#include <string.h>
#include "disk_fs.h"
#include "fat32.h"
#include "disk.h"
#include "printf.h"
#ifdef DEBUG_FS
#define FAT_DBG(_f_, ...) wolfBoot_printf(_f_, ##__VA_ARGS__)
#else
#define FAT_DBG(_f_, ...) do{}while(0)
#endif
/* Accumulator for one long filename set. LFN entries precede their short
* entry in reverse ordinal order, so the set is assembled as it is walked
* and only committed once the short entry passes the checksum test. */
struct fat_lfn_state {
char name[WOLFBOOT_FS_MAX_NAME + 1];
uint8_t expect_ord;
uint8_t cksum;
uint8_t have;
};
/* ---------------------------------------------------------------------
* Name handling
* --------------------------------------------------------------------- */
static char fat32_upper(char c)
{
if ((c >= 'a') && (c <= 'z')) {
return (char)(c - ('a' - 'A'));
}
return c;
}
/* FAT filenames are case-insensitive. */
static int fat32_name_eq(const char *a, const char *b)
{
uint32_t i = 0;
while ((a[i] != '\0') && (b[i] != '\0')) {
if (fat32_upper(a[i]) != fat32_upper(b[i])) {
return 0;
}
i++;
if (i > WOLFBOOT_FS_MAX_NAME) {
return 0;
}
}
return (a[i] == b[i]) ? 1 : 0;
}
/* Rotate-and-add checksum of an 8.3 name, per the FAT spec. This is the
* ONLY thing binding an LFN set to the short entry after it: unverified, a
* crafted directory could pair an attacker-chosen long name with an
* unrelated file's cluster and size, opening a different file than the one
* configured - before any signature check runs. */
static uint8_t fat32_sfn_checksum(const uint8_t *sfn)
{
uint8_t sum = 0;
uint32_t i;
for (i = 0; i < FAT_SFN_LEN; i++) {
sum = (uint8_t)((uint8_t)((sum >> 1) | (uint8_t)(sum << 7)) + sfn[i]);
}
return sum;
}
/* Render the 8.3 name of a directory entry as "NAME.EXT". */
static void fat32_sfn_to_name(const uint8_t *e, char *out)
{
uint32_t i;
uint32_t n = 0;
uint8_t c;
for (i = 0; i < FAT_SFN_NAME_LEN; i++) {
c = e[i];
if (c == ' ') {
break;
}
/* 0x05 stands in for a leading 0xE5, which marks a free entry. */
if ((i == 0U) && (c == FAT_DIR_KANJI_E5)) {
c = FAT_DIR_FREE;
}
out[n] = (char)c;
n++;
}
if (e[FAT_SFN_NAME_LEN] != ' ') {
out[n] = '.';
n++;
for (i = FAT_SFN_NAME_LEN; i < FAT_SFN_LEN; i++) {
c = e[i];
if (c == ' ') {
break;
}
out[n] = (char)c;
n++;
}
}
out[n] = '\0';
}
/* Fold one LFN entry into the accumulator. Any inconsistency (bad or
* out-of-sequence ordinal, changed checksum, non-ASCII, over-long name)
* discards the set. Not fatal: the entry just becomes unmatchable by its
* long name and the short name is still tried. */
static void fat32_lfn_accum(struct fat_lfn_state *s, const uint8_t *e)
{
/* Byte offsets of the 13 UCS-2 units within a long filename entry. */
static const uint8_t chr_off[FAT_LFN_CHARS] = {
1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30
};
uint8_t ord = (uint8_t)(e[0] & FAT_LFN_ORD_MASK);
uint32_t base;
uint32_t i;
uint16_t uc;
if ((e[0] & FAT_LFN_LAST) != 0U) {
memset(s, 0, sizeof(*s));
if ((ord == 0U) || (ord > FAT_LFN_MAX_ORD)) {
return;
}
/* Drop a set whose FIRST character already lies past the buffer:
* that name can never match a configured path component.
*
* The bound is on the first index, not ord * FAT_LFN_CHARS. The last
* entry holds 1 to 13 characters, so multiplying overestimates the
* length by up to 12 and would discard every 5-entry set, i.e. every
* name of 53 to 64 characters, even though those fit. The
* per-character check below rejects the ones that genuinely do not. */
if ((((uint32_t)ord - 1U) * FAT_LFN_CHARS) >=
(uint32_t)WOLFBOOT_FS_MAX_NAME) {
return;
}
s->cksum = e[FAT_LFN_CKSUM_OFF];
s->expect_ord = ord;
s->have = 1;
}
else {
if ((s->have == 0U) || (ord != s->expect_ord) ||
(e[FAT_LFN_CKSUM_OFF] != s->cksum)) {
memset(s, 0, sizeof(*s));
return;
}
}
base = ((uint32_t)ord - 1U) * FAT_LFN_CHARS;
for (i = 0; i < FAT_LFN_CHARS; i++) {
uc = fs_le16(e + chr_off[i]);
if ((uc == 0x0000U) || (uc == 0xFFFFU)) {
break;
}
/* Configured paths are ASCII, so a name that is not cannot match.
* Dropping the set avoids having to define a lossy mapping. */
if (uc > 0x7FU) {
memset(s, 0, sizeof(*s));
return;
}
if ((base + i) >= (uint32_t)WOLFBOOT_FS_MAX_NAME) {
memset(s, 0, sizeof(*s));
return;
}
s->name[base + i] = (char)uc;
}
s->expect_ord--;
}
/* ---------------------------------------------------------------------
* Mount
* --------------------------------------------------------------------- */
int fat32_mount(struct fs_volume *vol)
{
uint8_t bpb[FAT_BPB_SIZE];
uint8_t sig[2];
struct fat32_volume *fv;
uint32_t byts_per_sec, sec_per_clus, rsvd, num_fats;
uint32_t tot_sec32, fat_sz32, root_clus, clus_count, clus_sz;
uint16_t ext_flags;
uint64_t fat_off, fat_sz, data_off, meta_secs, data_secs;
uint8_t media;
int ret;
fv = &vol->u.fat;
if (vol->part_sz < (uint64_t)FAT_SECTOR_SIZE) {
return WOLFBOOT_FS_E_NOFS;
}
ret = fs_meta_read(vol, 0, FAT_BPB_SIZE, bpb);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
ret = fs_meta_read(vol, FAT_BOOT_SIG_OFF, 2, sig);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
byts_per_sec = fs_le16(bpb + FAT_BPB_BYTS_PER_SEC);
sec_per_clus = bpb[FAT_BPB_SEC_PER_CLUS];
rsvd = fs_le16(bpb + FAT_BPB_RSVD_SEC_CNT);
num_fats = bpb[FAT_BPB_NUM_FATS];
media = bpb[FAT_BPB_MEDIA];
/* Stage 1: is this a FAT volume at all? These are the fields every FAT
* variant shares, so failing any of them means "not FAT" and the probe
* moves on to the next filesystem rather than reporting an error. */
if (fs_le16(sig) != FAT_BOOT_SIG_VALUE) {
return WOLFBOOT_FS_E_NOFS;
}
if ((media != 0xF0U) && (media < 0xF8U)) {
return WOLFBOOT_FS_E_NOFS;
}
if ((fs_is_pow2(byts_per_sec) == 0) || (byts_per_sec < 512U) ||
(byts_per_sec > 4096U)) {
return WOLFBOOT_FS_E_NOFS;
}
if ((num_fats < 1U) || (num_fats > 2U)) {
return WOLFBOOT_FS_E_NOFS;
}
if (rsvd == 0U) {
return WOLFBOOT_FS_E_NOFS;
}
/* Stage 2: it is FAT. From here on a failure is an error, never a
* fallback to raw -- reading a real boot sector as an image header
* would be a silent wrong result. */
if (byts_per_sec != FAT_SECTOR_SIZE) {
FAT_DBG("fat32: sector size %u unsupported\r\n", byts_per_sec);
return WOLFBOOT_FS_E_UNSUPPORTED;
}
/* A non-zero root entry count, 16-bit total sector count or 16-bit FAT
* size all mean FAT12/FAT16. */
if (fs_le16(bpb + FAT_BPB_ROOT_ENT_CNT) != 0U) {
FAT_DBG("fat32: FAT12/FAT16 volume\r\n");
return WOLFBOOT_FS_E_UNSUPPORTED;
}
if (fs_le16(bpb + FAT_BPB_TOT_SEC16) != 0U) {
return WOLFBOOT_FS_E_UNSUPPORTED;
}
if (fs_le16(bpb + FAT_BPB_FAT_SZ16) != 0U) {
return WOLFBOOT_FS_E_UNSUPPORTED;
}
if (fs_le16(bpb + FAT_BPB_FS_VER) != 0U) {
return WOLFBOOT_FS_E_UNSUPPORTED;
}
/* With mirroring disabled, only FAT #0 being the active one is safe:
* otherwise the chain we would follow is a stale copy. */
ext_flags = fs_le16(bpb + FAT_BPB_EXT_FLAGS);
if (((ext_flags & FAT_EXT_NO_MIRROR) != 0U) &&
((ext_flags & FAT_EXT_ACTIVE_MASK) != 0U)) {
FAT_DBG("fat32: active FAT is not FAT 0\r\n");
return WOLFBOOT_FS_E_UNSUPPORTED;
}
if ((fs_is_pow2(sec_per_clus) == 0) || (sec_per_clus > 128U)) {
return WOLFBOOT_FS_E_FORMAT;
}
clus_sz = FAT_SECTOR_SIZE * sec_per_clus;
if (clus_sz > FAT_MAX_CLUS_SIZE) {
return WOLFBOOT_FS_E_FORMAT;
}
tot_sec32 = fs_le32(bpb + FAT_BPB_TOT_SEC32);
fat_sz32 = fs_le32(bpb + FAT_BPB_FAT_SZ32);
root_clus = fs_le32(bpb + FAT_BPB_ROOT_CLUS);
if ((tot_sec32 == 0U) || (fat_sz32 == 0U)) {
return WOLFBOOT_FS_E_FORMAT;
}
/* The volume must fit inside its partition; every byte offset derived
* below is then bounded by construction. */
if (((uint64_t)tot_sec32 * FAT_SECTOR_SIZE) > vol->part_sz) {
FAT_DBG("fat32: volume larger than partition\r\n");
return WOLFBOOT_FS_E_FORMAT;
}
fat_off = (uint64_t)rsvd * FAT_SECTOR_SIZE;
fat_sz = (uint64_t)fat_sz32 * FAT_SECTOR_SIZE;
data_off = fat_off + ((uint64_t)num_fats * fat_sz);
meta_secs = (uint64_t)rsvd + ((uint64_t)num_fats * (uint64_t)fat_sz32);
if (meta_secs >= (uint64_t)tot_sec32) {
return WOLFBOOT_FS_E_FORMAT;
}
data_secs = (uint64_t)tot_sec32 - meta_secs;
clus_count = (uint32_t)(data_secs / (uint64_t)sec_per_clus);
/* Fewer than 65525 data clusters is the definition of FAT16/FAT12.
* Checking it here makes the "no FAT12/FAT16" guarantee hold even if
* the individual BPB fields were forged to look like FAT32. */
if (clus_count < FAT_MIN_CLUSTERS) {
FAT_DBG("fat32: %u clusters, below the FAT32 minimum\r\n",
clus_count);
return WOLFBOOT_FS_E_UNSUPPORTED;
}
if (clus_count > FAT_CLUS_MAX) {
return WOLFBOOT_FS_E_FORMAT;
}
/* The FAT must be large enough to hold an entry for every cluster.
* Without this, a chain could name a cluster whose FAT entry lies past
* the end of the FAT, so the "link" read would be adjacent file data. */
if (fat_sz < (((uint64_t)clus_count + 2U) * 4U)) {
FAT_DBG("fat32: FAT too small for %u clusters\r\n", clus_count);
return WOLFBOOT_FS_E_FORMAT;
}
if ((data_off + ((uint64_t)clus_count * (uint64_t)clus_sz)) >
vol->part_sz) {
return WOLFBOOT_FS_E_FORMAT;
}
if ((root_clus < 2U) || (root_clus > (clus_count + 1U))) {
return WOLFBOOT_FS_E_FORMAT;
}
fv->fat_off = fat_off;
fv->fat_sz = fat_sz;
fv->data_off = data_off;
fv->clus_sz = clus_sz;
fv->clus_count = clus_count;
fv->root_clus = root_clus;
FAT_DBG("fat32: %u clusters of %u bytes, data at %x_%x\r\n",
clus_count, clus_sz, (uint32_t)(data_off >> 32), (uint32_t)data_off);
return WOLFBOOT_FS_OK;
}
/* ---------------------------------------------------------------------
* Cluster chain
* --------------------------------------------------------------------- */
/* A cluster number is usable only if it names a real data cluster: 0 and 1
* are reserved, and the volume's clus_count clusters are numbered from 2. */
static int fat32_clus_ok(const struct fat32_volume *fv, uint32_t clus)
{
return ((clus >= 2U) && (clus <= (fv->clus_count + 1U))) ? 1 : 0;
}
/* Follow one link of a cluster chain. Returns 0 with *next set, 1 at end
* of chain, or negative WOLFBOOT_FS_E_* on error. */
static int fat32_next_clus(struct fs_volume *vol, uint32_t clus,
uint32_t *next)
{
struct fat32_volume *fv = &vol->u.fat;
uint8_t ent[4];
uint64_t off;
uint32_t val;
int ret;
if (fat32_clus_ok(fv, clus) == 0) {
return WOLFBOOT_FS_E_CORRUPT;
}
/* clus is bounded by FAT_CLUS_MAX, so this cannot overflow. */
off = fv->fat_off + ((uint64_t)clus * 4U);
if ((off + 4U) > (fv->fat_off + fv->fat_sz)) {
return WOLFBOOT_FS_E_CORRUPT;
}
ret = fs_meta_read(vol, off, 4, ent);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
val = fs_le32(ent) & FAT_CLUS_MASK;
if (val >= FAT_CLUS_EOC_MIN) {
*next = 0;
return 1;
}
if (val == FAT_CLUS_BAD) {
return WOLFBOOT_FS_E_CORRUPT;
}
/* Free or reserved, or past the end of the volume: a chain must never
* point at one of these. */
if (fat32_clus_ok(fv, val) == 0) {
return WOLFBOOT_FS_E_CORRUPT;
}
*next = val;
return 0;
}
/* Position the cursor on the cluster holding a file offset. Forward-only, so
* a sequential load walks the chain once; a backwards seek restarts at the
* head. Bounded three ways: by the clusters the length can occupy, by the
* volume's cluster count (longer means a cycle), and by an absolute stop. A
* visited-set is unaffordable at 2^28 clusters. */
static int fat32_seek(struct fs_file *f, uint64_t off)
{
struct fs_volume *vol = f->vol;
struct fat32_volume *fv = &vol->u.fat;
uint64_t target, cur_idx, max_clusters, steps;
uint32_t next;
int ret;
target = off / (uint64_t)fv->clus_sz;
if ((f->cur_clus < 2U) ||
(target < (f->cur_off / (uint64_t)fv->clus_sz))) {
f->cur_clus = f->first_clus;
f->cur_off = 0;
}
if (fat32_clus_ok(fv, f->cur_clus) == 0) {
return WOLFBOOT_FS_E_CORRUPT;
}
max_clusters = (f->size + (uint64_t)fv->clus_sz - 1U) /
(uint64_t)fv->clus_sz;
cur_idx = f->cur_off / (uint64_t)fv->clus_sz;
steps = 0;
while (cur_idx < target) {
ret = fat32_next_clus(vol, f->cur_clus, &next);
if (ret < 0) {
return ret;
}
if (ret == 1) {
/* The chain ended before the file's declared length. */
return WOLFBOOT_FS_E_CORRUPT;
}
f->cur_clus = next;
f->cur_off += (uint64_t)fv->clus_sz;
cur_idx++;
steps++;
if ((steps > max_clusters) || (steps > (uint64_t)fv->clus_count) ||
(steps > (uint64_t)WOLFBOOT_FS_MAX_CHAIN)) {
FAT_DBG("fat32: cluster chain bound tripped\r\n");
return WOLFBOOT_FS_E_CORRUPT;
}
}
return WOLFBOOT_FS_OK;
}
/* Walk the whole chain at open time, so a malformed one errors before any
* payload loads. A cycle never hits end-of-chain, so it overruns the cluster
* count and trips that bound; no visited-set needed. A chain LONGER than the
* length needs is fine (preallocating writers do this); only short is an
* error. */
static int fat32_validate_chain(struct fs_volume *vol, uint32_t first,
uint64_t size)
{
struct fat32_volume *fv = &vol->u.fat;
uint64_t need, i, limit;
uint32_t clus = first;
uint32_t next;
int ret;
if (size == 0U) {
return WOLFBOOT_FS_OK;
}
need = (size + (uint64_t)fv->clus_sz - 1U) / (uint64_t)fv->clus_sz;
if (need > (uint64_t)fv->clus_count) {
return WOLFBOOT_FS_E_CORRUPT;
}
limit = (uint64_t)fv->clus_count;
if (limit > (uint64_t)WOLFBOOT_FS_MAX_CHAIN) {
limit = (uint64_t)WOLFBOOT_FS_MAX_CHAIN;
}
for (i = 1; i <= limit; i++) {
ret = fat32_next_clus(vol, clus, &next);
if (ret < 0) {
return ret;
}
if (ret == 1) {
/* The chain has i clusters. Fewer than the declared length
* needs means the directory entry and the FAT disagree. */
if (i < need) {
FAT_DBG("fat32: chain shorter than file size\r\n");
return WOLFBOOT_FS_E_CORRUPT;
}
return WOLFBOOT_FS_OK;
}
clus = next;
}
FAT_DBG("fat32: cluster chain never terminates\r\n");
return WOLFBOOT_FS_E_CORRUPT;
}
/* ---------------------------------------------------------------------
* Directory lookup
* --------------------------------------------------------------------- */
static int fat32_lookup(struct fs_volume *vol, uint32_t dir_clus,
const char *name, uint32_t *out_clus,
uint32_t *out_size, uint8_t *out_attr)
{
struct fat32_volume *fv = &vol->u.fat;
struct fat_lfn_state lfn;
uint8_t e[FAT_DIR_ENTRY_SIZE];
char sfn[FAT_SFN_MAX + 1];
uint64_t base;
uint32_t clus = dir_clus;
uint32_t blocks = 0;
uint32_t entries = 0;
uint32_t ents_per_clus;
uint32_t idx;
uint32_t next;
uint8_t attr;
int matched;
int ret;
ents_per_clus = fv->clus_sz / FAT_DIR_ENTRY_SIZE;
memset(&lfn, 0, sizeof(lfn));
for (;;) {
/* A directory chain has no declared length, so it is bounded by
* the volume's cluster count (anything longer is a cycle) and by
* the configurable block cap. */
if ((blocks >= (uint32_t)WOLFBOOT_FS_MAX_DIR_BLOCKS) ||
(blocks >= fv->clus_count)) {
FAT_DBG("fat32: directory chain bound tripped\r\n");
return WOLFBOOT_FS_E_CORRUPT;
}
if (fat32_clus_ok(fv, clus) == 0) {
return WOLFBOOT_FS_E_CORRUPT;
}
base = fv->data_off + (((uint64_t)clus - 2U) * (uint64_t)fv->clus_sz);
for (idx = 0; idx < ents_per_clus; idx++) {
if (entries >= (uint32_t)WOLFBOOT_FS_MAX_DIR_ENTRIES) {
FAT_DBG("fat32: directory entry bound tripped\r\n");
return WOLFBOOT_FS_E_CORRUPT;
}
entries++;
ret = fs_meta_read(vol,
base + ((uint64_t)idx * FAT_DIR_ENTRY_SIZE),
FAT_DIR_ENTRY_SIZE, e);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
if (e[0] == FAT_DIR_END) {
return WOLFBOOT_FS_E_NOENT;
}
if (e[0] == FAT_DIR_FREE) {
memset(&lfn, 0, sizeof(lfn));
continue;
}
attr = e[FAT_DIR_ATTR];
if ((attr & FAT_ATTR_LFN_MASK) == FAT_ATTR_LFN) {
fat32_lfn_accum(&lfn, e);
continue;
}
/* The volume label lives in a directory entry but is not a
* name any path can refer to. */
if ((attr & FAT_ATTR_VOLUME_ID) != 0U) {
memset(&lfn, 0, sizeof(lfn));
continue;
}
matched = 0;
/* A complete long name set, whose checksum matches this short
* entry, is the file's real name. */
if ((lfn.have != 0U) && (lfn.expect_ord == 0U) &&
(fat32_sfn_checksum(e) == lfn.cksum)) {
matched = fat32_name_eq(lfn.name, name);
}
/* The accumulator must never survive a short entry, or a
* discarded set could be paired with the following file. */
memset(&lfn, 0, sizeof(lfn));
if (matched == 0) {
fat32_sfn_to_name(e, sfn);
matched = fat32_name_eq(sfn, name);
}
if (matched == 0) {
continue;
}
*out_attr = attr;
*out_clus = ((uint32_t)fs_le16(e + FAT_DIR_FST_CLUS_HI) << 16) |
(uint32_t)fs_le16(e + FAT_DIR_FST_CLUS_LO);
*out_size = fs_le32(e + FAT_DIR_FILE_SIZE);
return WOLFBOOT_FS_OK;
}
blocks++;
ret = fat32_next_clus(vol, clus, &next);
if (ret < 0) {
return ret;
}
if (ret == 1) {
return WOLFBOOT_FS_E_NOENT;
}
clus = next;
}
}
/* ---------------------------------------------------------------------
* Public backend entry points
* --------------------------------------------------------------------- */
int fat32_open(struct fs_volume *vol, struct fs_file *f, const char *path,
uint64_t max_size)
{
struct fat32_volume *fv = &vol->u.fat;
char comp[WOLFBOOT_FS_MAX_NAME + 1];
const char *p = path;
uint32_t comp_len;
uint32_t clus;
uint32_t ent_clus = 0;
uint32_t ent_size = 0;
uint8_t attr = 0;
int depth = 0;
int have = 0;
int ret;
clus = fv->root_clus;
for (;;) {
ret = fs_path_next(&p, comp, &comp_len, &depth);
if (ret < 0) {
return ret;
}
if (ret == 0) {
break;
}
if (have != 0) {
/* Descend into whatever the previous component named. */
if ((attr & FAT_ATTR_DIRECTORY) == 0U) {
return WOLFBOOT_FS_E_NOTFILE;
}
if (fat32_clus_ok(fv, ent_clus) == 0) {
return WOLFBOOT_FS_E_CORRUPT;
}
clus = ent_clus;
}
ret = fat32_lookup(vol, clus, comp, &ent_clus, &ent_size, &attr);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
have = 1;
}
if (have == 0) {
/* The path named the root directory itself. */
return WOLFBOOT_FS_E_NOTFILE;
}
if ((attr & (FAT_ATTR_DIRECTORY | FAT_ATTR_VOLUME_ID)) != 0U) {
return WOLFBOOT_FS_E_NOTFILE;
}
/* A file cannot be larger than the partition holding it. */
if ((uint64_t)ent_size > vol->part_sz) {
return WOLFBOOT_FS_E_CORRUPT;
}
/* The directory-reported size is attacker-controlled and is about to
* bound a load into RAM, so it is checked before any of it is used. */
if ((uint64_t)ent_size > max_size) {
FAT_DBG("fat32: file size %u exceeds max\r\n", ent_size);
return WOLFBOOT_FS_E_TOOBIG;
}
if (ent_size > 0U) {
if (fat32_clus_ok(fv, ent_clus) == 0) {
return WOLFBOOT_FS_E_CORRUPT;
}
}
ret = fat32_validate_chain(vol, ent_clus, (uint64_t)ent_size);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
f->size = (uint64_t)ent_size;
f->first_clus = ent_clus;
f->cur_clus = ent_clus;
f->cur_off = 0;
return WOLFBOOT_FS_OK;
}
int fat32_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf)
{
struct fs_volume *vol = f->vol;
struct fat32_volume *fv = &vol->u.fat;
uint64_t done = 0;
uint64_t byte_off, avail, this_len;
uint32_t in_clus, run, clus, next;
int ret;
while (done < len) {
ret = fat32_seek(f, off + done);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
clus = f->cur_clus;
in_clus = (uint32_t)((off + done) % (uint64_t)fv->clus_sz);
/* Coalesce physically adjacent clusters into a single media read.
* An unfragmented file then loads with the same number of disk
* transfers as a raw partition would, which matters on the slower
* SD targets where a per-cluster read would dominate boot time. */
run = 1;
while ((((uint64_t)run * fv->clus_sz) - in_clus) < (len - done)) {
if (((uint64_t)clus + run) > ((uint64_t)fv->clus_count + 1U)) {
break;
}
ret = fat32_next_clus(vol, clus + run - 1U, &next);
if (ret != 0) {
/* End of chain, or a bad link. Either way stop extending
* the run; a real error is reported by the next seek. */
break;
}
if (next != (clus + run)) {
break;
}
run++;
}
avail = ((uint64_t)run * fv->clus_sz) - in_clus;
this_len = len - done;
if (this_len > avail) {
this_len = avail;
}
if (this_len > (uint64_t)DISK_IO_MAX_SIZE) {
this_len = (uint64_t)DISK_IO_MAX_SIZE;
}
byte_off = fv->data_off +
(((uint64_t)clus - 2U) * (uint64_t)fv->clus_sz) + in_clus;
/* Defence in depth: mount validated the geometry, so this cannot
* fire, but the cost of being wrong here is an out-of-partition
* read straight into the RAM load region. */
if ((byte_off > vol->part_sz) ||
(this_len > (vol->part_sz - byte_off))) {
return WOLFBOOT_FS_E_RANGE;
}
ret = fs_disk_read_exact(vol, byte_off, (uint32_t)this_len,
buf + done);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
done += this_len;
}
return (int)done;
}
int fat32_label_eq(struct fs_volume *vol, const char *label)
{
char vl[FAT_BS_VOL_LAB_LEN + 1];
uint8_t raw[FAT_BS_VOL_LAB_LEN];
uint32_t n;
uint32_t i;
int ret;
ret = fs_meta_read(vol, FAT_BS_VOL_LAB, FAT_BS_VOL_LAB_LEN, raw);
if (ret != WOLFBOOT_FS_OK) {
return ret;
}
for (i = 0; i < FAT_BS_VOL_LAB_LEN; i++) {
vl[i] = (char)raw[i];
}
vl[FAT_BS_VOL_LAB_LEN] = '\0';
/* The label is space-padded to a fixed width on media. */
n = FAT_BS_VOL_LAB_LEN;
while ((n > 0U) && (vl[n - 1U] == ' ')) {
n--;
}
vl[n] = '\0';
if (n == 0U) {
return 0;
}
return fat32_name_eq(vl, label);
}
#endif /* WOLFBOOT_DISK_FS && WOLFBOOT_FAT32 */
#endif /* _WOLFBOOT_FAT32_C_ */

View File

@ -135,6 +135,20 @@ TESTS+=unit-sdhci-acmd41-timeout
TESTS+=unit-ti-hercules-write
TESTS+=unit-p1021-qe-firmware
TESTS+=unit-t10xx-dts-memac
TESTS+=unit-fs-probe
TESTS+=unit-fs-malicious
# The filesystem interop tests build their fixtures with the host's real
# mkfs tools, so they are only added when those tools exist. The hostile
# input coverage lives in unit-fs-malicious, which needs no tools and is
# therefore unconditional.
HAVE_MKFS_VFAT := $(shell command -v mkfs.vfat 2>/dev/null)
HAVE_MCOPY := $(shell command -v mcopy 2>/dev/null)
ifneq ($(and $(HAVE_MKFS_VFAT),$(HAVE_MCOPY)),)
TESTS+=unit-fat32
else
$(warning Skipping unit-fat32: mkfs.vfat and/or mtools unavailable)
endif
# The x86-64 EFI unit test needs the gnu-efi development headers (same
# dependency as the CMake x86_64_efi target). Probe and only add the test
@ -850,6 +864,48 @@ gpt-sfdisk-test.h:
unit-disk: unit-disk.c gpt-sfdisk-test.h
gcc -o $@ $< $(CFLAGS) $(LDFLAGS)
# Backend-independent half of the read-only filesystem layer. Built with
# neither WOLFBOOT_FAT32 nor WOLFBOOT_EXT4, so it also pins the behaviour
# of a DISK_FS build with no parser selected: everything probes as raw.
unit-fs-probe: ../../include/target.h unit-fs-probe.c
gcc -o $@ unit-fs-probe.c $(CFLAGS) -DWOLFBOOT_DISK_FS $(LDFLAGS)
# Hostile-input coverage for every parser bound. Builds all of its
# metadata in C, so unlike the interop suites it is never skipped.
unit-fs-malicious: ../../include/target.h unit-fs-malicious.c
gcc -o $@ unit-fs-malicious.c $(CFLAGS) -DWOLFBOOT_DISK_FS \
-DWOLFBOOT_FAT32 $(LDFLAGS)
# A conformant FAT32 volume needs at least 65525 clusters, so even with
# one 512-byte sector per cluster the smallest valid image is about 33 MB.
# That is far too large to embed as a C array the way gpt-sfdisk-test.h
# does, so the image stays a file and the test reads it through a
# file-backed mock disk_read().
fat32-test.img:
rm -f $@ $@.tmp fat32-payload.bin fat32-nested.bin fat32-short.bin
truncate -s 40M $@.tmp
mkfs.vfat -F 32 -s 1 -S 512 -n WBTEST $@.tmp >/dev/null
head -c 1048576 /dev/urandom > fat32-payload.bin
head -c 4096 /dev/urandom > fat32-nested.bin
head -c 777 /dev/urandom > fat32-short.bin
MTOOLS_SKIP_CHECK=1 mmd -i $@.tmp ::/boot
MTOOLS_SKIP_CHECK=1 mmd -i $@.tmp ::/boot/sub
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-payload.bin \
::/boot/a-very-long-image-name-for-lfn.itb
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-nested.bin \
::/boot/sub/nested.bin
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-short.bin ::/SHORT.BIN
# 60 characters, so five LFN entries. Names of 53-64 characters fit
# WOLFBOOT_FS_MAX_NAME but need the fifth entry, which is the band a
# too-eager ordinal bound silently drops.
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-nested.bin \
"::/boot/a-deliberately-long-filename-that-needs-five-lfn-entries.itb"
mv $@.tmp $@
unit-fat32: ../../include/target.h unit-fat32.c fat32-test.img
gcc -o $@ unit-fat32.c $(CFLAGS) -DWOLFBOOT_DISK_FS -DWOLFBOOT_FAT32 \
$(LDFLAGS)
unit-multiboot: unit-multiboot.c
gcc -o $@ unit-multiboot.c $(CFLAGS) $(LDFLAGS)
@ -1365,7 +1421,9 @@ GENERATED_SRC:=aurix_erased_extract.h fdt_memrsv_extract.h \
t2080_fman_extract.h \
ti_hercules_write_extract.h versal_ext_write_extract.h versal_host.c \
versal_host.h versal_qspidev_extract.h zynq_erase_extract.h \
zynq_write_extract.h
zynq_write_extract.h \
fat32-test.img fat32-test.img.tmp fat32-payload.bin fat32-nested.bin \
fat32-short.bin
# Tests that are only in $(TESTS) when their toolchain/headers are
# present; clean must remove them regardless, or a host that has since

View File

@ -0,0 +1,537 @@
/* unit-fat32.c
*
* Interoperability tests for the read-only FAT32 backend (src/fat32.c)
* against a volume produced by the host's real mkfs.vfat and mcopy.
*
* Crafted and hostile metadata is covered separately in
* unit-fs-malicious.c, which needs no external tools and therefore always
* runs.
*
* Copyright (C) 2026 wolfSSL Inc.
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
/* fseek() takes a long, which is 32-bit on some hosts, so offsets past
* 2 GiB need fseeko()/off_t even with large-file support enabled. */
#define _FILE_OFFSET_BITS 64
#include <sys/types.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <check.h>
#include "gpt.c"
#include "disk.c"
#include "disk_fs.c"
#include "fat32.c"
#define IMG_PATH "fat32-test.img"
#define PAYLOAD_PATH "fat32-payload.bin"
#define NESTED_PATH "fat32-nested.bin"
#define SHORT_PATH "fat32-short.bin"
#define LFN_NAME "/boot/a-very-long-image-name-for-lfn.itb"
/* 60 characters: five LFN entries, still inside WOLFBOOT_FS_MAX_NAME. */
#define LFN5_NAME \
"/boot/a-deliberately-long-filename-that-needs-five-lfn-entries.itb"
/* The filesystem starts this far into the fake drive, so every offset the
* parser computes has to be partition-relative to land in the right place. */
#define PART_PAD (1024ULL * 1024ULL)
static FILE *img_fp = NULL;
static uint64_t img_size = 0;
static unsigned int disk_read_count = 0;
int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf)
{
uint64_t off;
(void)drv;
disk_read_count++;
if (start < PART_PAD) {
if ((start + count) > PART_PAD)
return -1;
memset(buf, 0, count);
return 0;
}
off = start - PART_PAD;
if ((off + count) > img_size)
return -1;
if (fseeko(img_fp, (off_t)off, SEEK_SET) != 0)
return -1;
if (fread(buf, 1, (size_t)count, img_fp) != (size_t)count)
return -1;
return 0;
}
int disk_write(int drv, uint64_t start, uint32_t count, const uint8_t *buf)
{
(void)drv; (void)start; (void)count; (void)buf;
return -1;
}
int disk_init(int drv) { (void)drv; return 0; }
void disk_close(int drv) { (void)drv; }
static uint8_t *load_file(const char *path, uint64_t *len)
{
FILE *fp = fopen(path, "rb");
uint8_t *buf;
off_t sz;
ck_assert(fp != NULL);
ck_assert_int_eq(fseeko(fp, 0, SEEK_END), 0);
sz = ftello(fp);
ck_assert(sz > 0);
ck_assert_int_eq(fseeko(fp, 0, SEEK_SET), 0);
buf = (uint8_t *)malloc((size_t)sz);
ck_assert(buf != NULL);
ck_assert_uint_eq(fread(buf, 1, (size_t)sz, fp), (size_t)sz);
fclose(fp);
*len = (uint64_t)sz;
return buf;
}
/* Present the whole test image as partition 0 of drive 0. */
static void open_image(void)
{
if (img_fp == NULL) {
img_fp = fopen(IMG_PATH, "rb");
ck_assert(img_fp != NULL);
ck_assert_int_eq(fseeko(img_fp, 0, SEEK_END), 0);
img_size = (uint64_t)ftello(img_fp);
ck_assert(img_size > 0);
}
memset(Drives, 0, sizeof(Drives));
Drives[0].drv = 0;
Drives[0].is_open = 1;
Drives[0].n_parts = 1;
Drives[0].part[0].drv = 0;
Drives[0].part[0].start = PART_PAD;
Drives[0].part[0].end = PART_PAD + img_size - 1;
fs_cache_invalidate();
disk_read_count = 0;
}
static void mount_ok(struct fs_volume *vol)
{
open_image();
ck_assert_int_eq(fs_mount(vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(vol->type, FS_TYPE_FAT32);
}
/* --- mount --- */
START_TEST(test_fat32_mount_geometry)
{
struct fs_volume vol;
mount_ok(&vol);
/* Below this cluster count the volume would be FAT16 by definition. */
ck_assert_uint_ge(vol.u.fat.clus_count, 65525U);
ck_assert_uint_eq(vol.u.fat.clus_sz, 512U);
ck_assert_uint_ge(vol.u.fat.root_clus, 2U);
ck_assert_uint_le(vol.u.fat.root_clus, vol.u.fat.clus_count + 1U);
/* The FAT must be able to describe every cluster. */
ck_assert(vol.u.fat.fat_sz >= ((uint64_t)vol.u.fat.clus_count + 2U) * 4U);
/* The data region must lie inside the partition. */
ck_assert(vol.u.fat.data_off +
((uint64_t)vol.u.fat.clus_count * vol.u.fat.clus_sz) <= vol.part_sz);
}
END_TEST
START_TEST(test_fat32_type_name)
{
struct fs_volume vol;
mount_ok(&vol);
ck_assert_str_eq(fs_type_name(&vol), "fat32");
}
END_TEST
/* --- name resolution --- */
/* A name of 53-64 characters needs five LFN entries but still fits
* WOLFBOOT_FS_MAX_NAME (64). Bounding on ord * 13 overestimates the length
* by up to 12, which drops this whole band and makes such a BOOT_FILE_A
* unresolvable. The per-character bound is what catches genuinely
* over-long names. */
START_TEST(test_fat32_open_five_entry_long_name)
{
struct fs_volume vol;
struct fs_file f;
open_image();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(vol.type, FS_TYPE_FAT32);
ck_assert_int_eq(fs_open(&vol, &f, LFN5_NAME, 1U << 30), WOLFBOOT_FS_OK);
ck_assert_uint_eq((unsigned)fs_size(&f), 4096U);
}
END_TEST
START_TEST(test_fat32_open_long_name)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)plen);
free(payload);
}
END_TEST
START_TEST(test_fat32_open_short_name)
{
struct fs_volume vol;
struct fs_file f;
uint64_t slen;
uint8_t *sdata = load_file(SHORT_PATH, &slen);
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, "/SHORT.BIN", slen), WOLFBOOT_FS_OK);
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)slen);
free(sdata);
}
END_TEST
/* FAT names are case-insensitive, for both the 8.3 and long forms. */
START_TEST(test_fat32_open_case_insensitive)
{
struct fs_volume vol;
struct fs_file f;
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f,
"/BOOT/A-VERY-LONG-IMAGE-NAME-FOR-LFN.ITB", 1U << 30),
WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(&vol, &f, "/short.bin", 1U << 30),
WOLFBOOT_FS_OK);
/* The 8.3 alias mkfs generated for the long name. */
ck_assert_int_eq(fs_open(&vol, &f, "/boot/A-VERY~1.ITB", 1U << 30),
WOLFBOOT_FS_OK);
}
END_TEST
START_TEST(test_fat32_open_nested_path)
{
struct fs_volume vol;
struct fs_file f;
uint64_t nlen;
uint8_t *ndata = load_file(NESTED_PATH, &nlen);
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, "/boot/sub/nested.bin", nlen),
WOLFBOOT_FS_OK);
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)nlen);
free(ndata);
}
END_TEST
START_TEST(test_fat32_open_missing)
{
struct fs_volume vol;
struct fs_file f;
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, "/boot/nope.itb", 1U << 30),
WOLFBOOT_FS_E_NOENT);
ck_assert_int_eq(fs_open(&vol, &f, "/nodir/nope.itb", 1U << 30),
WOLFBOOT_FS_E_NOENT);
}
END_TEST
START_TEST(test_fat32_open_directory_is_not_a_file)
{
struct fs_volume vol;
struct fs_file f;
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, "/boot", 1U << 30),
WOLFBOOT_FS_E_NOTFILE);
ck_assert_int_eq(fs_open(&vol, &f, "/", 1U << 30),
WOLFBOOT_FS_E_NOTFILE);
}
END_TEST
START_TEST(test_fat32_cannot_descend_into_a_file)
{
struct fs_volume vol;
struct fs_file f;
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, "/SHORT.BIN/x", 1U << 30),
WOLFBOOT_FS_E_NOTFILE);
}
END_TEST
/* The size in the directory entry is attacker-controlled and is about to
* bound a load into RAM, so it must be refused before any of it is used. */
START_TEST(test_fat32_open_respects_max_size)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen - 1),
WOLFBOOT_FS_E_TOOBIG);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
free(payload);
}
END_TEST
START_TEST(test_fat32_label)
{
struct fs_volume vol;
mount_ok(&vol);
ck_assert_int_eq(fs_label_eq(&vol, "WBTEST"), 1);
ck_assert_int_eq(fs_label_eq(&vol, "wbtest"), 1);
ck_assert_int_eq(fs_label_eq(&vol, "WBTES"), 0);
ck_assert_int_eq(fs_label_eq(&vol, "WBTESTX"), 0);
ck_assert_int_eq(fs_label_eq(&vol, ""), 0);
}
END_TEST
/* --- reading --- */
START_TEST(test_fat32_read_whole_file)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
uint8_t *got = (uint8_t *)malloc((size_t)plen);
ck_assert(got != NULL);
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, 0, plen, got), (int)plen);
ck_assert_int_eq(memcmp(got, payload, (size_t)plen), 0);
free(got);
free(payload);
}
END_TEST
/* The disk loader reads in fixed-size chunks, so the same file must come
* back identically when it is fetched piecewise. */
START_TEST(test_fat32_read_chunked)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen, off;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
uint8_t *got = (uint8_t *)malloc((size_t)plen);
const uint64_t chunk = 4096;
int ret;
ck_assert(got != NULL);
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
for (off = 0; off < plen; off += chunk) {
ret = fs_read(&f, off, chunk, got + off);
ck_assert_int_gt(ret, 0);
ck_assert_uint_eq((unsigned int)ret,
(unsigned int)((plen - off) < chunk ? (plen - off) : chunk));
}
ck_assert_int_eq(memcmp(got, payload, (size_t)plen), 0);
free(got);
free(payload);
}
END_TEST
/* Exercise every combination of unaligned start and length around the
* cluster boundaries, where the head/tail split happens. */
START_TEST(test_fat32_read_cluster_boundaries)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
uint8_t buf[4096];
static const int deltas[] = { -3, -1, 0, 1, 3 };
uint32_t cs;
unsigned int b, d, l;
static const uint32_t lens[] = { 1, 7, 511, 512, 513, 1024, 4096 };
uint64_t off;
int ret;
mount_ok(&vol);
cs = vol.u.fat.clus_sz;
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
for (b = 1; b <= 4; b++) {
for (d = 0; d < sizeof(deltas) / sizeof(deltas[0]); d++) {
off = (uint64_t)((int64_t)((uint64_t)b * cs) + deltas[d]);
for (l = 0; l < sizeof(lens) / sizeof(lens[0]); l++) {
if ((off + lens[l]) > plen)
continue;
ret = fs_read(&f, off, lens[l], buf);
ck_assert_int_eq(ret, (int)lens[l]);
ck_assert_int_eq(memcmp(buf, payload + off, lens[l]), 0);
}
}
}
free(payload);
}
END_TEST
/* A backwards seek must restart the chain walk and still be correct. */
START_TEST(test_fat32_read_backwards)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
uint8_t buf[256];
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, plen - 256, 256, buf), 256);
ck_assert_int_eq(memcmp(buf, payload + plen - 256, 256), 0);
ck_assert_int_eq(fs_read(&f, 0, 256, buf), 256);
ck_assert_int_eq(memcmp(buf, payload, 256), 0);
ck_assert_int_eq(fs_read(&f, 100000, 256, buf), 256);
ck_assert_int_eq(memcmp(buf, payload + 100000, 256), 0);
free(payload);
}
END_TEST
START_TEST(test_fat32_read_eof)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
uint8_t buf[512];
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, plen, sizeof(buf), buf), 0);
ck_assert_int_eq(fs_read(&f, plen - 10, sizeof(buf), buf), 10);
ck_assert_int_eq(memcmp(buf, payload + plen - 10, 10), 0);
free(payload);
}
END_TEST
START_TEST(test_fat32_read_small_files)
{
struct fs_volume vol;
struct fs_file f;
uint64_t slen, nlen;
uint8_t *sdata = load_file(SHORT_PATH, &slen);
uint8_t *ndata = load_file(NESTED_PATH, &nlen);
uint8_t buf[8192];
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, "/SHORT.BIN", slen), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, 0, slen, buf), (int)slen);
ck_assert_int_eq(memcmp(buf, sdata, (size_t)slen), 0);
ck_assert_int_eq(fs_open(&vol, &f, "/boot/sub/nested.bin", nlen),
WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, 0, nlen, buf), (int)nlen);
ck_assert_int_eq(memcmp(buf, ndata, (size_t)nlen), 0);
free(sdata);
free(ndata);
}
END_TEST
/* An unfragmented file must coalesce into large media reads rather than
* one read per cluster. On the slower SD targets a per-cluster read would
* dominate boot time, so this is a performance contract, not a nicety. */
START_TEST(test_fat32_contiguous_runs_are_coalesced)
{
struct fs_volume vol;
struct fs_file f;
uint64_t plen;
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
uint8_t *got = (uint8_t *)malloc((size_t)plen);
unsigned int reads;
unsigned int per_cluster;
ck_assert(got != NULL);
mount_ok(&vol);
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
disk_read_count = 0;
ck_assert_int_eq(fs_read(&f, 0, plen, got), (int)plen);
reads = disk_read_count;
per_cluster = (unsigned int)(plen / vol.u.fat.clus_sz);
ck_assert_int_eq(memcmp(got, payload, (size_t)plen), 0);
/* Far fewer than one media read per cluster: the FAT walk itself costs
* a few cached reads, the payload should cost only a handful. */
ck_assert_uint_lt(reads, per_cluster / 8U);
free(got);
free(payload);
}
END_TEST
static Suite *wolfboot_suite(void)
{
Suite *s = suite_create("wolfboot-fat32");
TCase *tc_mount = tcase_create("fat32-mount");
TCase *tc_name = tcase_create("fat32-names");
TCase *tc_read = tcase_create("fat32-read");
tcase_add_test(tc_mount, test_fat32_mount_geometry);
tcase_add_test(tc_mount, test_fat32_type_name);
tcase_add_test(tc_mount, test_fat32_label);
suite_add_tcase(s, tc_mount);
tcase_add_test(tc_name, test_fat32_open_long_name);
tcase_add_test(tc_name, test_fat32_open_five_entry_long_name);
tcase_add_test(tc_name, test_fat32_open_short_name);
tcase_add_test(tc_name, test_fat32_open_case_insensitive);
tcase_add_test(tc_name, test_fat32_open_nested_path);
tcase_add_test(tc_name, test_fat32_open_missing);
tcase_add_test(tc_name, test_fat32_open_directory_is_not_a_file);
tcase_add_test(tc_name, test_fat32_cannot_descend_into_a_file);
tcase_add_test(tc_name, test_fat32_open_respects_max_size);
suite_add_tcase(s, tc_name);
tcase_add_test(tc_read, test_fat32_read_whole_file);
tcase_add_test(tc_read, test_fat32_read_chunked);
tcase_add_test(tc_read, test_fat32_read_cluster_boundaries);
tcase_add_test(tc_read, test_fat32_read_backwards);
tcase_add_test(tc_read, test_fat32_read_eof);
tcase_add_test(tc_read, test_fat32_read_small_files);
tcase_add_test(tc_read, test_fat32_contiguous_runs_are_coalesced);
suite_add_tcase(s, tc_read);
return s;
}
int main(void)
{
int fails;
Suite *s = wolfboot_suite();
SRunner *sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
fails = srunner_ntests_failed(sr);
srunner_free(sr);
return fails;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,738 @@
/* unit-fs-probe.c
*
* Unit tests for the backend-independent parts of the read-only
* filesystem layer (src/disk_fs.c): filesystem probing, the raw
* passthrough, the metadata cache and the path splitter.
*
* Backend-specific probe rejection (FAT16 geometry, unsupported ext4
* features) is covered in unit-fat32.c and unit-ext4.c, where those
* parsers are compiled in.
*
* Copyright (C) 2026 wolfSSL Inc.
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <check.h>
#include "gpt.c"
#include "disk.c"
#include "disk_fs.c"
/* Fake disk backing store for mock disk_read/disk_write */
#define FAKE_DISK_SIZE (256 * 1024)
static uint8_t fake_disk[FAKE_DISK_SIZE];
/* Set to a byte offset to make disk_read fail at that address. -1 = no fail */
static int64_t mock_disk_read_fail_at = -1;
/* Number of disk_read calls since the last reset */
static unsigned int disk_read_count = 0;
int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf)
{
(void)drv;
disk_read_count++;
if (mock_disk_read_fail_at >= 0 && (int64_t)start == mock_disk_read_fail_at)
return -1;
if (start + count > FAKE_DISK_SIZE)
return -1;
memcpy(buf, fake_disk + start, count);
return 0;
}
int disk_write(int drv, uint64_t start, uint32_t count, const uint8_t *buf)
{
(void)drv;
if (start + count > FAKE_DISK_SIZE)
return -1;
memcpy(fake_disk + start, buf, count);
return 0;
}
int disk_init(int drv) { (void)drv; return 0; }
void disk_close(int drv) { (void)drv; }
/* Two MBR partitions, in sectors. */
#define P0_LBA 8
#define P0_SECS 64
#define P1_LBA 128
#define P1_SECS 64
#define P0_OFF ((uint64_t)P0_LBA * 512)
#define P0_SIZE ((uint64_t)P0_SECS * 512)
#define P1_OFF ((uint64_t)P1_LBA * 512)
#define P1_SIZE ((uint64_t)P1_SECS * 512)
static void put_le32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)((v >> 8) & 0xFF);
p[2] = (uint8_t)((v >> 16) & 0xFF);
p[3] = (uint8_t)((v >> 24) & 0xFF);
}
/* Build an MBR with two type-0x83 partitions and open the drive. */
static void build_disk(void)
{
uint8_t *e;
memset(fake_disk, 0, sizeof(fake_disk));
memset(Drives, 0, sizeof(Drives));
e = fake_disk + GPT_MBR_ENTRY_START;
e[4] = 0x83;
put_le32(e + 8, P0_LBA);
put_le32(e + 12, P0_SECS);
e += 16;
e[4] = 0x83;
put_le32(e + 8, P1_LBA);
put_le32(e + 12, P1_SECS);
fake_disk[GPT_MBR_BOOTSIG_OFFSET] = 0x55;
fake_disk[GPT_MBR_BOOTSIG_OFFSET + 1] = 0xAA;
ck_assert_int_eq(disk_open(0), 2);
mock_disk_read_fail_at = -1;
disk_read_count = 0;
fs_cache_invalidate();
}
/* Fill a partition with a deterministic, offset-dependent pattern. */
static void fill_pattern(uint64_t off, uint64_t len, uint8_t seed)
{
uint64_t i;
for (i = 0; i < len; i++)
fake_disk[off + i] = (uint8_t)((i * 7U) + seed);
}
/* --- probing --- */
START_TEST(test_probe_wolfboot_magic_is_raw)
{
struct fs_volume vol;
build_disk();
memcpy(fake_disk + P0_OFF, "WOLF", 4);
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
ck_assert_uint_eq((unsigned int)vol.part_sz, (unsigned int)P0_SIZE);
}
END_TEST
/* A raw image whose header happens to be followed by a 0x55AA at the end
* of the sector must still take the raw short-circuit, not be probed. */
START_TEST(test_probe_magic_wins_over_boot_signature)
{
struct fs_volume vol;
build_disk();
memcpy(fake_disk + P0_OFF, "WOLF", 4);
fake_disk[P0_OFF + 0x1FE] = 0x55;
fake_disk[P0_OFF + 0x1FF] = 0xAA;
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
}
END_TEST
START_TEST(test_probe_empty_partition_is_raw)
{
struct fs_volume vol;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
}
END_TEST
/* Garbage that is neither an image header nor a filesystem falls back to
* raw, exactly as a build without DISK_FS would behave. */
START_TEST(test_probe_garbage_is_raw)
{
struct fs_volume vol;
build_disk();
fill_pattern(P0_OFF, P0_SIZE, 0x11);
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
}
END_TEST
START_TEST(test_probe_bad_partition_index)
{
struct fs_volume vol;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 7), WOLFBOOT_FS_E_IO);
ck_assert_int_eq(vol.type, FS_TYPE_NONE);
}
END_TEST
START_TEST(test_probe_null_volume)
{
build_disk();
ck_assert_int_eq(fs_mount(NULL, 0, 0), WOLFBOOT_FS_E_PARAM);
}
END_TEST
/* --- raw passthrough --- */
START_TEST(test_raw_open_size_is_partition_size)
{
struct fs_volume vol;
struct fs_file f;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 1), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(&vol, &f, NULL, 16), WOLFBOOT_FS_OK);
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)P1_SIZE);
}
END_TEST
START_TEST(test_raw_read_matches_disk_part_read)
{
struct fs_volume vol;
struct fs_file f;
uint8_t via_fs[600];
uint8_t via_disk[600];
build_disk();
fill_pattern(P0_OFF, P0_SIZE, 0x5A);
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, 300, sizeof(via_fs), via_fs),
(int)sizeof(via_fs));
ck_assert_int_eq(disk_part_read(0, 0, 300, sizeof(via_disk), via_disk),
(int)sizeof(via_disk));
ck_assert_int_eq(memcmp(via_fs, via_disk, sizeof(via_fs)), 0);
}
END_TEST
START_TEST(test_raw_read_past_eof_returns_zero)
{
struct fs_volume vol;
struct fs_file f;
uint8_t buf[16];
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, P0_SIZE, sizeof(buf), buf), 0);
ck_assert_int_eq(fs_read(&f, P0_SIZE + 1024, sizeof(buf), buf), 0);
}
END_TEST
START_TEST(test_raw_read_straddling_eof_is_clamped)
{
struct fs_volume vol;
struct fs_file f;
uint8_t buf[64];
build_disk();
fill_pattern(P0_OFF, P0_SIZE, 0x33);
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
/* Ask for 64 bytes starting 16 before the end: 16 must come back. */
ck_assert_int_eq(fs_read(&f, P0_SIZE - 16, sizeof(buf), buf), 16);
}
END_TEST
START_TEST(test_raw_read_zero_length)
{
struct fs_volume vol;
struct fs_file f;
uint8_t buf[4];
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(&f, 0, 0, buf), 0);
}
END_TEST
START_TEST(test_raw_label_never_matches)
{
struct fs_volume vol;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_label_eq(&vol, "boot"), 0);
}
END_TEST
START_TEST(test_type_name)
{
struct fs_volume vol;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_str_eq(fs_type_name(&vol), "raw");
ck_assert_str_eq(fs_type_name(NULL), "none");
vol.type = FS_TYPE_NONE;
ck_assert_str_eq(fs_type_name(&vol), "none");
}
END_TEST
/* --- bounded disk access --- */
/* disk_part_read() silently clamps a read to the partition end and reports
* the clamped count. fs_disk_read_exact() must refuse it rather than let a
* truncation look like success. */
START_TEST(test_read_exact_refuses_partition_end_clamp)
{
struct fs_volume vol;
uint8_t buf[128];
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
/* Straddles the end of the partition. disk_part_read would return 16. */
ck_assert_int_eq(fs_disk_read_exact(&vol, P0_SIZE - 16, sizeof(buf), buf),
WOLFBOOT_FS_E_RANGE);
/* Entirely past the end. */
ck_assert_int_eq(fs_disk_read_exact(&vol, P0_SIZE, 4, buf),
WOLFBOOT_FS_E_RANGE);
/* The last bytes of the partition are still readable. */
ck_assert_int_eq(fs_disk_read_exact(&vol, P0_SIZE - 16, 16, buf),
WOLFBOOT_FS_OK);
}
END_TEST
START_TEST(test_read_exact_io_error)
{
struct fs_volume vol;
uint8_t buf[16];
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
mock_disk_read_fail_at = (int64_t)(P0_OFF + 512);
ck_assert_int_eq(fs_disk_read_exact(&vol, 512, sizeof(buf), buf),
WOLFBOOT_FS_E_IO);
}
END_TEST
START_TEST(test_read_exact_null_and_zero)
{
struct fs_volume vol;
uint8_t buf[4];
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_disk_read_exact(&vol, 0, 0, buf), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_disk_read_exact(NULL, 0, 4, buf), WOLFBOOT_FS_E_PARAM);
ck_assert_int_eq(fs_disk_read_exact(&vol, 0, 4, NULL), WOLFBOOT_FS_E_PARAM);
}
END_TEST
/* --- metadata cache --- */
START_TEST(test_meta_read_spans_cache_windows)
{
struct fs_volume vol;
uint8_t buf[1400];
uint64_t i;
build_disk();
fill_pattern(P0_OFF, P0_SIZE, 0x77);
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
/* Unaligned start, length far larger than one cache window. */
ck_assert_int_eq(fs_meta_read(&vol, 300, sizeof(buf), buf),
WOLFBOOT_FS_OK);
for (i = 0; i < sizeof(buf); i++)
ck_assert_uint_eq(buf[i], fake_disk[P0_OFF + 300 + i]);
}
END_TEST
START_TEST(test_meta_read_is_cached)
{
struct fs_volume vol;
uint8_t buf[4];
unsigned int first;
build_disk();
fill_pattern(P0_OFF, P0_SIZE, 0x22);
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
/* fs_mount() already warmed the window covering offset 0, so start
* from a cold cache to observe the first miss. */
fs_cache_invalidate();
disk_read_count = 0;
ck_assert_int_eq(fs_meta_read(&vol, 16, sizeof(buf), buf), WOLFBOOT_FS_OK);
first = disk_read_count;
ck_assert_uint_gt(first, 0);
/* Same window: must not hit the media again. */
ck_assert_int_eq(fs_meta_read(&vol, 20, sizeof(buf), buf), WOLFBOOT_FS_OK);
ck_assert_uint_eq(disk_read_count, first);
/* A different window must miss. */
ck_assert_int_eq(fs_meta_read(&vol, WOLFBOOT_FS_CACHE_SIZE, sizeof(buf),
buf), WOLFBOOT_FS_OK);
ck_assert_uint_gt(disk_read_count, first);
}
END_TEST
/* The cache window is keyed by drive and partition as well as by offset.
* Slot A and slot B can be different partitions, and an offset-only key
* would hand one partition's reads the other partition's bytes. */
START_TEST(test_meta_cache_keyed_by_partition)
{
struct fs_volume v0, v1;
uint8_t b0[8], b1[8];
build_disk();
fill_pattern(P0_OFF, P0_SIZE, 0x01);
fill_pattern(P1_OFF, P1_SIZE, 0xF0);
ck_assert_int_eq(fs_mount(&v0, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_meta_read(&v0, 0, sizeof(b0), b0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_mount(&v1, 0, 1), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_meta_read(&v1, 0, sizeof(b1), b1), WOLFBOOT_FS_OK);
ck_assert_int_eq(memcmp(b0, fake_disk + P0_OFF, sizeof(b0)), 0);
ck_assert_int_eq(memcmp(b1, fake_disk + P1_OFF, sizeof(b1)), 0);
ck_assert_int_ne(memcmp(b0, b1, sizeof(b0)), 0);
}
END_TEST
/* A window at the tail of the partition is shorter than the cache size.
* The short window must still satisfy reads that fall inside it. */
START_TEST(test_meta_read_short_tail_window)
{
struct fs_volume vol;
uint8_t buf[8];
build_disk();
fill_pattern(P0_OFF, P0_SIZE, 0x44);
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_meta_read(&vol, P0_SIZE - sizeof(buf), sizeof(buf),
buf), WOLFBOOT_FS_OK);
ck_assert_int_eq(memcmp(buf, fake_disk + P0_OFF + P0_SIZE - sizeof(buf),
sizeof(buf)), 0);
ck_assert_int_eq(fs_meta_read(&vol, P0_SIZE - 4, 8, buf),
WOLFBOOT_FS_E_RANGE);
}
END_TEST
START_TEST(test_meta_read_out_of_range)
{
struct fs_volume vol;
uint8_t buf[8];
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_meta_read(&vol, P0_SIZE, 8, buf),
WOLFBOOT_FS_E_RANGE);
ck_assert_int_eq(fs_meta_read(&vol, 0xFFFFFFFFFFFFFF00ULL, 8, buf),
WOLFBOOT_FS_E_RANGE);
ck_assert_int_eq(fs_meta_read(&vol, 0, 0, buf), WOLFBOOT_FS_OK);
}
END_TEST
/* --- partition accessors added for the filesystem layer --- */
START_TEST(test_disk_part_size)
{
uint64_t sz = 0;
build_disk();
ck_assert_int_eq(disk_part_size(0, 0, &sz), 0);
ck_assert_uint_eq((unsigned int)sz, (unsigned int)P0_SIZE);
ck_assert_int_eq(disk_part_size(0, 1, &sz), 0);
ck_assert_uint_eq((unsigned int)sz, (unsigned int)P1_SIZE);
/* Past the end of the parsed table, and past MAX_PARTITIONS. */
ck_assert_int_eq(disk_part_size(0, 2, &sz), -1);
ck_assert_int_eq(disk_part_size(0, MAX_PARTITIONS, &sz), -1);
ck_assert_int_eq(disk_part_size(0, -1, &sz), -1);
ck_assert_int_eq(disk_part_size(MAX_DISKS, 0, &sz), -1);
ck_assert_int_eq(disk_part_size(0, 0, NULL), -1);
}
END_TEST
START_TEST(test_disk_part_count)
{
build_disk();
ck_assert_int_eq(disk_part_count(0), 2);
/* Out-of-range drives, and a drive that was never opened. */
ck_assert_int_eq(disk_part_count(-1), -1);
ck_assert_int_eq(disk_part_count(MAX_DISKS), -1);
memset(Drives, 0, sizeof(Drives));
ck_assert_int_eq(disk_part_count(0), -1);
}
END_TEST
/* --- little-endian accessors --- */
START_TEST(test_le_accessors)
{
static const uint8_t b[8] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
};
ck_assert_uint_eq(fs_le16(b), 0x0201U);
ck_assert_uint_eq(fs_le32(b), 0x04030201U);
}
END_TEST
/* --- path splitter --- */
static int split_all(const char *path, char out[][WOLFBOOT_FS_MAX_NAME + 1],
int max)
{
const char *p = path;
char name[WOLFBOOT_FS_MAX_NAME + 1];
uint32_t len;
int depth = 0;
int n = 0;
int ret;
for (;;) {
ret = fs_path_next(&p, name, &len, &depth);
if (ret <= 0)
return (ret < 0) ? ret : n;
if (n >= max)
return -100;
strcpy(out[n], name);
n++;
}
}
START_TEST(test_path_split_basic)
{
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
ck_assert_int_eq(split_all("/boot/image.itb", parts, 4), 2);
ck_assert_str_eq(parts[0], "boot");
ck_assert_str_eq(parts[1], "image.itb");
}
END_TEST
START_TEST(test_path_split_collapses_slashes)
{
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
ck_assert_int_eq(split_all("//boot///image.itb//", parts, 4), 2);
ck_assert_str_eq(parts[0], "boot");
ck_assert_str_eq(parts[1], "image.itb");
}
END_TEST
/* A backslash is an ordinary filename character on both FAT and ext4.
* Treating it as a separator would let a crafted name split in two. */
START_TEST(test_path_backslash_is_not_a_separator)
{
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
ck_assert_int_eq(split_all("/a\\b", parts, 4), 1);
ck_assert_str_eq(parts[0], "a\\b");
}
END_TEST
START_TEST(test_path_rejects_dot_and_dotdot)
{
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
ck_assert_int_eq(split_all("/boot/./image", parts, 4),
WOLFBOOT_FS_E_PARAM);
ck_assert_int_eq(split_all("/boot/../image", parts, 4),
WOLFBOOT_FS_E_PARAM);
ck_assert_int_eq(split_all("/..", parts, 4), WOLFBOOT_FS_E_PARAM);
/* Names that merely begin with a dot are fine. */
ck_assert_int_eq(split_all("/.config", parts, 4), 1);
ck_assert_int_eq(split_all("/...", parts, 4), 1);
}
END_TEST
START_TEST(test_path_rejects_excess_depth)
{
char parts[16][WOLFBOOT_FS_MAX_NAME + 1];
char path[WOLFBOOT_FS_MAX_PATH + 8];
int i;
path[0] = '\0';
for (i = 0; i < WOLFBOOT_FS_MAX_PATH_DEPTH; i++)
strcat(path, "/a");
ck_assert_int_eq(split_all(path, parts, 16), WOLFBOOT_FS_MAX_PATH_DEPTH);
strcat(path, "/a");
ck_assert_int_eq(split_all(path, parts, 16), WOLFBOOT_FS_E_PARAM);
}
END_TEST
START_TEST(test_path_rejects_long_component)
{
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
char path[WOLFBOOT_FS_MAX_NAME + 8];
int i;
path[0] = '/';
for (i = 0; i < WOLFBOOT_FS_MAX_NAME; i++)
path[1 + i] = 'x';
path[1 + WOLFBOOT_FS_MAX_NAME] = '\0';
ck_assert_int_eq(split_all(path, parts, 4), 1);
path[1 + WOLFBOOT_FS_MAX_NAME] = 'x';
path[2 + WOLFBOOT_FS_MAX_NAME] = '\0';
ck_assert_int_eq(split_all(path, parts, 4), WOLFBOOT_FS_E_PARAM);
}
END_TEST
START_TEST(test_path_empty)
{
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
ck_assert_int_eq(split_all("/", parts, 4), 0);
ck_assert_int_eq(split_all("", parts, 4), 0);
}
END_TEST
START_TEST(test_path_next_null_args)
{
char name[WOLFBOOT_FS_MAX_NAME + 1];
uint32_t len;
int depth = 0;
ck_assert_int_eq(fs_path_next(NULL, name, &len, &depth),
WOLFBOOT_FS_E_PARAM);
}
END_TEST
/* --- fs_open argument validation --- */
/* With no backend compiled in, every partition probes as raw, so these
* checks are exercised against a synthetic non-raw type. */
START_TEST(test_open_rejects_bad_type)
{
struct fs_volume vol;
struct fs_file f;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
vol.type = FS_TYPE_NONE;
ck_assert_int_eq(fs_open(&vol, &f, "/boot/x", 1024),
WOLFBOOT_FS_E_PARAM);
}
END_TEST
START_TEST(test_open_null_args)
{
struct fs_volume vol;
struct fs_file f;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(NULL, &f, "/x", 16), WOLFBOOT_FS_E_PARAM);
ck_assert_int_eq(fs_open(&vol, NULL, "/x", 16), WOLFBOOT_FS_E_PARAM);
}
END_TEST
START_TEST(test_read_null_args)
{
uint8_t buf[4];
struct fs_volume vol;
struct fs_file f;
build_disk();
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
ck_assert_int_eq(fs_read(NULL, 0, 4, buf), WOLFBOOT_FS_E_PARAM);
ck_assert_int_eq(fs_read(&f, 0, 4, NULL), WOLFBOOT_FS_E_PARAM);
ck_assert_uint_eq((unsigned int)fs_size(NULL), 0);
}
END_TEST
static Suite *wolfboot_suite(void)
{
Suite *s = suite_create("wolfboot-fs-probe");
TCase *tc_probe = tcase_create("probe");
TCase *tc_raw = tcase_create("raw");
TCase *tc_io = tcase_create("bounded-io");
TCase *tc_cache = tcase_create("cache");
TCase *tc_path = tcase_create("path");
TCase *tc_cov = tcase_create("fs-coverage");
tcase_add_test(tc_probe, test_probe_wolfboot_magic_is_raw);
tcase_add_test(tc_probe, test_probe_magic_wins_over_boot_signature);
tcase_add_test(tc_probe, test_probe_empty_partition_is_raw);
tcase_add_test(tc_probe, test_probe_garbage_is_raw);
tcase_add_test(tc_probe, test_probe_bad_partition_index);
tcase_add_test(tc_probe, test_probe_null_volume);
suite_add_tcase(s, tc_probe);
tcase_add_test(tc_raw, test_raw_open_size_is_partition_size);
tcase_add_test(tc_raw, test_raw_read_matches_disk_part_read);
tcase_add_test(tc_raw, test_raw_read_past_eof_returns_zero);
tcase_add_test(tc_raw, test_raw_read_straddling_eof_is_clamped);
tcase_add_test(tc_raw, test_raw_read_zero_length);
tcase_add_test(tc_raw, test_raw_label_never_matches);
tcase_add_test(tc_raw, test_type_name);
suite_add_tcase(s, tc_raw);
tcase_add_test(tc_io, test_read_exact_refuses_partition_end_clamp);
tcase_add_test(tc_io, test_read_exact_io_error);
tcase_add_test(tc_io, test_read_exact_null_and_zero);
suite_add_tcase(s, tc_io);
tcase_add_test(tc_cache, test_meta_read_spans_cache_windows);
tcase_add_test(tc_cache, test_meta_read_is_cached);
tcase_add_test(tc_cache, test_meta_cache_keyed_by_partition);
tcase_add_test(tc_cache, test_meta_read_short_tail_window);
tcase_add_test(tc_cache, test_meta_read_out_of_range);
suite_add_tcase(s, tc_cache);
tcase_add_test(tc_path, test_path_split_basic);
tcase_add_test(tc_path, test_path_split_collapses_slashes);
tcase_add_test(tc_path, test_path_backslash_is_not_a_separator);
tcase_add_test(tc_path, test_path_rejects_dot_and_dotdot);
tcase_add_test(tc_path, test_path_rejects_excess_depth);
tcase_add_test(tc_path, test_path_rejects_long_component);
tcase_add_test(tc_path, test_path_empty);
tcase_add_test(tc_path, test_path_next_null_args);
suite_add_tcase(s, tc_path);
tcase_add_test(tc_cov, test_disk_part_size);
tcase_add_test(tc_cov, test_disk_part_count);
tcase_add_test(tc_cov, test_le_accessors);
tcase_add_test(tc_cov, test_open_rejects_bad_type);
tcase_add_test(tc_cov, test_open_null_args);
tcase_add_test(tc_cov, test_read_null_args);
suite_add_tcase(s, tc_cov);
return s;
}
int main(void)
{
int fails;
Suite *s = wolfboot_suite();
SRunner *sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
fails = srunner_ntests_failed(sr);
srunner_free(sr);
return fails;
}