# DF-3481 VERDICT — v1 ng_ppp cross-context private-state race

**Status: not_reproduced** (structural defect certain by inspection; harness
races contained by undocumented stock-topology funnels)

## Root cause (certain, path:line)
`sys/netgraph/ppp/ng_ppp.c` has zero synchronization for `priv` state:
grep for lock/token/spin/serializer in the file returns nothing but the
timer's `crit_enter` (ng_ppp.c:1453), which is per-CPU only.  Unlocked
shared mutators include:
- `ng_ppp_mp_input` insert (ng_ppp.c:1144-1159), `ng_ppp_frag_process`
  deliveries/cap-drops (:1290-1350), `ng_ppp_get_packet` removes/kfrees
  (:1210-1234), `ng_ppp_frag_trim` (:1241-1285), `ng_ppp_frag_checkstale`
  junk-removal+delivery (:1364-1442), `ng_ppp_frag_reset` free-all
  (:1999-2012), `ng_ppp_update` config rewrite (:1876-1947),
  `ng_ppp_rmnode` bzero+free (:791-809).
Structurally concurrent contexts reaching them in v1 (no per-node funnel,
unlike netgraph7's cpu0 item funnel that made DF-0330 "latent"):
- ngintr queued data — holds the BGL (ng_base.c:2034)
- fragTimer callout — non-MPSAFE `callout_init` => runs under the BGL
  (kern_timeout.c:580-584); armed on the configuring CPU's softclock thread
- AF_NETGRAPH pru handlers (ngc/ngd) — cpu0 netisr thread, no BGL
  (uipc_socket.c:259 so->so_port = netisr_cpuport(0))
- ng_ksocket inbound upcall — inline in `sowakeup` (uipc_socket2.c:604) in
  the udp_input netisr context
- ng_iface output — sending process's thread under only the per-interface
  serializer (if_ng.c ng_iface_output), NO BGL, NO netisr — the one truly
  unfuneled context, and the one that makes DF-0330's ng7 twin REAL in v1.

## What was attempted (all on the stock INVARIANTS guest, root harness)
1. **Timer-vs-data stale-batch race** (`pppfragrace.c`): two-link mseq-sticker
   parking ([J middle-frag pin + contiguous packets]), 2.05s staleness
   staging, dup-seq full-queue reverse-walk hammer (walks without inserting,
   so only the timer delivers), 8 staggered bundles x 200 cycles.  No panic.
   (Note: the staging had to work around DF-0386's scanner desync — gapped
   batches are invisible to ng_ppp_frag_checkstale; contiguity-from-head is
   required, which the pack's final design satisfies.)
2. **SHUTDOWN vs in-flight data** (`pppteardown.c` mode 0): 400+300 attempts,
   hammer at ~700k deliveries/s across 6 CPU-hashed pump flows.  No panic.
3. **SET_CONFIG-toggle frag_reset vs in-flight data** (mode 1): 2000+4000
   attempts x ~5k deliveries per attempt window.  No panic.

## Why it did not reproduce (the interesting negative result)
- `ng_ppp_rcvdata` reached via ng_ksocket: delivery is serialized through
  the ksocket's UDP socket — `so_pru_soreceive` in `ng_ksocket_incoming`
  DOMSGs to the socket's port, and connected-socket packet processing is
  port-forwarded to the socket's protocol thread (cpu0 for sockets created
  from netgraph control messages).  The SHUTDOWN path additionally
  synchronizes via the soclose/token handshake before `ng_cutlinks`.
- The fragTimer holds the BGL, as does ngintr — those two never race each
  other; they only race non-BGL contexts, and every non-BGL *inbound* feeder
  I could build is socket-funneled as above.
- The one unfuneled feeder (ng_iface output) feeds the *transmit* side; it
  exercises mp_output/strategy (DF-3482) — and the iface node types
  themselves panic on creation on this stock kernel (see DF-3482 pack),
  blocking a hybrid topology.

Conclusion: on stock single-box topologies with socket-terminated links,
v1 ng_ppp execution is effectively serialized by the socket-port funnel
(mirroring netgraph7's cpu0 funnel that made DF-0330 latent).  Deployments
whose inbound feeds do NOT pass through a connected ksocket (ng_ether-fed
PPPoE bundles on multi-CPU input hashing, ng_tty/ng_async-fed links via
ngintr, driver-direct feeds) still expose genuinely concurrent mutators,
and the code remains one refactor away from racing (e.g. MPSAFE soreceive,
per-socket port selection, or any new feeder).

## Primitive if it raced (characterization)
Concurrent `ng_ppp_get_packet`/`frag_reset`/insert on the same TAILQ gives:
double `TAILQ_REMOVE` of a `struct ng_ppp_frag` (~56B, M_NETGRAPH kmalloc)
=> double `kfree` + double `m_freem` of the fragment mbufs, plus writes of
list pointers through freed chunks — classic reclaim-based heap corruption.
Attacker-controlled content is high (seq numbers order the queue; payloads
are arbitrary), but on this guest no collision was achieved, so the
exploitation chain remains theoretical.  NOT pushed to uid=0 (no primitive
demonstrated).

## Fix (authored, compiled -Werror, smoke-tested; not race-validated since
the race never fires on stock topologies)
`fix.diff`: per-node `struct lwkt_token` in `ng_ppp_private`, acquired at
every entry point (rcvmsg / newhook / rcvdata / rmnode / disconnect /
frag_timeout).  Regression: 1000 config-toggle attempts + 160 frag cycles
on the patched module completed cleanly with the data path functional
(fix_regression.log).

## Side discovery (out of file scope — for the orchestrator)
`ngiface`/`ng_eiface` node creation panics the stock kernel deterministically:
`panic: trying to free NULL pointer` in `ng_iface_constructor+0x3a2` /
`ng_eiface_constructor+0xf6` from a plain root `ngctl mkpeer . iface i0 inet`
on a fresh boot (logs in the DF-3482 pack).  Both hand-rolled-ifnet node
types are crash-on-sight; that is a separate finding for
sys/netgraph/iface/ng_iface.c and sys/netgraph/eiface/ng_eiface.c.

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