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

hammer2_vop_setattr applies va_mtime with no owner/VA_UTIMES_NULL privilege check (divergence from the VFS contract UFS enforces)

Field Value
ID DF-2629
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
CWE CWE-863 Incorrect Authorization
File sys/vfs/hammer2/hammer2_vnops.c
Lines 561-566
Area vfs
Confidence certain
Discovered 2026-08-28
Pass 2 (GLM 5.3 second pass)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

hammer2_vop_setattr writes any caller-supplied va_mtime into ip->meta.mtime (:561-566) with no ownership or privilege check. DragonFly's syscall layer lets NON-owners through to VOP_SETATTR whenever they hold write permission (kern_utimensat sets NLC_OWN|NLC_WRITE, vfs_syscalls.c:3961; naccess_lva passes group/world-writable access at vfs_nlookup.c:1910-1925) and relies on the filesystem to enforce the POSIX owner-or-privilege / VA_UTIMES_NULL distinction β€” UFS does exactly this (ufs_vnops.c:461-470). hammer2 omits it, so any user with mere write permission on a hammer2 file can forge its modification time to arbitrary values, defeating timestamp-based integrity tools (make-style dependency checking, incremental backups, forensic timelines, NFS-exposed timestamp semantics).

Root cause

vnops.c:561-566: if (vap->va_mtime.tv_sec != VNOVAL) { hammer2_inode_modify(ip); ip->meta.mtime = hammer2_timespec_to_time(&vap->va_mtime); ... } β€” no cr_uid comparison, no caps_priv_check(SYSCAP_NOVFS_SETATTR), no VA_UTIMES_NULL/VWRITE fallback. Contrast ufs_setattr: if (cred->cr_uid != ip->i_uid && (error = caps_priv_check(cred, SYSCAP_NOVFS_SETATTR)) && ((vap->va_vaflags & VA_UTIMES_NULL) == 0 || (error = VOP_EACCESS(vp, VWRITE, cred)))) return (error); (ufs_vnops.c:464-470). The gate must live in the FS because setutimes() passes VA_UTIMES_NULL only when times are UTIME_NOW/NULL (vfs_syscalls.c:3740-3741) while naccess_lva accepts any write-permitted non-owner.

Threat model & preconditions

  • Attacker position: unprivileged local user on a hammer2 mount who has write permission (group- or world-writable file) but is NOT the owner.
  • Privileges gained or impact: utimensat()/futimens() with arbitrary times succeeds on hammer2 (returns EPERM on an identical UFS file); corrupts metadata relied upon by backup/incremental-sync/forensic tooling. Same gap on the hammer2-backed NFS server path for write-authorized non-owner clients.
  • Required config or capabilities: write permission on the file.
  • Reachability: utimensat/touch -d.

Proof of concept

Build & run

as user A: chmod 666 /h2/a-owned-file
as user B: struct timespec ts[2]={{1000000000,0},{1000000000,0}};
utimensat(AT_FDCWD, "/h2/a-owned-file", ts, 0);   /* 0 on hammer2 */
stat("/h2/a-owned-file")  /* mtime 2001-09-09 */

Expected output

returns 0 on hammer2 (mtime forged) vs -1/EPERM on the equivalent UFS file.

Impact

Timestamp forgery by non-owners on hammer2; integrity/forensics impact, no memory safety.

Mirror the UFS gate before applying mtime:

--- a/sys/vfs/hammer2/hammer2_vnops.c
+++ b/sys/vfs/hammer2/hammer2_vnops.c
@@ -561,6 +561,17 @@ hammer2_vop_setattr(struct vop_setattr_args *ap)
 #endif
    if (vap->va_mtime.tv_sec != VNOVAL) {
+       uid_t cur_uid = hammer2_to_unix_xid(&ip->meta.uid);
+
+       if (ap->a_cred->cr_uid != cur_uid &&
+           (error = caps_priv_check(ap->a_cred,
+                         SYSCAP_NOVFS_SETATTR)) &&
+           ((vap->va_vaflags & VA_UTIMES_NULL) == 0 ||
+           (error = VOP_EACCESS(ap->a_vp, VWRITE, ap->a_cred))))
+       {
+           goto done;
+       }
+       error = 0;
        hammer2_inode_modify(ip);
        ip->meta.mtime = hammer2_timespec_to_time(&vap->va_mtime);

References

  • ufs_vnops.c:461-470 (the UFS gate), vfs_syscalls.c:3740-3741, 3961
  • POSIX utimensat owner/privilege semantics

Timeline

  • 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2629 Β· 8 files
FileTypeDescriptionSize
utimes_ab.c β€” A/B PoC: utimensat explicit/NULL against a path, printing uid, file uid/mode, errno, mtime before/after 1.7 KB view raw
setup.sh β€” root: creates UFS on vn0 and the 5-quadrant matrix files on UFS + hammer2 1.2 KB view raw
run.sh β€” unprivileged 5x2 matrix run 497 B view raw
run_baseline.log β€” stock kernel #0: hammer2 q2 forged mtime OK vs UFS EPERM (the reproduction) 1.6 KB view raw
run_fixed.log β€” patched kernel: all 10 cells identical between hammer2 and UFS 1.6 KB view raw
build.log β€” guest compile output (clean) 45 B view raw
fix.diff β€” UFS-equivalent gate (owner / SYSCAP_NOVFS_SETATTR / VA_UTIMES_NULL+VWRITE) before the mtime application 1.1 KB view raw
env.txt β€” guest/kernel/compiler environment 1.0 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied in guest kernel B (single fix-build carrying both DF-2628 and DF-2629 patches, same fix_kernel_uname in both verdicts; compiled clean under -Werror). Re-running the identical 5x2 matrix: the previously-forging cell (hammer2, non-owner, 0666, explicit times) now returns RET=-1 errno=1(Operation not permitted) with mtime unchanged, byte-identical to UFS; owner explicit set still works (q1 RET=0), POSIX UTIME_NOW with write permission still works (q4 RET=0), no-write quadrants still EACCES (q3/q5), and root can still set timestamps on both filesystems (root sanity RET=0). No behavioral regressions detected.

['run_fixed.log', 'fix.diff', 'VERDICT.md section 3']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Fri Aug 28 22:42:25 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_NOINV x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["run_baseline.log: hammer2 q2 line 'RET=0 errno=0(OK) mtime 1787954386.479197000 -> 1000000000.123456000' vs UFS q2 'RET=-1 errno=1(Operation not permitted)' as uid 1001 on a root-owned 0666 file; stat -f %Sm showed 'Sep  9 01:46:40 2001' persisting", 'run_fixed.log: all 5 quadrants x {ufs, hammer2} identical on the patched kernel; q2 EPERM on both', 'VERDICT.md section 1: full syscall-layer + FS-layer trace with path:line', 'fix.diff: the UFS-equivalent gate']

PoC changes

Seed PoC replaced with utimes_ab.c: single binary taking op (explicit|now) + path, printing uid/file-uid/mode/errno/mtime-before/after so the whole 5x2 matrix is captured in comparable records; the NULL-times (UTIME_NOW) mode was added to document that the VA_UTIMES_NULL rule is NOT the divergence (q4 behaves the same on both filesystems). Also added setup.sh/run.sh and a UFS vn volume for the A/B (the finding sketch suggested this; the guest root fs being hammer2 made the hammer2 side trivial).

Verified recommended fix

Mirror the ufs_setattr timestamp gate into hammer2_vop_setattr: allow mtime/atime changes only for the owner, privileged callers (caps_priv_check SYSCAP_NOVFS_SETATTR), or VA_UTIMES_NULL with VOP_EACCESS(vp, VWRITE) (see fix.diff).

Verdict

Reproduced exactly as claimed on the stock kernel: the mtime branch of hammer2_vop_setattr (hammer2_vnops.c:561-566) has no owner/privilege/VA_UTIMES_NULL gate, so an unprivileged user (uid 1001) successfully forged the mtime of a root-owned 0666 hammer2 file to 1000000000.123456000 (Sep 9 01:46:40 2001) via utimensat with explicit times, persisting on disk, while the identical call on UFS returns EPERM from the ufs_setattr gate (ufs_vnops.c:461-470). The syscall layer only enforces owner-OR-write-access (kern_utimensat NLC_OWN|NLC_WRITE at vfs_syscalls.c:3961, evaluated in naccess_va vfs_nlookup.c:1869-1925), which is insufficient for explicit timestamps - UFS re-gates in VOP_SETATTR, hammer2 did not. All four other permission quadrants (owner, no-write, UTIME_NOW variants) behave identically on both filesystems, isolating the divergence precisely to explicit-times + non-owner + write-granted. fix.diff mirrors the UFS gate into hammer2_vop_setattr; on the patched kernel hammer2 matches UFS byte-for-byte in all 10 matrix cells (q2 now EPERM), with no regressions to owner sets, UTIME_NOW+write, or root. Impact is a metadata-integrity violation (forgeable mtime for write-permitted non-owners on hammer2) - no memory-safety impact, consistent with the filed Low severity.