register_int check-then-create windows span blocking allocations: NULL info->i_thread dereference and re-arm of an already-armed emergency systimer under concurrent registration
| Field | Value |
|---|---|
| ID | DF-2777 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-362 / CWE-476 |
| File | sys/kern/kern_intr.c |
| Lines | 308-319, 325-337 (use :349, :585-596) |
| Area | kern |
| Confidence | speculative |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
register_int sets i_state = ISTATE_NORMAL before the potentially blocking kmalloc(M_INTWAIT)/lwkt_create. If the creating thread blocks, a second registration on the same target CPU observes ISTATE_NORMAL and proceeds to use info->i_thread (still NULL) β atomic_set_int on td_flags through NULL + sched_ithd_intern dereference β NULL-deref panic. Symmetrically, the emergency-thread creation checks emergency_intr_thread[cpuid]==NULL then runs blocking calls; two interleaved registrations both pass and the second systimer_init_periodic re-adds the ALREADY-ARMED timer (systimer_add KKASSERTs !SYSTF_ONQUEUE on INVARIANTS; production: the same node linked twice into gd_systimerq, corrupting the per-CPU timer queue). Concurrent driver attach (parallel probe/attach, kldload racing boot attach) on the same CPU plus memory pressure; not unpriv-reachable.
Recommended fix
Publish i_thread fully before setting ISTATE_NORMAL; create the emergency thread under a once-only guard that cannot be re-entered across a blocking call (set the slot pointer only after systimer init; check under crit_enter before re-arming).
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_intr.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2777 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| run.log | β | 2.3 KB | view raw | |
| panic_sc1.txt | β | 674 B | view raw | |
| panic_sc2.txt | β | 813 B | view raw | |
| panic_sc3.txt | β | 654 B | view raw | |
| df2777_fixed.dmesg | β | 316 B | β download | |
| env.txt | β | 263 B | view raw | |
| fix.diff | β | 5.3 KB | view raw | |
| build.log.gz | β | 189.6 KB | β download | |
| build.fixed.log.gz | β | 189.5 KB | β download | |
| src/df_harness.c | β | 22.3 KB | view raw | |
| src/kern_intr.pristine.c | β | 32.0 KB | view raw | |
| src/kern_intr.hooks_stock.diffside.c | β | 32.5 KB | view raw | |
| src/kern_intr.hooks_fixed.c | β | 34.5 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-2777 evidence pack β register_int check-then-create windows (NULL i_thread deref, double-armed systimer)
Finding: sys/kern/kern_intr.c:308-349 β two check-then-create windows span
blocking operations, and state is published before the object it describes:
info->i_state = ISTATE_NORMAL(:326) is published BEFOREinfo->i_threadis allocated (:327,M_INTWAITkmalloc β can block) and created vialwkt_create(:329). A second concurrentregister_intfor the same (cpu, intr) observesISTATE_NORMALwith a NULLi_threadand either writes&info->i_thread->td_flagsat :349 (INTR_CLOCK path) or leaves the slot ready to blow up insched_ithd_intern(:589,info->i_thread->td_gd) when an interrupt arrives.- The emergency-thread block (:308-319) checks
emergency_intr_thread[cpuid] == NULLand then spanslwkt_create+systimer_init_periodic_nq; two concurrent first-registrations on a CPU both create a thread and both (re)initializeemergency_intr_timer[cpuid]β the secondsystimer_initon an already-armed timer corrupts timer state (crash insidesystimer_add, kern_systimer.c).
Rig
Runtime kldload GPFs on this guest, so the interposition is compiled into
kern_intr.c as two DORMANT hook points (df2777_hook, no-op unless armed;
src/kern_intr.hooks_stock.diffside.c = stock + hooks,
src/kern_intr.pristine.c = stock):
- hook(1) immediately after
info->i_state = ISTATE_NORMAL;(:326) β stalls the first registrant inside the window; - hook(2) immediately inside
if (emergency_intr_thread[cpuid] == NULL) {(:308) β stalls the first emergency creator (first-through gate).
Scenario 3 additionally uses dfh_reset_emergency(cpu) (rig-only helper in
the hooked kern_intr.c) to reopen the boot-time creation window, because on
a booted system every CPU already created its emergency thread at boot β
documented honestly: the double-arm race is boot-time/first-registration
reachable, the rig reopens it to expose the defect.
Scenarios (sysctl dfh.scen2777 + dfh.run2777), two harness threads pinned
to cpu2/cpu3 migrating onto the registration cpu:
- two concurrent
register_int(254, INTR_MPSAFE|INTR_CLOCK, cpu2); - first registrant stalled mid-window, second completes, then
sched_ithd_soft(255)(interrupt arrival); dfh_reset_emergency(2)+ two concurrentregister_int(254, cpu2).
Expected / observed
- SC1 stock:
Stopped at register_int+0x1b9: lock orl $0x20000000,0xd8(%rdx)β theatomic_set_int(&info->i_thread->td_flags, TDF_CLKTHREAD)at :349 with NULLi_thread(fault VA 0xd8 = offsetof(td_flags)). - SC2 stock:
Stopped at sched_ithd_intern+0x1f: movq 0xb8(%rdx),%r8βinfo->i_thread->td_gdat :589 with NULLi_thread(VA 0xb8). - SC3 stock:
Stopped at systimer_add+0x15c: movq (%rcx),%rsi(VA 0, thread in CRIT) β second concurrentsystimer_init_periodic_nqre-initialized an armed timer and crashed insidesystimer_add. - Fixed (
src/kern_intr.hooks_fixed.c= pristine + fix.diff + hooks): all three scenarios survive (SC1/SC3 print non-NULL rec pointers; SC2 degrades to the rate-limitedsched_ithd: stray interrupt 255report) βdf2777_fixed.dmesg.
Build/run: harness kernel as in DF-2775 README; stock+hooks build log in
build.log.gz, fixed+hooks build log in build.fixed.log.gz.
fix.diff = canonical combined fix (DF-2777 hunks: ISTATE_CREATING cmpset
gate with publish-thread-before-state, NULL-guard at :349, stray-guard in
sched_ithd_intern, emergency once-only cmpset gate).
DF-2777 VERDICT β reproduced (three distinct panics; fix validated)
Bottom line
register_int publishes i_state = ISTATE_NORMAL (kern_intr.c:326) before
it allocates/creates i_thread (:327-331) β a window that spans a blocking
M_INTWAIT kmalloc and lwkt_create β and its emergency-thread block
(:308-319) is a check-then-create spanning lwkt_create +
systimer_init_periodic_nq. With dormant hook points compiled into
kern_intr.c that stall the first registrant inside these windows (a stand-in
for the blocking allocations starving under memory pressure, or any
preemption), all three predicted failure modes reproduced exactly on the
stock INVARIANTS guest:
- SC1 (
panic_sc1.txt): second concurrentregister_int(254, INTR_MPSAFE|INTR_CLOCK, cpu2)observedISTATE_NORMALwith NULLi_threadand executedatomic_set_int(&info->i_thread->td_flags, TDF_CLKTHREAD)(kern_intr.c:349):Stopped at register_int+0x1b9: lock orl $0x20000000,0xd8(%rdx)(write fault at 0xd8 = offsetof(struct thread, td_flags)). - SC2 (
panic_sc2.txt): with the first registrant stalled mid-window and the second's rec already linked, an interrupt arrival (sched_ithd_soft(255)) dereferencedinfo->i_thread->td_gd(kern_intr.c:589):Stopped at sched_ithd_intern+0x1f: movq 0xb8(%rdx),%r8(read fault at 0xb8 = offsetof(struct thread, td_gd)). - SC3 (
panic_sc3.txt): two concurrent first-registrations on cpu2 (the boot-time window reopened by the rig-onlydfh_reset_emergency, because a booted system has already created every CPU's emergency thread) both entered the emergency block; the secondsystimer_init_periodic_nq(&emergency_intr_timer[2], ...)re-initialized an already-armed timer and crashed insidesystimer_add(kern_systimer.c):Stopped at systimer_add+0x15c: movq (%rcx),%rsi(VA 0, thread CRIT). The stockKKASSERT((info->flags & SYSTF_ONQUEUE) == 0)at kern_systimer.c:148 does not save the code because the re-init wipesflagsbefore arming again β the timer subsystem then runs on corrupted state.
Threat model honesty
Reaching the windows requires two concurrent register_int calls for the
same (cpu, intr) β i.e. driver probe/attach races or boot-time
first-registration concurrency β or an interrupt arriving during the window
of a first registration. All are kernel-internal; an unprivileged user
cannot drive register_int directly. Impact = kernel panic (DoS at
driver-attach time); no memory disclosure or corruption control was
demonstrated beyond the crash. Severity Low as filed is right. The SC3
window on a booted system is closed in practice (first boot-time
registration wins permanently); the rig reopens it to expose the defect β
labeled as such.
Fix validation
fix.diff (DF-2777 hunks), applied to a pristine tree, hooks preserved:
- ithread creation claimed by
atomic_cmpset_int(&info->i_state, ISTATE_NOTHREAD, ISTATE_CREATING), thread built into a local,info->i_threadpublished BEFOREi_state = ISTATE_NORMAL; if (intr_flags & INTR_CLOCK)NULL-guardsinfo->i_thread(the creator sets the flag itself after publishing);sched_ithd_interntreatsi_thread == NULLas a rate-limited stray;- emergency block gated by a once-only cmpset.
On the rebuilt fixed kernel (build.fixed.log.gz) the exact same scenarios
all survive with the same orchestration:
DF2777-SC1: r1=0xfffff8008daa43e0 r2=0xfffff8008daa43a8 -- survived, no
NULL-deref; sched_ithd: stray interrupt 255 on cpu3 + SC2 ... survived;
DF2777-SC3: ... survived, no double-arm (df2777_fixed.dmesg).
Kernel references
- sys/kern/kern_intr.c:308-319 (emergency check-then-create)
- sys/kern/kern_intr.c:325-337 (state published before i_thread, blocking window)
- sys/kern/kern_intr.c:348-350 (TDF_CLKTHREAD deref of possibly-NULL i_thread)
- sys/kern/kern_intr.c:582-602 (sched_ithd_intern i_thread->td_gd deref)
- sys/kern/kern_systimer.c:144-148 (systimer_add / SYSTF_ONQUEUE assert)
Rig honesty notes
- Hooks are pure no-ops unless a scenario arms them (single flag test); boot-time and driver registration paths were unaffected in 3 booted harness kernels.
- The stalls substitute for real blocking/preemption inside the windows (M_INTWAIT kmalloc under memory pressure, scheduler preemption); they do not create windows that do not exist in the stock code β the hooked lines are exactly the window boundaries the finding cites.
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
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['panic_sc1.txt: Stopped at register_int+0x1b9 lock orl 0x20000000,0xd8 rdx -- TDF_CLKTHREAD store through NULL i_thread', 'panic_sc2.txt: DF2777-SC2 calling sched_ithd_soft(255) ... Stopped at sched_ithd_intern+0x1f movq 0xb8 rdx -- td_gd read through NULL i_thread', 'panic_sc3.txt: Stopped at systimer_add+0x15c movq rcx rsi (VA 0, CRIT) -- double concurrent systimer_init_periodic_nq', 'df2777_fixed.dmesg: SC1/SC2/SC3 survived on the fixed kernel (stray report for SC2)', 'run.log: full stock and fixed A/B transcript', 'src/kern_intr.hooks_stock.diffside.c and src/kern_intr.pristine.c: exact hook placement vs stock']
PoC changes
Runtime kldload GPFs on this guest, so the interposition is compiled into kern_intr.c as two dormant hook calls (df2777_hook) placed exactly at the window boundaries, plus a rig-only dfh_reset_emergency helper for SC3 (boot-time window reopen, labeled as rig). The stall hooks are no-ops unless armed via sysctl dfh.
Verified recommended fix
Gate ithread creation with atomic_cmpset into a new ISTATE_CREATING state and publish i_thread before i_state; NULL-guard the INTR_CLOCK flag store and treat NULL i_thread in sched_ithd_intern as a stray; gate the emergency-thread block with a once-only cmpset (see fix.diff).
Verdict
Reproduced on the stock INVARIANTS guest with dormant stall hooks compiled into kern_intr.c at the exact window boundaries the finding cites (after info->i_state = ISTATE_NORMAL at kern_intr.c:326 before the i_thread allocation/creation, and at the emergency_intr_thread NULL check at :308). SC1: a second concurrent register_int(254, INTR_MPSAFE|INTR_CLOCK, cpu2) observed ISTATE_NORMAL with a NULL i_thread and faulted writing TDF_CLKTHREAD at kern_intr.c:349 (Stopped at register_int+0x1b9 lock orl 0x20000000,0xd8 rdx; VA 0xd8 = offsetof td_flags). SC2: with the first registrant stalled mid-window and the second rec linked, sched_ithd_soft(255) faulted reading info->i_thread->td_gd at kern_intr.c:589 (Stopped at sched_ithd_intern+0x1f movq 0xb8 rdx; VA 0xb8). SC3: two concurrent first-registrations on cpu2 (rig reopened the boot-time emergency window via dfh_reset_emergency, since a booted system has already created each cpu emergency thread) both ran systimer_init_periodic_nq on emergency_intr_timer[2]; the second re-init of an armed timer crashed inside systimer_add (Stopped at systimer_add+0x15c, VA 0, thread CRIT; the kern_systimer.c:148 SYSTF_ONQUEUE assert is bypassed because re-init wipes flags). Threat model: all triggers are kernel-internal (driver probe/attach races, boot-time first-registration concurrency, interrupt arrival during first registration); unprivileged users cannot drive register_int, so the ceiling is a kernel panic (DoS) at attach/boot time; severity Low as filed. Fix validated on a rebuilt kernel with the same orchestration: SC1 and SC3 print non-NULL results and survive, SC2 degrades to a rate-limited stray-interrupt report, no panics.
No comments yet.