β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2669

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2669 Β· 3 files
FileTypeDescriptionSize
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.

--- 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_testable
per-fix-DF-2669

Confirmed 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).