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

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)

No comments yet.

PoC verification

Evidence pack

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

  1. "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_spending are (a) the DO_SETBITS functions themselves, which resolve gd = mdcpu β€” always the calling CPU's private struct (ipl_funcs.c:48-49) β€” and (b) the same-CPU btrl clears in ipl.s:275 / ipl.s:431 (doreti_soft/splz_soft, via PCPU(spending)). No foreign-CPU writer exists anywhere in sys/.
  2. "setbits uses a locked or, making simple cases MP safe" (ipl_funcs.c:42) β€” FALSE: :49 emits atomic_set_int_nonlocked = an unlocked orl (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.

VERDICT.md
↓ download raw

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_testable
baseline reproduced→ patch + rebuild →patched clean

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

Confirmed kernel references

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.