# FAT recovery: agreement between readers still needs content identity

Two independent FAT readers returned the same payloads from all 80 constructed
images, including all 12 deliberately rebound files. Their agreement therefore
does not establish that an original pathname still identifies its original
contents. Meanwhile, eight reversed long-name slot sequences produced different
visible names without changing the readable payload.

This extends the [existing association study](../README.md). It is a native
userspace experiment on ordinary files, not a damaged-media population study,
security audit, new vulnerability, or independent peer review.

## What was actually compared

The unchanged GNU mtools 4.0.49 harness regenerated its 80 FAT12/FAT16 cases.
Every original observation and image hash matched the frozen parent capture
before the second reader ran. For each image, FatFs enumerated the root,
opened both enumerated files, and attempted the original target spelling and
current short alias. Full image hashes before and after matched.

The second implementation is FatFs R0.16 patch-2 from the Zephyr-maintained
tree at commit [4f60a12](https://github.com/zephyrproject-rtos/fatfs/tree/4f60a12c58c3de4246857ad87c40239ecd93349b).
Its own disk adapter is **not** used: the small Hesela adapter permits bounded
reads from regular image files only. This is not Zephyr running on a device.

The build enables long names with the UTF-8 API, 255-code-unit working buffer,
255-byte output-name buffer, CP437 and read-only operation. Names in this study
are ASCII and at most 40 characters. The source files are unchanged;
[source-lock.json](source-lock.json) records all compiler-input hashes.
Two builds, optimized and undefined-behavior-sanitized, produced identical
observations. Their receipts identify the actual binaries and collection times.

## Results

[results.csv](results.csv) is generated by [table.py](table.py), not transcribed.

<!-- generated-results:start -->
| Mutation | Cases | Same displayed name as mtools | Original spelling opens in FatFs | Short alias opens |
|---|---:|---:|---:|---:|
| Baseline | 12 | 12 | 12 | 12 |
| Colliding alias | 12 | 12 | 12 | 12 |
| Noncolliding alias | 12 | 12 | 0 | 12 |
| Long-name text edit | 12 | 12 | 2 | 12 |
| First-cluster rebind | 12 | 12 | 12 | 12 |
| Wrong slot checksum | 12 | 12 | 2 | 12 |
| Reversed slots | 8 | 0 | 0 | 8 |
<!-- generated-results:end -->

FatFs returned donor bytes for the 12 cluster rebindings and target bytes for
the other 68 cases, matching mtools. A rebind creates a cross-link deliberately;
it is not a valid rename. The donor directory entry remained readable too.

All eight reversed sequences fell back to the short alias in FatFs, whereas
mtools retained the long name. In FatFs, opening those original long spellings
returned FR_NO_FILE; all eight short aliases still opened. The pinned parser
requires a complete ordered sequence and checksum agreement before displaying
the long name ([ff.c, dir_read](https://github.com/zephyrproject-rtos/fatfs/blob/4f60a12c58c3de4246857ad87c40239ecd93349b/ff.c#L2334)).
The API explicitly permits a short-name fallback when a long name is unavailable
([f_readdir](https://elm-chan.org/fsw/ff/doc/readdir.html)).

## Listing and lookup answer different questions

In four cases, the original spelling still opened after a name edit or checksum
damage: the FAT12 and FAT16 12-character cases use Laaaaaaa.txt, whose uppercase
8.3 alias is LAAAAAAA.TXT. The original lookup can therefore match the unchanged
short entry even when the displayed long name differs. The source has a separate
short-name matching branch ([ff.c, dir_find](https://github.com/zephyrproject-rtos/fatfs/blob/4f60a12c58c3de4246857ad87c40239ecd93349b/ff.c#L2438)).

An initial exploratory validator incorrectly assumed all 44 affected original
lookups would fail. The raw output disproved it: 40 failed and four succeeded
through alias matching. The expected rule was corrected from the observed
aliases and parser source, then both full captures were rerun. Original parent
evidence was never edited. This is a methodology correction, not a FatFs defect.

## Recovery implications and limits

Preserve the image, reader version/configuration, displayed name, attempted
lookup path, file length and payload hash as separate observations. Compare
content to an independently retained manifest when possible. Agreement of two
readers can corroborate current interpretation while still agreeing on a
deliberately wrong historical name-to-content association.

The experiments do not establish failure probabilities or recovery completeness.
They cover the existing 80 designed cases, two FAT variants and two reader
implementations on one host. No FAT32, exFAT, Windows, mounted Linux filesystem,
power interruption, real damaged device or adversarial-image campaign was tested.
The Linux v6.12 comparison in the parent remains source-only.

## Reproduction

The historical mtools 4.0.49 comparator is affected by
[CVE-2026-118384](https://ubuntu.com/security/CVE-2026-118384).
Use only this study's generated fixtures in an isolated, unprivileged environment;
do not process arbitrary untrusted images with that historical binary.
The observations remain version-pinned and do not establish newer-reader behavior.

Obtain the pinned source archive in source-lock.json and extract it separately.
The upstream download host timed out during this session; the exact Zephyr
source commit was downloaded instead. This variant includes official patches
and documented downstream changes. It must not be called pristine upstream R0.16.

From this directory, set SOURCE to the extracted pinned tree and BIN to the
parent study's independently built mtools 4.0.49 directory:

```sh
python3 -B build.py --source "$SOURCE" --output /tmp/hesela-fat-reader
python3 -B compare.py --mtools "$BIN" --reader /tmp/hesela-fat-reader/fatfs-optimized --output optimized.json
python3 -B compare.py --mtools "$BIN" --reader /tmp/hesela-fat-reader/fatfs-ubsan --output ubsan.json
python3 -B table.py
HESELA_FATFS_READER=/tmp/hesela-fat-reader/fatfs-optimized python3 -B -m unittest -v test_compare
HESELA_FATFS_READER=/tmp/hesela-fat-reader/fatfs-ubsan python3 -B -O -m unittest -v test_compare
```

The official [patch notes](https://elm-chan.org/fsw/ff/patches.html) and
[Zephyr source README](https://github.com/zephyrproject-rtos/fatfs/blob/4f60a12c58c3de4246857ad87c40239ecd93349b/README)
were checked before compiling. The retained compiler warning is an unused
downstream oldsize variable in read-only f_lseek; no external source was changed
to silence it. Build commands and stderr are in build-receipt.json.
No third-party source or binary is redistributed.
