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)
PoC verification
Evidence pack
findings/poc/DF-0547 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_freecalled N times with the same index β bitmap bit set N times (idempotent in current code),objfreeinflated 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:
-
netmap source does not compile against this kernel. Building it via
cd /usr/src/sys/net/netmap && makefails atdragonfly/net/netmap/netmap_kern.h:747with: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'sstruct ifnet(insys/net/if_var.h) only hasif_unused2(:370) andif_unused4(:412); the netmap code referencesif_unused7, which doesn't exist. The netmap source is stale relative to the currentstruct ifnetlayout β netmap has not been kept in sync with ifnet's evolution. -
netmap is not shipped. Even if it compiled,
/boot/kernel/contains nonetmap.ko, and the defaultX86_64_GENERICconfig does not includeoptions NETMAPor 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:
- Open
/dev/netmap, register a NIC (or VALE port). - mmap the rings (this gives shared-memory access to slot[].buf_idx).
- Rewrite
slot[i].buf_idxto: 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. - Close the netmap fd β
netmap_dtor_lockedβnetmap_mem_rings_deletetriggers 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):
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:
- Add
uint32_t *nkr_buf_idxtostruct netmap_kring(alongside the existingnkr_leases). - Populate it in
netmap_mem_rings_createalongsidering->slot[i].buf_idx. - Use it in
netmap_mem_rings_deleteinstead of the userspace- writablering->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_unused7build 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.diffis a minimal first-layer defense against the most obvious attack (double- free / inflation) and does not close the cross-adapter case.
Fix verification
not_testablecompile 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).
No comments yet.