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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3123 Β· 5 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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 failed bwrite(bp); caller ext2_htree_add_entry() then takes finish: with write_info == 0 and ext2_htree_release() (:958-959) brelse's all level buffers again β€” including every buffer bwrite() already consumed.
  • ext2_htree_add_entry() :882-885: bwrite(dst_bp) error β†’ finish: :950-951 brelse's dst_bp again.
  • ext2_htree_add_entry() :938-941: bwrite(bp) error β†’ finish: :952-953 brelse's bp again.
  • ext2_htree_create_index() :742-746: bwrite(bp) error β†’ out: :765-767 brelse's bp again.

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_testable
↓ fix.diffper-fix-DF-3123

Confirmed 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).