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>