Linux kernel -stable discussions
 help / color / mirror / Atom feed
* [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg
       [not found] <20261105140815.92841-1-alex@badcode.im>
@ 2026-11-05 14:08 ` Alexander Ildiyarov
  2026-11-10 16:52   ` Jason A. Donenfeld
  2026-11-05 14:08 ` [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation Alexander Ildiyarov
  1 sibling, 1 reply; 7+ messages in thread
From: Alexander Ildiyarov @ 2026-11-05 14:08 UTC (permalink / raw)
  To: alexvancasper; +Cc: 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] 7+ messages in thread

* [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
       [not found] <20261105140815.92841-1-alex@badcode.im>
  2026-11-05 14:08 ` [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg Alexander Ildiyarov
@ 2026-11-05 14:08 ` Alexander Ildiyarov
  2026-11-10 16:51   ` Jason A. Donenfeld
  1 sibling, 1 reply; 7+ messages in thread
From: Alexander Ildiyarov @ 2026-11-05 14:08 UTC (permalink / raw)
  To: alexvancasper; +Cc: 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] 7+ messages in thread

* [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
       [not found] <20261105141006.95083-1-alex@badcode.im>
@ 2026-11-05 14:10 ` Alexander Ildiyarov
  2026-11-09 15:20   ` netdev-bot+sashiko
  0 siblings, 1 reply; 7+ 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] 7+ messages in thread

* Re: [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
  2026-11-05 14:10 ` Alexander Ildiyarov
@ 2026-11-09 15:20   ` netdev-bot+sashiko
  0 siblings, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-11-09 15:20 UTC (permalink / raw)
  To: alex
  Cc: Jason, wireguard, netdev, andrew+netdev, davem, edumazet, kuba,
	pabeni, linux-kernel, stable

Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.

Low priority / nit picks for your consideration:

- [Low] The commit message names the response-side fix as `commit
  2a73c9b2c0ca ("wireguard: noise: reject response consumption after…

--

wireguard: noise: recheck replay and flood under write lock on initiation

This commit makes wg_noise_handshake_consume_initiation() run the replay
and flood checks a second time after taking handshake->lock for writing.
Without the second check, two copies of the same initiation processed in
parallel could both get past the read-locked check. The read-locked check
stays 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")

[Severity: Low]
Is this the right hash for the referenced commit? 2a73c9b2c0ca doesn't
resolve here. git log reports it as an unknown revision.

The commit with this subject appears to be:

commit d0305f8d9002 ("wireguard: noise: reject response consumption after intermediate initiation")

Running git blame on the response-side recheck under down_write() in
wg_noise_handshake_consume_response() also points to d0305f8d9002:

    if (handshake->state != state ||
        crypto_memneq(handshake->ephemeral_private, ephemeral_private, NOISE_PUBLIC_KEY_LEN)) {

Could the reference be changed to d0305f8d9002? This patch is tagged for
stable, so readers who follow the cited hash to the related fix would
find the right commit.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261105141006.95083-1-alex%40badcode.im

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

* Re: [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
  2026-11-05 14:08 ` [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation Alexander Ildiyarov
@ 2026-11-10 16:51   ` Jason A. Donenfeld
  2026-11-10 18:08     ` alex
  0 siblings, 1 reply; 7+ messages in thread
From: Jason A. Donenfeld @ 2026-11-10 16:51 UTC (permalink / raw)
  To: Alexander Ildiyarov; +Cc: alexvancasper, stable

On Thu, Nov 05, 2026 at 05:08:15PM +0300, Alexander Ildiyarov wrote:
> 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.

The downside, though, is that now it means there's potential to do flood
attacks that take the exclusive lock, which is what was trying to be
avoided, right?

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

* Re: [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg
  2026-11-05 14:08 ` [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg Alexander Ildiyarov
@ 2026-11-10 16:52   ` Jason A. Donenfeld
  0 siblings, 0 replies; 7+ messages in thread
From: Jason A. Donenfeld @ 2026-11-10 16:52 UTC (permalink / raw)
  To: Alexander Ildiyarov; +Cc: alexvancasper, stable

On Thu, Nov 05, 2026 at 05:08:14PM +0300, Alexander Ildiyarov wrote:
> 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.

I wasn't aware of this trick, I think mostly because most drivers don't
use this. Does any other part of the networking stack do this? Is this a
normal thing?

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

* Re: [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
  2026-11-10 16:51   ` Jason A. Donenfeld
@ 2026-11-10 18:08     ` alex
  0 siblings, 0 replies; 7+ messages in thread
From: alex @ 2026-11-10 18:08 UTC (permalink / raw)
  To: Jason A. Donenfeld; +Cc: alexvancasper, stable

I don't think it changes which packets take the exclusive lock. Before
this patch, every initiation that passed the read-locked check already
went on to take down_write() unconditionally to copy the new state into
the handshake. With the patch, exactly the same set of packets reaches
down_write(). The only difference is that a packet that lost the race
now sees the updated latest_timestamp/last_initiation_consumption and
bails out right away, instead of overwriting the handshake state and
generating a second response. So the time spent under the write lock
goes down for those packets, not up.

Everything else is still filtered under the read lock as before. Once
the first copy has stored its timestamp and consumption time, later
replays, and any initiation within the next 1/INITIATIONS_PER_SECOND,
are rejected at the down_read() fast path and never touch the write
lock. Only copies of the same initiation that are processed truly in
parallel, so at most one per CPU, can get to down_write(), and that was
already the case before.

This is the same pattern the response side uses after 2a73c9b2c0ca:
check under the read lock as a fast path, then check again after
down_write() before committing.

Thanks,
Alexander

-------- Исходное сообщение --------
Тема: Re: [PATCH net 2/2] wireguard: noise: recheck replay and flood 
under write lock on initiation
Дата: 10.11.2026 16:51
От: "Jason A. Donenfeld" <Jason@zx2c4.com>
Кому: Alexander Ildiyarov <alex@badcode.im>

On Thu, Nov 05, 2026 at 05:08:15PM +0300, Alexander Ildiyarov wrote:
> 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.

The downside, though, is that now it means there's potential to do flood
attacks that take the exclusive lock, which is what was trying to be
avoided, right?

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

end of thread, other threads:[~2026-11-10 18:08 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <20261105140815.92841-1-alex@badcode.im>
2026-11-05 14:08 ` [PATCH net 1/2] wireguard: account peer and allowedips allocations to memcg Alexander Ildiyarov
2026-11-10 16:52   ` Jason A. Donenfeld
2026-11-05 14:08 ` [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation Alexander Ildiyarov
2026-11-10 16:51   ` Jason A. Donenfeld
2026-11-10 18:08     ` alex
     [not found] <20261105141006.95083-1-alex@badcode.im>
2026-11-05 14:10 ` Alexander Ildiyarov
2026-11-09 15:20   ` netdev-bot+sashiko

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