From: "Jérémy Jean" <jeremy.jean@oss.cyber.gouv.fr>
To: "Jason A. Donenfeld" <Jason@zx2c4.com>
Cc: wireguard@lists.zx2c4.com, netdev@vger.kernel.org,
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, stable@vger.kernel.org
Subject: Re: [PATCH net] wireguard: noise: reject responses for replaced initiations
Date: Tue, 06 Oct 2026 14:40:36 +0200 [thread overview]
Message-ID: <fbeae2e157e335504157b6a6683fb505@oss.cyber.gouv.fr> (raw)
In-Reply-To: <asSo7kefA3IQFpQp@zx2c4.com>
On 2026-10-06 09:53, Jason A. Donenfeld wrote:
> On Mon, Oct 05, 2026 at 11:18:33PM +0200, Jason A. Donenfeld wrote:
>> Hi,
>>
>> On Mon, Oct 05, 2026 at 08:35:55PM +0000, Jérémy Jean wrote:
>> > WireGuard can accept an old handshake response after starting a new
>> > handshake. This reinstalls old keys and resets transport counters and
>> > replay state, enabling nonce reuse, replay and packet forgery. This
>> > breaks confidentiality and integrity guarantees.
>> >
>> > Compare ephemeral secrets under the write lock to reject responses for
>> > replaced initiations.
>> >
>> > Fixes: e7096c131e51 ("net: WireGuard secure network tunnel")
>> > Cc: stable@vger.kernel.org
>> > Assisted-by: LLM
>> > Signed-off-by: Jérémy Jean <Jeremy.Jean@oss.cyber.gouv.fr>
>> > ---
>> > drivers/net/wireguard/noise.c | 8 +++++---
>> > 1 file changed, 5 insertions(+), 3 deletions(-)
>> >
>> > diff --git a/drivers/net/wireguard/noise.c b/drivers/net/wireguard/noise.c
>> > index 9c0a09bf6c95..88cc9acc7dc7 100644
>> > --- a/drivers/net/wireguard/noise.c
>> > +++ b/drivers/net/wireguard/noise.c
>> > @@ -784,10 +784,12 @@ wg_noise_handshake_consume_response(struct message_handshake_response *src,
>> >
>> > /* Success! Copy everything to peer */
>> > down_write(&handshake->lock);
>> > - /* It's important to check that the state is still the same, while we
>> > - * have an exclusive lock.
>> > + /* Check that this is still the initiation we authenticated against,
>> > + * while we have an exclusive lock.
>> > */
>> > - if (handshake->state != state) {
>> > + if (handshake->state != state ||
>> > + crypto_memneq(handshake->ephemeral_private, ephemeral_private,
>> > + NOISE_PUBLIC_KEY_LEN)) {
>> > up_write(&handshake->lock);
>> > goto fail;
>> > }
>>
>> Could you describe the flow that you think causes a bug? Trying
>> to recreate your mental model. Something like this?
>>
>> == thread 1 ==
>>
>> down_read(&handshake->lock);
>> state = handshake->state;
>> memcpy(hash, handshake->hash, NOISE_HASH_LEN);
>> memcpy(chaining_key, handshake->chaining_key, NOISE_HASH_LEN);
>> memcpy(ephemeral_private, handshake->ephemeral_private,
>> NOISE_PUBLIC_KEY_LEN);
>> memcpy(preshared_key, handshake->preshared_key,
>> NOISE_SYMMETRIC_KEY_LEN);
>> up_read(&handshake->lock);
>>
>> if (state != HANDSHAKE_CREATED_INITIATION)
>> goto fail;
>>
>> /* e */
>> message_ephemeral(e, src->unencrypted_ephemeral, chaining_key, hash);
>>
>> /* ee */
>> if (!mix_dh(chaining_key, NULL, ephemeral_private, e))
>> goto fail;
>>
>> /* se */
>> if (!mix_dh(chaining_key, NULL, wg->static_identity.static_private,
>> e))
>> goto fail;
>>
>> /* psk */
>> mix_psk(chaining_key, hash, key, preshared_key);
>>
>> /* {} */
>> if (!message_decrypt(NULL, src->encrypted_nothing,
>> sizeof(src->encrypted_nothing), key, hash))
>> goto fail;
>>
>> == thread 2 ==
>>
>> down_read(&handshake->static_identity->lock);
>> down_write(&handshake->lock);
>>
>> if (unlikely(!handshake->static_identity->has_identity))
>> goto out;
>>
>> dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION);
>>
>> handshake_init(handshake->chaining_key, handshake->hash,
>> handshake->remote_static);
>>
>> /* e */
>> curve25519_generate_secret(handshake->ephemeral_private);
>> if (!curve25519_generate_public(dst->unencrypted_ephemeral,
>> handshake->ephemeral_private))
>> goto out;
>> message_ephemeral(dst->unencrypted_ephemeral,
>> dst->unencrypted_ephemeral, handshake->chaining_key,
>> handshake->hash);
>>
>> /* es */
>> if (!mix_dh(handshake->chaining_key, key,
>> handshake->ephemeral_private,
>> handshake->remote_static))
>> goto out;
>>
>> /* s */
>> message_encrypt(dst->encrypted_static,
>> handshake->static_identity->static_public,
>> NOISE_PUBLIC_KEY_LEN, key, handshake->hash);
>>
>> /* ss */
>> if (!mix_precomputed_dh(handshake->chaining_key, key,
>> handshake->precomputed_static_static))
>> goto out;
>>
>> /* {t} */
>> tai64n_now(timestamp);
>> message_encrypt(dst->encrypted_timestamp, timestamp,
>> NOISE_TIMESTAMP_LEN, key, handshake->hash);
>>
>> dst->sender_index = wg_index_hashtable_insert(
>> handshake->entry.peer->device->index_hashtable,
>> &handshake->entry);
>>
>> handshake->state = HANDSHAKE_CREATED_INITIATION;
>> ret = true;
>>
>> out:
>> up_write(&handshake->lock);
>> up_read(&handshake->static_identity->lock);
>>
>> == thread 1 ==
>>
>> down_write(&handshake->lock);
>> /* It's important to check that the state is still the same, while we
>> * have an exclusive lock.
>> */
>> if (handshake->state != state) {
>> up_write(&handshake->lock);
>> goto fail;
>> }
>> memcpy(handshake->remote_ephemeral, e, NOISE_PUBLIC_KEY_LEN);
>> memcpy(handshake->hash, hash, NOISE_HASH_LEN);
>> memcpy(handshake->chaining_key, chaining_key, NOISE_HASH_LEN);
>> handshake->remote_index = src->sender_index;
>> handshake->state = HANDSHAKE_CONSUMED_RESPONSE;
>> up_write(&handshake->lock);
>> ret_peer = peer;
>> goto out;
>>
>> And now begin_session is called on the older completed handshake
>> rather
>> than the half-completed newer handshake?
>>
>> Or did you see some other flow?
>
> Okay I think I worked something plausible out:
>
> - two threads begin processing the same response
> - the first succeeds. the second gets halfway, when it blocks on taking
> a lock that a queued initiation has taken
> - the queued initiation does its thing and resets the state
> - the second thread resumes and completes the old handshake and
> reinstalls keys
>
> I'll continue analyzing real world feasibility, but in all cases, thank
> you for the patch.
>
> Jason
Hello Jason,
Thanks for your quick reply. Sorry that I could not answer in time.
Your analysis is indeed the scenario that I had in mind, yet, the
second thread does not really have to block, I'd say it can just
be slow?
As for real world feasibility and impacts, I must admit that
I have no idea.
Regards,
Jérémy
next prev parent reply other threads:[~2026-10-06 12:40 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-05 20:35 Jérémy Jean
2026-10-05 20:43 ` netdev-bot+sinfo
2026-10-05 21:18 ` Jason A. Donenfeld
2026-10-06 7:53 ` Jason A. Donenfeld
2026-10-06 12:40 ` Jérémy Jean [this message]
2026-10-06 15:18 ` Jason A. Donenfeld
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=fbeae2e157e335504157b6a6683fb505@oss.cyber.gouv.fr \
--to=jeremy.jean@oss.cyber.gouv.fr \
--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
all inboxes | Powered by JetHome®