sys_setgroups publishes the COW credential before mutating it: mid-copyin window exposes mixed old/new group sets to other threads, and a failing copyin leaves the credential partially mutated despite the error return
| Field | Value |
|---|---|
| ID | DF-2762 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N |
| CWE | CWE-367 / partial update on error |
| File | sys/kern/kern_prot.c |
| Lines | 682 (publish), 692-696 (copyin + count) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sys_setgroups calls cratom_proc(p) at :682, which installs the new credential as p_ucred BEFORE the group vector is filled in. The copyin at :692 can take a user fault and block; during that window any other thread entering the kernel caches the published-but-mid-mutation cred as td_ucred and executes group checks against cr_groups holding a mix of old and new gids while cr_ngroups still holds the old count. Additionally, when copyin faults partway, the syscall returns the error but the published credential keeps the partially overwritten cr_groups β POSIX expects no change on failure. All accesses stay in-bounds (not a memory-safety bug): impact is a transient self-race on group-membership checks and a state change despite an error return. Unprivileged callers cannot reach the code (SYSCAP gate).
Recommended fix
Copy the group vector into a stack temp before publishing, then atomically install it into the cred under p_token (full-atomicity alternative: build a complete new cred and publish with a single p_ucred store, as FreeBSD does).
Timeline
- 2026-08-30 Discovered during pass-2 audit of kern_prot.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2762 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | β | 2.1 KB | β raw | |
| README.md | β | 244 B | β raw | |
| build.sh | β | 102 B | view raw | |
| run.sh | β | 71 B | view raw | |
| env.txt | β | 314 B | view raw | |
| manifest.json | β | 671 B | view raw | |
| verdict.json | β | 2.6 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| setgroups_race.c | β | 3.5 KB | view raw | |
| run.log | β | 404 B | view raw |
DF-2762 evidence pack
- finding: sys/kern/kern_prot.c (682-696)
- verdict: reproduced (certain) - see VERDICT.md
- runtime PoC: setgroups_race.c
- reproduce: build.sh / run.sh ; expected output in run.log
- fix.diff: present, git-apply-able
DF-2762 - Verification Verdict
Status: reproduced Impact: none (hardening) Confidence: certain
Mechanism
sys_setgroups calls cratom_proc(p) FIRST - which publishes the fresh COW credential to the process - and only THEN copyins the new group list into that live credential. During the copyin window other threads observe a credential with the OLD count but progressively NEW group bytes; a faulting copyin returns an error while the published credential has already been (partially) mutated.
Verdict
REPRODUCED as root on the stock guest: a mutator thread alternating setgroups(setA)/setgroups(setB) plus an observer thread produced 115 observations of MIXED group sets out of 6.9M getgroups reads (samples: {1000,1001,1002,1003,5004,5005,5006,5007} - old prefix, new suffix), proving the credential is exposed mid-mutation exactly as claimed. The EFAULT variant (copyin straddling a guard page) returned EFAULT without an observable partial mutation in this run - DFly copyin for the 32-byte window completed or faulted before publication became visible to the same thread, so that sub-claim stays code-certain; the mixed-set window itself is runtime-proven.
Threat / reachability
Root-gated credential-consistency race: authorization checks (group_member) running concurrently with setgroups can evaluate against a mixed old/new group set.
Privilege escalation assessment
n/a (self-inflicted window; setgroups is root-gated)
Fix
copyin into a stack staging buffer BEFORE cratom, then memcpy+publish under the exclusive window.
Kernel references
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Note: an earlier draft of this fix.diff failed the first combined build (see _batch41/README.md); the shipped diff is the corrected, compile-clean version.
Fix verification
not_testablefix.diff compile-validated in the batch combined kernel build (all 37 diffs of this batch applied to in-guest /usr/src; make -j6 nativekernel KERNCONF=X86_64_GENERIC, see batch build log referenced in VERDICT.md); no behavior A/B performed for this Info-tier item; behavior A/B intentionally skipped for Info tier
batch combined kernel build log (see findings/poc/_batch41 note in VERDICT.md)
Confirmed kernel references
Detail
Exploit chain
no escalation path: setgroups is SYSCAP-gated; the observable mixed window is inside the calling process own credential
Evidence (decisive lines)
run.log: 115 of 6.9M getgroups reads returned MIXED old/new group sets (samples: {1000,1001,1002,1003,5004,5005,5006,5007})
PoC changes
Added a mutation pause around the EFAULT probe (the racing writer otherwise repairs the credential before the observer samples) and a guard-page-straddling copyin source to seek partial mutation; the mixed-set window reproduced strongly, the EFAULT partial-mutation variant did not manifest (copyin faults before bytes become visible to the same thread) and stays code-certain.
Verified recommended fix
copyin into a stack staging buffer BEFORE cratom, then memcpy+publish under the exclusive window.
Verdict
REPRODUCED as root on the stock guest: a mutator thread alternating setgroups(setA)/setgroups(setB) plus an observer thread produced 115 observations of MIXED group sets out of 6.9M getgroups reads (samples: {1000,1001,1002,1003,5004,5005,5006,5007} - old prefix, new suffix), proving the credential is exposed mid-mutation exactly as claimed. The EFAULT variant (copyin straddling a guard page) returned EFAULT without an observable partial mutation in this run - DFly copyin for the 32-byte window completed or faulted before publication became visible to the same thread, so that sub-claim stays code-certain; the mixed-set window itself is runtime-proven.
No comments yet.