[GH-ISSUE #228] wg_get_device_dump re-sends the same message forever when the first peer does not fit #132

Open
opened 2026-10-13 21:06:51 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @rudenko-ks on GitHub (Aug 31, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/228

When a device's own attributes fill most of the dump buffer, the first peer
cannot be appended, and the dump never advances. The kernel re-emits the same
message indefinitely instead of failing or making progress.

Plain WireGuard cannot reach this: its device block is a few hundred bytes, so
the first peer always fits. The AmneziaWG I1–I5 attributes make the device block
large enough to leave no room, which turns a latent condition into a reachable
one.

Mechanism

wg_get_device_dump, netlink.c (v3.1.20260812):

429  next_peer_cursor = ctx->next_peer;
...
516  peer = list_prepare_entry(ctx->next_peer, &wg->peer_list, peer_list);
517  list_for_each_entry_continue(peer, &wg->peer_list, peer_list) {
518          if (get_peer(peer, skb, ctx)) {
519                  done = false;
520                  break;
521          }
522          next_peer_cursor = peer;
523  }
...
537  genlmsg_end(skb, hdr);
538  if (done) {
539          ctx->next_peer = NULL;
540          return 0;
541  }
542  ctx->next_peer = next_peer_cursor;
543  return skb->len;

On the first call ctx->next_peer is NULL, so next_peer_cursor starts NULL.
If get_peer fails on the very first peer, the loop breaks at line 520 with
next_peer_cursor still NULL and done == false.

Line 542 then assigns NULL back into ctx->next_peer — no progress — and line
543 returns skb->len, which is non-zero. Netlink treats that as "more to
come" and calls the dump callback again with identical state. The same device
block is produced again, forever.

The peer path already has the pattern this case is missing: get_peer saves
ctx->next_allowedip (netlink.c:389) so a peer whose allowed-ips overflow
resumes correctly in the next message. Only the "not even the first peer fits"
case has no cursor to save.

Reproduction

Any interface whose device attributes leave less room than one peer needs.
With a 4096-byte reader buffer (what amneziawg-tools uses), the threshold is:

limit = 3808 − nlaBytes(ifname) − 36·[HeaderProtectionKey set] − P
P = 148 (peer has no endpoint) / 168 (IPv4 endpoint) / 180 (IPv6)

Derived from the source and confirmed by 37 measurements with zero residual.
Concretely: awgo-1, no header-protection key, one peer with an IPv4 endpoint
→ 3628 bytes of I1–I5. A 15-character name with a key and an IPv6 endpoint
→ 3572.

Three bands, by total I1–I5 size in bytes:

size behaviour
at or below the limit reads normally
above it this bug — same message forever
from roughly 3800 EMSGSIZE

Measured on one wedged interface: 20 000 identical 3932-byte messages in
0.554 s
, and it does not stop.

The trigger is a handshake, not a configuration change

The kernel writes a peer's endpoint when the handshake completes, and the
endpoint costs 20 bytes (IPv4) or 32 (IPv6) in the dump. So an interface that
reads fine can become unreadable with no change to its configuration.

Observed on an unchanged interface with 3648 bytes of I1–I5: awg show returns
in 2 ms before any client connects, and hangs after the first handshake —
1.47 s of CPU and 599 MB RSS in the reader before it was killed.

This is what makes the bug hard to attribute in the field: the operator changes
nothing, a client connects, and the interface becomes unreadable.

Note on allowed-ips

They are not part of this. The resumable branch at netlink.c:389 paginates them
correctly; the continuation message carries the peer without key, PSK,
handshake or endpoint (52 bytes instead of 168). Verified up to 100 subnets on
a single peer with no change to the threshold.

Suggested direction

Two independent things, both cheap:

  1. When the first peer of a message does not fit, that is not "more to come".
    Either return -EMSGSIZE, or record a cursor that lets the next call skip
    ahead, so the dump terminates either way.
  2. nlmsg_total_size of the device attributes is known before any peer is
    attempted — refusing early would give a clear error instead of a spin.

Environment

Kernel 6.8.0-138, module and tools amneziawg 3.1.20260812, x86-64.

Originally created by @rudenko-ks on GitHub (Aug 31, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/228 When a device's own attributes fill most of the dump buffer, the first peer cannot be appended, and the dump never advances. The kernel re-emits the same message indefinitely instead of failing or making progress. Plain WireGuard cannot reach this: its device block is a few hundred bytes, so the first peer always fits. The AmneziaWG I1–I5 attributes make the device block large enough to leave no room, which turns a latent condition into a reachable one. ### Mechanism `wg_get_device_dump`, netlink.c (v3.1.20260812): ```c 429 next_peer_cursor = ctx->next_peer; ... 516 peer = list_prepare_entry(ctx->next_peer, &wg->peer_list, peer_list); 517 list_for_each_entry_continue(peer, &wg->peer_list, peer_list) { 518 if (get_peer(peer, skb, ctx)) { 519 done = false; 520 break; 521 } 522 next_peer_cursor = peer; 523 } ... 537 genlmsg_end(skb, hdr); 538 if (done) { 539 ctx->next_peer = NULL; 540 return 0; 541 } 542 ctx->next_peer = next_peer_cursor; 543 return skb->len; ``` On the first call `ctx->next_peer` is NULL, so `next_peer_cursor` starts NULL. If `get_peer` fails on the very first peer, the loop breaks at line 520 with `next_peer_cursor` still NULL and `done == false`. Line 542 then assigns NULL back into `ctx->next_peer` — no progress — and line 543 returns `skb->len`, which is non-zero. Netlink treats that as "more to come" and calls the dump callback again with identical state. The same device block is produced again, forever. The peer path already has the pattern this case is missing: `get_peer` saves `ctx->next_allowedip` (netlink.c:389) so a peer whose allowed-ips overflow resumes correctly in the next message. Only the "not even the first peer fits" case has no cursor to save. ### Reproduction Any interface whose device attributes leave less room than one peer needs. With a 4096-byte reader buffer (what `amneziawg-tools` uses), the threshold is: limit = 3808 − nlaBytes(ifname) − 36·[HeaderProtectionKey set] − P P = 148 (peer has no endpoint) / 168 (IPv4 endpoint) / 180 (IPv6) Derived from the source and confirmed by 37 measurements with zero residual. Concretely: `awgo-1`, no header-protection key, one peer with an IPv4 endpoint → 3628 bytes of I1–I5. A 15-character name with a key and an IPv6 endpoint → 3572. Three bands, by total I1–I5 size in bytes: | size | behaviour | |---|---| | at or below the limit | reads normally | | above it | **this bug** — same message forever | | from roughly 3800 | EMSGSIZE | Measured on one wedged interface: **20 000 identical 3932-byte messages in 0.554 s**, and it does not stop. ### The trigger is a handshake, not a configuration change The kernel writes a peer's endpoint when the handshake completes, and the endpoint costs 20 bytes (IPv4) or 32 (IPv6) in the dump. So an interface that reads fine can become unreadable with no change to its configuration. Observed on an unchanged interface with 3648 bytes of I1–I5: `awg show` returns in 2 ms before any client connects, and hangs after the first handshake — 1.47 s of CPU and 599 MB RSS in the reader before it was killed. This is what makes the bug hard to attribute in the field: the operator changes nothing, a client connects, and the interface becomes unreadable. ### Note on allowed-ips They are not part of this. The resumable branch at netlink.c:389 paginates them correctly; the continuation message carries the peer without key, PSK, handshake or endpoint (52 bytes instead of 168). Verified up to 100 subnets on a single peer with no change to the threshold. ### Suggested direction Two independent things, both cheap: 1. When the first peer of a message does not fit, that is not "more to come". Either return `-EMSGSIZE`, or record a cursor that lets the next call skip ahead, so the dump terminates either way. 2. `nlmsg_total_size` of the device attributes is known before any peer is attempted — refusing early would give a clear error instead of a spin. ### Related - amnezia-vpn/amneziawg-linux-kernel-module#148 — a user reporting a router hang and reboot with multi-kilobyte I1–I3. The symptom matches this bug. - amnezia-vpn/amneziawg-tools#69 — the write side of the same 4 KB buffer, and the userspace half of this hang. ### Environment Kernel 6.8.0-138, module and tools `amneziawg 3.1.20260812`, x86-64.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amneziawg-linux-kernel-module#132
No description provided.