[PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
From: Alexander Ildiyarov
Date: Thu Nov 05 2026 - 09:18:12 EST
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@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Alexander Ildiyarov <alex@xxxxxxxxxx>
---
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