mirror of
https://github.com/amnezia-vpn/amneziawg-linux-kernel-module.git
synced 2026-10-14 13:52:06 +00:00
[GH-ISSUE #225] AWG 3.x send and teardown paths (heap overflow via Jmin > Jmax, unchecked kzalloc, ispec use-after-free, content padding sized against the wrong ceiling) #128
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amnezia-vpn/amneziawg-linux-kernel-module#128
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @hoaxisr on GitHub (Aug 28, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/225
buffer is wg->jmax bytes, but the size is drawn with
get_random_u32_inclusive(wg->jmin, wg->jmax) and nothing anywhere enforces
jmin <= jmax. The netlink policy declares both as plain NLA_U16
(src/netlink.c:47-48) and wg_set_device stores them unvalidated
(src/netlink.c:802-806), so awg setconf with Jmin = 2000, Jmax = 1000
is accepted.
get_random_u32_inclusive(floor, ceil) computes
floor + get_random_u32_below(ceil - floor + 1). With floor > ceil the count
underflows to nearly 2^32, the draw is truncated to u16, and
get_random_bytes(buffer, junk_packet_size) writes up to 65535 bytes into a
jmax-sized allocation - heap corruption reachable from a normal interface
configuration.
The exact boundary jmin == jmax + 1 makes the count zero. On kernels that use
the module's own compat shim (src/compat/compat.h:1236,
< 6.1.4 && !ISRHEL9) that is get_random_u32() % 0 - a divide-by-zero in
kernel context.
Minimal fix - clamp at the draw:
Rejecting jmin > jmax in wg_set_device would be better still, but the clamp
also covers configurations already stored by older builds.
Same block. buffer = kzalloc(wg->jmax, GFP_KERNEL) is used at lines 73 and 75
without a NULL check, so an allocation failure is a NULL dereference. jmax is
guaranteed non-zero by the if (wg->jc && wg->jmax) guard above, so the only
failure mode is memory pressure, but this runs on routers with 64-128 MB of
RAM, where GFP_KERNEL failures are not theoretical.
The ispecs are released before wg_peer_remove_all() and before the handshake
workqueues are destroyed, the code's own comment on line 270 notes that the
final references are only cleared by those destroy_workqueue() calls. A work
item already queued on handshake_send_wq (src/send.c:128,
src/timers.c:121) runs wg_packet_send_handshake_initiation, which reads
wg->ispecs[i] at src/send.c:50-59.
The pkt_size > 0 guard does not close the window: jp_spec_free
(src/junk.c:239) takes no lock and clears pkt_size last
While the reader checks pkt_size first and only then takes spec->lock and
dereferences spec->pkt / spec->mods. A worker that read the old pkt_size
proceeds into freed memory.
Can be fixed: move the jp_spec_free() loop below the three destroy_workqueue() calls,
where no worker can still be running.
amneziawg-go fixed the equivalent code on 2026-08-28 in
da11c9f
("fix: use udp window for RandomPaddingAddition"). The kernel still carries the
pre-fix shape, so the same ContentPaddingAddition range now produces different
datagram sizes depending on which implementation sends the packet.
The clamp measures the inner packet against the tun MTU, ignoring
transport_padding (S4) and MESSAGE_MINIMUM_LENGTH (32 bytes: the 16-byte
message_data header plus the 16-byte auth tag) that the finished datagram
carries. The datagram can therefore exceed the ceiling the clamp was meant to
enforce by transport_padding + 32 bytes. On paths with a reduced MTU (mobile
carriers, PPPoE) that means fragmentation or drops.
This is not an interop break, the padding sits inside the AEAD payload and the
receiver strips it by the inner IP length.
The needed value is already at hand. encrypt_packet refreshes the peer's UDP
window ten lines above the call (src/send.c:266-268), and the sibling branch of
the very same if/else chain already uses it (src/send.c:276-283):
The asymmetry looks unintended: randomize_skb_padding arrived with AmneziaWG
3.0 (
b6ca11f, 2026-07-30), before peer->udp_window existed, that came with3.1 (
4680320, 2026-08-12) and was wired into the trailer path only.Suggested fix, mirroring da11c9f:
called with the same argument the trailer branch already passes: