test code v1
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# repo22 Final Review WIP Manual Test Report
|
||||
|
||||
Started: 2026-08-09 01:24 UTC
|
||||
|
||||
Completed: 2026-08-09 03:33 UTC
|
||||
|
||||
Parent commit: `9a3604fba32cfe2ff263d954d80fe588f99db88f`
|
||||
|
||||
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
|
||||
|
||||
Linux reference: `/work/dev-src-linux/fs/erofs`
|
||||
|
||||
FreeBSD reference: `/work/dev-freebsd-releng`
|
||||
|
||||
## Scope
|
||||
|
||||
This run reviews and validates four final correctness findings:
|
||||
|
||||
- Linux-compatible 48-bit superblock union selection;
|
||||
- rejection of extended inode sizes above FreeBSD `OFF_MAX`;
|
||||
- bounded reads from multi-GiB explicit extent holes;
|
||||
- 64-bit directory block-search indexes.
|
||||
|
||||
No CI work, binary fixture, guest overlay, build object, or raw VM artifact is
|
||||
part of the intended commit.
|
||||
|
||||
## Result
|
||||
|
||||
| Test | Result | Evidence |
|
||||
|---|---|---|
|
||||
| TC150 48-bit fallback root | PASS | Real FreeBSD mount, root read, inode and `df` |
|
||||
| TC151 size above `OFF_MAX` | PASS | Six direct failures, rc 1, syscall errno 97 |
|
||||
| TC152 bounded extent hole | PASS | Two pread/mmap probes, stable active allocation |
|
||||
| TC153 large directory index | SHELVED | Host fixture reproducible; kernel run incomplete |
|
||||
|
||||
TC153 is deliberately not marked PASS. See
|
||||
`issues/TC153-large-directory-block-index-validation.md`.
|
||||
|
||||
## Source Review
|
||||
|
||||
Linux `super.c` initializes `blocks_lo` and uses `rb.blocks_hi` only inside
|
||||
`48BIT && rootnid_8b`. repo22 now decodes blocks and root NID together under
|
||||
that same selector.
|
||||
|
||||
FreeBSD exposes signed `off_t` pager and vnode interfaces bounded by
|
||||
`OFF_MAX`; repo22 rejects a larger decoded inode before vnode/pager setup.
|
||||
|
||||
The extent-hole path now zeroes only the current requested span in
|
||||
`z_erofs_do_read()` instead of allocating the complete logical extent.
|
||||
|
||||
The directory block search now uses 64-bit bounds and a checked block-offset
|
||||
multiplication. The within-block search remains 32-bit, matching the validated
|
||||
block-sized domain.
|
||||
|
||||
## Build Results
|
||||
|
||||
Commands:
|
||||
|
||||
```sh
|
||||
EROFS_ZSTDIO=0 ./build.sh
|
||||
EROFS_ZSTDIO=1 ./build.sh
|
||||
nm -u build/erofs.ko
|
||||
git diff --check -- repo-community/repo22
|
||||
```
|
||||
|
||||
| Configuration | Module SHA256 | Result |
|
||||
|---|---|---|
|
||||
| `EROFS_ZSTDIO=0` | `65bc19d53a2a7f0525bfacadb441dab37ee318f5f5b4c62b5ecd1b5090fe46d2` | PASS |
|
||||
| `EROFS_ZSTDIO=1` | `d46ca4dfc858deaf4840fad8b8589b16d4b71b34afa96dc7acf1255890c5cef7` | PASS |
|
||||
|
||||
Both freestanding builds completed. Neither module had an unresolved `bcmp`
|
||||
reference. The ZSTDIO-enabled module was transferred to the guest, loaded as
|
||||
KLD ID 7, and unloaded after testing.
|
||||
|
||||
## Fixture Evidence
|
||||
|
||||
| Fixture | SHA256 |
|
||||
|---|---|
|
||||
| `fallback-48bit-root2.erofs` | `bed3be4dfb8499d4e794b03eddd5b9cda95bc8e5571ae7d765c4852ece0d95a3` |
|
||||
| `extended-size-bit63.erofs` | `e4a0f550168f1a2911603863d0074d474e61adc787c14c0278c83a060643ee38` |
|
||||
| `extent-hole-5g.erofs` | `50014a24493918247e36511ad34a2fe8ab47ae09ea46d7fd62a1bed445a6c65f` |
|
||||
| `large-dir-intmax.erofs` | `0f90d3d57adbbbd946e41b225c1f6c464915c6abb0b13478ec9b2a318def1f72` |
|
||||
|
||||
The TC153 hash is host generator evidence only.
|
||||
|
||||
## TC150
|
||||
|
||||
The fixture encoded `rootnid_2b=36`, `rootnid_8b=0`, `blocks_lo=1`, and the
|
||||
48-bit incompat bit. The 4 KiB image mounted on `/dev/md0` and produced:
|
||||
|
||||
```text
|
||||
content=48-bit fallback root
|
||||
sha256=d361f537492113ca93cfbf91c06ebc06e2b8b695d8b6e63ad3666013eaa029f0
|
||||
root inode=36
|
||||
df total=4 one-KiB blocks
|
||||
```
|
||||
|
||||
This proves the union was not shifted into a high block count. It does not
|
||||
replace the large-provider TC010 test.
|
||||
|
||||
## TC151
|
||||
|
||||
The fixture used extended inode NID 40 at byte 1280 with
|
||||
`i_size=0x8000000000000000`. The image mounted, but every access to `big.dat`
|
||||
failed before open or pager setup:
|
||||
|
||||
| Access | Attempts | Direct rc | `truss` result |
|
||||
|---|---:|---:|---|
|
||||
| `stat` | 2 | 1, 1 | `fstatat ... ERR#97` |
|
||||
| `cat` | 2 | 1, 1 | `openat ... ERR#97` |
|
||||
| `mmap_fault` | 2 | 1, 1 | `openat ... ERR#97` |
|
||||
|
||||
All six errors were `Integrity check failed`. No file descriptor reached the
|
||||
read or mmap phase.
|
||||
|
||||
## TC152
|
||||
|
||||
The mounted file size was `5368713216` bytes. The native helper probed offset
|
||||
`3221225472` with a one-byte pread and one-page private mmap:
|
||||
|
||||
```text
|
||||
attempt 1: PASS, real 0.03 s
|
||||
attempt 2: PASS, real 0.02 s
|
||||
```
|
||||
|
||||
The `erofs` malloc row was `3` active allocations and `768` active bytes both
|
||||
before and after. The cumulative allocation counter moved from 89 to 95, as
|
||||
expected for temporary request buffers; active memory did not scale with the
|
||||
5 GiB hole.
|
||||
|
||||
## TC153
|
||||
|
||||
The first 4096-byte fixture was Layout 2 and failed before the target namei
|
||||
path. A second 65536-byte base with 400 entries was still Layout 2. The tracked
|
||||
generator now converts the directory to Layout 0 by moving its complete data
|
||||
to appended blocks and produced a 90112-byte patched image with raw block 16,
|
||||
22 image blocks, and the hash listed above.
|
||||
|
||||
The corrected fixture was not executed in the FreeBSD kernel during this run.
|
||||
TC153 remains SHELVED, and the complete attempt history and acceptance criteria
|
||||
are in its issue document.
|
||||
|
||||
## Cleanup
|
||||
|
||||
The qualified rerun used dmesg line count 123 before and after. It ended with:
|
||||
|
||||
```text
|
||||
EROFS mounts: 0
|
||||
md providers: none
|
||||
EROFS modules: 0
|
||||
```
|
||||
|
||||
An earlier unqualified probe encountered a preloaded differently named EROFS
|
||||
KLD and a `truss` exit-status ambiguity. It was discarded. The qualified run
|
||||
first unloaded that KLD, loaded the exact module hash above, captured direct
|
||||
command exit codes separately from syscall traces, and then cleaned up.
|
||||
|
||||
## Deferred Issues
|
||||
|
||||
- `issues/TC153-large-directory-block-index-validation.md`
|
||||
- `issues/TC010-48bit-statfs-large-provider.md`
|
||||
- `issues/extent-metadata-fixture-unavailable.md`
|
||||
|
||||
The raw guest transcripts remain untracked under `/work/build`. This report,
|
||||
the deterministic generator, and source/test documentation are the tracked
|
||||
evidence.
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/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 python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
echo "missing tool: $tool" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
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/fallback" "$fixture_dir/oversize" \
|
||||
"$fixture_dir/extent" "$fixture_dir/large/huge" "$artifact_dir"
|
||||
|
||||
FIXTURE_DIR="$fixture_dir" python3 <<'PY'
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
root = Path(os.environ["FIXTURE_DIR"])
|
||||
(root / "fallback" / "root.txt").write_text(
|
||||
"48-bit fallback root\n", encoding="ascii")
|
||||
(root / "oversize" / "big.dat").write_bytes(b"x")
|
||||
(root / "extent" / "hole.dat").write_bytes(b"\0" * (1024 * 1024))
|
||||
(root / "large" / "huge" / "anchor.txt").write_text(
|
||||
"large directory anchor\n", encoding="ascii")
|
||||
for index in range(400):
|
||||
(root / "large" / "huge" /
|
||||
f"entry-{index:03d}-abcdefghijklmnopqrstuvwxyz.txt").write_text(
|
||||
f"entry {index:03d}\n", encoding="ascii")
|
||||
PY
|
||||
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 22222222-3333-4444-5555-666666666650 \
|
||||
fallback-base.erofs fallback -E force-inode-compact
|
||||
build_image 22222222-3333-4444-5555-666666666651 \
|
||||
oversize-base.erofs oversize -E force-inode-extended
|
||||
build_image 22222222-3333-4444-5555-666666666652 \
|
||||
extent-base.erofs extent -E legacy-compress,force-inode-extended -z lz4
|
||||
build_image 22222222-3333-4444-5555-666666666653 \
|
||||
large-dir-base.erofs large -E force-inode-extended
|
||||
|
||||
ARTIFACT_DIR="$artifact_dir" python3 <<'PY'
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
|
||||
artifact_dir = Path(os.environ["ARTIFACT_DIR"])
|
||||
SUPER = 1024
|
||||
FEATURE_INCOMPAT = SUPER + 80
|
||||
ROOTNID_2B = SUPER + 14
|
||||
BLOCKS_LO = SUPER + 36
|
||||
META_BLKADDR = SUPER + 40
|
||||
ROOTNID_8B = SUPER + 112
|
||||
EROFS_FEATURE_INCOMPAT_48BIT = 0x80
|
||||
EROFS_INODE_COMPRESSED_FULL = 1
|
||||
Z_EROFS_ADVISE_EXTENTS = 0x1
|
||||
Z_EROFS_EXTENT_RECSZ_16 = 0x4
|
||||
HOLE_SIZE = 5 * 1024 * 1024 * 1024 + 4096
|
||||
HUGE_DIR_BLOCKS = (1 << 31) + 1
|
||||
|
||||
|
||||
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 u64(image, offset):
|
||||
return struct.unpack_from("<Q", 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 put_u64(image, offset, value):
|
||||
struct.pack_into("<Q", image, offset, value)
|
||||
|
||||
|
||||
def update_superblock_checksum(image):
|
||||
polynomial = 0x82F63B78
|
||||
checksum = 0xFFFFFFFF
|
||||
block_size = 1 << image[SUPER + 12]
|
||||
put_u32(image, SUPER + 4, 0)
|
||||
for byte in image[SUPER:block_size]:
|
||||
checksum ^= byte
|
||||
for _ in range(8):
|
||||
checksum = (checksum >> 1) ^ (
|
||||
polynomial if checksum & 1 else 0)
|
||||
put_u32(image, SUPER + 4, checksum & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def inode_offset(image, nid):
|
||||
block_bits = image[SUPER + 12]
|
||||
return (u32(image, META_BLKADDR) << block_bits) + (nid << 5)
|
||||
|
||||
|
||||
def inode_info(image, nid):
|
||||
offset = inode_offset(image, nid)
|
||||
inode_format = u16(image, offset)
|
||||
inode_size = 64 if inode_format & 1 else 32
|
||||
size = u64(image, offset + 8) if inode_size == 64 else u32(
|
||||
image, offset + 8)
|
||||
xattr_count = u16(image, offset + 2)
|
||||
xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1)
|
||||
layout = (inode_format >> 1) & 7
|
||||
return offset, inode_format, inode_size, xattr_size, layout, size
|
||||
|
||||
|
||||
def directory_entries(image, nid):
|
||||
offset, _, inode_size, xattr_size, layout, size = inode_info(image, nid)
|
||||
block_size = 1 << image[SUPER + 12]
|
||||
assert 0 < size <= block_size
|
||||
if layout == 2:
|
||||
data = offset + inode_size + xattr_size
|
||||
elif layout == 0:
|
||||
data = u32(image, offset + 16) << image[SUPER + 12]
|
||||
else:
|
||||
raise AssertionError(f"unsupported directory layout {layout}")
|
||||
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 = u64(image, entry)
|
||||
nameoff = u16(image, entry + 8)
|
||||
endoff = (u16(image, entry + 20)
|
||||
if index + 1 < count else size)
|
||||
name = bytes(image[data + nameoff:data + endoff]).split(b"\0", 1)[0]
|
||||
assert name
|
||||
entries.append((name, child_nid))
|
||||
return entries
|
||||
|
||||
|
||||
def child_nid(image, parent_nid, name):
|
||||
return next(nid for entry_name, nid in directory_entries(image, parent_nid)
|
||||
if entry_name == name)
|
||||
|
||||
|
||||
def convert_inline_directory_to_plain(image, nid):
|
||||
offset, inode_format, inode_size, xattr_size, layout, size = inode_info(
|
||||
image, nid)
|
||||
assert layout == 2 and inode_size == 64 and size > 0
|
||||
block_size = 1 << image[SUPER + 12]
|
||||
tail_size = size % block_size
|
||||
assert tail_size > 0
|
||||
raw_block = u32(image, offset + 16)
|
||||
full_size = size - tail_size
|
||||
inline_offset = offset + inode_size + xattr_size
|
||||
directory_data = bytes(
|
||||
image[raw_block * block_size:raw_block * block_size + full_size] +
|
||||
image[inline_offset:inline_offset + tail_size])
|
||||
assert len(directory_data) == size
|
||||
|
||||
new_raw_block = (len(image) + block_size - 1) // block_size
|
||||
image.extend(b"\0" * (new_raw_block * block_size - len(image)))
|
||||
image.extend(directory_data)
|
||||
image.extend(b"\0" * (-len(image) % block_size))
|
||||
put_u16(image, offset, inode_format & ~(7 << 1))
|
||||
put_u32(image, offset + 16, new_raw_block)
|
||||
put_u32(image, BLOCKS_LO, len(image) // block_size)
|
||||
assert inode_info(image, nid)[4] == 0
|
||||
return new_raw_block, len(image) // block_size
|
||||
|
||||
|
||||
def write_image(name, image):
|
||||
update_superblock_checksum(image)
|
||||
(artifact_dir / name).write_bytes(image)
|
||||
|
||||
|
||||
evidence = []
|
||||
|
||||
fallback = bytearray((artifact_dir / "fallback-base.erofs").read_bytes())
|
||||
fallback_root = u16(fallback, ROOTNID_2B)
|
||||
fallback_blocks = u32(fallback, BLOCKS_LO)
|
||||
assert fallback_root != 0 and u64(fallback, ROOTNID_8B) == 0
|
||||
put_u32(fallback, FEATURE_INCOMPAT,
|
||||
u32(fallback, FEATURE_INCOMPAT) | EROFS_FEATURE_INCOMPAT_48BIT)
|
||||
put_u64(fallback, ROOTNID_8B, 0)
|
||||
write_image("fallback-48bit-root2.erofs", fallback)
|
||||
evidence.append(
|
||||
f"fallback rootnid_2b={fallback_root} rootnid_8b=0 "
|
||||
f"blocks_lo={fallback_blocks} union=0x{u16(fallback, ROOTNID_2B):04x}")
|
||||
|
||||
oversize = bytearray((artifact_dir / "oversize-base.erofs").read_bytes())
|
||||
oversize_root = u16(oversize, ROOTNID_2B)
|
||||
oversize_nid = child_nid(oversize, oversize_root, b"big.dat")
|
||||
oversize_off, oversize_format, oversize_isize, _, _, _ = inode_info(
|
||||
oversize, oversize_nid)
|
||||
assert oversize_format & 1 and oversize_isize == 64
|
||||
put_u64(oversize, oversize_off + 8, 1 << 63)
|
||||
write_image("extended-size-bit63.erofs", oversize)
|
||||
evidence.append(
|
||||
f"oversize nid={oversize_nid} inode_off={oversize_off} "
|
||||
f"i_size=0x{u64(oversize, oversize_off + 8):016x}")
|
||||
|
||||
extent = bytearray((artifact_dir / "extent-base.erofs").read_bytes())
|
||||
extent_root = u16(extent, ROOTNID_2B)
|
||||
extent_nid = child_nid(extent, extent_root, b"hole.dat")
|
||||
extent_off, extent_format, extent_isize, extent_xattr, extent_layout, _ = \
|
||||
inode_info(extent, extent_nid)
|
||||
assert extent_format & 1 and extent_isize == 64
|
||||
assert extent_layout == EROFS_INODE_COMPRESSED_FULL
|
||||
header = (extent_off + extent_isize + extent_xattr + 7) & ~7
|
||||
record = (header + 8 + 15) & ~15
|
||||
root_off = inode_offset(extent, extent_root)
|
||||
assert not (header < root_off + 64 and root_off < record + 16)
|
||||
put_u64(extent, extent_off + 8, HOLE_SIZE)
|
||||
struct.pack_into("<IHH", extent, header, 1,
|
||||
Z_EROFS_ADVISE_EXTENTS | Z_EROFS_EXTENT_RECSZ_16, 0)
|
||||
struct.pack_into("<IIII", extent, record, 0, 0, 0, 0)
|
||||
write_image("extent-hole-5g.erofs", extent)
|
||||
evidence.append(
|
||||
f"extent-hole nid={extent_nid} inode_off={extent_off} size={HOLE_SIZE} "
|
||||
f"header={header} record={record} extents=1 recsize=16 plen=0 lstart=0")
|
||||
|
||||
large = bytearray((artifact_dir / "large-dir-base.erofs").read_bytes())
|
||||
large_root = u16(large, ROOTNID_2B)
|
||||
large_nid = child_nid(large, large_root, b"huge")
|
||||
large_off, large_format, large_isize, _, _, _ = inode_info(large, large_nid)
|
||||
assert large_format & 1 and large_isize == 64
|
||||
large_raw_block, large_image_blocks = convert_inline_directory_to_plain(
|
||||
large, large_nid)
|
||||
large_size = HUGE_DIR_BLOCKS * (1 << large[SUPER + 12])
|
||||
put_u64(large, large_off + 8, large_size)
|
||||
write_image("large-dir-intmax.erofs", large)
|
||||
evidence.append(
|
||||
f"large-dir nid={large_nid} inode_off={large_off} size={large_size} "
|
||||
f"blocks={HUGE_DIR_BLOCKS} last_block={HUGE_DIR_BLOCKS - 1} "
|
||||
f"layout=flat-plain raw_block={large_raw_block} "
|
||||
f"image_blocks={large_image_blocks}")
|
||||
|
||||
(artifact_dir / "fixture-evidence.txt").write_text(
|
||||
"\n".join(evidence) + "\n", encoding="ascii")
|
||||
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
|
||||
|
||||
cat "$artifact_dir/fixture-evidence.txt"
|
||||
cat "$artifact_dir/SHA256SUMS"
|
||||
Reference in New Issue
Block a user