8.5 KiB
TC153 Large Directory Block Index Validation
Status: RESOLVED - exact-source kernel validation passed
Last updated: 2026-08-09
Priority: Critical validation gap
Implementation status: Source fix present, build-verified, and dynamically validated
Problem
erofs_find_target_block() used signed int values for the binary-search
front, back, and midpoint indexes. A directory whose logical size describes
2147483649 4 KiB blocks produces a final block index of 2147483648.
Converting that value to int can make the initial back bound negative. The
search can then return ENOENT without reading any directory block, and the
FreeBSD namecache can retain a false negative result for corrupt media.
The review change uses uint64_t for block-search bounds and midpoints, checks
the midpoint multiplication, and handles the mid == 0 lower-bound case. The
within-block search remains uint32_t because a validated EROFS directory
block can contain only a block-sized number of entries.
Affected Features
- Cold pathname lookup in very large or maliciously sized directories.
- Corruption reporting and
EINTEGRITYpropagation from directory reads. - Negative namecache correctness after a failed lookup.
- Linux/FreeBSD behavioral review of EROFS's two-level directory search.
Normal directories are not expected to approach this bound. The practical
risk is a crafted image causing an incorrect ENOENT, not an ordinary mkfs
image losing entries.
Trigger Conditions
All of the following are needed to exercise the original width error:
- A directory inode is accepted with a logical block count greater than
INT_MAX. - The directory layout reaches
erofs_find_target_block()rather than being rejected by an earlier inline-data bounds check. - A cold lookup is issued for a name that is not satisfied by an already cached vnode or negative namecache entry.
- The backing image is too short for the computed midpoint read, so the fixed
code should return
EINTEGRITYinstead of a synthetic miss.
For 4 KiB blocks, TC153 encodes a size of 8796093026304 bytes,
2147483649 blocks, and a final index of 2147483648.
Current Analysis
The source change is mechanically narrow and has passed both repo22 build
configurations. The checked multiplication protects a future block-size or
index change even though the current OFF_MAX inode limit already bounds the
product. The mid == 0 guard prevents unsigned subtraction from wrapping when
the search moves below the first block.
Dynamic proof must show more than a large st_size. It must prove that the
lookup enters the block binary search. An image rejected in
erofs_read_inode() is not evidence for this fix, and an ENOENT observed
after a warm negative cache is ambiguous.
Attempts and Results
Attempt 1: Small FLAT_INLINE image
- Artifact directory:
/work/build/repo22-review-artifact.LtqNxH - Base image:
large-dir-base.erofs, 4096 bytes, SHA256973d2a1c90ac1bf776ed76a1fc6a7e88b2156fac3bf04ddbae19cfafebbd562e - Patched image:
large-dir-intmax.erofs, 4096 bytes, SHA256ce5cad3eff34ea50ca89eedf93d8ef90e6cea96858a138685fae45f39084de1f - Recorded inode: NID 40 at byte 1280.
The directory was Layout 2 (FLAT_INLINE). Patching only i_size made its
declared inline tail range invalid, so inode validation rejected it before
erofs_find_target_block(). This attempt is invalid as TC153 dynamic evidence
and must never be marked PASS.
Attempt 2: More entries to force block data
- Artifact directory:
/work/build/repo22-review-artifact2.FmMTup - Generated base:
large-dir-base.erofs, 65536 bytes, SHA25650eb28351788ab1801408aca0a6fca6352755f5ca56efe6e2bb3e2fe08305663 - Directory: NID 40, 21052 bytes, still Layout 2.
The generation process was interrupted before producing a patched image or a
FreeBSD result. A later deterministic rerun reproduced the same base hash and
showed why the approach failed: erofs-utils 1.8.6 tail-packs directories even
when they span multiple data blocks. Its noinline_data option does not turn
directory tail packing off.
Attempt 3: Follow-up agents
Multiple follow-up agents were assigned the fixture and guest validation. They either stopped while preparing the second image or returned no executed commands, files, hashes, or guest output. These attempts added no evidence and are recorded to prevent their elapsed time from being mistaken for progress.
Attempt 4: Sparse-provider design
A guest-side sparse vnode provider was considered so a valid directory could declare the complete multi-terabyte range. It was not executed. Such a scheme must prove the GEOM media size, avoid materializing terabytes, preserve valid initial directory blocks, and still force a cold read at the binary-search midpoint. No result exists for this approach.
Attempt 5: Reproducible FLAT_PLAIN conversion
The tracked generator now copies all 21052 bytes of the generated directory
to appended blocks, changes only that inode to Layout 0 (FLAT_PLAIN), updates
the image block count and CRC32C, and then applies the large logical size.
Host reproduction on 2026-08-09 produced:
large-dir-base.erofs: 65536 bytes, SHA25650eb28351788ab1801408aca0a6fca6352755f5ca56efe6e2bb3e2fe08305663large-dir-intmax.erofs: 90112 bytes, SHA2560f90d3d57adbbbd946e41b225c1f6c464915c6abb0b13478ec9b2a318def1f72- Patched inode: NID 40 at byte 1280, Layout 0, raw block 16.
- Declared image blocks: 22.
This resolved the fixture-construction problem. The qualified kernel run is recorded below.
Attempt 6: Qualified sparse GEOM provider
The G3 takeover generated a checksum-valid sparse prefix from Attempt 5 and performed one targeted FreeBSD 15 run against the exact source requested by the assignment.
- Source commit: 9ae22009f23a65320730072a780998e80aa9b728.
- KLD SHA256: 19ad086bd2508cbb4c57b1a51f38c93057d438b84fbcfa2e637ff2519f5bc590.
- Sparse-prefix SHA256: f9337f83b1f568f6d904331a7749e6362a691055cd5af95b59bc89772f04e3b0.
- Prefix qualification: NID 40, inode offset 1280, extended Layout 0, start block 16, 2147483649 directory blocks, final block index 2147483648, valid superblock checksum.
- Sparse provider: 8796093091840 logical bytes, 448 allocated 512-byte sectors, and the first 90112 bytes retained the prefix SHA256.
- GEOM diskinfo size: 8796093091840 bytes.
- Mounted /huge: size 8796093026304, NID 40.
The first two cold stat operations for /huge/missing each returned EINTEGRITY (97). Root readdir then returned three complete entries and validated all kernel/libc restart cookies. A third lookup after that readdir again returned EINTEGRITY. No lookup returned ENOENT, so no false negative namecache entry masked the corruption.
Unmount, md detach, sparse-provider removal, and KLD unload all succeeded. The only new dmesg lines in the complete G3 run were expected SIGBUS child exits from the unrelated assigned mmap tests; TC153 added no diagnostic.
Historical Remaining Validation
Attempt 6 completed steps 1-5 and 7 below. The assignment mandated the exact fixed source, so the optional pre-fix comparison in step 6 was not run.
- Regenerate the images with
tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.shand verify the hashes and Layout 0 assertion. - Load the exact module under test in a clean FreeBSD 15 guest.
- Mount the 90112-byte image and confirm
/hugereports8796093026304bytes. - With a cold parent cache, run two lookups of
/huge/missingand capture direct command exit codes plustrusserrno. - Require
EINTEGRITYon every fixed-module lookup, including after root readdir. Confirm no negative cache changes the result. - Repeat with the pre-fix module to demonstrate the erroneous
ENOENTpath. - Record dmesg, mount/md/KLD cleanup, module and fixture hashes in a dated manual report.
Original Acceptance Criteria
TC153 can move from SHELVED to PASS only after the fixed FreeBSD KLD returns
EINTEGRITY repeatedly from the valid Layout 0 fixture, the pre-fix behavior
is distinguished, no trap or panic occurs, and complete cleanup evidence is
recorded. Host fixture generation and static source review alone are
insufficient.
Resolution
TC153 is PASS because the exact fixed FreeBSD KLD returned EINTEGRITY repeatedly from the valid Layout 0 fixture before and after root readdir, no trap or panic occurred, and complete hash/provider/cleanup evidence is recorded in tests/results/manual/2026-08-09T1059Z-g3/manual-test-report.md. Host fixture generation and static source review alone remain insufficient.