Unsynchronized si_drv1/refcount lifecycle in fwmem open/close/ioctl/strategy β live-race kernel panic, post-race NULL write, and an 8-byte controlled UAF write over the slab freelist pointer (arbitrary-address allocation / double allocation)
Summary
open check-then-increments, close decrements/kfrees/NULLs, ioctl/strategy consume si_drv1 - all with no lock, devfs serializing none of it. Live: 4 threads racing open/close panicked the guest at fwmem_close+0x13 (NULL refcount deref); the post-race NULL state makes FW_SDEUI64 a supervisor write at 0x0; the ioctl-vs-final-close interleaving is an 8-byte fully attacker-controlled write at offset 0 of the freed 16-byte-zone chunk - the DFly SLAB freelist next pointer - after which kmalloc handed out the attacker-chosen address (zone assert) and duplicated a live chunk (already-allocated assert; silent double allocation on production builds). Operator-group local + FireWire controller present. Spinlock+NULL-guard fix validated: baseline panic vs 769,792 racing pairs with zero damage after; NULL ioctl face returns ENXIO.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5548 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| df5547.c | β | 24.3 KB | view raw | |
| Makefile | β | 1.8 KB | β download | |
| shim/ | β | 4.0 KB | β download | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 183 B | view raw | |
| run_fatal.sh | β | 388 B | view raw | |
| build.log | β | 6.9 KB | view raw | |
| run.log | β | 4.6 KB | view raw | |
| fix_validation.log | β | 1.8 KB | view raw | |
| fix.diff | β | 3.3 KB | view raw | |
| env.txt | β | 619 B | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-5548 β unsynchronized si_drv1/refcount lifecycle in fwmem open/close/ioctl/strategy
- File:
sys/bus/firewire/fwmem.c fwmem_open(fwmem.c:266-278): check-then-increment ofdev->si_drv1/fms->refcountwith no lock;fwmem_close(fwmem.c:291-298): decrement +kfree+ NULL store with no lock;fwmem_ioctl(fwmem.c:407-410) andfwmem_strategy(fwmem.c:342-343) consumesi_drv1with no lock.- Consequences (all from unprivileged-but-device-permitted local users
racing open/close on
/dev/fwmem*, which devfs does not serialize): 1. open loadssi_drv1non-NULL, final close frees + NULLs it, open resumes βNULL->refcount++(write at NULL+8) β kernel panic; or (compiler-CSE variant)refcount++into the freed chunk (UAF write), and the new fd is left withsi_drv1 == NULLso the next ioctl/read/write panics atfwmem.c:410/fwmem.c:343(test 3). 2.fwmem_ioctl(FW_SDEUI64)in flight across the final close β 8 fully attacker-controlled bytes written at offset 0 of the freed 16-byte-zone chunk == the DragonFly slab freelist next pointer β next-nextkmallocreturns the attacker-chosen address (arbitrary allocation; test 2 proves the poisoning). 3. two closes racing therefcount--β double free /kfree(NULL)β_kfreepanics ("trying to free NULL pointer",sys/kern/kern_slaballoc.c).
Build (guest, root)
sh build.sh # same harness as DF-5547 (production firewire.c + fwmem.c)
Run
sh run.sh # test 2 (replay/poisoning: proof lines, then the
# allocator assert panics on INVARIANTS kernels)
# test 4 (live race: Fatal trap 12 at fwmem_close+0x13)
sh run5.sh # test 5: arbitrary-address poisoning variant (panics:
# chunk_mark_allocated zone-membership assert)
sh run_fatal.sh # test 3: deterministic NULL-write panic (write at 0x0)
Expected
- test 2:
FREED chunk ... (== ATTACKER VALUE, slab freelist poisoned),kmalloc#1 = ... (== freed chunk), thenpanic: memory chunk ... is already allocated!;kern.df5547_statusβDF-5548 REPRODUCED. - test 4: Fatal trap 12 in
dfw_fwmem_open/dfw_fwmem_close(the renamed productionfwmem_open/fwmem_close) β panic on serial console. If the window is not hit in 15 s, the replay results still prove the interleavings' consequences (on the recorded baseline runs it hit within moments). - test 3:
Fatal trap 12: page fault while in kernel modeat0x0(supervisor write), faulting call in the renamedfwmem_ioctl(inlined into the sysctl frame in the recorded build). - test 5:
panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0" failed in chunk_mark_allocatedβ kmalloc followed the poisoned pointer to the attacker-chosen module-global address.
DF-5548 VERDICT β unsynchronized si_drv1/refcount lifecycle in fwmem open/close/ioctl/strategy
Reproduced: YES β live race kernel panic + controlled UAF write with slab-freelist poisoning (production code)
fwmem_open() (sys/bus/firewire/fwmem.c:266-278) does a check-then-act on
dev->si_drv1 / fms->refcount with no lock; fwmem_close()
(fwmem.c:291-298) decrements, kfrees and NULLs si_drv1 with no lock;
fwmem_ioctl() (fwmem.c:407-410) and fwmem_strategy() (fwmem.c:342-343)
consume si_drv1 with no lock. devfs serializes neither: two processes
open()/close()ing /dev/fwmem0.0 (0660 root:operator, created by fwdev_clone(),
fwdev.c:827-849) race these windows directly. crit_enter() in strategy does not
help across CPUs.
Evidence (all production entry points, df5547.ko harness)
- Live race β kernel panic (run.log test 4). 4 kernel threads racing production
fwmem_open/fwmem_closeon one shared cdev β the in-kernel equivalent of two processes open()+close()ing the device. Result within moments:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x8 (supervisor READ)
Stopped at dfw_fwmem_close+0x13: movl 0x8(%rbx),%eax
0x8 is offsetof(struct fwmem_softc, refcount) β production fwmem.c:292
dereferenced a NULL si_drv1, constructible only through the race (every close in
the racer was preceded by a successful open).
- Post-race state β NULL write panic (run.log test 3). The state "fd alive,
si_drv1 == NULL" (open'sfwmem.c:266check passed, the final close'sfwmem.c:295-297freed + NULLed in between): productionfwmem_ioctl(FW_SDEUI64)bcopys 8 bytes to&fms->euiwithfms == NULL:
Fatal trap 12 ... fault virtual address = 0x0, supervisor WRITE
- Controlled UAF write β slab freelist poisoning (run.log test 2 / test 5).
Stepwise replay of the ioctl-vs-final-close interleaving (ioctl had loaded
si_drv1atfwmem.c:407before the close'skfreeatfwmem.c:296):
df5548: after production close: si_drv1=0, freed chunk head (c_Next) = 0xfffff8008d5e15f0
df5548: after production ioctl(FW_SDEUI64): FREED chunk 0xfffff8008d5f05d0 now holds 0xfffff8008d5ef5e0 (== ATTACKER VALUE, slab freelist poisoned)
df5548: kmalloc#1 = 0xfffff8008d5f05d0 (== freed chunk) -- zone head now walks to the poisoned pointer
panic: memory chunk 0xfffff8008d5ef5e0 is already allocated!
FW_SDEUI64 writes the user-supplied 8-byte EUI64 at offset 0 of the freed 16-byte
zone chunk β exactly where DragonFly's SLAB allocator keeps the freelist next
pointer (sys/kern/kern_slaballoc.c). kmalloc#2 followed the attacker value onto
a LIVE chunk ("already allocated" INVARIANTS panic on this kernel; on a production
non-INVARIANTS kernel this is a silent double allocation β two owners of one
kernel object). With an arbitrary kernel address as the value, kmalloc handed out
the attacker-chosen address (test 5: chunk_mark_allocated zone-membership assert β
again, the only thing that stopped a silent arbitrary-address allocation here).
The racing variant is the mid-interleaving of fwmem_ioctl vs fwmem_close; the
equivalent open-side window (fwmem.c:266β269-270) yields the same freed-chunk
increment and the NULL+8 write observed live in (1).
- Double-close / desync consequences (code-read, consistent with (1)): two closes
racing
fms->refcount--can both take 1β0 βkfreetwice, and the secondkfree(dev->si_drv1, ...)withsi_drv1 == NULLpanics ("trying to free NULL pointer",sys/kern/kern_slaballoc.c:1410).
Exploit chain (demonstrated skeleton)
race open-vs-close (or ioctl-vs-close) β 8-byte fully-controlled write at offset 0 of
a freed 16-byte-zone chunk (the SLAB freelist next pointer) β next-next kmalloc(16)
returns the attacker-chosen address / duplicates a live chunk β overlap with a live
kernel object of the 16-byte zone β object-field corruption. On this INVARIANTS guest
the allocator's asserts convert the last step into a reliable kernel panic
(deterministic DoS); the arbitrary-address hand-out itself was observed.
Fix validation
fix.diff: a spinlock serializing the whole si_drv1/refcount lifecycle (open's
check-increment and alloc-install, close's decrement-free-NULL, ioctl's locked read +
ENXIO NULL guard, strategy's locked EUI copy), plus the M_FWβM_FWMEM type fix
(DF-5549). Applied to the guest's /usr/src, module rebuilt from the patched
production file (fix_validation.log):
| case | baseline | fixed |
|---|---|---|
| test 4 live race (4 threads) | Fatal trap 12 in fwmem_close+0x13 moments after start |
769,792 open+close pairs, guest up, zero damage |
test 3 si_drv1==NULL ioctl |
Fatal trap 12, supervisor WRITE at 0x0 | returns ENXIO, guest up |
fix_status: fixed. (The stepwise replay test 2 deliberately hand-constructs the mid-race state, bypassing the added serialization; fix validation therefore uses the production-path live race and the NULL-guard path.)
Notes
- Related-but-distinct from DF-5321 (firewire.c xfer/tlabel completion race): same
missing-serialization family, different objects (
si_drv1/fwmem_softc/bp vs tlabel/xfer) and different entry points. - The
kfree(dev->si_drv1, M_FW)wrong-malloc-type free (fwmem.c:296vs theM_FWMEMalloc atfwmem.c:272) is DF-5549 (accounting-only on DFly's allocator, which derives the zone from the chunk header) and is fixed in this pack's diff.
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
fixedfix.diff applied to guest /usr/src and module rebuilt from patched production sources: live race survived 769,792 open+close pairs across 4 threads with zero damage (baseline: Fatal trap 12 in fwmem_close within moments); the NULL-si_drv1 ioctl face returns ENXIO instead of a supervisor write at 0x0; the stepwise replay is intentionally not fix-sensitive (it hand-constructs the mid-race state), so validation used the production-path race and NULL-guard faces
fix_validation.log (verbatim baseline/fixed outputs for tests 3 and 4); fix.diff (spinlock + NULL guards + M_FWMEM)
Confirmed kernel references
- sys/bus/firewire/fwmem.c:266
- sys/bus/firewire/fwmem.c:269
- sys/bus/firewire/fwmem.c:270
- sys/bus/firewire/fwmem.c:291
- sys/bus/firewire/fwmem.c:292
- sys/bus/firewire/fwmem.c:296
- sys/bus/firewire/fwmem.c:297
- sys/bus/firewire/fwmem.c:342
- sys/bus/firewire/fwmem.c:407
- sys/bus/firewire/fwmem.c:410
- sys/kern/kern_slaballoc.c:1659
Detail
Exploit chain
operator-group user, FW controller present: race open() vs close() (or ioctl vs final close) on /dev/fwmem0.0 -> (a) NULL+8 write / NULL bcopy -> kernel panic DoS, or (b) FW_SDEUI64's 8 user-controlled bytes land on the freed fwmem_softc chunk's slab freelist next pointer -> next-next kmalloc(16) returns the attacker-chosen kernel address or duplicates a live 16-byte-zone object -> overlap/corruption of a live kernel object (arbitrary-address allocation observed; on INVARIANTS kernels the allocator asserts panic first)
Evidence (decisive lines)
run.log: 'Stopped at dfw_fwmem_close+0x13: movl 0x8(%rbx),%eax' (live race); 'fault virtual address = 0x0, supervisor write' (test 3); 'FREED chunk ... now holds ... (== ATTACKER VALUE, slab freelist poisoned)' + 'kmalloc#1 = ... (== freed chunk)' + 'panic: memory chunk ... is already allocated!' (test 2) and the chunk_mark_allocated zone assert (test 5); fix_validation.log: 'race finished, 769792 open+close pairs survived' and 'UNREACHED: ioctl returned' (ENXIO) on the fixed build
PoC changes
no seed PoC; same DF-5413-family harness as DF-5547 (production firewire.c+fwmem.c, -D renames incl. fwmem block-RPC exports and M_FWMEM). Fixes vs drafts: racer threads must sleep 1 tick every 256 pairs (spinning LWKT kernel threads starve userland and wedge the guest without proving anything); the same-slab groom needs a 4096-chunk bank on the HEAP (a 4096-entry stack array blew the kernel stack guard) to carve fresh contiguous slab pages; the groom partner index bug (pairc vs pair+1) corrected after the first successful run
Verified recommended fix
serialize the si_drv1/refcount lifecycle with a spinlock across fwmem_open/fwmem_close, take the lock (and return ENXIO on NULL) in fwmem_ioctl/fwmem_strategy; free with M_FWMEM (see fix.diff)
Verdict
Reproduced three ways on production code: (1) 4 threads racing production fwmem_open/fwmem_close on one shared cdev panicked the guest in fwmem_close+0x13 (movl 0x8(%rbx),%eax, fms==NULL); (2) the post-race fd-with-NULL-si_drv1 state turns production fwmem_ioctl(FW_SDEUI64) into a supervisor write at 0x0; (3) the ioctl-vs-final-close interleaving gives an 8-byte controlled UAF write over the freed chunk's slab freelist pointer, followed by kmalloc handing out the poisoned address ('already allocated' assert for a live-chunk target; zone-membership assert for an arbitrary module-global target). Fix (spinlock around the whole lifecycle + NULL guards + right malloc type) validated: 769,792 racing open/close pairs with zero damage vs baseline panic, and the NULL ioctl face returns ENXIO.
No comments yet.