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

hammer_ioc_gethistory ATKEY: ++cursor.key_beg.key signed-overflow at HAMMER_MAX_KEY wraps to INT64_MIN (full history disclosed for a past-EOF key); obj_type default EINVAL silently overwritten

Summary

The ATKEY validation only rejects key > nxt_key (:296-299), so key = nxt_key = 0x7fffffffffffffff passes; the REGFILE branch then executes ++cursor.key_beg.key (:354) - signed overflow (UB) wrapping to INT64_MIN in practice - and the btree iteration starts at the file's first record, returning the file's ENTIRE TID history for a key offset at which no record can exist. No memory unsafety (add_history bounds-guards every hist_ary write; cursor pinned to the open file's obj_id). Secondary latent defect: the default obj_type EINVAL (:367-370) is unconditionally overwritten at :386 - unreachable from userland today (fifo ioctls route to soo_ioctl). VERIFIED guest: GETHISTORY key=nxt_key=INT64_MAX returns rc=0 entries=3 (all TIDs) on stock kernel, 3 runs stable; on the fix-rebuilt kernel EINVAL entries=0. Fix validated: reject key >= HAMMER_MAX_KEY + honor validation errors before btree_first.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3114 Β· 10 files
FileTypeDescriptionSize
hammer_gate_poc.c β€” 5.8 KB view raw
build.sh β€” 107 B view raw
run.sh β€” 167 B view raw
run.log β€” 955 B view raw
run.root.log β€” 881 B view raw
env.txt β€” 228 B view raw
fix.diff β€” 809 B view raw
run.patched.root.log β€” 710 B view raw
VERDICT.md β€” 3.0 KB ↓ raw
verdict.json β€” 3.5 KB view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-3114 (hammer_ioc_gethistory ATKEY signed-overflow wrap)

Status: reproduced β€” impact: none directly (incorrect result / out-of-contract information return; a same-family cousin of the DF-3111 key-wrap class). Fix validated on the patched guest kernel.

How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS). /mnt/h1/histfile has three distinct history TIDs (0x0000000100008020/60/a0). PoC test [H]: GETHISTORY with flags=ATKEY, key=nxt_key=0x7fffffffffffffff.

Stock kernel, unprivileged (uid 1001): rc=0, entries=3, flags=0x9 (ATKEY|EOF) β€” the ++cursor.key_beg.key at hammer_ioctl.c:354 wrapped to INT64_MIN and the scan covered every record of the file, returning the whole history for a key offset at which no record can exist (records are stored as BASE+LEN; key INT64_MAX means "beyond end of file"). Identical result as root (run.root.log). Stable across 3 runs.

Root cause trace (stock source): - hammer_ioctl.c:296-299 β€” validation allows key == nxt_key == HAMMER_MAX_KEY; - hammer_ioctl.c:352-356 β€” REGFILE branch executes ++cursor.key_beg.key (signed overflow, UB β€” wraps on this build); - hammer_ioctl.c:386-397 β€” iteration from the wrapped key returns every record; add_history (hammer_ioctl.c:419-444) then admits them all because hist->key (INT64_MAX) is β‰₯ every key - data_len; - no OOB: add_history guards hist->count == HAMMER_MAX_HISTORY_ELMS (lines 470, 493) before each hist_ary[] write; cursor remains pinned to obj_id/localization, so only the caller's own open file is scanned.

Why it matters (bounded)

The wrap defeats the intended key-range semantics and returns data the API contract says cannot exist for that offset; combined with DF-3112 (GETHISTORY ungated) any local user can enumerate a file's full modification history with a single call that should have returned nothing. There is no memory-safety consequence (bounded arrays, pinned cursor) β€” severity Low.

Secondary latent defect (fixed in the same diff, not reachable from userland): the default: obj_type EINVAL at hammer_ioctl.c:367-370 is silently overwritten by error = hammer_btree_first(&cursor) at line 386.

Fix validation

fix.diff (2 hunks) β€” merged into ../DF-3112/fix_combined_for_validation.diff, applied to the guest /usr/src, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC), rebooted:

  • patched kernel, root run (/tmp/poc /mnt/h1/histfile keep): [H] β†’ rc=-1 errno=22 (EINVAL) β€” wrap eliminated;
  • patched kernel, unprivileged run: GETHISTORY β†’ EPERM (DF-3112 gate), consistent fix interaction.

Transcripts: run.log / run.root.log (baseline), run.patched.root.log (patched).

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 merged into the combined validation diff, rebuilt, rebooted): test [H] returns EINVAL (rc=-1 errno=22) as root; the wrap no longer occurs.

['findings/poc/DF-3114/run.patched.root.log', 'findings/poc/DF-3114/fix.diff', '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

none (validation defect; disclosure limited to the caller's own open file's history TIDs)

Evidence (decisive lines)

['findings/poc/DF-3114/run.log: [H] rc=0 entries=3 flags=0x9 as uid 1001', 'findings/poc/DF-3114/run.root.log: identical wrap as root', 'findings/poc/DF-3114/run.patched.root.log: [H] -> rc=-1 errno=22 (EINVAL) on the fix-built kernel', 'findings/poc/DF-3114/fix.diff: reject key >= HAMMER_MAX_KEY in ATKEY validation + honor the obj_type switch error']

PoC changes

Wrote from scratch (test [H] of the shared PoC). Needed the 'keep' argv mode for the patched-kernel root run because DF-3112's fix gates GETHISTORY, making the EINVAL observable only when the gate is legitimately passed.

Verified recommended fix

Reject hist->key >= HAMMER_MAX_KEY in the ATKEY validation and return the obj_type switch's EINVAL instead of overwriting it with hammer_btree_first()'s return.

Verdict

Reproduced on the audit guest: GETHISTORY with ATKEY key=nxt_key=0x7fffffffffffffff returns rc=0 with the file's ENTIRE TID history (entries=3, flags=ATKEY|EOF) for a key offset at which no record can exist - the ++cursor.key_beg.key at hammer_ioctl.c:354 signed-overflows and wraps to INT64_MIN, starting the scan at the file's first record. Correct result is 0 entries or EINVAL. As GETHISTORY is simultaneously ungated (DF-3112), any local user can trigger this. No memory-unsafety: add_history bounds-guards every hist_ary write (hammer_ioctl.c:470,493) and the cursor stays pinned to the open file's obj_id/localization. Patched kernel returns EINVAL. The latent swallowed-EINVAL for non-REGFILE/DIR/DBFILE obj types (hammer_ioctl.c:367-370 overwritten at :386) is unreachable from userland (fifo vnodes route ioctls through soo_ioctl) but is fixed by the same diff.