Stale MP-safety comment asserts a locked OR and cross-CPU manipulation of gd_spending where the code emits a nonlocked OR β safety rests on an unstated per-CPU invariant (pc64 twin of DF-5671)
Summary
The comment makes two false claims: gd_spending 'may be manipulated by other cpu's' (exhaustive grep: zero foreign-CPU writers exist - all resolve mdcpu same-CPU; cross-CPU work uses the Xipi handshake instead) and 'setbits uses a locked or' (the code emits atomic_set_int_nonlocked, documented local-interrupt-safe only). Live disassembly of the running 6-CPU guest proves it: every generated setsoft* shows unlocked orl into gd_spending followed by locked orl into gd_reqflags - the split is deliberate and correct, the comment is the defect. No runtime manifestation (verified under real IPI traffic: ipiq_count climbing, system stable); the risk is a maintainer trusting the comment to add a cross-CPU accessor -> silent lost pend bits -> undebuggable taskqueue/tty/cam stalls. DF-5586 class KILLED for pc64 (doreti gates TD_CRITCOUNT; splz self-guards with incl TD_CRITCOUNT). rewrite-the-comment fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5672 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 7.1 KB | β raw | |
| disasm_live.sh | β | 977 B | view raw | |
| run.sh | β | 108 B | view raw | |
| run.log | β | 4.6 KB | view raw | |
| env.txt | β | 183 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-5672 β pc64 twin of DF-5671: stale "locked or" comment vs nonlocked emission in ipl_funcs.c
What to look at
sys/platform/pc64/x86_64/ipl_funcs.c:37-43 (comment) vs :49 (emission), twin of
DF-5671 (sys/platform/vkernel64/platform/ipl_funcs.c:42/49).
The comment block asserts, for gd_spending:
- "spending may be manipulated by interrupts or other cpu's without holding
any locks" (ipl_funcs.c:38-40) β FALSE on pc64: exhaustive writer grep shows
the only RMWers of any CPU's
gd_spendingare (a) theDO_SETBITSfunctions themselves, which resolvegd = mdcpuβ always the calling CPU's private struct (ipl_funcs.c:48-49) β and (b) the same-CPUbtrlclears inipl.s:275/ipl.s:431(doreti_soft/splz_soft, viaPCPU(spending)). No foreign-CPU writer exists anywhere insys/. - "setbits uses a locked or, making simple cases MP safe" (ipl_funcs.c:42)
β FALSE:
:49emitsatomic_set_int_nonlocked= an unlockedorl(sys/cpu/x86_64/include/atomic.h:110-115, instantiated at:128), which the header itself documents as only "local-interrupt-safe", not MP-safe (atomic.h:72-74).
Reproduce (live guest)
./run.sh # disassembles the DO_SETBITS functions in the RUNNING kernel
# + splz critcount self-guard + live softint/IPI counters
Expected (see run.log): every generated pend function shows
orl $<bit>,0x42cc(%rax) # gd_spending β NO lock prefix lock orl $0x2,0x18(%rax) # gd_reqflags RQF_INTPEND β locked
i.e. the spending store is unlocked (contradicting the comment) while only the
reqflags store is locked. The locked/unlocked split is deliberate and correct
under the unstated per-CPU invariant β the defect is the misleading comment,
which invites a future cross-CPU accessor of gd_spending believing the locked-OR
claim, which would produce silent lost-update pend drops (missed softints β
system hangs).
Verdict
Status reproduced (commentβcode divergence demonstrated on the live guest kernel), impact none (documentation defect; no runtime manifestation β the unlocked OR is safe because the per-CPU invariant holds by construction). Severity: Info, confidence certain.
See VERDICT.md for the full narrative including the negative results on the spl0/splx gates, the unpend-loop bit math, and the cross-CPU caller sweep.
DF-5672 VERDICT β pass-2 audit of sys/platform/pc64/x86_64/ipl_funcs.c
Mode: pass-2 audit (GLM 5.3). File: 71 LOC, the pc64 DO_SETBITS pend family
(setdelayed, setsoft{camnet,cambio,tty,vm,tq,crypto}, schedsofttty).
Result: 1 finding (Info, certain β DF-5671's pc64 twin, live-verified). Everything
else on the pass-2 checklist is a documented negative.
The finding (a): stale MP-safety comment vs nonlocked emission
Claim verified two ways β source trace + live disassembly of the running kernel.
Source:
- ipl_funcs.c:37-43 comment: spending "may be manipulated by interrupts or other
cpu's without holding any locks" and "setbits uses a locked or, making simple
cases MP safe".
- ipl_funcs.c:49 emits atomic_set_int_nonlocked(var, bits) β per
sys/cpu/x86_64/include/atomic.h:110-115 that is an inline-asm orl with no
lock prefix (atomic.h:128), documented at atomic.h:72-74 as
"local-interrupt-safe" only, NOT MP-safe.
- The very next line (ipl_funcs.c:50) uses the locked atomic_set_int for
gd_reqflags β so the macro's author clearly knew the difference; the spending
store being nonlocked is deliberate, and the comment is the defect.
Live proof (run.log, guest kernel 6.5-DEVELOPMENT #0, X86_64_GENERIC, 6 CPUs):
setsofttty @ 0xffffffff80c1bf00: mov %gs:0x0,%rax ; rax = mdcpu (CALLING cpu's private area) orl $0x1,0x42cc(%rax) ; gd_spending |= SWI_TTY_PENDING β UNLOCKED lock orl $0x2,0x18(%rax) ; gd_reqflags |= RQF_INTPEND β LOCKED
Identical shape for setdelayed (xchg on 0x42d0 = gd_sdelayed, then unlocked
or %edx,0x42cc(%rax)), setsoftcamnet/cambio/vm/tq, schedsofttty (0x42d0), and
setsoftcrypto (tail-jump to setsoftcamnet β SWI_CRYPTO == SWI_CAMNET,
sys/sys/interrupt.h:81). Offsets match struct mdglobaldata
(sys/platform/pc64/include/globaldata.h:76-77: gd_spending/gd_sdelayed adjacent).
Why the unlocked OR is nonetheless SAFE today (the unstated invariant):
- Every writer of CPU n's gd_spending/gd_sdelayed executes on CPU n:
the DO_SETBITS functions take no argument and resolve mdcpu
(ipl_funcs.c:48) β structurally CPU-local; the unpend clears are
btrl %ecx,PCPU(spending) in doreti_soft/splz_soft
(ipl.s:275, ipl.s:431) β again current-CPU only.
- Same-CPU, single-instruction RMWs (orl, btrl, xchgl) cannot tear against
interrupt handlers: interrupts are taken at instruction boundaries.
- Cross-CPU work never touches a foreign gd_spending: it uses the physical IPI
+ handshake path (lwkt_send_ipiq3: ATOMIC_CPUMASK_ORBIT(target->gd_ipimask)
and atomic_swap_int(&target->gd_npoll,1) + cpu_send_ipiq,
sys/kern/lwkt_ipiq.c:308-315); gd_reqflags bits are set by the target
itself via the need_* macros (sys/cpu/x86_64/include/cpu.h:73-87).
(Corroborating: ipl.s:316/ipl.s:446 clear RQF_IPIQ with a nonlocked
andl β reqflags is de-facto self-CPU too, so the locked store at
ipl_funcs.c:50 is belt-and-braces, not a correctness requirement.)
- Live corroboration: lwkt.ipiq_count climbing (6707β7955 during this session)
on a 6-CPU guest with the softint machinery active (vmstat -i shows
swi_siopoll/swi_vm dispatching) β no instability; invariant holds under load.
Threat: none at runtime today. The risk is the trap the comment sets: a maintainer
relying on "locked or β¦ MP safe" to add a cross-CPU accessor of gd_spending
would get silent lost updates (a set-OR racing the owner's clear-BTR β pend bit
lost β soft interrupt never dispatched β e.g. taskqueue/tty stalls, potential
system hang). Same class/shape as DF-5671 on the vkernel twin
(sys/platform/vkernel64/platform/ipl_funcs.c), which the pc64 file inherited
verbatim from the i386 lineage (FreeBSD $FreeBSD: src/sys/i386/isa/ipl_funcs.c
,v 1.32.2.5 header at ipl_funcs.c:26).
Negative results (checked and killed)
(b) spl0/splx / pending-redispatch gates β no DF-5586-class hole on pc64.
pc64 has no separate C spl0/splx; redispatch happens through:
- doreti β explicitly gated: testl $-1,TD_CRITCOUNT(%rbx); jne 5f
(ipl.s:142-143) before any unpend;
- splz β self-guarding: incl TD_CRITCOUNT at entry (ipl.s:355; live disasm:
incl 0xd4(%rbx) right after loading curthread from %gs:0x8), backing the
crit section out around sched_ithd_soft (ipl.s:437-441);
- splz_check / lwkt_maybe_splz β gated on gd_intr_nesting_level == 0 &&
td_nest_count < 2 / td_critcount == 0 (sys/kern/lwkt_thread.c:1073-1077,
:1095-1099);
- lwkt_yield/lwkt_yield_quick/lwkt_user_yield β nest-guarded
(lwkt_thread.c:1163,1190,1232), spinlock-guarded (:1157,1225);
- call-site splz's (exception.S:714 fork_trampoline, kern/tty.c:1701,
machdep.c halt/boot paths) are all safe because pc64 splz is self-guarding.
The vkernel DF-5586 divergence (ungated splz inside outer handler context) has
no pc64 counterpart.
(c) unpend loop / ffs bit math β clean. doreti_soft/splz_soft:
bsfl %ecx,%ecx; btrl %ecx,PCPU(spending); jnc <next>; addl $FIRST_SOFTINT,%ecx;
call sched_ithd_soft (ipl.s:274-282, ipl.s:430-439). SWI_PENDING bits are
0..6 of a 32-bit word (sys/sys/interrupt.h:78-91), consistent with 32-bit
bsfl/btrl on int gd_spending (globaldata.h:76); intr = 192..198 lands inside
FIRST_SOFTINT=MAX_HARDINTS=192 .. +MAX_SOFTINTS=64 (interrupt.h:66-69). The
btrl+jnc re-check protocol makes the loadβclear window race-safe (worst case
coalescing, which is the softint model). Hardware side: 3Γ64-bit ipending groups
with shlq $6 group math (ipl.s:248-256) β consistent. No OOB, no off-by-one.
(d) cross-CPU callers of setsoft* β invariant's real test, holds.
All 13 call sites run in current-CPU context: kern_clock.c:873 (setdelayed from
hardclock β also proves the schedsofttty deferred pend terminates),
subr_taskqueue.c:532, busdma_machdep.c:1432, cam_xpt.c:4919,
sio.c:1898/1905/1908/1945/1998, usb_serial.c:2039, ng_bt3c_pccard.c:775.
Since the functions are void f(void) resolving mdcpu, pend-on-foreign-CPU is
structurally impossible; remote work uses the IPI path (see above).
Memory-safety classes: no attacker-controlled data reaches this file β the macro family only ORs compile-time constants into per-CPU words. No lengths, no copies, no lifetimes, no privileges. Clean by construction (verified, not assumed).
Bottom line
status=reproduced, impact=none, confidence=certain. The finding is real but
documentation-class: fix the comment to state the actual invariant (per-CPU
ownership; nonlocked OR is safe because each CPU RMWs only its own word;
reqflags store is locked). fix.diff supplied (comment-only β no runtime delta;
fix validation not applicable, fix_status=not_testable).
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_testablefix.diff is comment-only (zero code delta β no instruction changes, verified by inspection of the diff against ipl_funcs.c:37-43). A patched-kernel rebuild+re-run would produce byte-identical disassembly, so there is no observable runtime delta to validate; classification not_testable per the fix-validation vocabulary. Baseline (pre-fix) divergence reproduced live in run.log.
findings/poc/DF-5672/fix.diff (comment-only: no lines outside the /* ... */ block changed); baseline divergence in run.log.
Confirmed kernel references
- sys/platform/pc64/x86_64/ipl_funcs.c:37-43
- sys/platform/pc64/x86_64/ipl_funcs.c:44-51
- sys/platform/pc64/x86_64/ipl_funcs.c:53-67
- sys/cpu/x86_64/include/atomic.h:72-74
- sys/cpu/x86_64/include/atomic.h:110-115
- sys/cpu/x86_64/include/atomic.h:128
- sys/platform/pc64/include/globaldata.h:76-77
- sys/platform/pc64/x86_64/ipl.s:275
- sys/platform/pc64/x86_64/ipl.s:431
- sys/kern/lwkt_ipiq.c:308-315
- sys/cpu/x86_64/include/cpu.h:73-87
- sys/kern/kern_clock.c:873
Detail
Evidence (decisive lines)
findings/poc/DF-5672/run.log β decisive lines: 'setsofttty @ 0xffffffff80c1bf00: orl $0x1,0x42cc(%rax)' (UNLOCKED spending store) immediately followed by 'lock orl $0x2,0x18(%rax)' (locked reqflags store); same shape for setdelayed (xchg 0x42d0 + unlocked or 0x42cc), setsoftcamnet/cambio/vm/tq, schedsofttty (0x42d0), setsoftcrypto (tail-jmp setsoftcamnet per interrupt.h:81); '== splz' block shows incl 0xd4(%rbx) self-guard; counters show live ipiq traffic. VERDICT.md carries the full writer-inventory grep and the negative results for checklist (b)/(c)/(d).
PoC changes
n/a β finding verified by live disassembly of the running guest kernel (objdump/nm on /boot/kernel/kernel) plus source writer-inventory; no compiled trigger needed for a comment-vs-emission divergence.
Verified recommended fix
Rewrite the ipl_funcs.c:37-43 comment to state the real invariant (strictly per-CPU gd_spending/gd_sdelayed; nonlocked OR safe only because each CPU RMWs its own word; do not add cross-CPU accessors) β see fix.diff.
Verdict
pc64 twin of DF-5671, live-verified: the comment at ipl_funcs.c:37-43 claims gd_spending 'may be manipulated by interrupts or other cpu's' and that 'setbits uses a locked or, making simple cases MP safe', but ipl_funcs.c:49 emits atomic_set_int_nonlocked β an UNLOCKED orl (atomic.h:110-115/:128, self-described as local-interrupt-safe only at atomic.h:72-74). Disassembly of the running 6-CPU guest kernel shows every generated pend function as 'orl $bit,0x42cc(%rax)' (no lock prefix) followed by 'lock orl $0x2,0x18(%rax)' β only the gd_reqflags store is locked. Both comment assertions are false: exhaustive grep proves gd_spending/gd_sdelayed are only ever RMW'd by their owning CPU (DO_SETBITS resolves mdcpu at ipl_funcs.c:48; clears are same-CPU btrl at ipl.s:275/431; cross-CPU work goes via Xipi + gd_npoll/gd_ipimask at lwkt_ipiq.c:308-315, never via a foreign gd_spending; live ipiq_count climbing 6707->7955 with the system stable). The unlocked OR is therefore safe by construction today β impact none β but the comment is a trap: anyone adding a cross-CPU accessor trusting the 'locked or / MP safe' claim gets silent lost-update pend drops (missed softints, taskqueue/tty stalls, potential hang). Comment-only defect; documentation fix supplied.
No comments yet.