# DF-0165 — fix notes (authored post-verification)

## What the fix changes

Single file: `sys/kern/kern_caps.c`, inside `caps_priv_check()` (lines 333-340).

The bug: the function mutates its `cap` argument in the group-handling block
(line 335), reducing a specific capability (e.g. `SYSCAP_NONET_RAW = 0x61`)
to its group-master number (e.g. `SYSCAP_NONET = 6`). The mutated value is
then forwarded to `prison_priv_check()` at line 340, where the group-master
`case SYSCAP_NONET: return 0` / `case SYSCAP_NOMOUNT: return (0)`
unconditionally allow the action — bypassing the per-capability jail policy
switch arms (`case SYSCAP_NONET_RAW`, `case SYSCAP_NOMOUNT_*`) that actually
consult the `PRISON_CAP_*` flags.

## The fix

Do not reuse `cap` for the group-master bitmask test. Introduce a block-local
`gcap` for the `caps_check_cred()` group-master self-restriction test, and
leave `cap` holding the **original** specific capability for the
`prison_priv_check()` call:

```c
res = caps_check_cred(cred, cap);
if (cap & __SYSCAP_GROUP_MASK) {
    int gcap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT;

    res |= caps_check_cred(cred, gcap);
}
...
return (prison_priv_check(cred, cap));   /* now receives ORIGINAL specific cap */
```

## Why this is correct and minimal

- `caps_check_cred(cred, gcap)` computes the **same** value the old code did
  (the old line 335-336 produced exactly `(group-master)` then called
  `caps_check_cred` with it). The `__SYSCAP_SELF` self-restriction semantics
  are therefore unchanged.
- `prison_priv_check()` itself strips `__SYSCAP_XFLAGS` before switching
  (`switch (cap & ~__SYSCAP_XFLAGS)` at `sys/kern/kern_jail.c:854`), so
  forwarding the original cap (which may carry `__SYSCAP_NULLCRED` etc., as
  the `rip_attach` caller does) is the intended contract.
- Non-jailed callers are unaffected: `prison_priv_check()` returns 0
  immediately for non-jailed creds (`kern_jail.c:851-852`), so passing the
  specific cap vs. the group-master number is immaterial outside a jail.
- Group-master caps passed directly (e.g. `cap = SYSCAP_NONET = 6`, for which
  `cap & __SYSCAP_GROUP_MASK == 0` so the `if` body is skipped) keep exactly
  their old behavior — `prison_priv_check(cred, 6)` still hits
  `case SYSCAP_NONET: return 0`.

Only the specific caps whose per-capability jail policy is conditional/EPERM
while their group master returns 0 (`SYSCAP_NONET_RAW`, the
`SYSCAP_NOMOUNT_{NULLFS,DEVFS,TMPFS,PROCFS,FUSE}` set) change behavior — and
they change from "incorrectly allowed" to "correctly policy-gated", which is
exactly the bug being closed.

## Validation

- `git apply --check findings/poc/DF-0165/fix.diff` → clean.
- `patch --dry-run -p1 < findings/poc/DF-0165/fix.diff` → clean.
- Generated against the read-only `sys/` tree; `sys/` was not modified.

## Relation to the finding markdown proposal

Matches the finding markdown's `## Recommended fix` proposal. The only
deviation: this diff drops the comment-only edit to the `prison_priv_check`
return line (unnecessary churn) — the `cap` variable passed there is already
the original specific cap once the mutation is removed.
