ext2_readdir never corrects the caller's cookie count β missing '*' deref leaves stale uninitialized kernel heap in the over-reported tail (VOP contract violation, latent NFS-facing heap disclosure)
| Field | Value |
|---|---|
| ID | DF-3031 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-628 (latent CWE-908) |
| File | sys/vfs/ext2fs/ext2_vnops.c |
| Lines | sink ext2_lookup.c:262 (dispatch :2142) |
| Area | vfs/ext2fs |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
ext2_readdir allocates a worst-case cookie array with malloc (no M_ZERO), reports the ALLOCATED count to the caller via ap->a_ncookies = ncookies, fills only allocated-minus-remaining slots, and then subtracts the remainder from the POINTER instead of through it: 'ap->a_ncookies -= ncookies;' (:262) β leaving the caller's ncookies at the full allocated value. Every sibling FS dereferences (msdosfs, ufs, hammer, hammer2, tmpfs). The in-tree consumers (nfsrv_readdir/nfsrv3_readdir) are coincidentally dirent-bounded, so no live OOB today β but any consumer trusting the count reads uninitialized kernel heap straight into NFS READDIR replies, and the 'ncookies == 0' stale-cookie retry guard is unreachable for ext2. Filed Low/certain: defect objectively wrong, impact latent. Fix: one character ('ap->a_ncookies -= ncookies;') + optionally M_ZERO.
Timeline
- 2026-09-02 Discovered during pass-2 audit of ext2_vnops.c (GLM 5.3). DF-0783 re-confirmed still present (fresh runtime GPF on stock kernel appended to its pack); DF-0784 unchanged.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3031 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 2.9 KB | β raw | |
| fix.diff | β | 388 B | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 1.6 KB | view raw |
DF-3031 β ext2_readdir never updates the caller's *a_ncookies (missing * deref)
Severity: Low (latent uninitialized-kernel-heap disclosure / VOP contract violation)
Class: CWE-628 (incorrect API contract) + latent CWE-908 (uninitialized memory exposure)
File: sys/vfs/ext2fs/ext2_lookup.c:262 (function dispatched from
sys/vfs/ext2fs/ext2_vnops.c:2142 β .vop_readdir = ext2_readdir)
Confidence: certain (the defect is objectively wrong vs. its own intent and every
sibling FS; impact ceiling is honestly latent β see below)
Summary
ext2_readdir allocates a cookie array sized for the worst case, reports the
allocated count to the caller, fills only part of it, and then tries to
subtract the unfilled remainder β but forgets the *, so the caller's count is
never corrected:
/* ext2_lookup.c:171-175 β allocation is NOT zeroed */
ncookies = ncookies / (offsetof(struct ext2fs_direct_2, e2d_namlen) + 4) + 1;
cookies = malloc(ncookies * sizeof(*cookies), M_TEMP, M_WAITOK);
*ap->a_ncookies = ncookies; /* caller sees the ALLOCATED count */
*ap->a_cookies = cookies;
...
/* ext2_lookup.c:241-246 β one cookie filled per dirent actually written */
...
/* ext2_lookup.c:260-262 */
if (ap->a_ncookies != NULL) {
if (error == 0) {
ap->a_ncookies -= ncookies; /* BUG: pointer arithmetic on the
arg pointer; should be
*ap->a_ncookies -= ncookies */
Every other DragonFly filesystem dereferences here:
sys/vfs/msdosfs/msdosfs_vnops.c:1738 (*ap->a_ncookies -= ncookies),
sys/vfs/ufs/ufs_vnops.c:1634 (*ap->a_ncookies = cookie_index),
hammer/hammer2/tmpfs all set the used count. The typo corrupts only the local
copy of the int * (harmless in itself) while leaving the caller's ncookies
at the full allocated value (e.g. uio_resid/10 + 1), of which only
allocated - remaining slots were initialized.
Impact
- The cookies array is
malloc'd without M_ZERO (ext2_lookup.c:173), so the unfilled tail is stale kernel heap. Any consumer that iteratescookies[0 .. ncookies)(as the contract entitles it to) reads uninitialized kernel memory β for the only in-tree cookie consumer (the NFS server,sys/vfs/nfs/nfs_serv.c:3045and:3337) that would turn into a kernel-heap disclosure over the network in READDIR replies. - Today's in-tree consumers are coincidentally safe: both nfsrv loops pair
each cookie with a dirent and are bounded by
cpos < cendwherecendtracks the bytes actually written (nfs_serv.c:3066-3067, 3101-3102), so they never touch the uninitialized tail. The over-report additionally makes the stale-cookie recovery checkncookies == 0 β retry(nfs_serv.c:3120, 3407) unreachable for ext2, degrading NFS READDIR restart correctness on ext2. - Net: contract violation + latent heap-info-leak/OOB read primitive for any current or future consumer that trusts the count. Filed Low because no live disclosure exists in-tree today.
Fix
One character: dereference the pointer.
--- a/sys/vfs/ext2fs/ext2_lookup.c
+++ b/sys/vfs/ext2fs/ext2_lookup.c
@@ -259,7 +259,7 @@
if (error == EJUSTRETURN)
error = 0;
if (ap->a_ncookies != NULL) {
if (error == 0) {
- ap->a_ncookies -= ncookies;
+ *ap->a_ncookies -= ncookies;
} else {
free(*ap->a_cookies, M_TEMP);
(Applying M_ZERO to the malloc at :173 is a reasonable belt-and-suspenders
addition, but the deref fix restores the contract.)
Verification status
Verification skipped by policy (Low severity / latent impact β no Critical/High,
memcorrupt, privesc, or trivially-runnable local trigger). A runtime
demonstration would require exporting an ext2 filesystem over loopback NFS in
the single-tenant guest and instrumenting the server; even then the in-tree
consumer's dirent-bounded loop would show no visible misbehavior β the defect
is a code-level contract violation proven by inspection above. See
VERDICT.md for the full static proof.
DF-3031 β VERDICT
Status: untested (verification skipped by policy β Low severity, latent impact) Defect: certain. Impact: latent (no live in-tree OOB/leak β proven below).
The defect is real and certain
sys/vfs/ext2fs/ext2_lookup.c:173-174:cookies = malloc(ncookies * sizeof(*cookies), M_TEMP, M_WAITOK);(no M_ZERO) and*ap->a_ncookies = ncookies;β the caller is handed the allocated count.sys/vfs/ext2fs/ext2_lookup.c:241-247: exactly one cookie slot is filled pervop_write_dirent()success; skipped entries (e2d_ino == 0oroffset < startoffset, line 227-228) fill neither dirent nor cookie. The used count is thereforeallocated - ncookies_remaining, strictly smaller than the allocation whenever any entry is skipped or the uio fills before the worst case.sys/vfs/ext2fs/ext2_lookup.c:262:ap->a_ncookies -= ncookies;performs pointer arithmetic on the local copy ofint *a_ncookies(advancing itncookies * sizeof(int)bytes into nfsrv's frame β unused afterwards). The caller'sncookieskeeps the allocated value. Every sibling implementation writes through the pointer:sys/vfs/msdosfs/msdosfs_vnops.c:1738,sys/vfs/ufs/ufs_vnops.c:1634,sys/vfs/hammer/hammer_vnops.c:1769,sys/vfs/hammer2/hammer2_vnops.c:767,sys/vfs/tmpfs/tmpfs_vnops.c(cookie_index). The vop contract (documented atsys/vfs/nfs/nfs_serv.c:2890-2902) is "VOP_READDIR() returns the number of valid cookies".
Why the impact is only latent today
The only in-tree consumers are nfsrv_readdir (nfs_serv.c:3045) and
nfsrv3_readdir (nfs_serv.c:3337). Both walk cookies strictly in lockstep
with dirents inside while (cpos < cend && ncookies > 0) loops
(nfs_serv.c:3112-3119/3141-3204 and 3399-3405/3445-3549) where cend = rbuf +
siz and siz was reduced by io.uio_resid (nfs_serv.c:3066-3067) β i.e.
bounded by the bytes ext2_readdir actually wrote. Cookie reads therefore
never run past the filled region with the current server code. Consequences
that do exist today: the stale-cookie retry guard if (cpos >= cend ||
ncookies == 0) (nfs_serv.c:3120, 3407) can never take its ncookies == 0
branch for ext2, and any future/refactored consumer that trusts the reported
count reads uninitialized kernel heap (malloc, no M_ZERO) straight into NFS
reply buffers β 8 stale bytes per over-counted cookie.
Why we did not build a guest PoC
- No Critical/High, memcorrupt, privesc bucket; not a trivially-runnable local trigger (requires an NFS server fronting an ext2 mount).
- Even with loopback NFS in the guest, the in-tree consumer is dirent-bounded, so no externally observable misbehavior exists to observe; the demonstration would have to be a code-reading proof identical to the one above.
Recommended fix
*ap->a_ncookies -= ncookies; (one character). Optionally add M_ZERO at
the malloc. See fix.diff.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-3031/VERDICT.md β static proof with sibling-FS comparison (msdosfs_vnops.c:1738 dereferences)', 'findings/poc/DF-3031/fix.diff β one-character fix']
PoC changes
n/a β no PoC seed; static-proof pack only
Verified recommended fix
*ap->a_ncookies -= ncookies; (dereference the pointer; optionally M_ZERO the cookie malloc)
Verdict
Defect certain, impact latent: ext2_readdir reports the allocated cookie count and never corrects it because line 262 does pointer arithmetic (ap->a_ncookies -= ncookies) instead of writing through the pointer. The cookie array is malloc'd without M_ZERO, so the unfilled tail is uninitialized kernel heap; today's only consumers (nfs_serv.c:3045/3337) are coincidentally dirent-bounded, so no live disclosure exists in-tree β filed Low as a VOP-contract violation with latent heap-leak/OOB-read for any consumer that trusts the count. Verification skipped by policy (Low/latent; no runnable local trigger).
No comments yet.