Skip to content
FreeDOSDeep Dive Published Updated 11 min readViews unavailable

FreeDOS FAT32 FSInfo: Parse Allocation Hints Safely

Inspect FAT32 FSInfo signatures and allocation hints, validate BPB bounds, distinguish stale counters from FAT state, and preserve evidence.

FAT32’s FSInfo sector is easy to over-trust because it contains numbers that look authoritative: a free-cluster count and a next-free-cluster hint. The Microsoft FAT specification describes them as last-known values, and 0xFFFFFFFF means a value is unknown. The allocation table remains the structure that records whether each data cluster is free or allocated. FSInfo can accelerate a search; it cannot prove that the volume is healthy or that a displayed free-space total is current.

This is a focused format and triage guide, not a replacement for the broader FAT32 overview. Its goal is to help a FreeDOS operator or image-analysis tool locate the structure, reject malformed offsets, preserve the original bytes, and decide when a full FAT scan is required.

Locate the sector from the BPB, not from a customary layout

The FAT32 BIOS Parameter Block is in the volume’s boot sector. Its BPB_FSInfo field is a 16-bit sector number relative to the beginning of that volume. Many formatters place FSInfo in sector 1, but a parser should read the field rather than hard-code that convention. If the volume is inside a partitioned disk image, the byte offset for the FSInfo sector is:

partition_start_bytes + BPB_FSInfo * BPB_BytsPerSec

Do not confuse the partition’s starting LBA with a byte offset, or the volume-relative sector number with an absolute disk LBA. A tool that mixes those units can read a convincing-looking but unrelated sector.

The standard FSInfo fields occupy the first 512 bytes of the sector:

Byte offset Size Field Meaning
0x000 4 Lead signature 0x41615252
0x004–0x1E3 480 Reserved Reserved space
0x1E4 4 Structure signature 0x61417272
0x1E8 4 Free-cluster count Last known count, or 0xFFFFFFFF if unknown
0x1EC 4 Next-free hint Suggested search position, or 0xFFFFFFFF if unknown
0x1F0–0x1FB 12 Reserved Reserved space
0x1FC 4 Trail signature Little-endian 0xAA550000

These are little-endian 32-bit values. The trail signature’s byte sequence is 00 00 55 AA; it is not the same field as the boot sector’s two-byte 55 AA marker at offsets 510–511. Checking one signature does not validate the other structure.

Perform a bounded, read-only inspection first

The following Python example reads a disk image without opening it for writing. Pass the partition start as a byte offset; use 0 only when the image begins directly with the FAT volume boot sector. The script validates basic bounds and the three FSInfo signatures, then prints the hint fields. It intentionally does not declare the filesystem consistent: confirming the exact free-cluster count requires scanning the FAT entries for free clusters; checking cluster chains is a separate filesystem-consistency task.

#!/usr/bin/env python3
import struct
import sys
from pathlib import Path

if len(sys.argv) != 3:
    raise SystemExit(f"usage: {sys.argv[0]} IMAGE PARTITION_START_BYTES")

image = Path(sys.argv[1])
base = int(sys.argv[2], 0)
size = image.stat().st_size
if base < 0 or base + 512 > size:
    raise SystemExit("volume boot sector is outside the image")

with image.open("rb") as disk:
    disk.seek(base)
    boot = disk.read(512)
    if len(boot) != 512 or boot[510:512] != b"\x55\xaa":
        raise SystemExit("missing complete boot-sector prefix or 55AA marker")

    bytes_per_sector = struct.unpack_from("<H", boot, 11)[0]
    sectors_per_cluster = boot[13]
    reserved_sectors = struct.unpack_from("<H", boot, 14)[0]
    fat_count = boot[16]
    root_entry_count = struct.unpack_from("<H", boot, 17)[0]
    total_sectors_16 = struct.unpack_from("<H", boot, 19)[0]
    fat_size_16 = struct.unpack_from("<H", boot, 22)[0]
    total_sectors_32 = struct.unpack_from("<I", boot, 32)[0]
    fat_size_32 = struct.unpack_from("<I", boot, 36)[0]
    fsinfo_sector = struct.unpack_from("<H", boot, 48)[0]
    if bytes_per_sector not in (512, 1024, 2048, 4096):
        raise SystemExit(f"unsupported logical sector size: {bytes_per_sector}")
    if (sectors_per_cluster == 0 or sectors_per_cluster > 128 or
        sectors_per_cluster & (sectors_per_cluster - 1) or
        bytes_per_sector * sectors_per_cluster > 32768):
        raise SystemExit("cluster geometry exceeds this parser's conservative FAT32 cluster-size policy")
    if (fat_count == 0 or root_entry_count != 0 or
        fat_size_16 != 0 or fat_size_32 == 0):
        raise SystemExit("BPB does not match the expected FAT32 layout")
    if total_sectors_16 != 0 or total_sectors_32 == 0:
        raise SystemExit("BPB does not use the expected FAT32 total-sector field")
    total_sectors = total_sectors_32
    first_data_sector = reserved_sectors + fat_count * fat_size_32
    if total_sectors <= first_data_sector:
        raise SystemExit("BPB data region is empty or outside the declared volume")
    if base + total_sectors * bytes_per_sector > size:
        raise SystemExit("declared volume extends beyond the image")
    cluster_count = (total_sectors - first_data_sector) // sectors_per_cluster
    if cluster_count < 65525:
        raise SystemExit("cluster count does not classify this volume as FAT32")
    # Fastfat classifies masked FAT entries 0x0FFFFFF0-0x0FFFFFF6 as
    # reserved; 0x0FFFFFF7 is bad. Exclude that range from this profile's
    # data-cluster IDs (see the discussion below).
    if cluster_count + 1 > 0x0FFFFFEF:
        raise SystemExit("data-cluster range exceeds this Fastfat-compatible profile")
    fat_entry_capacity = (fat_size_32 * bytes_per_sector) // 4
    if fat_entry_capacity < cluster_count + 2:
        raise SystemExit("FAT32 allocation table is too small for the declared data-cluster count")
    if reserved_sectors == 0 or fsinfo_sector in (0, 0xFFFF):
        raise SystemExit("BPB does not identify an FSInfo sector")
    if fsinfo_sector >= reserved_sectors:
        raise SystemExit("FSInfo sector is outside the reserved volume region")

    offset = base + fsinfo_sector * bytes_per_sector
    if offset + bytes_per_sector > size:
        raise SystemExit("FSInfo sector is outside the image")
    disk.seek(offset)
    sector = disk.read(bytes_per_sector)
    if len(sector) != bytes_per_sector:
        raise SystemExit("truncated FSInfo sector")

lead = struct.unpack_from("<I", sector, 0)[0]
structure = struct.unpack_from("<I", sector, 484)[0]
free_count = struct.unpack_from("<I", sector, 488)[0]
next_free = struct.unpack_from("<I", sector, 492)[0]
trail = struct.unpack_from("<I", sector, 508)[0]

print(f"volume byte offset: {base}")
print(f"FSInfo relative sector: {fsinfo_sector}; byte offset: {offset}")
print(f"FAT32 data clusters from BPB geometry: {cluster_count}")
print(f"signatures: lead={lead:#010x} structure={structure:#010x} trail={trail:#010x}")
print(f"free-count hint: {free_count:#010x}; next-free hint: {next_free:#010x}")
if (lead, structure, trail) != (0x41615252, 0x61417272, 0xAA550000):
    raise SystemExit("signature mismatch: preserve evidence and do not repair in place")
if free_count != 0xFFFFFFFF and free_count > cluster_count:
    raise SystemExit("free-count hint exceeds the BPB data-cluster count")
if next_free != 0xFFFFFFFF and not (2 <= next_free <= cluster_count + 1):
    raise SystemExit("next-free hint is outside the BPB data-cluster range")

The 512-byte boot-sector read is sufficient for the BPB fields shown, while the FSInfo read uses the sector size reported by that BPB. The sample checks the boot marker, expected FAT32 layout, volume extent within the image, FAT32 data-cluster-number limit, minimum FAT entry capacity, cluster-count class, and FSInfo boundaries. It does not receive the containing partition’s length, so an in-image extent check does not prove that a partitioned volume fits inside its partition. It also does not validate every FAT32 BPB invariant, partition-table entry, FAT mirror contents, or cluster-chain entry. Extend a forensic parser with explicit checks for those structures before using it as an automated recovery tool. Keep the image read-only and record the image hash, selected byte offset, and parser version alongside the output.

The sample combines specification rules with deliberately conservative parser bounds. The minimum cluster count of 65,525 is the FAT32 type-classification threshold, but the specification cautions formatters to stay at least 16 clusters away from the 4,085 and 65,525 cut-over values when choosing a FAT type. The upper check is specifically a Windows Fastfat compatibility profile: FatInterpretClusterType masks 32-bit entries with FAT32_ENTRY_MASK, treats zero as available, classifies nonzero values below 0x0FFFFFF0 as next-cluster links, reserves 0x0FFFFFF0–0x0FFFFFF6, marks 0x0FFFFFF7 bad, and treats higher values as end-of-chain. This parser therefore caps data-cluster numbers at 0x0FFFFFEF to keep them below Fastfat’s reserved range. Do not treat that stricter ceiling as a universal FAT32 limit. The FAT32 1.03 specification defines the maximum valid cluster number as CountofClusters + 1 and says 0x0FFFFFF7, the bad-cluster marker, must not be allocatable. Linux v6.5 defines MAX_FAT32 as 0x0FFFFFF6 in a comment describing the maximum number of clusters; its mount code compares total_clusters to that limit. That is a Linux implementation count limit, not a normative definition of the greatest cluster ID. A parser targeting another implementation should choose and document its own compatibility policy rather than silently inheriting Fastfat’s stricter cutoff. The cited Fastfat files are Microsoft’s published driver samples, so treat the behavior as sample-implementation evidence rather than proof about every Windows release. The capacity check is a structural derivation: a FAT with four-byte entries must contain slots for reserved entries 0 and 1 plus every data-cluster number through cluster_count + 1.

Interpret the values as hints, not as allocation truth

The free count may be exact, stale, or unknown. Treat 0xFFFFFFFF as “not supplied,” not as a count of four billion clusters. Any other value is only a candidate until it is checked against the volume’s data-cluster range and a scan of the allocation table. A count larger than the volume can contain is inconsistent; a plausible number can still be stale. A FAT32 scan must interpret each raw entry through its low 28 bits (raw_entry & 0x0FFFFFFF): the specification reserves the upper four bits, says readers ignore them, and requires writers to preserve them when updating an entry.

The next-free value is a search hint, not a cluster chain and not a reservation. An implementation must validate it against the volume’s legal data-cluster range before using it. A hint that points to an occupied cluster can be handled by consulting the FAT and continuing the search; it must never authorize overwriting that cluster. The FreeDOS kernel source illustrates this distinction: its FAT32 allocation path starts from a remembered cluster when available, tests entries in the FAT, wraps its search, and describes the FSInfo entry as a hint. Its current source also range-checks the cached values it reads. Those are implementation details of the reviewed kernel revision, not universal promises for every DOS implementation.

Do not compare the FSInfo count with an operating-system free-space display and declare one authoritative based only on agreement. Different filesystems, caches, and tools can report from different snapshots. For a forensic or recovery decision, preserve the raw sector, inspect the FAT copies and mirroring policy indicated by BPB_ExtFlags, and run an independent checker on a separate copy. A clean check means only that the chosen checker accepted the structures it understands; it does not prove every file’s contents are intact.

Repair only after preserving a baseline

For the conservative recovery policy used here, stop before writing if the signatures are wrong, the reserved-region bounds fail, the next-free hint is outside the legal cluster range, or a non-unknown free-cluster count disagrees with a verified FAT scan. The specification describes FSInfo values as last-known hints and permits an unknown value; the stop-before-write rule is a safeguard for this workflow, not a claim that the format specification mandates one repair response. An in-range next-free hint may identify an allocated cluster because it marks where allocation search should begin; it is not itself a claim that the cluster is free. Acquire or duplicate the image, hash it, and log the BPB values and sector bytes. Then use a FAT32-aware repair tool whose documented mode and target are understood, on a disposable working copy. Do not “fix” a stale FSInfo hint by writing a guessed free count, setting the hint to cluster 2, or copying a backup boot sector over the primary record. 0xFFFFFFFF is the specification-defined unknown value for either hint; a filesystem tool may deliberately store it when the value is not known, but an offline forensic pass should preserve the original and make no such change to its evidence copy. The specification describes restoring a boot record from its backup when the primary boot record was overwritten; that is a different recovery case. Validate the backup and volume geometry, preserve the original evidence, and perform any justified restoration only on a working copy. These distinctions prevent a plausible-looking FSInfo sector from concealing a damaged boot record or FAT.

For a maintained production volume, cleanly unmount it before offline repair, use a tested backup, and verify the whole filesystem after the repair. If repair is performed by a filesystem tool, retain its version, command line, exit status, and before/after images. FSInfo is a cache-like aid to allocation performance; a trustworthy recovery decision comes from validated volume geometry and the allocation structures that actually own cluster state.

Related:

Sources:

Comments