From: Alexander Ildiyarov <alex@badcode.im>
To: "Jason A . Donenfeld" <Jason@zx2c4.com>,
wireguard@lists.zx2c4.com, netdev@vger.kernel.org
Cc: Andrew Lunn <andrew+netdev@lunn.ch>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@kernel.org>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
linux-kernel@vger.kernel.org,
Alexander Ildiyarov <alex@badcode.im>,
stable@vger.kernel.org
Subject: [PATCH net 2/2] wireguard: noise: recheck replay and flood under write lock on initiation
Date: Thu, 5 Nov 2026 17:10:06 +0300 [thread overview]
Message-ID: <20261105141006.95083-3-alex@badcode.im> (raw)
In-Reply-To: <20261105141006.95083-1-alex@badcode.im>
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
next prev parent reply other threads:[~2026-11-05 14:10 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
2026-11-05 14:14 ` [PATCH net 0/2] wireguard: memcg accounting and initiation race fix netdev-bot+sinfo
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261105141006.95083-3-alex@badcode.im \
--to=alex@badcode.im \
--cc=Jason@zx2c4.com \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=stable@vger.kernel.org \
--cc=wireguard@lists.zx2c4.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox