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

vnode_pager_generic_putpages() undirties pages on short writes β€” silent stale-data substitution (write-side DF-2663 family)

Field Value
ID DF-2843
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
CWE CWE-754 Improper Check for Exceptional Conditions
File sys/vm/vnode_pager.c
Lines 771-781
Area vm
Confidence likely
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket base:vm
Reported pending
Known CVE none
CVE match novel

Summary

When VOP_WRITE returns error==0 with auio.uio_resid != 0, the residual is rate-limited-logged but all ncount pages are still marked VM_PAGER_OK and vm_page_undirty()'d. The never-written bytes are silently dropped from the pageout pipeline: the page may later be reclaimed and re-fetched from disk, resurrecting stale on-disk content where the application wrote new data, and pageout never retries because rtvals report success. No filesystem on the audit guest exhibits error==0 with residual in normal operation β€” latent defect gated on FS behavior (no honest trigger on stock; honest untested).

Fold the residual into the undirty decision: if (error == 0 && auio.uio_resid == 0) for the OK/undirty path, else error = EIO β€” one-line diff in the pack.

Timeline

  • 2026-08-31 Discovered during pass-2 audit of vnode_pager.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2843 Β· 2 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
verdict.json β€” 1.5 KB view raw

DF-2843 β€” vnode_pager_generic_putpages() marks short-written pages clean

Bug: sys/vm/vnode_pager.c:767-781 Class: data-integrity (silent substitution family β€” write-side variant) Severity: Low Confidence: likely Phase V: not run (Low, non-corruption bucket; no FS on the audit guest exhibits error==0 with uio_resid>0 on VOP_WRITE, so there is no honest trigger available β€” see below).

The defect

767:    if (error) {
768:        krateprintf(&vbadrate,
769:                "vnode_pager_putpages: I/O error %d\n", error);
770:    }
771:    if (auio.uio_resid) {
772:        krateprintf(&vresrate,
773:                "vnode_pager_putpages: residual I/O %zd at %lu\n",
774:                auio.uio_resid, (u_long)m[0]->pindex);
775:    }
776:    if (error == 0) {
777:        for (i = 0; i < ncount; i++) {
778:            rtvals[i] = VM_PAGER_OK;
779:            vm_page_undirty(m[i]);
780:        }
781:    }

A VOP_WRITE that returns error==0 with uio_resid != 0 (short write) is detected β€” the residual is even rate-limited-logged β€” but the pages are still marked VM_PAGER_OK and undirtied. The never-written bytes are therefore silently dropped from the pageout pipeline:

  • the page can be reclaimed and re-read from disk, resurrecting the OLD on-disk content where the application wrote new data (silent stale-data substitution β€” the write-side twin of the DF-2663 short-read family);
  • rtvals[i]=VM_PAGER_OK tells vm_pageout_page() the write succeeded, so no retry ever occurs.

The rest of the function's EOF clipping (ncount/maxsize at :724-737) is correct, and the error!=0 path correctly leaves rtvals at VM_PAGER_AGAIN (pageout retry discipline).

Reachability

Requires a filesystem whose VOP_WRITE returns 0 with residual bytes. None of the filesystems on the audit guest (hammer2, ufs, tmpfs, devfs) do this in normal operation β€” their write loops run while (uio->uio_resid > 0) and propagate errors. The krateprintf exists because residual writes have been observed historically (interrupted large clustering paths). Hence: latent correctness bug, no trigger on the stock guest; PoC honestly classified untested.

-   if (error == 0) {
+   if (error == 0 && auio.uio_resid == 0) {
        for (i = 0; i < ncount; i++) {
            rtvals[i] = VM_PAGER_OK;
            vm_page_undirty(m[i]);
        }
+   } else if (error == 0) {
+       error = EIO;    /* short write: leave pages AGAIN/dirty */
    }

(i.e. treat a residual write as an I/O error for the clean/undirty decision while keeping the per-page rtvals discipline.)

Fix verification

not_testable
per-fix-DF-2843

Confirmed kernel references

Detail

Evidence (decisive lines)

['README.md: code excerpt with line cites and fix']

PoC changes

no PoC run; trigger requires a filesystem exhibiting short-but-clean VOP_WRITE

Verified recommended fix

only undirty/report OK when error==0 && auio.uio_resid==0; otherwise treat residual as EIO for the undirty decision

Verdict

vnode_pager_generic_putpages() (sys/vm/vnode_pager.c:776-781) clears the dirty state of all ncount pages and reports VM_PAGER_OK whenever VOP_WRITE returns error==0, even when auio.uio_resid != 0 (short write). The residual is logged (rate-limited) but the unwritten bytes are silently dropped from the pageout pipeline: the page can later be reclaimed and re-fetched from disk, resurrecting stale on-disk content in place of the application's writes, and pageout never retries because rtvals say OK. Not verified on the guest: no filesystem available there (hammer2/ufs/tmpfs/devfs) returns 0 with residual on VOP_WRITE under normal operation, so an honest trigger does not exist on the stock kernel; classified untested rather than fabricating an FS-specific trigger.