Overview

CVE-2026-58085, disclosed as FreeBSD-SA-26:52.if_wg, is a flaw in FreeBSD’s in-kernel WireGuard implementation, wg(4) (if_wg). It allowed if_wg to accept a transport data packet with an invalid Poly1305 authentication tag.

The flaw was in the interface between if_wg and OpenCrypto. The wrapper checked the immediate return value from crypto_dispatch(), but did not propagate the completion status stored in crp_etype. With the tested cryptosoft provider, authentication failed with EBADMSG while dispatch returned zero. The wrapper then removed the tag and returned success, allowing unchanged attacker-controlled bytes to enter the WireGuard receive path as an inner IP packet.

This write-up covers the root cause, demonstrated attack conditions, results from an isolated reproduction, and the upstream fix.

TL;DR

Affected versions

According to the advisory, all FreeBSD versions supported on July 29, 2026 were affected. The CVE record identifies the affected-version boundaries below, which align with the corrected RELEASE versions listed in the advisory:

Release Affected versions Corrected version
15.1-RELEASE 15.1-RELEASE-p1 and earlier 15.1-RELEASE-p2
15.0-RELEASE 15.0-RELEASE-p11 and earlier 15.0-RELEASE-p12
14.4-RELEASE 14.4-RELEASE-p7 and earlier 14.4-RELEASE-p8

Systems that do not use wg(4) interfaces are not affected.

Background

A WireGuard transport data packet carries a receiver index used to select a receive keypair, a counter used by replay protection, an encrypted payload, and an authentication tag. A packet whose tag fails verification must be rejected before its inner packet is processed or any protocol state is updated.

FreeBSD’s OpenCrypto framework distinguishes dispatch status from cryptographic operation status. A zero return from crypto_dispatch() can mean that dispatch itself succeeded while the completed operation reports an error in the crp_etype member of struct cryptop. An AEAD consumer must ensure that both stages succeeded before releasing plaintext or advancing protocol state.

Root cause

At the tested releng/15.1 revision, chacha20poly1305_decrypt_mbuf() effectively performed the following sequence:

ret = crypto_dispatch(&crp);
crypto_destroyreq(&crp);
if (ret)
	return (ret);
m_adj(m, -POLY1305_HASH_LEN);
return (0);

Here, ret contains only the dispatch return value. The function never examines crp.crp_etype. In the same revision, the cryptosoft provider stores the operation result in crp_etype and then returns zero from its dispatch path:

crp->crp_etype = ses->swcr_process(ses, crp);
crypto_done(crp);
return (0);

The invalid-tag reproduction therefore produced two different results:

OpenCrypto operation result: EBADMSG
if_wg wrapper return:        0
Inner packet delivery:       1
Peer endpoint changed:       yes

The tested provider verified the tag before decrypting the payload. When authentication failed, the payload bytes remained unchanged and still encoded the attacker-supplied inner IPv4/UDP packet. Because the wrapper looked only at the zero dispatch result, it removed the 16-byte tag and continued the receive path as if authentication had succeeded.

The cryptographic primitive itself was not the problem. The consumer failed to propagate the operation’s completion status.

Dynamic reproduction

I reproduced the issue dynamically in exactly one configuration:

FreeBSD 15.1-RELEASE-p1 amd64
releng/15.1-n283582-0f691888dc56
source revision: 0f691888dc56a068f74c213bc87b32939b6b354e
crypto provider: cryptosoft
outer transport: IPv4/UDP
inner packet: IPv4/UDP

The test used an isolated VNET topology with RFC 5737 addresses. I first established a legitimate WireGuard session, then destroyed the sender-side WireGuard interface. The injector ran as UID 2002 and sent the forged outer packet through an ordinary UDP socket. Root privileges were used to prepare the isolated topology, legitimate session, instrumentation, listener, and privilege drop.

The run produced the following observations:

Live two-shell demonstration

The left pane is the unprivileged attacker shell in wg_e2e_a; the right pane is the victim shell in wg_e2e_b. The recording shows the invalid-tag UDP send, delivery of the inner marker, and the corresponding OpenCrypto and if_wg results from the same fresh run. The attacker pane prints the observed receiver index, counter, address tuples, all-zero tag, and complete forged datagram. The victim pane prints the real listener output, raw DTrace events, and the resulting RX, endpoint, and handshake state. Only private keys, the recording VM’s SSH key, and host-side paths are omitted. The animation plays at 75% of the original recording speed for readability.

Animated two-shell reproduction of CVE-2026-58085

I also ran controls in the same topology:

These controls show that successful delivery depends on a valid receiver index, an acceptable counter, and an inner source permitted by AllowedIPs.

Attack prerequisites

The demonstrated path required all of the following:

The attacker did not need either peer’s private key or the transport session key. A receiver index is visible in legitimate WireGuard transport traffic, and the harness used a value obtained from its own legitimate capture. The demonstrated attacker model therefore includes observation of legitimate WireGuard traffic.

Security impact

Under the demonstrated conditions, an attacker can inject an unauthenticated inner packet that is attributed to the configured peer and spoof a source address permitted by that peer’s AllowedIPs. The packet may then reach services or routing domains that trust traffic arriving through the WireGuard tunnel. The forged packet can also update the peer’s roaming endpoint, as observed in the reproduction.

Replay protection and AllowedIPs narrow the acceptable packet space, but neither restores the missing cryptographic authentication decision. The downstream impact depends on each deployment’s tunnel policy and on which services trust traffic from permitted inner source addresses. The demonstrated impact is unauthenticated inner-packet injection and a roaming-endpoint update.

Upstream fix

The official releng/15.1 fix propagates crp_etype when dispatch itself succeeds:

 ret = crypto_dispatch(&crp);
+if (ret == 0)
+	ret = crp.crp_etype;
 crypto_destroyreq(&crp);

The change was applied to both encryption and decryption. It also rejects an OpenCrypto session unless the session is synchronous, making explicit the assumption that the stack-allocated request completes synchronously. The main branch fix follows the same approach.

The subsequent official regression test injects EBADMSG through an OpenCrypto fail point. It verifies that a ping across the tunnel fails and that the interface input-error counter increases. It is a driver-level regression test for correct handling of decryption failures.

Remediation

The FreeBSD advisory lists no workaround. Affected systems should update to a supported version containing the fix and reboot so that the corrected kernel or module is in use. Refer to FreeBSD-SA-26:52.if_wg for the official update instructions and correction revisions for each branch.

Taking affected wg(4) interfaces out of service until the update is installed, or restricting access to the outer UDP listener with a firewall, may reduce exposure. These are operational mitigations and do not repair the missing authentication decision.

References