[PATCH] xfrm: esp4: use current ESN high bits for IV and AAD
From: Jérémy Jean
Date: Fri Sep 25 2026 - 05:54:12 EST
esp_xmit() saves the low half of the current packet sequence number
before advancing the GSO sequence state, but builds esp.seqno with the
high half after the advance. When the low half wraps, the packet whose
transmitted sequence number is 0xffffffff is encrypted as though it
belonged to the next sequence-number cycle. With AES-GCM, this assigns
the boundary packet the same nonce as the packet sent one complete
32-bit sequence-number cycle later. esp_output_set_extra() also reads
the advanced high half when constructing the associated data, so the
two packets use identical associated data.
Because GCM uses CTR mode for encryption, known plaintext from either
record reveals the corresponding plaintext in the other. More
importantly, reusing the nonce makes the GHASH authentication key
recoverable, allowing an attacker to forge valid tags for arbitrary
ciphertexts under that nonce and key.
Starting from sequence number zero, reaching the faulty packet requires
2^32 - 1 outbound ESP packet sequence increments. Reusing its nonce
requires another 2^32 increments under the same AES-GCM key, for
2^33 - 1 increments in total.
Snapshot both halves of the current sequence before changing the GSO
state. Derive the authenticated high half from that same immutable
sequence value so the IV and associated data cannot diverge.
Fixes: 4b549ccce941 ("xfrm: replay: Fix ESN wrap around for GSO")
Assisted-by: LLM
Signed-off-by: Jérémy Jean <Jeremy.Jean@xxxxxxxxxxxxxxxxx>
---
net/ipv4/esp4.c | 13 ++++---------
net/ipv4/esp4_offload.c | 6 ++++--
2 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c
index e76db5817e78..04f27c41ea50 100644
--- a/net/ipv4/esp4.c
+++ b/net/ipv4/esp4.c
@@ -271,20 +271,15 @@ static void esp_output_restore_header(struct sk_buff *skb)
static struct ip_esp_hdr *esp_output_set_extra(struct sk_buff *skb,
struct xfrm_state *x,
struct ip_esp_hdr *esph,
- struct esp_output_extra *extra)
+ struct esp_output_extra *extra,
+ __be64 seqno)
{
/* For ESN we move the header forward by 4 bytes to
* accommodate the high bits. We will move it back after
* encryption.
*/
if ((x->props.flags & XFRM_STATE_ESN)) {
- __u32 seqhi;
- struct xfrm_offload *xo = xfrm_offload(skb);
-
- if (xo)
- seqhi = xo->seq.hi;
- else
- seqhi = XFRM_SKB_CB(skb)->seq.output.hi;
+ __u32 seqhi = upper_32_bits(be64_to_cpu(seqno));
extra->esphoff = (unsigned char *)esph -
skb_transport_header(skb);
@@ -543,7 +538,7 @@ int esp_output_tail(struct xfrm_state *x, struct sk_buff *skb, struct esp_info *
else
dsg = &sg[esp->nfrags];
- esph = esp_output_set_extra(skb, x, esp->esph, extra);
+ esph = esp_output_set_extra(skb, x, esp->esph, extra, esp->seqno);
esp->esph = esph;
sg_init_table(sg, esp->nfrags);
diff --git a/net/ipv4/esp4_offload.c b/net/ipv4/esp4_offload.c
index abd77162f5e7..79f7d08325c5 100644
--- a/net/ipv4/esp4_offload.c
+++ b/net/ipv4/esp4_offload.c
@@ -272,7 +272,7 @@ static int esp_xmit(struct xfrm_state *x, struct sk_buff *skb, netdev_features_
struct crypto_aead *aead;
struct esp_info esp;
bool hw_offload = true;
- __u32 seq;
+ __u32 seq, seq_hi;
int encap_type = 0;
esp.inplace = true;
@@ -315,7 +315,9 @@ static int esp_xmit(struct xfrm_state *x, struct sk_buff *skb, netdev_features_
return esp.nfrags;
}
+ /* Keep the sequence used by this packet before advancing GSO state. */
seq = xo->seq.low;
+ seq_hi = xo->seq.hi;
esph = esp.esph;
esph->spi = x->id.spi;
@@ -334,7 +336,7 @@ static int esp_xmit(struct xfrm_state *x, struct sk_buff *skb, netdev_features_
if (xo->seq.low < seq)
xo->seq.hi++;
- esp.seqno = cpu_to_be64(seq + ((u64)xo->seq.hi << 32));
+ esp.seqno = cpu_to_be64(seq + ((u64)seq_hi << 32));
if (hw_offload && encap_type == UDP_ENCAP_ESPINUDP) {
/* In the XFRM stack, the encapsulation protocol is set to iphdr->protocol by
--
2.47.3