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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3112 Β· 12 files
FileTypeDescriptionSize
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.md
↓ download 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_len contract (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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sun Sep 6 13:09:37 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.