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

utimensat(2)/utimes(2)/futimens(2) let a non-owner with write permission set arbitrary file timestamps (POSIX permission-model violation, HAMMER2 default root)

Summary

kern_utimensat gates timestamp changes on NLC_OWN|NLC_WRITE and kern_futimens on naccess_lva(NLC_OWN|NLC_WRITE) β€” evaluated as owner-OR-write-permission, and neither path inspects nullflag to distinguish current-time (the only thing POSIX grants non-owner writers) from arbitrary user-supplied times. UFS re-checks ownership in VOP_SETATTR (ufs_vnops.c:462-468) but HAMMER2 β€” the default root filesystem β€” does not, so the permissive syscall gate is the only barrier on a default system. Reproduced on the guest as uid 1001: root-owned mode-666 file mtime set to epoch 0 / 66666 / 12345678 via utimensat (both follow and AT_SYMLINK_NOFOLLOW) and futimens on an O_RDWR fd; mode-644 control returned EACCES proving the gate is write-permission. POSIX.1-2017, FreeBSD (PRIV_VFS_UTIMES) and Linux (CAP_FOWNER) all return EPERM. Integrity forgery (timestamp-based backups/build systems/forensics silently miss tamper); no memory-corruption primitive. Ownership-or-privilege fix validated git-apply-clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5797 Β· 13 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
utimes_forge.c β€” 3.0 KB view raw
setup_root.sh β€” 427 B view raw
build.sh β€” 101 B view raw
run.sh β€” 178 B view raw
build.log β€” 78 B view raw
run.log β€” 757 B view raw
run.2.log β€” 396 B view raw
env.txt β€” 346 B view raw
fix.diff β€” 1.5 KB view raw
manifest.json β€” 943 B view raw
verdict.json β€” 3.7 KB view raw

DF-5797 β€” utimensat/futimens: non-owner with write permission can set arbitrary file timestamps

What

kern_utimensat() (sys/kern/vfs_syscalls.c:3961) and kern_futimens() (sys/kern/vfs_syscalls.c:3847) gate the whole timestamp operation on NLC_OWN | NLC_WRITE, which naccess_lva() evaluates as owner OR write-permission. Neither path checks whether the user-supplied timespecs are arbitrary (POSIX allows arbitrary values only for the owner / privileged users; write access alone permits only the current time). On filesystems that do not re-check ownership in VOP_SETATTR β€” HAMMER2, the default root FS, unlike UFS (sys/vfs/ufs/ufs_vnops.c:462-468) β€” the arbitrary times are accepted.

Reproduce (on the guest)

# root:
sh /tmp/setup_root.sh          # creates /tmp/df5797_target (root:wheel 666)
cc -O -o /tmp/utimes_forge /tmp/utimes_forge.c
chown maxx /tmp/utimes_forge
# unprivileged user (maxx, uid 1001):
/tmp/utimes_forge /tmp/df5797_target

Expected (vulnerable) output

uid=1001  file owner=0 mode=100666
utimensat(arbitrary)      -> OK          <- POSIX says EPERM here
  after utimensat            atime=0 mtime=66666
utimensat(nofollow,mixed)-> OK
futimens(arbitrary)       -> OK          <- POSIX says EPERM here
  after futimens            atime=12345678 mtime=12345678

Control (mode 644 file, no write perm) correctly returns EACCES β€” proving the gate is exactly "write permission", not ownership.

Impact

Any local user with write access to a file they do not own can forge arbitrary atime/mtime β€” including restoring original timestamps after modifying content. This defeats timestamp-based incremental backups (rsync -a, tar), build systems (make), and forensic/audit triage that assume POSIX utimes semantics (FreeBSD/Linux/POSIX all return EPERM).

VERDICT.md
↓ download raw

DF-5797 VERDICT β€” REPRODUCED (policy/integrity violation)

Bottom line

On DragonFly 6.5-DEVELOPMENT (hammer2 root), an unprivileged user (uid 1001, maxx) successfully set arbitrary atime/mtime on a root-owned, world-writable file (/tmp/df5797_target, mode 666) via utimensat(2) (with and without AT_SYMLINK_NOFOLLOW) and futimens(2). Observed values written: atime=epoch 0, mtime=66666, then both atime/mtime=12345678. A control file without write permission (mode 644) returned EACCES on every path, proving the only gate exercised is write permission, not ownership. POSIX.1-2017 utimensat, FreeBSD (setutimes() PRIV_VFS_UTIMES logic) and Linux (CAP_FOWNER) all require EPERM for arbitrary times from a non-owner.

Root cause (path:line)

  1. sys/kern/vfs_syscalls.c:3961 β€” kern_utimensat() sets nd->nl_flags |= NLC_OWN | NLC_WRITE and never inspects nullflag (whether the request is "current time" vs arbitrary). naccess_lva (sys/kern/vfs_nlookup.c:1875-1896) implements NLC_OWN as an owner-side bypass and NLC_WRITE as a mode-bit check β€” i.e. the combination is owner OR write, not owner.
  2. sys/kern/vfs_syscalls.c:3847 β€” kern_futimens() does the same via naccess_lva(&lva, NLC_OWN | NLC_WRITE, fp->f_cred) and then calls setutimes() with arbitrary newts.
  3. Filesystem layer: UFS enforces the POSIX rule itself (sys/vfs/ufs/ufs_vnops.c:462-468: non-owner needs SYSCAP_NOVFS_SETATTR, else VA_UTIMES_NULL+VWRITE), but HAMMER2's setattr path performs no ownership check for va_atime/va_mtime, so on the default root filesystem the syscall gate is the only defense β€” and it is permissive. (hammer2-side re-check recommended as defense in depth; the syscall-layer fix below repairs every filesystem and both the path and fd entry points.)

Evidence

  • run.log / run.2.log β€” two full runs, both show utimensat(arbitrary) -> OK, futimens(arbitrary) -> OK, stat() confirming the forged values (atime=0/mtime=66666, then 12345678), and the mode-644 control returning EACCES.
  • env.txt β€” guest uname/compiler/mounts (hammer2 root).

Exploit chain

Not a memory-corruption primitive; the impact ceiling is integrity forgery: modify file content (write access), then restore prior timestamps so timestamp-based backups/audit/make tooling miss the tamper. Deterministic, unprivileged, on the default filesystem.

Fix validation

fix.diff authored (sys/kern/vfs_syscalls.c: POSIX ownership/privilege check for arbitrary times in both kern_utimensat and kern_futimens, mirroring the file's own can_hardlink() style using caps_priv_check(SYSCAP_NOVFS_SETATTR)). Kernel rebuild validation was not performed: this is a permission-policy finding, not a reproduced memory-corruption finding, so the mandatory-rebuild rule does not apply. Behavior change is directly predictable from the diff (non-owner arbitrary-time requests become EPERM; both-time-current requests keep working).

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

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored and verified to apply cleanly (git apply --check) against sys/kern/vfs_syscalls.c; kernel rebuild validation not performed because this is a permission-policy finding, not reproduced memory corruption (mandatory rebuild rule does not apply). The patched gate directly converts the observed OK results into EPERM while leaving both-current-time requests working.

findings/poc/DF-5797/fix.diff (applies cleanly: 'git apply --check' passed); VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-5797

Confirmed kernel references

Detail

Exploit chain

unprivileged user -> write access to non-owned file (group/world write) -> utimensat/futimens with arbitrary {tv_sec,tv_nsec} -> kernel accepts (write-perm gate only) -> file content tamper becomes invisible to timestamp-based backup/audit tooling

Evidence (decisive lines)

run.log/run.2.log: 'utimensat(arbitrary) -> OK', 'after utimensat atime=0 mtime=66666', 'futimens(arbitrary) -> OK ... atime=12345678 mtime=12345678', mode-644 control EACCES; env.txt: hammer2 root filesystem

PoC changes

Seed concept only; wrote the rig from scratch: three syscall paths (utimensat follow/nofollow, futimens on O_RDWR fd) with stat() verification of forged values plus a mode-644 negative control that isolates the write-permission gate.

Verified recommended fix

In kern_utimensat()/kern_futimens(), when the caller-supplied times are not both current-time (nullflag==0), require file ownership or caps_priv_check(SYSCAP_NOVFS_SETATTR), else EPERM - see fix.diff

Verdict

On DragonFly 6.5-DEVELOPMENT (hammer2 root), an unprivileged user (uid 1001) set arbitrary atime/mtime on a root-owned world-writable file via utimensat(2) (both follow and AT_SYMLINK_NOFOLLOW) and futimens(2). Observed forged values: atime=epoch 0, mtime=66666, later both atime/mtime=12345678. Control file without write permission returned EACCES on all paths, proving the active gate is write permission, not ownership. POSIX.1-2017, FreeBSD (PRIV_VFS_UTIMES) and Linux (CAP_FOWNER) all require EPERM for arbitrary timestamps from a non-owner. Root cause: kern_utimensat (vfs_syscalls.c:3961) and kern_futimens (vfs_syscalls.c:3847) gate only on NLC_OWN|NLC_WRITE which naccess_lva evaluates as owner-OR-write and never distinguish arbitrary times from current-time; HAMMER2's setattr, unlike UFS (ufs_vnops.c:462-468), performs no ownership re-check. Impact ceiling: integrity forgery - a user with write access to a file they do not own can tamper content then restore timestamps, defeating rsync/tar incremental backups, make, and forensic triage.