Re: [BUG] ipv4: slab-use-after-free in fib_nhc_update_mtu() - unlocked walk of kfree_rcu'd fib_nh_exception on NETDEV_CHANGEMTU
Ido Schimmel <[email protected]>
| Newsgroups | org.kernel.vger.netdev |
|---|---|
| Message-ID | <20260806123212.GA1868331@shredder> |
Please send plain text emails. HTML emails are filtered and don't make their way to the ML. See a comment at the end. On Thu, Aug 06, 2026 at 03:07:27PM +0900, 안도현 wrote: > HANDLING NOTE > ============= > This bug was identified with the assistance of an automated (AI) code > audit. Per Documentation/process/security-bugs.rst, an AI-assisted > finding must be treated as public, so this is reported in the open and > was not submitted to the [1][email protected] embargo process. > For the same reason, I am not including the reproducer in this public > message. I have a tested source reproducer and will provide it privately > to maintainers on request. > > SUMMARY > ======= > fib_nhc_update_mtu() traverses and writes fib_nh_exception ("fnhe") > objects using rcu_dereference_protected(x, 1) -- asserting a lock is held > -- but its only caller holds RTNL, not the fnhe lock, and is not in an RCU > read-side critical section. fnhe objects are freed with kfree_rcu() from > softirq under fnhe_lock, so on a CONFIG_PREEMPT_RCU kernel a grace period > can end mid-walk and free an fnhe the loop still dereferences. This is a > slab-use-after-free (read and write) of a kmalloc-96 object, reproduced > under KASAN on 7.2-rc6. Present unchanged since v4.19. > > AFFECTED VERSIONS > ================= > Introduced in v4.19 by: > af7d6cce5369 ("net: ipv4: update fnhe_pmtu when first hop's MTU > changes") > which added the loop (then nh_update_mtu(); renamed and exported as > fib_nhc_update_mtu() by 06c77c3e67b0 "ipv4: Rename and export > nh_update_mtu"). The unlocked walk is identical from v4.19 to mainline. > > Reproduced on: > Linux 7.2-rc6, mainline commit 075b74841bd0, arm64, KASAN. > Present in current mainline (fib_semantics.c fib_nhc_update_mtu()). > > THE DEFECT > ========== > net/ipv4/fib_semantics.c, fib_nhc_update_mtu(): > > bucket = rcu_dereference_protected(nhc->nhc_exceptions, 1); > if (!bucket) > return; > for (i = 0; i < FNHE_HASH_SIZE; i++) { > struct fib_nh_exception *fnhe; > for (fnhe = rcu_dereference_protected(bucket[i].chain, 1); fnhe; > fnhe = rcu_dereference_protected(fnhe->fnhe_next, 1)) { > if (fnhe->fnhe_mtu_locked) { > if (new <= fnhe->fnhe_pmtu) { > fnhe->fnhe_pmtu = new; > fnhe->fnhe_mtu_locked = false; > } > } else if (new < fnhe->fnhe_pmtu || orig == fnhe->fnhe_pmtu) { > fnhe->fnhe_pmtu = new; > } > } > } > > The "1" asserts the caller holds the fnhe update-side lock. It does not. > The only caller is fib_sync_mtu() (same file), reached on a device MTU > change: > > NETDEV_CHANGEMTU -> fib_netdev_event() -> fib_sync_mtu() > -> fib_nhc_update_mtu() > > and call_netdevice_notifiers_info() (net/core/dev.c) runs the chain under > ASSERT_RTNL() only -- no rcu_read_lock(), no local_bh_disable(). > > fnhe objects are created and freed under fnhe_lock in softirq on the > ICMP/redirect input path: > > __ip_rt_update_pmtu() / __ip_do_redirect() -> update_or_create_fnhe() > -> fnhe_remove_oldest() / ip_del_fnhe() -> kfree_rcu() > > RTNL and fnhe_lock are unrelated locks; RTNL does not exclude softirq. > Under CONFIG_PREEMPT_RCU a process-context loop that never entered an RCU > read-side critical section is not an RCU reader, so a grace period may end > mid-walk and the kfree_rcu() callback frees an fnhe the loop is still > dereferencing. > > fnhe_lock is static to net/ipv4/route.c and not visible in > fib_semantics.c, which is presumably why the code used the constant > assertion instead of taking the lock. > > KASAN REPORT (7.2-rc6, arm64; timestamps removed, stacks trimmed) > ================================================================= > BUG: KASAN: slab-use-after-free in fib_nhc_update_mtu+0x78/0xd0 > Read of size 1 at addr ffff000016596794 by task payload/100 > > CPU: 0 ... 7.2.0-rc6 #1 PREEMPT > Call trace: > fib_nhc_update_mtu > fib_sync_mtu > fib_netdev_event > call_netdevice_notifiers_info > netif_set_mtu_ext > dev_set_mtu > dev_ioctl > sock_ioctl > > Allocated by task 99: > update_or_create_fnhe > __ip_rt_update_pmtu > icmp_unreach > icmp_rcv > ip_local_deliver > > Freed by task 27: > __rcu_free_sheaf_prepare > rcu_free_sheaf > rcu_core > handle_softirqs > > The buggy address belongs to the cache kmalloc-96 of size 96. > The buggy address is located 20 bytes inside of freed 96-byte region. > > (struct fib_nh_exception is 80 bytes -> kmalloc-96; offset 20 is the bool > fnhe_mtu_locked read at the top of the loop body.) > > CONDITIONS > ========== > - CONFIG_PREEMPT_RCU=y (all CONFIG_PREEMPT / PREEMPT_RT kernels). > - Concurrency between a device MTU change (the walk) and ICMP-induced fnhe > churn/expiry (the alloc + kfree_rcu free). > - Reachability: SIOCSIFMTU on a device carrying routes with exceptions > needs CAP_NET_ADMIN in the device's netns; the fnhe population is driven > by ICMP "fragmentation needed". Where unprivileged user namespaces are > permitted, both are reachable by an unprivileged local user via > unshare(CLONE_NEWUSER|CLONE_NEWNET) + /dev/net/tun; where restricted, > it requires CAP_NET_ADMIN in some netns. > > REPRODUCER > ========== > I have a tested C reproducer that triggers the KASAN report above on > 7.2-rc6/arm64, typically within seconds. Per the handling note it is not > included here; I can provide it privately to maintainers on request. > > IMPACT > ====== > The demonstrated result is a KASAN-detected read, and a write of dev->mtu, > to an RCU-freed kmalloc-96 object (a memory-safety violation). No > privilege-escalation exploit has been developed or demonstrated. > > MITIGATION > ========== > Restricting unprivileged user namespaces, restricting creation of network > namespaces, or denying unprivileged access to /dev/net/tun reduces > exposure to unprivileged local users. None of these protect a context > that already holds CAP_NET_ADMIN in a network namespace. (I have not > verified that these fully block every path.) > > PROPOSED FIX > ============ > fnhe objects are freed via kfree_rcu(), so an RCU read-side critical > section around the traversal keeps them alive for its duration; fnhe_lock > is not visible here, so this is also the least invasive fix. > Best regards, > Dohyeon An Chengfeng Ye sent a fix last week, but looks like we need v2 which acquires fnhe_lock for the per-entry update (while still using RCU for the traversal). https://lore.kernel.org/netdev/[email protected]/