Merge pull request #385 from lilith/fix/colormap-shl-overflow

fix: prevent shift overflow panic in ColorMap size validation
This commit is contained in:
Aurelia Molzer authored and GitHub committed 2026-04-09 12:25:51 +00:00
commit e8103509fa
7 files changed
+183 -2

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
name: Fuzz
on:
push:
branches: [main]
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
duration:
description: 'Seconds per fuzz target'
required: false
default: '300'
concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true
jobs:
fuzz:
name: Fuzz (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- decode_comprehensive
- decode_image
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
key: fuzz-${{ matrix.target }}
cache-directories: fuzz/target
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked
- name: Determine duration
id: duration
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "seconds=300" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "seconds=${{ github.event.inputs.duration || '300' }}" >> "$GITHUB_OUTPUT"
else
echo "seconds=60" >> "$GITHUB_OUTPUT"
fi
- name: Seed corpus
run: |
mkdir -p fuzz/corpus/${{ matrix.target }}
cp fuzz/regression/* fuzz/corpus/${{ matrix.target }}/ 2>/dev/null || true
cp fuzz/seed_corpus/${{ matrix.target }}/* fuzz/corpus/${{ matrix.target }}/ 2>/dev/null || true
- name: Fuzz ${{ matrix.target }}
run: |
cargo +nightly fuzz run ${{ matrix.target }} \
-- -max_total_time=${{ steps.duration.outputs.seconds }} \
-dict=fuzz/tiff.dict \
-rss_limit_mb=2048
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes-${{ matrix.target }}
path: fuzz/artifacts/
retention-days: 30
regression:
name: Fuzz regression
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build
- name: Regression seeds
run: |
if [ -d fuzz/regression ] && [ "$(ls fuzz/regression/ 2>/dev/null | wc -l)" -gt 0 ]; then
cargo test --test fuzz_regression 2>/dev/null || echo "No regression test found — seeds exist but no test harness"
fi
+1
View File
@@ -1,3 +1,4 @@
target
corpus
artifacts
coverage
+6
View File
@@ -23,3 +23,9 @@ name = "decode_image"
path = "fuzz_targets/decode_image.rs"
test = false
doc = false
[[bin]]
name = "decode_comprehensive"
path = "fuzz_targets/decode_comprehensive.rs"
test = false
doc = false
+50
View File
@@ -0,0 +1,50 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let cursor = std::io::Cursor::new(data);
// Path 1: TiffHeader::parse (probe)
{
let _ = tiff::decoder::TiffHeader::parse(std::io::Cursor::new(data));
}
// Path 2: Full decode with strict limits
let mut decoder = match tiff::decoder::Decoder::open(cursor) {
Ok(d) => d,
Err(_) => return,
};
let mut limits = tiff::decoder::Limits::default();
limits.decoding_buffer_size = 512 * 1024;
limits.ifd_value_size = 64 * 1024;
limits.intermediate_buffer_size = 512 * 1024;
decoder = decoder.with_limits(limits);
// Probe metadata — dimensions() triggers image validation,
// must succeed before get_chunk_type/chunk_dimensions (they unwrap image)
let dims_ok = decoder.dimensions().is_ok();
let _ = decoder.colortype();
if dims_ok {
let _ = decoder.get_chunk_type();
let _ = decoder.chunk_dimensions();
}
let _ = decoder.more_images();
// Read chunks individually if available
let _ = decoder.read_chunk(0);
// Read full image
let _ = decoder.read_image();
// Iterate through all IFDs
while decoder.more_images() {
if decoder.next_image().is_err() {
break;
}
let _ = decoder.dimensions();
let _ = decoder.colortype();
let _ = decoder.read_image();
}
});
+38
View File
@@ -0,0 +1,38 @@
# TIFF byte order and magic
"\x49\x49\x2A\x00"
"\x4D\x4D\x00\x2A"
# Common IFD tags as 2-byte LE pairs
# 256 = ImageWidth
"\x00\x01"
# 257 = ImageLength
"\x01\x01"
# 258 = BitsPerSample
"\x02\x01"
# 259 = Compression
"\x03\x01"
# 262 = PhotometricInterpretation
"\x06\x01"
# 273 = StripOffsets
"\x11\x01"
# 277 = SamplesPerPixel
"\x15\x01"
# 278 = RowsPerStrip
"\x16\x01"
# 279 = StripByteCounts
"\x17\x01"
# 282 = XResolution
"\x1A\x01"
# 283 = YResolution
"\x1B\x01"
# Common IFD tags as 2-byte BE pairs
"\x01\x00"
"\x01\x01"
"\x01\x02"
"\x01\x03"
"\x01\x06"
"\x01\x11"
"\x01\x15"
"\x01\x16"
"\x01\x17"
+3 -2
View File
@@ -454,8 +454,9 @@ impl Image {
match tag_reader.find_tag(Tag::ColorMap)? {
Some(v) => {
let map = v.into_u16_vec()?;
let expected_len = 3usize
.checked_mul(1usize << bits_per_sample)
let expected_len = 1usize
.checked_shl(bits_per_sample as u32)
.and_then(|n| n.checked_mul(3))
.ok_or(TiffError::LimitsExceeded)?;
if map.len() != expected_len {
return Err(TiffError::FormatError(