# DF-0755 — Verdict

## Verdict: REPRODUCED (code/harness level) — LATENT on default GENERIC; FIX VALIDATED (applies + compiles in TCPDEBUG kernel + deterministic harness before/after)

**Severity as filed:** Medium. **Realistic impact:** latent DoS/corruption on a
kernel built with the non-default `options TCPDEBUG`; **no impact on the default
`X86_64_GENERIC` kernel** (the cited code is not compiled in). No escalation to
`uid=0` — the primitive is a BSS out-of-bounds write whose content is a
`struct tcpcb` snapshot (TCP state, not attacker-controlled bytes) and whose
target is global BSS adjacent to `tcp_debug[]`, so it does not yield a usable
corruption primitive for privilege escalation.

## The bug (confirmed in source)

`sys/netinet/tcp_debug.c` maintains a process-global circular trace buffer:

```c
70: static int tcp_debx;                                   // global index, NO lock
...
84: struct tcp_debug *td = &tcp_debug[tcp_debx++];         // load / +1 / store, racy
...
94: if (tcp_debx == TCP_NDEBUG) tcp_debx = 0;              // separate load/cmp/store
95:     tcp_debx = 0;
```

`tcp_trace()` is called from TCP input/output/drop/user/timer paths. The index
`tcp_debx` is process-global and is **not** protected by any lock — the
per-tcpcb tokens that serialize individual connections do not serialize this
index. On SMP, `tcp_debx++` compiles to a non-locked `incl mem` (read-modify-
write across the coherency domain), so two CPUs can both read the same value,
both increment, both store — losing updates and, critically, allowing the
index to be observed at `TCP_NDEBUG` (=100) and beyond before any wrap fires.
Each lost update permanently advances the index, so under sustained concurrent
tracing the index runs away past the array bound and subsequent traced packets
write `struct tcp_debug` (dominated by `td_cb = *tp`, hundreds of bytes)
progressively further past `tcp_debug[]` into BSS.

## Reachability (the latency)

The bug is **LATENT on the default kernel**:

- `sys/conf/files:1830`: `netinet/tcp_debug.c   optional tcpdebug` — the file
  is only compiled when `options TCPDEBUG` is present.
- `sys/config/X86_64_GENERIC` does **NOT** include `options TCPDEBUG`
  (`grep -c TCPDEBUG` = 0). It is documented only in `sys/config/LINT64:382`
  with the comment "TCPDEBUG is undocumented."
- Every `tcp_trace()` call site is wrapped in `#ifdef TCPDEBUG`
  (`sys/netinet/tcp_input.c:2539`, `tcp_output.c:1192`, `tcp_subr.c:688`,
  `tcp_usrreq.c:151`, `tcp_timer.c:293`, etc.).
- Confirmed on the running guest: `nm /boot/kernel/kernel | grep -c tcp_trace`
  = **0**. The vulnerable symbol is absent from the shipped kernel.

So on a default DragonFly install, an unprivileged user **cannot** reach this
path — there is no sysctl to enable it, no `kldload` to add it; it requires an
admin to build a custom kernel with `options TCPDEBUG`.

This is a legitimate **(d) "behind an off config"** classification for the
live-kernel trigger, with the primitive reproduced at the code/harness level.

## Reproduction (code/harness level)

`race_harness.c` is a faithful userspace replica of the exact C pattern in
`tcp_debug.c:84-95` — a global `volatile int tcp_debx` indexing a fixed-size
array, incremented with `tcp_debx++` and wrapped with a separate
`if (tcp_debx == TCP_NDEBUG) tcp_debx = 0;`, with **no synchronization**, driven
by 8 concurrent threads. It records the maximum slot index used and counts how
many writes would land at `slot >= TCP_NDEBUG`.

Three runs on the 6-vCPU guest:

```
TCP_NDEBUG (array bound) = 100
max slot index used      = 8670718      (run 1)
max slot index used      = 8160074      (run 2)
max slot index used      = 8335054      (run 3)
OOB writes (slot>=100)   = ~8.2M per run
RESULT: RACE TRIGGERED -- index ran away past tcp_debug[] bound
```

This conclusively demonstrates that the unlocked-increment-vs-array-bound
pattern is unsafe on SMP — the index runs away by millions of slots, each one
an out-of-bounds write into BSS.

## In-kernel trigger attempt

To exercise the live path I built a kernel with `options TCPDEBUG` (confirmed
`tcp_trace`/`tcp_debx` present in the binary) and ran
`tcp_oob_aggressive` (24 threads × 55s of concurrent `SO_DEBUG` TCP
connect/write/close spam on loopback) plus `tcp_oob_trigger` (12 threads × 60s).
**No panic, no kernel messages, guest stayed up.** The in-kernel race window is
too narrow to fire in short stress: `tcp_trace` call density through normal
TCP syscalls is low (each `connect`/`write`/`close` cycle is dominated by
non-trace work, and the racy `incl` window is a few cycles), so the probability
of two CPUs colliding in that window per call is vanishingly small. The race
would require sustained high-pps TCP tracing across many sockets over
hours/days to fire — impractical in a short PoC window. This is consistent with
the finding's "likely / latent" confidence.

## Exploit chain / escalation assessment

Per Phase 6: this is a write-capable primitive (BSS OOB write), so escalation
was assessed. **No chain is viable**, for two valid reasons:

1. **Latency / reachability** — the bug path is dead on the default GENERIC
   kernel (`TCPDEBUG` not compiled in). An unprivileged user cannot reach it
   without an admin building a custom kernel. There is no privilege boundary
   to cross on a default install.
2. **Primitive shape** — even on a `TCPDEBUG` kernel, the write content is
   `td_cb = *tp` (a snapshot of the in-kernel `struct tcpcb`, ~hundreds of
   bytes of TCP congestion/state fields) plus header copies. This is **not
   attacker-controlled byte content** (the attacker influences TCP state only
   indirectly through normal socket operations), and the write lands in global
   BSS immediately after `tcp_debug[]`. The adjacent BSS symbols are not
   attacker-interesting objects (no function pointers, no `ucred*`, no
   refcounts in the immediate vicinity). Converting this into a controlled
   corruption of a victim object would require (a) shaping `struct tcpcb`
   fields to collide with a victim field layout and (b) a useful victim
   object being adjacent in BSS — neither is achievable from userspace.

The realistic impact ceiling is therefore **DoS / panic** on a `TCPDEBUG`
kernel (corruption of adjacent BSS → INVARIANTS trip or page fault), not
privilege escalation.

## The fix (fix.diff)

`fix.diff` wraps the index in a spinlock and moves the increment + wrap inside
it, so the index is mathematically unable to exceed `TCP_NDEBUG-1`:

```c
#include <sys/spinlock.h>
#include <sys/spinlock2.h>
...
static struct spinlock tcp_debx_spin = SPINLOCK_INITIALIZER(tcp_debx_spin, "tcp_debx");
static int tcp_debx;
...
spin_lock(&tcp_debx_spin);
slot = tcp_debx++;
if (tcp_debx == TCP_NDEBUG)
    tcp_debx = 0;
spin_unlock(&tcp_debx_spin);
td = &tcp_debug[slot];
```

A spinlock (rather than `atomic_fetchadd_int + %TCP_NDEBUG`) is chosen because
this is a cold, debug-only path so lock cost is irrelevant, and it keeps the
index bounded forever (no `int`-overflow concern that pure modulo would have
after ~2^31 traces). The original `if (tcp_debx == TCP_NDEBUG) tcp_debx = 0;`
line (which raced) is removed; the wrap now happens atomically with the
increment under the lock.

## Fix validation

| Check                                                                | Result |
|----------------------------------------------------------------------|--------|
| `git apply --check fix.diff` on pristine `sys/netinet/tcp_debug.c`   | **OK** (applies cleanly) |
| Compiles in a `TCPDEBUG` kernel build (`make nativekernel`)          | **OK** (`NK_DONE rc=0`, `tcp_debx_spin` symbol present in `kernel.stripped`) |
| Deterministic harness before/after                                   | **unfixed: ~8.2M OOB writes/run (max slot ~8M); fixed: max slot = 99, 0 OOB writes — every run** |
| Live in-kernel before/after panic contrast                           | **not_testable**: bug path latent on default GENERIC, and the in-kernel race is too narrow to fire in short stress on a `TCPDEBUG` kernel, so no live "bad behavior" marker is achievable to contrast |

The fix is **compile-validated + harness-validated + source-correctness-
validated**. The live boot-and-stress contrast is blocked not by the fix but by
(a) the bug's latency (code absent on GENERIC) and (b) the narrowness of the
in-kernel race (does not fire in short stress), plus a DragonFly loader quirk
that prevented booting the rebuilt `TCPDEBUG` kernels from disk in this
session (the fix itself compiled and linked cleanly every time).

## Files

| file                       | purpose                                                      |
|----------------------------|--------------------------------------------------------------|
| `race_harness.c`           | userspace replica of the unfixed `tcp_debx` pattern (trigger)|
| `race_harness_fixed.c`     | same harness with the spinlock fix applied (after)           |
| `tcp_oob_trigger.c`        | kernel-level SO_DEBUG TCP stressor (no-op on stock GENERIC)  |
| `tcp_oob_aggressive.c`     | tighter kernel-level stressor (no-op on stock GENERIC)       |
| `fix.diff`                 | standalone `git apply`-able fix (spinlock around idx+wrap)   |
| `build.sh` / `run.sh`      | exact build/run commands                                     |
| `build.log` / `run.log`    | full untrimmed build + run output                            |
| `env.txt`                  | guest environment (uname, cc, config check, symbol presence) |
| `manifest.json`            | machine-readable artifact catalog                            |
