CVE-2026-58085: Invalid authentication tag handling in FreeBSD if_wg
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
- I reproduced the issue dynamically on stock FreeBSD 15.1-RELEASE-p1 amd64 using the
cryptosoftpath. - OpenCrypto reported
EBADMSGfor the authentication operation, but the vulnerable wrapper returned zero. - A forged inner IPv4/UDP packet was delivered exactly once, and the peer endpoint was updated to the forged packet’s source.
- An attacker needs UDP reachability to the WireGuard listener, a current receiver index, a counter accepted by the replay window, and an inner source accepted by the peer’s
AllowedIPs. - FreeBSD fixed the supported release branches. The official advisory lists no workaround; affected systems should update and reboot.
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:
cryptosoftcompleted withEBADMSG, while the stockif_wgwrapper and its caller returned zero.- A packet capture on the inner interface and the UDP listener each observed exactly one delivery of the test marker.
wg(4)recorded 74 additional received bytes, matching one forged WireGuard transport message.- The peer’s roaming endpoint changed to the forged packet’s source port.
- The last-handshake timestamp did not change.
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.
I also ran controls in the same topology:
- An incorrect receiver index was rejected before decryption.
- A packet with an inner source outside
AllowedIPsreached the vulnerable crypto wrapper but was rejected by the source-policy check. - Reusing the accepted counter was rejected by replay protection.
- A counter beyond the implementation threshold was rejected by the threshold check.
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:
- UDP reachability to the target WireGuard listener.
- An active receive keypair for the target peer.
- The current 32-bit receiver index for that keypair.
- A counter accepted by the replay window and implementation threshold.
- An inner source address chosen by the attacker that matches the peer’s configured
AllowedIPs.
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.
Comments