# DF-2705 — vop_write quota accounting skips unlinked-open files → complete quota enforcement bypass (when vfs.quota_enabled=1)

File: sys/kern/vfs_vopops.c (pass 2, GLM 5.3)
Severity: Low (feature default-off, boot-tunable only) — Confidence: certain (logic), conditional on config

## What

The generic write-path quota glue in `vop_write()` (sys/kern/vfs_vopops.c:470-497):

```c
if (vfs_quota_enabled && (vp->v_type == VREG)) {
        ...
        size_before = va.va_size;
        /* this file may already have been removed */
        if (va.va_nlink > 0)
                do_accounting = 1;                      /* :477-478 */
        ...
        if (!vq_write_ok(mp, va.va_uid, va.va_gid, delta)) {   /* :489 check */
                error = EDQUOT;
                goto done;
        }
}
DO_OPS(ops, error, &ap, vop_write);
if ((error == 0) && do_accounting) {                    /* :495-497 */
        VFS_ACCOUNT(mp, va.va_uid, va.va_gid, size_after - size_before);
}
```

Charging (`VFS_ACCOUNT` → `vfs_stdaccount`, sys/kern/vfs_quota.c:150-171) is skipped
whenever `va_nlink == 0`, but the limit *check* (`vq_write_ok`) still runs against a
usage counter that will never be charged for this file's growth. Combined with
`vop_nremove()` (sys/kern/vfs_vopops.c:1665-1668), which subtracts the whole file
size from usage when the last link is removed, the enforcement is fully bypassable:

1. `f = open("/q/f", O_CREAT|O_WRONLY)`; write up to the uid limit (charged, checks enforce).
2. `unlink("/q/f")` while keeping `f` open — `vop_nremove` subtracts the entire size
   (`VFS_ACCOUNT(..., -va.va_size)`) and the inode's nlink drops to 0.
3. Keep writing through `f` forever: every write's `delta` is checked against a
   uid usage that stays ~0 (`0 + delta <= limit` passes for any single write that
   fits the limit once), and `do_accounting` stays 0, so nothing is ever charged.
   → unbounded growth on a quota-limited mount.

Secondary weaknesses in the same glue (same lines):

- **Concurrent-writer TOCTOU**: `size_before`/uid/gid are read pre-write; two
  concurrent writers on two files both pass `vq_write_ok` for deltas computed
  against the same stale uid usage, then both charge — usage can exceed the limit
  by up to (threads-1) × limit.
- uid/gid snapshot TOCTOU (chown between check and charge charges the wrong uid).

All of it is gated on `vfs_quota_enabled` (sys/kern/vfs_quota.c:111-113): default
**0**, `CTLFLAG_RD` sysctl — settable only via the `vfs.quota_enabled` boot tunable,
so the whole class is conditional on a non-default admin configuration. The feature
is a fairness/accounting mechanism (vquotactl limits), not a hard security
boundary; impact is disk-exhaustion across users on mounts where it was enabled.

## Recommended fix

Charge unlinked-open files too (they still consume blocks until the last close) —
i.e. drop the `va_nlink > 0` condition, or charge nlink==0 files against a separate
"unlinked usage" bucket; and re-read size/uid after the write for the delta
(or account inside the FS on block allocation, which is the accurate basis).

```diff
--- a/sys/kern/vfs_vopops.c
+++ b/sys/kern/vfs_vopops.c
@@ -474,9 +474,7 @@
 		size_before = va.va_size;
-		/* this file may already have been removed */
-		if (va.va_nlink > 0)
-			do_accounting = 1;
+		do_accounting = 1;	/* unlinked-open files still consume space */
```
(the symmetric subtraction in vop_nremove already handles last-link removal)

## Reproduction status

Not executed on the guest (Phase V skipped): demonstrating requires booting the
guest with the non-default `vfs.quota_enabled=1` boot tunable plus `vquota`/
`vquotactl` limit setup — a configuration change to the shared baseline image —
for a Low-severity accounting bypass. The logic chain above is complete and each
line was verified against source in this pass.
