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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2777 Β· 16 files
FileTypeDescriptionSize
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:

  1. info->i_state = ISTATE_NORMAL (:326) is published BEFORE info->i_thread is allocated (:327, M_INTWAIT kmalloc β€” can block) and created via lwkt_create (:329). A second concurrent register_int for the same (cpu, intr) observes ISTATE_NORMAL with a NULL i_thread and either writes &info->i_thread->td_flags at :349 (INTR_CLOCK path) or leaves the slot ready to blow up in sched_ithd_intern (:589, info->i_thread->td_gd) when an interrupt arrives.
  2. The emergency-thread block (:308-319) checks emergency_intr_thread[cpuid] == NULL and then spans lwkt_create + systimer_init_periodic_nq; two concurrent first-registrations on a CPU both create a thread and both (re)initialize emergency_intr_timer[cpuid] β€” the second systimer_init on an already-armed timer corrupts timer state (crash inside systimer_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:

  1. two concurrent register_int(254, INTR_MPSAFE|INTR_CLOCK, cpu2);
  2. first registrant stalled mid-window, second completes, then sched_ithd_soft(255) (interrupt arrival);
  3. dfh_reset_emergency(2) + two concurrent register_int(254, cpu2).

Expected / observed

  • SC1 stock: Stopped at register_int+0x1b9: lock orl $0x20000000,0xd8(%rdx) β€” the atomic_set_int(&info->i_thread->td_flags, TDF_CLKTHREAD) at :349 with NULL i_thread (fault VA 0xd8 = offsetof(td_flags)).
  • SC2 stock: Stopped at sched_ithd_intern+0x1f: movq 0xb8(%rdx),%r8 β€” info->i_thread->td_gd at :589 with NULL i_thread (VA 0xb8).
  • SC3 stock: Stopped at systimer_add+0x15c: movq (%rcx),%rsi (VA 0, thread in CRIT) β€” second concurrent systimer_init_periodic_nq re-initialized an armed timer and crashed inside systimer_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-limited sched_ithd: stray interrupt 255 report) β€” 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).

VERDICT.md
↓ download raw

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 concurrent register_int(254, INTR_MPSAFE|INTR_CLOCK, cpu2) observed ISTATE_NORMAL with NULL i_thread and executed atomic_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)) dereferenced info->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-only dfh_reset_emergency, because a booted system has already created every CPU's emergency thread) both entered the emergency block; the second systimer_init_periodic_nq(&emergency_intr_timer[2], ...) re-initialized an already-armed timer and crashed inside systimer_add (kern_systimer.c): Stopped at systimer_add+0x15c: movq (%rcx),%rsi (VA 0, thread CRIT). The stock KKASSERT((info->flags & SYSTF_ONQUEUE) == 0) at kern_systimer.c:148 does not save the code because the re-init wipes flags before 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_thread published BEFORE i_state = ISTATE_NORMAL;
  • if (intr_flags & INTR_CLOCK) NULL-guards info->i_thread (the creator sets the flag itself after publishing);
  • sched_ithd_intern treats i_thread == NULL as 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

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_testable
↓ fix.diffper-fix-DF-2777

Confirmed 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.