# DF-3456 — pf anchor walk-stack race → rule bypass (PoC evidence pack)

## What this proves

`pf_anchor_stack[64]` (sys/net/pf/pf.c:148) is a single **global** array used
as the anchor-walk state by `pf_step_into_anchor()`/`pf_step_out_of_anchor()`.
The pf pfil hooks (`pf_check_in/out`, pf_ioctl.c:3199+) hold `pf_token` only
**shared**, and DragonFly lwkt shared tokens allow concurrent holders on
multiple CPUs (sys/kern/lwkt_token.c:726+).  Two CPUs evaluating anchor rules
concurrently therefore write and read **the same frames**, tearing each
other's walk.  A torn frame makes the victim walk resume the rule list at the
*other* walk's position — skipping every rule in between, including
`block drop` rules.

Observable result on the guest: UDP datagrams that pf is configured to
`block drop` **inside an anchor** are delivered to userland, while an
identical block in the main ruleset (control) drops 100%.

## Files

- `anchor_bypass.c` — lo UDP flooders + blocked-port receivers (unprivileged)
- `dns_flood.c` — DNS query workers (unprivileged); their vtnet0-bound UDP
  query output walks are the concurrent walk population that makes the
  lo-output walks tear (see VERDICT.md "attribution")
- `setup_pf.sh` — loads the sandwich ruleset (run as root in the guest)
- `run.sh` — host-side driver (uses dfbsd-qemu/vm.sh)
- `fix.diff` — caller-local anchor walk stacks (verified fix)
- `run.6.log` — REPRODUCED (117 bypassed datagrams + counter forensics)
- `run8_supplement.log` — second reproduction (1 bypassed datagram)
- `env.txt`, `build.log`, `kbuild.log` — environment / build evidence

## Ruleset shape (why it works)

```
pass all no state
anchor "spam" {
    <300 udp decoys>
    anchor "inner1" { pass udp port 9998 }   # walked as spam/spam/inner1
    block drop udp 127.0.0.1 -> 127.0.0.1 port 5555   # sandwiched block
    block drop udp 10.0.2.2 port 53
    anchor "inner2" { pass udp port 9999 }   # walked as spam/spam/inner2
    <300 tail decoys>
}
anchor "ham" { pass udp port 9997 }          # walked as ham
block drop udp 127.0.0.1 -> 127.0.0.1 port 5556    # control (main ruleset)
```

Walk A enters `inner1` (frame[1] = {rs=spam, r=rule-inner1}) and walks its
1000 decoys.  Concurrent walk B, slightly ahead, exits inner1, passes the
block, and enters `inner2` (frame[1] = {rs=spam, r=rule-inner2}).  When A
pops its frame it reads **B's** frame and resumes spam's list at
`TAILQ_NEXT(rule-inner2)` — *past both block rules*.  A's packet is then
decided by `pass all` → delivered.

Two pfctl quirks had to be worked around (documented in VERDICT.md):
1. empty inline anchors are optimized away by pfctl (placeholder rule needed)
2. an inline `anchor "inner1"` inside `anchor "spam"` is walked by the kernel
   as the doubled path `spam/spam/inner1`; the walked decoys are loaded there
   with `pfctl -a spam/spam/inner1 -f`.

## Build & run

On the host, from the repo root:

    sh findings/poc/DF-3456/run.sh 90

The driver copies the pack into the guest, compiles as the unprivileged user
`maxx`, sanity-checks the harness (probe with pf off), loads the ruleset as
root, starts receivers as `maxx`, floods for 90 s, then prints both counters.

## Success criterion

Receiver on 5555 (block inside anchor) reports `> 0 datagrams`, receiver on
5556 (identical block in main ruleset) stays `0`, and per-rule evaluation
counters show the sandwich block lagging the surrounding decoys
(tear/skip signature).
