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,188 @@
# repo22 手工测试执行记录
- 执行时间:2026-08-08 10:36-10:38 UTC
- 工作区:`/work`
- 仓库:`/work/repo-community/repo22`
- 执行方式:严格按 Markdown 用例进行前置条件检查;未生成 wrapper 或 CI;未修改代码;未 commit/push
- 总结:TC001、TC002、TC007、TC008、TC009、TC010 均为 **BLOCKED**。没有用例进入 FreeBSD 客体内的正式测试步骤,因此没有伪造 PASS/FAIL。
## 环境版本
实际命令:
```sh
date -u '+%Y-%m-%dT%H:%M:%SZ'
uname -a
cat /etc/os-release
```
实际输出:
```text
2026-08-08T10:37:36Z
Linux da8f2da26d77 6.12.74+deb13+1-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
VERSION_ID="13"
DEBIAN_VERSION_FULL=13.5
```
结论:当前 shell 位于 Linux 容器,不是 FreeBSD`kldload``kldstat``mdconfig` 等用例命令不能在此环境代替执行。
## FreeBSD VM 可访问性
实际命令:
```sh
timeout 3 bash -c '</dev/tcp/127.0.0.1/9222'
ssh -vv -o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 \
-p 9222 [email protected] 'uname -a'
ps auxww | grep -E '[q]emu|[b]hyve|[v]mrun|[s]sh.*9222'
tmux ls
screen -ls
```
实际输出摘要:
```text
/dev/tcp/127.0.0.1/9222: Connection refused
CLOSED
ssh: connect to host 127.0.0.1 port 9222: Connection refused
未发现 qemu/bhyve/vmrun 进程
tmux: command not found
screen: command not found
```
PID 文件检查:
```sh
cat /work/debug-qemu/local/vm/freebsd-build-runtime.pid
ps -p 5274 -o pid,stat,lstart,args
cat /work/debug-qemu/local/vm/qemu.pid
ps -p 23736 -o pid,stat,lstart,args
```
实际输出:PID 文件分别记录 `5274``23736`,但 `ps` 对两者都没有返回进程;均为残留 PID 文件。
## QEMU 与磁盘基线
实际命令:
```sh
command -v qemu-system-x86_64
dpkg-query -W -f='${Status} ${Version}\n' qemu-system-x86 qemu-system-x86_64
find /work -type f -name qemu-system-x86_64 -perm /111 -print
ls -lh --full-time \
/work/debug-qemu/local/vm-freebsd-build.qcow2.bp \
/work/debug-qemu/local/vm/freebsd-build-runtime.qcow2
```
实际输出:
```text
command -v: 无输出
dpkg-query: no packages found matching qemu-system-x86
dpkg-query: no packages found matching qemu-system-x86_64
find: 未找到工作区内可执行的 qemu-system-x86_64
-r--r--r-- ... 13G ... /work/debug-qemu/local/vm-freebsd-build.qcow2.bp
-r--r--r-- ... 15G ... /work/debug-qemu/local/vm/freebsd-build-runtime.qcow2
```
结论:现成 FreeBSD 磁盘基线存在,但宿主机没有 QEMU 可执行文件,无法启动 `/work/debug-qemu/run-freebsd-build-vm.sh`
## erofs.ko 盘点
实际命令:
```sh
find /work -type f \( -name erofs.ko -o -name '*erofs*.ko' \) -printf '%s %p\n' | sort
ls -l /work/repo-community/repo22/build/erofs.ko
sha256sum /work/debug-qemu/local/erofs.ko /work/build/erofs.ko
```
实际输出摘要:
```text
22784 /work/build/dev-src-check/erofs-kmod-obj/erofs.ko
23088 /work/build/dev-src-recheck-obj/erofs-dev-src.ko
30480 /work/build/repo11-kmod-obj/erofs.ko
30480 /work/build/repo12-erofs.ko
30480 /work/build/repo12-kmod-obj/erofs.ko
33800 /work/build/erofs-kmod-obj/erofs.ko
33800 /work/build/erofs.ko
37056 /work/debug-qemu/local/erofs.ko
ls: cannot access '/work/repo-community/repo22/build/erofs.ko': No such file or directory
246a1ab6cc4e77d88b8ee689339e6565085cd0913f769c91ad1a1ef13d3a3cd9 /work/debug-qemu/local/erofs.ko
1fb12045c1fce653cb9f2e75dc76ae3339db6b84def06b92d828357726215f8b /work/build/erofs.ko
```
结论:存在其他历史构建模块,但没有 repo22 的预期产物 `repo22/build/erofs.ko`,不能把来源不明或旧仓库模块当作 repo22 测试对象。
## 测试镜像盘点
实际命令:
```sh
for n in test.erofs test-valid.erofs test-bad-crc.erofs \
test-bad-magic.erofs test-truncated.erofs test-empty.erofs \
test-large.erofs; do
find /work -type f -name "$n"
done
```
实际输出:七个指定名称均为 0 个文件。
发现的非指定通用 fixture 包括:
```text
24576 /work/debug-qemu/local/erofs-fixtures/erofs-test.img
36864 /work/debug-qemu/local/erofs-fixtures/erofs-plain.img
```
它们不满足 TC009 声明的 10MB/2560 blocks 镜像,也没有证据满足 TC001/TC007 的 `testfile.txt`、TC002 的有效 CRC/损坏 CRC 配对或 TC010 的 48-bit block count,因此未擅自替代。
## 用例结果
### TC001-mount-basic — BLOCKED
- 未执行 `kldload erofs`:当前没有可访问的 FreeBSD 客体。
- 未执行后续 mount/umount:缺少已加载 repo22 `erofs.ko` 的 FreeBSD 环境,且缺少 `test.erofs` 与已附加的 `/dev/md0`
- PASS/FAIL:不适用;状态为 BLOCKED。
### TC002-superblock-crc32c — BLOCKED
- TC001 前置环境未满足。
- 缺少 `test-valid.erofs``test-bad-crc.erofs`,不能执行有效 CRC 与损坏 CRC 两部分。
- PASS/FAIL:不适用;状态为 BLOCKED。
### TC007-concurrent-mount — BLOCKED
- TC001 前置环境未满足。
- 缺少 `test.erofs`;现有 fixture 未证明包含 `/testfile.txt`,不能按步骤比较两个挂载点的 SHA-256。
- PASS/FAIL:不适用;状态为 BLOCKED。
### TC008-mount-errors — BLOCKED
- TC001 前置环境未满足。
- 缺少 `test-bad-magic.erofs``test-truncated.erofs``test-empty.erofs`,三个错误分支均无法执行。
- PASS/FAIL:不适用;状态为 BLOCKED。
### TC009-statfs-basic — BLOCKED
- TC001 前置环境未满足。
- 缺少用例声明的 10MB、2560 blocks `test.erofs`,不能验证 `df`/`statfs` 数值。
- PASS/FAIL:不适用;状态为 BLOCKED。
### TC010-statfs-48bit — BLOCKED
- TC001 前置环境未满足。
- 缺少带 48-bit block count、块数大于 `2^32``test-large.erofs`
- PASS/FAIL:不适用;状态为 BLOCKED。
## 精确解阻条件
1. 提供可访问的 FreeBSD 13.0+ VM;最直接方式是恢复 `127.0.0.1:9222` SSH,或在宿主机安装可用的 `qemu-system-x86_64` 后启动现有基线。
2. 提供由当前 repo22 源码构建、与客体 FreeBSD ABI 匹配的 `erofs.ko`
3. 提供并确认以下镜像及内容/元数据:`test.erofs``test-valid.erofs``test-bad-crc.erofs``test-bad-magic.erofs``test-truncated.erofs``test-empty.erofs``test-large.erofs`
4. TC001/TC007 的基本镜像需包含 `/testfile.txt`TC009 的镜像需为约 10MB、2560 blocksTC010 的镜像需具有可验证的 48-bit block count。
@@ -0,0 +1,435 @@
# repo22 手工测试报告
- 执行日期:2026-08-08UTC
- 执行时段:2026-08-08T11:08Z 至 2026-08-08T11:14:44Z
- 工作区:`/work`
- 测试对象:`/work/repo-community/repo22`
- ABI 提交:`f2caaae2890a910d0f11547d9ff5f7cf92c062a2`
- 提交时间:`2026-08-08T11:12:36Z`
- 提交主题:`repo22: align core on-disk structures`
- 执行方式:手工 SSH/SCP、手工模块命令;未使用测试 wrapper 或 CI;未修改源码;未 commit/push
- 总体结论:**FAIL / BLOCKED**
## 结果摘要
| 项目 | 结果 | 证据 |
|---|---|---|
| SSH 与 guest 环境 | PASS | FreeBSD 15.0-RELEASE-p8 amd64rootsecurelevel -1,所需系统工具存在 |
| 等待 repo22 ABI 提交 | PASS | `f2caaae28` 出现后 repo22 scoped 工作树干净 |
| 宿主构建 repo22 `erofs.ko` | PASS | 构建退出 0,产物 45088 字节 |
| SCP 到 guest | PASS | 宿主/guest SHA-256 均为 `f119bb2c902bf6e221b532b1c8e7668caaa5c744c46b4eac8f9f105cd7494e9f` |
| guest `kldload` | **FAIL** | 返回 1dmesg`link_elf_obj: symbol bcmp undefined` |
| guest `kldstat`(加载后) | BLOCKED | `kldload` 未成功,不能出现已加载模块 |
| guest `kldunload` | BLOCKED | 模块从未加载,不能执行有效卸载验证 |
| 制作/复制 plain 镜像 | NOT RUN | 按“任何步骤失败立即停止”要求,在模块加载失败后停止 |
| TC001-mount-basic | BLOCKED / NOT RUN | 前置条件“EROFS 模块已加载”不满足 |
| TC009-statfs-basic | BLOCKED / NOT RUN | 仅在 TC001 通过后执行;TC001 未执行 |
## 1. 初始 SSH 验证
首次按题目给出的普通 SSH 方式验证,端口可达,但 guest 不接受宿主默认公钥。
实际命令:
```sh
ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=8 -p 9222 [email protected] \
'echo "== identity =="; id; echo "== uname =="; uname -a'
```
实际输出:
```text
Warning: Permanently added '[127.0.0.1]:9222' (ED25519) to the list of known hosts.
[email protected]: Permission denied (publickey,keyboard-interactive).
```
检查现有 VM 基线文档后,确认该 guest 使用固定测试密码和
`keyboard-interactive,password`。宿主没有 `sshpass`,因此创建了仅用于 SSH/SCP
认证的临时 `/tmp/repo22-ssh-askpass.sh`;它不是测试 wrapper,未写入仓库。
实际命令:
```sh
cat > /tmp/repo22-ssh-askpass.sh <<'EOF'
#!/bin/sh
printf '%s\n' '<固定测试密码>'
EOF
chmod 700 /tmp/repo22-ssh-askpass.sh
```
随后所有 SSH/SCP 命令使用以下认证前缀和选项:
```sh
DISPLAY=:0 SSH_ASKPASS=/tmp/repo22-ssh-askpass.sh \
SSH_ASKPASS_REQUIRE=force setsid -w ssh \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o PreferredAuthentications=keyboard-interactive,password \
-o PubkeyAuthentication=no \
-p 9222 [email protected] '<guest command>'
```
## 2. Guest 环境验证
实际 guest 命令:
```sh
echo "== UTC date =="; date -u "+%Y-%m-%dT%H:%M:%SZ"
echo "== identity =="; id
echo "== uname =="; uname -a
echo "== freebsd-version =="; freebsd-version -ku 2>&1
echo "== hostname =="; hostname
echo "== architecture =="; uname -m; sysctl -n hw.machine_arch 2>&1
echo "== securelevel =="; sysctl kern.securelevel
echo "== module tools =="; command -v kldload; command -v kldstat; command -v kldunload
echo "== filesystem tools =="; command -v mdconfig; command -v mount; command -v umount; command -v sha256; command -v stat
echo "== existing erofs module/mount/md =="; kldstat | grep -i erofs || true; mount | grep -i erofs || true; mdconfig -l
echo "== temp space =="; df -h /tmp /root
```
实际输出:
```text
Warning: Permanently added '[127.0.0.1]:9222' (ED25519) to the list of known hosts.
== UTC date ==
2026-08-08T11:10:13Z
== identity ==
uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)
== uname ==
FreeBSD freebsd-build 15.0-RELEASE-p8 FreeBSD 15.0-RELEASE-p8 releng/15.0-n281036-53054229dcb3 GENERIC amd64
== freebsd-version ==
15.0-RELEASE-p8
15.0-RELEASE-p8
== hostname ==
freebsd-build
== architecture ==
amd64
amd64
== securelevel ==
kern.securelevel: -1
== module tools ==
/sbin/kldload
/sbin/kldstat
/sbin/kldunload
== filesystem tools ==
/sbin/mdconfig
/sbin/mount
/sbin/umount
/sbin/sha256
/usr/bin/stat
== existing erofs module/mount/md ==
== temp space ==
Filesystem Size Used Avail Capacity Mounted on
/dev/vtbd0p2 112G 12G 91G 12% /
/dev/vtbd0p2 112G 12G 91G 12% /
```
结论:guest 连接及基础环境 PASS;开始时没有已加载 EROFS 模块、EROFS 挂载或 md 设备。
## 3. 等待 ABI 提交
轮询条件:HEAD 必须晚于测试开始时的 `db524d69f`,且
`git status --porcelain -- repo-community/repo22` 行数必须为 0。
实际命令:
```sh
baseline=db524d69f
for attempt in $(seq 1 40); do
now=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
head=$(git rev-parse --short HEAD)
subject=$(git log -1 --pretty=%s)
dirty=$(git status --porcelain -- repo-community/repo22 | wc -l)
printf '%s attempt=%02d HEAD=%s dirty_repo22=%s subject=%s\n' \
"$now" "$attempt" "$head" "$dirty" "$subject"
if [ "$head" != "$baseline" ] && [ "$dirty" -eq 0 ]; then
echo 'READY: repo22 has a newer commit and its scoped worktree is clean'
git log --date=iso-strict -5 --pretty=format:'%h %ad %an %s'
echo
exit 0
fi
sleep 15
done
```
实际输出:
```text
2026-08-08T11:10:33Z attempt=01 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers
2026-08-08T11:10:49Z attempt=02 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers
2026-08-08T11:11:04Z attempt=03 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers
2026-08-08T11:11:20Z attempt=04 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers
2026-08-08T11:11:35Z attempt=05 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers
2026-08-08T11:11:50Z attempt=06 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers
2026-08-08T11:12:05Z attempt=07 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers
2026-08-08T11:12:21Z attempt=08 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers
2026-08-08T11:12:36Z attempt=09 HEAD=f2caaae28 dirty_repo22=0 subject=repo22: align core on-disk structures
READY: repo22 has a newer commit and its scoped worktree is clean
f2caaae28 2026-08-08T11:12:36Z Ruicheng Pan repo22: align core on-disk structures
db524d69f 2026-08-08T10:46:37Z Ruicheng Pan repo22: record initial manual test blockers
524dcfac5 2026-08-08T10:43:55Z Ruicheng Pan repo22: harden core I/O and build path
173385f79 2026-08-08T10:09:38Z Ruicheng Pan Add repo22 based on repo21 for comprehensive validation and review
c782c1dc8 2026-06-18T20:01:35Z Ruicheng Pan 补充和完善 repo21_explain/src 中文注释
```
## 4. 宿主构建 repo22 erofs.ko
实际命令(工作目录 `/work/repo-community/repo22`):
```sh
date -u '+%Y-%m-%dT%H:%M:%SZ'
git rev-parse HEAD
clang --version | head -1
FREEBSD_SRC=/work/dev-freebsd-releng ./build.sh
echo "build_exit=$?"
ls -l --full-time build/erofs.ko
sha256sum build/erofs.ko
```
实际输出(编译器对 FreeBSD 内核头文件产生了多组重复的 builtin redeclaration
warning;构建未使用 `-Werror`,没有编译或链接 error):
```text
2026-08-08T11:13:13Z
f2caaae2890a910d0f11547d9ff5f7cf92c062a2
Debian clang version 19.1.7 (3+b1)
==> Building repo22 erofs.ko
[data.c、dir.c、erofs_vnops.c、inode.c、namei.c、super.c、xattr.c、lz4.c、deflate.c、zstd.c、lzma.c 编译期间重复出现以下 warning:]
warning: incompatible redeclaration of library function 'log' [-Wincompatible-library-redeclaration]
warning: incompatible redeclaration of library function 'strdup' [-Wincompatible-library-redeclaration]
warning: incompatible redeclaration of library function 'strndup' [-Wincompatible-library-redeclaration]
warning: incompatible redeclaration of library function 'free' [-Wincompatible-library-redeclaration]
warning: incompatible redeclaration of library function 'malloc' [-Wincompatible-library-redeclaration]
warning: incompatible redeclaration of library function 'realloc' [-Wincompatible-library-redeclaration]
/work/repo-community/repo22/src/lzma.c:133:19: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare]
/work/repo-community/repo22/src/lzma.c:135:16: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare]
==> SUCCESS: /work/repo-community/repo22/build/erofs.ko
build_exit=0
-rw-r--r-- 1 root root 45088 2026-08-08 11:13:16.704187568 +0000 build/erofs.ko
f119bb2c902bf6e221b532b1c8e7668caaa5c744c46b4eac8f9f105cd7494e9f build/erofs.ko
```
说明:原始构建输出包含每个源文件的同类完整 warning 展开;本报告逐类保留了实际
warning 文本和两个非重复的 `lzma.c` warning,并保留了完整的成功、退出码、尺寸和哈希证据。
## 5. SCP 模块到 guest
实际命令(密码由临时 askpass 提供):
```sh
DISPLAY=:0 SSH_ASKPASS=/tmp/repo22-ssh-askpass.sh \
SSH_ASKPASS_REQUIRE=force setsid -w scp -O \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o PreferredAuthentications=keyboard-interactive,password \
-o PubkeyAuthentication=no \
-P 9222 build/erofs.ko [email protected]:/root/repo22-erofs.ko
echo "scp_exit=$?"
```
实际输出:
```text
Warning: Permanently added '[127.0.0.1]:9222' (ED25519) to the list of known hosts.
scp_exit=0
```
guest 校验命令:
```sh
ls -l /root/repo22-erofs.ko
sha256 /root/repo22-erofs.ko
```
实际输出:
```text
-rw-r--r-- 1 root wheel 45088 Aug 8 11:13 /root/repo22-erofs.ko
SHA256 (/root/repo22-erofs.ko) = f119bb2c902bf6e221b532b1c8e7668caaa5c744c46b4eac8f9f105cd7494e9f
```
结论:复制成功且字节身份一致。
## 6. Guest 手工模块加载测试
### 6.1 加载前状态与 dmesg
实际命令:
```sh
kldstat | grep -i erofs
echo "pre_kldstat_grep_exit=$?"
dmesg | tail -30
```
实际输出:
```text
pre_kldstat_grep_exit=1
device_attach: fdc0 attach returned 6
ppc0: <Parallel port> port 0x378-0x37f irq 7 on acpi0
ppc0: Generic chipset (NIBBLE-only) in COMPATIBLE mode
ppbus0: <Parallel port bus> on ppc0
lpt0: <Printer> on ppbus0
lpt0: Interrupt-driven port
ppi0: <Parallel I/O> on ppbus0
uart0: <16550 or compatible> port 0x3f8-0x3ff irq 4 flags 0x10 on acpi0
orm0: <ISA Option ROM> at iomem 0xe7800-0xeffff pnpid ORM0000 on isa0
vga0: <Generic ISA VGA> at port 0x3c0-0x3df iomem 0xa0000-0xbffff pnpid PNP0900 on isa0
attimer0: <AT timer> at port 0x40 on isa0
Timecounter "i8254" frequency 1193182 Hz quality 0
Event timer "i8254" frequency 1193182 Hz quality 100
attimer0: non-PNP ISA device will be removed from GENERIC in FreeBSD 16.
Timecounters tick every 10.000 msec
usb_needs_explore_all: no devclass
Trying to mount root from ufs:/dev/vtbd0p2 [rw]...
WARNING: / was not properly dismounted
WARNING: /: mount pending error: blocks 0 files 32
cd0 at ata1 bus 0 scbus1 target 0 lun 0
cd0: <QEMU QEMU DVD-ROM 2.5+> Removable CD-ROM SCSI device
cd0: Serial Number QM00003
cd0: 16.700MB/s transfers (WDMA2, ATAPI 12bytes, PIO 65534bytes)
cd0: Attempt to query device size failed: NOT READY, Medium not present
intsmb0: <Intel PIIX4 SMBUS Interface> irq 9 at device 1.3 on pci0
intsmb0: intr IRQ 9 enabled revision 0
smbus0: <System Management Bus> on intsmb0
lo0: link state changed to UP
vtnet0: link state changed to UP
Security policy loaded: MAC/ntpd (mac_ntpd)
```
### 6.2 kldload
实际命令:
```sh
kldload /root/repo22-erofs.ko
echo "kldload_exit=$?"
```
实际输出:
```text
kldload: an error occurred while loading module /root/repo22-erofs.ko. Please check dmesg(8) for more details.
kldload_exit=1
```
失败后的实际 dmesg 命令:
```sh
dmesg | tail -80
```
实际输出末尾(失败相关行):
```text
link_elf_obj: symbol bcmp undefined
linker_load_file: /root/repo22-erofs.ko - unsupported file type
```
最终复核命令:
```sh
date -u "+%Y-%m-%dT%H:%M:%SZ"
kldstat | grep -i erofs
echo "kldstat_grep_exit=$?"
mount | grep -i erofs
echo "mount_grep_exit=$?"
mdconfig -l
dmesg | grep -E "bcmp|repo22-erofs|linker_load_file" | tail -20
ls -l /root/repo22-erofs.ko
sha256 /root/repo22-erofs.ko
```
实际输出:
```text
2026-08-08T11:14:44Z
kldstat_grep_exit=1
mount_grep_exit=1
link_elf_obj: symbol bcmp undefined
linker_load_file: /root/repo22-erofs.ko - unsupported file type
-rw-r--r-- 1 root wheel 45088 Aug 8 11:13 /root/repo22-erofs.ko
SHA256 (/root/repo22-erofs.ko) = f119bb2c902bf6e221b532b1c8e7668caaa5c744c46b4eac8f9f105cd7494e9f
```
结论:**模块加载 FAIL**。失败不是 SCP 损坏导致,因为宿主和 guest 哈希完全一致。
FreeBSD 内核链接器无法解析模块引用的 `bcmp` 符号。
### 6.3 kldstat 与 kldunload
- 加载后的 `kldstat` 验证:**BLOCKED**。加载操作返回 1,最终复核也确认没有 EROFS 模块。
- `kldunload erofs`**BLOCKED / NOT RUN**。模块从未成功加载;执行卸载不能构成有效卸载测试。
- 已按“任何步骤失败立即记录并停止”要求停止,没有尝试替换模块、修改 guest、绕过未解析符号或继续文件系统测试。
## 7. mkfs.erofs 与 plain 镜像
宿主工具前置检查实际命令:
```sh
command -v mkfs.erofs
mkfs.erofs -V
```
实际输出:
```text
/usr/bin/mkfs.erofs
mkfs.erofs (erofs-utils) 1.8.6
available compressors: lz4, lz4hc, lzma, deflate, libdeflate, zstd
```
状态:**NOT RUN**。工具版本满足要求,但 `kldload` 已失败,因此没有生成源数据、
没有生成 plain 镜像、没有复制镜像到 guest。这样避免在首个失败后继续推进并产生误导性结果。
## 8. TC001-mount-basic
状态:**BLOCKED / NOT RUN**。
阻塞原因:`TC001-mount-basic.md` 的前置条件要求 FreeBSD 系统已加载 EROFS 内核模块;
本轮 `kldload /root/repo22-erofs.ko` 返回 1。因而以下命令均未执行:
```sh
mdconfig -a -t vnode -f test.erofs -u 0
mkdir -p /mnt/test
mount -t erofs /dev/md0 /mnt/test
mount | grep erofs
df -h /mnt/test
sha256 /mnt/test/<测试文件>
umount /mnt/test
mdconfig -d -u md0
```
没有把 BLOCKED 记录为 PASS 或文件系统行为 FAIL。
## 9. TC009-statfs-basic
状态:**BLOCKED / NOT RUN**。
用户要求仅在 TC001 通过后执行 TC009。TC001 因模块加载失败没有进入测试步骤,故
`df -h``df -i``stat -f` 和只读标志检查均未执行。
## 10. 工作树与提交状态
ABI 提交完成后的实际确认:
```text
HEAD=f2caaae2890a910d0f11547d9ff5f7cf92c062a2
git status --porcelain -- repo-community/repo22
<无输出>
```
本报告是本轮唯一预期新增文件。未修改 repo22 源码,未 commit,未 push。
## 最终判定
- 手工测试准备环境:PASS
- repo22 模块宿主构建:PASS
- 模块传输与完整性:PASS
- FreeBSD guest 模块加载:**FAIL**
- 失败根因证据:`link_elf_obj: symbol bcmp undefined`
- TC001BLOCKED / NOT RUN
- TC009BLOCKED / NOT RUN
- 总体:**FAIL / BLOCKED**
@@ -0,0 +1,513 @@
# repo22 手工基础测试报告
- 执行时间:2026-08-08 11:38-11:43 UTC
- 工作区:`/work`
- repo22`/work/repo-community/repo22`
- Git 分支:`main`
- 测试源码提交:`0c173fa5c07a0c2af7dafcf8f89f0167ae3e1f9d`
- FreeBSD VMSSH `127.0.0.1:9222`
- 执行方式:手工构建、SSH/SCP 和逐条 shell 命令;未生成测试 wrapper 或 CI;未修改源码
- 前一轮失败报告:保留 `tests/results/manual/2026-08-08T1114Z/manual-test-report.md`
- 总体结论:**TC001 PASSTC009 FAIL(用例中的 GNU 风格 `stat` 命令与 FreeBSD `stat(1)` 不兼容)**
## 结果摘要
| 项目 | 状态 | 实际结果 |
|---|---|---|
| repo22 模块重建 | PASS | `build/erofs.ko`41712 字节 |
| `bcmp` 构建检查 | PASS | `nm -u` 中没有 `bcmp` |
| 模块传输完整性 | PASS | host/guest SHA-256 均为 `6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f` |
| `kldload` / `kldstat` / `kldunload` | PASS | canonical `/root/erofs.ko` 三步退出码均为 0 |
| 固定 plain 镜像 | PASS | 10 MiB、2560×4096 blocks、重复构建逐字节相同 |
| TC001-mount-basic | **PASS** | 挂载、只读标志、容量、读取、卸载均符合预期 |
| TC009-statfs-basic | **FAIL** | `df`/只读/容量正确;原文步骤 3 未显示文件系统信息,步骤 4 返回 1 |
| 清理 | PASS | 无 EROFS 模块、挂载和 md 设备残留;guest 临时文件已删除 |
## 1. Host 环境与源码状态
实际命令:
```sh
date -u '+%Y-%m-%dT%H:%M:%SZ'
uname -a
cat /etc/os-release
git rev-parse HEAD
git branch --show-current
git show HEAD:repo-community/repo22/build.sh | sha256sum
sha256sum repo-community/repo22/build.sh
git diff -- repo-community/repo22/src repo-community/repo22/build.sh
```
实际输出:
```text
2026-08-08T11:38:49Z
Linux da8f2da26d77 6.12.74+deb13+1-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
VERSION_ID="13"
DEBIAN_VERSION_FULL=13.5
0c173fa5c07a0c2af7dafcf8f89f0167ae3e1f9d
main
70b6c015b33479c271e11abf36108c7f9e7ca9e2b9aa9d931fe7ae21f27f59ff -
70b6c015b33479c271e11abf36108c7f9e7ca9e2b9aa9d931fe7ae21f27f59ff repo-community/repo22/build.sh
<git diff 无输出>
```
## 2. 重新构建模块与 `bcmp` 检查
实际命令:
```sh
cd /work/repo-community/repo22
FREEBSD_SRC=/work/dev-freebsd-releng ./build.sh
ls -l --full-time build/erofs.ko
sha256sum build/erofs.ko
nm -u build/erofs.ko | awk '$NF == "bcmp" {print; found=1} END {if (!found) print "none"}'
```
实际输出:
```text
==> Building repo22 erofs.ko
/work/repo-community/repo22/src/lzma.c:133:19: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare]
133 | } while (byte < 0x100);
| ~~~~ ^ ~~~~~
/work/repo-community/repo22/src/lzma.c:135:16: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare]
135 | while (byte < 0x100)
| ~~~~ ^ ~~~~~
2 warnings generated.
==> SUCCESS: /work/repo-community/repo22/build/erofs.ko
-rw-r--r-- 1 root root 41712 2026-08-08 11:38:52.056076393 +0000 build/erofs.ko
6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f build/erofs.ko
none
```
结论:构建退出 0,上一轮导致 guest 加载失败的未解析 `bcmp` 已不在模块中。
## 3. SSH 与 Guest 基线
普通公钥探测:
```sh
timeout 5 bash -c '</dev/tcp/127.0.0.1/9222'
ssh -o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-p 9222 [email protected] 'date -u; uname -a; id; kldstat'
```
```text
PORT_OPEN
[email protected]: Permission denied (publickey,keyboard-interactive).
```
按现有 VM helper 的固定密码认证方式继续。密码脱敏;临时 askpass 文件位于 `/tmp`,不是测试 wrapper,未加入仓库。
```sh
cat > /tmp/repo22-ssh-askpass.sh <<'ASKPASS'
#!/bin/sh
printf '%s\n' "$REPO22_VM_PASS"
ASKPASS
chmod 700 /tmp/repo22-ssh-askpass.sh
export REPO22_VM_PASS='<redacted>'
export DISPLAY=:0 SSH_ASKPASS=/tmp/repo22-ssh-askpass.sh SSH_ASKPASS_REQUIRE=force
setsid -w ssh \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o PreferredAuthentications=keyboard-interactive,password \
-o PubkeyAuthentication=no -p 9222 [email protected] '
date -u "+%Y-%m-%dT%H:%M:%SZ"
uname -a
freebsd-version -ku
id
sysctl kern.securelevel
command -v kldload; command -v kldstat; command -v kldunload
command -v mdconfig; command -v mount; command -v umount
command -v stat; command -v sha256
kldstat | grep -i erofs; echo "kldstat_erofs_exit=$?"
mount | grep -i erofs; echo "mount_erofs_exit=$?"
mdconfig -l
'
```
```text
2026-08-08T11:39:48Z
FreeBSD freebsd-build 15.0-RELEASE-p8 FreeBSD 15.0-RELEASE-p8 releng/15.0-n281036-53054229dcb3 GENERIC amd64
15.0-RELEASE-p8
15.0-RELEASE-p8
uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)
kern.securelevel: -1
/sbin/kldload
/sbin/kldstat
/sbin/kldunload
/sbin/mdconfig
/sbin/mount
/sbin/umount
/usr/bin/stat
/sbin/sha256
kldstat_erofs_exit=1
mount_erofs_exit=1
<mdconfig -l 无输出>
```
结论:VM 是 FreeBSD 15.0-RELEASE-p8 amd64;初始无 EROFS 模块、挂载或 md 设备。
## 4. 模块复制与生命周期回归
```sh
setsid -w scp -O \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o PreferredAuthentications=keyboard-interactive,password \
-o PubkeyAuthentication=no -P 9222 \
repo-community/repo22/build/erofs.ko \
[email protected]:/root/repo22-erofs.ko
```
首次使用非 canonical 文件名加载的实际命令与输出:
```sh
ls -l /root/repo22-erofs.ko
sha256 /root/repo22-erofs.ko
dmesg | grep -E "bcmp|repo22-erofs|linker_load_file|erofs" | tail -20
kldload /root/repo22-erofs.ko; echo "kldload_exit=$?"
kldstat | grep -i erofs; echo "kldstat_grep_exit=$?"
dmesg | grep -E "bcmp|repo22-erofs|linker_load_file|erofs" | tail -20
kldunload erofs; echo "kldunload_exit=$?"
```
```text
-rw-r--r-- 1 root wheel 41712 Aug 8 11:40 /root/repo22-erofs.ko
SHA256 (/root/repo22-erofs.ko) = 6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f
link_elf_obj: symbol bcmp undefined
linker_load_file: /root/repo22-erofs.ko - unsupported file type
kldload_exit=0
5 1 0xffffffff82822000 5d30 repo22-erofs.ko
kldstat_grep_exit=0
link_elf_obj: symbol bcmp undefined
linker_load_file: /root/repo22-erofs.ko - unsupported file type
kldunload: can't find file erofs
kldunload_exit=1
```
两条 `bcmp`/`unsupported file type` 是本轮加载前已存在的上一轮 dmesg 历史行;加载前后输出相同,本轮 `kldload_exit=0`,没有新增链接错误。首次卸载失败仅因加载文件名是 `repo22-erofs.ko`
按模块 ID 清理后,用 canonical 文件名重新执行完整生命周期:
```sh
module_id=$(kldstat | awk '/repo22-erofs[.]ko/ {print $1}')
echo "module_id=$module_id"
kldunload -i "$module_id"; echo "kldunload_by_id_exit=$?"
kldstat | grep -i erofs; echo "post_id_unload_kldstat_exit=$?"
cp -f /root/repo22-erofs.ko /root/erofs.ko
sha256 /root/erofs.ko
kldload /root/erofs.ko; echo "kldload_exit=$?"
kldstat | grep -i erofs; echo "kldstat_grep_exit=$?"
kldunload erofs; echo "kldunload_exit=$?"
kldstat | grep -i erofs; echo "post_unload_kldstat_grep_exit=$?"
```
```text
module_id=5
kldunload_by_id_exit=0
post_id_unload_kldstat_exit=1
SHA256 (/root/erofs.ko) = 6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f
kldload_exit=0
5 1 0xffffffff82822000 5d30 erofs.ko
kldstat_grep_exit=0
kldunload_exit=0
post_unload_kldstat_grep_exit=1
```
结论:`bcmp` 修复后的 `kldload``kldstat``kldunload` 回归 **PASS**
## 5. 固定 plain 镜像
工具检查:
```sh
command -v mkfs.erofs
mkfs.erofs -V
command -v dump.erofs
dump.erofs -V
```
```text
/usr/bin/mkfs.erofs
mkfs.erofs (erofs-utils) 1.8.6
available compressors: lz4, lz4hc, lzma, deflate, libdeflate, zstd
/usr/bin/dump.erofs
dump.erofs (erofs-utils) 1.8.6
```
首次尝试显式禁用 inline
```sh
mkfs.erofs -b4096 -x-1 -T0 --all-root --ignore-mtime \
-Enoinline -L repo22-manual \
/tmp/repo22-manual-plain/test.erofs \
/tmp/repo22-manual-plain/src
```
```text
<E> erofs: unknown extended option noinline
Try 'mkfs.erofs --help' for more information.
mkfs.erofs 1.8.6
```
该命令退出 1 且未产出镜像。随后用整块大小、无压缩的主文件,并固定时间戳、UUID、label、属主和 xattr 设置:
```sh
rm -rf /tmp/repo22-manual-plain
mkdir -p /tmp/repo22-manual-plain/src
printf 'repo22 FreeBSD EROFS manual test\n' > /tmp/repo22-manual-plain/src/README.txt
dd if=/dev/zero of=/tmp/repo22-manual-plain/src/plain.bin bs=4096 count=2559 status=none
touch -d @0 /tmp/repo22-manual-plain/src \
/tmp/repo22-manual-plain/src/README.txt \
/tmp/repo22-manual-plain/src/plain.bin
mkfs.erofs -b4096 -x-1 -T0 --all-root --ignore-mtime \
-U 00000000-0000-0000-0000-000000000022 -L repo22-manual \
/tmp/repo22-manual-plain/test.erofs /tmp/repo22-manual-plain/src
mkfs.erofs -b4096 -x-1 -T0 --all-root --ignore-mtime \
-U 00000000-0000-0000-0000-000000000022 -L repo22-manual \
/tmp/repo22-manual-plain/test-repeat.erofs /tmp/repo22-manual-plain/src
cmp /tmp/repo22-manual-plain/test.erofs /tmp/repo22-manual-plain/test-repeat.erofs
echo "cmp_exit=$?"
sha256sum /tmp/repo22-manual-plain/src/README.txt \
/tmp/repo22-manual-plain/src/plain.bin \
/tmp/repo22-manual-plain/test.erofs \
/tmp/repo22-manual-plain/test-repeat.erofs
dump.erofs -s /tmp/repo22-manual-plain/test.erofs
```
关键实际输出:
```text
Filesystem UUID: 00000000-0000-0000-0000-000000000022
Filesystem total blocks: 2560 (of 4096-byte blocks)
Filesystem total inodes: 3
Filesystem total metadata blocks: 1
cmp_exit=0
c1f6e23b161735085a00a36cb72926bbe187a63eabcb4425a4f5acad5fe3ff4a /tmp/repo22-manual-plain/src/README.txt
20bf72d8cbc4dbc7e214b671f2547e3e8d57d455d10ac6b645656ef26afc6880 /tmp/repo22-manual-plain/src/plain.bin
fec83f76b8b7eb1d83a9a51246e093c319061838b04633184e9a0c601a386ff3 /tmp/repo22-manual-plain/test.erofs
fec83f76b8b7eb1d83a9a51246e093c319061838b04633184e9a0c601a386ff3 /tmp/repo22-manual-plain/test-repeat.erofs
Filesystem magic number: 0xE0F5E1E2
Filesystem blocksize: 4096
Filesystem blocks: 2560
Filesystem inode count: 3
Filesystem created: Thu Jan 1 00:00:00 1970
Filesystem features: sb_csum mtime
Filesystem UUID: 00000000-0000-0000-0000-000000000022
```
镜像大小为 10485760 字节,即 10 MiB。两次独立输出逐字节相同。
复制并验证:
```sh
setsid -w scp -O <同上 SSH 认证选项> -P 9222 \
/tmp/repo22-manual-plain/test.erofs [email protected]:/root/test.erofs
ls -l /root/test.erofs
sha256 /root/test.erofs
```
```text
-rw-r--r-- 1 root wheel 10485760 Aug 8 11:42 /root/test.erofs
SHA256 (/root/test.erofs) = fec83f76b8b7eb1d83a9a51246e093c319061838b04633184e9a0c601a386ff3
```
## 6. TC001-mount-basic
原文步骤前仅做规定前置条件:把 canonical 模块放入标准搜索目录,并附加镜像为 `/dev/md0`
```sh
ls -l /boot/modules/erofs.ko
cp -f /root/erofs.ko /boot/modules/erofs.ko
sha256 /boot/modules/erofs.ko
umount /mnt/test 2>/dev/null
mdconfig -d -u md0 2>/dev/null
mdconfig -a -t vnode -f /root/test.erofs -u 0
echo "mdconfig_attach_exit=$?"
mdconfig -lv
```
```text
ls: /boot/modules/erofs.ko: No such file or directory
SHA256 (/boot/modules/erofs.ko) = 6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f
mdconfig_attach_exit=0
md0 vnode 10M /root/test.erofs -
```
原文步骤 1-5
```sh
kldload erofs; echo "tc001_step1_exit=$?"
kldstat | grep erofs; echo "tc001_step2_pipeline_exit=$?"
mkdir -p /mnt/test; echo "tc001_step3_exit=$?"
mount -t erofs /dev/md0 /mnt/test; echo "tc001_step4_exit=$?"
mount | grep erofs; echo "tc001_step5a_pipeline_exit=$?"
df -h /mnt/test; echo "tc001_step5b_exit=$?"
```
```text
tc001_step1_exit=0
5 1 0xffffffff82822000 5d30 erofs.ko
tc001_step2_pipeline_exit=0
tc001_step3_exit=0
tc001_step4_exit=0
/dev/md0 on /mnt/test (erofs, local, read-only)
tc001_step5a_pipeline_exit=0
Filesystem Size Used Avail Capacity Mounted on
/dev/md0 10M 10M 0B 100% /mnt/test
tc001_step5b_exit=0
```
补充读取验证:
```sh
ls -la /mnt/test
sha256 /mnt/test/README.txt /mnt/test/plain.bin
cat /mnt/test/README.txt
echo "tc001_content_exit=$?"
dmesg | tail -20
```
```text
ls: /mnt/test/.: Operation not supported
ls: /mnt/test/README.txt: Operation not supported
ls: /mnt/test/plain.bin: Operation not supported
total 10248
drwxr-xr-x 2 root wheel 70 Jan 1 1970 .
drwxr-xr-x 3 root wheel 512 Aug 8 11:42 ..
-rw-r--r-- 1 root wheel 33 Jan 1 1970 README.txt
-rw-r--r-- 1 root wheel 10481664 Jan 1 1970 plain.bin
SHA256 (/mnt/test/README.txt) = c1f6e23b161735085a00a36cb72926bbe187a63eabcb4425a4f5acad5fe3ff4a
SHA256 (/mnt/test/plain.bin) = 20bf72d8cbc4dbc7e214b671f2547e3e8d57d455d10ac6b645656ef26afc6880
repo22 FreeBSD EROFS manual test
tc001_content_exit=0
```
`ls -la` 在读取附加 stat/pathconf 信息时打印 `Operation not supported`,但仍列出目录;两个文件均完整可读且 SHA-256 与 host 一致。该补充现象不属于 TC001 规定判定项,单独保留为观察。`dmesg | tail -20` 没有本轮 mount 错误;末尾两条 `bcmp` 行是上一轮历史记录。
原文步骤 6-7
```sh
umount /mnt/test; echo "tc001_step6_exit=$?"
mount | grep erofs; echo "tc001_step7_pipeline_exit=$?"
```
```text
tc001_step6_exit=0
<mount | grep erofs 无输出>
tc001_step7_pipeline_exit=1
```
步骤 7 的 grep 返回 1 正是“没有 EROFS 挂载”的预期。
**TC001 最终判定:PASS。**
## 7. TC009-statfs-basic
原文步骤 1-2
```sh
mount -t erofs /dev/md0 /mnt/test; echo "tc009_step1_exit=$?"
df -h /mnt/test; echo "tc009_step2a_exit=$?"
df -i /mnt/test; echo "tc009_step2b_exit=$?"
```
```text
tc009_step1_exit=0
Filesystem Size Used Avail Capacity Mounted on
/dev/md0 10M 10M 0B 100% /mnt/test
tc009_step2a_exit=0
Filesystem 1K-blocks Used Avail Capacity iused ifree %iused Mounted on
/dev/md0 10240 10240 0 100% 3 0 100% /mnt/test
tc009_step2b_exit=0
```
总容量 10 MiB、可用块 0、inode 总数 3、可用 inode 0。
严格执行原文步骤 3
```sh
stat -f /mnt/test
echo "tc009_step3_exit=$?"
```
```text
/mnt/test
tc009_step3_exit=0
```
FreeBSD `stat(1)``-f` 表示“后一个参数是格式字符串”,不是 GNU `stat -f` 的“显示文件系统状态”。命令只回显 `/mnt/test`,没有达到用例要求的“Shows EROFS filesystem type”。
严格执行原文步骤 4
```sh
stat -f -f "%b %f %c %a %d %i %t %n" /mnt/test
echo "tc009_step4_exit=$?"
```
```text
stat: %b %f %c %a %d %i %t %n: No such file or directory
-f
tc009_step4_exit=1
```
原文步骤 5 与补充容量核对:
```sh
mount | grep /mnt/test | grep read-only
echo "tc009_step5_pipeline_exit=$?"
mount | grep /mnt/test
df -k /mnt/test
```
```text
/dev/md0 on /mnt/test (erofs, local, read-only)
tc009_step5_pipeline_exit=0
/dev/md0 on /mnt/test (erofs, local, read-only)
Filesystem 1024-blocks Used Avail Capacity Mounted on
/dev/md0 10240 10240 0 100% /mnt/test
```
`10240` 个 1 KiB 块等于 `2560` 个 4096-byte 块;只读标志、文件系统类型和 free=0 均正确。但严格按用例判定,步骤 3 未产生要求的信息,步骤 4 退出 1。
**TC009 最终判定:FAIL。失败原因是测试文档中的 `stat` 命令与 FreeBSD `stat(1)` CLI 语义不兼容;本轮未修改测试文档或源码。**
## 8. 清理与最终状态
```sh
umount /mnt/test; echo "cleanup_umount_exit=$?"
mdconfig -d -u md0; echo "cleanup_mdconfig_exit=$?"
kldunload erofs; echo "cleanup_kldunload_exit=$?"
rmdir /mnt/test; echo "cleanup_rmdir_exit=$?"
rm -f /boot/modules/erofs.ko /root/test.erofs /root/erofs.ko /root/repo22-erofs.ko
echo "cleanup_rm_exit=$?"
kldstat | grep -i erofs; echo "final_kldstat_grep_exit=$?"
mount | grep -i erofs; echo "final_mount_grep_exit=$?"
mdconfig -l
```
```text
cleanup_umount_exit=0
cleanup_mdconfig_exit=0
cleanup_kldunload_exit=0
cleanup_rmdir_exit=0
cleanup_rm_exit=0
final_kldstat_grep_exit=1
final_mount_grep_exit=1
<mdconfig -l 无输出>
```
结论:guest 清理完成,无模块、挂载、md 设备或本轮临时文件残留。
## 最终判定
- `bcmp` 修复后模块构建与 host 检查:**PASS**
- guest `kldload` / `kldstat` / `kldunload`**PASS**
- 固定 10 MiB plain 镜像生成、重复性与 SHA-256**PASS**
- TC001-mount-basic**PASS**
- TC009-statfs-basic**FAIL(测试命令不兼容 FreeBSD `stat(1)`**
- BLOCKED**0**
- 源码修改:**无**
- wrapper/CI**未生成**
@@ -0,0 +1,141 @@
# repo22 压缩功能手工测试报告
- 执行日期:2026-08-08UTC
- 执行时段:约 12:58-13:07 UTC
- 测试对象:`/work/repo-community/repo22`
- 测试提交:`f230129565677e602510347a55a80856237fe909`
- 远端基线:执行前 `xdm/main` 指向同一提交
- FreeBSD VM15.0-RELEASE-p8 amd64QEMU TCGSSH `127.0.0.1:9222`
- 镜像工具:erofs-utils 1.8.6
- 执行方式:逐条 host/guest shell 命令、SSH/SCP、`mdconfig` 和真实内核挂载;未实现 CI 或测试 wrapper
- 总体结论:**压缩功能不可验收。真实 compact LZ4 镜像可挂载,但首个 4 KiB 数据读取返回 `EINTEGRITY`,输出 0 字节。**
## 状态定义
| 状态 | 含义 |
|---|---|
| PASS | 该步骤已经在本轮实际执行,结果符合预期 |
| KERNEL-FAIL | 真实镜像进入 FreeBSD 内核路径后失败 |
| MKFS-UNAVAILABLE | erofs-utils 1.8.6 不能直接生成测试要求的镜像 |
| TEST-DOC | 当前 Markdown 的命令或预期不能可靠验证目标行为 |
| NOT RUN | 已准备 fixture,但收到立即停止新增测试的指令后未进入对应内核路径 |
## 最先出现的内核失败
使用 erofs-utils 1.8.6 生成根目录单文件 compact LZ4 镜像:
```sh
mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \
-zlz4 -C4096 lz4-root-compact-4k.erofs src-root-lz4
```
`dump.erofs` 确认该文件不是 plain fallback
```text
Filesystem features: sb_csum mtime 0padding
Path : /test-lz4.txt
Size: 1048576 On-disk size: 8192 regular file
NID: 39 Layout: 3 Compression ratio: 0.78%
Ext 0: logical 0..639004, physical 4096..8192, physical length 4096
Ext 1: logical 639004..1048576, physical 8192..12288, physical length 4096
```
Guest 中模块加载和挂载成功:
```text
SHA256 (/root/erofs.ko) = c81957971e670c8b8cc45d5119f568eb84adb6114699538588e5a842ea4d97b3
kldload_rc=0
5 1 0xffffffff82822000 8468 erofs.ko
/dev/md0 on /mnt/repo22-test (erofs, local, read-only)
```
文件 lookup 和 `stat` 成功后,第一次读取即失败:
```text
$ sha256 /mnt/repo22-test/test-lz4.txt
sha256: /mnt/repo22-test/test-lz4.txt: Integrity check failed
read_rc=1
$ dd if=/mnt/repo22-test/test-lz4.txt of=/tmp/repo22-first4k bs=4096 count=1
dd: /mnt/repo22-test/test-lz4.txt: Integrity check failed
0+0 records in
0+0 records out
0 bytes transferred
first_read_rc=1
```
因此首个确定性压缩内核失败是:
- fixture:真实 `mkfs.erofs 1.8.6` compact LZ4Layout 3
- mountPASS
- lookup/statPASS
- 首个 4 KiB read**KERNEL-FAIL / `EINTEGRITY`**
- 用户缓冲区:0 字节,没有把部分输出误判为成功;
- 稳定性:命令后 VM 仍响应,时间为 `2026-08-08T13:06:43Z`,未观察到 panic 或 hang。
并发静态审查已经指出 compact 索引位置按 filesystem block size 计算,而不是按 compact index pack 大小计算;本轮失败与该问题相符,但手工测试本身只证明“真实 compact LZ4 首读失败”,不单独宣称完成了代码级根因证明。
## 已尝试 TC 结果
| TC | 状态 | 本轮证据 |
|---|---|---|
| TC003-lz4-compressed-read | **KERNEL-FAIL** | 真实 compact LZ4 镜像 mount PASS;完整 hash 与首个 4 KiB read 都返回 `EINTEGRITY` |
| TC084-lz4-basic | **KERNEL-FAIL** | 与 TC003 共用的最小真实 LZ4 基础读路径失败,无法进行透明解压和内容比较 |
| TC086-lz4-sequential-read | **TEST-DOC** | 用例要求不存在的 `vfs:erofs::read` DTrace provider,并把 TCG VM 上固定吞吐量当功能判据 |
| TC088-lz4-config-handling | **TEST-DOC** | 4K/64K/256K fixture 均生成;“pcluster 越大则镜像必然更小、随机读必然更慢”不是对任意语料都成立的功能断言 |
| TC090-lz4-pcluster-64k | **TEST-DOC** | 步骤 4 把 `dd ... of=/tmp/out` 再 pipe 给 `tail/head`,pipe 中没有文件数据,无法验证跨边界读取 |
| TC098-fragments-support | **TEST-DOC** | EROFS fragments 使用 packed inode,不是 Markdown 所写的独立 fragment device`-o device=/dev/md1` 验证的是多设备而非 fragments |
| TC101-unified-address-mapping | **TEST-DOC** | 前置要求多设备,但 mount 命令只提供 device0`ktrace` 也不能直接证明驱动选中了正确 EROFS device id |
| TC110-lzma-corrupt | **TEST-DOC** | 固定覆盖 image offset 8192 没有先定位目标文件的 compressed extent,可能破坏无关数据或元数据,不能确定性验证 LZMA 错误路径 |
| TC115-unsupported-algorithm | **MKFS-UNAVAILABLE** | mkfs 1.8.6 只提供 lz4/lz4hc/lzma/deflate/libdeflate/zstd,不能直接生成 future algorithm ID;需要有校验意识的定向 ABI patch fixture |
| TC116-truncated-compressed | **TEST-DOC** | 盲目从镜像末尾截去 4096 字节不保证命中目标 compressed extent;可能只移除 padding、其他文件或元数据 |
## 已生成但按停止指令未运行的 fixture
以下 fixture 的 `mkfs.erofs` 和目标布局检查已经成功,但没有据此把对应 TC 标为 PASS:
| TC 范围 | Fixture 准备结果 | TC 状态 |
|---|---|---|
| TC004、TC108、TC109 | 101 MiB MicroLZMA/LZMA level 6Layout 319 个真实 compressed extents | NOT RUN |
| TC085、TC087、TC089 | 256 MiB/50 MiB/4K LZ4 corpuscompact 4K 与 legacy full 镜像均生成 | NOT RUN |
| TC090 | compact 64K big-pcluster 镜像生成,superblock 含 `compr_cfgs big_pcluster` | TEST-DOC,未运行内核读 |
| TC091、TC092 | `-Eztailpacking` 镜像生成,superblock 含 `ztailpacking` | NOT RUN |
| TC093、TC095 | single-device 1 MiB chunk 与 512 KiB chunk-index 镜像生成,Layout 4 | NOT RUN |
| TC094、TC096、TC099 | main image + 5 MiB external blob 生成;首次必须预创建 blob 文件后 mkfs 才成功 | NOT RUN |
| TC097、TC100 | 没有生成确定性 invalid table 或四外部设备 fixture | NOT RUN |
| TC102-TC104 | DEFLATE level 1/6/9 镜像均生成,file1m 为真实 Layout 3 | NOT RUN |
| TC105-TC107 | ZSTD level 1/15/22 镜像均生成,file1m 为真实 Layout 3 | NOT RUN |
收到“立即停止新增测试”指令后,没有继续 mount/read 上述镜像,也没有继续制作 corruption、unsupported algorithm、interlaced、extent 或 partial-ref 变体。
## Fixture 审核摘要
本轮生成并由 `dump.erofs` 确认的主要布局:
| 镜像 | 关键特征 |
|---|---|
| `lz4-compact-4k.erofs` | Layout 3 compact`sb_csum mtime 0padding` |
| `lz4-full-4k.erofs` | Layout 1 full`-Elegacy-compress` |
| `lz4-compact-64k.erofs` | Layout 3`compr_cfgs big_pcluster`;真实物理 extent 最大 53248 字节 |
| `lz4-compact-256k.erofs` | Layout 3`compr_cfgs big_pcluster` |
| `lz4-ztailpacking.erofs` | `ztailpacking` incompat feature |
| `lz4-fragments.erofs` | `fragments dedupe`,目标文件物理长度由 packed inode 提供 |
| `lzma-level6.erofs` | 101 MiB 逻辑文件,126976 字节 on-diskLayout 3 |
| `deflate-level{1,6,9}.erofs` | 三个等级均为真实 compressed Layout 3 |
| `zstd-level{1,15,22}.erofs` | 三个等级均为真实 compressed Layout 3 |
| `chunk-single-1m.erofs` | `chunked_file`Layout 4,三个 1 MiB extent |
| `chunk-index-512k.erofs` | `chunked_file`Layout 48-byte chunk index fixture |
| `chunk-multidev-main.erofs` + `.blob` | 4 KiB metadata image + 5 MiB external blob |
所有镜像、source corpus、VM overlay 和模块 build 产物仅位于 `/work/build`、guest `/root` 或 repo22 忽略的 `build/`,没有加入 Git。
## 其他观察
1. 多文件嵌套目录镜像首次直接 lookup `/files/test-lz4.txt` 偶发返回 `ENOENT`;先执行目录枚举后 `stat` 可成功。根目录单文件 fixture 消除了该前置干扰,并稳定进入压缩 read 后返回 `EINTEGRITY`
2. FreeBSD `ls -l` 对 EROFS 条目打印 `Operation not supported`,同时仍能列出项目;这看起来来自 ACL/扩展属性查询,不应当被误记为目录读取失败。
3. dmesg 中的 `bcmp undefined` 是前一轮旧模块的历史日志;本轮 canonical `/root/erofs.ko``kldload` 返回 0,且模块 SHA-256 为新的 `c819...97b3`
4. 并发静态审查另报 incompat `0x8`、64K `clusterofs` 截断、通用剥前导零和短输出泄漏。本轮遵照停止指令没有再构造独立运行时 fixture 复核这些问题。
## 结论
`f2301295` 的压缩重构已经达到“模块可加载、真实压缩镜像可挂载”的阶段,但尚未达到“可读取最基本 compact LZ4 文件”的最低 feature gate。应先修复 compact zmap/read 闭环,并针对静态审查列出的五个确定性阻断补足定向手测,再恢复其余算法和高级布局验证。
@@ -0,0 +1,84 @@
# repo22 compact LZ4 root-cause verification
- Date: 2026-08-08 UTC
- Source parent: `c939c472134c69fa4665f52ab1aec9a17cd583b9`
- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG
- Image tool: erofs-utils 1.8.6
- Scope: the existing minimal compact LZ4 fixture only; no CI or wrapper was added
## Changes under verification
1. Accept incompat bit `0x8` for compressed HEAD2 while rejecting images with
external device slots until true multi-device I/O exists.
2. Widen zmap `clusterofs` so the 64 KiB NONHEAD sentinel is representable.
3. Count compact indexes using the logical cluster size.
4. Apply leading zero padding according to the algorithm and the LZ4
`0padding` feature instead of stripping it unconditionally for all streams.
5. Zero decompression targets and require exact output lengths.
6. Apply DEFLATE `windowbits` and ZSTD `windowlog` to the decoder calls.
7. Separate LZ4 full and partial completion rules and validate input/output
boundaries.
8. Permit valid compact NONHEAD deltas to cross compact-pack boundaries. The
previous BSD-only restriction caused the first real image read to fail at
the second compressed extent head.
## Build and module lifecycle
`./build.sh` completed successfully. The resulting module had no unresolved
`bcmp` symbol and contained no temporary zmap/zdata diagnostic strings.
On the guest, all lifecycle operations succeeded:
```text
kldload_rc=0
mount_rc=0
umount_rc=0
md_detach_rc=0
kldunload_rc=0
```
## Real image result
The fixture was generated by the existing manual-test command:
```sh
mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \
-zlz4 -C4096 lz4-root-compact-4k.erofs src-root-lz4
```
`dump.erofs` identifies `/test-lz4.txt` as layout 3 (compact), 1 MiB logical,
8 KiB on disk, with two compressed extents. The same image previously failed
its first read with `EINTEGRITY` on `f2301295`.
Final FreeBSD results:
```text
SHA256 (/mnt/repo22-rootfix/test-lz4.txt) = 370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
SHA256 (source test-lz4.txt) = 370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
SHA256 (first 4096 bytes from EROFS) = 3d6d283a95f80b3e4a9c243cf82dc0644283910b6a3be931fede5d771e132b14
SHA256 (first 4096 bytes from source) = 3d6d283a95f80b3e4a9c243cf82dc0644283910b6a3be931fede5d771e132b14
full_read_rc=0
first_4k_read_rc=0
```
Result: the minimal real compact LZ4 mount/read gate passes.
## Integration follow-up
The integration review after `6c9543892` found that the leading-padding
predicate still selected every non-LZ4 algorithm. The predicate was narrowed
to LZ4 streams with the `0padding` incompat feature, matching the stated
algorithm-specific behavior. `./build.sh` and the same FreeBSD 15 compact LZ4
mount/read lifecycle were rerun after this correction; the full-file SHA256
remained `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52`,
the first 4096-byte read returned 4096 bytes, and module unload succeeded.
## Remaining gaps
This batch does not claim runtime completion for legacy full indexes, 64 KiB
or larger pclusters, ztailpacking, interlaced pclusters, fragments, extents,
partial references, MicroLZMA, DEFLATE, or ZSTD images. DEFLATE and ZSTD
configuration handling was corrected and the module loaded on this FreeBSD 15
kernel, but those algorithms were not image-tested in this converged batch.
The LZ4 partial-decoding branch was code-reviewed but is not exercised by the
current synchronous full-extent read path.
@@ -0,0 +1,88 @@
# repo22 non-LZ4 leading-zero verification
- Date: 2026-08-08 UTC
- Source baseline: `7cba92c0ec434b96180200b09e5dddfa213e67f5`
- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG
- Image tool: erofs-utils 1.8.6
- Scope: MicroLZMA, DEFLATE, and ZSTD leading-zero handling only
## Code change
Linux 7.1-rc1 calls `z_erofs_fixup_insize()` for every MicroLZMA, DEFLATE,
and ZSTD stream, while LZ4 only requires it when the `LZ4_0PADDING` feature is
enabled. The FreeBSD dispatcher now applies the same predicate. Existing
first-block limits, empty-input rejection, and input-length subtraction remain
unchanged.
## Build and module lifecycle
`./build.sh` completed successfully and produced `build/erofs.ko` with SHA256:
```text
4b3459a02da29642cb8e61f58e34ff5266e668e86256937c91b8af7e0e31fdcb
```
FreeBSD 15 results:
```text
lifecycle_kldload_rc=0
lifecycle_kldunload_rc=0
dmesg_delta_lines=0
dmesg_error_grep_rc=1
```
The last two lines mean the isolated lifecycle and read pass added no kernel
messages, so no decompression, linker, panic, or integrity error was present.
## Deterministic images
The source file was 1 MiB and had SHA256:
```text
370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
```
The images were regenerated with erofs-utils 1.8.6:
```sh
mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \
-zlzma,level=6 -C4096 lzma-root.erofs src-root
mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \
-zdeflate,level=6 -C4096 deflate-root.erofs src-root
mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \
-zzstd,level=3 -C4096 zstd-root.erofs src-root
```
`dump.erofs` reported layout 3 for `/test-lz4.txt` in all images. MicroLZMA
and ZSTD used one 4096-byte physical extent; DEFLATE used two 4096-byte
physical extents.
## FreeBSD read results
Each image was attached with `mdconfig`, mounted read-only, read completely by
`sha256`, unmounted, and detached before testing the next image.
```text
lzma mount_rc=0 read_rc=0 sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
lzma umount_rc=0 detach_rc=0
deflate mount_rc=0 read_rc=0 sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
deflate umount_rc=0 detach_rc=0
zstd mount_rc=0 read_rc=0 sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
zstd umount_rc=0 detach_rc=0
```
Result: MicroLZMA, DEFLATE, and ZSTD all pass real-image full-file integrity
verification on this FreeBSD 15 kernel configuration.
## Notes and limits
An exploratory image with the file below a `/files` directory mounted, but the
target lookup failed. Root-level fixtures were used to isolate decompression
from the separately tracked directory lookup work. This does not change the
three successful compressed read results above.
This batch does not claim coverage for other compression levels, large files,
random access, corrupt streams, alternative FreeBSD kernel configurations, or
other compression mapping features. DEFLATE and ZSTD kernel symbols were
available on the tested FreeBSD 15 GENERIC kernel; no unsupported algorithm or
kernel-symbol limitation was encountered.
@@ -0,0 +1,99 @@
# repo22 compressed mapping shape manual test report
- Date: 2026-08-08 UTC
- Source baseline: `b1f9e7c0ef5ffb9c4aab38e4047534ad2071af6c`
- Remote baseline before testing: `xdm/main` at the same commit
- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG
- Image tool: erofs-utils 1.8.6
- Module SHA256: `4b3459a02da29642cb8e61f58e34ff5266e668e86256937c91b8af7e0e31fdcb`
- Scope: manual image generation, layout inspection, and the completed compact-index kernel reads; no CI or test wrapper was added
## Status definitions
| Status | Meaning |
|---|---|
| PASS | The stated mkfs/layout or kernel operation was actually run and matched its deterministic expectation |
| KERNEL-FAIL | A real image reached the FreeBSD kernel path and failed |
| MKFS-UNAVAILABLE | erofs-utils 1.8.6 cannot generate the required format directly |
| TEST-DOC | The existing Markdown command or assertion does not deterministically test the stated feature |
| NOT RUN | The fixture was generated, but kernel execution stopped on user request and no feature pass is claimed |
## Generated fixture matrix
All images were regenerated under `/work/build/repo22-shape-manual-20260808T1407Z` with fixed timestamps, root ownership, disabled xattrs, and a cleared UUID. Images, sources, VM state, and module build products were not staged.
| Shape | mkfs/layout status | Evidence | Kernel status |
|---|---|---|---|
| Compact indexes | PASS | default `-zlz4 -C4096`; `/shape.dat` is Layout 3 with two compressed extents | PASS |
| Legacy full indexes | PASS | `-Elegacy-compress`; `/shape.dat` is Layout 1 with two compressed extents | NOT RUN |
| 64 KiB big pcluster | PASS | `-C65536`; superblock reports `compr_cfgs big_pcluster`; Layout 3 has one 1 MiB logical extent backed by 8192 bytes | NOT RUN |
| Inline ztailpacking | PASS | `-Eztailpacking`; superblock reports `ztailpacking`; `/inline.dat` has on-disk size 0 and physical bytes `1352..1787` inside the metadata block | NOT RUN |
| Fragments / packed inode | PASS | `-Eall-fragments`; superblock reports packed NID 42 plus `fragments dedupe`; `/fragment.dat` has on-disk size 0 | NOT RUN |
| Partial reference | PASS | `-Ededupe`; mkfs reports `Dedupe 409572 compressed data (delta 315374)` and `/b.dat` reuses the physical extent at `8192..12288` | NOT RUN |
| Extent metadata format | MKFS-UNAVAILABLE | erofs-utils v1.8.6 has no extent-map advise/structure or mkfs option for the newer extent metadata format; `--max-extent-bytes` only limits decompressed extent size | NOT RUN |
Host-side `fsck.erofs --extract` succeeded for the initially generated compact, full, big-pcluster, ztailpacking-option, and all-fragments images. This is fixture validation only, not a FreeBSD kernel feature pass.
## Completed FreeBSD compact-index verification
The compact image used a deterministic 1 MiB file. `dump.erofs` reported:
```text
Layout: 3
Ext 0: logical 0..639004, physical 4096..8192, physical length 4096
Ext 1: logical 639004..1048576, physical 8192..12288, physical length 4096
```
Guest operations completed as follows:
```text
kldload_rc=0
md_attach_rc=0
mount_rc=0
stat_size=1048576
full_sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
expected_full_sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
cross_extent_offset=638972
cross_extent_length=128
cross_extent_sha256=941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60
expected_cross_extent_sha256=941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60
```
Result: compact full-file integrity and a read crossing the logical boundary immediately before the second compressed extent both pass.
## First failure and dmesg
No KERNEL-FAIL occurred in this batch. The first incomplete command was:
```sh
dd if=/mnt/repo22-shape/shape.dat bs=1 skip=777777 count=8192 | sha256 -q
```
It was operator-terminated when the user requested immediate convergence. Byte-sized `dd` caused repeated page reads under QEMU TCG and was an inefficient manual command; it did not return a kernel error and is not classified as KERNEL-FAIL.
The guest was responsive after termination, and the mount and md device were cleaned up. The dmesg tail contained only successful mapping diagnostics such as:
```text
erofs zread: nid=39 la=1044480 mapla=639004 llen=409572 pa=8192 plen=4096 flags=1 alg=0
erofs zread: nid=39 la=0 mapla=0 llen=639004 pa=4096 plen=4096 flags=1 alg=0
```
There was no new `EINTEGRITY`, decompression error, panic, trap, or hang message. The volume of unconditional `erofs zread` diagnostics should be reviewed separately as a style/logging issue.
## TC disposition
| TC area | Status | Result |
|---|---|---|
| TC084 compact LZ4 basic | PASS | Complete SHA256 and cross-extent boundary read match the source |
| TC088 pcluster configuration | TEST-DOC | Previous monotonic ratio/performance assertions were invalid; commands and pass criteria were corrected |
| TC090 64 KiB pcluster | TEST-DOC | Previous pipeline read from files redirected away from the pipe; deterministic cross-boundary command was corrected; kernel execution remains NOT RUN |
| TC091 ztailpacking | TEST-DOC | Enabling the option alone did not tail-pack the first 1 MiB target; the TC now requires `dump.erofs` proof of an inline extent; kernel execution remains NOT RUN |
| TC098 fragments | TEST-DOC | Previous TC incorrectly described fragments as an external device; it now tests the same-image packed inode; kernel execution remains NOT RUN |
| TC115 unsupported algorithm | MKFS-UNAVAILABLE | mkfs.erofs 1.8.6 only emits its supported algorithm IDs; a checksum-aware ABI patch fixture is required |
| TC116 truncated compressed data | TEST-DOC | Blind `truncate -s -4096` was nondeterministic; the TC now truncates inside a located target extent; kernel execution remains NOT RUN |
TC085-TC087, TC089, TC092-TC101 other than TC098 were read for dependencies but were not executed in this converged batch. Multi-device and chunk tests are independent follow-up work and are not implied by the packed-inode fragment fixture.
## Conclusion
The completed kernel evidence establishes compact-index LZ4 full-read and compressed-extent-boundary correctness on the tested FreeBSD 15 guest. Legacy full indexes, big pclusters, inline ztailpacking, fragments, and partial references are confirmed as real erofs-utils 1.8.6 fixtures but remain explicitly NOT RUN in the FreeBSD kernel after the stop instruction. No kernel failure was observed.
@@ -0,0 +1,53 @@
# repo22 remaining compressed mapping manual read report
- Date: 2026-08-08 UTC
- Source baseline: `df5b92c902295dc74a08aef89fd93d698bbddf21`
- Remote baseline before testing: `xdm/main` at the same commit
- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG
- Image tool: erofs-utils 1.8.6
- Scope: kernel mount/read verification for the five previously generated compression layouts only; no source, CI, or test-wrapper changes
## Result matrix
| Mode | Fixture proof | Mount | Full SHA256 | Offset/boundary read | Isolated dmesg | Result |
|---|---|---:|---|---|---|---|
| Legacy full index | Layout 1, two compressed extents | PASS | `EIO`, no digest | `EIO` at offset 638972 across the 639004 extent boundary | No panic, trap, integrity, corruption, or decompression-error signature; guest responsive | **KERNEL-FAIL** |
| 64 KiB pcluster | Layout 3; `compr_cfgs big_pcluster`; one 1 MiB logical extent backed by 8192 bytes | PASS | Match | Match at offset 65504, length 64 | No error signature; guest responsive | **PASS** |
| Inline ztailpacking | `ztailpacking`; compressed extent at physical 1352..1787 inside metadata block | PASS | Match | Match at offset 31744, length 2048 | No error signature; guest responsive | **PASS** |
| Fragments / packed inode | `fragments`; packed NID; whole-file extent has zero direct on-disk bytes | PASS | Match | Match at offset 65536, length 8192 | No error signature; guest responsive | **PASS** |
| Partial reference | `/b.dat` Layout 1 reuses `/a.dat` physical extent 8192..12288 at logical 641052..1050624 | PASS | `/b.dat` returns `EIO` | `EIO` at offset 641020 crossing into the reused extent; companion Layout 3 `/a.dat` matches | No error signature; guest responsive | **KERNEL-FAIL** |
All five fixtures contain the requested layout. No mode is classified as `FIXTURE-NOT-PROVEN` or `NOT RUN`.
## Integrity evidence
| Target | Full SHA256 |
|---|---|
| Legacy full reference | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` |
| 64 KiB pcluster, actual and expected | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` |
| Inline ztailpacking, actual and expected | `e2aa4a0a0cbcf422f397c7069a38ae0f073781386958e7db0dfa3ff2ca075513` |
| Packed fragment, actual and expected | `a3a83e5c524b5ed446a06ce78cf407192a0c80119f15d2bc3489a50515eb49e0` |
| Partial-ref `/b.dat` reference | `61b17076c2dfae88da7912d00df534b894cf6d27178863c6d8e91f8617ebb91e` |
| Partial-ref companion `/a.dat`, actual and expected | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` |
Offset-read evidence:
| Mode | Actual | Expected |
|---|---|---|
| Legacy full | `f387a3a74488528803454f0c05601fb632d919f1bc5281538f687a3a6231ebea` from partial failed output | `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` |
| 64 KiB pcluster | `b6ebba880cfcc438044f943370b9936a130ce1cecf13d784a1eb871f0a126182` | same |
| Inline ztailpacking | `3a630b7e9618c7e27efb1483058baaa0475ca325c3ecafb42778871d700afa96` | same |
| Packed fragment | `a826cc36ad8c9eaa1606713117fcf118b7bae5fee7503b5619dbf024213059db` | same |
| Partial reference | empty output `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` |
## Failure disposition
The legacy full-index image mounts, but both the complete read and a read crossing its compressed-extent boundary fail with `EIO`. The fixture and its source remain under `/work/build/repo22-shape-manual-20260808T1407Z`; copies plus per-mode dmesg files remain in the guest under `/root/repo22-short-20260808`.
The partial-reference fixture is proven: `/b.dat` reuses the final physical compressed extent of `/a.dat`. However, `/b.dat` itself uses Layout 1 and fails before partial-reference semantics can be accepted. The same image's Layout 3 `/a.dat` reads correctly. This result is therefore a kernel failure with the legacy full-index path as the immediate blocker, not a fixture failure.
Each mode used a separate md attachment and mount lifecycle. Cleanup succeeded after every run, the module remained loaded, and the guest answered a responsiveness check after every failure and pass. The isolated dmesg scans contained no panic, trap, explicit integrity, corruption, I/O-error, or decompression-error message; the two read failures surfaced only as user-visible `EIO`.
## Conclusion
The 64 KiB big-pcluster, inline ztailpacking, and packed-inode fragment read paths pass the requested kernel verification. Legacy full indexes remain broken. The proven partial-reference fixture also fails because its target file traverses the broken Layout 1 path, so partial-reference support cannot be accepted until that blocker is fixed and the same fixture is rerun.
@@ -0,0 +1,41 @@
# repo22 legacy full-index fix manual report
- Date: 2026-08-08 UTC
- Source baseline: `377d467a652beb2124d4adc0d1f8e735fca22344`
- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG
- Module SHA256: `f3b9fa70bfb8e98e2cef436583fd13cb91787514a5be76f9f108698d7586ca3c`
- Scope: legacy full-index and partial-reference minimum regression only
## Root cause and fix
The proven legacy Layout 1 fixture maps its second extent as `HEAD1` with
`clusterofs=28`, `pblk=2`, `m_la=639004`, `m_llen=409572`, and
`m_plen=4096`. The LZ4 stream completes after consuming 2627 bytes; the
remaining 1469 bytes of the pcluster are zero padding. The repo22 decoder
incorrectly required all 4096 input bytes to be consumed and returned `EIO`.
The decoder now accepts complete output only when the remaining pcluster bytes
are all zero, while continuing to reject nonzero trailing data. The mapping
recorder also no longer clears `compressedblks` and `partialref` on every
lcluster load, matching the Linux state machine and preserving lookback state.
## Results
| Test | Mount | Full SHA256 | Boundary read | Result |
|---|---:|---|---|---|
| Legacy full index `/shape.dat` | PASS | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` matches | offset 638972, 128 bytes: `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` matches | **PASS** |
| Partial reference `/b.dat` | PASS | read returns `EIO` | offset 641020 read returns no data | **KERNEL-FAIL** |
`./build.sh`, `kldload`, and exact-name `kldunload` passed. The isolated dmesg
delta was empty, with no panic, trap, or integrity diagnostic.
## Remaining partial-reference hypothesis
The minimal reproducer is the existing
`lz4-partial-ref.erofs` fixture: mount it and read `/b.dat`; the companion
compact `/a.dat` remains the known-good source extent. The full-index mapping
blocker is removed, but `zdata.c` still invokes `z_erofs_decompress()` with
`partial=false` for every mapped extent even when `EROFS_MAP_PARTIAL_REF` is
set. Linux propagates this state to partial LZ4 decoding. Per the convergence
instruction, this batch records that hypothesis without further debugging or
additional source changes.
@@ -0,0 +1,33 @@
# repo22 partial-reference fix manual report
- Date: 2026-08-08 UTC
- Source baseline: `616d23e59b2bc16d9735ca5ad07502a21153bc0a`
- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG
- Module SHA256: `0270671fd73c4ee604294a5f94863f1fc8cc9a63083a4c83f89887891f06f1b2`
- Scope: partial-reference output range and LZ4 regression only
## Root cause and fix
`zmap.c` correctly marked the reused extent with `EROFS_MAP_PARTIAL_REF`, but
`zdata.c` always decompressed the complete mapped logical length and passed
`partial=false`. Linux keeps such pclusters in partial-decoding mode and sets
the decompressor output size to the highest byte needed by the current read.
The synchronous FreeBSD path now preserves that behavior. For a partial
reference, it decodes only through `mapoff + want`, passes `partial=true`, and
copies exactly the requested `want` bytes beginning at `mapoff`. Non-partial
extents still require full logical output and retain strict LZ4 trailing-data
validation.
## Results
| Test | Mount | Full SHA256 | Boundary read | Result |
|---|---:|---|---|---|
| Partial reference `/b.dat` | PASS | `61b17076c2dfae88da7912d00df534b894cf6d27178863c6d8e91f8617ebb91e` matches | offset 641020, 128 bytes: `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` matches | **PASS** |
| Legacy full index `/shape.dat` | PASS | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` matches | smoke only | **PASS** |
| Compact index `/shape.dat` | PASS | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` matches | smoke only | **PASS** |
`./build.sh`, `kldload`, and exact-name `kldunload` passed. The isolated dmesg
delta was empty, with no panic, trap, integrity diagnostic, or decompression
error. Build objects, kernel module output, images, and VM overlays were not
staged for commit.
@@ -0,0 +1,46 @@
# repo22 ACL/xattr integration smoke report
- Date: 2026-08-08 UTC
- Source baseline before this batch: `8e629bb9f798f0c76497bd2b623329a0aff430b9`
- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG
- Module SHA256: `18967bd32e89b08a2363241ed4b9b67344b7bfb555f1f77daa64f382925e4fda`
- Scope: ACL/xattr worktree integration, build/link validation, and minimal
Markdown-directed manual smoke tests
## Results
| Check | Result | Evidence |
|---|---|---|
| `./build.sh` | PASS | `build/erofs.ko` generated without errors |
| unresolved `bcmp` | PASS | `nm -u build/erofs.ko` contains no `bcmp` |
| ACL kernel dependency | PASS | module declares `acl_posix1e`; `kldload` succeeds |
| module lifecycle | PASS | `kldload`, `kldstat`, and exact module-ID `kldunload` succeed |
| TC005 user xattr smoke | PASS | `user.comment` lists and reads as `repo22-user-xattr` |
| TC082 access ACL smoke | PASS | system xattr lists `posix_acl_access`; raw ACL reads successfully |
| FreeBSD ACL decode | PASS | `getfacl` reports named user, group, mask, and other entries |
| TC060 ACL capability | PASS | `getconf ACL_EXTENDED` returns `1` |
| read-only access smoke | PASS | read succeeds and write access is denied |
| cleanup | PASS | no EROFS module, mount, or md device remains |
The ACL fixture contained a real extended POSIX.1e access ACL with a named
user and mask, so Linux did not collapse it into mode bits. `dump.erofs`
reported 88 bytes of inode xattrs. FreeBSD returned:
```text
user::rw-
user:ntpd:r--
group::r--
mask::r--
other::---
```
The fixture image, source directory, kernel module, VM overlay, and generated
objects remained under `/work/build` or `repo22/build` and were not staged.
## Remaining scope
- Default-directory ACL retrieval and access checks under multiple credentials
were not exercised in this smoke batch.
- Shared-xattr base calculation, long/packed prefixes, xattr filters, malformed
xattr bounds, and metabox-backed shared xattrs still require dedicated tests.
- Multi-device work is outside this ACL/xattr integration batch.
@@ -0,0 +1,112 @@
# 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.
@@ -0,0 +1,199 @@
#!/bin/sh
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
fixture_dir="$script_dir/fixture"
artifact_dir="$script_dir/artifacts"
mkdir -p \
"$fixture_dir/alpha/bravo/charlie" \
"$fixture_dir/alpha/sibling" \
"$fixture_dir/wide" \
"$artifact_dir"
printf '%s\n' 'cold nested lookup payload' > \
"$fixture_dir/alpha/bravo/charlie/payload.txt"
printf '%s\n' 'repeat lookup payload' > \
"$fixture_dir/alpha/bravo/repeat.txt"
printf '%s\n' 'sibling marker' > \
"$fixture_dir/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/wide/$name"
index=$((index + 1))
done
find "$fixture_dir" -exec touch -h -t 197001010000.00 {} +
build_image()
{
image=$1
mkfs.erofs -d0 -x-1 -T0 --all-time --all-root --workers=1 \
-U 11111111-2222-3333-4444-555555555555 \
"$image" "$fixture_dir"
}
build_image "$artifact_dir/nested.erofs"
build_image "$artifact_dir/nested-repeat.erofs"
cmp "$artifact_dir/nested.erofs" "$artifact_dir/nested-repeat.erofs"
ARTIFACT_DIR="$artifact_dir" python3 <<'PY'
import hashlib
import os
import shutil
import struct
artifact_dir = os.environ["ARTIFACT_DIR"]
base_path = os.path.join(artifact_dir, "nested.erofs")
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, 1024 + 4, 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, 1024 + 4, checksum & 0xFFFFFFFF)
def inode_offset(nid):
return nid << 5
def compact_inline_dir(image, nid):
offset = inode_offset(nid)
inode_format = u16(image, offset)
layout = (inode_format >> 1) & 0x7
assert (inode_format & 0x1) == 0
assert layout == 2
assert u16(image, offset + 2) == 0
return offset, offset + 32, u32(image, offset + 8)
def dir_entries(image, data_offset, size):
first_nameoff = u16(image, data_offset + 8)
assert first_nameoff >= 12
assert first_nameoff % 12 == 0
assert first_nameoff < size
count = first_nameoff // 12
entries = []
for index in range(count):
entry_offset = data_offset + index * 12
nid, nameoff, file_type, reserved = struct.unpack_from(
"<QHBB", image, entry_offset
)
endoff = (
u16(image, data_offset + (index + 1) * 12 + 8)
if index + 1 < count
else size
)
assert first_nameoff <= nameoff < endoff <= size
slot = bytes(image[data_offset + nameoff : data_offset + endoff])
name = slot.split(b"\0", 1)[0]
assert name
entries.append((nid, nameoff, file_type, reserved, name))
return entries
with open(base_path, "rb") as source:
base = bytearray(source.read())
assert u32(base, 1024) == 0xE0F5E1E2
assert base[1024 + 12] == 12
assert u16(base, 1024 + 14) == 36
root_nid = 36
root_inode, root_data, root_size = compact_inline_dir(base, root_nid)
root_entries = dir_entries(base, root_data, root_size)
assert [entry[4] for entry in root_entries] == [b".", b"..", b"alpha", b"wide"]
dot_omitted = bytearray(base)
removed = 13
new_size = root_size - removed
new_entries = root_entries[1:]
new_first_nameoff = len(new_entries) * 12
for index, entry in enumerate(new_entries):
nid, nameoff, file_type, reserved, _ = entry
struct.pack_into(
"<QHBB",
dot_omitted,
root_data + index * 12,
nid,
nameoff - removed,
file_type,
reserved,
)
names = b"".join(entry[4] for entry in new_entries)
dot_omitted[root_data + new_first_nameoff : root_data + new_size] = names
dot_omitted[root_data + new_size : root_data + root_size] = b"\0" * (
root_size - new_size
)
put_u16(dot_omitted, root_inode, u16(dot_omitted, root_inode) | (1 << 4))
put_u32(dot_omitted, root_inode + 8, new_size)
assert [entry[4] for entry in dir_entries(dot_omitted, root_data, new_size)] == [
b"..",
b"alpha",
b"wide",
]
short_block = bytearray(base)
put_u32(short_block, root_inode + 8, 8)
nonmonotonic = bytearray(base)
first_nameoff = u16(nonmonotonic, root_data + 8)
put_u16(nonmonotonic, root_data + 12 + 8, first_nameoff)
wide_nid = next(entry[0] for entry in root_entries if entry[4] == b"wide")
wide_inode = inode_offset(wide_nid)
assert ((u16(base, wide_inode) >> 1) & 0x7) == 2
wide_startblk = u32(base, wide_inode + 16)
wide_block = wide_startblk << 12
wide_first_nameoff = u16(base, wide_block + 8)
wide_count = wide_first_nameoff // 12
wide_last_nameoff = u16(base, wide_block + (wide_count - 1) * 12 + 8)
padding_nul = base.index(0, wide_block + wide_last_nameoff, wide_block + 4096)
assert padding_nul + 1 < wide_block + 4096
assert base[padding_nul + 1] == 0
bad_padding = bytearray(base)
bad_padding[padding_nul + 1] = ord("X")
outputs = {
"dot-omitted.erofs": dot_omitted,
"corrupt-short-block.erofs": short_block,
"corrupt-nameoff.erofs": nonmonotonic,
"corrupt-padding.erofs": bad_padding,
}
for filename, image in outputs.items():
update_superblock_checksum(image)
path = os.path.join(artifact_dir, filename)
with open(path, "wb") as output:
output.write(image)
with open(os.path.join(artifact_dir, "SHA256SUMS"), "w", encoding="ascii") as sums:
for filename in sorted(["nested.erofs", "nested-repeat.erofs", *outputs]):
path = os.path.join(artifact_dir, filename)
with open(path, "rb") as image_file:
digest = hashlib.sha256(image_file.read()).hexdigest()
sums.write(f"{digest} {filename}\n")
PY
cat "$artifact_dir/SHA256SUMS"
@@ -0,0 +1,71 @@
#include <sys/types.h>
#include <sys/dirent.h>
#include <dirent.h>
#include <err.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main(int argc, char **argv)
{
struct dirent *entry;
off_t base, before, start;
char *buffer;
char *end;
size_t buffer_size;
ssize_t bytes;
int calls, fd;
if (argc < 2 || argc > 5)
errx(2, "usage: %s directory [offset [buffer-size [calls]]]",
argv[0]);
start = argc >= 3 ? strtoll(argv[2], NULL, 0) : 0;
buffer_size = argc >= 4 ? strtoul(argv[3], NULL, 0) : 128;
calls = argc >= 5 ? strtol(argv[4], NULL, 0) : 32;
if (buffer_size < 32 || calls < 1)
errx(2, "invalid buffer size or call count");
fd = open(argv[1], O_RDONLY | O_DIRECTORY);
if (fd < 0)
err(1, "open %s", argv[1]);
if (lseek(fd, start, SEEK_SET) < 0)
err(1, "lseek %jd", (intmax_t)start);
buffer = malloc(buffer_size);
if (buffer == NULL)
err(1, "malloc");
for (int call = 0; call < calls; call++) {
before = lseek(fd, 0, SEEK_CUR);
if (before < 0)
err(1, "lseek current");
base = -1;
bytes = getdirentries(fd, buffer, buffer_size, &base);
if (bytes < 0)
err(1, "getdirentries");
printf("call=%d before=%jd after=%jd base=%jd bytes=%zd\n",
call, (intmax_t)before,
(intmax_t)lseek(fd, 0, SEEK_CUR), (intmax_t)base, bytes);
if (bytes == 0)
break;
end = buffer + bytes;
for (entry = (struct dirent *)buffer;
(char *)entry < end;
entry = (struct dirent *)((char *)entry + entry->d_reclen)) {
if (entry->d_reclen == 0 ||
(char *)entry + entry->d_reclen > end)
errx(1, "invalid dirent record");
printf(" off=%jd ino=%ju reclen=%u type=%u name=%.*s\n",
(intmax_t)entry->d_off, (uintmax_t)entry->d_fileno,
entry->d_reclen, entry->d_type, entry->d_namlen,
entry->d_name);
}
}
free(buffer);
close(fd);
return (0);
}
@@ -0,0 +1,201 @@
#!/bin/sh
set -u
test_dir=/root/repo22-namei
mount_dir=/mnt/repo22-namei
module_id=
md_device=
fail()
{
echo "FAIL: $*" >&2
exit 1
}
cleanup()
{
set +e
if mount | grep -q " on $mount_dir "; then
umount "$mount_dir"
fi
if [ -n "$md_device" ]; then
mdconfig -d -u "${md_device#md}"
fi
if [ -n "$module_id" ] && kldstat -q -i "$module_id"; then
kldunload -i "$module_id"
fi
}
trap cleanup EXIT INT TERM
mount_image()
{
image=$1
md_device=$(mdconfig -a -t vnode -f "$test_dir/$image") ||
fail "mdconfig $image"
mount -t erofs "/dev/$md_device" "$mount_dir" ||
fail "mount $image"
echo "mounted image=$image device=$md_device"
}
unmount_image()
{
umount "$mount_dir" || fail "umount $md_device"
mdconfig -d -u "${md_device#md}" || fail "detach $md_device"
md_device=
}
expect_integrity_failure()
{
image=$1
lookup_path=$2
readdir_path=$3
label=${image%.erofs}
mount_image "$image"
attempt=1
while [ "$attempt" -le 2 ]; do
output="$test_dir/$label-lookup-$attempt.txt"
if stat "$mount_dir/$lookup_path" >"$output" 2>&1; then
fail "$image lookup attempt $attempt unexpectedly succeeded"
fi
if grep -qi "No such file" "$output"; then
fail "$image lookup attempt $attempt became ENOENT"
fi
attempt=$((attempt + 1))
done
output="$test_dir/$label-readdir.txt"
if ./readdir_probe "$mount_dir/$readdir_path" 0 512 2 \
>"$output" 2>&1; then
fail "$image readdir unexpectedly succeeded"
fi
unmount_image
echo "integrity image=$image repeated-lookup=error readdir=error"
}
cd "$test_dir" || exit 1
mkdir -p "$mount_dir"
if mount | grep -qi erofs; then
fail "pre-existing EROFS mount"
fi
if kldstat | grep -qi erofs; then
fail "pre-existing EROFS module"
fi
if [ -n "$(mdconfig -l)" ]; then
fail "pre-existing md device"
fi
echo "== guest =="
uname -a
date -u
echo "== module load =="
kldload "$test_dir/erofs.ko" || fail "kldload"
module_id=$(kldstat | awk '$NF == "erofs.ko" { print $1 }')
[ -n "$module_id" ] || fail "loaded module not found"
kldstat -v -i "$module_id"
echo "== cold nested lookup =="
mount_image nested.erofs
payload=$(cat "$mount_dir/alpha/bravo/charlie/payload.txt") ||
fail "cold nested lookup"
[ "$payload" = "cold nested lookup payload" ] || fail "cold payload mismatch"
payload=$(cat "$mount_dir/alpha/bravo/charlie/payload.txt") ||
fail "repeated nested lookup"
[ "$payload" = "cold nested lookup payload" ] || fail "repeat payload mismatch"
payload=$(cat "$mount_dir/alpha/bravo/repeat.txt") ||
fail "sibling nested lookup"
[ "$payload" = "repeat lookup payload" ] || fail "sibling payload mismatch"
attempt=1
while [ "$attempt" -le 2 ]; do
output="$test_dir/missing-$attempt.txt"
if stat "$mount_dir/alpha/bravo/missing" >"$output" 2>&1; then
fail "missing lookup attempt $attempt unexpectedly succeeded"
fi
grep -qi "No such file" "$output" || fail "missing lookup was not ENOENT"
attempt=$((attempt + 1))
done
payload=$(cat "$mount_dir/alpha/bravo/charlie/payload.txt") ||
fail "existing lookup after negative cache"
[ "$payload" = "cold nested lookup payload" ] || fail "post-negative payload mismatch"
echo "cold lookup=pass repeat=pass negative-cache=pass"
echo "== large readdir and cookies =="
wide_count=$(ls -A1 "$mount_dir/wide" | wc -l | tr -d ' ')
[ "$wide_count" = 320 ] || fail "wide entry count $wide_count"
./readdir_probe "$mount_dir/wide" 0 128 400 > wide-probe.txt ||
fail "wide readdir probe"
awk '/^ off=/ {
off = $1; sub(/^off=/, "", off);
name = $5; sub(/^name=/, "", name);
print off, name;
}' wide-probe.txt > wide-cookies.txt
wide_dirents=$(wc -l < wide-cookies.txt | tr -d ' ')
[ "$wide_dirents" = 322 ] || fail "wide dirent count $wide_dirents"
awk '{ cookie[NR] = $1; name[NR] = $2 }
END {
for (i = 1; i <= NR; i++)
print cookie[i], (i < NR ? name[i + 1] : "<EOF>");
}' wide-cookies.txt > wide-resume-cases.txt
while read -r cookie expected; do
./readdir_probe "$mount_dir/wide" "$cookie" 128 1 > wide-resume.txt ||
fail "resume cookie $cookie"
if [ "$expected" = "<EOF>" ]; then
grep -q 'bytes=0$' wide-resume.txt || fail "cookie $cookie not EOF"
else
actual=$(awk '/^ off=/ {
name = $5; sub(/^name=/, "", name); print name; exit;
}' wide-resume.txt)
[ "$actual" = "$expected" ] ||
fail "cookie $cookie expected $expected got $actual"
fi
done < wide-resume-cases.txt
echo "wide entries=$wide_dirents all-resume-cookies=pass"
unmount_image
echo "== dot omitted cookies =="
mount_image dot-omitted.erofs
./readdir_probe "$mount_dir" 0 512 8 > dot-probe.txt || fail "dot probe"
awk '/^ off=/ {
off = $1; sub(/^off=/, "", off);
name = $5; sub(/^name=/, "", name);
print off, name;
}' dot-probe.txt > dot-cookies.txt
cat > dot-expected.txt <<'EOF'
12 ..
24 alpha
47 wide
48 .
EOF
cmp dot-cookies.txt dot-expected.txt || fail "dot cookie sequence"
./readdir_probe "$mount_dir" 47 128 1 > dot-resume-47.txt || fail "dot resume 47"
grep -q '^ off=48 .* name=\.$' dot-resume-47.txt || fail "dot resume 47 result"
./readdir_probe "$mount_dir" 48 128 1 > dot-resume-48.txt || fail "dot resume 48"
grep -q 'bytes=0$' dot-resume-48.txt || fail "dot resume 48 not EOF"
echo "dot cookies=12,24,47,48 resume=pass"
unmount_image
echo "== corrupted directories =="
expect_integrity_failure corrupt-short-block.erofs alpha .
expect_integrity_failure corrupt-nameoff.erofs alpha .
expect_integrity_failure \
corrupt-padding.erofs \
wide/entry-000-abcdefghijklmnopqrstuvwxyz.txt \
wide
echo "== module unload =="
kldunload -i "$module_id" || fail "kldunload"
module_id=
if kldstat | grep -qi erofs; then
fail "module remains loaded"
fi
if mount | grep -qi erofs; then
fail "mount remains"
fi
if [ -n "$(mdconfig -l)" ]; then
fail "md remains"
fi
dmesg | tail -120 > dmesg-tail.txt
echo "PASS: all VM regressions"
@@ -0,0 +1,113 @@
# repo22 xattr/ACL root-cause manual test report
Date: 2026-08-08 18:12 UTC; final integration rerun on 2026-08-08
Baseline: `29a215dd4519579f6313b3df67d524a6b4bdf3ca` plus the scoped xattr/ACL fixes
Guest: FreeBSD 15.0-RELEASE-p8 amd64
Host tools: erofs-utils 1.8.6 plus deterministic transformed fixtures
## Build and module
- `git diff --check`: PASS.
- `./build.sh`: PASS; final `build/erofs.ko` SHA-256 is
`50a19ea96c7414f73d92049671e66a9eacc9ff5abe27950a46d16c8e6c763baf`.
- `nm -u build/erofs.ko | grep -w bcmp`: no match, PASS.
- `kldload /tmp/repo22-integration-20260808/erofs.ko`: PASS.
- Repeated `mdconfig -a -t vnode -f IMAGE`, `mount -t erofs`, `umount`, and
`mdconfig -d`: PASS for every fixture below.
- `kldunload erofs`: PASS; final module, EROFS mount, and md-device counts were
all zero.
- New dmesg errors, traps, or panics: none.
## Existing xattr and ACL regression
- Inline user, trusted, and security xattrs: PASS.
- Shared user xattrs and shared POSIX access ACL: PASS.
- Packed long-prefix table and xattr-name-filter: PASS.
- Access/default ACL decode and inherited child ACL: PASS.
- Owner, owning-group, named-user, and other access matrix: PASS.
- Read-only ACL/xattr mutation rejection: PASS.
- TC117 malformed name length and value size: PASS. Xattr operations returned
`EINTEGRITY`; directory metadata and file contents remained readable.
- An invalid long-prefix reference without a declared prefix table is now
skipped like Linux. Other valid xattrs and file data remain readable.
## New regression cases
- TC134 metabox shared nonzero base: PASS with
`metabox-shared-nonzero-base.erofs`. The image has
`SHARED_EA_IN_METABOX`, `xattr_blkaddr=1`, and a plain metabox carrier.
`getextattr -qq user metaboxshared /mnt/repo22-integration/hello.txt`
returned exactly `nonzero-base`.
- TC135 metabox long-prefix backing: PASS with
`metabox-prefix-shared.erofs`.
`getextattr -qq user repo22.application.component.setting` on
`/mnt/repo22-integration/long/file2.txt` returned `long-prefix-value`.
Primary-image long-prefix fallback also passed with
`long-prefix-primary-fallback.erofs` and the same expected value.
- TC136 truncated metabox extension: PASS; mount returned `EINTEGRITY`.
Out-of-range ishare prefix ID: PASS; mount returned `EINTEGRITY`.
- TC137 shared xattr outside declared image: PASS; normal data and an in-bounds
xattr remained readable, while the redirected entry returned `EINTEGRITY`.
Prefix record outside declared image: PASS; mount failed. The valid primary
fallback control mounted and returned the expected value.
- TC138 unordered unique UID qualifiers: PASS and preserved order 3002, 2002.
Header-only ACL: PASS and fell back to mode. Duplicate UID qualifier: PASS
negative test and returned `EINTEGRITY`.
- TC139 FIFO access ACL, list/get system xattr: PASS. `setfacl`, `setextattr`,
and `rmextattr` all returned read-only filesystem errors.
- TC140 compressed metabox: PASS with `metabox-large-shared.erofs`. On-disk
qualification found `METABOX`, metabox inode datalayout 1
(`EROFS_INODE_COMPRESSED_FULL`), and no fragment pcluster. A cold file read,
the metabox long-prefix lookup, and a 231-byte xattr value all matched the
source fixture.
## Final metabox and metadata-boundary smoke
The final combined module was exercised with these exact guest operations:
```sh
kldload /tmp/repo22-integration-20260808/erofs.ko
mdconfig -a -t vnode -f /tmp/repo22-integration-20260808/IMAGE.erofs
mount -t erofs /dev/md0 /mnt/repo22-integration
getextattr -qq user NAME /mnt/repo22-integration/PATH
umount /mnt/repo22-integration
mdconfig -d -u 0
kldunload erofs
```
- Compressed metabox and metabox long-prefix:
`metabox-large-shared.erofs` returned `long-prefix-value` for
`repo22.application.component.setting`; its regular file content also
matched `long prefix fixture 1`.
- Primary shared fallback while `METABOX` is enabled:
`metabox-large-shared.erofs` has `SHARED_EA_IN_METABOX` clear and
`xattr_blkaddr=1`; `shared_key` returned `repo22-shared-value` from the
primary shared-xattr area.
- `SHARED_EA_IN_METABOX` nonzero base:
`metabox-shared-nonzero-base.erofs` returned `nonzero-base` from the shared
entry addressed relative to metabox block 1.
- Cross-metadata shared xattr: `shared-cross-metadata.erofs` returned
`repo22-shared-value` for `shared_key` on `shared/file3.txt`.
- Cross-metadata inline xattr: `xattr-cross-metadata.erofs` returned the exact
expected `alpha`, `gamma`, and `delta` values on `file000`, `file090`, and
`file179`. The metabox-specific inline boundary fixture also returned
`valid-inline-boundary` for `crossboundary`.
The fragment-backed compressed metabox negative layout was **not generated and
was not executed**. The passing TC140 fixture is compressed but non-fragment.
The code still rejects a compressed metabox inode whose fragment flag is set;
that rejection remains layout-reviewed rather than fixture-verified.
## Fixture qualification
The old `namespace-shared.erofs` contains an xattr entry with name index `0x80`
but advertises no `XATTR_PREFIXES` feature and contains no prefix table. It is
not a valid long-prefix fixture. The driver now follows Linux behavior by
skipping that unresolved entry while preserving all valid xattrs and file data.
## Separate namei write set
The previously observed cold nested lookup issue was fixed by the separate
`src/namei.c` and `src/dir.c` write set and passed TC141 in the same final
integration build. Those source files are intentionally excluded from the
xattr/ACL commit.
@@ -0,0 +1,232 @@
# repo22 real multi-device manual test report
Date: 2026-08-08 19:37 UTC
Baseline: `96e22cc713cc57180ce3ecb2b98852f090e3868b` plus this multi-device batch
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
Host tools: erofs-utils 1.8.6
## Result
**PASS**. Real external providers, explicit slot mapping, flatdev, unified
addresses, 48-bit fields, failure rollback, forced GEOM orphaning, and the
single-device regressions all passed. There were no new dmesg lines, traps, or
panics. Final EROFS mount, md-provider, and loaded-module counts were zero.
## Build and module
Commands:
```sh
git diff --check
./build.sh
nm -u build/erofs.ko | awk '$NF == "bcmp" {bad=1} END {exit bad}'
sha256sum build/erofs.ko
```
Actual:
- `git diff --check`: PASS.
- Cross-build against `/work/dev-freebsd-releng`: PASS.
- Unresolved `bcmp`: none, PASS.
- Final `build/erofs.ko` SHA256:
`aa6708256246b303f4be2a8d516bf503f8a232df1ad3f466f5685573f221c180`.
- Final `kldload` and `kldunload`: PASS.
## Fixture commands
The legal baseline was generated by erofs-utils 1.8.6, not by synthesizing an
EROFS image from scratch:
```sh
WORK=/work/build/repo22-multidev-fixtures-20260808
mkdir -p "$WORK/src/cross"
python3 - <<'PY'
from pathlib import Path
import hashlib
root = Path('/work/build/repo22-multidev-fixtures-20260808/src')
def content(label, blocks, tail=0):
out = bytearray()
for block in range(blocks):
seed = f'{label}:block:{block}'.encode()
chunk = bytearray()
counter = 0
while len(chunk) < 4096:
chunk += hashlib.sha256(
seed + counter.to_bytes(4, 'little')).digest()
counter += 1
out += chunk[:4096]
if tail:
out += hashlib.sha256(f'{label}:tail'.encode()).digest()[:tail]
return bytes(out)
root.joinpath('cross/striped.bin').write_bytes(content('striped', 24, 173))
root.joinpath('cross/second.bin').write_bytes(content('second', 17, 29))
root.joinpath('control.txt').write_text(
'repo22 multidev deterministic control\n', encoding='ascii')
PY
truncate -s 0 "$WORK/base.blob"
mkfs.erofs -T0 --all-root --chunksize=4096 \
--blobdev="$WORK/base.blob" "$WORK/base.primary" "$WORK/src"
```
erofs-utils 1.8.6 accepts one `--blobdev`; direct generation of two or more
external slots is therefore **MKFS-UNAVAILABLE**. Multi-slot fixtures were made
by a structured, assertion-driven patch of the legal baseline:
1. `dump.erofs --path=PATH -e base.primary` supplied each NID and file size.
2. The patch asserted compact chunk layout, zero xattr size, index format, old
device ID 1, and every old block address.
3. A complete physical chunk, rounded to 4096 bytes for the final partial
chunk, was copied into a deterministic round-robin blob.
4. Each index was rewritten as little-endian
`(startblk_hi, device_id, startblk_lo)`.
5. One zeroed primary block was appended for the enlarged device table;
`blocks_lo`, `extra_devices`, and `devt_slotoff` were updated. This avoids
overwriting inode metadata merely to add slots.
6. Each 128-byte slot was written at offsets 64/68/72/74 with `blocks_lo`,
`uniaddr_lo`, `blocks_hi`, and `uniaddr_hi`.
7. The EROFS CRC32C was recomputed over bytes 1024 through 4095 with polynomial
`0x82f63b78` and initial value `0xffffffff`.
The asserted patch manifest was:
```text
/control.txt nid=44 size=38 chunk=4096 indexes=1 base=1440
/cross/second.bin nid=50 size=69661 chunk=4096 indexes=18 base=1632
/cross/striped.bin nid=56 size=98336 chunk=32768 indexes=4 base=1824
```
Pre-kernel qualification:
```sh
fsck.erofs --device=two.blob1 --device=two.blob2 \
--extract=two.extract two.primary
fsck.erofs --device=three.blob1 --device=three.blob2 \
--device=three.blob3 --extract=three.extract three.primary
fsck.erofs --device=four.blob1 --device=four.blob2 \
--device=four.blob3 --device=four.blob4 \
--extract=four.extract four.primary
```
All extracted source SHA256 values matched. The 48-bit feature and patched
`device_id=0 + uniaddr` extraction are not understood by erofs-utils 1.8.6;
those layouts were qualified by the FreeBSD kernel SHA256 tests below and the
Linux 7.1 `erofs_map_dev` semantics.
FreeBSD attachment and mount commands used one md provider per image/blob:
```sh
mdconfig -a -t vnode -f primary -u 90
mdconfig -a -t vnode -f blob1 -u 91
mdconfig -a -t vnode -f blob2 -u 92
mount -t erofs -o ro -o device.2=/dev/md92 \
-o device.1=/dev/md91 /dev/md90 /mnt/repo22-multidev
```
The reversed option order was intentional. Flatdev used only:
```sh
mdconfig -a -t vnode -f two.flat -u 90
mount -t erofs -o ro /dev/md90 /mnt/repo22-multidev
```
## Fixture SHA256
| Fixture | SHA256 |
|---|---|
| one-blob primary | `fa2f35f59f63cc4a56d230ba960a6c1d27748506fccce081447cab0dc0d540fd` |
| one-blob slot 1 | `3aeb19c519636b61dc3ebd47c6286098deaa0990ff618ce3facbff4fd65907cd` |
| two-slot primary | `46d76d5cc311f97f263ea4c20a2510338f5b11da9d71e0c73f69ae3751afd696` |
| two-slot blob 1 | `53853d033adf154a956192d9813726888c3f9575ecd13fa45a26aea5c4f07eaf` |
| two-slot blob 2 | `9ee3e3ad420024ff1945d92469d3d337817977f28c5b745c4ca7b1664f4d6584` |
| two-slot flatdev | `4b561db1eb0048989256f3c51504b4611881069bf283fbfe2051847e8162301e` |
| three-slot primary | `4f2dc4ee6385f31139151e1b04279a8fb90fcd2cbba928ee6949df62f1bb9585` |
| three-slot blobs 1/2/3 | `12c5d73cb12e1bb5fd1a9d9715ec88d2132dbaa8df62e2b5aad3cfb454116a3a`, `3d1574254a97e572fca323f153bb2a46ebdfb060a0765298700768e39a7f91d8`, `ba42a2a3724771975c27a43ac8d64a2568515fb4e8ad14b29cc09a7a865ffba0` |
| four-slot primary | `d3366dfb5f4c98db2db669ead3209a0abbba5df989d9aea7e1217aff46f0154b` |
| four-slot blobs 1/2/3/4 | `ad24e257baf97f855f756cc917878a9616858860ddbfab4ffe3cc362e9233662`, `eb55bd05f873dd8732bcbe35a0818ef232a9eff6fedec618f5f949253d7cd2d1`, `59f9a3aed34cc8598e6a96381b55a18f6a0e6674e3e26921834948ad333cb3c5`, `d5080743a85e47752cd33e86e26690b8978d314e685b27d449d5394c44e54244` |
| device ID 0 + uniaddr | `e4cf71f76f2a2e690e905e4135c21ce29d647be5dd2b6ce567ac19f82d1875d2` |
| 48-bit high uniaddr | `00d49beea1237125886f13912bf062153b992582c1cf3a2494b44679285b192e` |
| overlapping-slot negative | `6ecfc77b688ac1931f3e904f48fa43a81c05a6047c21ccfc4636e9e07c17d5cf` |
| mapped-ID-3 negative | `dd6ddc6943b7edd2009a48999d03a2d5b69a9ce7d820357527dbcdb52adb2afc` |
Source SHA256 values for the cross-device fixture:
```text
6a38f93ca44de4affcef23ad5979505225f21e5b6912b5c886866170c007b16c ./control.txt
45b7df16a5833013042694c2e8171d782102bf20abbc6c1ec2789518d5a65125 ./cross/second.bin
7ab83ca35cc4bb361bf60b5619d3f55b717fb8e2a7a81a2f069dce859efe8de9 ./cross/striped.bin
```
## Test matrix
| Test | Expected | Actual | Status |
|---|---|---|---|
| TC093 plain single device | full SHA256 match | both files matched | PASS |
| TC093 chunk single device | device ID 0 reads primary | both files matched | PASS |
| compression regression | LZ4 data unchanged | both files matched | PASS |
| TC098 fragments | packed-inode file matches | `65b9a59e...e7cd` | PASS |
| metabox regression | cold metabox-backed files match | `f1` and `f10` matched known hashes | PASS |
| TC006/TC099 primary + one blob | all external chunks use slot 1 | three files matched | PASS |
| TC094 two external slots | reversed option order, cross-slot SHA256 | all three files matched | PASS |
| TC100 four providers total | slots 1-3, order 3/1/2 | all three files matched | PASS |
| additional four external slots | file crosses four blobs | SHA256 matched | PASS |
| statfs combined blocks | sum primary and external blocks | 184 KiB reported for 46 blocks | PASS |
| TC101 flatdev | non-zero IDs add `uniaddr` on primary | SHA256 matched | PASS |
| TC101 device ID 0 + uniaddr | unified range selects external provider | SHA256 matched | PASS |
| 48-bit root/index/uniaddr | high fields are not truncated | source SHA256 matched | PASS |
## Error and rollback matrix
| Case | Expected | Actual | Status |
|---|---|---|---|
| split primary, no external options | `ENXIO` flatdev media bound | `Device not configured` | PASS |
| missing slot 2 | `ENXIO` | `external devices don't match ... Device not configured` | PASS |
| short slot 2 | `ENXIO` | `Device not configured` | PASS |
| swapped short/long providers | fail before root | `Device not configured` | PASS |
| same provider in two slots | `EINVAL` | `Invalid argument` | PASS |
| overlapping unified ranges | `EINTEGRITY` | `Integrity check failed` | PASS |
| explicit writable request | `EROFS` | `Read-only file system` | PASS |
| mapped device ID 3 with two slots | read-time `ENODEV` | `Operation not supported by device` | PASS |
| second concurrent reuse mount | `EBUSY` | `Device busy`; first mount remained readable | PASS |
| normal detach while mounted | `EBUSY` | `mdconfig ... Device busy` | PASS |
| forced orphan then cold slot-2 read | `ENXIO` | `Device not configured` | PASS |
| unmount after forced orphan | clean close of detached consumer | succeeded | PASS |
Every mount failure was followed immediately by successful md detach. Opened
external devices were therefore rolled back, including failures after one or
more earlier slots had opened.
## dmesg and cleanup
The final matrix captured `dmesg` before and after all tests:
```text
before_lines=2
after_lines=2
no-new-lines
```
The two pre-existing lines were unchanged historical messages:
```text
interface erofs.1 already present in the KLD 'erofs-repo22-xattrfix.ko'!
md0: truncating fractional last sector by 14 bytes
```
Final state:
```text
kldstat -n erofs: empty
mdconfig -l: empty
mount -p | grep erofs: empty
```
## Tool limitations
- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 cannot directly emit more than one
external blob option in this environment. Multi-slot images were derived
from a legal mkfs image with deterministic, asserted structural patches.
- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 rejects the experimental 48-bit
incompat feature and does not correctly extract the patched device-ID-0
unified-address control. Both were verified by complete FreeBSD kernel
SHA256 reads.
- **KERNEL-FAIL**: none.
@@ -0,0 +1,559 @@
# repo22 FreeBSD 15 NFS export manual test report
Date: 2026-08-08 20:08-20:37 UTC
Baseline: `4ef680df6dcbfad329c0b2b1c0af8fcb21cbed4a` plus this NFS export batch
Guest: FreeBSD 15.0-RELEASE-p8 amd64,
`releng/15.0-n281036-53054229dcb3`, QEMU TCG, 4 vCPUs, 6144 MB RAM
Host tools: erofs-utils 1.8.6, clang cross-target build
## Result
**PASS** for the implementation and all reasonably executable NFS tests.
FreeBSD mountd installed real EROFS exports through export-only `MNT_UPDATE`;
NFSv3 clients resolved 16-byte EROFS handles for regular files, directories,
symlinks, FIFOs, metabox bit-63 inodes, and multidevice files. Handle mutation,
same-md remount stability, nfsd restart, 12,050-entry READDIRPLUS pagination,
cold remounts, concurrent traversal/read/stat load, read-only enforcement, and
device-boundary errors all behaved as required.
The only non-code issue was a cleanup-order mistake after the completed test
matrix: one hard loopback NFS mount remained while nfsd and the direct EROFS
mount were stopped. This blocked `mount -p` and later produced five client
`fileid changed` messages when nfsd was temporarily restarted without the
underlying export. The functional-test dmesg snapshot taken before that cleanup
mistake was byte-identical to its baseline. The VM was rebooted to clear the
blocked cleanup process, and a final correctly ordered NFS rerun completed with
byte-identical pre/post dmesg and zero remaining resources.
## Repository and build
Initial verification:
```sh
git -C /work/repo-community/repo22 fetch xdm main
git -C /work/repo-community/repo22 rev-parse HEAD
git -C /work/repo-community/repo22 rev-parse xdm/main
git -C /work/repo-community/repo22 diff --quiet
git -C /work/repo-community/repo22 diff --cached --quiet
```
Both revisions were:
```text
4ef680df6dcbfad329c0b2b1c0af8fcb21cbed4a
```
Tracked files were clean. Existing untracked `repo22/build`,
`tests/results/manual/2026-08-08T1800Z-namei/artifacts`, and files outside
repo22 were not deleted, modified, staged, or committed.
To execute the required `./build.sh` without overwriting the pre-existing
untracked build products, the current source was copied to an isolated build
directory:
```sh
BUILD_ROOT=$(mktemp -d /work/build/repo22-nfs-build.XXXXXX)
mkdir -p "$BUILD_ROOT/src"
cp repo-community/repo22/build.sh "$BUILD_ROOT/build.sh"
cp repo-community/repo22/src/* "$BUILD_ROOT/src/"
chmod +x "$BUILD_ROOT/build.sh"
cd "$BUILD_ROOT"
./build.sh
nm -u build/erofs.ko | \
awk '$NF == "bcmp" { found = 1 } END { if (found) exit 1 }'
sha256sum build/erofs.ko
```
Result:
```text
==> SUCCESS: /work/build/repo22-nfs-build.eFulVf/build/erofs.ko
48776485d1679f00100bc7b2be5f891c8f03a3e8b53ac7c00da8c2fd5f74cc70 erofs.ko
```
There was no unresolved `bcmp` reference.
The FreeBSD helper compiled without warnings:
```sh
cc -O2 -Wall -Wextra -std=c17 nfs_fh_tool.c -o nfs_fh_tool
```
## Fixtures
The primary fixture was generated with:
```sh
mkdir -p plain-src/basic/subdir plain-src/bigdir plain-src/concurrent
printf 'repo22-nfs-regular\nline-two\n' > plain-src/basic/regular.txt
printf 'nested-directory-file\n' > plain-src/basic/subdir/nested.txt
ln -s regular.txt plain-src/basic/link-to-regular
mkfifo plain-src/basic/test.fifo
for i in $(seq -w 0 12049); do
printf 'entry-%s\n' "$i" > plain-src/bigdir/file-$i
done
for i in $(seq -w 0 255); do
awk -v n="$i" 'BEGIN {
for (j = 0; j < 256; j++)
printf "worker-%s-line-%03d-abcdefghijklmnopqrstuvwxyz0123456789\\n", n, j
}' > plain-src/concurrent/file-$i.dat
done
awk 'BEGIN {
for (i = 0; i < 262144; i++)
printf "throughput-%08d-abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\\n", i
}' > plain-src/throughput.dat
mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \
plain-nfs.erofs plain-src
```
Fixture and manifest hashes:
| Object | SHA256 |
|---|---|
| `plain-nfs.erofs` | `6928f05b58ce462596f9c7bf1d3442ad50d555dc5c31ed01d28e1a921a448bf2` |
| `metabox-nfs.erofs` | `8f493ed70d4be8096ab1adf8208622734d61a64903cf96a1ca1a1cc1854b1985` |
| multidevice primary | `fa2f35f59f63cc4a56d230ba960a6c1d27748506fccce081447cab0dc0d540fd` |
| multidevice blob 1 | `3aeb19c519636b61dc3ebd47c6286098deaa0990ff618ce3facbff4fd65907cd` |
| source manifest | `0eda6f88c3b6c4b7c8d81d940a237d1f63c958a7f6fe7bdd4f77d6b293736fa9` |
| `basic/regular.txt` | `93e7f327676f582356e9de2fcca7b714910227008fa1c5e7820e942c471edb6f` |
| `basic/subdir/nested.txt` | `6da159d7ea19e9d1253ccb0f2c67586e6e075459954e740ff8405546683d87c3` |
| `concurrent/file-000.dat` | `8435866622642b6dadf502b09f95af32cb01907ba3376e4f1fe96278e5ada692` |
| `throughput.dat` (21,757,952 bytes) | `0841effed82d1adf394b6834ce30d4d9eb5fc9427527e854ec1cb6bb4b86c119` |
| expected 12,050-name sorted list | `4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71` |
The metabox image was the previously qualified compressed-metabox fixture. The
multidevice fixture was the previously qualified primary-plus-one-blob image.
## FreeBSD 15 setup commands
The artifacts were copied to the existing VM through SSH port 9222. Password
material and askpass script contents were neither printed nor recorded.
```sh
scp -P 9222 erofs.ko plain-nfs.erofs metabox-nfs.erofs \
tests/nfs_fh_tool.c [email protected]:/tmp/repo22-nfs-test/
ssh -p 9222 [email protected]
cd /tmp/repo22-nfs-test
sha256 erofs.ko plain-nfs.erofs metabox-nfs.erofs
cc -O2 -Wall -Wextra -std=c17 nfs_fh_tool.c -o nfs_fh_tool
```
The guest hashes matched the host hashes.
EROFS and NFS setup:
```sh
kldload /tmp/repo22-nfs-test/erofs.ko
mdconfig -a -t vnode -f /tmp/repo22-nfs-test/plain-nfs.erofs -u 42
mkdir -p /mnt/repo22-erofs
mount -t erofs -o ro /dev/md42 /mnt/repo22-erofs
mount -v | grep /mnt/repo22-erofs
printf '%s\n' \
'/mnt/repo22-erofs -ro -maproot=root -network 127.0.0.0 -mask 255.0.0.0' \
> /etc/exports
service rpcbind onestart
service mountd onestart
service nfsd onestart
service mountd onereload
rpcinfo -p 127.0.0.1
showmount -e 127.0.0.1
mount -v | grep /mnt/repo22-erofs
```
Before mountd reload the EROFS line did not contain `NFS exported`. Afterwards:
```text
/dev/md42 on /mnt/repo22-erofs (erofs, NFS exported, local, read-only, acls, ...)
```
NFSv2/v3 and mountd were registered over TCP and UDP. NFS components are built
into this GENERIC kernel: `kldload nfsd` and `kldload nfscl` reported
`already loaded or in kernel`, while rpcbind/mountd/nfsd operated normally.
## File-handle validation commands
Normal vnode types:
```sh
./nfs_fh_tool capture /mnt/repo22-erofs/basic/regular.txt regular.before.fh
./nfs_fh_tool capture /mnt/repo22-erofs/basic/subdir directory.fh
./nfs_fh_tool lcapture /mnt/repo22-erofs/basic/link-to-regular symlink.fh
./nfs_fh_tool capture /mnt/repo22-erofs/basic/test.fifo fifo.fh
./nfs_fh_tool describe regular.before.fh
./nfs_fh_tool describe directory.fh
./nfs_fh_tool describe symlink.fh
./nfs_fh_tool describe fifo.fh
./nfs_fh_tool stat regular.before.fh
./nfs_fh_tool stat directory.fh
./nfs_fh_tool stat symlink.fh
./nfs_fh_tool stat fifo.fh
./nfs_fh_tool cat regular.before.fh regular.fhopen.out
```
Observed handles:
| Type | NID | Generation | Result |
|---|---:|---:|---|
| regular | `0x74` | 1 | `fhstat` and `fhopen` PASS |
| directory | `0x76` | 1 | `fhstat` PASS |
| symlink (`lgetfh`) | `0x72` | 1 | `fhstat` PASS |
| FIFO | `0x79` | 1 | `fhstat` PASS |
All had `len=16` and `pad=0`. The regular full-handle SHA256 was:
```text
fb1778fc6bdc979e10d003dd6131a961e7532baa16273c5465935a665bf990f6
```
Malformed and stale classification:
```sh
./nfs_fh_tool mutate regular.before.fh bad-len.fh len 15
./nfs_fh_tool mutate regular.before.fh bad-pad.fh pad 1
./nfs_fh_tool mutate regular.before.fh bad-gen.fh gen 2
./nfs_fh_tool mutate regular.before.fh bad-nid.fh nid_hi 0xffffffff
./nfs_fh_tool expect-stat bad-len.fh EINVAL
./nfs_fh_tool expect-open bad-len.fh EINVAL
./nfs_fh_tool expect-stat bad-pad.fh EINVAL
./nfs_fh_tool expect-open bad-pad.fh EINVAL
./nfs_fh_tool expect-stat bad-gen.fh ESTALE
./nfs_fh_tool expect-open bad-gen.fh ESTALE
./nfs_fh_tool expect-stat bad-nid.fh ESTALE
./nfs_fh_tool expect-open bad-nid.fh ESTALE
```
All eight checks passed. `fhstat` exercised the shared-lock `VFS_FHTOVP` path;
`fhopen` exercised the exclusive-lock path.
Same explicit md-unit remount:
```sh
umount /mnt/repo22-erofs
mdconfig -d -u 42
mdconfig -a -t vnode -f plain-nfs.erofs -u 42
mount -t erofs -o ro /dev/md42 /mnt/repo22-erofs
./nfs_fh_tool capture /mnt/repo22-erofs/basic/regular.txt regular.after.fh
./nfs_fh_tool compare regular.before.fh regular.after.fh
./nfs_fh_tool cat regular.before.fh regular.remount.out
```
The complete handles were byte-identical and the pre-remount handle still read
the correct file after remount.
Non-export mount update rejection:
```sh
mount -u -o noexec /mnt/repo22-erofs
```
Result: `Operation not supported`; the mount remained exported, local,
read-only, and did not gain `noexec`.
## NFSv3 basic and restart tests
Four NFSv3 TCP READDIRPLUS clients were mounted:
```sh
mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \
127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-512
mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=1024 \
127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-1024
mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=4096 \
127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-4096
mount_nfs -o nfsv3,tcp,rdirplus \
127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-default
nfsstat -m
```
FreeBSD clamped the requested 512/1024/4096 values to an effective
`readdirsize=8192`; the default client used 65536. This was recorded as a client
environment limit, not a failure.
For each client:
```sh
cmp NFS/basic/regular.txt DIRECT/basic/regular.txt
test -d NFS/basic/subdir
test "$(readlink NFS/basic/link-to-regular)" = regular.txt
test -p NFS/basic/test.fifo
test "$(stat -f %i NFS/basic/regular.txt)" = \
"$(stat -f %i DIRECT/basic/regular.txt)"
! touch NFS/write-must-fail
```
All passed. Writes failed with `Read-only file system`.
nfsd restart with an open descriptor:
```sh
exec 3< /mnt/repo22-nfs-default/basic/regular.txt
service nfsd onerestart
cat <&3 > open-fd-after-nfsd-restart.out
exec 3<&-
cmp open-fd-after-nfsd-restart.out /mnt/repo22-erofs/basic/regular.txt
```
The open descriptor and subsequent path reopen both passed. Direct file handles
captured before and after restart were byte-identical.
## Metabox bit-63 test
Commands:
```sh
mdconfig -a -t vnode -f metabox-nfs.erofs -u 43
mount -t erofs -o ro /dev/md43 /mnt/repo22-metabox
./nfs_fh_tool capture /mnt/repo22-metabox/long/file1.txt metabox.before.fh
./nfs_fh_tool describe metabox.before.fh
./nfs_fh_tool stat metabox.before.fh
./nfs_fh_tool cat metabox.before.fh metabox.fhopen.out
./nfs_fh_tool mutate metabox.before.fh metabox.bad-nid.fh \
nid_lo 0xffffffff
./nfs_fh_tool expect-stat metabox.bad-nid.fh ESTALE
./nfs_fh_tool expect-open metabox.bad-nid.fh ESTALE
```
Observed handle:
```text
len=16 pad=0 nid=800000000000010a gen=1
ino=9223372036854776074
```
The metabox mount was exported with mountd and mounted over NFSv3. Direct,
`fhopen`, and NFS contents had SHA256:
```text
661b22d2a7bd94a7da35834d4cc3647eb527d48eb08acc75bde2e3a7691ed45e
```
The NFS and direct inode numbers matched. The out-of-range metabox mutation
returned `ESTALE` through both shared and exclusive paths.
## Multidevice boundary test
Normal NFS commands:
```sh
mdconfig -a -t vnode -f primary.img -u 44
mdconfig -a -t vnode -f blob1.img -u 45
mount -t erofs -o ro -o device.1=/dev/md45 /dev/md44 \
/mnt/repo22-multidev
mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \
127.0.0.1:/mnt/repo22-multidev /mnt/repo22-nfs-multidev
cmp /mnt/repo22-nfs-multidev/alpha.bin /mnt/repo22-multidev/alpha.bin
cmp /mnt/repo22-nfs-multidev/small.txt /mnt/repo22-multidev/small.txt
```
Normal multidevice NFS reads passed. Representative hashes:
```text
f779dbe3aeaa609beb32212579f3aea441f244621d13c977cb8b52d7ec3016d9 alpha.bin
5990b055a3c6d27681185a25e15ff21ba3cbdca7632faf4332233238e5f45aad small.txt
001bcf4626e7d52f1bd76dee9a6c6e7d3a718c03602c5f9b07840a6c98b8fdc3 tree/beta.bin
```
Cold detached-provider test:
```sh
umount /mnt/repo22-multidev
mdconfig -d -u 44
mdconfig -a -t vnode -f primary.img -u 44
mdconfig -a -t vnode -f blob1.img -u 45
mount -t erofs -o ro -o device.1=/dev/md45 /dev/md44 \
/mnt/repo22-multidev
./nfs_fh_tool capture /mnt/repo22-multidev/tree/beta.bin multidev-beta.fh
./nfs_fh_tool stat multidev-beta.fh
mdconfig -d -o force -u 45
! ./nfs_fh_tool cat multidev-beta.fh beta.after-orphan
```
The handle resolved to NID `0x36`, then the cold read returned:
```text
Device not configured
```
This preserved the multidevice `ENXIO` boundary error instead of converting it
to `ESTALE`, reading past the provider, or panicking.
## READDIRPLUS and stress
Every one of the four initial listings returned exactly 12,050 unique expected
names. The expected list and all four sorted outputs had SHA256:
```text
4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71
```
Dot handling:
```sh
ls -a1 /mnt/repo22-nfs-default/bigdir > nfs-dot-list
test "$(wc -l < nfs-dot-list)" -eq 12052
test "$(grep -cx '\.' nfs-dot-list)" -eq 1
test "$(grep -cx '\.\.' nfs-dot-list)" -eq 1
```
All passed.
Cold pagination used five unmount/remount/list/compare cycles on the small
client. Concurrency then used eight workers, three complete 12,050-entry rounds
each, distributed across the four mounts. All 29 cold/concurrent sorted outputs
had one unique SHA256 value, matching the expected list.
Parallel data/metadata load:
```sh
for M in 512 1024 4096 default; do
find /mnt/repo22-nfs-$M/concurrent -type f -maxdepth 1 -print0 | \
xargs -0 -n 1 -P 8 cat > /dev/null &
find /mnt/repo22-nfs-$M/concurrent -type f -maxdepth 1 -print0 | \
xargs -0 -n 1 -P 8 stat -f '%i %z' > /dev/null &
done
wait
```
All jobs completed successfully.
Final stress counters:
| Counter | Client | Server |
|---|---:|---:|
| requests / cache misses | 270,854 | 271,174 |
| lookup | 264,989 | 265,109 |
| read | 1,360 | 1,360 |
| READDIRPLUS | 2,512 | 2,512 |
| write | 0 | 0 |
| timed out | 0 | n/a |
| retries | 0 | n/a |
There were no READDIR RPCs because the clients negotiated READDIRPLUS.
Informational throughput:
```text
21757952 bytes transferred in 5.843937 seconds
3723167 bytes/second
```
No fixed throughput threshold was used.
## Test matrix
| Test | Actual | Status |
|---|---|---|
| isolated `./build.sh` | success; module hash recorded | PASS |
| unresolved `bcmp` | none | PASS |
| FreeBSD helper compile | `-Wall -Wextra`, no warnings | PASS |
| final `kldload` / `kldunload` | load, unload, reload, unload succeeded | PASS |
| initial mount export flag | absent before mountd | PASS |
| export-only `MNT_UPDATE` | mountd installed export | PASS |
| non-export `MNT_UPDATE` | `EOPNOTSUPP`, flags unchanged | PASS |
| regular file | direct, `fhopen`, NFS content match | PASS |
| directory | handle and NFS traversal | PASS |
| symlink | `lgetfh`, target and NFS lookup | PASS |
| FIFO | handle metadata and NFS type | PASS |
| read-only behavior | direct and all NFS mounts rejected writes | PASS |
| 16-byte handle ABI | len/pad/NID hi/NID lo/gen observed | PASS |
| generation | `1`, matching `va_gen` | PASS |
| shared/exclusive lock contract | `fhstat` and `fhopen` passed | PASS |
| malformed len/pad | `EINVAL` | PASS |
| stale generation/NID | `ESTALE` | PASS |
| same `md42` remount | complete handle byte-identical | PASS |
| nfsd restart | open descriptor and reopen passed | PASS |
| metabox bit 63 | real `0x800000000000010a` round-trip | PASS |
| metabox boundary mutation | `ESTALE` | PASS |
| multidevice NFS | external-data reads matched | PASS |
| detached external device | cold handle read preserved `ENXIO` | PASS |
| 12,050-entry listings | exact count and names on four clients | PASS |
| cookie pagination | five cold remounts, no duplicate/omission | PASS |
| dot omitted | one `.` and one `..`, 12,052 total | PASS |
| concurrent traversal | 8 workers x 3 rounds, all hashes equal | PASS |
| concurrent read/stat | all jobs completed | PASS |
| READDIRPLUS | client/server 2,512 | PASS |
| NFS timeouts/retries | 0 / 0 | PASS |
| successful NFS writes | 0 | PASS |
| throughput | 3,723,167 B/s, informational | INFO |
## dmesg and cleanup
The functional matrix captured dmesg before loading the test module and after
all functional tests plus a load/unload cycle. Both files contained the same
two pre-existing historical lines and compared byte-for-byte equal:
```text
interface erofs.1 already present in the KLD 'erofs-repo22-xattrfix.ko'!
md0: truncating fractional last sector by 14 bytes
```
No EROFS error, stale-handle message, trap, panic, or new kernel line occurred
during the functional matrix.
The first cleanup attempt used the wrong order: it stopped nfsd and unmounted
the direct EROFS mounts before one hard loopback client was fully gone. This
left a `mount -p` process in uninterruptible NFS wait. When rpcbind/nfsd were
temporarily restarted without the underlying EROFS export to release those
clients, the NFS client logged five `fileid changed` messages. These messages
occurred after the byte-identical functional dmesg snapshot and are classified
as **environment cleanup artifact**, not an EROFS functional failure.
The VM was rebooted to remove the blocked cleanup process. Post-reboot audit:
```text
erofs_mounts=0
nfs_mounts=0
md_units=0
erofs_modules=0
nfsd_pids=0 mountd_pids=0 rpcbind_pids=0
exports_exists=no
```
A final correctly ordered clean rerun then performed:
```sh
kldload erofs.ko
mdconfig -a -t vnode -f plain-nfs.erofs -u 42
mount -t erofs -o ro /dev/md42 /mnt/repo22-erofs
service rpcbind onestart
service mountd onestart
service nfsd onestart
mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \
127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-clean
cmp /mnt/repo22-nfs-clean/basic/regular.txt \
/mnt/repo22-erofs/basic/regular.txt
test "$(find /mnt/repo22-nfs-clean/bigdir -type f -maxdepth 1 | wc -l)" \
-eq 12050
umount /mnt/repo22-nfs-clean
: > /etc/exports
service mountd onereload
service nfsd onestop
service mountd onestop
service rpcbind onestop
umount /mnt/repo22-erofs
mdconfig -d -u 42
kldunload erofs
```
The clean rerun's pre/post dmesg files compared byte-for-byte equal. Final
state after that rerun:
```text
clean_rerun_erofs=0 nfs=0 md=0 module=0 services=0 exports=absent
```
## Limitations
- **ENVIRONMENT-LIMIT**: FreeBSD clamped requested readdir sizes 512, 1024, and
4096 to an effective minimum of 8192. The test still exercised a much smaller
value than the 65536 default and produced 2,512 real READDIRPLUS calls.
- **ENVIRONMENT-CLEANUP-ARTIFACT**: the five post-test `fileid changed` lines
were caused by the explicitly documented wrong cleanup order. A reboot and a
correctly ordered rerun left no new dmesg lines or resources.
- **KERNEL-FAIL**: none.
- **NOT RUN**: none of the required functional cases were omitted.
@@ -0,0 +1,142 @@
# repo22 multi-device review findings manual test report
Date: 2026-08-08 21:14 UTC
Baseline: `2354b4487c84458c1e447f4d0bb7b55daebcca26` plus this review fix
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
Linux reference: 7.1-rc1 `/work/dev-src-linux/fs/erofs`
Host tools: erofs-utils 1.8.6
## Result
**PASS**. All five multi-device review findings were fixed and exercised with
deterministic positive and negative fixtures. A real two-block LZ4 pcluster
was read from an external provider and the complete 1 MiB output matched the
source SHA256. Multi-device NFS reads and forced-device-removal behavior also
passed.
## Source changes validated
- Non-zero device IDs accept a slot with `uniaddr=0`; unified device-ID-0
lookup skips such slots.
- `devt_slotoff=0` reads the table from byte offset zero, as Linux 7.1 does.
- Device mapping carries the requested extent length. Flatdev reads must fit
wholly in the primary range or one non-zero unified slot; gaps, adjacent-slot
crossings, and pcluster crossings fail closed.
- Declared image and slot bounds are checked before physical media size, so
format corruption is `EINTEGRITY` and a genuinely short provider is
`ENXIO`.
- Primary and external `namei()` failures preserve `ENOENT`; omitted required
`device.N` options still return `ENXIO`.
The NFS file-handle ABI, `VFS_FHTOVP`, inode bounds, readdir cookies, and GEOM
orphan lifecycle from `2354b448` were not changed.
## Build
Commands:
```sh
git diff --check
./build.sh
nm -u build/erofs.ko | awk '$NF == "bcmp" {bad=1} END {exit bad}'
sha256sum build/erofs.ko
```
Results:
- Cross-build: PASS.
- `git diff --check`: PASS.
- Unresolved `bcmp`: none.
- Module SHA256:
`b85c429fa73e6cebc10e6e3941575a6ca170306ab4fa9ccca4006c922971acb8`.
- FreeBSD `kldload` and `kldunload`: PASS.
## Fixture construction
The chunk fixtures derive from the checksum-valid TC094 images recorded in the
19:37 multi-device report. The patcher asserted the original table values
`slot1=(blocks=19, uniaddr=2)`, `slot2=(blocks=25, uniaddr=21)`, and the first
32 KiB chunk index `(device_id=2, startblk=9)` before each change.
The compressed baseline was generated from the deterministic `shape.dat`:
```sh
mkfs.erofs -T0 -U00000000-0000-0000-0000-000000000000 \
--all-root -E legacy-compress -zlz4 -C65536 \
lz4-full-64k.erofs src-big64k
dump.erofs --path=/shape.dat -e lz4-full-64k.erofs
```
The baseline contains one 1 MiB logical extent backed by an 8192-byte physical
pcluster. Its HEAD pblk was changed from block 1 to unified block 4; the two
compressed blocks were copied to a two-block external provider and zeroed in
the primary image. A checksum-valid device table declared
`slot1=(blocks=2, uniaddr=4)`.
For the crossing negative, the same pcluster was placed across adjacent
`slot1=[4,5)` and `slot2=[5,6)`. The flatdev contains both real compressed
blocks, proving that failure is caused by the declared slot boundary rather
than absent data.
erofs-utils 1.8.6 parses the external compressed extent but its userspace
compressed read path does not propagate the unified slot ID to `erofs_dev_read`;
therefore userspace extraction of this layout is tool-unavailable. The
FreeBSD kernel full-file SHA256 is the authoritative read validation.
## Fixture hashes
| Fixture | SHA256 |
|---|---|
| zero-uniaddr primary | `d38935291fb2ee756e0c192d1b0d39b4f4f5b4047aa37c2de44cad91a55fa018` |
| zero-uniaddr device-ID-0 control | `fac9db7631cd06d17212cbe3e7b1d681766f031d1e41e443f71f1bad39f9b960` |
| slotoff-zero primary | `39e3e8c60bea962f1200550316be87ec7da7b15c0ba11c6e5edf05f0715db451` |
| cross-slot primary | `edd0cba92a26bc9a7a59561732fb8e045b2e1565d18d9f9b3143f86f64649dee` |
| cross-slot flatdev | `6e2834450081d778cee37fa3e050249f04927def205d6816aab814a2912744bc` |
| gap flatdev | `5bc07b66f7b0f83e9568ba781dcc78a08d45e617dc27bb365ed3ee2c12353218` |
| external-compressed primary | `5625612426d68624c77dd94b09754da583fc6bd90767e9e9d958e4fc89131a4d` |
| external-compressed blob | `0d71102ce471af0a30d4d61a1fbeef1fc33437c85651d7c7f9c60ebd305ec9d0` |
| cross-pcluster primary | `2bce4a91c1aedc4bab2e65582ef9cb465094f825a051c0203d52b3d18bb17ed8` |
| cross-pcluster flatdev | `bc07fb06ca5c6bb60ef1a989eecbd3d714450092ba756f85f5b060c8b8b2ec3a` |
Source `shape.dat` SHA256:
`370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52`.
## FreeBSD result matrix
| Test | Actual | Status |
|---|---|---|
| slot 1 `uniaddr=0`, explicit device ID 1 | complete striped and second-file SHA256 matched | PASS |
| slot 1 `uniaddr=0`, device ID 0 control | slot skipped; primary-bound `EINTEGRITY` | PASS |
| `devt_slotoff=0` | mount and complete striped SHA256 matched | PASS |
| nonexistent primary path | `ENOENT` / `No such file or directory` | PASS |
| nonexistent external path | `ENOENT` / `No such file or directory` | PASS |
| omitted slot option | `ENXIO` / `Device not configured` | PASS |
| flatdev chunk crosses adjacent slots | read returned `EINTEGRITY` | PASS |
| device-ID-0 chunk starts in a gap | read returned `EINTEGRITY` | PASS |
| explicit chunk exceeds declared slot and media | `EINTEGRITY`, not `ENXIO` | PASS |
| real external 64 KiB LZ4 pcluster | complete 1 MiB SHA256 matched | PASS |
| flatdev pcluster crosses adjacent slots | read returned `EINTEGRITY` | PASS |
| explicit pcluster crosses two providers | read returned `EINTEGRITY` | PASS |
## NFS smoke
The external-compressed image was exported read-only over local NFSv3/TCP.
The NFS client read the complete `shape.dat`; SHA256 matched the source.
The zero-uniaddr two-slot image was then exported. The client obtained stable
inode and size data for a cold slot-2 file, slot 2 was forcibly orphaned, and
the same NFS pathname still resolved to identical metadata. Its first cold data
read failed with the underlying device error. This preserves the `2354b448`
contract: file-handle resolution is not misreported as `ESTALE`, while missing
external data remains an I/O/device failure.
## Cleanup
All EROFS and NFS mounts were unmounted, NFS services stopped, `/etc/exports`
cleared, md providers detached, and the module unloaded. No image, overlay,
fixture, or build artifact is part of this commit.
A final representative rerun covered the external-compressed positive, the
flatdev-gap negative, and the offset-zero table positive. The dmesg SHA256 was
unchanged before and after:
`897003051a6a50875d49715bfa27907611cedac9e42e0a06ca7377105ac695ae`.
@@ -0,0 +1,406 @@
# repo22 compression P0 WIP integration manual report
Date: 2026-08-08 23:06 UTC
Baseline: `78182686e968c659932458cbe7a1e0889397f20b`
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
Build reference: `/work/dev-freebsd-releng` (`REVISION=15.0`, source branch
`RELEASE-p9`)
Linux behavior reference: `/work/dev-src-linux/fs/erofs`
Host production tool: erofs-utils 1.8.6
## Result
**PASS**, with the extent-record guest case accurately recorded as
**MKFS-UNAVAILABLE** rather than PASS.
The retained ten-file WIP was reviewed and integrated without reset, checkout,
stash, or revert. Both module configurations build and load. DEFLATE, ZSTD,
and MicroLZMA full and partial references read correctly; targeted corruption
returns `EIO`. A fragment-backed compressed metabox now works after packed
inode initialization, while loop, recursive-NID, and range mutations fail
closed. HEAD2 and interlaced images pass real FreeBSD kernel reads. LZ4,
xattr/metabox, chunk, external compressed multi-device, and NFS regressions
also pass.
No CI or test runner was added. Only source, Markdown manual tests, and this
report are intended for the commit.
## Instruction and baseline audit
- `find /work -name AGENTS.md -type f -print` returned no paths. There were no
applicable `AGENTS.md` files.
- Initial local `HEAD` and `xdm/main` both resolved to
`78182686e968c659932458cbe7a1e0889397f20b`.
- The exact ten tracked WIP files were present:
`build.sh`, `src/Makefile`, `src/decompressor.c`, `src/deflate.c`,
`src/internal.h`, `src/lzma.c`, `src/super.c`, `src/zdata.c`, `src/zmap.c`,
and `src/zstd.c`.
- Untracked build, fixture, overlay, artifact, and other-repository paths were
not staged.
## Initial guest module cleanup
Before any build under test was loaded, the guest reported:
```text
Id Refs Address Size Name
5 1 0xffffffff82822000 a690 erofs-nozstd.ko
```
`kldstat -v -i 5` proved the path was `./erofs-nozstd.ko` and the contained
module name was `erofs`. The cleanup used the observed KLD ID, not a guessed
filename:
```sh
kldunload -i 5
kldstat
kldstat | grep -i erofs
```
The second `kldstat` contained no EROFS entry and the final grep printed
`none`.
## Source review
### Packed inode and metabox order
- `packed_nid` and `metabox_nid` are decoded and checked before carrier loads.
- A packed NID with the metabox selector bit is rejected before any metadata
recursion can begin.
- The packed inode is loaded before the metabox inode, matching the Linux
dependency order and allowing a compressed metabox to terminate in a packed
fragment.
- The packed inode must be a regular, non-fragment inode. This rejects a packed
carrier that would recurse back through itself.
- A fragment-backed metabox must have a distinct loaded packed inode, non-zero
size, a real fragment tail mapping, and a range wholly inside the packed
inode.
- Fragment recursion checks compare inode NIDs rather than object pointers,
which also catches separately allocated `erofs_node` objects for the same
on-disk inode.
### DEFLATE, ZSTD, and MicroLZMA
- DEFLATE uses `inflate(..., Z_SYNC_FLUSH)` until the requested output is full,
detects no-progress loops, accepts `Z_OK` for partial output, and requires
`Z_STREAM_END` plus complete input consumption for full output.
- ZSTD uses FreeBSD's formal `<contrib/zstd/lib/zstd.h>` API with
`ZSTD_createDCtx_advanced`, `ZSTD_DCtx_setParameter`,
`ZSTD_decompressStream`, `ZSTD_isError`, and `ZSTD_freeDCtx`.
- ZSTD partial decoding stops after the requested output is produced. Full
decoding requires frame completion and complete input consumption.
- MicroLZMA partial decoding accepts `XZ_OK` or `XZ_STREAM_END` after exact
requested output. Full decoding requires `XZ_STREAM_END` and
`buffer.in_pos == srclen`.
- All decoder failures are translated to the filesystem read error `EIO` and
all allocated decoder/output buffers are released.
### ZSTDIO build gate
- `src/Makefile` consumes `opt_zstdio.h` and adds FreeBSD's zstd compatibility
include directory for `zstd.c`.
- `build.sh` accepts only `EROFS_ZSTDIO=0` or `1` and creates the corresponding
option header.
- The disabled translation unit contains only the availability result and a
local stub; it references no `ZSTD_*` symbol.
- A filesystem advertising ZSTD is rejected during compression-config parsing
when the module lacks ZSTDIO, before any file read can reach the stub.
### HEAD2, interlaced, and extent records
- HEAD2 selects `z_algorithmtype[1]` and respects the HEAD2 big-pcluster bit.
- Plain records with interlaced advise use the interlaced byte rotation before
returning data.
- Extent records retain Linux ordering and responsibilities for 4-, 8-, 16-,
and 32-byte records, partial references, explicit algorithm format, shifted
and interlaced plain data, and final fragments.
- Explicit extent tables with a non-empty file and zero extent count fail
closed.
- Final fragment mappings are checked against the loaded packed inode before
reads.
## Build matrix
Host commands:
```sh
EROFS_ZSTDIO=0 ./build.sh
cp build/erofs.ko /tmp/repo22-erofs-nozstd-probe.ko
nm -u /tmp/repo22-erofs-nozstd-probe.ko
EROFS_ZSTDIO=1 ./build.sh
cp build/erofs.ko /tmp/repo22-erofs-zstd-probe.ko
nm -u /tmp/repo22-erofs-zstd-probe.ko
```
| Build | Size | SHA256 | `ZSTD_*` undefined | `bcmp` undefined | Guest KLD |
|---|---:|---|---|---|---|
| no ZSTDIO | 68744 | `c96f4e1620f005a19d7ed581f98f1894969b38a34d7021f54dbce99919888ef9` | none | none | load/unload PASS |
| ZSTDIO | 69984 | `7f8cb03afb9c76b535709af1a06bf993cbab74bb5899b2f4646a6fd20b00d15c` | five formal API names | none | load/unload PASS |
The enabled module's unresolved ZSTD set was exactly:
```text
ZSTD_DCtx_setParameter
ZSTD_createDCtx_advanced
ZSTD_decompressStream
ZSTD_freeDCtx
ZSTD_isError
```
Both were loaded by path, identified with `kldstat -v -i ID`, and unloaded by
that exact ID.
## ZSTDIO behavior
With the disabled module, a normal LZ4 image still read successfully:
```text
nozstd_lz4_sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52
nozstd_lz4_dmesg_delta=empty
```
The same module rejected the ZSTD image:
```text
nozstd_mount_rc=1
mount: /dev/md0: erofs: ZSTD compression requires ZSTDIO support: Operation not supported
```
The ZSTDIO module mounted the image and produced the expected complete SHA256
shown below.
## Fixture construction and proof
Fixtures and build outputs remained outside the repository commit.
Authentication used the existing private askpass workflow; its contents and
credentials were not printed or recorded.
### Partial-reference images
`dump.erofs -s` and `dump.erofs --path=PATH -e` established compressor,
layout, logical lengths, physical starts, and pcluster lengths. Byte-level
patching asserted the original inode NID, map-header offset, HEAD record, and
pblk before setting `Z_EROFS_LI_PARTIAL_REF` or redirecting a reused pblk.
CRC32C was recomputed for every superblock mutation.
| Image | SHA256 |
|---|---|
| `deflate-partial-ref.erofs` | `8bc720a1250ef28794d091b6264e76060fbf01d66ea706c59c24c1960fa347ac` |
| `deflate-partial-ref-corrupt.erofs` | `6ff9dff9b3b5aba7da9b4a93f5b068270b4916267d2fe0fd3702c02be4c19fae` |
| `lzma-partial-ref.erofs` | `2f4bd89d2340273dd4052ea73aa9ac43802db431258c118c5dfede7727be9408` |
| `lzma-partial-ref-corrupt.erofs` | `bfbbd304a401c091d63e8b8e42b2b1984d9750760e940aded89acc909551d838` |
| `zstd-partial-ref.erofs` | `7f5ee4f8a20f20d32eaf8780a7081229f7dbf6b896ad23986da36e1a1c580519` |
| `zstd-partial-ref-corrupt.erofs` | `0c6e6b312b879297e2f71f406dcd35236654800e7cd115df0cb9a748297c48c1` |
The LZMA and ZSTD full file was 1048576 bytes. Their partial file was 700000
bytes and reused the complete source pcluster. The DEFLATE fixture contained
multiple real compressed extents, including reused physical extents in the
partial file.
### Fragment-backed compressed metabox
A METABOX-capable inspector proved:
```text
packed_nid=40
metabox_nid=38
metabox inode: regular, compressed-full, size=20480
metabox fragment header=0x800000000004e800
fragmentoff=321536
packed inode: regular, plain, size=342016
321536 + 20480 = 342016
```
Positive image SHA256:
`8a9a62bd203994711b8272192915d811e6c3de23e07ad9607dd63e66cc109bcd`.
The negative images changed one proven field each:
| Negative | Exact mutation | SHA256 |
|---|---|---|
| self-loop | `packed_nid: 40 -> 38` | `e9501ed9d149e2d735d95156669d144e733bf4be620bb69ea8a0c41f996b436c` |
| range | `fragmentoff: 321536 -> 342016` | `3fdcf2a41f50da93a5931edcef0d86ff2f576ecba781833036eaa930d99197d5` |
| metabox recursion | set bit 63 in `metabox_nid` | `b873e0cf4d2892d2d154a5769f494660f47f4429161e7be001b833da6dd0d705` |
| packed recursion | set bit 63 in `packed_nid` | `4fa368d1b1d47bb56b82132e6055d105ed2508b179a8df2a98ec5a728f91c9ac` |
### HEAD2
The HEAD2 fixture patch asserted the original bytes and made only these semantic
changes, plus the resulting CRC32C bytes:
```text
feature_incompat: 0x00000003 -> 0x0000000b
map h_advise: 0x0002 -> 0x0006
first di_advise: HEAD1 (1) -> HEAD2 (3)
```
Image SHA256:
`fc70cbef0442a86ac2f507aebd7ac7bcfbdfcc3d0b9b3cf2ff5231334583d816`.
The targeted corrupted copy SHA256 was
`f0d9ad645804565c5aca7a92128df3ae9c81717a08ad7e8692489cd2ae606700`.
### Interlaced
This image was generated specifically with installed erofs-utils 1.8.6:
```sh
mkfs.erofs -zlz4 -C4096 -Efragments -T0 \
interlaced-1.8.6.erofs source
```
`dump.erofs -e` reported 105 real extents, with 4096-byte plain extents
interspersed with compressed extents. Image SHA256:
`d77d86f874361bae86cae9e8f6d05d9b6c68ef04cd5c8779368aef033fa485c3`.
### Extent metadata
Installed `mkfs.erofs -V` reported 1.8.6. Exact source-tree search found no
on-disk `Z_EROFS_ADVISE_EXTENTS`, `z_erofs_extent_recsize`, or
`struct z_erofs_extent {` definition in the 1.8.6 include/lib tree. Its
internal `struct z_erofs_extent_item` is an in-memory compressor item, not the
new on-disk extent-record ABI.
Although a newer-tool extent image existed in the WIP build area, it was not
mounted or scored. The required result is therefore:
```text
extent metadata guest result: MKFS-UNAVAILABLE
```
The independent static review compared `src/erofs_fs.h` and `src/zmap.c`
against `/work/dev-src-linux/fs/erofs/erofs_fs.h` and `zmap.c`, covering record
sizes 4/8/16/32, implicit physical bases, explicit-count binary search,
physical/logical high words, format bits, partial references, interlaced data,
fragments, metabox metadata reads, and malformed explicit zero counts.
## Core FreeBSD result matrix
The guest test used repeated `mdconfig -a -t vnode -f IMAGE`, read-only EROFS
mounts, `sha256 -q`, byte-exact `dd`/`cmp`, and a small C helper that printed
`errno` on read failure.
| Case | Complete SHA256 / result | Boundary or random proof | Status |
|---|---|---|---|
| DEFLATE full | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | full stream completion | PASS |
| DEFLATE partial | `61b17076c2dfae88da7912d00df534b894cf6d27178863c6d8e91f8617ebb91e` | offset 122900, 512 bytes: `fa381301af1b62fa259addbe7ae427fd54486abc7604ea7619e7a9c47965606d`; offset 736700, 1024 bytes: `be1d2941b054626376fa58155ce0ef8d6357dd0defcbf9eaa37d19ff6098873c` | PASS |
| DEFLATE corrupt | `read_errno=5`, failure after 65536 output bytes | target extent only | PASS |
| MicroLZMA full | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | full input consumed | PASS |
| MicroLZMA partial | `5a840803f5372b7be1db70713fed7705bf6e982ca2fd319f722c21f094f6c8dd` | offset 65500, 2048 bytes: `49c231f92dde0b0104d7e5b3a01d918dde818c3a6dca05393e2a424d01c214f2`; offset 524287, 4097 bytes: `b8e80c144eacd1f8863c72eab66272379923f0d738f63576ab8286455e0dde8c` | PASS |
| MicroLZMA corrupt | `read_errno=5`, zero output bytes | target pcluster only | PASS |
| ZSTD full | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | formal FreeBSD API | PASS |
| ZSTD partial | `5a840803f5372b7be1db70713fed7705bf6e982ca2fd319f722c21f094f6c8dd` | same two boundary/random hashes as MicroLZMA | PASS |
| ZSTD corrupt | `read_errno=5`, zero output bytes | target pcluster only | PASS |
| HEAD2 full | `7e2f40362554f4460e80ec2f8d91f4e6c04a8e58f2980d637b2d383a7aa3b3f8` | offset 65500, 4096 bytes: `f0a80e3217f54897eae271b6e570c862abcffad3a4ffd19ffec0444ee06ae721` | PASS |
| HEAD2 corrupt | `read_errno=5`, zero output bytes | patched HEAD2 pcluster | PASS |
| interlaced full | `7e2f40362554f4460e80ec2f8d91f4e6c04a8e58f2980d637b2d383a7aa3b3f8` | offset 16240, 8192 bytes across first compressed/plain transition: `b1387900e55e5672944f8299fe66ac9542007f2b0cbed81d5974831d6040cdae` | PASS |
| extent records | static format/control-flow review only | erofs-utils 1.8.6 cannot emit | MKFS-UNAVAILABLE |
## Fragment-backed metabox results
| Case | Actual | Status |
|---|---|---|
| positive file | SHA256 `4536c1d7121f48829475f29179f54baa57154b4ef817cf0776f81782585d29ad` | PASS |
| shared-prefix xattr | `shared-value` | PASS |
| per-file xattr | `value-000` | PASS |
| self-loop | mount exit 1; `packed inode nid=38 is not a non-recursive regular file: Integrity check failed` | PASS |
| out-of-range | mount exit 1; `Integrity check failed` | PASS |
| metabox NID bit 63 | mount exit 1; `Integrity check failed` | PASS |
| packed NID bit 63 | mount exit 1; `Integrity check failed` | PASS |
The `vmstat -m` EROFS row after the matrix showed zero active allocations:
```text
erofs 0 0 240695 16,32,64,128,256,384,1024,2048,4096,8192,16384,32768,65536
```
The cumulative allocation count increased as expected; the active allocation
and active-byte columns were both zero.
## Regression matrix
| Regression | Actual | Status |
|---|---|---|
| LZ4 legacy full index | SHA256 `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | PASS |
| LZ4 compact index | same SHA256 | PASS |
| LZ4 64 KiB big pcluster | same SHA256 | PASS |
| LZ4 all-fragments | SHA256 `a3a83e5c524b5ed446a06ce78cf407192a0c80119f15d2bc3489a50515eb49e0` | PASS |
| LZ4 ztailpacking | SHA256 `e2aa4a0a0cbcf422f397c7069a38ae0f073781386958e7db0dfa3ff2ca075513` | PASS |
| xattr/metabox | file SHA256 `4536c1...`; xattr `value-000` | PASS |
| single-device chunk | SHA256 `de29abbd47ecd9136f64f22f73fcb40bce1718a8865e6282e74953aa1df80c44` | PASS |
| external compressed LZ4 extent | complete SHA256 `370eb0...` | PASS |
| external compressed boundary | offset 65500, 4096 bytes: `0d66627577218a39a620e8b28d8c8d64b974b184063c52cb0cceeaf0c297c0db` | PASS |
| local NFSv3/TCP over external compressed extent | complete SHA256 `370eb0...` | PASS |
The external compressed fixture declared slot 1 at unified block 4, stored the
real two-block LZ4 pcluster in the external provider, and zeroed the
corresponding bytes in the primary image. Primary SHA256 was
`5625612426d68624c77dd94b09754da583fc6bd90767e9e9d958e4fc89131a4d`;
blob SHA256 was
`0d71102ce471af0a30d4d61a1fbeef1fc33437c85651d7c7f9c60ebd305ec9d0`.
## NFS startup-race investigation
The first regression run started rpcbind, mountd, and nfsd and immediately
called `mount_nfs`. The client printed one transient
`RPCPROG_NFS: RPC: Program not registered`, then retried successfully and read
the correct complete SHA256. No dmesg line changed.
A separate clean rerun waited for:
```sh
rpcinfo -t 127.0.0.1 nfs 3
```
Readiness succeeded on attempt 2:
```text
program 100003 version 3 ready and waiting
```
The subsequent NFS mount emitted no RPC warning and produced the same complete
SHA256. This proves the first message was a user-space service-registration
race, not an EROFS or NFS data-path failure.
## dmesg
Core, regression, and clean NFS snapshots were each byte-identical before and
after their respective test matrices. All six snapshot files had SHA256:
```text
2aa3500d33a7cfbe0db87854d3427231cda9cf92b7f85e553f957231f7359680
```
There was no new panic, trap, decompression diagnostic, integrity message, GEOM
orphan warning, or NFS kernel message.
## Cleanup
Every test used a trap that unmounted the current EROFS/NFS mount, detached the
specific md unit, stopped NFS services in client-first order, restored or
removed `/etc/exports`, and unloaded the module by the observed KLD ID.
Final audits after the core, regression, disabled-ZSTD LZ4, and clean NFS runs
all reported:
```text
erofs mounts=0
NFS mounts=0
md units=0
erofs modules=0
nfsd/mountd/rpcbind processes=0
```
## Limitations
- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 cannot generate the new on-disk extent
record format. Extent coverage is independent static ABI/control-flow review
only. No newer-tool extent image is counted as a FreeBSD PASS.
- METABOX and the transformed HEAD2/partial-reference fixtures require newer
format-aware tooling plus byte-level assertions. Their guest reads are real
FreeBSD kernel tests; fixture creation and mutations are documented and
checksum-validated.
- No CI, automated runner, benchmark threshold, memory-pressure run, or forced
OOM scenario was added. The requested functional manual matrix and active
allocation checks were completed.
- Kernel failures: none.
@@ -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"
@@ -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"
@@ -0,0 +1,98 @@
# repo22 Review Fixes Manual Test Report
Started: 2026-08-09 05:31 UTC
Completed: 2026-08-09 05:57 UTC
Parent commit: `077b37ab44780a31f6cd76045afe6e0a92f3a5be`
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
## Scope and Result
| Test | Result | Evidence |
|---|---|---|
| TC154 per-inode NFS generation | PASS | Same-image remount, same-superblock replacement, malformed replacement |
| TC155 explicit extent `pa + plen` overflow | PASS | Repeated `ERR#97` and DTrace device-I/O observation |
| TC156 inode timestamp validation | PASS | Five compact/extended corruption fixtures returned `ERR#97` |
## Build Results
Both builds ran natively in the guest with
`FREEBSD_SRC=/tmp/repo22-freebsd15-src` and completed with kernel `-Werror`.
| Configuration | Module SHA256 | Result |
|---|---|---|
| `WITH_ZSTDIO=0` | `d6e755c7a75dd5043b603d58215ae3e7c8f29318b31dc1885aa9bb070d6e57a5` | PASS |
| `WITH_ZSTDIO=1` | `b442f9f05af1d1a76801690c83c9a40ec6fc739adb0663e7dacaf837eeb2ecf3` | PASS |
The disabled module had no undefined `ZSTD_*` or `bcmp` symbol. The enabled
module referenced only the expected FreeBSD kernel ZSTD API symbols and no
`bcmp`.
## Fixture Reproduction
`python3 tests/review_fixtures.py make` was run twice in independent empty
directories with erofs-utils 1.8.6. All nine images were byte-identical between
runs, and both `SHA256SUMS` files self-verified.
| Fixture | SHA256 |
|---|---|
| `nfs-inode-a.erofs` | `307879542d5bb52ff02fdb12553fee9a94cd1360b99752ebd08bedf9c9dc1c8c` |
| `nfs-inode-b.erofs` | `64ddb0efcb3c5bd092710a3d3a5ea7aaf637c070e2e061dbf9d4538262fa2f49` |
| `nfs-inode-corrupt.erofs` | `5f49bcd9361531c53f6435f53fb10acc579a8ff7fb6716d78da9dff60f7e8539` |
| `extent-pa-wrap.erofs` | `1e49ecf1917265fde9b606e80f7dbae8b14aca8c6f149128b31d6c825da546dd` |
| `compact-epoch-range.erofs` | `3e031196266fe308f5443878cea7afd775c797856dfb4e9ed28d14bf71d81abb` |
| `compact-epoch-wrap.erofs` | `2c3971c42a85494a75317c05b6da3669ea17a0301b7aa36517048d5e6de60259` |
| `compact-nsec-invalid.erofs` | `b7137b4b18c121c613689c308d5938339ea3e867723046e268d1949e8328c27b` |
| `extended-nsec-invalid.erofs` | `2d8a05d993155895feba67b2ff2dcad224a590c798925c31708c1b284c7c7ef3` |
| `extended-seconds-range.erofs` | `aa9675ac3e857892d182b033bb9f96424364aace84a877d2001f47fa32503e45` |
## TC154
Image A reported FID `len=16`, `pad=0`, NID 452, and generation
`3895653226`; `fhstat`, `stat -f %v`, and the fixture manifest agreed. Reusing
md90 for an unchanged-image remount preserved the complete handle and the old
handle remained readable.
Image B retained fsid `00000034:000000e0` and NID 452 but reported generation
`548470773`. Both `fhstat` and `fhopen` on A's handle returned `ESTALE`; B's
handle succeeded. With the malformed replacement, both old-handle operations
returned positive FreeBSD errno 45 (`EOPNOTSUPP`).
## TC155
The fixture mounted and `stat` reported the 1 MiB target. Two direct reads at
logical offset 4096 exited 1; both independent truss runs showed:
```text
read(..., 1) ERR#97 'Integrity check failed'
```
An `io:::start` DTrace positive control captured a raw md90 read at physical
offset 4096. The failing EROFS read produced no md90 I/O event, proving the
wrapped physical address did not reach the provider.
## TC156
`compact-epoch-wrap` and `compact-nsec-invalid` failed mount directly with rc 1
and traced `nmount(...)=ERR#97`. `compact-epoch-range` mounted with root mtime
`INT64_MAX`, while target stat returned `ERR#97`. Both extended target fixtures
mounted successfully and returned `ERR#97` for invalid nanoseconds and
`INT64_MAX+1` seconds. Every failing operation was executed directly and again
under truss.
## Harness Notes
The first build invocation used absent `/usr/src` and was rerun with the actual
guest source path. One test attempt used an overly narrow KLD filename match and
executed no TC step. A second attempt discarded `truss`'s zero wrapper status;
the qualified runs record direct command status separately from traced syscall
errno. None of these discarded attempts is counted as PASS evidence.
## Cleanup
Each qualified run ended with unchanged dmesg line count (`123 -> 123`), zero
EROFS mounts, no md provider, and zero EROFS modules. Raw transcripts, build
objects, modules, and generated images remain outside the repository under
`/work/build` and guest `/tmp` only.
@@ -0,0 +1,159 @@
# repo22 G1 Full Manual Regression Report
Execution window: 2026-08-09 06:30-07:10 UTC
Code baseline: `cd0e985b5ac54a4b7acb7042422329ad1729fb3e`
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, FreeBSD clang 19.1.7
## Scope and Method
This run covers exactly these 32 test cases and no others:
`TC001, TC007, TC009, TC011-TC014, TC019-TC040, TC147, TC150, TC151`
Every test was executed directly from its Markdown procedure. No CI, runner,
or wrapper supplied a result, and no historical run was accepted as current
evidence. Each test used a fresh dynamically allocated md provider and was
cleaned before the next test.
Set audit: requested 32, selected 32, unique 32, duplicate 0, missing 0,
extra 0.
## Exact Build
The source archive exported from the code baseline had SHA256
`5b7fd2ac32ecf791f71c74849a6e8c1775e442d1be3c7852c654d074b2c8351b`.
The module was built natively in the guest with:
```text
FREEBSD_SRC=/tmp/repo22-freebsd15-src WITH_ZSTDIO=0 ./build.sh
```
The build completed with kernel `-Werror`. The module had no unresolved
`bcmp` or `ZSTD_*` symbols.
| Item | Value |
|---|---|
| Source commit | `cd0e985b5ac54a4b7acb7042422329ad1729fb3e` |
| `WITH_ZSTDIO` | `0` |
| KLD SHA256 | `023ddf0ea2205977f5921715d2a6206b607806840180ddf470103fd1d6deba84` |
| Loaded module | KLD ID 20, size `0xb8a8`, `erofs.ko` |
| Guest userland/kernel | `15.0-RELEASE-p8` / `15.0-RELEASE-p8` |
The later documentation-only progress commit `573aaab7ee0b47cb6aed7f76c52c68dd4041326b`
did not alter `src/`; the KLD therefore remains an exact build of the stated
code baseline.
## Fixture Qualification
`tests/prepare_g1_fixtures.sh` generated deterministic sources and production
erofs-utils 1.8.6 images. `tests/g1_fixtures.py` applied structured,
field-checked transforms and recalculated checksums. Independent empty output
directories `repo22-g1-repro-c.6IK7SM` and `repo22-g1-repro-d.egVQgj` produced
byte-identical source manifests, metadata, image manifests, and structured
evidence.
| Manifest | SHA256 |
|---|---|
| `SOURCE-SHA256SUMS` | `5ce53780f455208cb1efbf2738429a1f6f5c4eb3af1663779815c6afc0fea2bc` |
| `SOURCE-METADATA` | `bd27480c238ffff441d328ab71775989611d057ffaed691dae0238f738371e46` |
| `images/IMAGE-SHA256SUMS` | `54fb256b3847acc8b1e735eacaace56c1747f65237ee5730b2b5fa3e3ab2b0f3` |
| `images/fixture-evidence.txt` | `84e8e2e595a88e034ea0a05777207d4e3430e4e764a7fbc4d4a4770defcca3db` |
| Image | SHA256 |
|---|---|
| `compact-dot-omitted.erofs` | `bbd1d3d9f619a71ed7a874feb8c437aa9bf3e86f671abfa243a85de57e809d54` |
| `compact-nlink1.erofs` | `c9ecfc29fbd45a2c37c6422d689ac340943dbc23ee4428ffd4698ef88e0b331d` |
| `compact.erofs` | `41004ed19d58e698f277cb268b570b87203c2ba08ea4a68a72f6e3b1c7aec47a` |
| `extended-large-hole.erofs` | `7a69d791742096ddfece20c7a793d828ec7e887352d0b62f22839f66ecb26a6d` |
| `extended-size-bit63.erofs` | `9413a4197f5b2c43876e257690a5f96e82f61b2b62084e2df688144070f0825b` |
| `extended.erofs` | `3d96fd292749f1ef7ae72f8e370fc8a94231ed575fb36a6d66784a59d8c4ffa7` |
| `fallback-48bit-root2.erofs` | `8217da2ae664358e12d357524ef0bb52a897f1ed206b2353c2a2033b76d4ae79` |
| `flat.erofs` | `3785ca07e7bd16f6c611191596ae0314253c0ae9b7217a4b22de25799b0f08ac` |
| `inline-cross-block.erofs` | `354b674fd144cab93403d47ad95135968c24d41b34a8c908aa6717882243e936` |
| `inline-zero.erofs` | `c894f249ca6d4a7a16c7eb728687c6aba1f948630c4f72e20680655c0fca70c0` |
| `inline.erofs` | `1474c310a80766666c1d578174ca03972fb9e03dff8c2357a37883151d4acc47` |
| `invalid-dirent-nid.erofs` | `309d6829ebc87c22ae6ba23f07ae0b00a62c7e657c3cc2ec46fe6a64d24eea69` |
| `root8-48bit.erofs` | `2f48797586395ff7d2204d43e67d6e2aaaa04c9bdfb162c5c5e2c24dc6e88893` |
The corrected dot-omitted directory has size 35 and on-disk names
`..,child.txt`. Its encoding was independently checked with erofs-utils 1.9.3,
including a passing `fsck.erofs`; erofs-utils 1.8.6 does not recognize the
48-bit incompat feature and was not used to claim support for that image.
## Per-Test Results
| Test | Result | Actual command summary and observable evidence | Cleanup |
|---|---|---|---|
| TC001 | PASS | `mdconfig`, `mount -t erofs -o ro`, `cmp`, `sha256`, `statfs_probe`; root source/mount SHA `04690aad...3105`, blocks 17, files 15, readonly 1. | mount 0, md 0 |
| TC007 | PASS | Two fresh providers and concurrent mounts; both waits returned 0, and source/mount1/mount2 `testfile` SHA was `0e5303ad...e3b1`. | mounts 0, md units 0 |
| TC009 | PASS | `statfs_probe`, `df -kT`, `df -iT`; bsize 4096, blocks 17, free 0, files 15, ffree 0, 68 KiB used, readonly 1. | mount 0, md 0 |
| TC011 | PASS | `stat`, complete top-level name comparison, root payload `cmp`; root inode 36, directory mode 0755, nlink 4, root SHA `04690aad...3105`. | mount 0, md 0 |
| TC012 | PASS | `getfh`, `fhstat`, `fhopen`, repeated handle comparison; fsid `00000063:000000e0`, NID 71, generation 2262931519, data SHA `0e5303ad...e3b1`. | mount 0, md 0, outputs 0 |
| TC013 | PASS | Patched dirent NID 59 to 3200 with valid CRC; two `read_probe expect-error` calls returned errno 97 and truss showed `fstatat ERR#97`; dmesg `123->123`. | mount 0, md 0, outputs 0 |
| TC014 | PASS | Structured superblock inspect plus mount/statfs; magic `0xe0f5e1e2`, block bits 12, root NID 36, blocks 17, inodes 15, dmesg `123->123`. | mount 0, md 0 |
| TC019 | PASS | Mounted actual `feature=0x80 rootnid_8b=36 blocks_hi=0` image; root inode 36, blocks 17, root SHA `04690aad...3105`. | mount 0, md 0 |
| TC020 | PASS | `stat`, `cmp`, `sha256` on compact `small.txt`; NID 65, size 22, nlink 1, blocks 8, SHA `6e0d152a...`. | mount 0, md 0 |
| TC021 | PASS | Field evidence `i_format=0x14 i_nb=0x1234`; mounted NID 63 reported nlink 1 and source-exact SHA `ff151c...`. | mount 0, md 0 |
| TC022 | PASS | Structured raw rdev `0x543abc21`; native special-node probe reported char/block major 2748, minor 344865, while FIFO rdev was `NODEV`. | mount 0, md 0 |
| TC023 | PASS | Extended inode NID 45, 64-byte inode, size 2097883; `stat`, full `cmp`, SHA `eed000b0...` all matched source. | mount 0, md 0 |
| TC024 | PASS | Size 4294971393 sparse fixture; three 65536-byte reads at offsets 0, 2147483648, and 4294905857 matched deterministic zero source SHA `de2f2560...`. | mount 0, md 0, outputs 0 |
| TC025 | PASS | Baseline and patched mounts checked `single` NID 63/nlink 1, `hard-a` and `hard-b` NID 57/nlink 2, and `dotdir` NID 53/nlink 2; all source comparisons passed. | mounts 0, md units 0 |
| TC026 | PASS | Valid bit-4 fixture evidence: size 35, on-disk `..,child.txt`, root8 36; listing synthesized `.`, retained `..`, and child SHA matched `de256820...41f0`. | mount 0, md 0 |
| TC027 | PASS | Host `dump.erofs --ls --path=/dotdir` showed explicit `.,..,child.txt`; guest listing and child `cmp` matched SHA `de256820...41f0`. | mount 0, md 0 |
| TC028 | PASS | `flat.erofs` layout 0 small file; size 25, blocks 8, source/mount SHA `bb9c1e27...f486`. | mount 0, md 0 |
| TC029 | PASS | Full medium-file `cmp` SHA `471108a5...12e6`; `pread` offset 40960 length 20480 matched independent range SHA `72142972...5fb`. | mount 0, md 0, output 0 |
| TC030 | PASS | 10485791-byte full `dd`/`cmp` SHA `0b39b8a4...c56e`; 1 MiB read at 5 MiB matched SHA `c26bb16b...ba5c`. | mount 0, md 0, outputs 0 |
| TC031 | PASS | Inline `tiny.txt` evidence layout 2; size 12, blocks 8, source/mount SHA `863f7088...73f38`. | mount 0, md 0 |
| TC032 | PASS | Explicit layout-2 zero-length inode; size 0, blocks 0, EOF clean, both hashes `e3b0c442...b855`. | mount 0, md 0 |
| TC033 | PASS | 5000-byte tailpacked file full SHA `e4c7fbec...b2a6`; 64-byte range crossing offset 4096 matched SHA `6ff6b010...c0e`. | mount 0, md 0, outputs 0 |
| TC034 | PASS | Evidence layouts for 4095/4096/4097 were 0/0/2; full SHAs `5255d8c8...`, `677802c8...`, `eda39df0...`; nine-byte boundary range SHA `9e67b4e5...182c`. | mount 0, md 0, outputs 0 |
| TC035 | PASS | Sequential 4096-byte `dd` to exact EOF at size 262163; source/output SHA `056f8f75...3433`. | mount 0, md 0, output 0 |
| TC036 | PASS | Independent `pread` start/mid/end ranges returned 10240/10240/24576 bytes at exact offsets; SHAs `5b6a742e...d858`, `29fa7799...849`, `6477f5a2...163d`. | mount 0, md 0, outputs 0 |
| TC037 | PASS | 104857857-byte full `dd` took 61.44 s and matched SHA `1c8daa29...c661`; 5 MiB sample at 50 MiB matched SHA `f1b60e95...25c7`. | mount 0, md 0, outputs 0 |
| TC038 | PASS | `readlink` returned exact `target.txt`; followed target source/mount SHA `887f519c...8ea3`. | mount 0, md 0, outputs 0 |
| TC039 | PASS | Evidence size 73/layout 2; source and mounted long targets matched, output was 74 bytes including newline, followed file SHA `06539545...0f42`. | mount 0, md 0, outputs 0 |
| TC040 | PASS | Source/mounted target was `/nonexistent/repo22-g1-target`; two follow attempts returned exact errno 2 at open. | mount 0, md 0, outputs 0 |
| TC147 | PASS | Positive inline file SHA `0347f272...b01`; checksum-valid cross-block image returned errno 97 twice and truss `fstatat ERR#97`; dmesg `123->123`. | mounts 0, md units 0, outputs 0 |
| TC150 | PASS | Current-KLD rerun with `feature=0x80 rootnid_2b=36 rootnid_8b=0 blocks_lo=17`; root inode 36, root SHA `04690aad...3105`, statfs blocks 17. | mount 0, md 0 |
| TC151 | PASS | Current-KLD rerun: two direct opens returned errno 97, truss showed `fstatat ERR#97`, both mmap helpers exited 1 at open with integrity failure; dmesg `123->123`. | mount 0, md 0, outputs 0 |
## Outcome
| Result | Count |
|---|---:|
| PASS | 32 |
| KERNEL-FAIL | 0 |
| SHELVED/ISSUE | 0 |
| ENVIRONMENT-UNAVAILABLE | 0 |
No G1 issue file was created. Existing issue files concern other groups or
previously documented limitations and were not changed by this run.
## Discarded Attempts
- TC013 initially asserted a nonportable `stat(1)` wrapper exit status. The
qualified run used two direct syscall probes and truss errno instead.
- TC026 first exposed an invalid transformer assumption: a NUL had been added
after non-trailing `..`. That image was discarded, the transformer was fixed,
independent 1.9.3 validation and two-build reproduction passed, and TC026 was
rerun with the corrected image.
- TC037's first long-running remote stream did not preserve its final output.
It was not counted; the complete qualified rerun retained elapsed time, both
hashes, and cleanup evidence.
- TC151 had two pre-mount/post-evidence command assertions with incorrect grep
keys. Neither was counted; the complete qualified rerun used syscall errno and
per-output integrity counts.
## Final Cleanup
Before final unload, `kldstat` showed ID 20 and the exact KLD SHA256 above.
After `kldunload erofs`, the guest reported:
```text
erofs_mounts=0 md_units=0 kld_present=0
```
Generated fixtures, build objects, and native probes remain only in `/work/build`
and guest `/tmp`; none is staged for the repository.
@@ -0,0 +1,269 @@
# repo22 G2 Full Manual Regression Report
Started: 2026-08-09 06:07 UTC
Completed: 2026-08-09 07:10 UTC
Baseline: `cd0e985b5ac54a4b7acb7042422329ad1729fb3e`
Branch: `manual-g2-20260809T060709Z`
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, isolated port 9223
Overlay: `/work/build/repo22-manual-g2-20260809T060709Z-freebsd15-overlay.qcow2`
## Scope and Result
The exact assigned set was executed manually from the numbered Markdown
procedures. No CI, runner, result wrapper, or unassigned TC was used.
| Test | Result | Primary evidence |
|---|---|---|
| TC002 | PASS | Real CRC32C ranges; valid mount; bad covered byte `ERR#97` |
| TC008 | PASS | Bad magic `ERR#22`; 1023-byte provider `ERR#6`; empty md `EINVAL` |
| TC010 | PASS | Qualified 16 TiB sparse provider; exact 64-bit `df` total |
| TC015 | PASS | Bad block size/root/feature returned 22/97/45 |
| TC016 | PASS | Checksum-field-only mutation returned `ERR#97` |
| TC017 | PASS | Raw union fields; short `ERR#6`; qualified sparse mount |
| TC018 | PASS | Exact read at physical offset 17592191561728 |
| TC112 | PASS | Bad magic field returned `ERR#22` before root load |
| TC113 | PASS | Resolved inode `openat` returned `ERR#45` |
| TC114 | PASS | OOB FLAT_PLAIN `read` returned `ERR#97` |
| TC115 | PASS | Future algorithm bit returned `ERR#45` |
| TC116 | PASS | FBT decompressor entry count 1; `read` returned `ERR#5` |
| TC119 | PASS | FLAT_PLAIN changed byte/hash returned without kernel error |
Count: 13 assigned, 13 PASS, 0 KERNEL-FAIL, 0 SHELVED, 0 ENV,
0 duplicate, 0 omitted, 0 extra.
## Build and Guest
Both configurations were built natively in the isolated guest with kernel
`-Werror`, `FREEBSD_SRC=/tmp/repo22-freebsd15-src`, and FreeBSD source
`15.0-RELEASE-p9`. The running guest was `15.0-RELEASE-p8`.
| Configuration | KLD SHA256 | Result |
|---|---|---|
| `WITH_ZSTDIO=0` | `482e9c072c949f0459c422aa1efd9b35a2d99cf90b25bb01e59e00b440b6d0fe` | PASS |
| `WITH_ZSTDIO=1` | `8349c97c7ced253fff32a9e706f29313f309cb63a270e4e39a4501426f2b95c6` | PASS |
The ZSTDIO KLD was loaded as ID 5 and was the only repo22 test KLD. It had the
expected FreeBSD `ZSTD_*` references and no unresolved `bcmp`. The disabled KLD
had neither `ZSTD_*` nor `bcmp` unresolved symbols.
## Fixture Qualification
`tests/prepare_error_fixtures.sh` ran twice in independent empty host
directories with erofs-utils 1.8.6. Both `SHA256SUMS` files were identical and
self-verified. Every field mutation records its exact field/path and old/new
value. Same-size corruption variants preserve provider length; short and empty
providers record their intentionally different lengths.
The helper independently proved the 4096-byte-block checksum forms:
```text
canonical: field 1028 cleared, CRC32C [1024,4096), initial 0xffffffff
kernel: CRC32C [1032,4096), seed 0x5045b54a
valid control: stored=canonical=kernel=0xf7429681
```
Raw transcripts, build logs, KLDs, guest logs, fixture trees, and the overlay
remain untracked under `/work/build/repo22-manual-g2-20260809T060709Z*`.
## TC002
Result: **PASS**.
Commands: helper `inspect`; `sha256`; dynamic `mdconfig`; valid `mount`/`cmp`;
direct negative `mount`; independent `truss`.
Hashes: valid `eb97860671931c76a171d13caa70ddc2c9731c6cdef1e99b9a3deb580baf70ae`;
bad CRC `3f684e8cb03e3cb20a1920dccf23d9b7b19bc09d5745be04608e195c541ebf73`.
Both were 7622656 bytes. The corruption changed byte 1088 without recomputing
CRC; calculated checksum became `0x202160ab` while stored remained `0xf7429681`.
Valid data matched; bad direct mount rc was 1 and `nmount` returned errno 97.
Cleanup: mount absent and exact md detached.
## TC008
Result: **PASS**.
Commands: `wc -c`, `sha256`, dynamic `mdconfig`, direct `mount`, and `truss`
for each attachable provider.
Hashes: bad magic `d1bc3aec03c58dae1b74b54d895c0a72e5b3088f05907ab2f6a0cb8dc38a615f`;
1023-byte provider `5724796860baa23469b3118eff2567c96a0b64b7dadaf99eafb7ba3c65b9aa56`;
empty provider `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`.
Bad magic returned errno 22. The 1023-byte md attached and mount returned errno
6. The zero-byte provider was rejected by `mdconfig` with `EINVAL`. Cleanup:
zero mounts and md units after all three subcases.
## TC010
Result: **PASS**, not SHELVED.
Commands: copy qualified prefix, `truncate -s 17592193667072`, `stat`, dynamic
`mdconfig`, `diskinfo`, `mount`, `df -kT`, `df -iT`, and `cmp`.
Prefix hash: `d3ffadc6fc9e73f85a8a5973b4ac5ee2a2da57b4e8baeccd259fa4325a2146cf`.
Fields were `blocks_hi=1`, `blocks_lo=1861`, `rootnid_8b=36`, and
`total_blocks=4294969157`. The sparse file allocated 15232 UFS blocks while
GEOM reported all 17592193667072 bytes. `df -k` total and Used were exactly
17179876628; Avail was zero. Cleanup: unmounted, md detached, sparse provider
removed. The former environmental issue is now RESOLVED.
## TC015
Result: **PASS**.
Commands: evidence/hash/length checks; three dynamic md attachments; direct
mount status followed by independent `truss` errno capture.
Hashes: bad block size `b5c52c44ace6f693e39161102acde157efec2fce34eb4fef198681dfc7942b58`;
bad root `9cdd61eb99b2e7f45d2505b3b95d6427f8f636e117b5ffabed064a3f83997d55`;
unknown feature `95d19a490c38657a8c579a10f1f12088ab43c1819aa9b37ae86b841bb26a592a`.
All were 7622656 bytes with recomputed valid CRC. Direct mount rc was 1 for
each; `nmount` returned 22, 97, and 45 respectively. Cleanup: zero mounts/md.
## TC016
Result: **PASS**.
Commands: field evidence, `sha256`, `wc -c`, direct mount, and `truss`.
Hash: `04249f6b7d9b7322c171f1727408130d23cb0b83709aad7a4b77b39358a8dfe6`.
The 7622656-byte image changed only checksum `0xf7429681 -> 0xf7429680`.
Direct mount rc was 1 and `nmount` returned errno 97. Cleanup: no mount/md.
## TC017
Result: **PASS** with both negative guard and positive dynamic coverage.
Commands: raw `od` at offsets 1038/1060/1136; small-prefix mount; sparse
provider creation; `diskinfo`; positive mount; `cmp`; `df -kT`.
Hash: `d3ffadc6fc9e73f85a8a5973b4ac5ee2a2da57b4e8baeccd259fa4325a2146cf`.
Raw values were high 1, low 1861, root 36. The short prefix returned errno 6.
The qualified provider mounted and reported exactly 17179876628 one-KiB
blocks. Cleanup: unmounted, detached, sparse provider removed.
## TC018
Result: **PASS** with real I/O above 16 TiB.
Commands: sparse provider creation; high-offset `dd`; two raw `read_probe`
calls; low-decoy and high-source `cmp`/hash; dynamic md mount and mounted hash.
Prefix hash: `cfd86ed1e39528d958e32819282c7fd87191f80a25d9575078dad473f40563a8`.
The inode encoded `startblk_hi=1`, low 1347, combined block 4294968643, and
physical offset 17592191561728. The original low payload at 5517312 was zeroed:
its hash was `6a4875ddaceaa91fb3369f0f6d962f77442daf1b1d97733457d12bcabdf79441`.
Raw high and mounted hashes both matched source
`25eb31e024bc60745d68a5f7318753951804c7858b576cb698a4260af4a334a3`.
Cleanup: unmounted, detached, sparse provider and probe outputs removed.
## TC112
Result: **PASS**.
Commands: raw magic `od`, hash/length, direct mount, and `truss`.
Hash: `d1bc3aec03c58dae1b74b54d895c0a72e5b3088f05907ab2f6a0cb8dc38a615f`.
Magic was `0x21444142`; provider length remained 7622656. The canonical CRC was
invalid as required after changing magic, while the unchanged production
suffix checksum still matched `0xf7429681`. Direct rc was 1 and `nmount`
returned errno 22. Cleanup: no mount/md.
## TC113
Result: **PASS**.
Commands: structured path inspection; mount/control `cmp`; direct and trussed
`read_probe expect-error ... 45`; second control `cmp`.
Hash: `28d972c4065927326607fcd51dabff81cc88a28e2343f6056e55d04418172814`.
Resolved NID 52 at inode offset 1664 changed format `0x0001 -> 0x8001` with
valid CRC and equal provider length. `openat` returned errno 45; unaffected data
remained exact. Cleanup: clean unmount and md detach.
## TC114
Result: **PASS**.
Commands: path inspection; mount/control `cmp`; direct and trussed
`read_probe expect-error ... 97`; second control `cmp`.
Hash: `2afcb9dde4c8eaf30bb2541d00be36f726f5cf151d887e0d999803b3993285ed`.
NID 54 field offset 1744 changed start block `1349 -> 1861`, equal to declared
blocks, with valid CRC and equal media size. Target `read` returned errno 97;
control remained exact. Cleanup: clean unmount and md detach.
## TC115
Result: **PASS**.
Commands: raw field `od`, hash/length, direct mount, and `truss`.
Hash: `a8d7d9cf0abcf34a5cd2366059c0294e08fdd20d02ed3b5b21005ddf78c69029`.
`available_compr_algs` at byte 1106 changed `0x0000 -> 0x8000`; CRC was
recomputed and media remained 716800 bytes. Direct rc was 1 and `nmount`
returned errno 45. Cleanup: no mount/md.
## TC116
Result: **PASS** with direct decompressor-path proof.
Commands: parsed `dump.erofs -e` evidence; failed userspace extraction record;
mount/control `cmp`; FBT enumeration and count; direct/trussed `read_probe`.
Hash: `f442785ea7cdbdc3f64b2ff89ea04942460cbdc5bea5615cbed237029edda8d8`.
The equal-length 716800-byte image changed 64 bytes at 4128, wholly inside the
parsed first physical extent `[4096,69632)`. Mount succeeded, excluding the
media-size precheck. `fbt:erofs-zstdio:z_erofs_decompress:entry` counted one
call for the target command, and `read` returned errno 5. Control remained
exact. Cleanup: unmounted/detached; `dtraceall` and dependencies unloaded.
## TC119
Result: **PASS** with the revised real integrity semantics.
Commands: path/field evidence; image/source hashes; mount; complete mounted
hash; two one-byte `read_probe` calls; byte compare; control `cmp`.
Hash: `74b54f998481f9e55b30f2393ee780e3b46144157a54b4dc0fc0c044c160bb5a`.
The equal-length image flipped `/plain.bin` file offset 257 at provider byte
5525761 from `0x00` to `0x80`; the valid superblock CRC remained unchanged.
Source hash was `5647f05ec18958947d32874eeb788fa396a05d0bab7c1b71f112ceb7e9b31eee`;
mounted hash was `e98bb2acaecb24ab86112478bc8cc1e3668c127b449fe2e664dc4804f9477d81`.
Reads succeeded, the target byte differed, and control matched. Cleanup: clean
unmount and md detach.
## Harness Notes
Four incomplete attempts were discarded and are not PASS evidence:
1. TC002 repeated host Python inspection in a guest without Python and exited
before md attachment.
2. TC002 initially trusted `truss` wrapper status; the qualified run separated
direct mount rc from syscall tracing because FreeBSD `truss -o` returned 0
around a failing mount.
3. TC008 had a stray patch marker after its second subcase; trap cleanup ran and
the complete three-subcase command was repeated.
4. TC010 initially asserted the `df` Used column was free blocks; the qualified
run used the correct `f_bfree=0` interpretation and repeated all steps.
## Final Cleanup
Per-TC cleanup ended with zero EROFS mounts and zero md providers. Global dmesg
changed from 102 to 104 lines; both new lines were the expected
`md0: truncating fractional last sector by 511 bytes` from TC008. New panic,
fatal trap, page fault, general-protection fault, and double-fault count was
zero. The exact test KLD unloaded successfully, leaving zero EROFS modules.
The guest shut down normally and QEMU PID 285696 exited after 12 seconds. Port
9223 no longer has a VM process.
## Issues
No kernel issue was found and no TC remains SHELVED. The former TC010 provider
issue is retained as a resolved record in
`issues/TC010-48bit-statfs-large-provider.md`.
@@ -0,0 +1,132 @@
# repo22 G4 full manual regression report
## Scope and result
- Exact scope: TC005, TC067-TC083, TC117, TC134-TC140, TC142.
- Count check: 27 requested, 27 executed, 0 duplicate, 0 omitted.
- Result: **PASS 27, FAIL 0, SHELVE 0, NOT RUN 0**.
- Kernel source baseline: `9ae22009f23a65320730072a780998e80aa9b728`.
- No repo22 kernel source was changed during this regression.
- No kernel failure was found, so no new `issues/` entry was required.
## Isolated environment
- Worktree: `/work/build/repo22-manual-g4-20260809T072432Z`.
- Branch: `manual-g4-20260809T072432Z`.
- Dedicated qcow2 overlay backed by the FreeBSD 15 development base.
- Dedicated SSH forward: `127.0.0.1:9224`; port 9222 was not used.
- Guest: FreeBSD `15.0-RELEASE-p8`, amd64, GENERIC,
`releng/15.0-n281036-53054229dcb3`.
- Final guest cleanup: zero matching mounts, zero md units, EROFS malloc
active count 0, KLD unloaded, guest responsive, QEMU stopped.
## Exact KLD build
The module used only `repo-community/repo22/src` from the baseline worktree.
It was built for `x86_64-unknown-freebsd15` with all 14 Makefile source files,
FreeBSD `15.0 RELEASE-p9` headers, clang/LLD 19.1.7, and `WITH_ZSTDIO=0`.
The guest is p8; both header and guest are the same FreeBSD 15 release KBI.
```text
erofs.ko SHA256 = ee0b2e7c4ebf3602de8be8d47ddcfd021bc90a5265e7e9bf449bb9de2fbf4f04
guest kldstat = erofs.ko, module erofs, id 507
```
## Fixture provenance
Installed `mkfs.erofs`, `dump.erofs`, and `fsck.erofs` reported erofs-utils
1.8.6. `tests/g4_fixtures.py` created both source trees without consuming old
images, ran four deterministic mkfs commands, transformed structured fields,
and reopened every result. A second fresh output directory produced identical
source and image checksum files.
```text
source inventory SHA256 = c48f72777b0a04514fcfc7ea9c4ed77089ab0ed4537eac9cb7430e1acafe3d0a
metabox payload SHA256 = 50c5740b5fe5630ee919022d7ee1ce59d3e7d089c9d66e2baf82bf9de8233d40
```
| Image | SHA256 |
|---|---|
| `basic.erofs` | `b375a3ca60de64b2c635b3ec45c9285c840e0a05bec105d5d8f68308300e7670` |
| `prefix-primary.erofs` | `49fc472a26412d154df729d8b0079801b35e97af2eb7fc3fa226dc65ee1963f9` |
| `metabox-plain.erofs` | `dd7b04097d1bfe283b65c95d759acd2176beb7cb0f1fe9d5ddbc0694b5acba9d` |
| `metabox-compressed.erofs` | `682b9dc1e0b492d935c03deba2ab644c1f0bcbf8e1ec623c15fb4990d25fe584` |
| `metabox-fragment.erofs` | `c93f4c281ed621519ced42a45cdb21b543047b7058dab31ad0def851cc19f4f1` |
| `bad-inline-entry.erofs` | `f18693c880bac0a273f2497cb0f81c0c52e19edc3ad45354eb38a39624adafd2` |
| `bad-shared-entry.erofs` | `875c27df56de2336fded3ef1db1c9fdc035db618096aa9269bfe70ea6f59c664` |
| `bad-shared-declared-bounds.erofs` | `19fc69cbe21051de2cf1cc19454cf64cc4a0ece444c6607956e066c2d4d332b3` |
| `bad-prefix-declared-bounds.erofs` | `d24b7a171aa28c3a46e3893749a4d08ab5d467c103f47aefcc0ca0f4e153d365` |
| `bad-metabox-truncated-extension.erofs` | `0bad23b1ad77eb8f2ffee83f89b7ef91b1c49ba2a20661adca21cd9efce0911e` |
| `bad-ishare-prefix-id.erofs` | `10357f28a11c2a8bbc329e01a414248415719ef088734a6ad75b934c0f65fc43` |
| `bad-fragment-self-loop.erofs` | `3b61228041591716c650a6613de30b2d9abe8b01148a9ae513b7de4d9b958d78` |
| `bad-fragment-range.erofs` | `432618f98a6e32351448120d8c694a4f31b45cb3078e08dad39b85fa01b47039` |
| `bad-metabox-recursive-nid.erofs` | `ab28a5478f569eb8a59e59e94448a85918885fcd328dadc2557ba6222dcd5aaf` |
| `bad-packed-recursive-nid.erofs` | `ad9a0ed1598c8a8e16911190c9042d58315fbcdc23af824ea106af55ffe6f2f4` |
The fragment positive self-check found carrier NID 46, compressed-full layout,
size 32768, fragment header `0x8000000000000064` at image offset 9696,
fragment offset 100, packed NID 48, and packed size 32868. Thus
`100 + 32768 == 32868`. Shared entries were at metabox offsets 4096, 4128,
and 4156; prefix records were at 8192 and 8224 with base indexes 1 and 4.
## FreeBSD namespace semantics
Linux `user.*` was queried through FreeBSD namespace `user` with the leading
`user.` removed. Linux `trusted.*`, `security.*`, and ACL indexes were queried
through namespace `system`. Trusted/security list names retained their full
prefixes; ACL names were `posix_acl_access`/`posix_acl_default`. This is the
FreeBSD extattr ABI and intentionally does not copy Linux `getfattr` commands.
`getextattr` does not reliably communicate every kernel failure through its
process status, so errno evidence came from guest `truss`: `ENOATTR=87`,
`EINTEGRITY=97`, and `EROFS=30`. Every corruption in this scope returned
`EINTEGRITY`; no case required normalization to `EIO=5`.
## Per-TC results
| TC | Guest evidence | Result |
|---|---|---|
| TC005 | inline user list; exact NUL-containing value; miss `ENOATTR 87` | PASS |
| TC067 | shared `shared_key` exact on two files; inline `local` exact | PASS |
| TC068 | missing name and wrong namespace both `ENOATTR 87` | PASS |
| TC069 | three shared names once; text, NUL, and binary values exact | PASS |
| TC070 | shared `trusted.config` exact; unprivileged system access denied | PASS |
| TC071 | shared `security.selinux` exact; miss `ENOATTR 87` | PASS |
| TC072 | system lists trusted, security, ACL names; user subset empty | PASS |
| TC073 | plain metabox mounted; bit-63 inodes, file hashes, xattrs exact | PASS |
| TC074 | three inline user values exact, including `00..1f` | PASS |
| TC075 | inline `trusted.admin` exact; user lookup `ENOATTR 87` | PASS |
| TC076 | capability and SELinux binary bytes exact | PASS |
| TC077 | packed user long-prefix names and values exact | PASS |
| TC078 | packed trusted long-prefix names and values exact | PASS |
| TC079 | packed NID 1201; two qualified records; both namespaces exact | PASS |
| TC080 | four repeated prefix lookups exact; miss `ENOATTR 87` | PASS |
| TC081 | metabox shared, inline, and long-prefix shared values exact | PASS |
| TC082 | raw ACL bytes and `getfacl -n` agree; user lookup `ENOATTR 87` | PASS |
| TC083 | four user attributes exact; system list empty | PASS |
| TC117 | malformed inline/shared gets both `EINTEGRITY 97`; mount stable | PASS |
| TC134 | nonzero xattr base shared value exact on two bit-63 inodes | PASS |
| TC135 | metabox, packed, and primary prefix backings all exact | PASS |
| TC136 | truncated extension and bad ishare ID mounts `EINTEGRITY 97` | PASS |
| TC137 | shared get and prefix mount bounded at declared image, errno 97 | PASS |
| TC138 | UID order 3002/2002 accepted; empty fallback; 3 negatives errno 97 | PASS |
| TC139 | FIFO stat/ACL/xattr reads; set/delete operations `EROFS 30` | PASS |
| TC140 | compressed and fragment metabox positive; 4 negatives errno 97 | PASS |
| TC142 | independent fragment rerun positive; 4 negatives errno 97 | PASS |
## Stability and cleanup
Positive values were compared as `getextattr -qq -x` bytes, not display text.
Negative mount and VOP calls were traced at the kernel syscall boundary. Each
case recorded zero matching mounts and zero matching md providers after its
cleanup. TC140 and TC142 additionally showed:
```text
erofs active allocations = 0
guest responsiveness = ok
panic/trap/hang = none
```
The final KLD unload succeeded. The dedicated guest powered off, QEMU exited,
and TCP port 9224 no longer had a listener. Generated images, KLDs, overlays,
and raw logs remained outside Git and were not included in the commit.
@@ -0,0 +1,154 @@
# repo22 G3 Full Manual Regression Report
Execution window: 2026-08-09 10:42-10:59 UTC
Exact source baseline: 9ae22009f23a65320730072a780998e80aa9b728
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, port 9222
## Scope and Method
This run covers exactly these 31 test cases and no others:
TC041-TC066, TC141, TC148, TC149, TC152, TC153
Every test was executed directly from its numbered Markdown procedure with a
fresh dynamic md provider. No CI, runner, result wrapper, historical PASS, or
unassigned TC supplied a result.
Set audit: requested 31, selected 31, unique 31, duplicate 0, missing 0,
extra 0.
## Exact Build
The source archive was exported with git archive from the exact baseline. Its
SHA256 was:
c7f36610a523427dc406023e1c27fc87a03076bbde2e6b1406311fa5ddb06b0f
The module was built natively in the guest with:
FREEBSD_SRC=/tmp/repo22-freebsd15-src WITH_ZSTDIO=0 ./build.sh
The build completed with kernel -Werror. The KLD loaded as ID 20, size 0xb8a8,
and remained the only loaded EROFS module for the test interval.
| Item | SHA256 |
|---|---|
| erofs.ko | 19ad086bd2508cbb4c57b1a51f38c93057d438b84fbcfa2e637ff2519f5bc590 |
| readdir_probe | 2387964fd0c153f9cba0e41dd57fbdc1d48c2f2b779540f27387fb168cd35128 |
| g3_vfs_probe | 08fe3999600dd1826805c00a17af3d893d247a593c0812f767d85a2633302068 |
| stat_special | 1c9b4e8fea449d2fc7b985c468a0267448db5998dd413c6c4ed36a1ab0c78ea4 |
| nfs_fh_tool | db704f67d718d04b84ed32b5cdbecb93a99c5fe3b4ab238d603a10e005d790e3 |
| mmap_fault | ba32712f9a4a01f4d85d8f5f6fafe2741acbd6b185a85fa759da73dbc416233c |
| sparse_hole_probe | e5e1d8750fb671f0fccafdf51b61f4b0e73837718906dbd5fd5e8ab01cc790aa |
The KLD unresolved-symbol list contained both
vnode_pager_local_getpages and vnode_pager_local_getpages_async; both resolved
at load.
## Fixture Qualification
tests/prepare_g3_fixtures.sh completed its structured assertions and all four
checksum-manifest checks. The relevant image hashes are:
| Alias | Image | SHA256 |
|---|---|---|
| P | vfs-plain.erofs | 79f0b5f4aa8e7ea532b711ee8fb466f14f35d0952446d41ba4bbc4d6e008b8ff |
| Z | vfs-lz4.erofs | 07e2d1fbda0e6dc1233e6943a20f85e1f6652b580632c4c8264641a9a8805743 |
| N | namei-base.erofs | d1730ff23836797c6c09e1b39b1cf23efc16e27f85ab07fdcab577bb82871659 |
| Pad | namei-padding-nonzero.erofs | 9a94e9af2cab264b9c11975a20d78e615d6fe1e6f86267173cb5ac86aecb2b17 |
| Cshort | namei-corrupt-short.erofs | fb89f74795a5569ed3a85d63836dd75a06e1f17823d0508048c37da710ea6e75 |
| Coff | namei-corrupt-nameoff.erofs | b0b70ee615f163430f04edb91ab76c27ee8cc9a75b8ebb2008834f5b550e3933 |
| Cname | namei-corrupt-name.erofs | 6a980ad3e241603eda2ef71a470c82975291c614366a401e4e89c17c9adf9b91 |
| SC | special-compact.erofs | fd78256dd83d9d6d957e5f843c7a8e8a175a4b3243d528bebd299b0226853237 |
| SE | special-extended.erofs | e73e9b84d9ceb8c2b07e9c2732733b0fd607736c68c09522a2402fbeef6ba8d5 |
| H | extent-hole-5g.erofs | 50014a24493918247e36511ad34a2fe8ab47ae09ea46d7fd62a1bed445a6c65f |
| L | large-dir-intmax.erofs | 0f90d3d57adbbbd946e41b225c1f6c464915c6abb0b13478ec9b2a318def1f72 |
| LS | TC153 sparse prefix | f9337f83b1f568f6d904331a7749e6362a691055cd5af95b59bc89772f04e3b0 |
The wide directory has 320 real files. The structured padding mutation records
wide NID 42, directory block 6, 80 dirents in the patched block, final-name
offset 4043, and the eight changed bytes at offsets 4084:4092.
The 5 GiB fixture records size 5368713216 and one 16-byte extent with plen=0.
TC153 records extended FLAT_PLAIN/Layout 0, NID 40, start block 16,
2147483649 directory blocks, and final block index 2147483648.
## Per-Test Results
Every cleanup cell means the literal umount and mdconfig detach commands
returned zero; a following mount -t erofs and mdconfig -l produced no rows.
| Test | Result | Actual command, errno/behavior, and hash evidence | Cleanup |
|---|---|---|---|
| TC041 | PASS | readdir_probe testdir 512; errno 0; 36 entries/restarts, types 8 dir/24 reg/4 link, FNV a3b969b5aa90cdbe, exact 34-name manifest and 255-byte name; image P. | umount 0; md 0 |
| TC042 | PASS | readdir_probe wide 128 plus three stat calls; errno 0; 322 entries/restarts, 320 files, FNV f3bfa2c15b231a59, sample NIDs 72/390/710; image N. | umount 0; md 0 |
| TC043 | PASS | four g3_vfs_probe stat calls, cmp, sha256; errno 0; regular/dir/link metadata stable and mounted file matched source; image P. | umount 0; md 0 |
| TC044 | PASS | direct stat probes; missing/missing-parent errno 2, positive errno 0; image P. | umount 0; md 0 |
| TC045 | PASS | stat four case variants plus direct miss; NIDs 101/88/86/87, unmatched spelling errno 2; image P. | umount 0; md 0 |
| TC046 | PASS | readdir_probe testdir 4096 and exact manifest cmp; errno 0; 36 entries/restarts, FNV a3b969b5aa90cdbe; image P. | umount 0; md 0 |
| TC047 | PASS | readdir_probe wide 128, sorted count/duplicate checks; errno 0; 322 entries/restarts and 320 unique files; image N. | umount 0; md 0 |
| TC048 | PASS | readdir_probe wide 128; every kernel d_off reopened/lseeked, every libc cookie seeked on its producing DIR stream; 322/322 restarts, FNV f3bfa2c15b231a59; image N. | umount 0; md 0 |
| TC049 | PASS | readdir_probe plus two dot stat calls and cmp; errno 0; dot and directory both NID 44, source-exact path; image P. | umount 0; md 0 |
| TC050 | PASS | readdir_probe child plus parent/dotdot stat and cmp; errno 0; parent NID stable and multiple dotdot path source-exact; image P. | umount 0; md 0 |
| TC051 | PASS | repeated stat and cmp; errno 0; byte-identical NID/mode/size/generation and data; behavior-level only; image P. | umount 0; md 0 |
| TC052 | PASS | repeated negative stat then positive stat; errno 2/2/0; no positive poisoning; behavior-level only; image P. | umount 0; md 0 |
| TC053 | PASS | repeated stat, getfh, compare, describe; errno 0; NID 101, gen 3444757833, identical handle SHA 5000984ddaeca26d90e8fd59568a55afcb074f51aaa00eed8d422d698a4aefc6; image P. | umount 0; md 0 |
| TC054 | PASS | parent and two dotdot stat calls; errno 0; identical NID/generation/type, no lock diagnostic; behavior-level only; image P. | umount 0; md 0 |
| TC055 | PASS | g3_vfs_probe stat, native stat, sha256 on plain/LZ4; errno 0; size 21211, blocks 48/8, data SHA d09ed1cee6520e36e54d5f2fd8c3bc74bd46cd8a58426744cc301b638c83e9d4; images P/Z. | two umount 0; md 0 |
| TC056 | PASS | stat_special char/block/fifo plus stat on compact/extended images; errno 0; rdev 2748:344865, FIFO NODEV, size/blocks 0; images SC/SE. | two umount 0; md 0 |
| TC057 | PASS | nobody direct access-read, access-exec directory/file, open-read; errno 0 for all; image P. | umount 0; md 0 |
| TC058 | PASS | nobody direct access probes; restricted read/read/exec errno 13, regular-file write access errno 30; image P. | umount 0; md 0 |
| TC059 | PASS | four readlink probes, cmp, broken follow; target lengths 8/16/14/119 and FNV hashes recorded, follow errno 0/2; image P. | umount 0; md 0 |
| TC060 | KERNEL-FAIL | g3_vfs_probe pathconf; values 255/1024/64/2147483647, but NO_TRUNC and CHOWN_RESTRICTED each errno 22; probe status 1; image P; issue TC060-pathconf-standard-values.md. | umount 0; md 0 |
| TC061 | PASS | direct chmod probe and before/after stat cmp; errno 30, metadata unchanged; image P. | umount 0; md 0 |
| TC062 | PASS | direct chown probe and before/after stat cmp; errno 30, metadata unchanged; image P. | umount 0; md 0 |
| TC063 | PASS | direct O_RDWR/truncate/create probes plus before/after sha256; errno 30/30/30, hash unchanged and no new entry; image P. | umount 0; md 0 |
| TC064 | PASS | nm -u plus mmap_fault on plain/LZ4; helper errno assertions passed; size 21211, six faults, FNV a1890a1c216724be, EOF/SIGBUS/COW PASS; images P/Z. | two umount 0; md 0 |
| TC065 | PASS | real mmap_fault on plain/LZ4; MAP_SHARED EACCES and O_RDWR EROFS asserted, same FNV a1890a1c216724be; images P/Z. | two umount 0; md 0 |
| TC066 | PASS | exact source erofs_bmap EOPNOTSUPP audit, KLD symbols, real mmap fallback on plain/LZ4; both FNV a1890a1c216724be, no strategy diagnostic; images P/Z. | two umount 0; md 0 |
| TC141 | PASS | fresh open-read/cmp, two ENOENT lookups, 322-cookie probe on N/Pad; two lookup and readdir attempts on Cshort/Coff/Cname all errno 97; all five hashes above. | five umount 0; md 0 |
| TC148 | PASS | actual Pad image cmp/count/readdir_probe; errno 0; 320 files, 322 restarts, FNV f3bfa2c15b231a59; hash Pad and patch offsets recorded. | umount 0; md 0 |
| TC149 | PASS | real mmap_fault on plain/LZ4; same size/fault/FNV/EOF/SIGBUS/COW evidence as TC064 with fresh mounts; images P/Z. | two umount 0; md 0 |
| TC152 | PASS | stat, vmstat -m, two timed sparse_hole_probe calls at 3221225472; errno 0, pread/mmap zero, 0.02 s each, RSS 2652/2656 KiB, erofs memory 768 bytes; image H. | umount 0; md 0 |
| TC153 | PASS | 8796093091840-byte sparse provider, diskinfo, stat, two cold and one post-readdir lookup; /huge size 8796093026304, all lookup errno 97, root 3-cookie FNV cd20cb6ae9fe01ba; prefix LS/KLD hashes. | umount 0; md 0; sparse file removed |
## Outcome
| Result | Count |
|---|---:|
| PASS | 30 |
| KERNEL-FAIL | 1 |
| SHELVED/ISSUE | 0 |
| ENVIRONMENT-UNAVAILABLE | 0 |
The only new open issue is issues/TC060-pathconf-standard-values.md. The former
TC153 validation issue is resolved by the qualified sparse-provider run.
## Discarded Attempts
- The first TC041 helper run passed telldir cookies across a closed and newly
opened DIR stream. POSIX only guarantees a cookie on its producing stream.
The helper was corrected and rebuilt with -Werror; this run was not counted.
- TC041 was then attempted with a 128-byte getdirentries buffer, too small for
the fixture's 255-byte name record. The 512-byte qualified rerun passed.
- TC047 was first attempted with a 64-byte buffer, too small for its long
records. The 128-byte qualified rerun still forced repeated reads and passed.
No discarded command was counted as a kernel result.
## Dmesg and Final Cleanup
The dmesg baseline had 123 lines. The only eight new lines were expected
mmap_fault child exits on SIGBUS: two images for each of TC064, TC065, TC066,
and TC149. There was no new EROFS warning, integrity diagnostic, assertion,
trap, panic, OOM, dirty writeback, or "No strategy for buffer" line.
Before unload, the exact KLD hash was rechecked. KLD unload returned zero.
The guest then reported:
erofs_mounts=0 md_units=0 kld_present=0 guest_artifacts=0
The TC153 sparse provider and all transferred/generated guest files were
removed.
@@ -0,0 +1,157 @@
# repo22 TC060 pathconf fix manual test report
Execution window: 2026-08-09 11:20-11:35 UTC
Exact source commit: `cdba7e54fb9e9d82980a06f178e68d0bbc1663ac`
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, port 9222
## Scope and Result
TC060 is **PASS**. The original six-key helper returned the required values
with errno 0, compatibility queries retained their previous values, an unknown
name returned EINVAL, mount/read/read-only smoke passed, dmesg did not change,
and final mount/md/KLD/artifact counts were zero.
This was a direct manual run. No CI, result wrapper, or historical PASS supplied
the result. `WITH_ZSTDIO=1` was build-verified; runtime TC060 used
`WITH_ZSTDIO=0` because pathconf is compression-independent and this avoids an
unnecessary optional runtime dependency.
## Implementation Basis
FreeBSD 15 `sys/kern/vfs_default.c` shows that `vop_stdpathconf()` handles
generic `_PC_ASYNC_IO`, `_PC_PATH_MAX`, and several zero-valued optional
features, then returns EINVAL for other names. The same source tree's UFS,
tmpfs, and ext2fs vnode operations explicitly return 1 for
`_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` before delegating the remaining names
to `vop_stdpathconf()`.
The EROFS fix adds only those two switch labels and retains the existing
default delegation. `src/namei.c` rejects components longer than
`EROFS_NAME_LEN` with ENAMETOOLONG, while `src/erofs_vnops.c` rejects uid/gid
mutation with EROFS. Linux EROFS's 255-byte name limit and ENAMETOOLONG lookup
check were reviewed only for maintenance similarity; FreeBSD VOP behavior was
authoritative.
## Exact Build
The repo22 subtree was exported directly from the exact source commit. Later
`current/`-only reporting changes did not alter `src/`.
| Item | Value |
|---|---|
| Source archive SHA256 | `a4c871ca7cefc49470ef0003f876792380a8d8db6cbeaefb96456d06ad946d53` |
| FreeBSD source path | `/tmp/repo22-freebsd15-src` |
| Compiler | FreeBSD clang 19.1.7 |
| Build command | `FREEBSD_SRC=/tmp/repo22-freebsd15-src WITH_ZSTDIO=N ./build.sh` |
Both clean-object builds completed with kernel `-Werror`:
| Configuration | Module SHA256 | Size | Undefined-symbol audit | Result |
|---|---|---:|---|---|
| `WITH_ZSTDIO=0` | `68536e03ce93c6c04aab9cf29dab81ee5802d4b94401bd1a4bd752de40c0e504` | 73896 | no `bcmp`, no `ZSTD_*` | PASS |
| `WITH_ZSTDIO=1` | `598f171d355af78c64407a6d513d20f053530620da92df7f8ccfdf9a804e463d` | 78728 | no `bcmp`; only five expected FreeBSD `ZSTD_*` APIs | PASS |
The enabled module referenced `ZSTD_DCtx_setParameter`,
`ZSTD_createDCtx_advanced`, `ZSTD_decompressStream`, `ZSTD_freeDCtx`, and
`ZSTD_isError`.
## Fixture and Probes
| Artifact | SHA256 |
|---|---|
| `vfs-plain.erofs` | `79f0b5f4aa8e7ea532b711ee8fb466f14f35d0952446d41ba4bbc4d6e008b8ff` |
| tracked `g3_vfs_probe` binary | `08fe3999600dd1826805c00a17af3d893d247a593c0812f767d85a2633302068` |
| one-run `pathconf_audit` source | `c2cc69297848cad8c806d47585ef670ae5b378827450110e2cf47cf82ed527b3` |
| one-run `pathconf_audit` binary | `17aa9d738871ac424900ed7a928cb72cc2e5a288f2c2f9744906fa139a78835b` |
Both probes compiled natively with `-O2 -Wall -Wextra -Werror -std=c17`.
The one-run helper was kept outside the repository and only printed each
value/errno plus assertions for unknown-name and overlong-component behavior.
## TC060 Values
The exact original command was:
```text
./g3_vfs_probe pathconf /tmp/repo22-tc060-fix/mnt/testdir
```
It exited 0 and printed:
```text
name_max=255 path_max=1024 filesizebits=64 link_max=2147483647
no_trunc=1 chown_restricted=1
```
The expanded value/errno capture was:
| Query | Actual value | errno |
|---|---:|---:|
| `_PC_NAME_MAX` | 255 | 0 |
| `_PC_PATH_MAX` | 1024 | 0 |
| `_PC_FILESIZEBITS` | 64 | 0 |
| `_PC_LINK_MAX` | 2147483647 | 0 |
| `_PC_NO_TRUNC` | 1 | 0 |
| `_PC_CHOWN_RESTRICTED` | 1 | 0 |
| `_PC_ASYNC_IO` | 200112 | 0 |
| `_PC_ACL_EXTENDED` | 1 | 0 |
| `_PC_ACL_PATH_MAX` | 254 | 0 |
| `_PC_ACL_NFS4` | 0 | 0 |
| unknown name `INT_MAX` | -1 | 22 (`EINVAL`) |
A lookup using a 256-byte path component returned -1 with errno 63
(`ENAMETOOLONG`), confirming the reported no-truncation behavior.
## Mount and Read Smoke
The module loaded as KLD file ID 20, module `erofs`, and the fixture attached
as dynamic `md0`. The mount line was:
```text
/dev/md0 on /tmp/repo22-tc060-fix/mnt (erofs, local, read-only, acls)
```
`testdir` reported inode 44, mode `drwxr-xr-x`, and size 1039. Reading
`testdir/file.txt` produced `repo22 G3 file payload` and SHA256
`523af4c899ba3b8f4fa4d854fe609590be271905b63932cf067ca9630e612687`.
Opening the same file with `O_RDWR` returned errno 30 (`EROFS`) as expected.
## Dmesg and Cleanup
The qualified run's dmesg had 131 lines before and after, with identical
SHA256 `91bf74a3607f432b4802f143bcf9d050c2f9e741da0a54f3e6a1b47bcc996a06`.
There was no new EROFS diagnostic, assertion, trap, panic, or resource warning.
Qualified cleanup returned zero for umount, md detach, and KLD unload. After
removing the transferred archive and test directory, the final guest audit was:
```text
erofs_mounts=0
md_units=
erofs_klds=0
guest_artifacts=0
```
## Discarded Attempt
The first otherwise successful runtime pass loaded the KLD from the
non-canonical filename `erofs-zstdio0.ko`. Its explicit `kldunload erofs`
command could not resolve that filename, so the run stopped before final dmesg
and cleanup qualification. Its mount and md were already clean; KLD file ID 20
was then unloaded directly and the guest returned to zero resources. The full
test was repeated with canonical `erofs.ko`, and only that rerun is counted.
## Independent Review
The post-test review found no lock, resource, or error-path issue:
- `VOP_PATHCONF` is called with the vnode shared-locked; the function does not
change lock state.
- The new constant-return branches allocate nothing and acquire no references.
- Both normal and FIFO vnode vectors use the same filesystem-wide result.
- Existing value branches are unchanged, and unsupported names still reach
`vop_stdpathconf()` and return EINVAL.
- The syscall copies `retval` only on error 0, so the unknown-name error path
cannot expose a stale value.
@@ -0,0 +1,212 @@
# repo22 G5 compression full manual regression report
## Scope and result
- Exact scope: TC003, TC004, TC084-TC092, TC102-TC110, TC143-TC146.
- Count check: 24 requested, 24 executed, 0 duplicate, 0 omitted.
- Result: **PASS 23, PARTIAL 1, FAIL 0, NOT RUN 0**.
- SHELVED subscenario: TC146 explicit mapped extent only.
- TC146 overall: **PARTIAL**; HEAD2 PASS, interlaced PASS, explicit extent
SHELVED.
- Kernel baseline: `f383bbbbff301a6bde18894f03ab88a8c0cc885a`.
- Initial and pre-push `xdm/main` baseline: the same commit.
- No repo22 kernel source was changed. No kernel failure issue was opened.
## Isolated environment
- Worktree: `/work/build/repo22-manual-g5-20260809T085450Z`.
- Branch: `manual-g5-20260809T085450Z`.
- Artifact root: `/work/build/repo22-g5-20260809T085450Z`.
- Dedicated qcow2 overlay backed by the FreeBSD development base.
- Dedicated SSH forward: `127.0.0.1:9225`.
- Guest: FreeBSD `15.0-RELEASE-p8`, amd64,
`releng/15.0-n281036-53054229dcb3`.
- Guest toolchain: FreeBSD clang/LLD 19.1.7.
- Host tools: mkfs/dump/fsck erofs-utils 1.8.6.
- No CI, runner, or wrapper was used. Each Markdown test was issued manually.
## Reproducible fixture
`tests/g5_fixtures.py` generated source content, invoked mkfs with fixed UUID,
timestamp, owner, sort order, and one worker, transformed structured records,
reopened every image, and wrote a field manifest. Two final fresh output
directories produced byte-identical source and image checksum inventories.
```text
SHA256SUMS SHA256 = 8535696f187f818df8b3b30ee35cd8837aceadef9de7a85be1475732dcdf0596
SOURCE-SHA256SUMS SHA256 = 145bd663d7552025b486ec15c0b4c1c0ce99affa7728dc4d762134bdc92420c3
fixture manifest SHA256 = 63ecdb46f8cc3c04e348e24e0f1306ad5e4606bf1fa108919f946515d7b41d63
image count = 25
source count = 19
guest hash verification = 25 images, 19 sources
```
### Image hashes
| Image | SHA256 |
|---|---|
| `deflate-level1.erofs` | `58dc78da2ec5a751688f53c4eee34f94b7f828d68b7225aea006d08374187bfe` |
| `deflate-level6.erofs` | `4a3b5d2ce6b8d1bad23aa2c4d4ce38f83bc0e6642523e9165e58afe03e030c28` |
| `deflate-level9.erofs` | `c4bf219a5130d475c26452a1a5346c63ed1f434afd4315972ee30ebc66f9c751` |
| `deflate-partial-ref.erofs` | `fccb2f4038dca8b7277984e255a304bb6eee5de51fedd7b7261ef4f879362c81` |
| `deflate-partial-ref-corrupt.erofs` | `063e1bc07d2c3a3bf3b17c0cce0237eca7000eb16abdf8a1b4428d8cc7880811` |
| `extent-attempt.erofs` | `ee7472c23ccffd5eef6f4a3171ea53ae2e840f8a1ea417b2630065175ecf3225` |
| `head2.erofs` | `4948335059e605ff70da4f6e516c4ba24d58313d10e836cc6fe9c9c468775a23` |
| `head2-corrupt.erofs` | `7c25e9c8c767d2717c06b1efc854f0bb3b1f370cf3e2a8e2d53e767918bfe50f` |
| `interlaced.erofs` | `7b77055dfae301a0acc3202cf0d7cf7062eb82c6101d16b68c91f70615e1638b` |
| `lz4-compact-4k.erofs` | `0701a37430372f1240ad6485c83f0048d97bb49ae1ca85e1423dd05101bf5578` |
| `lz4-compact-64k.erofs` | `967cc1b625546f9f9f881472e71cc3d849c65feb4e98e67fbfdc9b90a4be6dda` |
| `lz4-compact-256k.erofs` | `6fbded24756b557c7ce19a5936dcde01fb75d4b1c91b0236fca46ee0c6331db0` |
| `lz4-full-4k.erofs` | `3bc6eb538752673e2b9f905a0382887bccf417574a0d04901932cfeaac258bc7` |
| `lz4-large.erofs` | `0340026fe5fea5fd7c6c4c03f288815bc86c7517c2c2a9a2ca21c61dfb96a973` |
| `lz4-ztail.erofs` | `18cd045d7f2b8e542a0dda6fb0063c266919928e6e485f498ed93c8992d9ae3a` |
| `lzma-level6.erofs` | `32107a084b27362a093768b88746c37c2e99a74b9f1301a9d4046988479defe9` |
| `lzma-large.erofs` | `6534870a686c3c5ba58fc665ea4995c07427a15c9d5ac7747690114138adf47d` |
| `lzma-partial-ref.erofs` | `fbbcbcb8a178370a8f7665c3e0cff8df5bb6f81c3ba38a0e44c137e8ed165402` |
| `lzma-partial-ref-corrupt.erofs` | `23981af2f4e36f88e382fff975f3c4fb00cb4b02c0f40f10948602ab4ce39f23` |
| `microlzma-edge.erofs` | `27a19758141d64430d7536774fadf34ba7f8befa0bc36442b789e494b4b37d4c` |
| `zstd-level1.erofs` | `34f51d5b1273cc3fc9efa458c200cf134dc7dca2c583e577a8d06ad563cfbff6` |
| `zstd-level15.erofs` | `8ca4ba52561cc8903f85048edfd958ef692583479748dcb8aa57e2c7aade1034` |
| `zstd-level22.erofs` | `99f79807ffd9f1216dbd624a2d8ecfcf17c9b42689fef5b5a69d0250ed84e5` |
| `zstd-partial-ref.erofs` | `cdef7c2a397722723dcc762044612a179358f9dbe4dcd879ac08e462cf9628d1` |
| `zstd-partial-ref-corrupt.erofs` | `ed6f4b23f6b6426e6af9180e892f03097d16a6b969edd68ad42d9a1ff8d2325e` |
### Source hashes
| Source | Size | SHA256 |
|---|---:|---|
| `large/large.bin` | 268435456 | `78f61f8eb37b5aeee026b579d244d766935d5655c0ac5383a96f631048611cfd` |
| `levels/level.dat` | 8388608 | `ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461` |
| `lz4/compressed.bin` | 8388608 | `3ff012b76087c4da65ce0b69813a76f47ea367e95782edf8b8cd6cd3ec4d1880` |
| `lzma-large/large.bin` | 104857601 | `de846af6c7e47fac72c0576c449fd0fc1fe9587286c9158ae08c81804c77f66b` |
| `microlzma/one-byte.bin` | 1 | `333e0a1e27815d0ceee55c473fe3dc93d56c63e3bee2b3b4aee8eed6d70191a3` |
| `microlzma/block-4k.bin` | 4096 | `42b2e4ac3afeb366a6407573d5a91a961775e0fe374b0941f19681c4e3f9ea96` |
| `microlzma/boundary-16k.bin` | 16384 | `1b5a7306ca67b75c18228d08f281eb7474b31a1a1ccbe4d680b7942fc81265b9` |
| `partial/a.dat` | 1048576 | `c7ac0fce9c56d732e4b8328c1ca48ae33ee4d449166bd25d2c88322c1a852d9f` |
| `partial/b.dat` | 700000 | `926a9bb05b20cb3da745eaff6e7fec38fb43d7fa155276474367550b67b97589` |
| `partial/control.bin` | 32768 | `7544a26039c5257ee07c1280468edb1f61f0b0edaa25273b15e0252a9cc5c90a` |
| `partial-deflate/a.dat` | 1048576 | `c7ac0fce9c56d732e4b8328c1ca48ae33ee4d449166bd25d2c88322c1a852d9f` |
| `partial-deflate/b.dat` | 100000 | `1fbe057da0cb994652ef664a4aa00628e948578055665160fc63e7079469da21` |
| `partial-deflate/control.bin` | 32768 | `7544a26039c5257ee07c1280468edb1f61f0b0edaa25273b15e0252a9cc5c90a` |
| `shape/shape.dat` | 1048576 | `5be510e6b43f1ed0cda4261288ea70df11607b6ced29bc90d32ed2849ecc5e35` |
| `ztail/inline.dat` | 131071 | `e54a4bac3b6b1c01ee846dbccdf234ddb30ed06dc13f1af1df0fbbc397431ce5` |
| `ztail/exact-pcluster.dat` | 4096 | `14587a494e72ecdc7d26b4c0b947d91d44eba4365b252d1a4ec10ba979978c52` |
| `ztail/one-byte-tail.dat` | 4097 | `718f4c27896edf8925300c50572df92b9127e56b2b84a3a86ced4d67c3be9596` |
| `ztail/max-tail.dat` | 8191 | `5e1e898dba6a6bbcc8beb0b0b097bc04ecaaf4ad505b77bfb8646a3011628df6` |
| `ztail/zero-tail.dat` | 0 | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |
## Key layout fields
| Fixture | Structured proof |
|---|---|
| LZ4 compact 4K | layout 3, map offset 2183200, `h_advise=1` |
| LZ4 full 4K | layout 1, map offset 2183200, `h_advise=0` |
| LZ4 compact 64K | layout 3, `h_advise=7`, max physical pcluster 65536 |
| LZ4 compact 256K | layout 3, `h_advise=7`, max physical pcluster 262144 |
| ztailpacking | layout 3, `h_advise=9`, `h_idata_size=536`, physical bytes 1512-2048 |
| HEAD2 | incompat `2 -> 10`, map advise `2 -> 6`, first record `1 -> 3`, pblk 1 |
| interlaced | layout 3, `h_advise=49`, 52 compressed, 1 plain, first transition 999593 |
| explicit attempt | layout 1, map offset 1312, `h_advise=2`, explicit bit absent |
The first HEAD2 logical pcluster ends at 326114; the boundary read started at
326000. The interlaced boundary read started at 999000 and crossed the proven
transition at 999593.
Partial-reference fields after reopen:
| Algorithm | a/b size | a/b pblk | b partial advise | b compressed blocks |
|---|---|---|---:|---|
| DEFLATE | 1048576 / 100000 | 1 / 1 | 32769 | 2 -> 17 |
| ZSTD | 1048576 / 700000 | 1 / 1 | 32769 | 1 -> 1 |
| MicroLZMA | 1048576 / 700000 | 1 / 1 | 32769 | 1 -> 1 |
Targeted corruption fields:
| Target | Algorithm | Pcluster | Patch |
|---|---:|---|---|
| DEFLATE `/a.dat` | 2 | 4096 + 69632 | offset 8055, length 64 |
| ZSTD `/a.dat` | 3 | 4096 + 4096 | offset 8073, length 64 |
| MicroLZMA `/a.dat` | 1 | 4096 + 4096 | offset 7957, length 64 |
| HEAD2 `/shape.dat` | 0 | 4096 + 65536 | offset 4096, length 64 |
Every corrupted image retained a valid superblock checksum. Each read was
bounded by `timeout 20`, returned errno 5, and left `control.bin` readable
where the fixture included one.
## Build and TC145
The invalid build value failed with rc 1 and the exact message
`WITH_ZSTDIO must be 0 or 1`.
| Build | Size | SHA256 | Undefined-symbol gate | Guest |
|---|---:|---|---|---|
| `WITH_ZSTDIO=0` | 73896 | `e103ebbaf7faf8041448d93022be44e7c93822a45a36a69bca2b7a2107461d09` | no `ZSTD_*`, no `bcmp` | file ID 5 load/unload PASS |
| `WITH_ZSTDIO=1` | 78728 | `56ffcce006eed1e555121d9b8f523578272ac3d82c27a19b635d0a0a664c8370` | five formal API names, no `bcmp` | file ID 5 load/unload PASS |
The enabled undefined ZSTD set was exactly
`ZSTD_DCtx_setParameter`, `ZSTD_createDCtx_advanced`,
`ZSTD_decompressStream`, `ZSTD_freeDCtx`, and `ZSTD_isError`.
The disabled module read the LZ4 control hash
`3ff012b76087c4da65ce0b69813a76f47ea367e95782edf8b8cd6cd3ec4d1880`.
A direct ZSTD mount returned rc 1 with the required message; `truss` proved
`nmount` returned `ERR#45 EOPNOTSUPP`. The enabled module read ZSTD hash
`ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461`.
## Per-TC results
| TC | Manual evidence | Result |
|---|---|---|
| TC003 | compact LZ4 full hash/cmp; first 1024 bytes cmp | PASS |
| TC004 | LZMA level 6 full hash/cmp; 102400/10240 and EOF 10000 cmp | PASS |
| TC084 | full-index LZ4 full hash/cmp; 4000/8192 cross-extent cmp | PASS |
| TC085 | 256 MiB full hash/cmp; 50/200 MiB ranges and two 4K offsets cmp | PASS |
| TC086 | deterministic sequential 4K and 1 MiB block reads both full cmp | PASS |
| TC087 | offsets 0, 4096, 65536, 1048576, and 8384512 source cmp | PASS |
| TC088 | 4K/64K/256K images full hash and 409600/40960 source cmp | PASS |
| TC089 | 0/4096, 4096/4096, 2048/4096, and 0/16384 source cmp | PASS |
| TC090 | 0/65536, 4096/4096, 65504/64, and 0/262144 source cmp | PASS |
| TC091 | ztail full hash/cmp and final 4096 bytes cmp | PASS |
| TC092 | actual 4096, 4097, 8191, and 0-byte files full hash/cmp | PASS |
| TC102 | DEFLATE level 1 full hash/cmp and two fixed ranges | PASS |
| TC103 | DEFLATE level 6 full hash/cmp and two fixed ranges | PASS |
| TC104 | DEFLATE level 9 full hash/cmp and two fixed ranges | PASS |
| TC105 | ZSTD level 1 full hash/cmp and two fixed ranges | PASS |
| TC106 | ZSTD level 15 full hash/cmp and two fixed ranges | PASS |
| TC107 | ZSTD level 22 full hash/cmp and two fixed ranges | PASS |
| TC108 | 104857601-byte LZMA level 6 full hash/cmp; 0/50/99 MiB ranges; active 0 | PASS |
| TC109 | actual 1B/4K/16K files match; 16K inode proven compressed LZMA | PASS |
| TC110 | valid LZMA control; targeted read EIO; same-image control; active 0 | PASS |
| TC143 | DEFLATE/ZSTD a+b full, partial ranges, targeted EIO, controls, active 0 | PASS |
| TC144 | MicroLZMA a+b full, partial ranges, targeted EIO, control, active 0 | PASS |
| TC145 | invalid gate, both builds/symbol sets/KLDs, disabled rejection, enabled read | PASS |
| TC146 | HEAD2 PASS; interlaced PASS; explicit mapped payload SHELVED | PARTIAL |
TC108 used a 1800-second hard timeout for each complete read under QEMU TCG.
It completed without timeout; the long duration was diagnostic only.
## Explicit extent disposition
erofs-utils 1.8.6 source had zero matches for
`Z_EROFS_ADVISE_EXTENTS`, `z_erofs_extent_recsize`, and the on-disk
`struct z_erofs_extent`. The fresh `--max-extent-bytes=65536` attempt remained
ordinary full-index metadata. The structured transformer refused an
unverifiable metadata/payload relocation. Existing `review_fixtures.py` only
builds a negative `pa + plen` overflow extent table, not a valid mapped
payload.
FreeBSD and Linux ABI/control flow for 4/8/16/32-byte records was reviewed.
That is static support, not dynamic positive coverage. Full details and
acceptance criteria are in `issues/extent-metadata-fixture-unavailable.md`.
## Stability and cleanup
- Every mount was read-only and every md provider was detached.
- All corruption probes returned errno 5 before their 20-second timeout.
- No dmesg delta, panic, trap, or OOM was observed.
- EROFS active allocations returned to zero after each corruption/large test.
- Final guest verification: 25 image hashes and 19 source hashes matched.
- Final guest: zero mounts, zero md providers, no EROFS allocator row, no EROFS
KLD, and responsive FreeBSD 15.0-RELEASE-p8.
- The dedicated VM was powered off and port 9225 was released after evidence
collection.
@@ -0,0 +1,139 @@
# repo22 G6 Chunk and Multi-Device Manual Regression
Date: 2026-08-09
Executor: G6 manual agent
Source baseline: `6b33b4afb490be7d6fec70e499469c306a58435d`
Scope: `TC006`, `TC093`-`TC101`, `TC118` (11 exact IDs)
## Result
PASS. All 11 requested test cases passed. There are no KFAIL, SHELVED, or
ENV results, no duplicate IDs, and no omitted IDs.
| Status | Count |
| --- | ---: |
| PASS | 11 |
| KFAIL | 0 |
| SHELVED | 0 |
| ENV | 0 |
| Duplicate | 0 |
| Omitted | 0 |
No kernel source was changed. `git diff -- src` was empty after testing.
## Environment and build identity
- Worktree: dedicated sparse worktree and branch
`manual-g6-20260809T114414Z`, based directly on `xdm/main` at the source
commit above.
- VM: dedicated qcow2 overlay, QEMU TCG, 6144 MB, 4 vCPUs, SSH forward
`127.0.0.1:9226` only.
- Guest: FreeBSD `15.0-RELEASE-p8` amd64,
`releng/15.0-n281036-53054229dcb3`, OSREL `1500068`.
- Guest kernel SHA256:
`b9abf7b58f9dd4d87f14d2fbc306cf255e6eadc47b8ae885a230f65e28be4562`.
- Tracked build sys source: `REVISION=15.0`, `BRANCH=RELEASE-p9`, archived
from the same repository commit; archive SHA256
`66c457159b758ab1a5d298292b81bd29ab439ca4ab396091602e030da4721cb9`.
- Build configuration: native guest build,
`WITH_ZSTDIO=1 FREEBSD_SRC=/root/freebsd-src`.
- KLD SHA256:
`d5ff5ceef1ad76eb5b608d362b04f3bc40379e58a73d91ed3ae747c035d7b8af`.
- repo22 source archive SHA256:
`7562e230d165f0c0caa050684a9c1e2bd1bbb178e54a1d5273428435ae512455`.
The latest tracked sys source is p9 while the clean guest kernel/userland is
p8. Both use OSREL 1500068; the module built and loaded successfully. This
source/guest patch-level distinction is recorded rather than hidden.
## Fresh fixture qualification
`tests/g6_multidev_fixtures.py` generated every source, primary image, blob,
flatdev, and negative image from zero with erofs-utils 1.8.6. No old test image
or report was an input.
- Generator SHA256:
`18638aee056e05099b39477fa6ad4d678ab0a74a027cfb45146ae225ff35d950`.
- Final manifest SHA256:
`b636a52c7b3ec7f2343f07e2d389783c8383ec07d9d9468f7552e817891c3a81`.
- Final `SHA256SUMS` SHA256:
`ed81d63469b40f1aba691e54a08d39737c71e66d2ff4fef963a0101fa2ad9854`.
- Final fixture archive SHA256:
`28be1a5cdcebb163710433c9e7c30162c1170312c34d0b286f4143480881d62f`.
- Generated set: 62 artifacts, 17 parsed fixtures, 7 table/index negative
images, and 10 explicit mutation assertions.
- Reproducibility: two clean output directories had byte-identical
`manifest.json` and `SHA256SUMS`.
- Host fsck extraction qualified the mkfs split image, single-index image,
explicit two/three-slot images, table-at-zero, `uniaddr=0`, packed fragment,
and the original two-block LZ4 pcluster.
- Guest final `sha256 -c SHA256SUMS` returned 0.
During qualification, an initial maximum-48-bit negative used all ones. Field
review identified that value as the legal chunk hole sentinel. The generator
was corrected to the largest non-NULL address `0xfffffffffffe`, regenerated
twice, and reverified. Only the intended primary artifact changed; its final
SHA256 is
`d6f113cb5d2cb7da7f892632930eedf1c84fb494f5e1e4f0d2a852496ee92951`.
The corrected kernel read returned `EINTEGRITY`.
## Exact results
| Test ID | Status | Manual evidence |
| --- | --- | --- |
| TC006 | PASS | Direct mkfs split primary/blob; full and cross-32K range `cmp`; external `MDIOCDETACH` returned `ERR#16 EBUSY`. |
| TC093 | PASS | 8-byte device-ID-0 indexes with no table; full and cross-32K range `cmp` on the primary provider. |
| TC094 | PASS | Reversed slot option order; 1/2/1 full and per-index ranges; `uniaddr=0` with nonzero ID; swapped media `ERR#6 ENXIO`; real external 8192-byte LZ4 pcluster produced the complete 131072-byte source SHA256. |
| TC095 | PASS | First/middle/last/cross-index reads; zero-high 48-bit control and nonzero-high unified read; mapped ID 3 returned `read ERR#19 ENODEV`, unaffected file remained readable. |
| TC096 | PASS | Parsed normal, table-at-zero, `uniaddr=0`, and high 48-bit tables; `df -k` reported 916 KiB = 229 blocks x 4096; table outside primary returned `ERR#97 EINTEGRITY`. |
| TC097 | PASS | Six structural variants each returned `nmount ERR#97 EINTEGRITY`; after every failure md/KLD/consumer state was zero; slot-2 `ENOENT` after slot 1 open released slot 1 immediately. |
| TC098 | PASS | Packed NID 42 whole-file fragment; size 100000; full SHA/`cmp` and 8192-byte offset range passed. |
| TC099 | PASS | Direct primary+one-blob layout; `df -k` 900 KiB = 225 blocks x 4096; full/range compare; omitted blob option returned `ERR#6 ENXIO`. |
| TC100 | PASS | Four providers with option order 3/1/2; full read, 262144-byte range across both transitions, and three concurrent reads passed; missing/duplicate/short returned `ERR#6/22/6`. |
| TC101 | PASS | Explicit unified, nonzero-ID flatdev, device-ID-0 flatdev, and nonzero-high 48-bit reads matched source; gap, explicit/unified cross-slot chunk, explicit/flatdev two-block pcluster, and largest non-NULL 48-bit address all returned `read ERR#97 EINTEGRITY`. |
| TC118 | PASS | No options/one option/short returned `ERR#6`; duplicate/primary-as-slot returned `ERR#22`; normal detach `ERR#16`; forced orphan cold slot-2 read `ERR#6`; second mount `ERR#16`; primary and external missing paths preserved `ERR#2`. |
## External-data proof
The TC094 compressed primary is 8192 bytes and contains only metadata plus its
device table. The original pcluster blocks are absent. Its HEAD pblk is the
slot-1 unified address, and the 8192 compressed bytes exist only in the
external provider. FreeBSD returned the complete source SHA256
`8dd7830946e7154d9feaaa87df6d1d39e47d51da22a690c5469dc4f2955ea0f1`;
therefore this was an external compressed read, not table-only parsing.
TC101 also mounted generated combined flatdev providers with no `device.N`
options. Both nonzero device-ID mapping and device-ID-0 unified mapping
returned the same source SHA256 values as explicit providers. erofs-utils
1.8.6 does not qualify these two forms, so the FreeBSD full/range comparisons
are the runtime qualification.
## Errno summary
- `EBUSY` = 16: live md detach and second concurrent mount.
- `ENXIO` = 6: missing/short media and forced-orphan cold I/O.
- `ENODEV` = 19: mapped but undeclared chunk device ID 3.
- `EINVAL` = 22: duplicate provider and primary reused as an external slot.
- `ENOENT` = 2: nonexistent primary or external pathname.
- `EINTEGRITY` = 97: malformed tables and complete-extent range failures.
## Lifecycle and platform behavior
Every TC ended with zero EROFS mounts, zero md units, no loaded EROFS KLD, and
no matching GEOM consumer. TC097 repeated this after each individual malformed
table. TC118 forced md92 orphaning, observed cold `read ERR#6 ENXIO`, then
unmounted and released the remaining providers without panic or hang. The
final dmesg anomaly scan for panic, fatal trap, assertion, watchdog, or EROFS
errors was empty.
Linux device tables allow table offset zero and skip `uniaddr=0` during
device-ID-0 lookup while retaining explicit nonzero-ID selection. FreeBSD
matches those on-disk semantics but receives external providers through
explicit `device.<slot>` mount options and owns read-only GEOM consumers.
Consequently FreeBSD-specific `EBUSY` detach and forced-orphan `ENXIO` behavior
was tested directly rather than inferred from Linux loop devices.
## Issues
None. No kernel failure remained after correcting the fixture's reserved hole
sentinel, so no issue file and no source change were created.
@@ -0,0 +1,269 @@
# repo22 G8 Documentation, NFS, and Integrity Manual Regression
Date: 2026-08-09 13:07-13:56 UTC
Executor: G8 manual agent
Source baseline: `f11fff5b8e8050e1017ed86f0bcf71042b2b45aa`
Exact scope: `TC111`, `TC131`-`TC133`, `TC154`-`TC156` (7 IDs)
## Result
All seven requested test cases passed. There are no KFAIL, SHELVED, or ENV
results, no duplicate IDs, and no omitted IDs.
| Status | Count |
| --- | ---: |
| PASS | 7 |
| KFAIL | 0 |
| SHELVED | 0 |
| ENV | 0 |
| Duplicate | 0 |
| Omitted | 0 |
| Test ID | Status | Manual evidence |
| --- | --- | --- |
| TC111 | PASS | Audited current source and documentation against real generic `/sbin/mount`; corrected helper and writable-request claims. |
| TC131 | PASS | Real mountd/nfsd NFSv3 export, four vnode types, read-only behavior, 16-byte handle resolution, and RPC statistics. |
| TC132 | PASS | Handle ABI/classes, stable remount, replacement `ESTALE`, metabox and external-provider regressions, and nfsd restart. |
| TC133 | PASS | Exact 12,050-name READDIRPLUS lists, five cold remounts, 20 individually waited workers, and zero RPC errors. |
| TC154 | PASS | Same-superblock/UUID/NID replacement changed per-inode generation; unchanged remount stayed stable. |
| TC155 | PASS | Explicit-extent physical-address wrap returned `EINTEGRITY` on both direct reads and in `truss`. |
| TC156 | PASS | Compact/extended epoch, `time_t`, and nanosecond boundary corruptions returned `EINTEGRITY`. |
No kernel source changed. No kernel failure remained, so no issue file was
created.
## Isolated Environment
- Worktree: `/work/build/repo22-manual-g8-20260809T130723Z`, branch
`manual-g8-20260809T130723Z`, created directly from the source baseline.
- VM: independent qcow2 overlay backed by the clean FreeBSD development base;
QEMU TCG, 6144 MB, 4 vCPUs, SSH only on `127.0.0.1:9228`.
- Guest: FreeBSD `15.0-RELEASE-p8` amd64,
`releng/15.0-n281036-53054229dcb3`, OSREL `1500068`.
- Guest kernel SHA256:
`b9abf7b58f9dd4d87f14d2fbc306cf255e6eadc47b8ae885a230f65e28be4562`.
- Host erofs-utils: 1.8.6.
- No CI, runner, or test wrapper was used. Markdown procedures were issued
manually through SSH, with direct command status kept separate from tracing
tool status.
## Exact Build
The repo22 source archive came from the exact baseline commit. The matching
tracked FreeBSD build input was `dev-freebsd-releng/sys`, identified by
`REVISION="15.0"` and `BRANCH="RELEASE-p9"`. The build ran natively in the
guest as:
```sh
FREEBSD_SRC=/root/freebsd-src WITH_ZSTDIO=1 ./build.sh
```
The compile command contained `-DZSTDIO` and kernel `-Werror`. The module had
the five expected `ZSTD_*` kernel references and no unresolved `bcmp`.
| Object | SHA256 |
| --- | --- |
| repo22 source archive | `2f76ef7df9117c6e7d62ad41809638ad306e676191346083bf95fe551f7df515` |
| FreeBSD 15 `sys` archive | `cd806ac4d6aee5d7ceb6a2f9603020b48b5012835bc8c7f2ec0985572fcd0262` |
| `erofs.ko` | `e8cd4839328de089355505efb1629c835d2f93faad2e14f63c6e6cb844ce9589` |
| `nfs_fh_tool` | `db704f67d718d04b84ed32b5cdbecb93a99c5fe3b4ab238d603a10e005d790e3` |
The guest kernel is p8 and the tracked source is p9, but both use the FreeBSD
15.0 OSREL ABI. The exact module loaded and unloaded successfully.
## Fixture Qualification
`tests/review_fixtures.py` generated two independent output directories. Their
manifests and checksum inventories were byte-identical, and all nine image
checksums self-verified. Its structured assertions recorded:
- target NID 452 at inode offset 14464 after checksum block 4096;
- identical complete superblock block and UUID for NFS images A and B;
- generations `3895653226 -> 548470773` from the changed raw inode;
- explicit extent base `0xfffffffffffff000`, `plen0=8192`, `plen1=4096`, and
wrap before logical cluster 4096;
- compact epoch wrap/range, compact nanoseconds 1000000000, extended
nanoseconds 1000000000, and extended seconds `INT64_MAX+1`.
Review generator SHA256 was
`b92fd6b6122d55882998138efc0109e24b236f9e877405dbff7914284a18c124`;
manifest SHA256 was
`be0836043d172b34eb625e5259f7e6cb9a8992ff74e7d1a5cdc10d69282e2335`.
The NFS stress image was generated from a fresh deterministic tree containing
12,050 `bigdir` files, 256 concurrent-read files, four basic vnode types, and a
22,020,096-byte throughput file. Image A SHA256 was
`88721877dc3762c8eafdf3bcca2653787cdcf41983978ab159705761abefac37`;
image B was
`964b3d75579740c9b08bc9cae8329e3d641d3c505e9391de3ab9f5d24fbecd17`.
The expected sorted-name list SHA256 was
`4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71`.
Fresh structured metabox and multidevice helpers also self-verified the two
TC132 regression inputs. The metabox image SHA256 was
`dd7b04097d1bfe283b65c95d759acd2176beb7cb0f1fe9d5ddbc0694b5acba9d`;
the multidevice primary/slot hashes were `918aa3a64e8861011914d967912909ce9de98418cff711cfbf8f91c0c7717f72`,
`4e31d95a12405f2f1396fb926a6854c591fc335eade8944f73fdc7fe6758aee6`,
and `355a74880bd3648dea22ad18e19f809cee12303ceed2a99f53094c2b66563a11`.
## TC111
`/sbin/mount_erofs` was absent and `command -v mount_erofs` returned 127.
Generic `/sbin/mount -t erofs` mounted the image successfully without `-o ro`.
The following real behavior was recorded:
- the default mount was read-only and `touch` failed with `EROFS`;
- `-o rw` returned 0 but the resulting mount was still read-only;
- `-o ro,noexec,nosuid` returned 0 and all three flags appeared;
- non-export `mount -u` returned `EOPNOTSUPP`;
- an unknown filesystem option returned direct rc 1, with
`nmount(..., MNT_RDONLY) ERR#22`, and created no mount.
Current `src`, `README.md`, `docs/features.md`, `docs/architecture.md`,
`docs/erofs.5`, and current progress material were checked. `README.md`,
`docs/erofs.5`, and the test procedure now describe the generic frontend and
the forced-read-only `rw` behavior accurately. The man page's split-device
example was also exercised by the TC132 multidevice mount. SEE ALSO entries
absent from the qualified guest were removed; `mandoc -Tlint -Werror` and
ASCII rendering both returned 0 in that guest.
## TC131
The direct EROFS line lacked `NFS exported` before mountd. After installing the
loopback export and reloading mountd, `showmount -e` listed the exact path and
the direct mount gained `NFS exported`. `rpcinfo` showed NFSv3 and mountd over
TCP and UDP.
The NFSv3 TCP client negotiated `rdirplus`. Regular file content, directory,
symlink target, FIFO type, and inode number matched the direct EROFS mount.
`touch` failed with `Read-only file system`. Local `fhstat` and `fhopen`
resolved the regular handle with `len=16`, `pad=0`, NID 116, and generation
2068234290.
After basic operations client READDIRPLUS was 2. Server Write and Create were
both 0. TimedOut, Invalid, X Replies, and Retries were all 0.
## TC132
Regular, directory, symlink, and FIFO handles all had `len=16`, `pad=0`, full
64-bit NIDs, and nonzero generations matching `st_gen`. Bad length/pad returned
`EINVAL`; bad generation/NID returned `ESTALE` through both `fhstat` and
`fhopen`.
The unchanged image remounted on md80 preserved the complete handle SHA256
`9e1df1952b43d383fea295301525ede7a69b7d57e5f60a75a6d7f1531e13df0a`,
and the old handle still read the file. Replacement B retained fsid and NID
116 but changed generation `2068234290 -> 857800581`; both old-handle paths
returned `ESTALE` while B resolved.
The metabox handle preserved NID `0x8000000000000010`; invalid metabox NID and
generation returned `ESTALE`. After forced orphaning of external slot 2, the
valid file handle still resolved metadata and the read returned `ENXIO(6)`,
not `ESTALE`.
An NFS client descriptor remained open across `service nfsd onerestart 3<&-`.
The old descriptor and a new path read both returned SHA256
`3e2d0b4971e1f4bf00c3562a8bf9c5d85378c9de054f3da89541f49f2b449a7b`;
the export and all RPC registrations remained live.
## TC133
Requested readdir sizes 512, 1024, and 4096 were all clamped by the FreeBSD
client to 8192; the default remained 65536. This is the recorded client floor.
Every one of the four mounts returned exactly 12,050 unique expected names.
Every sorted list, plus all five cold-remount lists, had SHA256
`4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71`.
The `ls -a1` list contained 12,052 entries, with `.` and `..` exactly once.
All 12 traversal workers were waited by PID and returned 0. All four parallel
cat workers and four parallel stat workers were also waited by PID and returned
0. The informational 22,020,096-byte read completed in 14.51 seconds.
Final client/server READDIRPLUS was 3420. TimedOut, Invalid, X Replies, and
Retries remained 0. Server Write, WriteRPC, Create, and Commit remained 0.
There were no new stale-handle, timeout, retry, panic, trap, assertion,
watchdog, or EROFS error lines.
## TC154
Image A produced fsid `00000034:000000e0`, `len=16`, `pad=0`, NID 452, and
generation 3895653226. `nfs_fh_tool`, `stat st_gen`, and the fixture manifest
agreed. The unchanged remount preserved complete handle SHA256
`79f2f4b8e354af0ae369849d7f66a739162ffadc4a5689c09643c9949b548de1`
and the old handle remained readable.
Image B retained the complete superblock block, UUID, fsid, NID, and content,
but generation became 548470773. Both old-handle paths returned `ESTALE`; B's
handle resolved. The malformed same-NID replacement returned positive errno 45
through both paths.
## TC155
The self-checked fixture SHA256 was
`1e49ecf1917265fde9b606e80f7dbae8b14aca8c6f149128b31d6c825da546dd`.
The image mounted and target `stat` reported inode 40, size 1,048,576. Two
direct one-byte reads at logical offset 4096 returned rc 1 and `Integrity check
failed`; the traced read was `ERR#97`. The repeat exercised the cached vnode.
A DTrace `io:::start` positive control observed one raw
`md90 offset=4096 bytes=4096` event. A separate trace around the failing EROFS
read observed zero md90 events, proving the wrapped address did not reach the
provider. No panic or resource remained.
## TC156
Both compact root corruptions failed `nmount` with `ERR#97`. The compact range
image mounted with root mtime `INT64_MAX`; target stat returned `ERR#97` for
`INT64_MAX+1`. Both extended images mounted, and target stat returned `ERR#97`
for nanoseconds 1000000000 and seconds `INT64_MAX+1`. Every failure was run
directly and again under `truss`; no panic or resource remained.
## Cleanup and Restoration
Before NFS setup, `/etc/exports` did not exist; rpcbind, mountd, and nfsd were
stopped; and `rpcbind_enable`, `mountd_enable`, and `nfs_server_enable` were all
`NO`. Cleanup unmounted every loopback client while nfsd was alive, cleared and
reloaded exports, stopped nfsd/mountd/rpcbind, then removed the direct EROFS
mount, md42, and the module.
Final state exactly matched the baseline: `/etc/exports` absent, all three
services stopped with rc 1, all three rc settings `NO`, zero NFS/EROFS mounts,
zero md providers, no EROFS KLD, no service process, and `rpcinfo` refused the
connection because rpcbind was stopped. The complete NFS-period dmesg delta had
zero anomaly lines.
The retained guest evidence archive is outside the repository at
`/work/build/repo22-manual-g8-20260809T130723Z-vm/evidence/`:
| Artifact | SHA256 |
| --- | --- |
| `final-guest-evidence.txt` | `eb67210818e663c15e4670ada9ba63b82b44e9ed3a9486c575df3a71ed3f0522` |
| `TC155-dtrace-assert.txt` | `f8b52985a74c8c777b3b830c39749be65c613165ffbcde6d0018dc471018c433` |
| `final-post-dtrace-cleanup.txt` | `90f04d8f5a4ae35c87cc3adb1aeb939c7cc5534233ed63e9ff4501c83fd857d2` |
| `repo22-g8-evidence.tar.gz` | `9a378fd619f9bca21f40689d746308933aa22e440f7201104732f91634c21290` |
## Non-Qualified Attempts
One TC131 setup command stopped at the expected nonzero service-status probe
because that probe was mistakenly under `set -e`; it occurred before module,
exports, or service changes. One TC133 attempt stopped after the four exact
lists because a shell-quoted dot assertion matched zero; the three added
clients were unmounted and the complete procedure was rerun from the start.
Tracing wrappers returned 0 even when their child failed, so all error claims
use separate direct statuses plus syscall errno. None of these discarded
attempts is counted as PASS evidence.
Two initial DTrace specifications used field names from other provider ABIs
and failed at compile time before any probe I/O. The qualified specification
used FreeBSD `struct devstat` and `struct bio` fields. Its first assertion used
a line anchor even though the format emitted a literal `\\n`; substring counts
then proved one positive md90 event and zero failing-read md90 events. DTrace's
automatically loaded module set was unloaded as a unit and the original KLD
set was reverified.
## Issues
None.
@@ -0,0 +1,183 @@
# repo22 G7 boundary and stress full manual regression report
## Scope and result
- Exact scope: TC120-TC130 inclusive.
- Count check: 11 requested, 11 executed, 0 duplicate, 0 omitted.
- Result: **PASS 11, KFAIL 0, SHELVED 0, ENV 0**.
- No repo22 kernel source was changed.
- No kernel failure occurred, so no G7 issue file was opened.
- No CI, runner, or test wrapper was used. Each test's Markdown commands were
issued manually in the dedicated FreeBSD guest.
## Baseline and source identity
- Requested initial `xdm/main`: `c566d8ac6bf8e801082bbccf108451f6ee46ad40`.
- G6 pushed first; G7 fetched and rebased onto
`f11fff5b8e8050e1017ed86f0bcf71042b2b45aa` before editing.
- Exact build commit:
`e44e24c2d7e4955498fb42b06bcf376d6c452190`.
- The equivalent G7 test/source commit after the final remote rebase is
`896683f14af2ba36705498f85d0d8af02b513d31`; its repo22 source tree is
byte-identical to the build input.
- Exact `repo-community/repo22/src` tree:
`34a56a583b3b43e7e103b26a30dea1d400c2bc6c`.
- G8 later advanced `xdm/main` to `6f336d0a7`; its repo22 source tree was the
same `34a56a58...`. G7 rebased before final push without force.
- Build configuration: `WITH_ZSTDIO=1`.
- Tracked FreeBSD sys source: `REVISION="15.0"`, `BRANCH="RELEASE-p9"`.
- repo22 source archive SHA256:
`e715e9d323be1448e0395add99cf017dea3c1bbb6579915cf3d07f7265ca2891`.
- FreeBSD sys archive SHA256:
`71f535ab5a9ef686c6adbe123a1d1e65fd9bfa9cd19724f9fcef222a8efbe9d8`.
## Isolated environment
- Worktree: `/work/build/repo22-manual-g7-20260809T125507Z`.
- Branch: `manual-g7-20260809T125507Z`.
- Artifact root: `/work/build/repo22-g7-20260809T125507Z`.
- Dedicated qcow2 overlay backed by `/work/build/vm-freebsd-dev-base.qcow2`.
- Dedicated SSH forward: `127.0.0.1:9227`.
- QEMU: TCG multi-thread, `qemu64`, 6144 MiB RAM, 4 vCPUs, virtio disk/network.
- Guest: FreeBSD `15.0-RELEASE-p8`, amd64,
`releng/15.0-n281036-53054229dcb3`, OSREL `1500068`.
- Guest kernel SHA256:
`b9abf7b58f9dd4d87f14d2fbc306cf255e6eadc47b8ae885a230f65e28be4562`.
- Host erofs-utils: 1.8.6.
- RACCT/RCTL was initially present but disabled. The dedicated overlay set
loader tunable `kern.racct.enable="1"`, rebooted, then reported enabled with
an empty initial rule set.
## Exact KLD and probe
The KLD built successfully from the archived commit with the tracked p9 sys
tree and loaded by full pathname. `kldstat -v` assigned file ID 5 and showed
module ID 507 for `erofs`.
| Artifact | SHA256 |
|---|---|
| `/root/repo22-g7/erofs.ko` | `06dfa530efb2d494950672d60453d86a6e70f06ca2ce7b4eff3396ca8018c76d` |
| `/root/repo22-g7/g7_probe` | `a0bca6ea596cec6063bc058aebee12929a1814e6d343283ffbe051551bf75c57` |
The KLD was an amd64 FreeBSD relocatable object with build ID
`d4fff529ae8e4c5395e1e867fd4ea5d57a9570b6`. The expected formal FreeBSD ZSTD
undefined symbols were present; no build or load error occurred.
## Reproducible fixture
Two fresh generations produced byte-identical source inventories, source
checksum lists, and image checksum lists. Each generation independently ran
the helper verification and erofs fsck checks.
```text
source file count = 24023
image count = 2
SOURCE-INVENTORY.tsv SHA256 = b9be1745900a8cf6755068b6a78b93bc294cf8857539159b7771e5170bc1b9ed
SOURCE-SHA256SUMS SHA256 = 445929465a3031e84de9887fc5b02d72cb797df7ca512ea1cc874d11f77747d3
SHA256SUMS SHA256 = f9cf45c1e3fe814290d9cd0b96d066509309cc3f764c545d5e7d7ddae4a9eac3
fixture-manifest.json SHA256 = 0ba86269962b00329b99177494cfb099c0f58fe5de77d2dc1f12da684dedf09d
guest image/source verification = 2 images and 24023 sources, all OK
```
| Image | Bytes | SHA256 |
|---|---:|---|
| `boundaries.erofs` | 37752832 | `ff7e804dc309039fa5dfa9b6e75416d16224137c8f6d6d96bf04cfa0ba8a1a8c` |
| `workloads.erofs` | 155111424 | `5f21a045114c5956b80752736c9833fe6a975a296cd28324734c9b9e423f3984` |
The sparse source was exactly 4294971393 bytes. It occupied 48 Linux
512-byte blocks before transfer and 512 FreeBSD blocks with a reported
32768-byte file-system block size after sparse-aware extraction. All five
markers and six boundary ranges passed the helper's source self-check.
## Per-TC results
| TC | Manual evidence | Result |
|---|---|---|
| TC120 | Exact size 0; source/mount empty SHA256 `e3b0c442...`; read returned 0; SET/END seeks returned 0 | PASS |
| TC121 | Exact 4294971393-byte size; six source-compared ranges across block, hole, 2 GiB, 4 GiB, and EOF boundaries; EOF read 0 | PASS |
| TC122 | Exactly 128 levels; deepest source/mount hash `ef151b18...`; full cmp and exact `pwd -P` | PASS |
| TC123 | Exact 255-byte name; one exact readdir row; size 4096; source/mount hash `0724316b...` | PASS |
| TC124 | Exactly 12000 one-KiB files and exact names; complete source/target hash-list digest `be93ef11...` | PASS |
| TC125 | Exactly 12000 direct entries and exact names/hashes; five boundary lookups matched; readdir timing recorded | PASS |
| TC126 | Exactly 16 saved PIDs, 16 waits, all rc 0; every expected/actual full-file hash matched; no PID remained | PASS |
| TC127 | Three RCTL-capped allocators reached ENOMEM, released all touched bytes, recovered allocation, and wait=0; pressure read hash/cmp matched | PASS |
| TC128 | 268435456-byte source/mount hash and full cmp matched; three complete sequential reads rc 0; metrics recording-only | PASS |
| TC129 | Three deterministic 16384-operation runs, all source-compared, zero mismatch, identical digest; metrics recording-only | PASS |
| TC130 | Seven saved PIDs and seven wait=0 results; sequential, hash, two random, 16-file, names, and stat workloads all matched; full cleanup | PASS |
TC121 range digests were `7775e9e82783e06f`, `c86358c1fb3fabf7`,
`b43a063055adc383`, `a1bd19f7b12c60b2`, `5a378d4efc494af4`, and
`1873eaea77354aa6`, in manifest order.
TC124's complete 12000-row source and mounted hash-list files both had SHA256
`be93ef11d2e4cd0cd42ef057397b89978b624e42674f9181e5dd151b30636171`.
TC125's complete 12000-row files both had SHA256
`8cde4ea887f9b21b265ac199bcbb447a656aa6d718a36424f4fa03a6c538ebe7`.
TC126 persisted each worker's PID, source hash, mounted hash, and wait status.
The PID table SHA256 was `fc67656d...`; the 16-row wait table SHA256 was
`888f026d...`. Every wait code was 0 and every hash pair was equal.
## Pressure evidence
TC127 used three synchronized `g7_probe` processes and valid FreeBSD RCTL
rules `process:<pid>:vmemoryuse:deny=640M`. The first process utilization
sample showed `memoryuse=532M` and `vmemoryuse=636M`.
| Worker | Allocated before ENOMEM | errno | Released | Recovery | wait rc |
|---|---:|---:|---:|---|---:|
| 1 | 553648128 | 12 | 553648128 | ok | 0 |
| 2 | 545259520 | 12 | 545259520 | ok | 0 |
| 3 | 553648128 | 12 | 553648128 | ok | 0 |
Free memory recorded by `vmstat -H` was about 4.01 GB before, 2.35 GB while
the allocations were held, and 3.89 GB after release. The 96 MiB source and
mounted file both hashed to
`e4baaad480721bfb69a0315f0092fcf1ad75426fda0b9242a750b5685ad75152`
and full `cmp` exited 0. RCTL rules and synchronization files were removed.
## Recording-only metrics
TC125's full 12000-entry sorted readdir scan under TCG recorded `real 11.48`,
`user 0.81`, and `sys 9.76` seconds. It would have failed the old invalid
10-second fixed threshold despite complete correctness, so timing was
correctly treated as recording-only.
TC128 sequential reads:
| Run | Bytes | Seconds | Bytes/s | wait rc |
|---|---:|---:|---:|---:|
| 1 | 268435456 | 6.764841 | 39680970 | 0 |
| 2 | 268435456 | 9.207558 | 29153816 | 0 |
| 3 | 268435456 | 10.603392 | 25315998 | 0 |
TC129 used seed `0x6a09e667f3bcc909`, 16384 operations, and 4096-byte
blocks. All runs produced digest `908bc2905f48695d` and zero mismatches.
| Run | Seconds | IOPS | Mean us |
|---|---:|---:|---:|
| 1 | 2.236047 | 7327.22 | 136.48 |
| 2 | 1.062087 | 15426.24 | 64.82 |
| 3 | 1.026698 | 15957.95 | 62.66 |
TC130 random workers ran concurrently with the other five workloads. Seed
`0xbb67ae8584caa73b` recorded 442.73 IOPS, 2258.72 us mean, digest
`78e9d2c7f86ed20e`; seed `0x3c6ef372fe94f82b` recorded 455.91 IOPS,
2193.42 us mean, digest `ee76a5d094a0c6f2`. Both had zero mismatches.
The exact names files shared SHA256 `8d1cf816...`; exact stat files shared
SHA256 `ca8937b1...`.
## Stability and cleanup
- Final dmesg delta was empty; its SHA256 was the empty-file hash. No panic,
trap, assertion, watchdog, OOM, or EROFS error appeared.
- Every test detached its exact md provider after unmounting.
- Final guest state had zero EROFS mounts, md providers, G7 child processes,
RCTL rules, synchronization files, and Python bytecode caches.
- KLD file ID 5 was unloaded with `kldunload -i 5`; no EROFS allocator row or
KLD remained.
- The guest remained responsive, powered off normally, QEMU PID 344535 exited,
port 9227 was released, and the dedicated overlay/log/PID files were deleted.
- Compressed evidence archive SHA256:
`f18246076e5ff93309187a1826afe9f2dfa4a0bcdf7a958419d2890d05727ccd`.
- No issues were created because all correctness gates passed.
@@ -0,0 +1,119 @@
# repo22 Final Review Independent Manual Regression
Execution window: 2026-08-09 18:04-18:29 UTC
Source baseline: `fcc85b93d5f8fd9671686bd68bf3b086c8bd25cf`
Scope: TC157-TC161, final dual-configuration build and mount smoke, TC111
coverage-matrix follow-up, and resource hygiene. No CI runner or automated
kernel-test harness was used.
## Result
All five assigned test cases passed.
| Test ID | Status | Manual evidence |
| --- | --- | --- |
| TC157 | PASS | Six 16/32-byte descending, duplicate, and cross-branch tables returned positive `EINTEGRITY`; repeated lookup agreed; a raw offset-4096 control was observable while failed lookups produced no payload read. |
| TC158 | PASS | A 17,592,186,056,704-byte sparse provider produced direct and file-handle `st_blocks=34359738384`; two FBT returns recorded `va_bytes=17592186052608`; the file remained readable. |
| TC159 | PASS | FIFO size-only setattr returned 0; size plus mode, owner, times, and all fields returned `EROFS(30)`; metadata remained `10640:0:0:0:0`. |
| TC160 | PASS | Both `OFF_MAX` and zero-offset `getdirentries` calls returned `EINTEGRITY(97)` and preserved their offsets; independent truss SHA256 was `6df9c594bfba8aaf849fa7105fda09a5cab99d79f04c42901a438321c90700e0`. |
| TC161 | PASS | Both normal configurations built, the private `nm` shim failed before publication without a SUCCESS line or temporary file, the previous KLD hash stayed unchanged, and a final normal build used `/usr/bin/nm` and exited 0. |
The TC157 fixtures do not cover a first nonzero `lstart` or an extreme
extent-count performance boundary. These are residual coverage limits, not
observed failures. Positive mapped-payload coverage remains the separate TC146
PARTIAL issue.
## Exact Inputs
The source archive exported from the baseline had SHA256
`a0c330601f9d7b2aa246a17e67bcd67914d457480cb69df025d09bc34d8b7b31`.
The independent fixture archive had SHA256
`bf8ab5430f06dfc067f041002cc7861b0b110dc00d9c447a82877f21b67b09ed`.
Its manifest and evidence hashes were
`b880be5d214985e6a7f75cc77fea333f046a30963a9d310d57a6cdc67104e740`
and `ace71446671b3d17e6a46997f43fc605a7a6bb5993c9de0e6e12cd223746b317`.
The fixture evidence recorded:
- complete TC157 logical-start arrays and old binary-search visit indices;
- TC158 `blocks_lo=2`, `blocks_hi=1`, and 4,294,967,298 data blocks;
- TC159 compact FIFO mode `010640`;
- TC160 `i_size=9223372036854775807` and expected errno 97.
## TC161 Build Qualification
The shim log SHA256 was
`a1a83797ba2c0111b66bb8783f3a67087420ef7a9bccc93fd42fb94c9c24c593`.
It contained `ERROR: nm failed while checking erofs.ko`, no SUCCESS line, and
left no `nm-undef.*` file. The post-shim status file recorded `exit=0` and
`nm=/usr/bin/nm`; its build log SHA256 was
`5ce2519a85de7a4846f95fdb858fd7818182aad3fa55a7ea8d06cf7687143f11`.
The post-shim build used an explicit normal path and environment:
```sh
env PATH=/usr/bin:/bin:/usr/sbin:/sbin \
FREEBSD_SRC=/tmp/repo22-freebsd15-src \
WITH_ZSTDIO=0 ./build.sh
```
Its key log lines were `==> Building repo22 erofs.ko` and
`==> SUCCESS: .../build/erofs.ko`.
Final exact-source builds used FreeBSD 15 kernel `-Werror`:
| Configuration | KLD SHA256 | Result |
| --- | --- | --- |
| `WITH_ZSTDIO=0` | `15fda9d334132cd81769ce4dff4f8411a6e2b4cf7531c352530d0de85f42a2d2` | PASS |
| `WITH_ZSTDIO=1` | `23782dc0ce7da188807d35020bf2d8c6044b796c5c9a8746b398ba784cd6ad4e` | PASS |
The final ZSTD build log SHA256 was
`e58bc73d1b0b8295d8baba0cf0ce07bf4e9c994837e65c7ccdd731dc91edb283`.
## Final Module Smoke and Cleanup
Each final KLD loaded independently, mounted `special-setattr.erofs` through a
fresh vnode md provider, exposed the expected FIFO, unmounted, detached the md
unit, and unloaded. The smoke log SHA256 was
`7ced7329da9f39527cf903213dee4c99dbe6d8d6fa96e55bca3427c4f4a56420`.
Final guest state was:
```text
residual-mount-count=0
residual-md=
residual-kld-count=0
smoke-exit=0
```
The host repo22 ignored `build/`, old ignored generated fixture/artifact trees,
and `__pycache__` content were removed before aggregation. No generated KLD,
object, image, or Python bytecode is acceptance evidence in Git.
## TC111 Coverage-Matrix Follow-up
A bounded filename audit found 162 specifications, TC000 through TC161, with
162 unique IDs, no duplicate, and no missing ID. TC000 is a template and is not
counted as executable.
The eight canonical G1-G8 report tables contained exactly 156 rows and 156
unique IDs covering TC001-TC156, with no duplicate or omission. TC157-TC161
add five unique executable cases. TC060's original G3 failure is superseded by
its dated fixed-source PASS report; TC153's G3 result is PASS.
The canonical final result is therefore:
```text
Executable test cases: 161
PASS: 160
PARTIAL: 1 (TC146)
FAIL/KERNEL-FAIL/ENV/SHELVED_TC: 0
```
TC146's shelved positive mapped-payload subitem is not counted as an additional
test case. G1-G8 evidence was collected across multiple source commits; this
report does not claim that every historical test ran on the final KLD. The
final `fcc85b93d` code baseline received the independent TC157-TC161 checks and
the affected final build/load/mount regressions recorded here.