mirror of
https://github.com/amnezia-vpn/amneziawg-tools.git
synced 2026-10-13 21:34:42 +00:00
[GH-ISSUE #69] Heap overflow: device-level I1–I5 attributes are written to the 4 KB netlink buffer without bounds checks #30
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-tools#30
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 @rudenko-ks on GitHub (Aug 22, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-tools/issues/69
Summary
awg setandawg setconfabort withSegmentation faultor glibc'sAborted(free(): invalid pointer/double free or corruption) when thecombined size of the
I1–I5junk-packet strings is large enough to push thenetlink request past the message buffer.
The device-level AmneziaWG attributes are written with the unchecked
mnl_attr_put*family, unlike every peer attribute in the same function.Where
The buffer is
min(pagesize, 8192)bytes — 4096 on x86-64:The junk strings go in unchecked:
dev->i1…dev->i5arechar *holding the literal CPS text(
src/containers.h:135), so a Pro-style QUIC profile puts roughly 3.9 KB ofstring into a 4 KB buffer that already holds the interface name, private key,
Jc/Jmin/Jmax,S1–S4andH1–H4.The peer path in the same function has what the device path lacks: a
bounds-checked nest and a recovery path that finishes the current message and
starts another.
To be precise, not every peer attribute is checked either —
ipc-linux.h:252and
:277-282use the unchecked variants. But the peer path as a whole candetect an overflow and split the message, so an oversized peer set degrades
into several messages instead of corrupting the heap. The device-level block
has no check and no split path at all, and
I1–I5are the only deviceattributes large enough to overflow.
How to reproduce
On x86-64 Linux with the kernel module loaded:
Sweeping
i5upward in 4-hex-character steps withi1–i4held constant, onamneziawg-toolsv3.1.20260812, the transition is sharp and reproducible —three runs at each step agreed:
Units matter here and I got them wrong in an earlier draft, so to be explicit:
these are decoded payload bytes, i.e. hex digits / 2. The same quantity as
characters of config text is roughly twice as large plus tag overhead —
1938 payload bytes is about 3906 characters, which is why these figures sit
above the 3868-character crash threshold in the table further down (that sweep
used a full config including a peer; this one sets device attributes only, which
leaves more room).
"No crash" is the honest label rather than "applied": as established below, the
configuration is applied on both sides of this boundary.
The cutoff is not a fixed I-payload size — it is the total message size.
Repeating the sweep while changing only the interface name (whose attribute
occupies
4 + align4(len+1), i.e. 8 / 12 / 20 bytes) moves the I-payloadceiling in exact lockstep:
swawgb2abcdefghijklmnoThree different I-payload ceilings, one identical message size, 8 bytes under
the 4096-byte page — and the probe step was 4 bytes, so the true boundary sits
in 4088…4096. The ratio is 1:2 because the I fields are carried as hex text, so
each extra byte elsewhere in the message costs half a byte of junk payload.
In this setup the
Hencoding made no difference (h1 5472andh1 5472-69603produced identical ceilings), so the wire encoding appears notto follow how the value was written.
awg setconfwith the same fields in a file fails identically, so this is notthe config parser.
Expected behavior
Either the request is rejected with a clear error ("configuration too large"),
or the message is split the way the peer path already splits it. What should not
happen is writing past the allocated buffer.
Actual behavior
Heap corruption.
awgaborts in glibc — but note when: the abort happens inthe tool's own free path, after the netlink message has already been sent
and accepted by the kernel. So the configuration is applied even in the crashing
case. The overflow is a memory-safety bug in the tool, not a failure to
configure the device.
Correction to an earlier version of this report
An earlier draft claimed a middle band where
setconfreturns 0 and the deviceis left completely unconfigured. That was a measurement error on my side and I
withdraw it. I had read back device state with
awg showconfcapturing stdoutonly. Re-running with the readback's own exit status and stderr captured shows
the device is fully configured; what breaks is reading it. Apologies for the
noise — the corrected picture is below, and it points at a second bug that the
suggested fix here does not address.
The size behaviour, remeasured
Measured on
v3.1.20260812+ current kernel module, Ubuntu 24.04, kernel 6.8.One
awg setconfper step with a complete client config (private key, address,Jc/Jmin/Jmax,S1-S4,H1-H4, fiveIfields, one peer with a preshared key,AllowedIPs = 0.0.0.0/0, ::/0, endpoint, keepalive). Only the combined length ofthe
I1–I5strings (characters in the config, not decoded payload bytes)varies; values are well-formed
<b 0xHEX>tags:I1-I5string length, characterssetconfexitawg showhangs, then EMSGSIZETwo independent witnesses that the device really is configured in the upper
bands, neither of which goes through the failing GET path:
awg set <if> i1 '<b 0xdeadbeef>') makes the interfacereadable again immediately: the private key matches what was written, the peer
public key matches, and
I2–I5still hold their exact character counts.latest handshake: 7 seconds ago,212 B received, 203 B sent, andip -s linkshows TX 4380 B / 20 packets,0 errors. A device with no private key cannot complete a handshake.
A genuinely unconfigured device reads back as
(none)with exit status 0, whichis plainly distinguishable from both failure modes above.
The GET path fails ~270 characters earlier, and that is the first thing users hit
WGDEVICE_GETbuilds its reply as a single message:netlink.c:421setsint ret = -EMSGSIZE;and the device-attribute block at:464-474has no wayto continue into a second message, unlike the peer loop at
:516-523whichresumes correctly. Once
I1–I5no longer fit, the whole dump fails.The practical consequence is that
awg showhangs in the 3600–3866 band(killed by timeout, empty stderr) before it starts returning EMSGSIZE from
~3770. Anything that polls the interface — status pages, monitoring, wrapper
scripts — hangs with it.
This is worth stating plainly because it affects the fix: bounds-checking the
device attributes in the tool (the change suggested below) removes the heap
overflow at ~3868 characters, but a user hits the unreadable-interface bug at
~3598 characters first. Both sides need the same treatment — the device-level
attribute block needs to be splittable, in the tool and in the module's GET
path.
On the host hang mentioned in the original report
The original report described a box that became unreachable — ICMP fine, TCP on
:22 accepted, SSH banner never completing. No hang occurred in these runs, so I
still cannot demonstrate the causal chain. But the fact that
awg showhangsrather than failing in the middle band is at least a plausible mechanism: any
supervisor that polls the interface accumulates stuck processes. Offering that
as a hypothesis only, not as a claim.
Suggested fix
Use the
_checkvariants for the device-level attributes, mirroring the peerpath:
with
toobig_devicesending what has accumulated and continuing in a freshmessage, as
toobig_allowedipsandtoobig_peersalready do.That fixes the memory-safety bug, which is the part this issue is about. It
does not fix the unreadable interface, since that lives in the module's GET
path — I am happy to file that separately against the kernel module if you would
prefer it kept apart. Mentioning it here only so the ~3598-character failure
isn't mistaken for something this change already covers.
Related, but not duplicates
Checked the tracker before filing — the existing
I1–I5reports are aboutdifferent failures:
awg-quickcrashes whenI1–I5are empty; this one is aboutthem being large.
awg-quickfails with a cleanUnable to modify interface: Invalid argumenton tagged junk strings; no crash and no memory-safety symptom.Environment
amneziawg-toolsv3.1.20260812, source inspected at commitee0f0a9amneziawg-linux-kernel-module, Ubuntu 24.04, kernel 6.8.0-138, x86-64getconf PAGESIZE)@rudenko-ks commented on GitHub (Aug 31, 2026):
Follow-up on the read side, which this report mentions only as "
awg showhangs and eventually fails with EMSGSIZE". I have since traced what actually
happens, and it is two separate things.
The hang is the kernel, not the tool. When the device attributes leave no
room for the first peer,
wg_get_device_dumpneither advances its cursor norfails: it returns
skb->len, netlink asks again, and the same message isproduced forever. Measured: 20 000 identical 3932-byte messages in 0.554 s.
Filed separately against the kernel module — .
The 4 KB is ours, not a kernel limit. The figure in the title is the buffer
this tool hands to
recvmsg:min(getpagesize(), 8192), which is 4096 onx86-64. The kernel sizes the dump skb from what the reader advertises
(
nlk->max_recvmsg_leninnetlink_dump), so the ceiling is a consequence ofthat choice. A reader that asks for more gets more — with a larger buffer the
readable size rises to roughly 31 980 bytes, and the practical limit on I1–I5
disappears.
The tool also leaks while it spins.
read_device_cbduplicates the stringsout of every message it receives and nothing frees them, so the endless dump is
not merely an endless loop — it grows. Measured at roughly 164 MB/s; a reader
left running reached 599 MB RSS before it was killed.
So there are two fixes on this side, both independent of the kernel one:
readable at all.
read_device_cbduplicates, so a malformed or oversized dumpcannot exhaust memory.
The threshold, for reference, is not a constant but a formula:
The last term is why this surfaces without any configuration change: the kernel
writes the endpoint at handshake time, so an interface that reads fine before a
client connects can stop being readable afterwards.
Environment: kernel 6.8.0-138, module and tools
amneziawg 3.1.20260812.@hoaxisr commented on GitHub (Sep 8, 2026):
Same bug from a different angle, with a data point on 4 KB-page MIPS.
Measured on a Linux 4.9-ndm-5, mipsel, page size 4096, amneziawg-tools v3.1.20260812, with a full AWG 3.1 parameter set (Jc/Jmin/Jmax, S1–S4, H1–H4, HeaderProtectionKey, the timing ranges, both 3.1 flags) plus one peer, the interface recreated
between runs so that no state carried over:
sum of I1–I5 = 3710 characters → applied, read back intact
sum of I1–I5 = 3720 characters → awg setconf returns rc=0 with no error,
and awg show/showconf on that interface
then answer "Unable to access interface:
Message too large"
Without recreating the interface the threshold wanders between runs, which matches the report - it is undefined behaviour, not a fixed limit.