FatGid - FreeBSD 14.x kernel local privilege escalation
FatGid+4
A four-byte type, an eight-byte stride, one root shell.
FreeBSD 14.x kernel local privilege escalation via setcred(2).
CVE-2026-45250<br>FreeBSD-SA-26:18.setcred
Proof of concept (GitHub)<br>Read the technique
Summary
A kernel stack buffer overflow exists in the setcred(2)<br>system call introduced in FreeBSD 14.x. The overflow occurs before<br>any privilege check, allowing any unprivileged local user to trigger<br>arbitrary behaviour ranging from a kernel panic to full local<br>privilege escalation. Working LPE exploits against an amd64 GENERIC<br>kernel both without SMAP/SMEP and with SMAP/SMEP enabled have been<br>developed and are described below. The SMAP/SMEP-safe variant<br>requires only that zfs.ko be loaded -- the case on every<br>FreeBSD installation with a ZFS pool. The root cause is a single<br>sizeof type error in<br>kern_setcred_copyin_supp_groups()<br>(sys/kern/kern_prot.c).
The bug was silently fixed in the main branch on<br>2025-11-27 (commit<br>000d5b52c19ff3858a6f0cbb405d47713c4267a4) as a side<br>effect of a broader function refactoring. The fix has not<br>been backported to stable/14 or releng/14.4.<br>FreeBSD 14.4-RELEASE remains vulnerable.
FreeBSD 15.0 still carries the sizeof(*groups) typo and<br>is therefore vulnerable, but the surrounding code differs enough<br>from 14.4 that the chain primitives developed here do not lift the<br>overflow into a working LPE on that branch. On 15.0 the bug<br>remains a kernel panic triggered by any unprivileged user.
Impact
01 / LPE - SMAP & SMEP enabled
Modern-kernel root, no info-leak
A single setcred(2) syscall lifts an unprivileged<br>shell to uid=0 on a kernel with SMAP and SMEP enabled. No<br>kernel info-leak primitive is required. This is the headline<br>result.
02 / LPE - no mitigations
Legacy-kernel root
Same single syscall, on a kernel without SMAP/SMEP. Useful as<br>a stepping stone and as a reference for the<br>amd64_syscall+0x155 chain primitive that both<br>techniques share.
Full chain on FreeBSD 14.4-RELEASE-p3 amd64 (current<br>patchset). Unprivileged user, build, run, root shell.
Am I affected?
Vulnerable + exploitable<br>FreeBSD 14.4-RELEASE (confirmed)<br>FreeBSD stable/14
Vulnerable, panic only<br>FreeBSD 15.0 (same<br>source-level typo, but the surrounding code differs enough<br>from 14.4 that no chain primitive we know of lifts the<br>overflow into a working LPE)
Not affected<br>FreeBSD main (fixed in commit 000d5b5, 2025-11-27)
FreeBSD 13.x and earlier (setcred(2) not present)
Vulnerability details
File: sys/kern/kern_prot.c
Function: kern_setcred_copyin_supp_groups()
Lines: 528-533
The function signature uses a double pointer for the<br>groups argument:
static int<br>kern_setcred_copyin_supp_groups(struct setcred *const wcred,<br>const u_int flags, gid_t *const smallgroups, gid_t **const groups)
Because groups has type gid_t **, the<br>expression sizeof(*groups) evaluates to<br>sizeof(gid_t *) == 8 on LP64, rather than the intended<br>sizeof(gid_t) == 4. This sizeof expression is used in<br>two places:
/* line 528-530: allocation */<br>*groups = wcred->sc_supp_groups_nb sc_supp_groups_nb + 1) *<br>sizeof(*groups), M_TEMP, M_WAITOK); /* sizeof(*groups) == 8 */
/* line 532-533: copyin */<br>error = copyin(wcred->sc_supp_groups, *groups + 1,<br>wcred->sc_supp_groups_nb * sizeof(*groups)); /* sizeof(*groups) == 8 */
The allocation on the heap path is 2× oversized, which is<br>safe. However, for the stack path (when<br>sc_supp_groups_nb ),<br>*groups is set to smallgroups, a<br>gid_t[CRED_SMALLGROUPS_NB] array declared as a local<br>variable in the caller user_setcred():
gid_t smallgroups[CRED_SMALLGROUPS_NB]; /* 16 * 4 = 64 bytes */
The copyin destination is *groups + 1 == &smallgroups[1],<br>which leaves 15 * 4 == 60 bytes of usable space. The<br>copyin copies sc_supp_groups_nb * sizeof(*groups) ==<br>sc_supp_groups_nb * 8 bytes. With the maximum stack-path<br>value of sc_supp_groups_nb == 15:
Bytes written: 15 * 8 = 120<br>Buffer capacity: 15 * 4 = 60<br>Overflow: 60 bytes past the end of smallgroups[]
The overflow is written with fully attacker-controlled data from<br>user space (wcred->sc_supp_groups points to an<br>attacker-supplied buffer).
Trigger path and privilege-check ordering
The overflow happens in<br>kern_setcred_copyin_supp_groups(), which is called<br>from user_setcred() at line 604 -- before<br>the privilege check . The privilege check<br>(priv_check_cred(PRIV_CRED_SETCRED)) does not occur<br>until kern_setcred() is called at line 623, and within<br>that function at line 813. Any local user can trigger the<br>overflow by issuing:
setcred(SETCREDF_SUPP_GROUPS, &wcred, sizeof(wcred))
with wcred.sc_supp_groups_nb == 15 and<br>wcred.sc_supp_groups pointing to a<br>15 * 8 == 120-byte user-space buffer.
LPE technique (no SMAP, no SMEP)
The 60-byte overflow corrupts every callee-saved register slot in<br>user_setcred()'s prologue except saved RBP.<br>Compiler ordering on 14.4 GENERIC places the corruption window at<br>[rbp - 0x40 .. -0x05]:
buf[60..67] mac.m_buflen<br>buf[68..75] mac.m_string<br>buf[76..83] td pointer...