Skip to content

Commit f042c6d

Browse files
authored
Merge pull request #2605 from image-rs/bmp-encoder-careful-arithmetic
Harden bmp encoder arithmetic against overflows
2 parents ceb71e5 + 1fb1c0e commit f042c6d

File tree

2 files changed

+73
-35
lines changed

2 files changed

+73
-35
lines changed

src/codecs/bmp/encoder.rs

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,32 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
7575
let bmp_header_size = BITMAPFILEHEADER_SIZE;
7676

7777
let (dib_header_size, written_pixel_size, palette_color_count) =
78-
get_pixel_info(color_type, palette)?;
79-
let row_pad_size = (4 - (width * written_pixel_size) % 4) % 4; // each row must be padded to a multiple of 4 bytes
80-
let image_size = width
81-
.checked_mul(height)
82-
.and_then(|v| v.checked_mul(written_pixel_size))
83-
.and_then(|v| v.checked_add(height * row_pad_size))
78+
written_pixel_info(color_type, palette)?;
79+
80+
let (padded_row, image_size) = width
81+
.checked_mul(written_pixel_size)
82+
// each row must be padded to a multiple of 4 bytes
83+
.and_then(|v| v.checked_next_multiple_of(4))
84+
.and_then(|v| {
85+
let image_bytes = v.checked_mul(height)?;
86+
Some((v, image_bytes))
87+
})
8488
.ok_or_else(|| {
8589
ImageError::Parameter(ParameterError::from_kind(
8690
ParameterErrorKind::DimensionMismatch,
8791
))
8892
})?;
89-
let palette_size = palette_color_count * 4; // all palette colors are BGRA
93+
94+
let row_padding = padded_row - width * written_pixel_size;
95+
96+
// all palette colors are BGRA
97+
let palette_size = palette_color_count.checked_mul(4).ok_or_else(|| {
98+
ImageError::Encoding(EncodingError::new(
99+
ImageFormatHint::Exact(ImageFormat::Bmp),
100+
"calculated palette size larger than 2^32",
101+
))
102+
})?;
103+
90104
let file_size = bmp_header_size
91105
.checked_add(dib_header_size)
92106
.and_then(|v| v.checked_add(palette_size))
@@ -98,14 +112,23 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
98112
))
99113
})?;
100114

115+
let image_data_offset = bmp_header_size
116+
.checked_add(dib_header_size)
117+
.and_then(|v| v.checked_add(palette_size))
118+
.ok_or_else(|| {
119+
ImageError::Encoding(EncodingError::new(
120+
ImageFormatHint::Exact(ImageFormat::Bmp),
121+
"calculated BMP size larger than 2^32",
122+
))
123+
})?;
124+
101125
// write BMP header
102126
self.writer.write_u8(b'B')?;
103127
self.writer.write_u8(b'M')?;
104128
self.writer.write_u32::<LittleEndian>(file_size)?; // file size
105129
self.writer.write_u16::<LittleEndian>(0)?; // reserved 1
106130
self.writer.write_u16::<LittleEndian>(0)?; // reserved 2
107-
self.writer
108-
.write_u32::<LittleEndian>(bmp_header_size + dib_header_size + palette_size)?; // image data offset
131+
self.writer.write_u32::<LittleEndian>(image_data_offset)?; // image data offset
109132

110133
// write DIB header
111134
self.writer.write_u32::<LittleEndian>(dib_header_size)?;
@@ -141,13 +164,13 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
141164

142165
// write image data
143166
match color_type {
144-
ExtendedColorType::Rgb8 => self.encode_rgb(image, width, height, row_pad_size, 3)?,
145-
ExtendedColorType::Rgba8 => self.encode_rgba(image, width, height, row_pad_size, 4)?,
167+
ExtendedColorType::Rgb8 => self.encode_rgb(image, width, height, row_padding, 3)?,
168+
ExtendedColorType::Rgba8 => self.encode_rgba(image, width, height, row_padding, 4)?,
146169
ExtendedColorType::L8 => {
147-
self.encode_gray(image, width, height, row_pad_size, 1, palette)?;
170+
self.encode_gray(image, width, height, row_padding, 1, palette)?;
148171
}
149172
ExtendedColorType::La8 => {
150-
self.encode_gray(image, width, height, row_pad_size, 2, palette)?;
173+
self.encode_gray(image, width, height, row_padding, 2, palette)?;
151174
}
152175
_ => {
153176
return Err(ImageError::Unsupported(
@@ -167,7 +190,7 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
167190
image: &[u8],
168191
width: u32,
169192
height: u32,
170-
row_pad_size: u32,
193+
row_padding: u32,
171194
bytes_per_pixel: u32,
172195
) -> io::Result<()> {
173196
let width = width as usize;
@@ -184,7 +207,7 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
184207
// written as BGR
185208
self.writer.write_all(&[b, g, r])?;
186209
}
187-
self.write_row_pad(row_pad_size)?;
210+
self.write_row_pad(row_padding)?;
188211
}
189212

190213
Ok(())
@@ -195,7 +218,7 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
195218
image: &[u8],
196219
width: u32,
197220
height: u32,
198-
row_pad_size: u32,
221+
row_padding: u32,
199222
bytes_per_pixel: u32,
200223
) -> io::Result<()> {
201224
let width = width as usize;
@@ -213,7 +236,7 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
213236
// written as BGRA
214237
self.writer.write_all(&[b, g, r, a])?;
215238
}
216-
self.write_row_pad(row_pad_size)?;
239+
self.write_row_pad(row_padding)?;
217240
}
218241

219242
Ok(())
@@ -224,7 +247,7 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
224247
image: &[u8],
225248
width: u32,
226249
height: u32,
227-
row_pad_size: u32,
250+
row_padding: u32,
228251
bytes_per_pixel: u32,
229252
palette: Option<&[[u8; 3]]>,
230253
) -> io::Result<()> {
@@ -261,7 +284,7 @@ impl<'a, W: Write + 'a> BmpEncoder<'a, W> {
261284
}
262285
}
263286

264-
self.write_row_pad(row_pad_size)?;
287+
self.write_row_pad(row_padding)?;
265288
}
266289

267290
Ok(())
@@ -297,37 +320,42 @@ impl<W: Write> ImageEncoder for BmpEncoder<'_, W> {
297320
}
298321
}
299322

300-
fn get_unsupported_error_message(c: ExtendedColorType) -> String {
301-
format!("Unsupported color type {c:?}. Supported types: RGB(8), RGBA(8), Gray(8), GrayA(8).")
302-
}
303-
304323
/// Returns a tuple representing: (dib header size, written pixel size, palette color count).
305-
fn get_pixel_info(
324+
fn written_pixel_info(
306325
c: ExtendedColorType,
307326
palette: Option<&[[u8; 3]]>,
308-
) -> io::Result<(u32, u32, u32)> {
309-
let sizes = match c {
310-
ExtendedColorType::Rgb8 => (BITMAPINFOHEADER_SIZE, 3, 0),
311-
ExtendedColorType::Rgba8 => (BITMAPV4HEADER_SIZE, 4, 0),
327+
) -> Result<(u32, u32, u32), ImageError> {
328+
let (header, color_bytes, palette_count) = match c {
329+
ExtendedColorType::Rgb8 => (BITMAPINFOHEADER_SIZE, 3, Some(0)),
330+
ExtendedColorType::Rgba8 => (BITMAPV4HEADER_SIZE, 4, Some(0)),
312331
ExtendedColorType::L8 => (
313332
BITMAPINFOHEADER_SIZE,
314333
1,
315-
palette.map(|p| p.len()).unwrap_or(256) as u32,
334+
u32::try_from(palette.map(|p| p.len()).unwrap_or(256)).ok(),
316335
),
317336
ExtendedColorType::La8 => (
318337
BITMAPINFOHEADER_SIZE,
319338
1,
320-
palette.map(|p| p.len()).unwrap_or(256) as u32,
339+
u32::try_from(palette.map(|p| p.len()).unwrap_or(256)).ok(),
321340
),
322341
_ => {
323-
return Err(io::Error::new(
324-
io::ErrorKind::InvalidInput,
325-
&get_unsupported_error_message(c)[..],
326-
))
342+
return Err(ImageError::Unsupported(
343+
UnsupportedError::from_format_and_kind(
344+
ImageFormat::Bmp.into(),
345+
UnsupportedErrorKind::Color(c),
346+
),
347+
));
327348
}
328349
};
329350

330-
Ok(sizes)
351+
let palette_count = palette_count.ok_or_else(|| {
352+
ImageError::Encoding(EncodingError::new(
353+
ImageFormatHint::Exact(ImageFormat::Bmp),
354+
"calculated palette size larger than 2^32",
355+
))
356+
})?;
357+
358+
Ok((header, color_bytes, palette_count))
331359
}
332360

333361
#[cfg(test)]
@@ -421,4 +449,13 @@ mod tests {
421449
assert_eq!(2, decoded[7]);
422450
assert_eq!(2, decoded[8]);
423451
}
452+
453+
#[test]
454+
fn regression_issue_2604() {
455+
let mut image = vec![];
456+
let mut encoder = BmpEncoder::new(&mut image);
457+
encoder
458+
.encode(&[], 1 << 31, 0, ExtendedColorType::Rgb8)
459+
.unwrap_err();
460+
}
424461
}

src/codecs/dxt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ fn alpha_table_dxt5(alpha0: u8, alpha1: u8) -> [u8; 8] {
196196

197197
/// decodes an 8-byte dxt color block into the RGB channels of a 16xRGB or 16xRGBA block.
198198
/// source should have a length of 8, dest a length of 48 (RGB) or 64 (RGBA)
199+
#[allow(clippy::needless_range_loop)] // False positive, the 0..3 loop is not an enumerate
199200
fn decode_dxt_colors(source: &[u8], dest: &mut [u8], is_dxt1: bool) {
200201
// sanity checks, also enable the compiler to elide all following bound checks
201202
assert!(source.len() == 8 && (dest.len() == 48 || dest.len() == 64));

0 commit comments

Comments
 (0)