Case study · libslirp · CWE-125, CWE-787

Two guest-to-host bugs in libslirp

libslirp is the user-mode TCP/IP stack behind QEMU’s default networking, and behind rootless Podman and Docker through slirp4netns. It parses guest traffic inside the host process. Two bugs turned up in parts of it the project does not fuzz: an out-of-bounds read that hands host memory back to the guest, and a heap write the guest controls.

22 September 2026 · CVE-2026-95507, CVE-2026-95508 · libslirp 4.9.4 (d09dc9a) and master, fixed in 4.9.5 · guest→host
Method: source audit with coverage-guided fuzzing (AFL++ / libdislocator) on aarch64
Status. Both CVEs were assigned on 22 September 2026 through Red Hat Product Security (PSIRTSUPT-23997). Both fixes shipped in libslirp 4.9.5, tagged the same day. Red Hat’s advisories say: “Red Hat would like to thank Stuart Thomas for reporting this issue.” Credited, not a paid bounty. Published after the fix.

Why libslirp

libslirp is a library rather than a product. It provides the “user networking” you get from -netdev user without asking for it, and it runs in the host process, parsing whatever the guest sends.

ProductUses libslirp forBoundary
QEMU-netdev user, the default VM networking modeguest → host
Podman / rootless Dockercontainer networking via slirp4netnscontainer → host
KubeVirtVM networking on Kubernetesguest → host
UTMQEMU front-end on macOS and iOSguest → host

So the attacker is already inside the guest or the container, and the target is the host process. One fix upstream reaches all of it once distributions rebuild, which is why Red Hat tracked the report: RHEL ships libslirp under QEMU, KubeVirt and Podman.

CVE-2026-95507 — NC-SI out-of-bounds read

NC-SI is a sideband management protocol. ncsi_input() checks an incoming frame against the 16-byte NC-SI common header. That check was added for CVE-2020-29129:

/* src/ncsi.c:287 — minimum 30 bytes */
if (pkt_len < ETH_HLEN + sizeof(struct ncsi_pkt_hdr)) {
    return;                       /* packet too short */
}

The OEM command handler came later. It reads four bytes at payload offset 16, which is frame offset 30, and copies them into the reply before it checks the manufacturer id:

/* src/ncsi.c:130-139 — ncsi_rsp_handler_oem() */
uint32_t mfr_id = ntohl(cmd->mfr_id);   /* reads 4 bytes past a 30-byte frame */
...
rsp->mfr_id = cmd->mfr_id;              /* copied into the reply, then sent */

A 30-byte OEM frame passes the header check but has no mfr_id in it. Those four bytes get read out of bounds and sent to the guest. If the host is configured as Mellanox (mfr_id = 0x8119) the sub-handler also reads cmd->data at offset 20, so a 34–37 byte frame over-reads there too.

The path is reachable because NC-SI gets the raw receive buffer with no mbuf copy in between. Only pkt_len >= 14 is guaranteed:

/* src/slirp.c:1313-1314 */
case ETH_P_NCSI:
    ncsi_input(slirp, pkt, pkt_len);

Under a guard-page allocator a 30-byte OEM frame (ethertype 0x88F8, type 0x50) faults at ncsi.c:136. To show the leak without relying on the allocator, sentinel bytes were placed straight after the frame; they came back in the NC-SI reply, with the reflected mfr_id reading 41 41 41 41.

Impact. About four bytes of adjacent host memory per frame, repeatable, up to eight with Mellanox configured. Red Hat rates it Moderate, CVSS 3.1 4.3 (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N). Four bytes is not much by itself, but heap pointers leak this way, and that is how host ASLR gets beaten.

CVE-2026-95508 — DHCPv6 and TFTP heap overflow

dhcpv6_info_request() builds its reply into an mbuf sized to the interface MTU, then copies the guest’s CLIENTID in. CLIENTID is capped at 256 bytes, but nothing checks it against the buffer:

/* src/dhcpv6.c:164 — no fit check */
memcpy(resp, ri.client_id, ri.client_id_len);

With a small MTU the reply mbuf is well under 256 bytes, so a long CLIENTID runs off the end of it. The threshold is client_id_len > if_mtu - 8. At if_mtu = 68, which is IF_MTU_MIN and libslirp accepts it, a 256-byte CLIENTID faults inside the copy. At the default 1500 it is clean.

There is a second one in the same function. The boot-URL option works out a signed smaxlen that can go negative, and slirp_fmt() takes it as a size_t, so it becomes huge and the truncation stops working. The path is reachable over IPv6/UDP to port 547.

Impact. A guest-controlled heap write. Together with the NC-SI leak that is the leak-then-write pair a guest-to-host escape needs, though the precondition here is not the default: IPv6 on, and an interface MTU under about 264 bytes. Red Hat rates it Important, CVSS 3.1 7.4 (AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H), with the attack complexity carried in the vector. The TFTP response builder has the same unbounded-write pattern and Red Hat’s advisory covers both.

Why they were still there

Both sit in code the project does not fuzz. NC-SI has no upstream harness at all, which is why it was picked. The 2020 fix was a single length check at the front of the parser, correct for the code as it stood; the OEM handler arrived later and reads further into the packet than that check covers. Length checks at the front of a parser go stale as handlers get added behind them. The fix upstream threads the packet length through to each handler so they can check their own fields.

The harness was AFL++ with the libdislocator guard-page allocator, on a Raspberry Pi 400. The guard page is the part that matters here: it turns any out-of-bounds access into a fault, so a four-byte over-read shows up instead of quietly reading whatever is next in the heap. An ordinary run would not catch it.

Verification. Every candidate was traced to a source line and reproduced before it was reported. Nothing went out on inference. It is slower and it throws findings away, but it is also why a maintainer could act on this the same day.

Disclosure

DateEvent
2026-09-16Audit and fuzzing of the NC-SI path; both bugs reproduced under the guard-page allocator.
2026-09-16Reported to the libslirp maintainer and Red Hat Product Security, with reproducers and patches.
2026-09-16Maintainer review, then revised patches: per-handler length checks, and MTU bounds on every DHCPv6 option write.
2026-09-16Both patches accepted, about five hours after the report.
2026-09-22CVE-2026-95507 and CVE-2026-95508 assigned (PSIRTSUPT-23997). libslirp 4.9.5 released with both fixes. This page published.

Five hours from report to accepted patch is a fast turnaround. It helped that the report arrived with a root cause, a reproducer and a patch, so there was nothing left to investigate.

Fix

Upgrade to libslirp 4.9.5, released 22 September 2026, or apply the merged patches to 4.9.4. Both are added length checks and change nothing for valid traffic. The upstream test/ncsitest still passes.

For the overflow there is a mitigation in the meantime: do not set an interface MTU below the IPv6 minimum of 1280 while IPv6 is on. The NC-SI read has no such option. The code fix is the only remedy.

The half that found nothing

The same method was pointed at Google Highway’s vqsort in parallel. It ships no in-tree fuzzer, and its ARM NEON paths get little of Google’s mostly x86 CI fuzzing. Ten harnesses covered sort, quickselect and partial-sort across scalar and key-value types.

Nothing came up. 1.2 billion executions, thousands of corpus cycles, zero crashes, with the harnesses checked beforehand for false positives on valid input. So vqsort’s masked-remainder handling and base-case networks hold on ARM. A find there would have been eligible for Google’s Open Source VRP. It is recorded because negative results still tell you something, and the harness will run on SVE and x86.

Environment

AFL++ with libdislocator guard-page heap and stack protector, fuzzing the parsers in place. Raspberry Pi 400, aarch64, ARM NEON.

Prior art

CVE-2020-29129, the NC-SI over-read whose fix CVE-2026-95507 extends.

Upstream

gitlab.freedesktop.org/slirp/libslirp, security contact samuel.thibault@ens-lyon.org.

Advisories

CVE-2026-95507 · CVE-2026-95508 · Red Hat Product Security PSIRTSUPT-23997.