113 lines
4.8 KiB
Markdown
113 lines
4.8 KiB
Markdown
# repo22 Cold Nested Namei Regression Report
|
|
|
|
- Date: 2026-08-08 UTC
|
|
- Baseline: `29a215dd4519579f6313b3df67d524a6b4bdf3ca`
|
|
- Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
|
|
- Final module SHA-256:
|
|
`50a19ea96c7414f73d92049671e66a9eacc9ff5abe27950a46d16c8e6c763baf`
|
|
- Result: PASS
|
|
|
|
## Root Cause
|
|
|
|
FreeBSD pathname lookup passes a component as `cn_nameptr` plus
|
|
`cn_namelen`. An intermediate component is followed by `/` in the pathname
|
|
buffer and is not NUL-terminated at `cn_namelen`. The imported Linux EROFS
|
|
comparison assumed Linux dentry-name termination, ignored the supplied length,
|
|
and tested `qn_name[i] == '\0'` after matching the on-disk name. Therefore a
|
|
final component worked, while the same name used as an intermediate component
|
|
compared greater than the on-disk entry and returned `ENOENT`. Looking up the
|
|
parent as a final component first populated the FreeBSD namecache and hid the
|
|
bug on the next nested lookup.
|
|
|
|
The old error path also inserted a negative cache entry for every lookup
|
|
error, including integrity and I/O errors, which could mask later corruption
|
|
as `ENOENT`.
|
|
|
|
## Implementation
|
|
|
|
- `src/namei.c`
|
|
- Compares pathname components by explicit length without reading beyond
|
|
`cn_namelen`.
|
|
- Uses unsigned-byte ordering compatible with EROFS directory sorting.
|
|
- Validates the minimum block size before reading the first dirent.
|
|
- Validates the dirent-array boundary, strictly increasing name offsets,
|
|
name-slot bounds, name length, and zero-only NUL padding.
|
|
- Inserts negative namecache entries only for real `ENOENT` misses.
|
|
- `src/dir.c`
|
|
- Applies the same directory-block and name-padding validation to `readdir`.
|
|
- Determines the actual last-name length before enforcing `EROFS_NAME_LEN`,
|
|
so valid full-block zero padding is accepted.
|
|
- Keeps on-disk offsets unchanged and appends a synthetic `.` at `i_size`
|
|
for `dot_omitted`, matching Linux EROFS.
|
|
- Aligns restart positions relative to each directory block and preserves
|
|
the `i_size` cookie needed to resume the synthetic dot entry.
|
|
- Initializes returned cookie-array outputs before allocation.
|
|
|
|
## Deterministic Fixture
|
|
|
|
`prepare-fixtures.sh` creates the same image twice and requires `cmp` success.
|
|
The base image contains a cold multi-level path and a 320-file, multi-block
|
|
directory. Fixed timestamp, ownership, UUID, worker count, and uncompressed
|
|
layout are used.
|
|
|
|
Final image hashes:
|
|
|
|
```text
|
|
2e9fd75159011ced31646a38f942fa0822d5b3e8e19b7df55b844575fba991ea corrupt-nameoff.erofs
|
|
63d12232f9ea0dc7f7ff477d20b95a8412d191fc10bf4b67dacc47e050c379fb corrupt-padding.erofs
|
|
77f8a56f969e173d291ccbd957821f1ba284d3e54d875dbcdb9bc398024dfa5a corrupt-short-block.erofs
|
|
11064ffbb814ff41027aebc8f36e56d2f24affb7b50836948b3ffbf6d79dee0e dot-omitted.erofs
|
|
7a2c244ec10e9a43531b0e8b92e26b11f52d67bab00528b8ba2f584e20e57420 nested-repeat.erofs
|
|
7a2c244ec10e9a43531b0e8b92e26b11f52d67bab00528b8ba2f584e20e57420 nested.erofs
|
|
```
|
|
|
|
## Build Results
|
|
|
|
- `./build.sh`: PASS.
|
|
- Final `build/erofs.ko` SHA-256 remained
|
|
`50a19ea96c7414f73d92049671e66a9eacc9ff5abe27950a46d16c8e6c763baf`.
|
|
- `nm -u build/erofs.ko | grep -w bcmp`: no match.
|
|
- `git diff --check` for all scoped source and test files: PASS.
|
|
|
|
The final integration rerun used the same combined module and these guest
|
|
commands:
|
|
|
|
```sh
|
|
cd /root/repo22-namei
|
|
cc -Wall -Wextra -O2 -o readdir_probe readdir_probe.c
|
|
./vm-regression.sh
|
|
```
|
|
|
|
`vm-regression.sh` performs `kldload`, creates each vnode-backed md device,
|
|
mounts it with `mount -t erofs`, executes the cold lookup and readdir probes,
|
|
unmounts and detaches each image, and finishes with exact module unload.
|
|
|
|
## FreeBSD VM Results
|
|
|
|
- `kldload`: PASS.
|
|
- Cold direct read of
|
|
`/alpha/bravo/charlie/payload.txt` without parent lookup or `readdir`: PASS.
|
|
- Repeated lookup and sibling nested lookup: PASS.
|
|
- Two negative lookups followed by an existing nested lookup: PASS.
|
|
- Multi-block `wide` readdir: 322 dirents (`.`, `..`, 320 files), PASS.
|
|
- Resume from every one of the 322 returned `d_off` cookies: PASS.
|
|
- `dot_omitted` root cookies: `12`, `24`, `47`, `48`; resume at `47`
|
|
returns only `.`, and resume at `48` returns EOF: PASS.
|
|
- Short directory block: two lookups and direct `getdirentries` all return
|
|
`EINTEGRITY`, PASS.
|
|
- Non-monotonic `nameoff`: two lookups and direct `getdirentries` all return
|
|
`EINTEGRITY`, PASS.
|
|
- Nonzero data after NUL padding: two lookups and direct `getdirentries` all
|
|
return `EINTEGRITY`, PASS.
|
|
- `kldunload`: PASS.
|
|
- Post-test EROFS module, mount, and md-device state: clean.
|
|
- Final explicit state check: zero EROFS modules, zero EROFS mounts, zero md
|
|
devices, and no recent panic or fatal trap in dmesg.
|
|
|
|
## Remaining Scope
|
|
|
|
No unresolved issue remains for the requested cold lookup and directory
|
|
regression. The NFS-specific `a_cookies` consumer path was not exercised by an
|
|
NFS export; the tested `d_off` restart-cookie sequence uses the same generated
|
|
cookie values.
|