# DF-0547 — netmap ring cleanup trusts userspace-writable buf_idx

## Verdict

**NOT REPRODUCED LIVE — bug confirmed in source, but netmap does not
compile against this kernel and is not shipped.** This is a latent bug
in stale code.

## Mechanism (cited path:line, confirmed by trace)

The vulnerable code at `sys/net/netmap/netmap_mem2.c`:

```c
995: void
996: netmap_mem_rings_delete(struct netmap_adapter *na)
997: {
998:     /* last instance, release bufs and rings */
999:     u_int i, lim;
1000:    struct netmap_kring *kring;
1001:    struct netmap_ring *ring;
1002:
1003:    NMA_LOCK(na->nm_mem);
1004:
1005:    for (kring = na->tx_rings; kring != na->tailroom; kring++) {
1006:        ring = kring->ring;
1007:        if (ring == NULL)
1008:            continue;
1009:        lim = kring->nkr_num_slots;
1010:        for (i = 0; i < lim; i++)
1011:            netmap_free_buf(na->nm_mem, ring->slot[i].buf_idx);
1012:    }
...
1016: }
```

`ring` is the userspace-visible `struct netmap_ring *` allocated in
shared memory and mmap'd back to userspace (the whole point of netmap).
`ring->slot[i].buf_idx` is a `uint32_t` that userspace can rewrite at
will — the `NS_BUF_CHANGED` flag (`sys/net/netmap/netmap.h:130`) exists
precisely so userspace can signal the kernel that it changed the slot.

`netmap_free_buf` at `:413-422`:

```c
413: netmap_free_buf(struct netmap_mem_d *nmd, uint32_t i)
414: {
415:     struct netmap_obj_pool *p = &nmd->pools[NETMAP_BUF_POOL];
416:
417:     if (i < 2 || i >= p->objtotal) {
418:         D("Cannot free buf#%d: should be in [2, %d[", i, p->objtotal);
419:         return;
420:     }
421:     netmap_obj_free(p, i);
422: }
```

Only range-checks `[2, objtotal)`. **Does NOT verify the index belongs
to THIS ring.** A malicious user rewriting `slot[i].buf_idx` to:

- **A value owned by another adapter** sharing the global `nm_mem`
  (e.g., two netmap clients on different NICs, or two VALE ports, or
  a netmap pipe pair) → frees a buffer the other adapter is still
  using → **cross-adapter memory aliasing** when the freed slot is
  reallocated.
- **All slots = a single valid index** → `netmap_obj_free` called N
  times with the same index → bitmap bit set N times (idempotent in
  current code), `objfree` inflated by N-1 → objfree desync from
  actual free bitmap → allocator accounting corruption.
- **An index belonging to a buffer that's currently in flight on
  another CPU** → premature free → UAF in the other ring's TX/RX path.

`netmap_obj_free` at `:329-338`:

```c
329: netmap_obj_free(struct netmap_obj_pool *p, uint32_t j)
330: {
331:     if (j >= p->objtotal) { ... return; }
335:     p->bitmap[j / 32] |= (1 << (j % 32));   // <-- sets bit, no double-free check
336:     p->objfree++;                            // <-- bumps counter unconditionally
337: }
```

No double-free detection. Setting an already-set bit is silent;
incrementing objfree past reality corrupts accounting.

The kernel maintains **no per-ring ownership record** of which buffer
indices belong to which ring — `slot[i].buf_idx` is the ONLY record,
and it lives in shared memory writable by userspace.

## Why not testable on this guest

Two independent blockers:

1. **netmap source does not compile against this kernel.** Building it
   via `cd /usr/src/sys/net/netmap && make` fails at
   `dragonfly/net/netmap/netmap_kern.h:747` with:
   ```
   error: 'struct ifnet' has no member named 'if_unused7'; did you mean 'if_unused2'?
   #define WNA(_ifp) (_ifp)->if_unused7 /* XXX better name ;) */
   ```
   The kernel's `struct ifnet` (in `sys/net/if_var.h`) only has
   `if_unused2` (`:370`) and `if_unused4` (`:412`); the netmap code
   references `if_unused7`, which doesn't exist. The netmap source is
   stale relative to the current `struct ifnet` layout — netmap has
   not been kept in sync with ifnet's evolution.

2. **netmap is not shipped.** Even if it compiled, `/boot/kernel/`
   contains no `netmap.ko`, and the default `X86_64_GENERIC` config
   does not include `options NETMAP` or any netmap devices. To use
   netmap, an admin must build the module from source (which currently
   fails per #1).

So the entire netmap subsystem is dead code on this guest: not in the
kernel, not shipped as a module, source doesn't even compile. The bug
is real in the source but unreachable in current state.

## Threat model (latent)

If netmap source were brought up to date with the current `struct
ifnet` (a one-line fix: `if_unused7` → `if_unused4` or similar, plus
possibly more), and the resulting `netmap.ko` loaded, the bug would
become reachable by any unprivileged user with access to `/dev/netmap`
(typically root:operator 0640 — an admin in the `operator` group, or
any user if the admin chmod's it world-writable). The exploit chain:

1. Open `/dev/netmap`, register a NIC (or VALE port).
2. mmap the rings (this gives shared-memory access to slot[].buf_idx).
3. Rewrite `slot[i].buf_idx` to:
   a. An index owned by another adapter → next netmap_mem_rings_delete
      on this ring frees a victim buffer.
   b. All same index → objfree inflation.
4. Close the netmap fd → `netmap_dtor_locked` → `netmap_mem_rings_delete`
   triggers the bad frees.

If the victim buffer was in use by another adapter (case a), the
re-allocation of that buffer to a new client produces a cross-client
shared-memory corruption primitive — attacker can read/write packets
belonging to another process.

This is a memory-corruption primitive, but its development into a
`uid=0` chain requires:
- Multiple netmap clients sharing the global allocator.
- Knowledge of the victim's buffer indices (info leak or brute force).
- A reclamation spray after the premature free.

None of this is testable on this guest because netmap doesn't run.

## Recommended fix (NOT compile-validated)

`fix.diff` adds a defense-in-depth double-free check in
`netmap_obj_free` (`sys/net/netmap/netmap_mem2.c:329`):

```c
if (p->bitmap[j / 32] & (1 << (j % 32))) {
    D("buf %u already free (double-free or cross-ring free?)", j);
    return;
}
```

This catches:
- Rewriting all `slot[].buf_idx` to the same value (the inflation
  attack) — the second free attempt hits an already-set bit and bails.
- Cross-ring free of a buffer that was already torn down by another
  ring's cleanup.

It does NOT catch the cross-adapter free of a buffer that's still
ALLOCATED to another ring (bit clear) — that needs per-ring ownership
tracking as the finding proposal suggests ("maintain kernel-private
array of buffer indices per kring"). A complete fix would:

1. Add `uint32_t *nkr_buf_idx` to `struct netmap_kring` (alongside
   the existing `nkr_leases`).
2. Populate it in `netmap_mem_rings_create` alongside
   `ring->slot[i].buf_idx`.
3. Use it in `netmap_mem_rings_delete` instead of the userspace-
   writable `ring->slot[i].buf_idx`.

That's a more invasive change across netmap_mem2.c and netmap_kern.h;
the minimal double-free guard in `fix.diff` is a first defensive layer.

**Cannot compile-validate** because netmap source doesn't build on this
kernel (the `if_unused7` issue is upstream from the bug and unrelated).
`fix_status: not_testable`.

## Kernel references (verified by source trace)

- `sys/net/netmap/netmap_mem2.c:995-1016`  — vulnerable rings_delete.
- `sys/net/netmap/netmap_mem2.c:1011`      — reads userspace-writable buf_idx.
- `sys/net/netmap/netmap_mem2.c:413-422`   — netmap_free_buf, range check only.
- `sys/net/netmap/netmap_mem2.c:329-338`   — netmap_obj_free, no double-free check.
- `sys/net/netmap/netmap_mem2.c:375-409`   — netmap_new_bufs sets initial buf_idx.
- `sys/net/netmap/netmap_kern.h:747`       — `WNA(_ifp) = if_unused7` (stale).
- `sys/net/if_var.h:370,412`               — only if_unused2 / if_unused4 exist.
- `sys/net/netmap/netmap.h:130`            — `NS_BUF_CHANGED` (userspace can change buf_idx).

## PoC

A live PoC would: open `/dev/netmap`, mmap rings, rewrite slot[].buf_idx
to all-equal or to a victim index, close. Cannot run on this guest.
The included `df0547.c` is a skeleton that documents the trigger but
exits early because `kldload netmap` fails (no module shipped).

## Notes for maintainers

- The `if_unused7` build breakage in netmap should be fixed regardless
  of this finding — it indicates the netmap source has been stale for
  some time and may have other latent issues.
- The complete fix for DF-0547 requires per-ring ownership tracking
  (kernel-private buffer index array per kring); my `fix.diff` is a
  minimal first-layer defense against the most obvious attack (double-
  free / inflation) and does not close the cross-adapter case.
