HAMMER1 ioctl dispatcher: 9 commands overwrite the caps_priv_check(SYSCAP_NOVFS_IOCTL) gate result and execute for unprivileged users (DF-0815 class on HAMMER1)
Summary
hammer_ioctl() computes the SYSCAP gate once at :72; nine case labels (GETHISTORY :86, SYNCTID :117, GET_PSEUDOFS :121, GET_VERSION :177, GET_INFO :181, LIST_VOLUMES :213, GET_SNAPSHOT :233, GET_CONFIG :237, SCAN_PSEUDOFS :263) assign error = handler(...) without the 'if (error == 0)' guard every other case uses, so any local user with any open fd on a HAMMER1 mount executes them. Worst companions: SCAN_PSEUDOFS's copyout uses the disk-controlled record length with no clamp (hammer_pfs.c:363), GET_SNAPSHOT/GET_CONFIG copy struct-size data regardless of the record's data_len, and SYNCTID drives global flusher syncs. VERIFIED guest uid 1001: GETHISTORY/GET_VERSION/GET_INFO/SYNCTID(NONE)/SCAN_PSEUDOFS all rc=0 while PRUNE and ADD_SNAPSHOT return EPERM from the same dispatcher; root contrasts reach handler validation. Fix validated (8-hunk if(error==0) guards, rebuilt kernel): all five EPERM unprivileged, root still succeeds.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3112 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| hammer_gate_poc.c | β | 5.8 KB | view raw | |
| build.sh | β | 107 B | view raw | |
| run.sh | β | 154 B | view raw | |
| run.log | β | 955 B | view raw | |
| run.root.log | β | 881 B | view raw | |
| env.txt | β | 228 B | view raw | |
| fix.diff | β | 2.6 KB | view raw | |
| fix_combined_for_validation.diff | β | 3.3 KB | view raw | |
| run.patched.log | β | 641 B | view raw | |
| run.patched.root.log | β | 710 B | view raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| verdict.json | β | 4.1 KB | view raw |
VERDICT β DF-3112 (HAMMER1 ioctl dispatcher privilege-gate bypass)
Status: reproduced (unprivileged local execution of 9 gated-class ioctl commands) β impact: none directly (info disclosure / policy bypass; no memory corruption). Fix validated on a patched guest kernel (see bottom).
How it was reproduced
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), cc 8.3.
A 1.1 GB HAMMER1 filesystem was created on vn0
(newfs_hammer -f -L pocfs /dev/vn0), mounted at /mnt/h1, with
/mnt/h1/histfile carrying 3 distinct history TIDs (write +
hammer synctid Γ 3).
hammer_gate_poc.c (uid 1001 maxx, plain ioctl(2) on an O_RDONLY fd):
- [A] GETHISTORY β rc=0, 3 history entries β gate bypassed
- [B] GET_VERSION β rc=0, version string β gate bypassed
- [C] GET_INFO β rc=0, label 'pocfs' β gate bypassed
- [D] SYNCTID β rc=0, flusher TID β gate bypassed
- [E] SCAN_PSEUDOFSβ rc=0, pfs 0 scanned β gate bypassed
- [F] PRUNE β EPERM (gated contrast β same dispatcher)
- [G] ADD_SNAPSHOT β EPERM (gated contrast β same dispatcher)
The same binary run as root shows [F]/[G] reaching the handler
(ADD_SNAPSHOT β EINVAL from its own index >= count validation), proving the
EPERM seen for the unprivileged run is exactly the
caps_priv_check(SYSCAP_NOVFS_IOCTL) result at hammer_ioctl.c:72 that
[A]-[E] overwrite. Stable across 3 runs (see run.log, run.root.log).
Root cause
sys/vfs/hammer/hammer_ioctl.c:72 computes the gate once; the nine case labels
listed in README.md assign error = hammer_ioc_...(...) without the
if (error == 0) guard used by every other case in the same switch.
Impact ceiling
- Unprivileged disclosure of filesystem topology/state: volume list (compounds DF-0870), PFS configuration incl. shared/unique UUIDs and sync TIDs (GET_PSEUDOFS), snapshot TID inventory (GET_SNAPSHOT), per-file modification history (GETHISTORY), version/statistics (GET_VERSION/GET_INFO).
- Unprivileged SYNCTID drives global flusher synchronization (op=SYNC1/SYNC2 do full-mount flush cycles) β a resource/DoS amplifier comparable to sync(2) spam, but with TID-precision disclosure for backup tooling.
- On filesystems with corrupted/crafted records, GET_SNAPSHOT /
GET_CONFIG / SCAN_PSEUDOFS copy disk-buffer contents to the caller without
clamping to the record's
data_lencontract (hammer_pfs.c:363 uses the disk-controlled length directly as the copyout size) β the missing gate makes that path reachable by any local user.
No memory-safety violation was demonstrated from this finding alone.
Fix validation
fix_combined_for_validation.diff (this finding's 9 dispatcher hunks + the
DF-3114 hunks) was applied to the guest's /usr/src, kernel rebuilt with
make nativekernel KERNCONF=X86_64_GENERIC, rebooted, and the identical PoC
re-run:
- unprivileged run: [A]-[E] now return EPERM (gate enforced), [F]/[G] unchanged (EPERM);
- root run: [A]-[E] succeed legitimately, [H] now returns EINVAL (DF-3114 fix).
Baseline vs patched transcripts: run.log / run.root.log (baseline) and run.patched.log / run.patched.root.log (patched) in this directory.
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 (fix.diff applied to /usr/src, make nativekernel KERNCONF=X86_64_GENERIC, reboot) returns EPERM for all five ungated-class commands as uid 1001 while root still succeeds - the exact bypass behavior is gone.
['findings/poc/DF-3112/run.patched.log', 'findings/poc/DF-3112/run.patched.root.log', 'findings/poc/DF-3112/fix_combined_for_validation.diff']
Confirmed kernel references
- sys/vfs/hammer/hammer_ioctl.c:72
- sys/vfs/hammer/hammer_ioctl.c:86
- sys/vfs/hammer/hammer_ioctl.c:117
- sys/vfs/hammer/hammer_ioctl.c:121
- sys/vfs/hammer/hammer_ioctl.c:177
- sys/vfs/hammer/hammer_ioctl.c:181
- sys/vfs/hammer/hammer_ioctl.c:213
- sys/vfs/hammer/hammer_ioctl.c:233
- sys/vfs/hammer/hammer_ioctl.c:237
- sys/vfs/hammer/hammer_ioctl.c:263
- sys/vfs/hammer/hammer_pfs.c:363
Detail
Exploit chain
unprivileged user opens any file on a HAMMER1 mount -> ioctl(2) with the ungated commands -> dispatcher runs handler despite failed caps_priv_check (info disclosure / policy bypass); no escalation chain exists for this class.
Evidence (decisive lines)
["findings/poc/DF-3112/run.log: lines [A]-[E] '*** PRIV GATE BYPASSED ***' as uid=1001 with [F]/[G] EPERM contrast", 'findings/poc/DF-3112/run.root.log: same binary as root reaches handler validation (ADD_SNAPSHOT EINVAL at count check), proving the EPERM is the caps gate', 'findings/poc/DF-3112/run.patched.log: [A]-[E] -> EPERM on the fix-built kernel', "findings/poc/DF-3112/fix.diff: 9 `if (error == 0)' guards", 'sys/vfs/hammer/hammer_ioctl.c:72,86,117,121,177,181,213,233,237,263']
PoC changes
Seed sketch had none; wrote from scratch. In-program privilege drop had to be conditional (setuid(65534) fails when the PoC is already run as unprivileged uid 1001); added 'keep' argv mode so the same binary demonstrates the root-path contrast and the DF-3114 fix validation on the patched kernel. Included kernel headers via -I/usr/src/sys using the sbin/hammer pattern.
Verified recommended fix
Wrap all nine ungated case labels in hammer_ioctl() with the same `if (error == 0)' guard used by the rest of the dispatcher.
Verdict
Reproduced on the audit guest (DFly 6.5-DEVELOPMENT, X86_64_GENERIC): an unprivileged uid-1001 process executed HAMMERIOC_GETHISTORY/GET_VERSION/GET_INFO/SYNCTID/SCAN_PSEUDOFS (rc=0) on an O_RDONLY fd on a HAMMER1 mount, while gated contrast commands HAMMERIOC_PRUNE/ADD_SNAPSHOT returned EPERM from the same dispatcher (hammer_ioctl.c:72 gate result overwritten by 9 case labels). No direct memory-safety impact; ceiling is unprivileged fs-topology/state disclosure (incl. unclamped disk-length copyout in scan_pseudofs, hammer_pfs.c:363), TID-history enumeration, and global flusher forcing via SYNCTID. Patched kernel (all 9 handlers wrapped in the standard if (error == 0) guard) returns EPERM for all five as unprivileged and still succeeds as root - fix validated end-to-end.
No comments yet.