β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5817

hammer2_vop_write checks RLIMIT_FSIZE against the pre-append offset, so O_APPEND writes bypass the file-size rlimit entirely (no EFBIG, no SIGXFSZ)

Summary

The RLIMIT_FSIZE gate evaluates uio_offset+uio_resid using the offset the write syscall entered with; for O_APPEND that is the file position (0 on a fresh open) while the actual write lands at EOF β€” the override happens only later inside hammer2_write_file. A file already larger than the limit accepts unlimited appended data; lseek(0) before each write makes the bypass repeatable at will. UFS applies the append offset FIRST and checks the limit SECOND (the VFS contract hammer2 diverges from). Reproduced unpriv uid 1001 on guest hammer2 root: write returns 4096 past a 1MiB rlimit with no SIGXFSZ, repeatable; control plain write correctly EFBIG+SIGXFSZ; same binary on UFS gets EFBIG. Containment bypass (ulimit -f shells, sandboxed writers) -> unbounded single-file disk growth. Move-check-after-override fix validated on rebuilt guest kernel: bypass gone, UFS unregressed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5817 Β· 12 files
FileTypeDescriptionSize
append_poc.c β€” 4.7 KB view raw
run.log β€” 866 B view raw
run.hammer2.2.log β€” 866 B view raw
run.hammer2.3.log β€” 866 B view raw
run.ufs.control.log β€” 747 B view raw
run.hammer2.patched.log β€” 747 B view raw
fix_run.log β€” 747 B view raw
build.log β€” 5.1 MB ↓ download
fix_build.log β€” 5.1 MB ↓ download
fix.diff β€” 2.0 KB view raw
env.txt β€” 180 B view raw
VERDICT.md β€” 2.5 KB ↓ raw
VERDICT.md
↓ download raw

DF-5817 VERDICT β€” reproduced (baseline) / fixed (patched kernel)

Bottom line

On the stock DragonFly 6.5-DEVELOPMENT guest (hammer2 root filesystem), an unprivileged user (maxx, uid 1001) grew a file past RLIMIT_FSIZE=1MiB through an O_APPEND descriptor: write() returned 4096 with no EFBIG and no SIGXFSZ, repeatedly (lseek(0) resets the checked offset). The identical binary and sequence on the same kernel's UFS filesystem gets -1/EFBIG + SIGXFSZ. After applying fix.diff (move the rlimit check after the IO_APPEND offset override, into hammer2_write_file) and rebuilding the kernel, the hammer2 behavior matches UFS: -1/EFBIG + SIGXFSZ.

Reproduced β€” evidence

  • run.hammer2.log (baseline, unprivileged): append-write(4096): ret=4096 errno=0 sigxfsz=0 size=2101248 -> BUG repeatable after lseek(0); control plain write at 2MiB: -1 errno=27 sigxfsz=1 proving the check itself works for non-append writes.
  • run.hammer2.2.log, run.hammer2.3.log: same result (3/3 runs).
  • run.ufs.control.log (root, /boot ufs): -1 errno=27 sigxfsz=1 -> OK: EFBIG (limit enforced) for the same append write β€” same kernel, same syscalls, only the filesystem differs.
  • run.hammer2.patched.log: after fix.diff kernel β€” -1 errno=27 sigxfsz=1 -> OK: EFBIG (limit enforced).

Why (path:line)

Fix validation

fix.diff (this pack): removes the pre-override check from hammer2_vop_write, adds the post-override check inside hammer2_write_file (after old_eof is established, releasing ip->lock + truncate_lock on the error exit). Guest /usr/src matched the audited tree (md5 cf7018082a9e765d352342b2a642e624), patch applied clean (3/3 hunks), make nativekernel KERNCONF=X86_64_GENERIC + installkernel + reboot, PoC re-run: bypass gone. Baseline vs patched outputs in run.hammer2.log vs run.hammer2.patched.log.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied clean to guest /usr/src (md5-identical to audited tree), kernel rebuilt and installed; the exact PoC now returns -1/EFBIG with SIGXFSZ on hammer2, size unchanged β€” identical to the UFS control. UFS behavior unchanged on the patched kernel (no regression).

fix_run.log (= run.hammer2.patched.log): 'append-write(4096): ret=-1 errno=27 sigxfsz=1 -> OK: EFBIG (limit enforced)'; fix_build.log ends '>>> Kernel build for X86_64_GENERIC completed'; baseline in run.log.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Tue Oct 13 05:08:03 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

not a corruption primitive: any process whose RLIMIT_FSIZE is used as a containment policy (ulimit -f, sandboxes, mailers) can grow a single file without bound through an O_APPEND descriptor (lseek(0)+write resets the checked offset each iteration), converting a per-process size cap into unbounded disk consumption.

Evidence (decisive lines)

run.hammer2.log lines 'append-write(4096): ret=4096 ... size=2101248 -> BUG: BYPASSED' (baseline, unpriv); run.ufs.control.log 'ret=-1 errno=27 sigxfsz=1 -> OK' (same kernel, UFS); run.hammer2.patched.log 'ret=-1 errno=27 sigxfsz=1 -> OK' after fix.diff kernel; run.hammer2.2.log/.3.log repeat runs; build.log/fix_build.log full kernel build of the patched tree.

PoC changes

wrote fresh harness (no seed): TEST1 pre-fills 2MiB, sets RLIMIT_FSIZE=1MiB, SIG_IGN(SIGXFSZ) so the errno is observable, fresh O_APPEND fd write, lseek(0)+write repeat, plus a plain-fd control write that must EFBIG on both filesystems.

Verified recommended fix

Move the RLIMIT_FSIZE check from hammer2_vop_write into hammer2_write_file, after the IO_APPEND offset override (mirroring ffs_write's ordering); see fix.diff.

Verdict

Unprivileged O_APPEND write() on DragonFly hammer2 bypasses RLIMIT_FSIZE entirely: the rlimit check in hammer2_vop_write (hammer2_vnops.c:869-874) evaluates the syscall-entry offset, while the EOF override happens later in hammer2_write_file (:1017-1018). Reproduced 3/3 as uid 1001 on the hammer2 root filesystem: write() returned 4096 (no EFBIG, no SIGXFSZ) appending to a 2MiB file under a 1MiB limit, repeatable via lseek(0); the same binary on the same kernel's UFS gets EFBIG+SIGXFSZ (UFS applies the append offset before the check, ufs_readwrite.c:237-269). After fix.diff (check moved after the override) and a kernel rebuild, hammer2 matches UFS: EFBIG+SIGXFSZ.