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 --- 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