FreeBSD setcred(2) Kernel Stack Buffer Overflow — Local Privilege Escalation (CVE-2026-45250)
by venglin (GitHub handle) · 2026-07-05
- Severity
- Critical
- CVE
- CVE-2026-45250
- Category
- binary
- Affected product
- FreeBSD kernel — setcred(2) system call (sys/kern/kern_prot.c)
- Affected versions
- FreeBSD 14.4-RELEASE and stable/14 (confirmed exploitable); FreeBSD 15.0 (vulnerable to DoS only, not known to be exploitable to LPE); FreeBSD main is fixed (commit 000d5b52c19ff3858a6f0cbb405d47713c4267a4, 2025-11-27); FreeBSD 13.x and earlier are not affected (setcred(2) not present)
- Disclosed
- 2026-07-05
- Patch status
- unpatched
Tags
References
Archive entry
intelseclab/poc-archiveMetadata
| Field | Value |
|---|---|
| Date Added | 2026-07-05 |
| Last Updated | 2026-07 |
| Author / Researcher | venglin (GitHub handle) |
| CVE / Advisory | CVE-2026-45250 |
| Category | binary |
| Severity | Critical |
| CVSS Score | Not specified in source |
| Status | PoC |
| Tags | freebsd, kernel, lpe, privilege-escalation, stack-overflow, setcred, smap-bypass, smep-bypass, zfs, kernel-exploit |
| Related | N/A |
Affected Target
| Field | Value |
|---|---|
| Software / System | FreeBSD kernel — setcred(2) system call (sys/kern/kern_prot.c) |
| Versions Affected | FreeBSD 14.4-RELEASE and stable/14 (confirmed exploitable); FreeBSD 15.0 (vulnerable to DoS only, not known to be exploitable to LPE); FreeBSD main is fixed (commit 000d5b52c19ff3858a6f0cbb405d47713c4267a4, 2025-11-27); FreeBSD 13.x and earlier are not affected (setcred(2) not present) |
| Language / Platform | C (FreeBSD amd64 kernel + userland exploit code) |
| Authentication Required | Local (any unprivileged local user account) |
| Network Access Required | No |
Summary
kern_setcred_copyin_supp_groups() in sys/kern/kern_prot.c uses sizeof(*groups) where groups is declared as gid_t **, so the size expression evaluates to 8 bytes (pointer size) instead of the intended 4 bytes (sizeof(gid_t)). When the supplementary-groups count is small enough to take the on-stack path (sc_supp_groups_nb < CRED_SMALLGROUPS_NB, i.e. < 16), this doubles the number of bytes copyin() writes into a fixed-size 16-entry gid_t array on the kernel stack, producing a fully attacker-controlled 60-byte kernel stack buffer overflow reachable by any unprivileged user before any privilege check runs. The repository documents and ships three working PoCs: a minimal kernel-panic DoS, a full LPE exploit for kernels without SMAP/SMEP that hijacks an indirect call in amd64_syscall() to jump to user-space shellcode, and a SMAP/SMEP-safe LPE that redirects the same indirect call chain into a code gadget inside zfs.ko (ZSTD_initCStream_advanced) to plant a forged struct ucred (cr_uid=0) inside a kernel-heap pargs slab, all without any kernel information leak.
Vulnerability Details
Root Cause
In sys/kern/kern_prot.c, function kern_setcred_copyin_supp_groups() (lines ~528-533), the parameter gid_t **const groups is used with sizeof(*groups) in both the allocation size calculation and the copyin() length calculation. Because *groups has type gid_t *, sizeof(*groups) == 8 on LP64 instead of the intended sizeof(gid_t) == 4. On the heap-allocation path this only over-allocates (harmless), but on the stack path — used when sc_supp_groups_nb < CRED_SMALLGROUPS_NB (16) — *groups points at smallgroups[CRED_SMALLGROUPS_NB], a 64-byte on-stack array declared in the caller user_setcred(). The copyin() call then writes sc_supp_groups_nb * 8 bytes into a buffer that only has (CRED_SMALLGROUPS_NB - 1) * 4 = 60 usable bytes beyond the first slot, allowing up to 60 bytes of fully attacker-controlled overflow past the end of smallgroups[] on the kernel stack. Critically, this overflow occurs inside user_setcred() before kern_setcred() performs the priv_check_cred(PRIV_CRED_SETCRED) privilege check, so no privilege is required to trigger it.
Attack Vector
- Call
setcred(SETCREDF_SUPP_GROUPS, &wcred, sizeof(wcred))withwcred.sc_supp_groups_nb == 15andwcred.sc_supp_groupspointing to a 120-byte (15 × 8) attacker-controlled user-space buffer. - The oversized
copyin()overflows 60 bytes pastsmallgroups[]on the kernel stack, corrupting the callee-saved register spill slots (rbx,r12,r13,r14, low 32 bits ofr15) inuser_setcred()’s stack frame. - Because
sys_setcred()’s prologue never saves/restoresr12, the corruptedr12value propagates unchanged through the epilogues up toamd64_syscall(), where it is dereferenced atamd64_syscall+0x155asmov rcx, [r12+0x3f8]followed by an indirect callcall [rcx+0xc8]— both fully attacker-controlled. - No-SMAP/no-SMEP path (
exp2_lpe_no_smap.c): fakestruct thread/struct proc/struct ucred/struct sysentvecobjects are placed in user memory; the indirect call lands on user-space shellcode that sets the calling thread’s credentials to uid 0. - SMAP/SMEP-safe path (
exp_setcred_smap_zfs.c): the indirect call is redirected toZSTD_initCStream_advancedinside the loadedzfs.komodule, whose function body (as an unintended gadget) executestd->td_ucred = rcx + 1. By planting a forgedstruct ucred(cr_uid=0) inside asetproctitle(2)-controlledpargsUMA slab of the parent process, and placing the pointer arithmetic valueK1 = parent_pargs - 1inside a child process’s ownpargsslab, the calling thread’s credential pointer is redirected entirely within kernel heap memory — no user-space pointer is ever dereferenced by the kernel, defeating SMAP/SMEP. Kernel symbols (ZSTD_initCStream_advanced,prison0) are resolved at runtime via the unprivilegedkldnext(2)/kldsym(2)interfaces, requiring no info-leak and no pre-existing knowledge of KASLR-equivalent offsets (FreeBSD’s kernel base is fixed). - Post-exploitation, the thread has effective root for VFS operations; the exploit uses
chown(2)/chmod(2)to install a tiny setuid-root wrapper (wrapper.c) at/tmp/rsh, since callingsetuid(0)directly would panic the kernel (the forged credential’scr_uidinfois not a valid pointer).
Impact
Any unprivileged local user can either panic the kernel (poc_dos.c) or obtain full root privileges (exp2_lpe_no_smap.c, exp_setcred_smap_zfs.c) on FreeBSD 14.4-RELEASE / stable/14 systems. The SMAP/SMEP-safe variant works on essentially any standard FreeBSD server configuration that has a ZFS pool (i.e., zfs.ko loaded), requiring no information leak and no non-default configuration.
Environment / Lab Setup
Target: FreeBSD 14.4-RELEASE amd64, GENERIC kernel
(with zfs.ko loaded for the SMAP/SMEP-safe path — kldload zfs)
Attacker: Local unprivileged user account on the target
Tooling: cc / make (base system toolchain), no third-party dependencies
Verified end-to-end on a qemu64 VM with +smap,+smep CPU features enabled.
Proof of Concept
PoC Script
See
exploits/poc_dos.c,exploits/exp2_lpe_no_smap.c,exploits/exp_setcred_smap_zfs.c,exploits/wrapper.c,exploits/Makefile.setcred_smap_zfs, andexploits/README_setcred_smap_zfs.mdin this folder. Full technical write-up insetcred.txtand the original upstreamUPSTREAM_README.md.
| |
Running exp_setcred_smap_zfs resolves the ZSTD_initCStream_advanced gadget address inside zfs.ko and the prison0 kernel symbol at runtime, triggers a single setcred(2) overflow to redirect a kernel indirect call into that gadget, plants a forged root credential inside a kernel-heap pargs slab, and installs a setuid-root shell wrapper at /tmp/rsh — demonstrating a single-shot, no-info-leak local root exploit that survives SMAP/SMEP.
Detection & Indicators of Compromise
- Kernel panics / crash dumps referencing user_setcred(), kern_setcred(),
or amd64_syscall() in the backtrace.
- setcred(2) syscalls with sc_supp_groups_nb close to but below
CRED_SMALLGROUPS_NB (16), especially value 15, from unprivileged
processes (auditable via FreeBSD's audit(4) subsystem if setcred
is audited).
- Unexpected creation of setuid-root files in world-writable locations
such as /tmp/rsh.
- Anomalous setproctitle(2) calls from unrelated parent/child process
pairs immediately followed by a setcred(2) call.
- kldnext(2)/kldsym(2) enumeration by unprivileged processes shortly
before a privilege escalation event.
Signs of compromise:
- Presence of an unexplained setuid-root binary (e.g.
/tmp/rsh) owned by root but created by a non-root process. - Kernel panic logs mentioning corruption near
user_setcred/kern_setcred/amd64_syscallorZSTD_initCStream_advanced. - A local unprivileged session suddenly obtaining
uid=0without going throughsu,sudo, or a legitimate setuid path.
Remediation
| Action | Detail |
|---|---|
| Primary fix | Apply/backport the fix equivalent to FreeBSD main commit 000d5b52c19ff3858a6f0cbb405d47713c4267a4 (2025-11-27), which refactors kern_setcred_copyin_supp_groups() to use a local gid_t * and replaces both sizeof(*groups) occurrences with sizeof(gid_t). This fix has NOT been backported to stable/14 or releng/14.4 as of the report date — administrators should track FreeBSD Security Advisories for an official patch/backport. |
| Interim mitigation | Restrict local user access where untrusted local accounts exist; there is no reliable userland mitigation since the flaw is a kernel bug reachable pre-privilege-check. Consider disabling non-essential local accounts, monitoring for the IOCs above, and prioritizing the kernel patch/upgrade once available for the 14.4/stable-14 branch. |
References
Notes
Mirrored from https://github.com/venglin/setcred on 2026-07-05.