# FAT long names: alias agreement does not establish content identity

A recovered filename can look convincing while referring to different bytes.
Controlled ordinary-file images show three distinct issues: a colliding short
alias preserves the long name; the long-name text is outside the alias checksum;
and a first-cluster edit changes the returned contents without changing that name.

This is a source synthesis and native userspace experiment, not a new
vulnerability claim, field failure study or physical-disk recovery test.

## What the checksum establishes

The FAT format associates adjacent long-name slots with the following short
entry using an 8-bit checksum of its 11 raw alias bytes. The short entry holds
the data locator and size. The specification already acknowledges alias
collisions and possible misassociation after deletion/reuse (pp. 27-29, 33).
Thus a matching value is only part of directory interpretation, not a
cryptographic binding between path and payload. [1]

This complements Hesela's [FAT cluster chain](https://hesela.dev/terms/fat-cluster-chain/)
and [checksum](https://hesela.dev/terms/checksum/) entries: following allocation
links and recognizing a directory name answer different questions.

## Executed experiment

GNU mtools 4.0.49 created 12 independent base images: FAT12 and FAT16, each with
ASCII filename lengths 12, 13, 14, 26, 27 and 40. These lengths straddle the
13-code-unit slot boundary. Each image contains one target and one equal-length
donor. No device was mounted, and no block-device path was accessed.

The harness normalized directory timestamps before applying one mutation at a
time. Each mutated image was enumerated with mdir and the resulting target name
read with mtype. It recorded exact byte edits, aliases, checksum values, slot
ordinals, payload hashes, exit codes and image hashes. Readers left image bytes
unchanged. A second full run reproduced all recorded values exactly.

| Mutation | Cases | Observed name | Observed payload |
|---|---:|---|---|
| Unmodified baseline | 12 | Original long name | Target |
| Replace short alias with a colliding alias | 12 | Original long name | Target |
| Replace short alias with a noncolliding alias | 12 | New short alias | Target |
| Edit first long-name character from L to X | 12 | Edited long name | Target |
| Redirect target first cluster to donor cluster | 12 | Original long name | Donor |
| Flip checksum bit in every long-name slot | 12 | Original short alias | Target |
| Reverse physical order of long-name slots | 8 | Original long name | Target |

The rebind deliberately creates a cross-linked allocation. It demonstrates
coverage limits, not a valid rename, allocator bug or naturally occurring event.
The table's counts are designed test cases, not estimates requiring confidence
intervals.

Concrete collision: raw aliases `LAAAAA~1TXT` and `F0001108TXT` both yield 37
(0x25). In FAT12-14, changing only the alias preserved `Laaaaaaaaa.txt`.
The noncolliding `F0000000TXT` yields 120 and exposed `F0000000.TXT`.

## The 1/256 shortcut depends on the population

The exact enumeration covers all 100,000 distinct aliases from `F0000000TXT`
through `F0099999TXT`, inclusive. It is an artificial but explicitly defined
ASCII family, not an estimate of filenames in use.

All 256 checksum values occur, but their bucket sizes range from 24 to 686.
For fixed target checksum 37, there are 625 matches. A uniformly selected alias
from this family therefore matches with probability 625/100000 = 0.625%,
not 1/256 = 0.390625%. Neither number is an on-device corruption rate.
An intentional selection, different alias generator or different error process
requires a different model.

Python and an independently written C arithmetic implementation agree on every
histogram bucket. Both optimized C and an undefined-behavior-sanitized C build
produced the same counts. The checksum test also enumerates all 2,805 single-byte
substitutions of `EXAMPLE TXT`; none retain that original checksum. This check
includes arbitrary byte values, not only legal filenames.

## Reader acceptance is a separate boundary

The mtools source explicitly accumulates slots by their ordinal and tests
presence plus alias checksum; its historical comment explains tolerance of
out-of-order slots. All eight multi-slot reversals were accepted in this native
run. [2]

Linux v6.12's `fat_parse_long` instead requires a leading final-slot flag and a
descending slot sequence. This is a source-level contrast for these fixtures,
not a claim that Linux was mounted or that all repair tools agree. [3]

Consequently, acceptance by one reader cannot serve as a format-conformance
oracle. Preserve the original directory bytes and reader version when reporting
recovery. Validate expected paths, lengths and content hashes against an
independently retained manifest when one exists. Without such external evidence,
report which association was reconstructed rather than asserting historical
identity. These are engineering implications of the coverage and reader
differences, not measured recovery-success rates.

## Reproduce

Safety note (checked 2026-11-07): Ubuntu records an out-of-bounds write in
mtools through 4.0.49 in
[CVE-2026-118384](https://ubuntu.com/security/CVE-2026-118384).
Use this historical version only with the generated study fixtures in an isolated,
unprivileged environment. Do not feed it arbitrary untrusted images or use it as
an operational recovery recommendation. The retained observations are unchanged;
no behavior of a newer release is inferred.

Requirements: Python 3.10+, a C compiler, make, and a separately obtained
GNU mtools 4.0.49 source release. No third-party source or binary is redistributed
here. Source URLs, SHA-256 values and locations are in [sources.json](sources.json).

Build mtools in its own extracted directory:

```sh
./configure --disable-floppyd --without-x
make -j2
```

From this packet directory, point BIN at that build:

```sh
python3 -B experiment.py --bin-dir "$BIN" --output capture-1.json
python3 -B experiment.py --bin-dir "$BIN" --output capture-2.json
gcc -std=c11 -Wall -Wextra -Werror -O2 checksum_reference.c -o checksum-o2
gcc -std=c11 -Wall -Wextra -Werror -O0 -fsanitize=undefined checksum_reference.c -o checksum-ubsan
./checksum-o2 > histogram-o2.json
./checksum-ubsan > histogram-ubsan.json
python3 -B distribution.py --reference histogram-o2.json --reference histogram-ubsan.json --output alias-histogram.json
python3 -B -m unittest discover -s . -p test_experiment.py -v
```

The replay covers the stated program version and environment. It does not test
FAT32, exFAT, Windows, a mounted Linux driver, Unicode edge cases, interrupted
writes or a population of damaged media. Checksums can detect many changes;
these counterexamples identify what their input coverage cannot establish.

## Second-reader extension

The [cross-reader supplement](cross-reader/README.md) executes a pinned FatFs
implementation on the same 80 images. Both readers agree on payloads, including
all 12 deliberate rebindings; eight reversed-slot cases disagree on the visible
name. Four original spellings remain usable through a short alias after long-name
edits or checksum damage. The original captures above remain unchanged.
The supplement records exact reader configuration, two builds, failed-lookup
controls, the exploratory expectation correction and a generated results table.

## Sources

1. [Microsoft FAT specification v1.03](https://www.cs.fsu.edu/~cop4610t/assignments/project3/spec/fatspec.pdf),
   6 December 2000, pp. 27-29 and 33; primary document hosted by a university.
2. [GNU mtools 4.0.49 source release](https://ftp.gnu.org/gnu/mtools/mtools-4.0.49.tar.bz2),
   vfat.c, sum_shortname/check_vfat and parse_vses; GPL-3.0-or-later source.
3. [Linux v6.12 FAT directory parser](https://github.com/torvalds/linux/blob/v6.12/fs/fat/dir.c#L273),
   fat_parse_long, lines 273-327; GPL-2.0-only source.
4. [Linux VFAT documentation](https://www.kernel.org/doc/html/v6.12/filesystems/vfat.html),
   extended directory-entry layout and alias checksum.
