vaccess() grants group file-access permissions from the SAVED gid (cr_svgid): kernel-enforced group privileges survive setegid()/setgid() drops (POSIX mqueue access control fail-open)
| Field | Value |
|---|---|
| ID | DF-2697 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N |
| CWE | CWE-284 Improper Access Control |
| File | sys/kern/vfs_subr.c |
| Lines | 1752-1753 (call site sys_mqueue.c:513) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | privesc |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The group branch of the kernel's generic permission check accepts the credential's SAVED gid in addition to the effective gid and supplementary groups. Because setegid() and unprivileged setgid() drops never clear cr_svgid (the reset runs only for privileged callers or gid==egid), any process that ever held egid=G (setgid binary exec seeds svgid) silently keeps group-G read/write access after dropping the effective gid. Only in-kernel caller: POSIX mqueue open (sys_mqueue.c:558); the filesystem path (vop_helper_access) is unaffected. Same confusion at the call site: sys_mqueue.c:513 stores the queue gid from the creator's cr_svgid.
Threat model & preconditions
Local unprivileged user in a setgid context (or any privilege-separated daemon that drops a group) retains read/write access to group-protected POSIX message queues the process believed it had relinquished. Cannot mint a group the process never held, so Medium rather than High.
Proof of concept
VERIFIED on the guest (findings/poc/DF-2697/): root creates mqueue
/svgidtest mode 0660 root:wheel and installs a setgid-wheel binary;
unpriv user execs it (egid=svgid=0), setegid(getgid()) drops egid to
1001, then mq_open(O_WRONLY) SUCCEEDS β vaccess matched cr_svgid==0.
Control without the setgid bit: EACCES. Fix kernel: dropped case denies
in both copies, egid=0 case still opens via groupmember.
Recommended fix
--- a/sys/kern/vfs_subr.c
+++ b/sys/kern/vfs_subr.c
@@ -1749,8 +1749,7 @@ vaccess(enum vtype type, mode_t file_mode, uid_t uid, gid_t gid,
}
/* Otherwise, check the groups. */
- ismember = groupmember(gid, cred);
- if (cred->cr_svgid == gid || ismember) {
+ if (groupmember(gid, cred)) {
if (acc_mode & VEXEC)
(and remove the now-unused int ismember;). Validated in-guest.
Timeline
- 2026-08-30 Discovered during pass-2 audit of vfs_subr.c (GLM 5.3); reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2697 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gdrop.c | β | 1.9 KB | view raw | |
| mqcreate.c | β | 767 B | view raw | |
| setup.sh | β | 374 B | view raw | |
| run.sh | β | 625 B | view raw | |
| build.sh | β | 172 B | view raw | |
| run.log | β | 1.0 KB | view raw | |
| fix.log | β | 499 B | view raw | |
| env.txt | β | 750 B | view raw | |
| fix.diff | β | 691 B | view raw | |
| fix.both.diff | β | 1.0 KB | view raw | |
| VERDICT.md | β | 3.5 KB | β raw |
DF-2697 β vaccess() honors the saved gid (cr_svgid) in group permission checks
Reproduced? YES (reproduced, impact = access-control bypass, not memory corruption)
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), uid 1001 (maxx).
Root cause
sys/kern/vfs_subr.c:1752-1753:
ismember = groupmember(gid, cred);
if (cred->cr_svgid == gid || ismember) {
groupmember() (sys/kern/kern_prot.c:949-960) already scans
cr_groups[0 .. cr_ngroups), i.e. it covers the effective gid
(#define cr_gid cr_groups[0], sys/sys/ucred.h:81) and the
supplementary groups β exactly the POSIX group access set. The additional
cred->cr_svgid == gid clause extends the group branch to the saved
gid, which is not part of any access decision set (POSIX 1003.1; FreeBSD's
vaccess checks only cr_groups[0] == gid || groupmember(...)).
cr_svgid survives every legitimate privilege drop:
setegid()(sys/kern/kern_prot.c:649-653) sets onlycr_groups[0];cr_svgidis untouched.- unprivileged
setgid(X)(kern_prot.c:572-625): thecr_svgid = gidreset at :611 happens only when the caller is privileged orgid == egid, so an unprivileged drop leaves svgid at the old egid. - svgid is seeded from a setgid binary at exec (kern/kern_exec.c:532-535).
So: exec setgid-G binary β egid=G, svgid=G; setegid(rgid) (allowed, rgid) β
egid=rgid but svgid stays G β vaccess(..., gid=G, ...) still takes the
group branch and grants group-mode read/write.
Note the same confusion appears at the call site: POSIX mqueue creation
stores the queue's gid from the creator's svgid
(sys/kern/sys_mqueue.c:513 mq_new->mq_egid = td->td_ucred->cr_svgid;)
instead of cr_gid. The vaccess() clause is what turns the odd stored
value into a grantable credential.
Exploit chain (demonstrated)
- root: create mqueue
/svgidtestmode 0660 (mq_euid=0, mq_egid=0=wheel). - root: install
gdropsetgid-wheel (2755) andgdrop_plain(755). - unpriv user (uid/gid 1001, groups={1001}) runs
/gdrop drop /svgidtest: * exec sets egid=svgid=0 (wheel); *setegid(getgid())β egid=1001, svgid remains 0; *mq_open(O_WRONLY)β vaccess: owner? uid 1001 != 0. Group? groupmember(0, cred)=false (groups={1001}) but cr_svgid==0==gid β group branch β S_IWGRP set in 0660 β granted. * Control run (/gdrop_plain, svgid=1001) β EACCES (correct).
Result: kernel-enforced write access to a wheel-group object after the
process dropped its effective wheel gid. Impact ceiling: silently defeats
group-privilege drops for POSIX mqueue access (e.g. sandboxed/privilege-
separated processes keep mqueue read/write they already dropped). It does
not grant a group the process never held, and it does not touch the main
filesystem path (vop_helper_access, sys/kern/vfs_helper.c:141-159, uses
cr_gid + supplementary only β no svgid).
Fix validation
Combined one-line fix kernel (fix.both.diff: vaccess drops the svgid
clause; hammer result fix), built with make nativekernel KERNCONF=X86_64_GENERIC
in the guest:
- baseline (stock kernel): setgid copy opens the queue after the drop β see
run.log - patched kernel:
mq_openβ EACCES aftersetegidin ALL cases, and the setgid "keep" case still opens (egid=0 via groupmember) β seefix.log
Verdict
Real, reproducible kernel access-control fail-open confined to vaccess()
(only caller: POSIX mqueue open). Severity Medium: defeats privilege drops
but cannot mint a group the process never had.
Fix verification
fixedSingle-fix kernel (fix.both.diff carried this one-line change + the DF-2698 one-liner) built with make nativekernel KERNCONF=X86_64_GENERIC and booted. Baseline stock kernel: setgid-wheel gdrop opens the 0660 queue after setegid drop (SUCCESS). Patched kernel: EACCES in all dropped cases; egid=0 case still succeeds via groupmember. Bug gone, no regression.
fix.log / fix_run.log (before/after in same pack); fix_build.log tail of the nativekernel build; uname -v #2 boot identity.
Confirmed kernel references
Detail
Exploit chain
root setup: mqueue /svgidtest created 0660 root:wheel (umask 0) + gdrop installed 2755 root:wheel; unpriv user (uid/gid 1001, groups={1001}) execs gdrop: egid=svgid=0; gdrop calls setegid(1001) (allowed, rgid) -> egid=1001, svgid stays 0; mq_open(O_WRONLY) -> vaccess group branch matches cr_svgid==0==queue gid -> write access to the wheel-group queue after the drop was 'completed'.
Evidence (decisive lines)
['run.log: ==SETGID-wheel-copy== mq_open(/svgidtest, O_WRONLY) = 3 -- SUCCESS after setegid drop, while ==CONTROL-no-setgid== gets EACCES', 'fix.log (patched kernel): identical run -> Permission denied in both cases; keep-egid case still opens (groupmember semantics preserved)', 'VERDICT.md: full path:line trace']
PoC changes
mqcreate.c initially did not reset an existing queue and inherited root's umask 022, making the queue 0640 (no group write) so every run was denied; recreated with umask 000. gdrop.c unchanged from the sketch.
Verified recommended fix
Drop the cr_svgid clause in vaccess(): replace 'ismember = groupmember(gid, cred); if (cred->cr_svgid == gid || ismember)' with 'if (groupmember(gid, cred))'.
Verdict
vaccess() at sys/kern/vfs_subr.c:1752-1753 grants the group branch of the POSIX permission check when cred->cr_svgid == gid. groupmember() already covers the effective gid (cr_groups[0]) and supplementary groups, so the svgid clause only ever ADDS the saved gid to the access set. setegid() (kern_prot.c:649-653) and unprivileged setgid() drops never clear cr_svgid, so after exec of a setgid-G binary and an egid drop the kernel keeps granting group-G read/write. Only in-kernel caller is POSIX mqueue open (sys_mqueue.c:558). Reproduced on the stock guest: a setgid-wheel binary that calls setegid(getgid()) then mq_open(O_WRONLY) on a 0660 root:wheel queue SUCCEEDS (control binary without the setgid bit: EACCES). One-line fix (drop the svgid clause) validated with an in-guest nativekernel build: after the drop mq_open fails with EACCES while the legitimate egid=0 case still succeeds.
No comments yet.