sis_stop uses callout_stop (which does not interlock against an in-progress callback) while sis_tick re-arms itself as its last serialized action: timer resurrects after stop; on detach the late tick dereferences freed softc/miibus/bus resources (use-after-free) β DF-3715 callout-discipline family
Summary
kern_timeout documents callout_stop as non-interlocking against an in-progress callback. sis_tick takes if_serializer then unconditionally callout_reset re-arms before releasing it; every sis_stop caller holds that same serializer, so a tick that fired and blocked during sis_stop completes afterwards and re-arms the 'stopped' timer. On the detach path the resurrected/late tick enters the freed ifnet's serializer, dereferences the deleted miibus child and released bus resources -> UAF panic. Also leaves a stray 1 Hz tick after plain ifconfig down. oce uses the correct callout_terminate; sis does not. callout_terminate fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4084 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 2.4 KB | β raw | |
| verdict.json | β | 2.2 KB | view raw | |
| manifest.json | β | 728 B | view raw |
DF-4084 β sis_stop() uses non-blocking callout_stop while sis_tick re-arms:
timer resurrects after stop; on detach, sis_tick runs against freed
softc/miibus/resources (use-after-free) β DF-3715 callout-discipline family
File: sys/dev/netif/sis/if_sis.c (pass 2 β first pass over this file) Class: callout/lifetime race (privileged detach β UAF)
Root cause (path:line)
- if_sis.c:1123:
callout_init(&sc->sis_timer);(plain init) - if_sis.c:1477-1500: sis_tick callback:
- 1484
lwkt_serialize_enter(ifp->if_serializer);β¦ work β¦ - 1498
callout_reset(&sc->sis_timer, hz, sis_tick, sc);β¦ 1499 exit. - if_sis.c:2033: sis_stop():
callout_stop(&sc->sis_timer); - sys/kern/kern_timeout.c:1086-1088 (API contract): "callout_stop β¦ does not interlock against a callout that is in-progress."
Sequence: sis_tick fires on CPU1 and blocks acquiring if_serializer, which a thread doing sis_stop (ifconfig down via sis_ioctl:1974-1980, watchdog via sis_watchdog:2013, or sis_detach:1203-1208) holds. callout_stop returns without stopping the in-progress callback. The stopper releases the serializer; sis_tick then completes and β at line 1498 β callout_reset re-arms the timer after stop. The callout is resurrected.
Consequences:
- ifconfig down / sis_init cycle: timer keeps ticking against a stopped chip (mii_tick on a halted PHY) β incorrect but mostly benign.
- sis_detach (devctl detach / kldunload if_sis): after sis_stop, detach
tears down bus_teardown_intr (1207), ether_ifdetach (1210),
device_delete_child(miibus) (1213), releases sis_irq/sis_res (1216-1219)
and sis_dma_free (1221); the softc is freed when the driver detaches.
The resurrected/late sis_tick then executes: enters the freed ifnet's
serializer,
device_get_softc(sc->sis_miibus)on the deleted child, CSR reads on released resources β use-after-free β panic or worse.
Contrast with the correct pattern used by oce in the same commit family:
callout_terminate(&sc->timer) (sys/dev/netif/oce/oce_if.c:331), which
cancels and waits.
Fix
In sis_stop (or sis_detach, before releasing resources):
callout_terminate(&sc->sis_timer); /* instead of callout_stop */
(callout_terminate, kern_timeout.c:1104-1121, synchronously cancels and prevents resurrection.)
Verification status
UNTESTED β hardware-gated: the sis_tick path needs the SiS/DP83815 NIC (mii_tick on sc->sis_miibus); QEMU has no model (guest pciconf shows no 0x1039/0x100b devices). The callout_stop contract violation is source-certain.
DF-4084 VERDICT β sis_stop callout resurrection / post-detach UAF
Status: UNTESTED (hardware-gated; mechanism source-certain)
No SiS/DP83815 device exists in the QEMU guest (pciconf: no 0x1039/0x100b), so sis_tick cannot be made to fire; the race cannot be staged end-to-end on the available system.
Why the mechanism is certain from source
- sys/kern/kern_timeout.c:1080-1094: callout_stop is explicitly documented "does not interlock against a callout that is in-progress" β it cannot stop a callback that has already fired and is blocked on the serializer.
- if_sis.c:1484/1498-1499: sis_tick takes if_serializer, then re-arms with callout_reset as its last action before releasing it.
- if_sis.c:2033: sis_stop only does callout_stop. Every sis_stop caller (sis_ioctl:1974-1980, sis_watchdog:2013, sis_init:1769, sis_detach:1206) holds if_serializer β exactly the lock the in-flight tick is waiting on, guaranteeing the resurrection window whenever stop races a firing tick.
- sis_detach (1196-1224) then frees the miibus child (1213), the IRQ and register resources (1216-1219) and DMA state (1221) β a late/resurrected tick dereferences all of them (1484-1495) β UAF.
- The sibling driver oce uses callout_terminate for exactly this reason (sys/dev/netif/oce/oce_if.c:331); sis predates the discipline (callout code even carries a DF-3715-era comment about lock-aided callouts at kern_timeout.c:1080-1082).
Trigger: privileged (devctl detach / kldunload if_sis / reboot-time sis_shutdown) racing the 1 Hz tick. Probability per attempt is small (microseconds-scale window per second) but the action repeats across up/down cycles (ifconfig down leaves the timer alive too, enlarging later windows).
Fix
callout_terminate(&sc->sis_timer); in sis_stop (replaces callout_stop)
β synchronous cancel; no resurrection; safe because sis_stop runs under the same serializer the tick needs, so no callback can be executing when it returns.
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableFix validation not possible: no SiS hardware/emulation in QEMU.
Guest pciconf -l: no vendor 0x1039/0x100b devices.
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md, VERDICT.md. Key lines: if_sis.c:1123 (callout_init), 1484/1498 (tick serialize + re-arm), 2033 (callout_stop), 1196-1224 (detach frees), kern_timeout.c:1087-1088 (non-interlocking stop), oce_if.c:331 (correct callout_terminate pattern).
PoC changes
No runnable PoC: hardware-gated (sis_tick requires the SiS/DP83815 PHY). Mechanism documented at source level.
Verified recommended fix
Replace callout_stop(&sc->sis_timer) with callout_terminate(&sc->sis_timer) in sis_stop.
Verdict
sis_stop uses callout_stop (if_sis.c:2033), which per kern_timeout.c:1087 'does not interlock against a callout that is in-progress', while sis_tick unconditionally re-arms itself with callout_reset as its last serialized action (if_sis.c:1498). A tick blocked on if_serializer during sis_stop completes afterwards and resurrects the timer; in the detach path (if_sis.c:1196-1224) that late tick dereferences the freed ifnet serializer, deleted miibus child, and released bus resources β use-after-free panic. Mechanism is source-certain (stop-vs-rearm contract), trigger is privileged (detach/kldunload) plus timing. Untested: QEMU has no SiS NIC.
No comments yet.