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