Skip to content

Stop rejecting sparse CHDv5 images - #191

Merged
rtissera merged 1 commit into
masterfrom
fix-rle-map-rejection
Sep 10, 2026
Merged

Stop rejecting sparse CHDv5 images#191
rtissera merged 1 commit into
masterfrom
fix-rle-map-rejection

Conversation

@rtissera

Copy link
Copy Markdown
Owner

Fixes #190.

5cc52fd bounded totalhunks by the file's own size in bits, on the reasoning that "every hunk map entry consumes at least one bit in the compressed on-disk map". That does not hold for CHDv5: its map is run-length and Huffman coded, so a run of identical, uncompressed or self-referenced hunks costs far less than one bit each.

Reproduced exactly as reported, and worse than the numbers in the issue:

64 MiB of zeros -> 203 bytes
logicalbytes=67108864  hunkbytes=4096  totalhunks=16384
file_size*8=1624   ->   16384 > 1624, rejected
chdman verify: Overall SHA1 verification successful!
libchdr:       open: invalid data

Eighty hunks per byte, where the check allowed eight. As @seanbrar notes, the better an image compresses the more certainly it is refused.

The fix

The guard is still needed — totalhunks sizes the map allocation, and a malformed header could otherwise ask for gigabytes against a tiny file. It is replaced by a bound on each version's actual constraint.

v1-v4 have an exact one. Their map is a plain array of fixed-size entries stored immediately after the header, so header->length + totalhunks * entrysize has to fit in the file. That is tighter than the old check, and true.

v5 has no such invariant, so this bounds what is really allocated: map_size_v5() caps the materialized map at CHD_MAX_MAP_SIZE. 256 MiB covers over 400 GB at CD geometry and about 89 GB at 4 KB hunks — past anything this library claims to handle — while keeping a corrupt hunkcount from requesting more than a host would give. LOWRAM_TARGET never materializes the map and is unaffected either way.

This is the direction the issue suggested: bound the allocation rather than infer a ratio.

Testing

tests/sparse_map.c opens the seed and decodes every hunk. It fails before this change with open failed: invalid data. The seed is built by generate.sh with chdman from a zero-filled file, so it needs no third-party data.

Six build configurations — default, CHDR_LOWRAM_TARGET=ON, CHDR_CD_SCRATCH_BUFFER=OFF, WITH_SYSTEM_ZLIB=ON, the no-subcode/no-CRC combination, and LOWRAM with system zlib. All accept the sparse image, and all still reject three hand-corrupted headers (v5 logicalbytes at 1 TiB and at INT64_MAX, v4 totalhunks at 2^30) under a hard 300 MB address-space limit, so nothing gets through by allocating first and failing later.

Decoded output is unchanged across the corpus — 36 files, no difference in data or in error codes. ASan/UBSan clean on the targeted cases and on the mutant set.

🤖 Generated with Claude Code

5cc52fd bounded totalhunks by the file's own size in bits, on the reasoning
that "every hunk map entry consumes at least one bit in the compressed on-disk
map". That is not true of CHDv5: its map is run-length and Huffman coded, so a
run of identical, uncompressed or self-referenced hunks costs far less than one
bit each. 64 MiB of zeros becomes a 203-byte file carrying 16384 hunks - eighty
hunks per byte where the check allowed eight - and chd_open() refused it while
chdman verified it. Reported in #190 against a 50 GB image of about 23 KB, and
the better an image compresses the more certainly it is rejected.

The guard is still needed: totalhunks sizes the map allocation, and a malformed
header could ask for gigabytes against a tiny file. Replaced with a bound on
each version's actual constraint.

v1-v4 have an exact one. Their map is a plain array of fixed-size entries
stored immediately after the header, so header->length + totalhunks * entrysize
has to fit in the file. Tighter than the old check, and true.

v5 has no such invariant, so bound what is really allocated instead:
map_size_v5() now caps the materialized map at CHD_MAX_MAP_SIZE. 256 MiB covers
over 400 GB at CD geometry and about 89 GB at 4 KB hunks, which is past
anything this library claims to handle, while keeping a corrupt hunkcount from
requesting more than a host would give. LOWRAM_TARGET never materializes the
map at all and is unaffected.

tests/sparse_map.c opens the seed and decodes every hunk; it fails before this
change with "invalid data". The seed comes from generate.sh, built with chdman
from a zero-filled file, so it needs no third-party data.

Checked in six build configurations - default, LOWRAM_TARGET, CD_SCRATCH_BUFFER
off, system zlib, the no-subcode/no-CRC combination, and LOWRAM with system
zlib. All accept the sparse image and all still reject three hand-corrupted
headers (v5 logicalbytes at 1 TiB and at INT64_MAX, v4 totalhunks at 2^30)
under a hard 300 MB address-space limit, so nothing slipped through by
allocating first. Decoded output is unchanged across the corpus - 36 files, no
difference in data or error codes - and ASan/UBSan is clean on the targeted
cases and the mutant set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01846EhHuAFk5qvxwEA5Gq6y
@rtissera
rtissera merged commit 3dad638 into master Sep 10, 2026
38 checks passed
@rtissera
rtissera deleted the fix-rle-map-rejection branch September 10, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Valid CHDv5 images with RLE maps rejected by totalhunks > file_size * 8 check

1 participant