Skip to content

Commit 62b4706

Browse files
Shnatseljaredforth
andauthored
Encoder soundness fix (#44)
* Add bytes_per_pixel function on PixelLayout * Add a private CheckedEncoder function that's not constructable from outside of a private module * Add test validating the presence of buffer overflow checks * Replace the fields of Encoder with a CheckedEncoder * Add comment * Turn a comment into an actionable TODO * Clearly document another TODO * cargo fmt * update tests * Fix invalid input parameters in a test --------- Co-authored-by: Jared Forth <[email protected]>
1 parent d523893 commit 62b4706

File tree

2 files changed

+122
-29
lines changed

2 files changed

+122
-29
lines changed

src/encoder.rs

Lines changed: 115 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,19 @@ use image::DynamicImage;
33
use libwebp_sys::*;
44

55
use crate::shared::*;
6+
use internal::CheckedEncoder;
67

78
/// An encoder for WebP images. It uses the default configuration of libwebp.
89
pub struct Encoder<'a> {
9-
image: &'a [u8],
10-
layout: PixelLayout,
11-
width: u32,
12-
height: u32,
10+
e: CheckedEncoder<'a>,
1311
}
1412

1513
impl<'a> Encoder<'a> {
1614
/// Creates a new encoder from the given image data.
1715
/// The image data must be in the pixel layout of the color parameter.
1816
pub fn new(image: &'a [u8], layout: PixelLayout, width: u32, height: u32) -> Self {
1917
Self {
20-
image,
21-
layout,
22-
width,
23-
height,
18+
e: CheckedEncoder::new(image, layout, width, height),
2419
}
2520
}
2621

@@ -47,20 +42,14 @@ impl<'a> Encoder<'a> {
4742
/// Creates a new encoder from the given image data in the RGB pixel layout.
4843
pub fn from_rgb(image: &'a [u8], width: u32, height: u32) -> Self {
4944
Self {
50-
image,
51-
layout: PixelLayout::Rgb,
52-
width,
53-
height,
45+
e: CheckedEncoder::new(image, PixelLayout::Rgb, width, height),
5446
}
5547
}
5648

5749
/// Creates a new encoder from the given image data in the RGBA pixel layout.
5850
pub fn from_rgba(image: &'a [u8], width: u32, height: u32) -> Self {
5951
Self {
60-
image,
61-
layout: PixelLayout::Rgba,
62-
width,
63-
height,
52+
e: CheckedEncoder::new(image, PixelLayout::Rgba, width, height),
6453
}
6554
}
6655

@@ -89,8 +78,99 @@ impl<'a> Encoder<'a> {
8978

9079
pub fn encode_advanced(&self, config: &WebPConfig) -> Result<WebPMemory, WebPEncodingError> {
9180
unsafe {
92-
let mut picture = new_picture(self.image, self.layout, self.width, self.height);
93-
encode(&mut picture, config)
81+
let mut picture = new_picture(
82+
self.e.image(),
83+
self.e.layout(),
84+
self.e.width(),
85+
self.e.height(),
86+
);
87+
let res = encode(&mut *picture, config);
88+
res
89+
}
90+
}
91+
}
92+
93+
/// This module contains the private CheckedEncoder so that it cannot be constructed directly from the outside.
94+
/// That ensures that the only way to construct one validates the supplied parameters.
95+
mod internal {
96+
use std::convert::TryInto;
97+
98+
use crate::shared::PixelLayout;
99+
100+
/// Validated encoder parameters, guaranteeing `image.len() >= width * height * bytes_per_pixel`.
101+
/// Required for memory safety. Their absence would allow out-of-bounds reads.
102+
pub(super) struct CheckedEncoder<'a> {
103+
image: &'a [u8],
104+
layout: PixelLayout,
105+
width: u32,
106+
height: u32,
107+
}
108+
109+
impl<'a> CheckedEncoder<'a> {
110+
/// Creates a new instance of `CheckedEncoder`, and performs the necessary bounds checks.
111+
///
112+
/// This is the only way to create a `CheckedEncoder` exposed outside the `internal` module.
113+
pub(super) fn new(image: &'a [u8], layout: PixelLayout, width: u32, height: u32) -> Self {
114+
// TODO: return an error instead of panicking in the next semver-breaking release
115+
116+
if width == 0 || height == 0 {
117+
// TODO: enable this check once this function returns a Result.
118+
// As of v0.3.0 it's possible to contruct an Encoder with width or height 0,
119+
// but encoding will fail and the error is really uninformative:
120+
// "called `Result::unwrap()` on an `Err` value: VP8_ENC_ERROR_BAD_DIMENSION"
121+
122+
//panic!("Width and height must be non-zero.");
123+
}
124+
125+
// We're going to compare the incoming width * height * bpp against the length of a slice.
126+
// The length of a slice is a `usize`, so we are going to do arithmetic in `usize` as well.
127+
//
128+
// On 32-bit and 64-bit platforms these conversions always suceeed and get optimized out.
129+
let width_u: usize = width.try_into().unwrap();
130+
let height_u: usize = height.try_into().unwrap();
131+
let bytes_per_pixel_u: usize = layout.bytes_per_pixel().into();
132+
133+
// If we simply calculate `width * height * bytes_per_pixel`, arithmetic may overflow in release mode
134+
// and make it possible to bypass the length check. So we need either .checked_mul or .saturating_mul here.
135+
//
136+
// Using `saturating_mul` is enough because it's not possible to construct a valid slice of size `usize::MAX` anyway,
137+
// and it makes for simpler code and a nicer error message.
138+
let expected_len = width_u
139+
.saturating_mul(height_u)
140+
.saturating_mul(bytes_per_pixel_u);
141+
if image.len() < expected_len {
142+
panic!(
143+
"Image buffer too small. Expected at least {} bytes for a {}x{} image with {:?} layout, got {}.",
144+
expected_len, width, height, layout, image.len()
145+
);
146+
}
147+
148+
if image.len() > expected_len {
149+
// TODO: reject this in the future, once this function is made to return Result
150+
}
151+
152+
CheckedEncoder {
153+
image,
154+
layout,
155+
width,
156+
height,
157+
}
158+
}
159+
160+
pub(super) fn width(&self) -> u32 {
161+
self.width
162+
}
163+
164+
pub(super) fn height(&self) -> u32 {
165+
self.height
166+
}
167+
168+
pub(super) fn layout(&self) -> PixelLayout {
169+
self.layout
170+
}
171+
172+
pub(super) fn image(&self) -> &'a [u8] {
173+
self.image
94174
}
95175
}
96176
}
@@ -141,14 +221,20 @@ mod tests {
141221
use super::*;
142222
use crate::shared;
143223

224+
#[test]
225+
#[should_panic]
226+
fn construct_encoder_with_buffer_overflow() {
227+
Encoder::from_rgb(&[], 16383, 16383);
228+
}
229+
144230
#[test]
145231
fn test_encoder_new_assigns_fields() {
146-
let data = [1, 2, 3, 4, 5, 6];
232+
let data = [5; 18];
147233
let enc = Encoder::new(&data, shared::PixelLayout::Rgb, 2, 3);
148-
assert_eq!(enc.image, &data);
149-
assert_eq!(enc.layout, shared::PixelLayout::Rgb);
150-
assert_eq!(enc.width, 2);
151-
assert_eq!(enc.height, 3);
234+
assert_eq!(enc.e.image(), &data);
235+
assert_eq!(enc.e.layout(), shared::PixelLayout::Rgb);
236+
assert_eq!(enc.e.width(), 2);
237+
assert_eq!(enc.e.height(), 3);
152238
}
153239

154240
#[test]
@@ -157,12 +243,12 @@ mod tests {
157243
let rgba = [1, 2, 3, 4, 5, 6, 7, 8];
158244
let enc_rgb = Encoder::from_rgb(&rgb, 2, 1);
159245
let enc_rgba = Encoder::from_rgba(&rgba, 2, 1);
160-
assert_eq!(enc_rgb.layout, shared::PixelLayout::Rgb);
161-
assert_eq!(enc_rgba.layout, shared::PixelLayout::Rgba);
162-
assert_eq!(enc_rgb.image, &rgb);
163-
assert_eq!(enc_rgba.image, &rgba);
164-
assert_eq!(enc_rgb.width, 2);
165-
assert_eq!(enc_rgba.height, 1);
246+
assert_eq!(enc_rgb.e.layout(), shared::PixelLayout::Rgb);
247+
assert_eq!(enc_rgba.e.layout(), shared::PixelLayout::Rgba);
248+
assert_eq!(enc_rgb.e.image(), &rgb);
249+
assert_eq!(enc_rgba.e.image(), &rgba);
250+
assert_eq!(enc_rgb.e.width(), 2);
251+
assert_eq!(enc_rgba.e.height(), 1);
166252
}
167253

168254
#[cfg(feature = "img")]

src/shared.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ impl PixelLayout {
139139
pub fn is_alpha(self) -> bool {
140140
self == PixelLayout::Rgba
141141
}
142+
143+
pub fn bytes_per_pixel(self) -> u8 {
144+
match self {
145+
PixelLayout::Rgb => 3,
146+
PixelLayout::Rgba => 4,
147+
}
148+
}
142149
}
143150

144151
#[cfg(test)]

0 commit comments

Comments
 (0)