19 KiB
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:
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:
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:
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:
==> 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:
cc -O2 -Wall -Wextra -std=c17 nfs_fh_tool.c -o nfs_fh_tool
Fixtures
The primary fixture was generated with:
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.
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:
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:
/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:
./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:
fb1778fc6bdc979e10d003dd6131a961e7532baa16273c5465935a665bf990f6
Malformed and stale classification:
./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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
f779dbe3aeaa609beb32212579f3aea441f244621d13c977cb8b52d7ec3016d9 alpha.bin
5990b055a3c6d27681185a25e15ff21ba3cbdca7632faf4332233238e5f45aad small.txt
001bcf4626e7d52f1bd76dee9a6c6e7d3a718c03602c5f9b07840a6c98b8fdc3 tree/beta.bin
Cold detached-provider test:
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:
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:
4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71
Dot handling:
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:
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:
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:
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:
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:
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:
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 changedlines 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.