Netdev List
 help / color / mirror / Atom feed
* [PATCH net 0/2] wireguard: memcg accounting and initiation race fix
@ 2026-11-05 14:10 Alexander Ildiyarov
  2026-11-05 14:10 ` [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg Alexander Ildiyarov
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Alexander Ildiyarov @ 2026-11-05 14:10 UTC (permalink / raw)
  To: Jason A . Donenfeld, wireguard, netdev
  Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-kernel, Alexander Ildiyarov

  Two independent fixes:
  
  1/2 charges peer, allowedips, dst_cache and hashtable allocations to the
  memcg of the configuring process, which currently can allocate
  unaccounted kernel memory through WG_CMD_SET_DEVICE.
  
  2/2 rechecks the replay/flood conditions under the write lock in
  wg_noise_handshake_consume_initiation(), the initiation-side counterpart
  of 2a73c9b2c0ca. The race window is narrow and was only reproduced with
  an artificial delay; details are in the commit message.
  
  Tested in qemu (x86_64, 4 vCPUs) with reproducers for both issues and
  CONFIG_WIREGUARD_DEBUG self-tests. netns.sh was run up to the
  allowed-ips removal step, where the available wg(8) is too old; that
  part was not run.


Alexander Ildiyarov (2):
  wireguard: account peer and allowedips allocations to memcg
  wireguard: noise: recheck replay and flood under write lock on
    initiation

 drivers/net/wireguard/allowedips.c |  2 +-
 drivers/net/wireguard/noise.c      | 29 ++++++++++++++++++++++-------
 drivers/net/wireguard/peer.c       |  4 ++--
 drivers/net/wireguard/peerlookup.c |  4 ++--
 4 files changed, 27 insertions(+), 12 deletions(-)

-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg
  2026-11-05 14:10 [PATCH net 0/2] wireguard: memcg accounting and initiation race fix Alexander Ildiyarov
@ 2026-11-05 14:10 ` Alexander Ildiyarov
  2026-11-05 14:10 ` [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation Alexander Ildiyarov
  2026-11-05 14:14 ` [PATCH net 0/2] wireguard: memcg accounting and initiation race fix netdev-bot+sinfo
  2 siblings, 0 replies; 4+ messages in thread
From: Alexander Ildiyarov @ 2026-11-05 14:10 UTC (permalink / raw)
  To: Jason A . Donenfeld, wireguard, netdev
  Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-kernel, Alexander Ildiyarov, stable

Peers, allowed IP trie nodes, the per-peer dst_cache and the per-device
hashtables are all allocated in the context of the process that
configures the device over netlink, but none of these allocations are
charged to that process's memory cgroup: the wg_peer and allowedips_node
caches are created without SLAB_ACCOUNT, and the hashtables and
dst_cache use plain GFP_KERNEL.

The amount of memory is bounded only by MAX_PEERS_PER_DEVICE (2^20) per
device, with no limit on the number of devices or on the number of
allowed IPs per peer. Since WG_CMD_SET_DEVICE uses GENL_UNS_ADMIN_PERM,
any user who can create a user and network namespace can do this, and
thereby escape the memory limits of the cgroup it runs in.

For example, a process in a cgroup with memory.max=64M that adds 100000
peers with 20 /128 allowed IPs each grows Slab by ~790 MiB, yet the
cgroup's memory.current stays below 1 MiB and its memory limit is never
hit.

Fix this by creating the two caches with SLAB_ACCOUNT and using
GFP_KERNEL_ACCOUNT for the hashtables and the dst_cache. The keypair
allocations are left alone, as they happen from the handshake worker and
are bounded to three per peer.

Fixes: e7096c131e51 ("net: WireGuard secure network tunnel")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Alexander Ildiyarov <alex@badcode.im>
---
 drivers/net/wireguard/allowedips.c | 2 +-
 drivers/net/wireguard/peer.c       | 4 ++--
 drivers/net/wireguard/peerlookup.c | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/drivers/net/wireguard/allowedips.c b/drivers/net/wireguard/allowedips.c
index 5ece9acad64d..2f2b8a94c8a7 100644
--- a/drivers/net/wireguard/allowedips.c
+++ b/drivers/net/wireguard/allowedips.c
@@ -411,7 +411,7 @@ struct wg_peer *wg_allowedips_lookup_src(struct allowedips *table,
 
 int __init wg_allowedips_slab_init(void)
 {
-	node_cache = KMEM_CACHE(allowedips_node, 0);
+	node_cache = KMEM_CACHE(allowedips_node, SLAB_ACCOUNT);
 	return node_cache ? 0 : -ENOMEM;
 }
 
diff --git a/drivers/net/wireguard/peer.c b/drivers/net/wireguard/peer.c
index 1cb502a932e0..05b80521c706 100644
--- a/drivers/net/wireguard/peer.c
+++ b/drivers/net/wireguard/peer.c
@@ -33,7 +33,7 @@ struct wg_peer *wg_peer_create(struct wg_device *wg,
 	peer = kmem_cache_zalloc(peer_cache, GFP_KERNEL);
 	if (unlikely(!peer))
 		return ERR_PTR(ret);
-	if (unlikely(dst_cache_init(&peer->endpoint_cache, GFP_KERNEL)))
+	if (unlikely(dst_cache_init(&peer->endpoint_cache, GFP_KERNEL_ACCOUNT)))
 		goto err;
 
 	peer->device = wg;
@@ -229,7 +229,7 @@ void wg_peer_put(struct wg_peer *peer)
 
 int __init wg_peer_init(void)
 {
-	peer_cache = KMEM_CACHE(wg_peer, 0);
+	peer_cache = KMEM_CACHE(wg_peer, SLAB_ACCOUNT);
 	return peer_cache ? 0 : -ENOMEM;
 }
 
diff --git a/drivers/net/wireguard/peerlookup.c b/drivers/net/wireguard/peerlookup.c
index 99651a86ac95..f333e906014a 100644
--- a/drivers/net/wireguard/peerlookup.c
+++ b/drivers/net/wireguard/peerlookup.c
@@ -21,7 +21,7 @@ static struct hlist_head *pubkey_bucket(struct pubkey_hashtable *table,
 
 struct pubkey_hashtable *wg_pubkey_hashtable_alloc(void)
 {
-	struct pubkey_hashtable *table = kvmalloc_obj(*table);
+	struct pubkey_hashtable *table = kvmalloc_obj(*table, GFP_KERNEL_ACCOUNT);
 
 	if (!table)
 		return NULL;
@@ -82,7 +82,7 @@ static struct hlist_head *index_bucket(struct index_hashtable *table,
 
 struct index_hashtable *wg_index_hashtable_alloc(void)
 {
-	struct index_hashtable *table = kvmalloc_obj(*table);
+	struct index_hashtable *table = kvmalloc_obj(*table, GFP_KERNEL_ACCOUNT);
 
 	if (!table)
 		return NULL;
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
  2026-11-05 14:10 [PATCH net 0/2] wireguard: memcg accounting and initiation race fix Alexander Ildiyarov
  2026-11-05 14:10 ` [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg Alexander Ildiyarov
@ 2026-11-05 14:10 ` Alexander Ildiyarov
  2026-11-05 14:14 ` [PATCH net 0/2] wireguard: memcg accounting and initiation race fix netdev-bot+sinfo
  2 siblings, 0 replies; 4+ messages in thread
From: Alexander Ildiyarov @ 2026-11-05 14:10 UTC (permalink / raw)
  To: Jason A . Donenfeld, wireguard, netdev
  Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-kernel, Alexander Ildiyarov, stable

wg_noise_handshake_consume_initiation() checks the initiation's
timestamp against latest_timestamp, and last_initiation_consumption
against the current time, while holding handshake->lock for reading.
It then drops the lock and takes it again for writing to store the new
state, without repeating the checks.

Incoming handshake packets are spread round-robin over all online CPUs,
so two copies of the same initiation, received back to back, are
processed in parallel. Both copies can pass the checks before either of
them updates latest_timestamp and last_initiation_consumption, so the
replay and flood protection is bypassed. The responder then creates two
responses, each with a new ephemeral key, and installs the keypair of
the last one, while the initiator accepts whichever response arrives
first. When those differ, the session is unusable until the initiator
gives up on it and starts a new handshake, and an attacker who can see
and duplicate initiations can repeat this for every handshake.

The window is narrow, since the expensive DH computations happen before
the check, so it is between up_read() and down_write() only. With netem
duplicating every initiation between two namespaces, no double response
was observed in 200 handshakes on a 4-CPU VM. With a usleep_range(1000,
2000) inserted before down_write() to widen the window, 30 of 50
duplicated initiations produced two responses, and in 12 of 50 cases
the session was unusable after the handshake. With this patch applied,
and the same delay, every duplicate was rejected and all 50 sessions
worked.

Fix this by doing the checks again once the write lock is held, keeping
the read-locked check as a fast path for rejecting replays.

This is the initiation-side counterpart of the response-side race fixed
by:
commit 2a73c9b2c0ca ("wireguard: noise: reject response consumption after intermediate initiation")

Fixes: e7096c131e51 ("net: WireGuard secure network tunnel")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Alexander Ildiyarov <alex@badcode.im>
---
 drivers/net/wireguard/noise.c | 29 ++++++++++++++++++++++-------
 1 file changed, 22 insertions(+), 7 deletions(-)

diff --git a/drivers/net/wireguard/noise.c b/drivers/net/wireguard/noise.c
index 428d3afe3ce4..209127654e65 100644
--- a/drivers/net/wireguard/noise.c
+++ b/drivers/net/wireguard/noise.c
@@ -580,13 +580,23 @@ wg_noise_handshake_create_initiation(struct message_handshake_initiation *dst,
 	return ret;
 }
 
+/* Must hold handshake->lock, for reading or writing */
+static bool initiation_is_replay_or_flood(const struct noise_handshake *handshake,
+					  const u8 timestamp[NOISE_TIMESTAMP_LEN])
+{
+	return memcmp(timestamp, handshake->latest_timestamp,
+		      NOISE_TIMESTAMP_LEN) <= 0 ||
+	       (s64)handshake->last_initiation_consumption +
+			       NSEC_PER_SEC / INITIATIONS_PER_SECOND >
+		       (s64)ktime_get_coarse_boottime_ns();
+}
+
 struct wg_peer *
 wg_noise_handshake_consume_initiation(struct message_handshake_initiation *src,
 				      struct wg_device *wg)
 {
 	struct wg_peer *peer = NULL, *ret_peer = NULL;
 	struct noise_handshake *handshake;
-	bool replay_attack, flood_attack;
 	u8 key[NOISE_SYMMETRIC_KEY_LEN];
 	u8 chaining_key[NOISE_HASH_LEN];
 	u8 hash[NOISE_HASH_LEN];
@@ -594,6 +604,7 @@ wg_noise_handshake_consume_initiation(struct message_handshake_initiation *src,
 	u8 e[NOISE_PUBLIC_KEY_LEN];
 	u8 t[NOISE_TIMESTAMP_LEN];
 	u64 initiation_consumption;
+	bool reject;
 
 	down_read(&wg->static_identity.lock);
 	if (unlikely(!wg->static_identity.has_identity))
@@ -630,17 +641,21 @@ wg_noise_handshake_consume_initiation(struct message_handshake_initiation *src,
 		goto out;
 
 	down_read(&handshake->lock);
-	replay_attack = memcmp(t, handshake->latest_timestamp,
-			       NOISE_TIMESTAMP_LEN) <= 0;
-	flood_attack = (s64)handshake->last_initiation_consumption +
-			       NSEC_PER_SEC / INITIATIONS_PER_SECOND >
-		       (s64)ktime_get_coarse_boottime_ns();
+	reject = initiation_is_replay_or_flood(handshake, t);
 	up_read(&handshake->lock);
-	if (replay_attack || flood_attack)
+	if (reject)
 		goto out;
 
 	/* Success! Copy everything to peer */
 	down_write(&handshake->lock);
+	/* Check again, now that we have an exclusive lock, so that two threads
+	 * processing the same initiation in parallel can't both pass the check
+	 * above before either of them updates the timestamp.
+	 */
+	if (initiation_is_replay_or_flood(handshake, t)) {
+		up_write(&handshake->lock);
+		goto out;
+	}
 	memcpy(handshake->remote_ephemeral, e, NOISE_PUBLIC_KEY_LEN);
 	if (memcmp(t, handshake->latest_timestamp, NOISE_TIMESTAMP_LEN) > 0)
 		memcpy(handshake->latest_timestamp, t, NOISE_TIMESTAMP_LEN);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH net 0/2] wireguard: memcg accounting and initiation race fix
  2026-11-05 14:10 [PATCH net 0/2] wireguard: memcg accounting and initiation race fix Alexander Ildiyarov
  2026-11-05 14:10 ` [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg Alexander Ildiyarov
  2026-11-05 14:10 ` [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation Alexander Ildiyarov
@ 2026-11-05 14:14 ` netdev-bot+sinfo
  2 siblings, 0 replies; 4+ messages in thread
From: netdev-bot+sinfo @ 2026-11-05 14:14 UTC (permalink / raw)
  To: Alexander Ildiyarov
  Cc: Jason A . Donenfeld, wireguard, netdev, Andrew Lunn,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	linux-kernel

Hi!

This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:

 - How the issue was discovered, e.g. hit in production, hit during
   development, syzbot report, manual code inspection, LLM or static
   analysis tool scan.

Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.

The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-11-05 14:14 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-05 14:10 [PATCH net 0/2] wireguard: memcg accounting and initiation race fix Alexander Ildiyarov
2026-11-05 14:10 ` [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg Alexander Ildiyarov
2026-11-05 14:10 ` [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation Alexander Ildiyarov
2026-11-05 14:14 ` [PATCH net 0/2] wireguard: memcg accounting and initiation race fix netdev-bot+sinfo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox