# DF-0846 — Verdict

**Verdict: REPRODUCED (on a `options QUOTA` kernel); UNREACHABLE on the default `X86_64_GENERIC` kernel.**

The credential + vnode-reference leak described by the finding is a **genuine
code defect** in `ufs_quotaon()`, but the vulnerable code path is **dead code
on the stock audit kernel**: the `Q_QUOTAON` dispatch in `ufs_quotactl()` is
`#else`-gated behind `options QUOTA`, which `X86_64_GENERIC` does not include.
On the default kernel `quotactl(Q_QUOTAON)` returns `EOPNOTSUPP` and
`ufs_quotaon()` is never reached. The bug was therefore demonstrated on a
purpose-built `X86_64_QUOTA` kernel (= GENERIC + `options QUOTA`), and the
authored `fix.diff` was validated before/after on that same kernel.

## Mechanism (trigger → primitive → effect)

File: `sys/vfs/ufs/ufs_quota.c`, `ufs_quotaon()`.

1. **Trigger setup** — `quotactl(ufs_mount, QCMD(Q_QUOTAON, USRQUOTA), 0, path)`
   reaches `ufs_quotaon()` only when the kernel was built with
   `options QUOTA` (dispatcher `ufs_vfsops.c:77`). The dispatcher also
   requires `caps_priv_check(cred, SYSCAP_NOQUOTA_WR)` (`ufs_vfsops.c:104`),
   i.e. **root**. So even on a QUOTA kernel this is a root-only path — not a
   privilege-boundary crossing.

2. **The leak** — `ufs_quotaon()`:
   - `ufs_quota.c:425` — `vn_open(&nd, NULL, FREAD|FWRITE, 0)` takes a fresh
     vnode reference on the quota file.
   - `ufs_quota.c:437` — `if (*vpp != vp) ufs_quotaoff(mp, type);`.
     `ufs_quotaoff()` is the *only* place that calls `vn_close()` on the
     stored quota vnode (`ufs_quota.c:519`) and `crfree()` on
     `ump->um_cred[type]` (`ufs_quota.c:521`). When the **same** quota file
     is re-supplied (`*vpp == vp`) — e.g. `Q_QUOTAON` issued twice for the
     same path — `quotaoff` is **skipped**, so neither the duplicate vnode
     reference nor the old credential is released.
   - `ufs_quota.c:448` — `ump->um_cred[type] = crhold(cred);` overwrites the
     credential pointer unconditionally; the previous `um_cred[type]` is
     orphaned (its `cr_ref` is never decremented → `crfree`/`kfree` never
     fires). The author flagged this with an `/* XXX release duplicate vp if
     *vpp == vp? */` note at `ufs_quota.c:443`.

   Each such call leaks **1 × credential reference** + **1 × vnode
   reference**. After ~2³¹ calls the signed `v_refcnt` could wrap negative
   (theoretical UAF ceiling); in practice this is a slow, unbounded memory /
   refcount leak that can be driven by an unprivileged-triggerable... no —
   only by root.

3. **Effect** — slow kernel memory leak (`M_CRED` zone growth + ever-growing
   vnode refcount) when root repeatedly re-enables quotas on an
   already-active quota file. DoS-via-exhaustion ceiling, not RCE/privesc.

## Why it is unreachable on the default kernel (the key fact)

`sys/vfs/ufs/ufs_vfsops.c`:
```c
int ufs_quotactl(struct mount *mp, int cmds, uid_t uid, caddr_t arg, struct ucred *cred)
{
#ifndef QUOTA
    return (EOPNOTSUPP);          /* <-- the entire dispatch is compiled out */
#else
    ...
    case Q_QUOTAON: error = ufs_quotaon(cred, mp, type, arg);
    ...
#endif
}
```

`sys/config/X86_64_GENERIC` does **not** contain `options QUOTA` (only
`sys/config/LINT64:510` does). The compiled `ufs_quotactl` in the running
`6.5-DEVELOPMENT #0` GENERIC kernel is literally the two-instruction stub:

```
ffffffff80917820 <ufs_quotactl>:
ffffffff80917820:   b8 2d 00 00 00    mov    $0x2d,%eax     ; EOPNOTSUPP = 45 = 0x2d
ffffffff80917825:   c3                retq
```

Behaviourally confirmed: `quotactl(Q_QUOTAON)` on the stock kernel returns
`errno=45 (Operation not supported)` immediately. The file
`ufs_quota.c` *is* linked (`optional ffs`) so the `ufs_quotaon` symbol is
present, but its sole caller is inside the excluded `#else`, so it is dead
code on GENERIC.

## Reproduction (three kernels compared)

The PoC forks N children that each obtain a **distinct** cred (via
`setresgid` → `crcopy`), re-issue `Q_QUOTAON` on the same already-active
quota file M times, then exit. Each child's cred gets its `cr_ref` pinned by
M leaked references, so it can never be freed → the cred **struct** leaks
permanently and the `vmstat -m` "cred" zone Count grows by ~N.

| Kernel | build | sha256 (kernel) | PoC result (60 children × 30 iters) |
|---|---|---|---|
| default `X86_64_GENERIC` | stock `#0` | — | `quotactl` → `EOPNOTSUPP`; `ufs_quotaon` never reached |
| `X86_64_QUOTA` **unpatched** | `#0` | `69940976…52e491` | cred zone **9 → 69, delta +60** = LEAK CONFIRMED |
| `X86_64_QUOTA` **patched (fix.diff)** | `#1` | `fc362811…3b2982` | cred zone **8 → 8, delta 0** = no leak (×2 runs) |

Decisive before/after:
- baseline (unpatched QUOTA): `cred delta : 60  VERDICT: LEAK CONFIRMED`
- patched QUOTA:              `cred delta : 0   VERDICT: no leak (fix in effect)`

The `/` root filesystem is **hammer2** on this guest, so the PoC targets the
**ufs** mount `/boot` (`/dev/vbd0s1a on /boot (ufs, local)`) and creates the
quota file at `/boot/quota.user`.

## Escalation

None applicable. This is a **refcount / resource leak**, not a memory-
corruption primitive (no attacker-shaped write, no UAF at trigger time — the
reference count only *grows*, it does not wrap on a realistic call budget).
There is no primitive to convert to control-flow hijack. The realistic impact
ceiling is a slow kernel-memory exhaustion DoS, and only when root repeatedly
re-enables quotas on the same file on a `options QUOTA` kernel.

## The fix (`fix.diff`)

Minimal, targeted change to `ufs_quotaon()` at `ufs_quota.c:437`. When
`*vpp == vp` (same vnode re-supplied), release the duplicate vnode reference
taken by `vn_open()` and drop the previously-saved credential before
overwriting it — mirroring exactly what `ufs_quotaoff()` does for the
`*vpp != vp` branch:

```c
if (*vpp != vp) {
    ufs_quotaoff(mp, type);
} else {
    vn_close(vp, FREAD|FWRITE, NULL);          /* release duplicate vn_open ref */
    if (ump->um_cred[type] != NOCRED)
        crfree(ump->um_cred[type]);            /* release stale stored cred */
}
```

The reference retained by `ump->um_quotas[type]` keeps `vp` alive across the
`vn_close()`, so the subsequent `vsetflags(vp, VSYSTEM)` / `*vpp = vp` remain
safe. Validated before/after on the `X86_64_QUOTA` kernel (table above): the
leak (+60) is completely gone (delta 0).

## PoC changes

The supplied PoC scaffolding for DF-0846 did not exist in the repo (the
finding markdown/poc dir were missing); this run authored the full evidence
pack from scratch: `quotaon_leak.c` (the demonstrator), `build.sh`, `run.sh`,
`README.md`, this `VERDICT.md`, `fix.diff`, the three kernel logs, and
`manifest.json`. The PoC targets the ufs mount `/boot` (the guest's `/` is
hammer2) and uses distinct per-child creds to make the refcount leak visible
as `vmstat -m` "cred" zone Count growth.

## Notes for maintainers

- The leak is real but **low impact**: root-only, and only on kernels built
  with `options QUOTA` (not the default). Worth fixing for correctness /
  hygiene (and to retire the author's own `XXX` note); not a security-critical
  issue.
- The `fix.diff` is the authoritative verified fix. It applies cleanly with
  `git apply -p1` and `patch -p1`, compiles into a `QUOTA` kernel, and
  eliminates the demonstrated leak.
