kern_truncate()/kern_ftruncate() execute VFS_ACCOUNT() even when VOP_SETATTR failed β quota accounting corruption on failed truncates
| Field | Value |
|---|---|
| ID | DF-2669 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N |
| CWE | CWE-754 Improper Check for Exceptional Conditions |
| File | sys/kern/vfs_syscalls.c |
| Lines | 4044-4049, 4119-4125 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
With VFS quota accounting wired (vfs.quota_enabled + quota-enabled
mount), a truncate that FAILS (EFBIG/EPERM/EDQUOT/ENOSPC) still adjusts
per-uid/gid accounting by (requested_length β old_size) although the
file size never changed; repeated failed truncates drive accounting to
arbitrary values β quota bypass or spurious EDQUOT for other users.
kern_truncate() (:4044-4049) and kern_ftruncate() (:4119-4125) call
VFS_ACCOUNT() unconditionally after error = VOP_SETATTR(...); the
sibling setfown() correctly guards with if (error == 0) at :3555-3562.
Threat model & preconditions
Unprivileged once quotas are enabled (non-default config). Stock guest mounts wire no vfs_account β inspection-certain, untested live.
Recommended fix
Guard both calls: if (error == 0) VFS_ACCOUNT(mp, uid, gid, length -
old_size); matching setfown(). (Diff in findings/poc/DF-2669/README.md.)
Timeline
- 2026-08-29 Discovered during pass-2 audit of vfs_syscalls.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2669 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| verdict.json | β | 1.4 KB | view raw | |
| manifest.json | β | 518 B | view raw |
DF-2669 β kern_truncate()/kern_ftruncate() call VFS_ACCOUNT() even when VOP_SETATTR failed
Impact (quota accounting corruption; non-default config)
When VFS quota accounting is active (vfs.quota_enabled=1 tunable plus a
mount whose vfs_acinit wired vfs_account, e.g. quota-enabled UFS or
HAMMER2), a truncate request that fails (EFBIG, EPERM, EDQUOT,
ENOSPC β¦) still moves the per-uid/gg byte accounting by
length - old_size even though the file size did not change. Repeated
failed truncates let a user drive its own (or another uid's) accounting
arbitrarily negative or positive β quota bypass (unlimited usage) or
spurious EDQUOT denial for other users. Requires quota support enabled
(non-default), unprivileged once enabled.
Root cause (line-accurate)
sys/kern/vfs_syscalls.c:4044-4049 (kern_truncate):
if ((error = vn_writechk(vp)) == 0) {
VATTR_NULL(&vattr);
vattr.va_size = length;
error = VOP_SETATTR(vp, &vattr, nd->nl_cred);
VFS_ACCOUNT(nd->nl_nch.mount, uid, gid, length - old_size); /* :4048 β runs even if error != 0 */
}
sys/kern/vfs_syscalls.c:4119-4125 (kern_ftruncate):
if ((error = vn_writechk(vp)) == 0) {
VATTR_NULL(&vattr);
vattr.va_size = length;
error = VOP_SETATTR_FP(vp, &vattr, fp->f_cred, fp);
mp = vq_vptomp(vp);
VFS_ACCOUNT(mp, uid, gid, length - old_size); /* :4124 β same */
}
The sibling code path setfown() guards the accounting correctly:
if (error == 0) { ... VFS_ACCOUNT ... } (:3555-3562). The two truncate
paths are missing the same guard.
Not verified on the guest
Impact requires VFS quota accounting to be enabled (vfs_quota_enabled
tunable at boot + per-mount vfs_acinit); the stock guest kernel/mounts do
not wire vfs_account (VFS_ACCOUNT is a no-op there), and setting up a
quota-enabled mount would not change the code-level conclusion. Filed as
certain-by-inspection logic bug; PoC left untested. A one-line guard makes
the behavior correct either way.
Recommended fix
--- a/sys/kern/vfs_syscalls.c
+++ b/sys/kern/vfs_syscalls.c
@@ kern_truncate()
if ((error = vn_writechk(vp)) == 0) {
VATTR_NULL(&vattr);
vattr.va_size = length;
error = VOP_SETATTR(vp, &vattr, nd->nl_cred);
- VFS_ACCOUNT(nd->nl_nch.mount, uid, gid, length - old_size);
+ if (error == 0)
+ VFS_ACCOUNT(nd->nl_nch.mount, uid, gid,
+ length - old_size);
}
@@ kern_ftruncate()
if ((error = vn_writechk(vp)) == 0) {
VATTR_NULL(&vattr);
vattr.va_size = length;
error = VOP_SETATTR_FP(vp, &vattr, fp->f_cred, fp);
mp = vq_vptomp(vp);
- VFS_ACCOUNT(mp, uid, gid, length - old_size);
+ if (error == 0)
+ VFS_ACCOUNT(mp, uid, gid, length - old_size);
}
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-2669/README.md (code excerpts + line refs + suggested fix)']
PoC changes
n/a β inspection-only finding
Verified recommended fix
Guard both VFS_ACCOUNT calls in kern_truncate()/kern_ftruncate() with 'if (error == 0)', matching setfown().
Verdict
Certain-by-inspection quota-accounting corruption: kern_truncate() (sys/kern/vfs_syscalls.c:4048) and kern_ftruncate() (:4124) execute VFS_ACCOUNT(mp, uid, gid, length - old_size) after VOP_SETATTR even when VOP_SETATTR failed, moving per-uid accounting by the requested delta although the file size never changed; repeated failed truncates (e.g. EFBIG) drive accounting to arbitrary values, enabling quota bypass or spurious EDQUOT. Not exercised on the guest because the stock kernel/mounts do not wire vfs_account (VFS_ACCOUNT is a no-op without vfs.quota_enabled); code-level defect unambiguous (sibling setfown() guards the identical call at :3555).
No comments yet.