Repository navigation
Assertion failure in cram_index_build() on a malformed CRAM file #2060
Description
Activity
Thanks for this. I put your suggested change into PR #2061.
You've also reminded me that I had some unfinished work to add index fuzzing, amongst other things. It's not finished yet as I was struggling to avoid spamming lots of temporary filenames (and for now just had fixed paths which forbid multi-threading or multiple tests running). However it did find a variety of issues, just not this one.
- added a commit that references this issue
on Aug 6, 2026 - added a commit that references this issue
on Aug 6, 2026 Thanks for the quick fix.
One follow-up, since it may be worth more than the single assert. That assert came from a generated fuzz harness calling sam_index_build2() on a temporary file of fuzzer bytes. test/fuzz/hts_open_fuzzer.c never calls sam_index_build*, sam_idx_init, or any other index-building entry point, so cram_index_build() and the BAI/CSI builders are not currently fuzzed at all — presumably why this one survived.
Would a target covering that be welcome in test/fuzz/? I am happy to write and test it. One caveat you should weigh first: it needs a real file per iteration. cram_index_build() is reachable only through the filename-based API — the call chain is sam_index_build2 → sam_index_build3 → hts_open(fn) — so the in-memory hopen("mem:", ...) approach hts_open_fuzzer.c uses cannot work here, and the per-iteration file costs throughput.
If you would rather leave index building unfuzzed, that is a perfectly good answer — I would just rather ask than send an unsolicited PR.
Fuzz testing the indexes is a bit of a pain due for the need for two files. We have already got a pull request (#2039) that adds a fuzzer to exercise the index code. Unfortunately it occasionally doesn't clean up after itself, so when I tried it the VM it was on eventually ran out of space. It also didn't find much, probably because it reads index files that it has made itself. It may be possible to fix it by getting the interfaces to read directly from file descriptors, but that will need some additions to the rest of the library.
It may be more useful to fuzz mutated indexes on a pre-existing file. That way there's only one file being mutated again, and it may manage to get to some interesting paths through the code. I'm not sure how efficient it would be though as many changes are likely to completely break the index, which would hopefully be rejected very quickly. That might make it difficult for the fuzzer to make progress as most of its changes would just lead to the same error handling code. It could be worth a try though to see how far it does get.
Thanks for offering to do the work and for asking first.
I do have a branch doing this, in https://github.com/jkbonfield/htslib/tree/fuzz-index, which found a few bugs although oddly not this one. I need to tidy that up and remove some of the commits which made their way into a separate memory-leak fixing PR (already merged).
The reason I didn't do a PR for this was it needs more work. Either it was creating lots of temporary files and leaving them in place as there's no tidy up procedure when a fuzzer fails, slowly running me out of disk space, or it was using a fixed filename (eg in /tmp) and forbidding multiple threads from running. I think we can overcome this maybe with data or mem URIs, but it needs more experimentation.
The point about harming throughput is an interesting one though, and it makes me wonder if we need a separate index fuzzer that can run in parallel with the file parsing fuzzer.
Summary
A 42-byte file makes
sam_index_build2()abort on an assertion instead of returning an error.cram_index_build()is documented to return-1on read failure, but a container whose firstblock is not a compression header trips
assert()and kills the process.repro.cis eleven lines and does only whatsamtools index <file>does:Version
Reproduced on the 1.24 release tarball (latest, 2026-07-09) and on current
develop; theassertion is at
cram/cram_index.c:823in both.No sanitizer, no fuzzing engine, no special flags. HTSlib's own configure does not define
NDEBUG, so the assertion is live in a default build.Host: Linux x86-64, GCC.
Cause
cram_index_build()(cram/cram_index.c:787) is documented asbut the container loop asserts on the content type of a block that comes straight from the file:
The block immediately before and the one immediately after are both handled with
goto err.Only the content-type check is an assertion, and nothing upstream of it guarantees the invariant
—
cram_read_block()returns whatever type the file declares.Impact
Assertion abort only. Rebuilding all of HTSlib with
-DNDEBUGand running the same input givesand an ASan+UBSan build of the same configuration is clean on it, so
cram_decode_compression_header()rejects the block through the normal error path and there is no memory-safety consequence behind the
assertion. The practical effect is that any program indexing an untrusted CRAM with a default-built
HTSlib aborts rather than reporting a bad file.
Possible fix
Treating it like the surrounding failures fixes it:
With that applied:
test/test.plgives 349 passed / 7 failed out of 356 — byte-identical to the unpatched 1.24 treein this environment (the 7 failures are pre-existing here and come from configuring without bz2 /
lzma / libcurl). I have not checked whether this is the fix you would prefer; you may want the
container reader to reject the block earlier instead.
Note on coverage
test/fuzz/hts_open_fuzzer.cnever callssam_index_build*or any other index-building entrypoint, so this path is not reachable from the OSS-Fuzz target — which is presumably why it has
survived.
How it was found
Automated fuzzing that called
sam_index_build2()on a temporary file filled with fuzzer bytes.The 162-byte original was minimised to the 42 bytes above and re-verified against a stock 1.24
release build, so the reproducer involves no fuzzing harness.