test code v1

This commit is contained in:
2026-08-13 10:44:59 +02:00
commit f3b1165f19
301 changed files with 37885 additions and 0 deletions
@@ -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"