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)
PoC verification
Evidence pack
findings/poc/DF-5797 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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)
sys/kern/vfs_syscalls.c:3961βkern_utimensat()setsnd->nl_flags |= NLC_OWN | NLC_WRITEand never inspectsnullflag(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.sys/kern/vfs_syscalls.c:3847βkern_futimens()does the same vianaccess_lva(&lva, NLC_OWN | NLC_WRITE, fp->f_cred)and then callssetutimes()with arbitrarynewts.- Filesystem layer: UFS enforces the POSIX rule itself
(
sys/vfs/ufs/ufs_vnops.c:462-468: non-owner needsSYSCAP_NOVFS_SETATTR, elseVA_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 showutimensat(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_testablefix.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
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.
No comments yet.