Latent memory-safety landmines in the #if 0-disabled htree write path: heap memcpy overflow in the index-node split, four double-brelse-after-bwrite error paths, u16 reclen truncation at 64K blocks
Summary
ext2_htree_add_entry(), ext2_htree_create_index() and helpers are dead code in the stock kernel (#if 0 at ext2_lookup.c:920-941 gates every caller) but harbor three defects that become live the moment anyone re-enables the path (FreeBSD keeps it compiled in): (1) the index-node split memcpy at :857-858 copies dst_ent_num*8 bytes derived from the on-disk interior count (only checked count<=on-disk-limit by find_leaf - DF-0850's missing node_limit validation) into a blksize buffer, overflowing by up to ~256 KiB on a crafted image, with the follow-up memmove at :475-476 writing one entry past the limit; (2) DFly bwrite() unconditionally consumes the buffer (vfs_bio.c:1000), but four error paths brelse() the same buffer again (:457-459+:958-959, :882-885+:950-951, :938-941+:952-953, :742-746+:765-767) - any index-write I/O error causes a buf double-release/UAF; (3) htole16(block + blksize - last_entry - csum_size) at :527-528/:644-645 wraps to 0 for 64 KiB fs blocks, producing reclen=0 dirents (infinite-skip/ext2_dirbad panic family). None in the stock kernel (unreachable). Analysis-only pack; illustrative fix.diff for the double-brelse family.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3123 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 868 B | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| fix.diff | β | 1.1 KB | view raw | |
| verdict.json | β | 2.2 KB | view raw | |
| manifest.json | β | 524 B | view raw |
DF-3123 β latent memory-safety landmines in the #if 0-disabled htree write path
Info-severity analysis-only pack. No Phase V run: the code is dead in the
stock kernel (ext2_lookup.c:920-941 #if 0 gates every caller).
Contents:
- VERDICT.md β the three latent defects (heap memcpy overflow in the
index-node split gated by DF-0850's missing validation; double-brelse of
bwrite-consumed buffers on all I/O-error paths; u16 reclen truncation at
64 KiB blocks) with path:line citations.
- fix.diff β illustrative patch for the double-brelse family (the
highest-risk one if the path is ever re-enabled).
Relevance: FreeBSD's ext2 keeps this path compiled in; a DragonFly re-enable without these fixes walks into a kernel heap write overflow from a crafted image. See DF-3122 (the live-code sibling in this file) for the verified bug.
DF-3123 β Latent memory-safety landmines in the disabled (#if 0) ext2 htree write path
Status
Info (latent / dead code). No Phase V verification performed β none of this
code is reachable in the stock kernel: ext2_direnter() compiles the entire
htree write path out (sys/vfs/ext2fs/ext2_lookup.c:920-941, #if 0), which
gates the only callers of ext2_htree_add_entry(), ext2_htree_create_index()
and (transitively) ext2_htree_writebuf(), ext2_htree_split_dirblock(),
ext2_htree_insert_entry*(), ext2_append_entry().
This pack documents three latent defects for whoever re-enables that path (FreeBSD has it enabled; a dfly re-enable without these fixes walks straight into a heap write overflow):
1. Heap buffer overflow in the index-node split (ext2_htree.c:841-877)
ext2_htree_add_entry() splits a full interior node:
816: entries = info.h_levels[info.h_levels_num - 1].h_entries; /* interior node */
817: ent_num = ext2_htree_get_count(entries); /* from disk */
818: if (ent_num == ext2_htree_get_limit(entries)) { /* limit from disk */
...
852: src_ent_num = ent_num / 2;
853: dst_ent_num = ent_num - src_ent_num;
857: memcpy(dst_entries, entries + src_ent_num,
858: dst_ent_num * sizeof(struct ext2fs_htree_entry)); /* dst = blksize malloc */
The only validation ext2_htree_find_leaf() performs on an interior node is
cnt <= on-disk limit (ext2_htree.c:313-315); the limit is never checked
against the buffer capacity (ext2_htree_node_limit()) β that missing check is
DF-0850. On a crafted image with interior count = limit = 0xFFFF, a file
create inside the directory overflows the fresh blksize index block by
dst_ent_num*8 - (blksize-8) bytes (up to ~256 KiB past a 1 KiB allocation)
with heap-adjacent content, plus the same OOB source read. The follow-up
ext2_htree_insert_entry() (:928 β :472-479) memmoves with the same
unchecked count, an intra-buffer OOB write of one entry past the limit.
Fixed automatically by DF-0850's recommended fix (validate interior limit ==
ext2_htree_node_limit() after the :335 descent), but should be stated.
2. Double-brelse of buffers already consumed by bwrite() (error paths)
DragonFly's bwrite() releases the buffer unconditionally, error or not
(sys/kern/vfs_bio.c:1000). Four error paths here brelse() the same buffer
again β buf refcount underflow / use-after-free of struct buf:
ext2_htree_writebuf():454-459: returns after a failedbwrite(bp); callerext2_htree_add_entry()then takesfinish:withwrite_info == 0andext2_htree_release()(:958-959) brelse's all level buffers again β including every bufferbwrite()already consumed.ext2_htree_add_entry():882-885:bwrite(dst_bp)error βfinish::950-951brelse'sdst_bpagain.ext2_htree_add_entry():938-941:bwrite(bp)error βfinish::952-953brelse'sbpagain.ext2_htree_create_index():742-746:bwrite(bp)error βout::765-767brelse'sbpagain.
Trigger precondition (if ever enabled): any I/O error writing the index (e.g. vn-backed image whose backing store fills / errors).
Fix sketch: on bwrite() error do not touch the buffer again; have
ext2_htree_writebuf() mark which levels were consumed, or brelse() before
returning only the not-yet-written ones.
3. u16 reclen truncation with 64 KiB blocks (ext2_htree.c:527-528, 644-645)
527: new_entry->e2d_reclen = htole16(block + blksize - (char *)last_entry - csum_size);
644: last->e2d_reclen = htole16(block1 + blksize - (char *)last - csum_size);
With e2fs_bsize == 65536 and the appended entry at block start, the value
65536 does not fit uint16_t and wraps to 0 β downstream walkers then see
reclen == 0 (infinite-skip / ext2_dirbad panic family, cf. DF-3083).
Clamp or refuse blksize > 32768 in this path.
Artifacts
analysis-only pack: no trigger, no guest run (dead code). See
fix.diff for the illustrative combined patch.
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.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-3123/VERDICT.md (three defects, path:line)', 'findings/poc/DF-3123/fix.diff (illustrative double-brelse fix)']
PoC changes
n/a (analysis-only; no PoC exists for dead code)
Verified recommended fix
If re-enabling the write path: validate interior node limit == ext2_htree_node_limit (folds into DF-0850's fix), stop touching buffers after bwrite() returns (bwrite consumes), and clamp/avoid u16 reclen math for 64K blocks.
Verdict
Latent-only: every affected function (ext2_htree_add_entry, ext2_htree_create_index, ext2_htree_writebuf, ext2_htree_split_dirblock, ext2_htree_insert_entry, ext2_append_entry) is dead code in the stock kernel behind the #if 0 at sys/vfs/ext2fs/ext2_lookup.c:920-941, so nothing is reachable at runtime. Documented for any future re-enable: (1) ext2_htree.c:852-858 memcpy of dst_ent_num8 on-disk-count-derived bytes into a blksize buffer overflows by up to ~256KiB (gated by DF-0850's missing interior node_limit validation, plus the :475-479 memmove write); (2) four error paths brelse() buffers already consumed by bwrite() (DFLY bwrite releases unconditionally, sys/kern/vfs_bio.c:1000) -> buf double-release/UAF on any index-write I/O error (:457-459+:958-959, :882-885+:950-951, :938-941+:952-953, :742-746+:765-767); (3) u16 reclen truncation to 0 for 64KiB fs blocks (:527-528, :644-645). No guest verification performed (unreachable code).
No comments yet.