- What: FreeBSD 14.x kernel has a local privilege escalation vulnerability
- Impact: Local users can gain full system control
A kernel stack buffer overflow exists in thesetcred(2)system call introduced in FreeBSD 14.x. The overflow occurs before any privilege check, allowing any unprivileged local user to trigger arbitrary behaviour ranging from a kernel panic to full local privilege escalation. Working LPE exploits against an amd64 GENERIC kernel both without SMAP/SMEP and with SMAP/SMEP enabled have been developed and are described below. The SMAP/SMEP-safe variant requires only thatzfs.kobe loaded -- the case on every FreeBSD installation with a ZFS pool. The root cause is a singlesizeoftype error inkern_setcred_copyin_supp_groups()(sys/kern/kern_prot.c). Thesetcred(2)system call (and the typo along with it) was introduced intomainon 2024-12-16 by commitddb3eb4efe55("New setcred() system call and associated MAC hooks"), first shipping to users with FreeBSD 14.3-RELEASE. The bug was silently removed frommainon 2025-11-24 by commit4cd93df95e69("setcred(): Remove an optimization for when cr_groups[0] was the egid") -- a side effect of a refactoring whose commit message does not mention the stack overflow. The FreeBSD Security Team publishedFreeBSD-SA-26:18.setcredon 2026-05-21, and patches have been issued for all currently supported branches. Users of 14.3, 14.4 and 15.0 should update to14.3-RELEASE-p14,14.4-RELEASE-p5or15.0-RELEASE-p9respectively. On FreeBSD 15.0 the surrounding code differed enough from 14.4 that the chain primitives developed here did not lift the overflow into a working LPE; on that branch the bug remained a kernel panic triggered by any unprivileged user. 15.0 is now patched as well. A singlesetcred(2)syscall lifts an unprivileged shell to uid=0 on a kernel with SMAP and SMEP enabled. No kernel info-leak primitive is required. This is the headline result. Same single syscall, on a kernel without SMAP/SMEP. Useful as a stepping stone and as a reference for theamd64_syscall+0x155chain primitive that both techniques share. FreeBSD-SA-26:18.setcred was published on 2026-05-21 and patches have been issued for every currently supported branch. If your system is at or above the patchlevel listed below, you are not affected. File:sys/kern/kern_prot.cFunction:kern_setcred_copyin_supp_groups()Lines: 528-533 The function signature uses a double pointer for thegroupsargument: Becausegroupshas typegid_t **, the expressionsizeof(*groups)evaluates tosizeof(gid_t *) == 8on LP64, rather than the intendedsizeof(gid_t) == 4. This sizeof expression is used in two places: The allocation on the heap path is 2× oversized, which is safe. However, for the stack path (whensc_supp_groups_nb < CRED_SMALLGROUPS_NB == 16),*groupsis set tosmallgroups, agid_t[CRED_SMALLGROUPS_NB]array declared as a local variable in the calleruser_setcred(): The copyin destination is*groups + 1 == &smallgroups[1], which leaves15 * 4 == 60bytes of usable space. The copyin copiessc_supp_groups_nb * sizeof(*groups) == sc_supp_groups_nb * 8bytes. With the maximum stack-path value ofsc_supp_groups_nb == 15: The overflow is written with fully attacker-controlled data from user space (wcred->sc_supp_groupspoints to an attacker-supplied buffer). The overflow happens inkern_setcred_copyin_supp_groups(), which is called fromuser_setcred()at line 604 --before the privilege check. The privilege check (priv_check_cred(PRIV_CRED_SETCRED)) does not occur untilkern_setcred()is called at line 623, and within that function at line 813. Any local user can trigger the overflow by issuing: withwcred.sc_supp_groups_nb == 15andwcred.sc_supp_groupspointing to a15 * 8 == 120-byte user-space buffer. The 60-byte overflow corrupts every callee-saved register slot inuser_setcred()'s prologue except saved RBP. Compiler ordering on 14.4 GENERIC places the corruption window at[rbp - 0x40 .. -0x05]: The crucial observation is thatsys_setcred()'s prologue saves onlyrbp/r14/rbx-- it doesnotsaver12. The corruptedr12popped byuser_setcred()'s epilogue therefore propagates unchanged throughsys_setcred()up toamd64_syscall(), which at+0x155uses it as if it were the livetd_procpointer: This is a two-level indirect call entirely controlled by the attacker:*(r12+0x3f8)suppliesrcx, and*(rcx+0xc8)is the call target. Without SMAP, the kernel happily dereferences user-mode pointers, so both indirections can be satisfied by fake structures placed in user memory. Without SMEP, the indirect call may target user-space code. The published no-SMAP exploit constructs a fakestruct sysentvecwhosesv_set_syscall_retvalslot (offset0xc8) points to user-space shellcode. The shellcode readsgs:[0]for the real curthread, restoresr12, then zeroescr_uid/cr_ruid/cr_svuid/cr_rgid/cr_svgidon the realtd_ucredand returns. The chain primitive atamd64_syscall+0x155reaches its target withrcx = K1(an attacker-chosen 8-byte value). If the target gadget writesrcx + 1totd->td_ucred, the current thread's credential pointer is now set to any address we choose -- and if that address happens to lie inside a kernel buffer we control (a h...