# DF-0513 — IPv6 firewall chain mutation raced with concurrent ip6_fw_chk

## Verdict: NOT-REPRODUCED at runtime (race window too tight on default GENERIC);
## code-path CONFIRMED real and exercisable from an unprivileged user.

## Mechanism (confirmed at code level)

`ip6_fw_chain` is a global singly-linked list (sys/net/ip6fw/ip6_fw.c:115)
walked **lock-free** by `ip6_fw_chk()` (sys/net/ip6fw/ip6_fw.c:498) on every
IPv6 packet (called from sys/netinet6/ip6_input.c:318 and
sys/netinet6/ip6_output.c:612).  The mutators only take
`crit_enter()/crit_exit()`, which masks interrupts on the *current* CPU
but does NOT serialize against other CPUs running `ip6_fw_chk`:

- `del_entry6` (sys/net/ip6fw/ip6_fw.c:902-923): `LIST_REMOVE` +
  `crit_exit()` + `kfree(fcp->rule)` + `kfree(fcp)` — **kfree happens
  AFTER crit_exit**, so a concurrent walker on another CPU can be
  derefing the rule while it's being freed.
- `ip6_fw_ctl` FLUSH path (sys/net/ip6fw/ip6_fw.c:1126-1135): same
  kfree-after-crit_exit pattern.
- `zero_entry6` (sys/net/ip6fw/ip6_fw.c:944-950) and `IPV6_FW_GET`
  (sys/net/ip6fw/ip6_fw.c:1106): walk the chain with no crit at all.
- The SKIPTO inner loop (sys/net/ip6fw/ip6_fw.c:741-744) keeps walking
  `chain->chain.le_next->rule` across multiple nodes, widening any
  race window against a concurrent delete.

## Secondary observation: NO PRIVILEGE CHECK on the mutator path

`ip6_output.c` cases `IPV6_FW_ADD/DEL/FLUSH/ZERO/GET` (sys/netinet6/
ip6_output.c:1534-1551, 1760-1774) dispatch directly to
`ip6_fw_ctl_ptr` with **no `priv_check` / `caps_priv_check_self`**.
Once the `ip6fw` module is loaded (one-time root setup, realistic for
any IPv6 router), **any unprivileged user can add/del/flush firewall
rules** via `setsockopt(IPPROTO_IPV6, IPV6_FW_*)`.  The PoC exploits
this to drive the race entirely from the `maxx` (uid 1001) account.

## Reproduction attempt (runtime)

PoC `df0513.c` runs two threads as the unprivileged `maxx` user:

- Thread A: opens AF_INET6 SOCK_DGRAM, floods packets to ::1:9999 to
  drive `ip6_output` → `ip6_fw_chk`.
- Thread B: opens AF_INET6 SOCK_DGRAM, alternates
  `setsockopt(IPV6_FW_ADD)` and `setsockopt(IPV6_FW_DEL)` for rule
  numbers 500..599.

On the buggy kernel:

```
DF-0513: racing ip6_fw_chk against chain mutation for 20s
[sender] sent 0 pkts, errors 28325              <- 28k chk calls
[mutator] add=21446 del=21446 err=0 (iters 21446)  <- 21k add+21k del cycles
```

A second run with 4 concurrent adders + 4 senders + 1 flusher for 30 s
drove ~520 k add/del cycles and ~159 k flushes — still no panic.  Guest
stayed up; dmesg showed only the (informational) `len=X, want 200`
messages from earlier probes.

**Why no panic despite the race being exercised millions of times:**
The window between `LIST_REMOVE` and `kfree` is very narrow (a few
instructions), `LIST_REMOVE` only rewrites the *neighbor's* next/prev
pointers (leaving the victim's pointers intact), and the freed slab
memory stays mapped and readable (its old contents remain valid until
reused).  INVARIANTS slab checks (`chunk_mark_free` bitmap / WEIRD_ADDR
poison) only trip on the *next allocation* of the freed chunk, not on a
UAF read by a concurrent walker.  Winning the race reliably would need
either CPU pinning (the guest has no `cpuset` utility) or a tighter
SKIPTO inner-loop pattern — both beyond the scope of a 30 s flood.

**Conclusion: code-path confirmed, race real, runtime panic not
triggered in the test window.**  Medium severity is appropriate (the
race exists and is unprivileged-reachable, but is not a one-shot
reproducible panic).

## Fix

`fix.diff` introduces a `lwkt_token ip6_fw_token` and takes it across:
- the entire `ip6_fw_chk` walk (acquire at entry, release before each
  return),
- `add_entry6`, `del_entry6`, `zero_entry6`, `IPV6_FW_GET`, `IPV6_FW_FLUSH`
  mutators (acquire around the crit section).
- Additionally moves `kfree(rule)` and `kfree(fcp)` INSIDE the
  crit_enter/exit window in `del_entry6` and FLUSH so the free is
  atomic w.r.t. the local CPU and the token-held walker.

`lwkt_token` is the DragonFly idiom for this kind of global state —
it's a non-recursive token that serializes all takers across CPUs and
is automatically released/reacquired on msgport switch, so it's safe
to hold across the (non-sleeping) chain walk.

## Build / run

```
ssh dfbsd 'kldload ip6fw'    # one-time root setup
ssh dfbsd-maxx 'mkdir -p poc/DF-0513'
scp findings/poc/DF-0513/{df0513.c,build.sh,run.sh} dfbsd-maxx:poc/DF-0513/
ssh dfbsd-maxx 'cd poc/DF-0513 && sh ./build.sh && sh ./run.sh 20'
# watch dmesg / dfbsd-qemu/boot.log for panic signature
```
