update
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# repo22 Metadata and VFS Manual Test Report
|
||||
|
||||
Started: 2026-08-08 23:37 UTC
|
||||
Completed: 2026-08-09 UTC
|
||||
Baseline: `c208bf1f4b8d7a85777f7fe45e8c6e8d3a9f2d1a`
|
||||
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
|
||||
FreeBSD source reference: `/work/dev-freebsd-releng`, releng/15.0
|
||||
Linux source reference: `/work/dev-src-linux/fs/erofs`
|
||||
Host production tool: erofs-utils 1.8.6
|
||||
|
||||
## Result
|
||||
|
||||
**PASS** for the implemented metadata/VFS changes and every runnable required
|
||||
FreeBSD 15 regression.
|
||||
|
||||
Two limitations are recorded rather than misreported as PASS:
|
||||
|
||||
1. The Linux host had no loop provider/EROFS kernel mount path, so the
|
||||
deterministic nonzero-padding fixture passed Linux erofs-utils 1.8.6
|
||||
`fsck.erofs` and `dump.erofs`, while the Linux kernel mount is
|
||||
**ENVIRONMENT-UNAVAILABLE**.
|
||||
2. A real TC010 48-bit `statfs` mount needs a provider as large as the declared
|
||||
multi-terabyte image. The corrected test records this as a provider/tool
|
||||
requirement; no small-media mount is called a positive PASS.
|
||||
|
||||
An initial NFS stress run completed all data and metadata assertions but failed
|
||||
cleanup because `service nfsd onerestart` inherited the deliberately open NFS
|
||||
client descriptor. The procedure was corrected to run the service command with
|
||||
`3<&-`; the complete stress/background/cleanup sequence then passed. The first
|
||||
cleanup failure is retained here as evidence for the TC132 ordering fix.
|
||||
|
||||
## Baseline and Scope Audit
|
||||
|
||||
- `HEAD` and `FETCH_HEAD` both resolved to the required baseline.
|
||||
- `find /work -name AGENTS.md -type f -print` returned no paths.
|
||||
- Only `repo-community/repo22` was modified.
|
||||
- Existing untracked `build/` objects and historical manual-test artifacts were
|
||||
preserved and excluded from staging.
|
||||
- No CI implementation, binary fixture, overlay, image, or VM artifact is part
|
||||
of the intended commit.
|
||||
|
||||
## Build and ABI Validation
|
||||
|
||||
Commands:
|
||||
|
||||
```sh
|
||||
EROFS_ZSTDIO=0 ./build.sh
|
||||
EROFS_ZSTDIO=1 ./build.sh
|
||||
nm -u module.ko | awk '$NF == "bcmp" { n++ } END { print n + 0 }'
|
||||
git diff --check -- repo-community/repo22
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
| Build | SHA256 | `bcmp` count | Result |
|
||||
|---|---|---:|---|
|
||||
| ZSTDIO disabled | `031038ef195497dc6a1d840d55b293292e051fb888c679c89c8cfbd19b56d525` | 0 | PASS |
|
||||
| ZSTDIO enabled | `16166b9ffc96a532bda9925513e10df11f4cf8a06b3c15afbc09b1a4e4b9d2df` | 0 | PASS |
|
||||
|
||||
Both modules loaded and unloaded on FreeBSD 15. The final unload used the
|
||||
observed KLD ID so arbitrary copied filenames did not affect cleanup.
|
||||
|
||||
FreeBSD 15 source inspection confirmed the exact current ABI:
|
||||
|
||||
```text
|
||||
vnode_pager_local_getpages(struct vop_getpages_args *)
|
||||
vnode_pager_local_getpages_async(struct vop_getpages_async_args *)
|
||||
```
|
||||
|
||||
The local ext2 vnode vector registers both functions directly. EROFS now does
|
||||
the same.
|
||||
|
||||
## Deterministic Fixture Evidence
|
||||
|
||||
The checked-in `prepare-fixtures.sh` was syntax-checked and rerun into separate
|
||||
untracked `repro3-*` directories. It reproduced the expected byte-identical
|
||||
inline, special, pager, NFS, and nlink images and generated the current namei
|
||||
variants.
|
||||
|
||||
Key structural evidence:
|
||||
|
||||
```text
|
||||
inline_nid=39 inode_off=1248 inode_blockoff=1248 xattr_icount=0->695
|
||||
inline_data_blockoff=4068 inline_size=31
|
||||
wide_nid=42 block=6 dirents=80 last_nameoff=4043 padding_patch=4084:4092
|
||||
nlink1_nid=43 i_format_bit4=1 i_nb=0x1234
|
||||
special compact rdev raw_u=0x543abc21
|
||||
special extended rdev raw_u=0x543abc21
|
||||
```
|
||||
|
||||
Generated image hashes:
|
||||
|
||||
| Image | SHA256 |
|
||||
|---|---|
|
||||
| `inline.erofs` | `0435b3ea748a88cccbdc6390dec4285a3706bec3dc09de58aa1808544ebc63d0` |
|
||||
| `inline-cross-block.erofs` | `63ebb7632687b564beb4c9dd8036eb4ac63c4495f63061ad7eec1c134c656932` |
|
||||
| `special-compact.erofs` | `fd78256dd83d9d6d957e5f843c7a8e8a175a4b3243d528bebd299b0226853237` |
|
||||
| `special-extended.erofs` | `e73e9b84d9ceb8c2b07e9c2732733b0fd607736c68c09522a2402fbeef6ba8d5` |
|
||||
| `namei-base.erofs` | `d1730ff23836797c6c09e1b39b1cf23efc16e27f85ab07fdcab577bb82871659` |
|
||||
| `namei-padding-nonzero.erofs` | `9a94e9af2cab264b9c11975a20d78e615d6fe1e6f86267173cb5ac86aecb2b17` |
|
||||
| `namei-corrupt-short.erofs` | `fb89f74795a5569ed3a85d63836dd75a06e1f17823d0508048c37da710ea6e75` |
|
||||
| `namei-corrupt-nameoff.erofs` | `b0b70ee615f163430f04edb91ab76c27ee8cc9a75b8ebb2008834f5b550e3933` |
|
||||
| `namei-corrupt-name.erofs` | `6a980ad3e241603eda2ef71a470c82975291c614366a401e4e89c17c9adf9b91` |
|
||||
| `pager-plain.erofs` | `36596edea5bfbaa1157f6c142095a7ee9949b5df1b1a35620c0d22ec853f3c09` |
|
||||
| `pager-lz4.erofs` | `b06daee6b02a6ebe967655be760b496c8a9d922cf47acb5c840e82a48a34c51d` |
|
||||
| `nfs-a.erofs` | `6d86dcf620b007d069895e3e94a21a74dadcf35e84c92dc4f7c21a2ec23bd901` |
|
||||
| `nfs-b.erofs` | `18913fd319daca20b3e4d30a89c05c416b4d3ca396394509e5112d548674f4c2` |
|
||||
| `nlink.erofs` | `798eb81b3ba7270ee653b00adeba47a6e03c982adef1982bf5b4e0935669ae83` |
|
||||
| `nlink1-patched.erofs` | `19fd85f32ed89117d8e02bc19ca09655dd3bae9152cf693a399eda2135233042` |
|
||||
|
||||
The padding patch was followed by a rigorous CRC32C recomputation over the
|
||||
superblock block. Linux erofs-utils produced the exact payload:
|
||||
|
||||
```text
|
||||
wide entry 079
|
||||
```
|
||||
|
||||
The Linux kernel comparison command failed before mount with “failed to setup
|
||||
loop device”; it is therefore not labeled PASS.
|
||||
|
||||
## TC147: FLAT_INLINE Bounds
|
||||
|
||||
Commands included fresh md attach/mount for the base and cross-block images,
|
||||
then cold `cat`/`stat` access.
|
||||
|
||||
Observed on the final module:
|
||||
|
||||
```text
|
||||
positive hash=0347f272ba395aff6df5fd824a7c552fa26d7f017283f0544136385abef31b01 stat=31 8
|
||||
corrupt stat: Integrity check failed
|
||||
corrupt stat: Integrity check failed
|
||||
corrupt cat: Integrity check failed
|
||||
dmesg_before=122 dmesg_after=122 mounts=0 mds= module_rc=1
|
||||
```
|
||||
|
||||
The positive file read exactly. The checksum-valid corrupt inode failed with
|
||||
`EINTEGRITY` before its 31-byte inline range could cross the metadata block.
|
||||
The mapping path also uses checked additions, and primary/metabox declared
|
||||
bounds are validated at inode decode.
|
||||
|
||||
Self-review first rejected the old `i_xattr_icount=1020` mutation because it
|
||||
merely moved the inline data into the next block without crossing that block.
|
||||
The corrected value `695` places the tail at block offset 4068. Its first
|
||||
genuine rerun exposed a stale constructing vnode: the second `stat` returned
|
||||
`EBADF`. The failure path now calls `vgone()` before `vput()`, and the complete
|
||||
final rerun above returned `EINTEGRITY` for every repeated access.
|
||||
|
||||
## TC022/TC056: Special `st_rdev`
|
||||
|
||||
The source fixture used real Linux char/block nodes with major `2748`, minor
|
||||
`344865`, plus a FIFO. `stat_special.c` checked `st_rdev` directly because
|
||||
FreeBSD `stat -f %Lr` truncates before `minor()` for large values.
|
||||
|
||||
Compact and extended results were identical:
|
||||
|
||||
```text
|
||||
PASS char rdev=0xa430005bc21 major=2748 minor=344865
|
||||
PASS block rdev=0xa430005bc21 major=2748 minor=344865
|
||||
PASS fifo rdev=0xffffffffffffffff
|
||||
```
|
||||
|
||||
This proves Linux `new_decode_dev(0x543abc21)` followed by FreeBSD `makedev()`;
|
||||
a little-endian integer cast would not produce this FreeBSD `dev_t`.
|
||||
|
||||
## TC055: Real Compressed Allocation
|
||||
|
||||
For every row, full FreeBSD kernel reads matched the expected SHA256 and
|
||||
`st_blocks * 512` matched the inode's real on-disk compressed size:
|
||||
|
||||
| Shape | File | Size | Allocated bytes | SHA256/result |
|
||||
|---|---|---:|---:|---|
|
||||
| LZ4 full | `shape.dat` | 1048576 | 8192 | `370eb0a8...` PASS |
|
||||
| LZ4 compact | `shape.dat` | 1048576 | 8192 | `370eb0a8...` PASS |
|
||||
| LZ4 fragment | `fragment.dat` | 1048699 | 0 | `a3a83e5c...` PASS |
|
||||
| LZ4 ztailpacking | `inline.dat` | 65536 | 0 | `e2aa4a0a...` PASS |
|
||||
| MicroLZMA partial A | `a.dat` | 1048576 | 4096 | `370eb0a8...` PASS |
|
||||
| MicroLZMA partial B | `b.dat` | 700000 | 4096 | `5a840803...` PASS |
|
||||
| DEFLATE compact/partial | `a.dat` | 1048576 | 36864 | `370eb0a8...` PASS |
|
||||
| DEFLATE full/partial | `b.dat` | 1050624 | 8192 | `61b17076...` PASS |
|
||||
| ZSTD partial A | `a.dat` | 1048576 | 4096 | `370eb0a8...` PASS |
|
||||
| ZSTD partial B | `b.dat` | 700000 | 4096 | `5a840803...` PASS |
|
||||
|
||||
Uncompressed regressions:
|
||||
|
||||
```text
|
||||
inline size=31 st_blocks=8
|
||||
plain size=21211 st_blocks=48
|
||||
chunk size=90017 st_blocks=176
|
||||
```
|
||||
|
||||
Representative compression image hashes were:
|
||||
|
||||
```text
|
||||
LZ4 full d784f8dc... LZ4 compact 37942ef1...
|
||||
LZ4 fragment a4e5d40e... LZ4 ztail 035069eb...
|
||||
MicroLZMA 2f4bd89d... DEFLATE 8bc720a1... ZSTD 7f5ee4f8...
|
||||
```
|
||||
|
||||
## TC141/TC148: Directory Compatibility and Strictness
|
||||
|
||||
FreeBSD results:
|
||||
|
||||
- Cold `/alpha/bravo/charlie/payload.txt`: PASS without parent warming.
|
||||
- Repeated cold lookup and post-negative-cache existing lookup: PASS.
|
||||
- Patched nonzero tail bytes: accepted.
|
||||
- `wide` enumeration: 320 files plus `.` and `..`; 322 dirents through a
|
||||
128-byte buffer and restart cookies.
|
||||
- Short block, non-monotonic `nameoff`, and `/` in an on-disk name: lookup and
|
||||
readdir both returned `EINTEGRITY`.
|
||||
- Repeated corrupted lookup remained `EINTEGRITY`, not cached `ENOENT`.
|
||||
|
||||
The duplicate validators were removed; lookup and readdir use the shared
|
||||
helper in `dir.c`.
|
||||
|
||||
## TC132/TC133: NFS Generation and Stress
|
||||
|
||||
Direct handle validation after the final superblock-hash implementation:
|
||||
|
||||
```text
|
||||
nfs-a: fsid=00000034:000000e0 nid=0x2e gen=849213208
|
||||
nfs-b: fsid=00000034:000000e0 nid=0x2e gen=4011239099
|
||||
```
|
||||
|
||||
- `nfs-a` remount on the same md unit produced byte-identical complete handles.
|
||||
- `va_gen` equaled handle generation.
|
||||
- Replacing `nfs-a` with `nfs-b` on the same md unit made the old handle return
|
||||
`ESTALE` through both `fhstat` and `fhopen`.
|
||||
- Bad length/pad returned `EINVAL`; bad generation/NID returned `ESTALE`.
|
||||
- Handle generation mutation used `gen_xor 1`, not a hard-coded value.
|
||||
|
||||
NFSv3/TCP READDIRPLUS stress:
|
||||
|
||||
- Four clients mounted; requested 512/1024/4096 readdir sizes were clamped by
|
||||
FreeBSD to 8192, while default was 65536.
|
||||
- All four 12,050-name sorted listings had SHA256
|
||||
`4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71`.
|
||||
- Twelve traversal workers and eight cat/stat controller jobs were waited by
|
||||
PID; every exit status was zero.
|
||||
- READDIRPLUS client/server count was 1304 in the recorded full run.
|
||||
- RPC timeouts, invalid replies, retries, and server write RPCs were zero.
|
||||
- The 21,757,952-byte throughput file hash was
|
||||
`0841effed82d1adf394b6834ce30d4d9eb5fc9427527e854ec1cb6bb4b86c119`.
|
||||
- nfsd restart with an open descriptor passed after the service command closed
|
||||
inherited fd 3.
|
||||
- Corrected full rerun ended `NFS-STRESS-CLEAN-PASS`.
|
||||
|
||||
## TC149: Real Pager Faults
|
||||
|
||||
`tests/mmap_fault.c` was compiled natively on FreeBSD 15 and run against both
|
||||
plain and LZ4 images.
|
||||
|
||||
Both runs printed:
|
||||
|
||||
```text
|
||||
PASS size=21211 pages=6 fnv1a64=a1890a1c216724be random-faults=6 \
|
||||
eof-zero=PASS sigbus=PASS private-cow=PASS
|
||||
```
|
||||
|
||||
The helper verified deterministic random faults after `MADV_DONTNEED`, full
|
||||
mapping equality with `pread`, partial EOF-page zeroes, child `SIGBUS` on the
|
||||
next full page, `MAP_SHARED` write denial, FreeBSD private COW semantics, and
|
||||
`O_RDWR -> EROFS`.
|
||||
|
||||
dmesg added exactly the two expected child exits on signal 10 (`SIGBUS`). No
|
||||
parent crash, VM assertion, trap, panic, or dirty writeback appeared.
|
||||
|
||||
## Additional Regressions
|
||||
|
||||
| Area | Evidence | Result |
|
||||
|---|---|---|
|
||||
| Inline/system xattr | trusted, security, long-prefix, user values exact | PASS |
|
||||
| Shared xattr | shared and per-inode values exact | PASS |
|
||||
| Metabox xattr | `dirA/nested.txt` and `hello.txt` enumerated/read | PASS |
|
||||
| Fragment-backed metabox | positive file/xattrs exact; self-loop and range images rejected | PASS |
|
||||
| Single-device chunk | `plain.bin`, `deep/payload.bin` hashes exact | PASS |
|
||||
| Chunk multidevice | block-map and indexed files hashes exact | PASS |
|
||||
| External compressed multidevice | complete 1 MiB LZ4 SHA256 `370eb0a8...` | PASS |
|
||||
| Compact nlink rules | explicit nlink 1/2 plus flagged nlink-one image | PASS |
|
||||
| ZSTD enabled final load/read | final module and `zstd-partial-ref` read | PASS |
|
||||
|
||||
Supporting fixture hashes:
|
||||
|
||||
```text
|
||||
system-inline-four 6f521b62...
|
||||
shared xattr 208b61ca...
|
||||
metabox xattr ef8d619c...
|
||||
chunk single 7aa8db22...
|
||||
chunk multidev 73343b70... + a36c2b9b...
|
||||
external LZ4 56256124... + 0d71102c...
|
||||
NFS stress image 6928f05b...
|
||||
```
|
||||
|
||||
The final-module fragment-backed metabox rerun used the previously qualified,
|
||||
checksum-valid image with SHA256
|
||||
`8a9a62bd203994711b8272192915d811e6c3de23e07ad9607dd63e66cc109bcd`.
|
||||
`/tree/d00/file000.txt` produced SHA256
|
||||
`4536c1d7121f48829475f29179f54baa57154b4ef817cf0776f81782585d29ad`;
|
||||
the shared and per-file xattrs were `shared-value` and `value-000`. The
|
||||
self-loop and out-of-range images returned `Integrity check failed`. dmesg
|
||||
remained at 122 lines, and the post-run audit showed zero mounts, md providers,
|
||||
and loaded EROFS modules.
|
||||
|
||||
## Final-Binary Closure Rerun
|
||||
|
||||
After the `vgone()` error-path fix, the final ZSTDIO module SHA256
|
||||
`16166b9ffc96a532bda9925513e10df11f4cf8a06b3c15afbc09b1a4e4b9d2df`
|
||||
was used for one continuous closure matrix:
|
||||
|
||||
- LZ4 full/compact/fragment/ztailpacking, MicroLZMA partial, DEFLATE
|
||||
compact/full partial, and ZSTD partial hashes and `st_blocks` all matched.
|
||||
- Inline trusted/security/long-prefix/user xattrs matched exact values.
|
||||
- Metabox shared xattrs returned `answer=forty-two`; the fragment carrier file
|
||||
and `repo22.item-000=value-000` matched.
|
||||
- Single-device chunk, external chunk provider, and external compressed LZ4
|
||||
hashes matched; compressed `st_blocks=16`.
|
||||
- The final module preserved the same NFS handle across same-md remount, made
|
||||
the old handle `ESTALE` after image replacement, synchronized `va_gen`, and
|
||||
completed a real local NFSv3/TCP client read with matching SHA256.
|
||||
|
||||
The first closure script stopped after the system-xattr row because it queried
|
||||
an obsolete metabox attribute name. Its trap left zero mounts, md providers,
|
||||
modules, and services. The corrected `answer`/`metaboxshared` queries and all
|
||||
remaining rows passed. The complete final audit was:
|
||||
|
||||
```text
|
||||
dmesg_before=122 dmesg_after=122
|
||||
erofs_mounts=0 nfs_mounts=0 mds= module_rc=1 services=0/0/0
|
||||
FINAL-REGRESSION-REST-PASS
|
||||
```
|
||||
|
||||
## Final Environment and Cleanup
|
||||
|
||||
Final guest audit:
|
||||
|
||||
```text
|
||||
mounts=0
|
||||
mds=
|
||||
modules=0
|
||||
services=0
|
||||
final_dmesg_before=122 final_dmesg_after=122
|
||||
```
|
||||
|
||||
The final dmesg tail contained only the historical pre-test duplicate-module
|
||||
diagnostic and the two intentional pager-child SIGBUS exits. The final
|
||||
compression, xattr, chunk, multidevice, generation, and NFS closure pass added
|
||||
no dmesg lines.
|
||||
|
||||
Guest helper hashes:
|
||||
|
||||
| Helper | SHA256 |
|
||||
|---|---|
|
||||
| `mmap_fault` | `b19c9c28a7abdfebe4243e3f4876b711eebc1c02b9856c15cb8bf15ae2da3a65` |
|
||||
| `nfs_fh_tool` | `0dcad233cc8768ca84569c8d030939b526d50c64190fc54d5f0de013191f11d6` |
|
||||
| `stat_special` | `b5c0eabd06541268e799df4f72fa7f37a460617bd8960f918b27710483db3841` |
|
||||
| `readdir_probe` | `44747ccd8d9a285a37d8049b7d61d00946db88482c08a3570e59b5a2fa1b4ea2` |
|
||||
|
||||
No password material was printed into this report or written into tracked
|
||||
files.
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
fixture_dir=${FIXTURE_DIR:-"$script_dir/fixture"}
|
||||
artifact_dir=${ARTIFACT_DIR:-"$script_dir/artifacts"}
|
||||
|
||||
for tool in mkfs.erofs fsck.erofs dump.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
echo "missing tool: $tool" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
test "$(id -u)" -eq 0 || {
|
||||
echo "root is required to create device-node fixtures" >&2
|
||||
exit 1
|
||||
}
|
||||
test ! -e "$fixture_dir" || {
|
||||
echo "fixture directory already exists: $fixture_dir" >&2
|
||||
exit 1
|
||||
}
|
||||
test ! -e "$artifact_dir" || {
|
||||
echo "artifact directory already exists: $artifact_dir" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p \
|
||||
"$fixture_dir/inline" \
|
||||
"$fixture_dir/special" \
|
||||
"$fixture_dir/namei/alpha/bravo/charlie" \
|
||||
"$fixture_dir/namei/alpha/sibling" \
|
||||
"$fixture_dir/namei/wide" \
|
||||
"$fixture_dir/pager" \
|
||||
"$fixture_dir/nfs/basic/subdir" \
|
||||
"$fixture_dir/nlink/subdir" \
|
||||
"$artifact_dir"
|
||||
|
||||
printf 'inline-tail-payload-0123456789\n' > "$fixture_dir/inline/inline.txt"
|
||||
printf 'special target\n' > "$fixture_dir/special/target"
|
||||
ln -s target "$fixture_dir/special/link"
|
||||
mknod "$fixture_dir/special/char-large" c 2748 344865
|
||||
mknod "$fixture_dir/special/block-large" b 2748 344865
|
||||
mkfifo "$fixture_dir/special/fifo"
|
||||
|
||||
printf 'cold nested lookup payload\n' > \
|
||||
"$fixture_dir/namei/alpha/bravo/charlie/payload.txt"
|
||||
printf 'repeat lookup payload\n' > \
|
||||
"$fixture_dir/namei/alpha/bravo/repeat.txt"
|
||||
printf 'sibling marker\n' > "$fixture_dir/namei/alpha/sibling/marker.txt"
|
||||
index=0
|
||||
while [ "$index" -lt 320 ]; do
|
||||
name=$(printf 'entry-%03d-abcdefghijklmnopqrstuvwxyz.txt' "$index")
|
||||
printf 'wide entry %03d\n' "$index" > "$fixture_dir/namei/wide/$name"
|
||||
index=$((index + 1))
|
||||
done
|
||||
|
||||
python3 - "$fixture_dir" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
data = bytes(((index * 131 + 17) ^ (index >> 3)) & 0xff
|
||||
for index in range(5 * 4096 + 731))
|
||||
(root / "pager" / "pager.bin").write_bytes(data)
|
||||
(root / "nfs" / "basic" / "regular.txt").write_text(
|
||||
"stable file handle payload\n", encoding="ascii")
|
||||
(root / "nfs" / "basic" / "subdir" / "child.txt").write_text(
|
||||
"child\n", encoding="ascii")
|
||||
PY
|
||||
ln -s regular.txt "$fixture_dir/nfs/basic/link-to-regular"
|
||||
mkfifo "$fixture_dir/nfs/basic/test.fifo"
|
||||
|
||||
printf 'single\n' > "$fixture_dir/nlink/single.txt"
|
||||
printf 'hardlinked\n' > "$fixture_dir/nlink/hard-a.txt"
|
||||
ln "$fixture_dir/nlink/hard-a.txt" "$fixture_dir/nlink/hard-b.txt"
|
||||
printf 'child\n' > "$fixture_dir/nlink/subdir/child.txt"
|
||||
|
||||
find "$fixture_dir" -exec touch -h -t 197001010000.00 {} +
|
||||
|
||||
build_image()
|
||||
{
|
||||
uuid=$1
|
||||
image=$2
|
||||
source=$3
|
||||
shift 3
|
||||
mkfs.erofs -d0 -x-1 -T0 --all-time --all-root --workers=1 \
|
||||
-U "$uuid" "$@" "$artifact_dir/$image" "$fixture_dir/$source"
|
||||
}
|
||||
|
||||
build_image 11111111-2222-3333-4444-555555555551 inline.erofs inline
|
||||
build_image 11111111-2222-3333-4444-555555555552 \
|
||||
special-compact.erofs special -E force-inode-compact
|
||||
build_image 11111111-2222-3333-4444-555555555553 \
|
||||
special-extended.erofs special -E force-inode-extended
|
||||
build_image 11111111-2222-3333-4444-555555555554 namei-base.erofs namei
|
||||
build_image 11111111-2222-3333-4444-555555555555 \
|
||||
pager-plain.erofs pager -E noinline_data
|
||||
build_image 11111111-2222-3333-4444-555555555556 \
|
||||
pager-lz4.erofs pager -z lz4
|
||||
build_image aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee1 nfs-a.erofs nfs
|
||||
build_image aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee2 nfs-b.erofs nfs
|
||||
build_image 11111111-2222-3333-4444-555555555557 \
|
||||
nlink.erofs nlink -E force-inode-compact
|
||||
|
||||
ARTIFACT_DIR="$artifact_dir" python3 <<'PY'
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
|
||||
artifact_dir = Path(os.environ["ARTIFACT_DIR"])
|
||||
|
||||
|
||||
def u16(image, offset):
|
||||
return struct.unpack_from("<H", image, offset)[0]
|
||||
|
||||
|
||||
def u32(image, offset):
|
||||
return struct.unpack_from("<I", image, offset)[0]
|
||||
|
||||
|
||||
def put_u16(image, offset, value):
|
||||
struct.pack_into("<H", image, offset, value)
|
||||
|
||||
|
||||
def put_u32(image, offset, value):
|
||||
struct.pack_into("<I", image, offset, value)
|
||||
|
||||
|
||||
def update_superblock_checksum(image):
|
||||
polynomial = 0x82F63B78
|
||||
checksum = 0xFFFFFFFF
|
||||
put_u32(image, 1028, 0)
|
||||
for byte in image[1024:4096]:
|
||||
checksum ^= byte
|
||||
for _ in range(8):
|
||||
checksum = (checksum >> 1) ^ (
|
||||
polynomial if checksum & 1 else 0)
|
||||
put_u32(image, 1028, checksum & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def inode_offset(image, nid):
|
||||
block_bits = image[1036]
|
||||
meta_blkaddr = u32(image, 1064)
|
||||
return (meta_blkaddr << block_bits) + (nid << 5)
|
||||
|
||||
|
||||
def compact_inode(image, nid):
|
||||
offset = inode_offset(image, nid)
|
||||
inode_format = u16(image, offset)
|
||||
assert (inode_format & 1) == 0
|
||||
return offset, (inode_format >> 1) & 7, u32(image, offset + 8)
|
||||
|
||||
|
||||
def inline_dir_entries(image, nid):
|
||||
inode = inode_offset(image, nid)
|
||||
inode_format = u16(image, inode)
|
||||
inode_size = 64 if inode_format & 1 else 32
|
||||
layout = (inode_format >> 1) & 7
|
||||
size = (struct.unpack_from("<Q", image, inode + 8)[0]
|
||||
if inode_size == 64 else u32(image, inode + 8))
|
||||
assert layout == 2 and u16(image, inode + 2) == 0
|
||||
data = inode + inode_size
|
||||
first_nameoff = u16(image, data + 8)
|
||||
assert first_nameoff >= 12 and first_nameoff % 12 == 0
|
||||
count = first_nameoff // 12
|
||||
entries = []
|
||||
for index in range(count):
|
||||
entry = data + index * 12
|
||||
child_nid, nameoff = struct.unpack_from("<QH", image, entry)
|
||||
endoff = (u16(image, data + (index + 1) * 12 + 8)
|
||||
if index + 1 < count else size)
|
||||
name = bytes(image[data + nameoff:data + endoff]).split(b"\0", 1)[0]
|
||||
assert name
|
||||
entries.append((child_nid, nameoff, endoff, name))
|
||||
return inode, data, size, entries
|
||||
|
||||
|
||||
def write_image(name, image):
|
||||
update_superblock_checksum(image)
|
||||
(artifact_dir / name).write_bytes(image)
|
||||
|
||||
|
||||
inline = bytearray((artifact_dir / "inline.erofs").read_bytes())
|
||||
_, _, _, inline_root = inline_dir_entries(inline, u16(inline, 1038))
|
||||
inline_nid = next(nid for nid, _, _, name in inline_root if name == b"inline.txt")
|
||||
inline_inode, layout, inline_size = compact_inode(inline, inline_nid)
|
||||
assert layout == 2 and u16(inline, inline_inode + 2) == 0
|
||||
inline_cross_xattr_icount = 695
|
||||
inline_xattr_size = 12 + 4 * (inline_cross_xattr_icount - 1)
|
||||
inline_data_off = inline_inode + 32 + inline_xattr_size
|
||||
block_size = 1 << inline[1036]
|
||||
assert inline_data_off % block_size + inline_size > block_size
|
||||
inline_cross = bytearray(inline)
|
||||
put_u16(inline_cross, inline_inode + 2, inline_cross_xattr_icount)
|
||||
write_image("inline-cross-block.erofs", inline_cross)
|
||||
|
||||
namei = bytearray((artifact_dir / "namei-base.erofs").read_bytes())
|
||||
root_nid = u16(namei, 1038)
|
||||
root_inode, root_data, root_size, root_entries = inline_dir_entries(
|
||||
namei, root_nid)
|
||||
wide_nid = next(nid for nid, _, _, name in root_entries if name == b"wide")
|
||||
wide_inode, wide_layout, _ = compact_inode(namei, wide_nid)
|
||||
assert wide_layout == 2
|
||||
block_bits = namei[1036]
|
||||
wide_block = u32(namei, wide_inode + 16) << block_bits
|
||||
wide_first_nameoff = u16(namei, wide_block + 8)
|
||||
wide_count = wide_first_nameoff // 12
|
||||
wide_last_nameoff = u16(namei, wide_block + (wide_count - 1) * 12 + 8)
|
||||
padding_nul = namei.index(0, wide_block + wide_last_nameoff,
|
||||
wide_block + (1 << block_bits))
|
||||
|
||||
nonzero_padding = bytearray(namei)
|
||||
nonzero_padding[padding_nul + 1:padding_nul + 9] = b"PAD!ERO!"
|
||||
write_image("namei-padding-nonzero.erofs", nonzero_padding)
|
||||
|
||||
short_block = bytearray(namei)
|
||||
put_u32(short_block, root_inode + 8, 8)
|
||||
write_image("namei-corrupt-short.erofs", short_block)
|
||||
|
||||
bad_nameoff = bytearray(namei)
|
||||
first_nameoff = u16(bad_nameoff, root_data + 8)
|
||||
put_u16(bad_nameoff, root_data + 12 + 8, first_nameoff)
|
||||
write_image("namei-corrupt-nameoff.erofs", bad_nameoff)
|
||||
|
||||
bad_name = bytearray(namei)
|
||||
slash_offset = wide_block + wide_last_nameoff + 5
|
||||
assert bad_name[slash_offset] not in (0, ord("/"))
|
||||
bad_name[slash_offset] = ord("/")
|
||||
write_image("namei-corrupt-name.erofs", bad_name)
|
||||
|
||||
nlink = bytearray((artifact_dir / "nlink.erofs").read_bytes())
|
||||
nlink_root_nid = u16(nlink, 1038)
|
||||
_, _, _, nlink_entries = inline_dir_entries(nlink, nlink_root_nid)
|
||||
single_nid = next(nid for nid, _, _, name in nlink_entries
|
||||
if name == b"single.txt")
|
||||
single_inode, _, _ = compact_inode(nlink, single_nid)
|
||||
nlink1 = bytearray(nlink)
|
||||
put_u16(nlink1, single_inode, u16(nlink1, single_inode) | 0x10)
|
||||
put_u16(nlink1, single_inode + 6, 0x1234)
|
||||
write_image("nlink1-patched.erofs", nlink1)
|
||||
|
||||
with (artifact_dir / "fixture-evidence.txt").open("w", encoding="ascii") as out:
|
||||
out.write(f"inline_nid={inline_nid} inode_off={inline_inode} "
|
||||
f"inode_blockoff={inline_inode % block_size} "
|
||||
f"xattr_icount=0->{inline_cross_xattr_icount} "
|
||||
f"inline_data_blockoff={inline_data_off % block_size} "
|
||||
f"inline_size={inline_size}\n")
|
||||
out.write(f"wide_nid={wide_nid} block={wide_block >> block_bits} "
|
||||
f"dirents={wide_count} last_nameoff={wide_last_nameoff} "
|
||||
f"padding_patch={padding_nul + 1 - wide_block}:"
|
||||
f"{padding_nul + 9 - wide_block}\n")
|
||||
out.write(f"nlink1_nid={single_nid} i_format_bit4=1 i_nb=0x1234\n")
|
||||
for image_name in ("special-compact.erofs", "special-extended.erofs"):
|
||||
image = bytearray((artifact_dir / image_name).read_bytes())
|
||||
special_root = u16(image, 1038)
|
||||
_, _, _, entries = inline_dir_entries(image, special_root)
|
||||
out.write(image_name + "\n")
|
||||
for nid, _, _, name in entries:
|
||||
if name not in (b"char-large", b"block-large", b"fifo"):
|
||||
continue
|
||||
offset = inode_offset(image, nid)
|
||||
out.write(f" {name.decode()} nid={nid} inode_size="
|
||||
f"{64 if u16(image, offset) & 1 else 32} "
|
||||
f"raw_u={u32(image, offset + 16):#010x}\n")
|
||||
|
||||
with (artifact_dir / "SHA256SUMS").open("w", encoding="ascii") as sums:
|
||||
for path in sorted(artifact_dir.glob("*.erofs")):
|
||||
sums.write(f"{hashlib.sha256(path.read_bytes()).hexdigest()} "
|
||||
f"{path.name}\n")
|
||||
PY
|
||||
|
||||
fsck.erofs -d1 "$artifact_dir/namei-padding-nonzero.erofs"
|
||||
dump.erofs --cat --path=/wide/entry-079-abcdefghijklmnopqrstuvwxyz.txt \
|
||||
"$artifact_dir/namei-padding-nonzero.erofs"
|
||||
cat "$artifact_dir/fixture-evidence.txt"
|
||||
cat "$artifact_dir/SHA256SUMS"
|
||||
Reference in New Issue
Block a user