Security News

Cybersecurity news aggregator

🐧
HIGH Vulnerabilities Reddit r/netsec

New Linux Bridge STP Vulnerability

A use-after-free vulnerability in the Linux kernel's bridge Spanning Tree Protocol (STP) implementation allows local attackers to potentially execute code by deleting a bridge link that is administratively down while kernel STP is enabled and a port is in the LEARNING state, leaving armed timers that later access freed memory. The vulnerability affects Linux kernel versions prior to the patch identified by commit 2a00517db8de4be7df3d483b215c5544fb30a191. The fix is included in the kernel commit and users must apply this patch.
Read Full Article →

Linux Bridge STP Timer Use-After-Free August 5, 2026 SSD Secure Disclosure technical team Vulnerability publication Summary A use-after-free vulnerability in the Linux kernel bridge (net/bridge) Spanning Tree Protocol (STP) implementation. A bridge that is administratively down while kernel STP is enabled, together with a port driven into the LEARNING state, arms periodic STP timers without an IFF_UP guard. The teardown path taken by dellink never synchronously deletes those timers, so the backing net_device (which embeds struct net bridge as private data) is freed with a timer list still queued on a per-CPU timer base. The result is a slab use-after-free in the kmalloc-cg-8k cache. Vendor Response A patch has been introduced to Linux – https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2a00517db8de4be7df3d483b215c5544fb30a191 Credit Two independent security researchers, n132 and sven sze, submitted this during our TyphoonPWN 2026 and won second place in the Linux PE category. Affected Versions Linux Kernel prior to patch 2a00517db8de4be7df3d483b215c5544fb30a191 Root Cause Analysis The Linux software bridge driver maintains per-bridge STP state, including several periodic timers (hello_timer, tcn_timer, topology_change_timer, and per-port timers) that drive the STP state machine. These timers are embedded directly inside struct net_bridge, which itself lives in the private-data region of the bridge’s net_device. The lifetime of those timers is therefore tied to the lifetime of the net_device allocation, and any path that frees the device while a timer remains queued constitutes a use-after-free. Vulnerability Analysis The STP timers are armed when STP is enabled and on port state transitions, via br_stp_enable_bridge() and br_port_state_selection() . Critically, the arming path on a port driven into the LEARNING state contains no IFF_UP guard: it will arm timers on a bridge that is administratively down. The only synchronous deletion of these timers happens in br_stp_disable_bridge() , which is reached from br_dev_stop() on an UP -> DOWN transition (i.e. from ndo_stop ). The problem is the asymmetry between two teardown paths: ndo_stop path: an UP -> DOWN transition runs br_dev_stop() -> br_stp_disable_bridge() , which del_timer_sync() ‘s every STP timer. Safe. dellink path: deleting the bridge link directly runs br_dev_delete() , which never calls br_stp_disable_bridge() . Moreover, unregister_netdevice_many() skips ndo_stop entirely for a device that is already DOWN. Consequently, if a bridge is left DOWN with kernel STP enabled and a port in LEARNING, the STP timers are queued but never synchronously cancelled. When the bridge link is deleted, the net_device backing it is free netdev()’d while a timer list is still linked into a per-CPU timer base. The next time that timer base runs ( __run_timers() in softirq context), it dereferences and fires a timer that points into freed slab memory. Primitive Analysis The freed object is a net_device with the bridge’s net_bridge embedded as private data, which itself contains the STP timer_lists. The crucial property is that these timers are still queued and will fire automatically a few seconds later: when __run_timers() runs the dangling timer, call_timer_fn() executes its function field with RDI pointing at the timer list itself. So as long as we refill the freed slot with a buffer carrying an attacker-controlled function pointer, we obtain a control-flow hijacking primitive. Exploit // net.c #include "net.h" int bring_interface_down_up(const char* ifname, int up) { struct ifreq ifr = {0}; int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) return -1; strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); int res = ioctl(sock, SIOCGIFFLAGS, &ifr); if (res < 0) return -1; if (up) ifr.ifr_flags |= IFF_UP; else ifr.ifr_flags &= ~IFF_UP; res = ioctl(sock, SIOCSIFFLAGS, &ifr); if (res < 0) return -1; close(sock); return 0; } int delete_root_qdisc(const char* ifname) { int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (sock < 0) return -1; struct { struct nlmsghdr nlh; struct tcmsg tcm; char buf[1024]; } req = {0}; req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)); req.nlh.nlmsg_type = RTM_DELQDISC; req.nlh.nlmsg_flags = NLM_F_REQUEST; req.tcm.tcm_family = AF_UNSPEC; req.tcm.tcm_ifindex = if_nametoindex(ifname); req.tcm.tcm_parent = 0xFFFFFFFF; struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK}; int res = sendto(sock, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*)&nladdr, sizeof(nladdr)); if (res < 0) return -1; close(sock); return 0; } int syz_net_reset() { const char* ifname = "lo"; if (bring_interface_down_up(ifname, 0) < 0) { perror("bring_interface_down_up(lo, 0)"); return -1; } if (delete_root_qdisc(ifname) < 0) { perror("delete_root_qdisc(lo)"); return -2; } if (bring_interface_down_up(ifname, 1) < 0) { perror("bring_interface_down_up(lo, 1)"); return -3; } return 0; } void loUp(void){ int sock; struct ifreq ifr; // Open a...

Share this article