β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0547

Ring cleanup trusts userspace-writable buf_idx: cross-adapter double-free / buffer aliasing

Summary

netmap_mem_rings_delete(:995-1016): frees each slot via netmap_free_buf(na->nm_mem, ring->slot[i].buf_idx)(:1011). ring is shared-memory mmapd to userspace. slot[i].buf_idx uint32 userspace can overwrite at will(netmap.h:130 NS_BUF_CHANGED). netmap_free_buf(:413-422) only range-checks [2,objtotal) does NOT verify index allocated to THIS ring. Malicious user rewrites buf_idx to: (a) point at buffers owned by another adapter sharing global nm_mem -> premature free -> aliasing when reallocated to different ring -> cross-process shared-memory corruption; (b) all point at one valid index -> inflate objfree desync from bitmap. No kernel per-ring ownership record. Fix: maintain kernel-private array of buffer indices per kring.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0547 Β· 9 files
FileTypeDescriptionSize
df0547.c trigger-source conceptual PoC skeleton (netmap not loadable) 1.9 KB view raw
fix.diff suggested-fix defense-in-depth double-free check in netmap_obj_free (NOT compile-validated; netmap source is stale) 972 B view raw
build.sh build-script cc -o df0547 df0547.c 139 B view raw
run.sh run-script runs conceptual PoC + shows netmap build failure 394 B view raw
build.log build-log netmap.ko build attempt showing the if_unused7 stale-source error 2.4 KB view raw
VERDICT.md verdict full narrative: source trace, why-not-testable, latent threat model 8.5 KB ↓ raw
env.txt environment guest uname, netmap unshipped + non-compiling status 970 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full narrative: source trace, why-not-testable, latent threat model
↓ download raw

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:

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:

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:

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.

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

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)

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.

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. netmap ring cleanup trusts userspace-writable buf_idx. Netmap dead code (if_unused7 build break).