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)
PoC verification
Evidence pack
findings/poc/DF-2931 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- Trigger (unprivileged, end-to-end):
/tmp/uuidleakrun as uid 1001 (run.first.log): all 4 UUIDs version 1, node field01:00:00:00:00:00, decoded v1 timestamp == host wall clock to the second. vtnet0's real MAC is52:54:00:12:34:56β the node is not the MAC. - 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 atif_softc+928for every IFT_ETHER interface: * vtnet0:00:00:00:00:00:00atsc+928(==vtnet_vlan_shadow[100..101], the field that actually lives there) β this is exactly whatif_getanyethermaccopied into the UUID node (module.log). * tap0 (conforming, arpcom-first): bytes atsc+928==00:bd:e8:3f:01:00== tap0's exact MAC (module.tap.log) β proving the read address is literallysc+928and that on conforming drivers the real MAC is returned (that branch is finding DF-2932). - 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. (Ifif_getanyethermachad returned ENOENT,read_randomat kern_uuid.c:89 would have produced per-call random node bytes β refuted.) - Mutation attempt: created
vlan3200to set a bit insidevtnet_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_uuidgenhas 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_testable3-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)
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).
No comments yet.