# DF-5829 — VERDICT

**Status: reproduced (fix validated)** — impact `dos`-class data-integrity
violation (silent loss of `msync(MS_SYNC)` durability), not memory
corruption. No escalation chain applies to this class; the deliverable is
the demonstrated semantic violation on the stock kernel and its
disappearance under the one-line fix.

## Root cause (all citations into the audited tree)

1. `sys/vm/vm_object.c:981` — `wholescan = (start == 0 && info.end_pindex
   == object->size - 1);` — pass1 is gated on the range covering the
   *entire object from page 0*.
2. `sys/vm/vm_object.c:991` — `if (wholescan) { … pass1 … }` — pass1 is the
   only code that ever sets `PG_CLEANCHK` (`:1039`).
3. `sys/vm/vm_object.c:1074-1075` — pass2 skips every page without
   `PG_CLEANCHK`: `if ((p->flags & PG_CLEANCHK) == 0) goto done;`
4. `sys/vm/vm_object.c:1175-1179` — `vm_object_page_collect_flush()` also
   stops at the first page without `PG_CLEANCHK`; no in-tree caller passes
   `OBJPC_IGNORE_CLEANCHK` (verified by grep over `sys/`).

   ⇒ any partial-range `vm_object_page_clean()` flushes nothing.

5. `sys/vm/vm_map.c:3148-3152` — `msync()` reaches the VM only through
   `vm_object_page_clean(object, OFF_TO_IDX(offset),
   OFF_TO_IDX(offset + size + PAGE_MASK), flags)`; there is no VOP_FSYNC
   fallback in `vm_map_clean()`.
6. `sys/kern/vfs_vm.c:465-467` (`nvnode_pager_setsize`) — filesystems that
   use `nvextendbuf`/`nvtruncbuf` (hammer2; buffer size 64 KiB) size
   `object->size` *beyond EOF* to cover the straddling buffer. For a
   4096-byte hammer2 file, `object->size == 16` pages — so **no msync range
   expressible on the file can ever satisfy the wholescan condition**.

## How it was verified (guest `DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2
06:02:54 UTC 2026`, stock kernel; root fs hammer2, `/boot` UFS)

Observable: `vm.stats.vm.v_vnodeout`/`v_vnodepgsout` increment only in
`vnode_pager_putpages()` (`sys/vm/vnode_pager.c:764`), reachable only from
the pass2 flush. Rig: `msync_poc.c` (unprivileged, uid 1001): dirty exactly
one page through `MAP_SHARED`, msync the given window, sample the counters
around the syscall, SIGKILL the child before teardown.

### Baseline (stock kernel)

hammer2 (`run.h2.log`, 3 reps, perfectly deterministic):

```
A partial-range  msync[5p,7p)   vnodeout delta = 0   vnodepgsout delta = 0
B whole-file ctrl msync[0p,8p)  vnodeout delta = 0   vnodepgsout delta = 0
C whole-mapping  msync[0p,4p)   vnodeout delta = 0   vnodepgsout delta = 0
D whole 1p file  msync[0p,1p)   vnodeout delta = 0   vnodepgsout delta = 0
E 1p file        msync[0p,4p)   vnodeout delta = 0   vnodepgsout delta = 0
F 1p file        msync[0p,16p)  vnodeout delta = 1   vnodepgsout delta = 1  <- flush!
```

* Arms A–D: **every in-file msync(MS_SYNC) on the default filesystem
  (hammer2) performs zero writeback and returns success.**
* Arm F flushes only because a 16-page range happens to equal the
  block-rounded `object->size` of the 1-page file — mechanistic proof that
  the gate is exactly `end_pindex == object->size - 1` with the object
  sized beyond EOF.

UFS (`run.3-ufs.log`, 2 reps): `A=0/0, B=1/4..6, C=0/0, D=1/1` — on UFS
(objects sized exactly to the file) the whole-file msync works, while the
partial range (A) and the whole-mapping-of-larger-file (C) cases are silent
no-ops.

Negative control (`run.log`): on tmpfs (objects are swap-backed)
`vm_object_page_clean()` early-returns at the `type != OBJT_VNODE` gate —
all arms flat; documents that the tmpfs result is the early-out, not the
wholescan bug.

### Fix validation (patched kernel, same guest)

`fix.diff` applied to the guest's `/usr/src` (`make nativekernel`, log in
`fix_build.log`), rebooted into `#1 Tue Oct 13 09:06:21 UTC 2026`,
identical rig (`run.patched-h2.log`, 3 reps, deterministic):

```
A=1/2 B=1/6 C=1/4 D=1/1 E=1/1 F=1/1
```

The partial-range msync (arm A) now flushes; the rig's own verdict line
flips to "partial range unexpectedly flushed", i.e. the vulnerable behavior
is **gone**. Guest reset to the clean-source snapshot afterwards.

## Impact

* Any unprivileged process that mmap()s a file PROT_WRITE and relies on
  `msync(MS_SYNC)` for durability gets a silent no-op: on hammer2 for every
  range; on UFS for every range that does not start at file page 0 and end
  exactly at EOF. Data the app believes durable lives only in the page
  cache until the ~30 s syncer (or munmap/last-close paths) happens to
  flush; a crash/panic in that window loses it.
* POSIX `msync()` contract broken; `MS_SYNC|MS_INVALIDATE` semantics broken
  (dirty pages are not dropped); the NFS server's partial-range clean
  (`sys/vfs/nfs/nfs_serv.c:3663`) is also a no-op.
* Not memory corruption; no privesc; no kernel crash.

## Fix

`fix.diff` — run the `PG_CLEANCHK` marking pass over the requested range
unconditionally; keep the `OBJ_WRITEABLE|OBJ_MIGHTBEDIRTY` /
`vclrobjdirty()` clearing gated on `wholescan && !error` (those flags
describe the whole object, not the cleaned range).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
