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

hammer2_write_file never enforces the append-only chflag (uappnd/sappnd): writable descriptors can overwrite arbitrary offsets of append-only files

Summary

hammer2's write path contains no ip->meta.uflags & APPEND check anywhere (the only APPEND checks are setattr-side). The generic kernel gate (nlookup) forces O_APPEND only for opens occurring while the flag is set and cannot retroactively fix an already-open descriptor. UFS closes exactly this hole in its write path (APPEND && offset != size -> EPERM); hammer2 does not. Reproduced unpriv uid 1001: open rw BEFORE the flag, chflags UF_APPEND, then pwrite at offset 0 and lseek+write mid-file both succeed silently (content rewritten); UFS control on the same kernel: EPERM both, content intact. Silent log forgery on hammer2 for tamper-evident sappnd/uappnd-flagged logs via already-open descriptors. ffs_write-mirror fix validated on rebuilt guest kernel: EPERM, content untouched, UFS unregressed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5818 Β· 11 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
VERDICT.md β€” 2.3 KB ↓ raw
VERDICT.md
↓ download raw

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

Bottom line

On the stock guest, an unprivileged user (maxx) holding a rw descriptor opened BEFORE the flag was raised, then chflags(path, UF_APPEND), could pwrite() and lseek()+write() at arbitrary non-EOF offsets of the append-only file on hammer2 β€” content mutated from AAAABBBBCCCC to XXXXYYYYCCCC with the syscall returning success. The identical sequence on the same kernel's UFS filesystem returns EPERM and leaves the content untouched. After fix.diff (append-flag check in hammer2_write_file, mirroring ffs_write), hammer2 returns EPERM and content is untouched.

Reproduced β€” evidence

  • run.hammer2.log (baseline, unprivileged): pwrite@0 on uappnd via pre-opened fd: ret=4 errno=0 -> BUG, write@4 ... ret=4, content now: [XXXXYYYYCCCC] -> BUG: append-only file content MODIFIED at non-EOF (3/3 across the three runs).
  • run.ufs.control.log (root, /boot ufs): ret=-1 errno=1 -> OK: EPERM, content untouched.
  • run.hammer2.patched.log: after fix.diff kernel β€” ret=-1 errno=1 -> OK: EPERM, content untouched.

Why (path:line)

  • The descriptor gate cannot retroactively fix open fds: sys/kern/vfs_nlookup.c:1857-1864 forces O_APPEND only for opens that occur while the flag is set.
  • UFS enforces in the write path: sys/vfs/ufs/ufs_readwrite.c:239-240 (ip->i_flags & APPEND && uio_offset != i_size -> EPERM).
  • hammer2 has no equivalent: hammer2_write_file (sys/vfs/hammer2/hammer2_vnops.c:997-1233) never consults ip->meta.uflags; the only APPEND checks are in setattr (hammer2_vnops.c:469,476).

Fix validation

Same fixed kernel as DF-5817 (fix.diff in this pack carries the append-flag hunk): if ((ip->meta.uflags & APPEND) && uio->uio_offset != ip->meta.size) return (EPERM); placed right after the IO_APPEND override in hammer2_write_file, releasing ip->lock + truncate_lock on the error exit. Patched run: EPERM, content intact.

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

Patched kernel returns EPERM for non-EOF writes on an append-only file through a pre-opened descriptor and leaves content untouched β€” matching UFS; no UFS regression on the same patched kernel.

fix_run.log TEST 2: 'pwrite@0 ... ret=-1 errno=1 -> OK: EPERM', 'content now: [AAAABBBBCCCC] -> OK: content untouched'.
↓ 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

integrity bypass, not memory corruption: a process holding a pre-flag rw descriptor (e.g. a running daemon whose log an operator then protects with chflags sappnd) can silently rewrite history of an append-only file at any offset β€” defeating the tamper-evidence guarantee of append-only logs on hammer2.

Evidence (decisive lines)

run.hammer2.log TEST 2: 'pwrite@0 on uappnd via pre-opened fd: ret=4 errno=0 -> BUG', 'content now: [XXXXYYYYCCCC] -> BUG: append-only file content MODIFIED at non-EOF'; run.ufs.control.log: 'ret=-1 errno=1 -> OK: EPERM', content untouched; run.hammer2.patched.log: 'ret=-1 errno=1 -> OK: EPERM', content untouched after fix.diff kernel.

PoC changes

initial PoC opened the fd after setting the flag and was blocked by the kernel's open-gate (EACCES); reworked to the correct trigger: open rw first, chflags UF_APPEND second, then write through the pre-opened descriptor (the gate cannot retroactively apply O_APPEND).

Verified recommended fix

Add to hammer2_write_file after the IO_APPEND override: if ((ip->meta.uflags & APPEND) && uio->uio_offset != ip->meta.size) return EPERM; (releasing ip->lock and truncate_lock); see fix.diff.

Verdict

hammer2_write_file never enforces the append-only chflag: a descriptor opened BEFORE chflags(uappnd/sappnd) can pwrite/lseek+write at arbitrary non-EOF offsets (the nlookup O_APPEND gate, vfs_nlookup.c:1857-1864, only covers opens that happen while the flag is set; UFS additionally enforces in the write path at ufs_readwrite.c:239-240). Reproduced 3/3 as uid 1001 on hammer2: pwrite@0 and write@4 both returned 4 on a UF_APPEND file, content mutated AAAABBBBCCCC -> XXXXYYYYCCCC with no error; the same sequence on UFS returns EPERM and leaves content untouched. After fix.diff (append-flag check in hammer2_write_file) and kernel rebuild, hammer2 returns EPERM and the content is intact.