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)
PoC verification
Evidence pack
findings/poc/DF-5818 Β· 11 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 | |
| VERDICT.md | β | 2.3 KB | β 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 consultsip->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
fixedPatched 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'.
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.
No comments yet.