Re: [PATCH net] tcp: ipv6: free ireq->ipv6_opt after an RCU grace period

From
Linkui Xiao <hidden>
Date
2026-11-04 14:11:15
Reply to
Eric Dumazet, 2026-11-03
From: Linkui Xiao <redacted>

Hi Eric,

I wrote a reproducer for this and ran it on both sides of your patch.

Setup
-----
v7.4-rc1 (6628e7d4c6b) on an x86_64 VMware guest with 2 vCPUs,
CONFIG_KASAN=y (generic), CONFIG_SLUB_DEBUG=y, PREEMPT_LAZY. The working
tree only carries the instrumentation described below and your patch (it
reports itself as 7.4.0-rc1-dirty); nothing else differs between the two
runs.

ireq->ipv6_opt has a single in-tree producer (calipso_req_setattr()),
which needs CALIPSO + NetLabel + an MLS SELinux policy, so to keep the
test self-contained I added a debug-only patch, never meant for upstream,
that

  - allocates a fake ipv6_txoptions in tcp_v6_route_req() and stores it
    in ireq->ipv6_opt, i.e. exactly the slot calipso_req_setattr() fills;
  - does an mdelay() inside the rcu_read_lock() section of
    tcp_v6_send_synack(), right after "opt = ireq->ipv6_opt;".

The trigger is a SO_REUSEPORT listener group on ::1 with
net.ipv4.tcp_migrate_req=1: a crafted SYN from a raw socket, a second
listener joining the group, the first one closing so that the request
socket migrates and the clone takes over ipv6_opt, the second listener
closing, and a retransmitted SYN then handled on the other CPU while that
CPU sits in the (deliberately widened) RCU section.

The instrumentation also makes the two sides measurable, which is what
the timestamps below come from: the reader is parked for 3.2s with the
pointer already in a local variable, and the destructor runs in the
middle of that window.

Without your patch
------------------
  t+0.30s  SYNACK #2 on CPU1: rcu_read_lock(), opt = ireq->ipv6_opt,
           then stalled for 3.2s
  t+1.00s  reqsk_timer_handler(): oreq is migrated to the second
           listener, inet_reqsk_clone() makes the clone share ipv6_opt,
           reqsk_migrate_reset(oreq) clears oreq's pointer
  t+3.09s  the clone's timer fires with no listener left to migrate to:
           __reqsk_free() -> tcp_v6_reqsk_destructor() -> kfree()
  t+3.51s  CPU1 resumes and ip6_xmit() reads the options

  BUG: KASAN: slab-use-after-free in ip6_xmit+0x1301/0x19a0
  Read of size 2 at addr ffff88801497908a by task poc/5786
  CPU: 1 UID: 0 PID: 5786 Comm: poc
  Call Trace:
   <IRQ>
   ip6_xmit+0x1301/0x19a0
   tcp_v6_send_synack+0x631/0xbe0
   tcp_rtx_synack+0x116/0x4a0
   tcp_check_req+0xb37/0x23c0
   tcp_v6_rcv+0xfff/0x34a0
  ...
  Freed by task 0 on cpu 0:
   kfree+0x1b1/0x510
   tcp_v6_reqsk_destructor+0x37/0x70
   __reqsk_free+0xd4/0x230
   reqsk_timer_handler+0x83a/0xe10
  ...
  The buggy address belongs to the cache kmalloc-96 of size 96

The dereference lands 416 ms after the free here, which is what makes the
ordering unambiguous; in a real deployment the window is a few
instructions wide, but nothing else changes.

With your patch
---------------
Same parameters, two rounds: the timeline does not move at all (the
destructor still runs at t+3.09s, the reader still dereferences the
pointer at t+3.51s) and there is no report whatsoever. kfree_rcu()
cannot complete a grace period while CPU1 is inside rcu_read_lock(),
which is what the changelog says.

(Note that KASAN reports only once per boot unless kasan_multi_shot is
set, so on the unpatched kernel the second round is silent by
construction. The fixed kernel being silent from the very first round is
the part that matters.)

If you end up respinning for the pktopts issue Sashiko raised, I can
re-run this on the new version. The reproducer and the debug-only patch
are easy to share if anyone wants to look at them.

Tested-by: Linkui Xiao <redacted>
Thread (10 messages) 10 messages, 8 authors, last reply 5d ago
Keyboard shortcuts
hback out one level
jnext message in thread / next series-diff position
kprevious message in thread / previous series-diff position
ldrill in
Escclose help / fold thread tree
?toggle this help