# 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`:

```c
	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 only `cr_groups[0]`;
  `cr_svgid` is untouched.
* unprivileged `setgid(X)` (kern_prot.c:572-625): the `cr_svgid = gid` reset
  at :611 happens only when the caller is privileged or `gid == 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)

1. root: create mqueue `/svgidtest` mode 0660 (mq_euid=0, mq_egid=0=wheel).
2. root: install `gdrop` setgid-wheel (2755) and `gdrop_plain` (755).
3. 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 after `setegid` in ALL cases, and the
  setgid "keep" case still opens (egid=0 via groupmember) — see `fix.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.
