PoC Archive PoC Archive
Critical CVE-2026-45250 unpatched

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

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researchervenglin (GitHub handle)
CVE / AdvisoryCVE-2026-45250
Categorybinary
SeverityCritical
CVSS ScoreNot specified in source
StatusPoC
Tagsfreebsd, kernel, lpe, privilege-escalation, stack-overflow, setcred, smap-bypass, smep-bypass, zfs, kernel-exploit
RelatedN/A

Affected Target

FieldValue
Software / SystemFreeBSD kernel — setcred(2) system call (sys/kern/kern_prot.c)
Versions AffectedFreeBSD 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 / PlatformC (FreeBSD amd64 kernel + userland exploit code)
Authentication RequiredLocal (any unprivileged local user account)
Network Access RequiredNo

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

  1. Call setcred(SETCREDF_SUPP_GROUPS, &wcred, sizeof(wcred)) with wcred.sc_supp_groups_nb == 15 and wcred.sc_supp_groups pointing to a 120-byte (15 × 8) attacker-controlled user-space buffer.
  2. The oversized copyin() overflows 60 bytes past smallgroups[] on the kernel stack, corrupting the callee-saved register spill slots (rbx, r12, r13, r14, low 32 bits of r15) in user_setcred()’s stack frame.
  3. Because sys_setcred()’s prologue never saves/restores r12, the corrupted r12 value propagates unchanged through the epilogues up to amd64_syscall(), where it is dereferenced at amd64_syscall+0x155 as mov rcx, [r12+0x3f8] followed by an indirect call call [rcx+0xc8] — both fully attacker-controlled.
  4. No-SMAP/no-SMEP path (exp2_lpe_no_smap.c): fake struct thread/struct proc/struct ucred/struct sysentvec objects are placed in user memory; the indirect call lands on user-space shellcode that sets the calling thread’s credentials to uid 0.
  5. SMAP/SMEP-safe path (exp_setcred_smap_zfs.c): the indirect call is redirected to ZSTD_initCStream_advanced inside the loaded zfs.ko module, whose function body (as an unintended gadget) executes td->td_ucred = rcx + 1. By planting a forged struct ucred (cr_uid=0) inside a setproctitle(2)-controlled pargs UMA slab of the parent process, and placing the pointer arithmetic value K1 = parent_pargs - 1 inside a child process’s own pargs slab, 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 unprivileged kldnext(2)/kldsym(2) interfaces, requiring no info-leak and no pre-existing knowledge of KASLR-equivalent offsets (FreeBSD’s kernel base is fixed).
  6. 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 calling setuid(0) directly would panic the kernel (the forged credential’s cr_uidinfo is 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, and exploits/README_setcred_smap_zfs.md in this folder. Full technical write-up in setcred.txt and the original upstream UPSTREAM_README.md.

1
2
3
4
5
6
7
8
cc -o poc_dos poc_dos.c && ./poc_dos

cc -o exp2_lpe_no_smap exp2_lpe_no_smap.c && ./exp2_lpe_no_smap

make -f Makefile.setcred_smap_zfs        # builds exp_setcred_smap_zfs + wrapper
make -f Makefile.setcred_smap_zfs install # installs wrapper as /tmp/rsh
./exp_setcred_smap_zfs
/tmp/rsh -c id   # uid=0(root) gid=0(wheel) groups=0(wheel)

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_syscall or ZSTD_initCStream_advanced.
  • A local unprivileged session suddenly obtaining uid=0 without going through su, sudo, or a legitimate setuid path.

Remediation

ActionDetail
Primary fixApply/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 mitigationRestrict 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.