Linux X.25 subsystem development
 help / color / mirror / Atom feed
* slab-use-after-free in x25_transmit_restart_request
@ 2026-09-01 13:10 luckdog
  2026-09-01 14:23 ` Andrew Lunn
  0 siblings, 1 reply; 2+ messages in thread
From: luckdog @ 2026-09-01 13:10 UTC (permalink / raw)
  To: horms, kuba, ms, davem, edumazet, pabeni; +Cc: linux-x25, netdev, linux-kernel

Dear maintainers,


I am reporting a slab use-after-free (UAF) in the X.25 neighbour/timer
path. It was found by static auditing of net/x25 for timer-UAF patterns
and then KASAN-verified. It is isomorphic to CVE-2025-21718 (rose_neigh):
a refcounted neigh with an embedded self-re-arming timer whose free path
does not synchronously stop the timer, so the timer callback can run
after kfree(nb) and dereference freed memory. Observed on Linux v7.3-rc1.
(commit: cee9395acd8043be0644b25c34bfa86623f2b935).


This mail contains: the KASAN evidence, the buggy code, the root cause,
the reproducible PoC (a small in-kernel delay-only helper + a userspace
SABM/UA injector + the shell sequence), and a proposed fix.


Call Trace & Context
==================================================================
BUG: KASAN: slab-use-after-free in x25_transmit_restart_request+0x245/0x260
Read of size 4 at addr ffff888010dabd1c by task swapper/5/0


CPU: 5 UID: 0 PID: 0 Comm: swapper/5 Tainted: G    B  7.3.0-rc1 #13 PREEMPT(lazy)
Call Trace:
 <IRQ>
 x25_transmit_restart_request+0x245/0x260    # reads nb->extended (offset 0x1c)
 x25_t20timer_expiry+0x8d/0x110              # the t20timer callback
 call_timer_fn+0x36/0x2b0
 __run_timers+0x609/0x960
 run_timer_softirq+0x1ba/0x210
 handle_softirqs+0x17f/0x590                 # TIMER_SOFTIRQ
 __irq_exit_rcu+0xc1/0x160
 sysvec_apic_timer_interrupt+0x70/0x80
 </IRQ>


Allocated by task 3088:
 x25_link_device_up+0x47/0x490               # nb allocated on NETDEV_REGISTER(ARPHRD_X25)
 x25_device_event+0x1dd/0x2e0
 register_netdevice / lapbeth_device_event  (ip link set lapb0 up)


Freed by task 3102:
 kfree+0x162/0x450
 __x25_remove_neigh+0x1cd/0x270             # frees nb, does NOT stop the t20timer
 x25_link_device_down+0xb9/0x160            # NETDEV_UNREGISTER(lapb0)
 x25_device_event+0x1f9/0x2e0
 unregister_netdevice_many_notify / lapbeth_device_event / rtnl_dellink  (ip link del vb0)


Second UAF site (timer core writing the freed timer_list):
BUG: KASAN: slab-use-after-free in __run_timers+0x89b/0x960
Write of size 8 at addr ffff888010dabd48 by task swapper/5/0   # offset 0x48 inside freed nb (timer_list field)
==================================================================


The read at offset 0x1c is `nb->extended`; the write at offset 0x48 is
inside the embedded `struct timer_list t20timer`. Both are inside the
freed `struct x25_neigh`.


Execution Flow & Code Context
The t20timer callback dereferences the neigh it is given and re-arms
itself, with no self-held reference:


```c
// net/x25/x25_link.c
static void x25_t20timer_expiry(struct timer_list *t)
{
    struct x25_neigh *nb = timer_container_of(nb, t, t20timer);   /* no ref taken */
    x25_transmit_restart_request(nb);        /* derefs nb->extended, nb->dev... */
    x25_start_t20timer(nb);                  /* mod_timer(&nb->t20timer, ...) — re-arms */
}
```


The free path does not stop the timer before kfree:


```c
// net/x25/x25_link.c
static void __x25_remove_neigh(struct x25_neigh *nb)
{
    if (nb->node.next) {
        list_del(&nb->node);
        x25_neigh_put(nb);                   /* refcount_dec_and_test -> kfree(nb) */
        /* no x25_stop_t20timer() here */
    }
}
```


`x25_stop_t20timer` (used only by `x25_link_terminated`, the NETDEV_DOWN
path) is the *async* `timer_delete` — it cancels a merely-pending timer
but does NOT synchronise a concurrently-running callback and does NOT
prevent the callback from re-arming:


```c
static inline void x25_stop_t20timer(struct x25_neigh *nb)
{
    timer_delete(&nb->t20timer);             /* async; old del_timer semantics */
}
```


`struct x25_neigh` (include/net/x25.h:138-148) is refcounted
(`refcount_t refcnt`) and embeds `struct timer_list t20timer`; the slab
is plain kmalloc (not SLAB_TYPESAFE_BY_RCU). There are no call_rcu/
kfree_rcu/synchronize_rcu/timer_shutdown_sync primitives anywhere in
net/x25.


Root Cause Analysis
This is the textbook "free a struct that still has an armed timer" bug
(the same shape as CVE-2025-21718 / rose_neigh). Two free paths reach
`__x25_remove_neigh` -> `x25_neigh_put` -> `kfree(nb)` without
synchronously stopping the t20timer:


1) Device unregister (NETDEV_UNREGISTER / NETDEV_PRE_TYPE_CHANGE):
     x25_device_event -> x25_link_device_down -> __x25_remove_neigh -> kfree
   For an UP device, NETDEV_DOWN is sent first and calls
   x25_link_terminated -> x25_stop_t20timer (= async timer_delete). That
   cancels a merely-pending timer but, if the callback is mid-execution,
   timer_delete returns without waiting and the callback's
   x25_start_t20timer re-arms it; the subsequent NETDEV_UNREGISTER then
   kfrees nb while the timer is pending again -> it fires after kfree ->
   UAF. This path is a race (narrow; needs the callback mid-execution
   during the DOWN->UNREGISTER window).


2) Module unload (rmmod x25 -> x25_exit -> x25_link_free):
     x25_link_free -> __x25_remove_neigh -> kfree
   This path does NOT call x25_link_terminated and does NOT call
   x25_stop_t20timer at all, so if the t20timer is armed when the module
   is unloaded, kfree happens with the timer still pending -> the timer
   fires after kfree -> UAF. By analysis this path is deterministic (not a
   race): the timer is pending, the freer never cancels it, so it will
   fire after kfree. (I did not switch CONFIG_X25=m to exercise this path
   under KASAN; the KASAN evidence below was obtained on path 1.)


In both paths the freer never synchronises the timer against the
callback. The callback unconditionally re-arms (x25_start_t20timer) and
dereferences nb (x25_transmit_restart_request reads nb->extended), so
once kfree beats the callback (path 1 race) or simply precedes the next
fire (path 2 deterministic), the callback/timer-core touches freed memory.


Potential Impact
Local. Path 1 requires CAP_NET_ADMIN (creating/bringing up an X.25 device
and unregistering it — e.g. via lapbether over a veth pair). Path 2
requires CAP_SYS_MODULE (rmmod x25). Best characterised as a local DoS
(kernel panic/Oops under KASAN; potential memory corruption otherwise)
within the CAP_NET_ADMIN/CAP_SYS_MODULE boundary, not an unprivileged LPE.
It is the same calibre as CVE-2025-21718 (rose_neigh).


==================================================================
Reproducer
==================================================================
The race in path 1 is narrow (the callback's post-kfree window must
overlap the freer). To make it reproducible under KASAN I added a
*gated, default-off* busy-wait in the callback. This is delay-only: it
does NOT change the execution path. The callback still runs the same
vanilla x25_transmit_restart_request + x25_start_t20timer; the freer
still goes through the same vanilla __x25_remove_neigh -> kfree which
still does not stop the timer. The helper only widens the callback's
lifetime so the freer's kfree lands inside it. mdelay (not msleep) is
used because x25_t20timer_expiry runs in TIMER_SOFTIRQ, which must not
sleep. With delay=0 the kernel behaves exactly as upstream.


--- Helper patch (kernel, apply on top of v7.3-rc1) ---
The following is the complete in-kernel helper. The pr_info lines are
optional tracing (not needed for the repro to function; can be dropped);
the repro-relevant parts are the includes, the debugfs toggle, and the
gated mdelay.


```diff
--- a/net/x25/x25_link.c
+++ b/net/x25/x25_link.c
@@ -22,6 +22,8 @@
 #include <linux/kernel.h>
 #include <linux/jiffies.h>
 #include <linux/timer.h>
 #include <linux/slab.h>
+#include <linux/delay.h>
+#include <linux/debugfs.h>
 #include <linux/netdevice.h>
 #include <linux/skbuff.h>
 #include <linux/uaccess.h>
@@ -33,6 +35,16 @@ DEFINE_RWLOCK(x25_neigh_list_lock);


 LIST_HEAD(x25_neigh_list);
 DEFINE_RWLOCK(x25_neigh_list_lock);


+/* REPRO (delay-only): tunable delay for x25_t20timer_expiry. Default 0=no-op. */
+unsigned int x25_repro_cb_delay_ms;
+
+static int __init x25_repro_debugfs_init(void)
+{
+	debugfs_create_u32("x25_repro_cb_delay_ms", 0644, NULL,
+			   &x25_repro_cb_delay_ms);
+	return 0;
+}
+late_initcall(x25_repro_debugfs_init);
+
 static void x25_t20timer_expiry(struct timer_list *);
@@ -60,7 +72,17 @@ static void x25_t20timer_expiry(struct timer_list *t)
 static void x25_t20timer_expiry(struct timer_list *t)
 {
  struct x25_neigh *nb = timer_container_of(nb, t, t20timer);


+	/* REPRO (delay-only): widen the callback's lifetime so a concurrent
+	 * freer (x25_link_device_down/__x25_remove_neigh -> kfree(nb)) can
+	 * free nb while this callback is still running. After the busy-wait,
+	 * the post-delay derefs (x25_transmit_restart_request + x25_start_t20timer
+	 * = mod_timer on &nb->t20timer) hit freed nb. Uses mdelay (NOT msleep)
+	 * because this runs in TIMER_SOFTIRQ which must not sleep. No-op unless
+	 * /sys/kernel/debug/x25_repro_cb_delay_ms > 0. NOT a fix. */
+	if (x25_repro_cb_delay_ms)
+		mdelay(x25_repro_cb_delay_ms);
+
  x25_transmit_restart_request(nb);
  x25_start_t20timer(nb);
 }
```
(Optional tracing pr_info lines added in x25_link_established and
__x25_remove_neigh to confirm timer-arming and the free path; omitted
here for brevity — they are pure printk and do not change logic.)


--- Userspace PoC: sabm_inject.c ---
Bring lapb0's L2 up so x25 arms the t20timer. lapbether's LAPB is DTE;
sending a LAPB SABM (then a UA to complete state_1) on the veth peer
causes lapb0 to connect -> x25_link_established -> arm t20timer. Build
statically (gcc -static -o sabm_inject sabm_inject.c) and run as root in
the guest.


```c
/* sabm_inject.c — inject LAPB SABM then UA on a veth peer so lapbether's
 * lapb0 connects and x25 arms the t20timer.
 * Build: gcc -static -o sabm_inject sabm_inject.c
 * Usage: sabm_inject <ifname>   (send on ifname; peer end's lapb receives)
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <linux/if.h>
#include <arpa/inet.h>


#define ETH_P_DEC 0x6000


int main(int argc, char **argv){
    if(argc < 2){ fprintf(stderr, "usage: %s <ifname> [count]\n", argv[0]); return 1; }
    int count = (argc >= 3) ? atoi(argv[2]) : 1;


    int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if(s < 0){ perror("socket"); return 1; }


    struct ifreq ifr; memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, argv[1], IFNAMSIZ-1);
    if(ioctl(s, SIOCGIFINDEX, &ifr) < 0){ perror("ioctl SIOCGIFINDEX"); return 1; }
    int ifindex = ifr.ifr_ifindex;


    /* Send SABM then UA to cover both lapb state_0 (SABM->connect_indication)
     * and state_1 (lapb self-sent SABM, UA->connect_confirmation).
     * Frame: dst(bcast)+src+ethertype(ETH_P_DEC)+lapbether len(2 LE)+LAPB(addr,ctrl).
     * SABM: ADDR_A(0x03) command, control SABM|P=1 (0x3F).
     * UA:   ADDR_B(0x01) response, control UA|F=1   (0x73). */
    unsigned char sabm[18] = {
        0xff,0xff,0xff,0xff,0xff,0xff, 0x00,0x11,0x22,0x33,0x44,0x55,
        0x60,0x00, 0x02,0x00, 0x03,0x3f
    };
    unsigned char ua[18] = {
        0xff,0xff,0xff,0xff,0xff,0xff, 0x00,0x11,0x22,0x33,0x44,0x55,
        0x60,0x00, 0x02,0x00, 0x01,0x73
    };


    struct sockaddr_ll dst; memset(&dst, 0, sizeof(dst));
    dst.sll_family   = AF_PACKET;
    dst.sll_protocol = htons(ETH_P_DEC);
    dst.sll_ifindex  = ifindex;
    dst.sll_halen    = 6;
    memset(dst.sll_addr, 0xff, 6);


    for(int i = 0; i < count; i++){
        int n;
        n = sendto(s, sabm, sizeof(sabm), 0, (struct sockaddr*)&dst, sizeof(dst));
        if(n < 0){ perror("sendto sabm"); return 1; }
        usleep(200000); /* 200ms: let lapb0 self-send SABM / process */
        n = sendto(s, ua, sizeof(ua), 0, (struct sockaddr*)&dst, sizeof(dst));
        if(n < 0){ perror("sendto ua"); return 1; }
        if(count > 1) usleep(500000);
    }
    printf("sent %d SABM+UA pair(s) on %s\n", count, argv[1]);
    return 0;
}
```


--- Reproduction sequence (root, KASAN kernel with X25=y/LAPB=y/LAPBETHER=y) ---


    # 1. set t20 to the sysctl minimum (1*HZ = 1s; HZ=100 here)
    echo 100 > /proc/sys/net/x25/restart_request_timeout
    # 2. create a veth pair; lapbether auto-creates lapb0 (on vb0) + lapb1 (on veth0)
    ip link add dev vb0 type veth peer name veth1
    ip link set dev vb0 up
    ip link set dev veth0 up
    ip link set dev lapb0 up
    # 3. enable the delay helper (5s busy-wait in the callback)
    echo 5000 > /sys/kernel/debug/x25_repro_cb_delay_ms
    # 4. bring lapb0's L2 up: inject SABM then UA on the veth peer (vb0's peer is veth0)
    ./sabm_inject veth0
    #    -> lapb0 connected -> x25_link_established -> t20timer armed (state_2)
    #    -> timer fires -> callback busy-waits 5s
    # 5. trigger the freer: delete the underlying veth -> lapb0 unregistered
    ip link del vb0
    #    NETDEV_DOWN  -> x25_link_terminated (async timer_delete; callback running -> not cancelled)
    #    NETDEV_UNREGISTER -> x25_link_device_down -> __x25_remove_neigh -> kfree(nb)
    #    callback wakes -> x25_transmit_restart_request(nb) reads freed nb -> KASAN


Expected KASAN output (produced):
    BUG: KASAN: slab-use-after-free in x25_transmit_restart_request+0x245/0x260
      Read of size 4 at ...dabd1c (nb->extended @ off 0x1c)
      x25_t20timer_expiry+0x8d/0x110 <- call_timer_fn <- __run_timers (TIMER_SOFTIRQ)
    BUG: KASAN: slab-use-after-free in __run_timers+0x89b/0x960
      Write of size 8 at ...dabd48 (timer_list field @ off 0x48 inside freed nb)
    Allocated by: x25_link_device_up
    Freed by: kfree <- __x25_remove_neigh <- x25_link_device_down (NETDEV_UNREGISTER)


The freer stack is the natural x25_link_device_down -> __x25_remove_neigh
-> kfree path (no helper in the freer); the access stack is the vanilla
x25_transmit_restart_request <- x25_t20timer_expiry callback. The core
race is on the real vanilla code; the helper only widens the callback
window.


Why the helper is valid evidence (and what it does NOT prove):
- UNCHANGED vs upstream: the reader (x25_t20timer_expiry callback) still
  runs vanilla x25_transmit_restart_request + x25_start_t20timer; the
  freer still runs vanilla __x25_remove_neigh -> kfree which still does
  not stop the timer. The freer is invoked by the natural NETDEV_UNREGISTER
  path (ip link del), not by the helper.
- REPLACED: nothing. The helper only inserts a gated busy-wait at the
  start of the callback (delay=0 -> no-op -> vanilla behaviour).
- PROVEN: when the callback is running (in the busy-wait) and the freer
  kfrees nb, the callback's post-busy-wait derefs of freed nb are a real
  slab-use-after-free (KASAN slab-use-after-free, exact field offsets
  0x1c/0x48). The defect is not a false positive.
- NOT PROVEN by this run: the deterministic module-unload path (rmmod x25
  -> x25_link_free -> kfree with no timer stop) needs CONFIG_X25=m; it is
  deterministic by analysis (the timer is pending and never cancelled, so
  it fires after kfree) and the proposed fix covers it, but I did not
  switch X25=m to exercise it under KASAN.


Proposed Fix
The freer must synchronously stop the timer (and prevent re-arm) before
the final put/kfree, and the existing stop must be synchronous:


```diff
--- a/net/x25/x25_link.c
+++ b/net/x25/x25_link.c
@@ static inline void x25_stop_t20timer(struct x25_neigh *nb)
-	timer_delete(&nb->t20timer);
+	timer_shutdown_sync(&nb->t20timer);   /* sync running callback + prevent re-arm */
@@ static void __x25_remove_neigh(struct x25_neigh *nb)
  if (nb->node.next) {
+		x25_stop_t20timer(nb);          /* stop before put/kfree (covers UNREGISTER + module unload) */
   list_del(&nb->node);
   x25_neigh_put(nb);
  }
```


`timer_shutdown_sync` (the modern replacement for del_timer_sync that
also disables the timer so the callback cannot re-arm it) fixes both the
async `timer_delete` weakness in `x25_link_terminated` (NETDEV_DOWN) and
the "no stop at all" in `__x25_remove_neigh` (NETDEV_UNREGISTER and
module-unload). An alternative/complementary fix is to make
`x25_t20timer_expiry` take a neigh reference for the duration of the
callback (x25_neigh_hold on entry, x25_neigh_put on exit) so the freer's
put cannot reach 0 while the callback runs; the timer_shutdown_sync
change above is the smaller, more local fix and mirrors the fix pattern
for CVE-2025-21718.


I would be grateful if the maintainers could assess practical severity
and pick a fix. If you have fixed this bug, 
please add "reported by: Jianzhou Zhao".


Best regards,
Jianzhou Zhao


^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: slab-use-after-free in x25_transmit_restart_request
  2026-09-01 13:10 slab-use-after-free in x25_transmit_restart_request luckdog
@ 2026-09-01 14:23 ` Andrew Lunn
  0 siblings, 0 replies; 2+ messages in thread
From: Andrew Lunn @ 2026-09-01 14:23 UTC (permalink / raw)
  To: luckdog
  Cc: horms, kuba, ms, davem, edumazet, pabeni, linux-x25, netdev,
	linux-kernel

On Tue, Sep 01, 2026 at 09:10:03PM +0800, luckdog wrote:
> Dear maintainers,
> 
> 
> I am reporting a slab use-after-free (UAF) in the X.25 neighbour/timer
> path. It was found by static auditing of net/x25 for timer-UAF patterns
> and then KASAN-verified. It is isomorphic to CVE-2025-21718 (rose_neigh):
> a refcounted neigh with an embedded self-re-arming timer whose free path
> does not synchronously stop the timer, so the timer callback can run
> after kfree(nb) and dereference freed memory. Observed on Linux v7.3-rc1.
> (commit: cee9395acd8043be0644b25c34bfa86623f2b935).
> 
> 
> This mail contains: the KASAN evidence, the buggy code, the root cause,
> the reproducible PoC (a small in-kernel delay-only helper + a userspace
> SABM/UA injector + the shell sequence), and a proposed fix.

Please could you just submit a proper fix patch.

    Andrew

---
pw-bot: cr

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-09-01 14:23 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-01 13:10 slab-use-after-free in x25_transmit_restart_request luckdog
2026-09-01 14:23 ` Andrew Lunn

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox