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)
PoC verification
Evidence pack
findings/poc/DF-5817 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 -> BUGrepeatable afterlseek(0); control plain write at 2MiB:-1 errno=27 sigxfsz=1proving 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: afterfix.diffkernel β-1 errno=27 sigxfsz=1 -> OK: EFBIG (limit enforced).
Why (path:line)
- sys/vfs/hammer2/hammer2_vnops.c:869-874 β rlimit check on the syscall-entry offset.
- sys/vfs/hammer2/hammer2_vnops.c:1017-1018 β IO_APPEND offset override applied only later, inside hammer2_write_file.
- sys/vfs/ufs/ufs_readwrite.c:237-238 then :264-269 β UFS order: override first, check second (the contract).
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
fixedfix.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.
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.
No comments yet.