Scope: drivers/net/wireguard/ (wireguard-linux-compat, GitHub mirror)
Focus: Integer overflows, attacker-controllable length fields, offset arithmetic bugs
Date: 2026-06-23
| # | File | Location | Severity | Status |
|---|---|---|---|---|
| F1 | receive.c |
decrypt_packet() — offset arithmetic |
Low | No underflow in practice; lacks explicit guard |
| F2 | receive.c |
prepare_skb_header() — skb->len - data_offset |
Low | Safe due to check ordering; fragile |
| F3 | receive.c |
counter_validate() — ++their_counter after bounds check |
Informational | Safe by design |
| F4 | send.c |
encrypt_packet() — plaintext_len overflow |
Informational | No practical path to overflow |
| F5 | send.c |
skb_to_sgvec length argument |
Informational | Correct after analysis |
| F6 | netlink.c |
WGPEER_A_ENDPOINT policy vs memcpy |
Minor | Safe; policy could be tightened |
| F7 | netlink.c |
WGALLOWEDIP_A_IPADDR policy vs exact check |
Informational | Safe |
| F8 | receive.c |
IPv6 payload_len + sizeof(ipv6hdr) width |
Informational | Safe on 32/64-bit |
No critical or high severity bugs were found. The codebase shows deliberate, careful length handling. The most notable patterns are structural risks — fragile check ordering and missing explicit guards — which are not exploitable in their current form but warrant attention for future refactoring.
File: receive.c, function decrypt_packet()
Severity: Low (not exploitable in current kernel path, but lacks a defensive guard)
Code:
offset = skb->data - skb_network_header(skb);
skb_push(skb, offset);
num_frags = skb_cow_data(skb, 0, &trailer);
offset += sizeof(struct message_data);
skb_pull(skb, offset);Analysis:
offset is computed as a pointer subtraction between two u8 * values. If skb->data < skb_network_header(skb), the result silently wraps to a huge size_t. That value then gets passed to skb_push(skb, offset), which performs:
skb->data -= len;
skb->len += len;A wrapped offset here would produce a massively corrupted skb->data pointer and skb->len.
Why it's not exploitable today: By the time decrypt_packet() is called, skb->data points into the WireGuard UDP payload — which is always at or after the IP header, so data >= network_header is guaranteed by the kernel's IP receive path. There is no known way for a remote attacker to invert this relationship via a crafted packet.
Why it still matters: There is no WARN_ON or explicit if (offset > MAX_SANE_OFFSET) return false guard. If this code is ever called from a different entry point, or if skb_reset_network_header is called at an unexpected point upstream, the check silently disappears.
Recommended fix:
if (unlikely(skb->data < skb_network_header(skb)))
return false;
offset = skb->data - skb_network_header(skb);File: receive.c, function prepare_skb_header()
Severity: Low (currently safe; correctness depends on check ordering)
Code:
data_len = ntohs(udp->len); // u16, attacker-controlled
if (unlikely(data_len < sizeof(struct udphdr) ||
data_len > skb->len - data_offset)) // (*)
return -EINVAL;At (*), skb->len - data_offset is an unsigned subtraction. If data_offset > skb->len, this wraps to a large value, making the data_len > comparison silently false — the check would pass with wrong data.
Why it's safe today: The preceding check ensures data_offset + sizeof(struct udphdr) <= skb->len, so data_offset < skb->len is guaranteed before the subtraction is reached. The check ordering saves it.
Risk: This is a classic "safety by ordering" pattern. If a future refactoring reorders the checks or introduces an early-return path between them, the unsigned underflow becomes live.
Recommended fix: Replace the implicit ordering dependency with an explicit precondition:
if (unlikely(skb->len < data_offset + sizeof(struct udphdr)))
return -EINVAL;
data_len = ntohs(udp->len);
if (unlikely(data_len < sizeof(struct udphdr) ||
data_len > skb->len - data_offset))
return -EINVAL;File: receive.c, function counter_validate()
Severity: Informational (safe, but worth documenting)
Code:
if (unlikely(/* ... */ their_counter >= REJECT_AFTER_MESSAGES))
goto out;
++their_counter;their_counter is u64. REJECT_AFTER_MESSAGES = U64_MAX - COUNTER_WINDOW_SIZE - 1. The bounds check ensures their_counter < REJECT_AFTER_MESSAGES before increment, so the post-increment reaches at most REJECT_AFTER_MESSAGES — which is well below U64_MAX. No overflow.
The subsequent array index computation is also safe:
index = their_counter >> ilog2(BITS_PER_LONG); // >> 6 on 64-bit
index &= (COUNTER_BITS_TOTAL / BITS_PER_LONG) - 1; // mask to [0..127]The backtrack[] array has exactly 128 entries. The mask guarantees in-bounds access.
Note: The ++their_counter before index computation is intentional — it shifts the window so counter=0 maps to index=1, not 0. This is correct RFC 6479 behavior. It reads surprising without a comment.
File: send.c, function encrypt_packet()
Severity: Informational (no realistic attack path)
Code:
padding_len = calculate_skb_padding(skb); // unsigned int, max 15
plaintext_len = skb->len + padding_len; // unsigned int + unsigned int
trailer_len = padding_len + noise_encrypted_len(0); // max 31skb->len + padding_len can theoretically overflow if skb->len is near UINT_MAX. The kernel enforces per-interface MTU limits that prevent skb->len from reaching anywhere near UINT_MAX in practice. However, there is no explicit check_add_overflow() guard.
Note on calculate_skb_padding: padding_len is bounded to [0, MESSAGE_PADDING_MULTIPLE - 1] = [0, 15] because ALIGN(x, 16) - x ∈ [0, 15] always. This keeps trailer_len to at most 31 bytes, making the auth tag addition safe.
File: send.c
Severity: Informational (correct)
skb_to_sgvec(skb, sg,
sizeof(struct message_data),
noise_encrypted_len(plaintext_len));At this point skb->len = sizeof(message_data) + noise_encrypted_len(plaintext_len). The call maps exactly the bytes from offset 16 to the end of the skb — the ciphertext region. Length and offset are consistent. Safe.
File: netlink.c
Severity: Minor (defence-in-depth)
Policy:
[WGPEER_A_ENDPOINT] = NLA_POLICY_MIN_LEN(sizeof(struct sockaddr)),
// sizeof(struct sockaddr) = 16 bytesUsage:
size_t len = nla_len(attrs[WGPEER_A_ENDPOINT]);
if ((len == sizeof(struct sockaddr_in) && addr->sa_family == AF_INET) ||
(len == sizeof(struct sockaddr_in6) && addr->sa_family == AF_INET6)) {
memcpy(&endpoint.addr, addr, len);
}The policy accepts any NLA payload ≥ 16 bytes. A caller could send a 1024-byte endpoint NLA; the code would silently ignore it (no len == 16 or len == 28 match). No overflow, no crash — the == checks on len prevent any misuse.
However, accepting and silently ignoring oversized attributes wastes parse resources and increases the grammar of valid inputs unnecessarily. An NLA_POLICY_RANGE or NLA_POLICY_EXACT_LEN for both valid sizes would be cleaner, though genetlink doesn't natively support "one of two exact lengths."
Suggested tightening:
// Accept up to the larger valid size; reject anything bigger
[WGPEER_A_ENDPOINT] = NLA_POLICY_MIN_LEN(sizeof(struct sockaddr_in6)),Then add a nla_len > sizeof(struct sockaddr_in6) reject in set_peer().
File: netlink.c
Severity: Informational (safe)
Policy uses NLA_POLICY_MIN_LEN(sizeof(struct in_addr)) = 4 bytes minimum, but set_allowedip() uses exact nla_len() == sizeof(in_addr) and == sizeof(in6_addr) checks before any use of nla_data(). This is correct and safe. The min-len policy serves as a quick early reject for trivially short attributes.
File: receive.c, function wg_packet_consume_data_done()
Severity: Informational (safe)
len = ntohs(ipv6_hdr(skb)->payload_len) + sizeof(struct ipv6hdr);ntohs() returns u16 (max 65535). sizeof(struct ipv6hdr) = 40. Sum is at most 65575, which fits comfortably in unsigned int. No overflow on 32-bit or 64-bit platforms. The subsequent len > skb->len check prevents any overread. Safe.
Note: This data comes from the decrypted, authenticated inner packet. A remote attacker without a valid session key cannot reach this path.
Strengths of the WireGuard codebase:
- Handshake message types use exact-length (
==) comparisons invalidate_header_len(), not>=, so there is no over-read from oversized handshake packets. - Netlink attributes for fixed-size crypto keys use
NLA_POLICY_EXACT_LEN, which is the correct and tightest possible policy. - The
counter_validate()replay window uses bitmask indexing rather than shift-based indexing, avoiding shift-amount UB. - Error paths throughout
prepare_skb_header()use early returns, keeping the "safe region" of each subsequent check well-defined. pskb_trim()return values are always checked indecrypt_packet().
Structural concerns to track:
- Implicit check-ordering dependencies (F2): Two checks must appear in a specific order for the second to be safe. This is correct but non-obvious. A comment or assertion would improve auditability.
- Missing pointer-comparison guard (F1): The
offset = skb->data - skb_network_header(skb)pattern is common in kernel networking code and is generally safe, but an explicitWARN_ONwould make the invariant visible. - Silent policy mismatches (F6, F7): Using
NLA_POLICY_MIN_LENwhere a more specific policy is possible means malformed attributes are rejected later (in application logic) rather than earlier (in netlink parsing). This increases the amount of code that needs to handle bad inputs.
noise.c—mix_key()andhkdf()length parameters, particularly thefirst_len/second_len/third_lenguardscookie.c—wg_cookie_message_create()buffer sizing for the xchacha encrypted cookieallowedips.c—push_rcu()stack depth: theWARN_ON(*len >= 128)inpush_rcusuggests a fixed-size stack that could theoretically overflow if trie depth exceeds 128 (maximum trie depth for IPv6 is 128 levels, exactly matching the limit — worth verifying that the stack is always sized to 129+1)