Skip to content

e2fsprogs: improve fuzzing coverage#58

Open
tc-agent wants to merge 2 commits into
masterfrom
e2fsprogs-coverage
Open

e2fsprogs: improve fuzzing coverage#58
tc-agent wants to merge 2 commits into
masterfrom
e2fsprogs-coverage

Conversation

@tc-agent

@tc-agent tc-agent commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

This change improves coverage of the three existing libFuzzer harnesses and adds two new ones: a filesystem walker (read side) and a write-side harness exercising allocation, write-back, journal-inode creation, and metadata mutation.

According to Fuzz Introspector the project currently sits at 10.99% line coverage (1,814 lines), with `ext2fs_check_directory_fuzzer` reported at 0%. The three existing harnesses ship without seed corpora, so libFuzzer starts from a zero-byte input and never gets past the superblock magic check in `ext2fs_open` within a typical CI run.

What changes

`build.sh` — bundle a per-harness seed corpus

Two seed sources, generated at build time so no binaries are checked in:

  1. Decompress every test image with compressed size under 20 KB (yielding ~100 valid/corrupt filesystem images of 100 KB–1 MB). These are the regression inputs for e2fsck.
  2. Generate fresh `mke2fs` images covering feature combinations the upstream test corpus underrepresents: `64bit`, `mmp`, `casefold`, `bigalloc`, `encrypt`, `verity`, `fast_commit`, `project,quota`, `ea_inode`, `inline_data`, `orphan_file`, `metadata_csum`, alternative block sizes, `extra_isize`, `huge_file`, etc.

Each candidate seed is replayed once through its target harness (3-second timeout) and only kept if the replay exits cleanly. A `.options` file is emitted alongside each fuzzer — including the three pre-existing ones — setting `max_len = 1048576`. libFuzzer's default 4 KB cap would otherwise discard every seed.

`fuzz/ext2fs_iterate_fuzzer.cc` — new read-side walker

Mirrors the maintainer-authored `misc/check_fuzzer.c` approach but broadens it. After `ext2fs_open` + `ext2fs_read_bitmaps` + `ext2fs_check_desc`, it walks the root directory recursively, then the inode scan. For each entry: `check_directory`, `read_inode`, `get_pathname`, `dirhash`, `lookup`. For each inode: `xattrs_open`/`xattrs_read`/`xattrs_iterate`/`xattr_get`, then directory iteration, extent walking (`extent_open` + `extent_get` with multiple traversal ops), or `block_iterate3`, plus `bmap2` and `file_read`.

`fuzz/ext2fs_write_fuzzer.cc` — new write-side harness

Opens the image `EXT2_FLAG_RW` and exercises allocation/write paths: `new_block2`/`block_alloc_stats2`, `new_inode`/`write_new_inode`/`write_inode`, `link`/`unlink`, `mkdir` + bulk link of 32 entries (htree split paths), `xattr_set`/`xattrs_write`/`xattr_remove`, `punch` (bounded and `~0ULL`), per-inode `extent_set_bmap`/`extent_fix_parents`/`extent_node_split`/`extent_delete`, `bmap2` with `BMAP_ALLOC`, `expand_dir`, `init_dblist` + `add_dir_block2` + `dblist_iterate`, `create_icount2` + `icount_store`/`increment`/`decrement`/`fetch`/`validate`, `inline_data_init` + `inline_data_set`, `add_journal_inode2`, `create_orphan_file`, `set_gdt_csum`, then `ext2fs_flush` + `ext2fs_close` to drive close-time superblock/bitmap/csum write paths in `closefs.c`, `csum.c`, `rw_bitmaps.c`.

Coverage after this change

Running `fuzz_for_pr` for 5 minutes wall-time with all harnesses concurrent, measured by `llvm-cov`:

harness line coverage
`ext2fs_check_directory_fuzzer` 6.2% (1,002)
`ext2fs_image_read_write_fuzzer` 10.6% (1,739)
`ext2fs_iterate_fuzzer` (new) 24.5% (4,015)
`ext2fs_read_bitmap_fuzzer` 9.8% (1,581)
`ext2fs_write_fuzzer` (new) 36.9% (7,236)
Union (global) 41.1% — 8,364 lines

Compared with the Fuzz Introspector baseline of 1,814 lines, this is +6,550 absolute lines of attacker-reachable libext2fs code under fuzzing — 4.6× more code exercised. Notable per-file gains (union across all harnesses): `extent.c` ~64%, `inode.c` ~70%, `block.c` ~70%, `dir_iterate.c` ~73%, `mkdir.c` ~86%, `unlink.c` ~84%, `csum.c` ~61%, `closefs.c` ~59%, `punch.c` ~67%, `orphan.c` ~72%, `bmap.c` ~41%, `inline_data.c` ~65%, `mmp.c` ~50%, `icount.c` ~50%, `alloc.c` ~44%, `ext_attr.c` ~48%.

Test plan

  • `build_fuzzers --sanitizer address` builds all five harnesses
  • `build_fuzzers --sanitizer coverage` builds for llvm-cov
  • All five harnesses load their seed corpora cleanly
  • 5-minute concurrent run via `fuzz_for_pr`

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown

Fuzzing Coverage Report

Tested: project e2fsprogs · base d6faa1f → head 87de67f · 300s total fuzz budget · updated 2026-05-28 00:29 UTC · workflow run

Metric Before After Delta
Static reachability 19.8% (159/805) 55.5% (548/987) +35.8%
Line coverage 2.8% (470/16510) 40.8% (8295/20342) +1664.9%
Branch coverage 1.6% (123/7908) 34.0% (3312/9739) +2592.7%
Function coverage 3.8% (31/811) 43.8% (435/994) +1303.2%

Per-harness

Harness Lines before Lines after Δ
ext2fs_check_directory_fuzzer 0.0% (0/16124) 6.6% (1069/16124) new
ext2fs_image_read_write_fuzzer 2.7% (446/16471) 10.6% (1739/16471) +289.9%
ext2fs_iterate_fuzzer 0% 24.5% (4025/16406) new
ext2fs_read_bitmap_fuzzer 2.7% (442/16146) 9.8% (1577/16146) +256.8%
ext2fs_write_fuzzer 0% 36.5% (7156/19623) new

Δ = (after − before) / before, to accommodate that denominators may change. "new" when before is 0; "deleted" when after is 0.

@tc-agent

Copy link
Copy Markdown
Owner Author

CI failures are both fuzz-for-me infra issues, not from this PR's diff:

  1. presubmit checks: Missing license header in .github/actions/fuzz-all/run_fuzzers.py — the script comes from fuzz-for-me's ci/run_fuzzers.py (overlaid by the PR server), which lacks the Apache 2.0 header that oss-fuzz/infra/presubmit.py:check_license requires. A fix is queued on the fuzz-for-me side (suggestions/run-fuzzers-license-header), but it hasn't reached the host's checkout yet. Sister files render_comment.py and check_new_seeds.py already carry the same header.

  2. infra tests: FAILED infra/helper_test.py::ShellTest::test_base_runner_debug — an upstream oss-fuzz helper_test.py assertion failure, unrelated to anything in this PR.

Both reproduce identically on consecutive runs and against the unchanged baseline diff. The actual project changes (projects/e2fsprogs/build.sh and projects/e2fsprogs/fuzz/ext2fs_iterate_fuzzer.cc) pass all build/measure jobs: build (libfuzzer, address), build (libfuzzer, coverage), build (afl, address), build (honggfuzz, address), build (centipede, address), etc., plus both Measure (baseline) and Measure (current).

@tc-agent
tc-agent force-pushed the master branch 5 times, most recently from 177029d to 30a48e5 Compare May 11, 2026 16:04
@tc-agent
tc-agent force-pushed the e2fsprogs-coverage branch from 4655f18 to 8f81b4a Compare May 11, 2026 16:04
@tc-agent

Copy link
Copy Markdown
Owner Author

Updated this PR with a second new harness (ext2fs_write_fuzzer) plus a richer iterate fuzzer + a small mke2fs-generated seed set. Coverage is now 41.9% (7,235/17,279 lines) vs the 10.99% Fuzz Introspector baseline — +30.9 absolute percentage points.

CI status: the project's own builds all pass — build (libfuzzer, address), (libfuzzer, undefined), (libfuzzer, memory), (libfuzzer, coverage), (libfuzzer, address, i386), (afl, address), (honggfuzz, address), (centipede, address), (centipede, none), (none, address), plus Measure (baseline) and Measure (current). The only failing job is the same fuzz-for-me infra issue from before: Missing license header in .github/actions/fuzz-all/run_fuzzers.py. That file is overlaid by the PR server from fuzz-for-me's ci/run_fuzzers.py; the queued suggestion branch (suggestions/run-fuzzers-license-header) hasn't reached the host's checkout yet.

@tc-agent
tc-agent force-pushed the master branch 3 times, most recently from c053f96 to 3fe38a2 Compare May 11, 2026 19:27
@tc-agent
tc-agent force-pushed the e2fsprogs-coverage branch from 8f81b4a to c3ebf8f Compare May 11, 2026 19:27
@tc-agent

Copy link
Copy Markdown
Owner Author

CI is fully green: build (libfuzzer, address), (libfuzzer, undefined), (libfuzzer, memory), (libfuzzer, coverage), (libfuzzer, address, i386), (afl, address), (honggfuzz, address), (centipede, address), (centipede, none), (none, address), plus presubmit checks, infra tests, Measure (baseline), Measure (current), CodeQL, Ubuntu File Synchronization Check, check-consistency.

@tc-agent
tc-agent force-pushed the master branch 7 times, most recently from ad01771 to 55041f5 Compare May 14, 2026 12:07
@tc-agent
tc-agent force-pushed the e2fsprogs-coverage branch from c3ebf8f to 7504d0e Compare May 14, 2026 12:07
@tc-agent
tc-agent force-pushed the master branch 6 times, most recently from ed731ba to 64e909c Compare May 16, 2026 14:17
@tc-agent
tc-agent force-pushed the e2fsprogs-coverage branch from 7504d0e to 7745f3c Compare May 16, 2026 15:42
@tc-agent
tc-agent force-pushed the master branch 9 times, most recently from 9a36aa8 to 6a4a93d Compare May 21, 2026 13:42
@tc-agent
tc-agent force-pushed the master branch 3 times, most recently from 6125235 to e512e7e Compare May 27, 2026 06:33
@tc-agent
tc-agent force-pushed the e2fsprogs-coverage branch from 7745f3c to 60da077 Compare May 27, 2026 07:48
@tc-agent
tc-agent force-pushed the master branch 5 times, most recently from b5fd10c to 712a52f Compare May 27, 2026 16:16
@tc-agent
tc-agent force-pushed the e2fsprogs-coverage branch from 60da077 to 87de67f Compare May 28, 2026 00:18
@tc-agent
tc-agent force-pushed the master branch 2 times, most recently from a7587cd to b952f62 Compare May 28, 2026 02:55
@tc-agent tc-agent added the ready label May 28, 2026
@tc-agent tc-agent added forwarded and removed ready labels May 28, 2026
@tc-agent
tc-agent force-pushed the master branch 3 times, most recently from af7f9a7 to 806b281 Compare May 28, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant