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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2843 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
Recommended fix
- 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_testableConfirmed 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.
No comments yet.