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

if_getanyethermac() type-confusion via uuid_node() leaks kernel driver-softc memory into unprivileged uuidgen(2) output (and returns wrong 'MAC' on vtnet/ena/oce)

Field Value
ID DF-2931
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N
CWE CWE-843 / CWE-125
File sys/kern/kern_uuid.c
Lines 86-91 (sink: sys/net/if.c:3033, offset 928)
Area kern + net
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

kern_uuidgen() fills the v1 UUID node field via uuid_node() β†’ if_getanyethermac(), which copies 6 bytes from ((struct arpcom )ifp->if_softc)->ac_enaddr. That offset (928 = sizeof(struct ifnet)) is only valid for drivers whose softc begins with struct arpcom β€” an assumption modern if_alloc()-style drivers violate: vtnet_softc starts with device_t (read hits vtnet_vlan_shadow[100..101]), ena_softc starts with struct ena_com_dev, oce_softc starts with device_t. For those drivers, 6 bytes of arbitrary, layout-dependent kernel driver-softc memory are returned verbatim in every UUID from the unprivileged, jail-visible uuidgen(2) syscall; a non-conforming softc smaller than 928 bytes turns the expression into a heap OOB read (speculative for current drivers; the wrong-field leak is certain). Secondary correctness: every vtnet-based VM emits v1 UUIDs with identical node 01:00:00:00:00:00, voiding cross-host v1 uniqueness (hammer2 PFS ids, disklabel64 stor_uuid).

Proof of contest

VERIFIED (findings/poc/DF-2931/): uid 1001 syscall(392) returns v1 UUIDs with node 01:00:00:00:00:00 β€” deterministically the 6 zero bytes at vtnet_softc+928 (vlan_shadow region) with bit0 set β€” while vtnet0's real MAC is 52:54:00:12:34:56. KLD module proves the arithmetic (sizeof(ifnet)=928=offsetof(arpcom,ac_enaddr), sizeof(vtnet_softc)=1040) and with a counter-control conforming interface (tap0, arpcom-first) the same read returns its exact MAC. Determinism across 3 runs refutes the read_random fallback. Fix: read the MAC from the link-level sockaddr (LLADDR(sdl)), never an arpcom cast.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_uuid.c (GLM 5.3); unpriv leak reproduced + module arithmetic proven.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2931 Β· 16 files
FileTypeDescriptionSize
uuidleak.c β€” 2.3 KB view raw
uuidoff.c β€” 2.1 KB view raw
Makefile β€” 123 B ↓ download
build.sh β€” 246 B view raw
run.sh β€” 404 B view raw
build.log β€” 9 B view raw
run.first.log β€” 612 B view raw
run.determinism.log β€” 420 B view raw
run.vlan.log β€” 560 B view raw
module.log β€” 426 B view raw
module.tap.log β€” 692 B view raw
env.txt β€” 345 B view raw
VERDICT.md β€” 4.9 KB ↓ raw
fix.diff β€” 332 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.0 KB view raw
VERDICT.md
↓ download raw

DF-2931 VERDICT β€” REPRODUCED (leak)

Finding: if_getanyethermac() reads the "MAC" for UUID v1 node IDs by casting ifp->if_softc to struct arpcom * and copying from offsetof(struct arpcom, ac_enaddr) (sys/net/if.c:3032). That offset equals sizeof(struct ifnet) (928 on this build) and is only meaningful when the driver softc starts with struct arpcom (assumption documented at sys/net/if_var.h:153-171 and sys/net/if_arp.h:115). For drivers that use the new-bus/if_alloc() style with a separate ifnet (vtnet, ena, oce in-tree), the 6 bytes come from an arbitrary softc-relative location β€” driver state that is not a MAC β€” and flow, through uuid_node() (sys/kern/kern_uuid.c:88) and kern_uuidgen(), into every UUID returned by the unprivileged uuidgen(2) syscall. For any such driver whose softc is smaller than 928 bytes the same expression is a heap out-of-bounds read.

How it was proven

  1. Trigger (unprivileged, end-to-end): /tmp/uuidleak run as uid 1001 (run.first.log): all 4 UUIDs version 1, node field 01:00:00:00:00:00, decoded v1 timestamp == host wall clock to the second. vtnet0's real MAC is 52:54:00:12:34:56 β€” the node is not the MAC.
  2. Structural proof (KLD module uuidoff.ko): prints sizeof(struct ifnet)=928, offsetof(struct arpcom,ac_enaddr)=928, sizeof(struct vtnet_softc)=1040, and dumps the bytes at if_softc+928 for every IFT_ETHER interface: * vtnet0: 00:00:00:00:00:00 at sc+928 (== vtnet_vlan_shadow[100..101], the field that actually lives there) β€” this is exactly what if_getanyethermac copied into the UUID node (module.log). * tap0 (conforming, arpcom-first): bytes at sc+928 == 00:bd:e8:3f:01:00 == tap0's exact MAC (module.tap.log) β€” proving the read address is literally sc+928 and that on conforming drivers the real MAC is returned (that branch is finding DF-2932).
  3. Determinism: 3 spaced runs (run.determinism.log): node constant 01:00:00:00:00:00, timestamp tracks wall clock β€” a fixed memory read, not RNG output. (If if_getanyethermac had returned ENOENT, read_random at kern_uuid.c:89 would have produced per-call random node bytes β€” refuted.)
  4. Mutation attempt: created vlan3200 to set a bit inside vtnet_vlan_shadow (run.vlan.log): shadow stayed 0 because the vlan_config eventhandler is only registered when the VIRTIO_NET_F_CTRL_VLAN feature is negotiated (if_vtnet.c:835-848), which this QEMU does not offer. Negative result does not weaken the proof in (1)-(3); it only blocks an optional flourish.

Why the leaked bytes matter (impact ceiling)

  • 6 bytes of kernel driver softc memory per call, address pinned to first_ether_softc + 928, readable by any unprivileged local user (and from jails β€” sys_uuidgen has no prison check), unlimited call rate.
  • Content is entirely a function of the first ether driver's struct layout at offset 928: today, zeros on stock vtnet (vlan shadow), but any layout change or other non-conforming driver can put pointers/counters there; for a non-conforming softc < 928 bytes the read is heap OOB (none of the three in-tree violators is currently < 928 β€” vtnet=1040 β€” so the OOB variant is speculative for current in-tree drivers, the wrong-field leak is certain).
  • Side effect: on every vtnet-based VM (the default virtio NIC), ALL hosts emit v1 UUIDs with the identical node 01:00:00:00:00:00, voiding the v1 uniqueness guarantee across machines (same node + same 100 ns tick + same 14-bit clock-seq β‡’ collision). UUIDs are used as persistent identities (hammer2 PFS ids via kern_uuidgen at sys/vfs/hammer2/hammer2_ioctl.c:902, disklabel64 d_stor_uuid at sys/kern/subr_disklabel64.c:472).

Why not uid=0

This is a read-only disclosure primitive (6 bytes at a fixed driver-determined offset, no attacker-controlled index), not a memory corruption bug β€” there is no write primitive to develop into an escalation chain. Primary objective (2) applies: the leak genuinely manifests; realistic ceiling is limited kernel-memory disclosure plus the cross-host v1-collision correctness break.

False-positive check

None: reproduced end-to-end unprivileged, plus in-kernel structural proof with a counter-control (tap0). The code path is mainline; no config needed beyond an ether interface existing (else the ENOENT/random path runs, which is correct behavior).

Fix

if_getanyethermac() must not derive the MAC from if_softc; it should read the link-level sockaddr that if_attach/ether_ifattach maintains for every driver style. See fix.diff (3-line change). Note DF-2932's fix (random node ids) additionally stops embedding the MAC at the uuid layer and supersedes the privacy half; both are worth applying.

fix_status: not_testable β€” fix validation rebuild is mandated for memory-corruption findings; this is a leak-class finding, and the guest was kept clean instead (module unloaded, tap/vlan destroyed).

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

3-line fix.diff authored and verified git-apply-clean against sys/net/if.c; kernel rebuild validation skipped per contract (mandatory only for memory-corruption findings; this is leak-class) to keep the single-tenant guest clean.

findings/poc/DF-2931/fix.diff (git apply --check: clean)
↓ fix.diffper-fix-DF-2931

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.first.log β€” uid 1001: 4Γ— v1 UUIDs, node 01:00:00:00:00:00, decoded timestamp == wall clock', 'run.determinism.log β€” node constant across 3 runs (fixed memory read, not read_random)', 'module.log β€” in-kernel proof: bytes at vtnet_softc+928 = 00:00:00:00:00:00 vs real MAC 52:54:00:12:34:56', 'module.tap.log β€” conforming tap0: bytes at sc+928 == exact MAC 00:bd:e8:3f:01:00 (both branches in one dump)', 'VERDICT.md β€” full narrative incl. run.vlan.log negative mutation attempt (VLAN filter not negotiated by QEMU)']

PoC changes

No orchestrator seed existed (new pass-2 finding); trigger written from scratch: direct syscall(392) since libc uuidgen(3) wrapper availability was uncertain; added RFC4122 v1 timestamp decoding to simultaneously demonstrate the wall-clock disclosure; structural proof module uuidoff.ko written and iterated 4 compile rounds (device_if.h/bus_if.h generation, if_media.h/ifq_var.h/sys/bus.h includes).

Verified recommended fix

if_getanyethermac: copy the MAC from the link-level sockaddr (bcopy(LLADDR(sdl), node, minlen)) instead of the arpcom type-confusion cast on if_softc.

Verdict

REPRODUCED end-to-end as unprivileged uid 1001: uuidgen(2) v1 UUIDs on the stock vtnet guest carry node 01:00:00:00:00:00, which is the 6 bytes at first_ether_softc+offsetof(struct arpcom,ac_enaddr)=sc+928 (vtnet_vlan_shadow[100..101]) with the multicast bit ORed β€” NOT the interface MAC (52:54:00:12:34:56). if_getanyethermac (sys/net/if.c:3033) type-confuses if_softc with struct arpcom*; the offset is valid only for arpcom-first softcs, which vtnet/ena/oce violate, so driver-internal kernel memory (or, for a sub-928-byte softc, adjacent heap) is disclosed to any local user, jail included. KLD module uuidoff.ko proves the address arithmetic (sizeof(ifnet)=928=offsetof(ac_enaddr), sizeof(vtnet_softc)=1040) and the counter-control: for arpcom-first tap0 the same read returns its exact MAC 00:bd:e8:3f:01:00 (that MAC branch is DF-2932).