1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
use crate::object::Dict;
use crate::object::dict::keys::{
BLACK_IS_1, COLUMNS, ENCODED_BYTE_ALIGN, END_OF_BLOCK, END_OF_LINE, K, ROWS,
};
use crate::object::stream::{FilterResult, ImageColorSpace, ImageData, ImageDecodeParams};
use alloc::vec::Vec;
use core::iter;
use hayro_ccitt::{DecodeSettings, Decoder, EncodingMode};
pub(crate) fn decode(
data: &[u8],
params: Dict<'_>,
image_params: &ImageDecodeParams,
) -> Option<FilterResult> {
let k = params.get::<i32>(K).unwrap_or(0);
// How many rows to decode. Three sources disagree and all three come from
// the file, so the rule is the maximum of what /Rows asks for and what the
// image dictionary declares:
//
// * /Rows 0 means "derive it from the end-of-block marker or the image
// height". Treated as absent; without this the Group4 decoder exits
// immediately, because decoded_rows == rows == 0.
// * /Rows *below* the declared height means the file is malformed, and
// upstream decodes to the height anyway rather than truncating the image
// -- Chromium does the same. Ported from LaurenzV/hayro#1339; before it,
// a page whose /Rows undercounted rendered as a sliver.
//
// The upper bound is safe: image_params.height is what the pixel-limit
// check in Stream::decoded_image was applied to, and the count actually
// produced is checked against that limit again below.
let mut rows = params
.get::<u32>(ROWS)
.unwrap_or(0)
.max(image_params.height);
let columns = params.get::<usize>(COLUMNS).unwrap_or(1728) as u32;
// Clamp BEFORE decoding, not after. The check further down rejected an
// over-large image only once hayro_ccitt::decode had already grown the
// output to roughly columns * rows / 8 bytes -- so the limit was enforced
// after the memory it exists to bound had been allocated. (Codex, #1609.)
//
// The limit comes from the stream's context via ImageDecodeParams, because
// `params` here is /DecodeParms and is Dict::default() when the stream has
// none, whose context carries no limits.
if let Some(limit) = image_params.pixel_limit
&& columns > 0
{
let max_rows = limit / columns;
if rows > max_rows {
log::warn!(
"CCITT asks for {rows} rows of {columns} columns, over the pixel limit \
{limit}; decoding at most {max_rows}"
);
rows = max_rows;
}
}
let end_of_block = params.get::<bool>(END_OF_BLOCK).unwrap_or(true);
// Refuse a /Columns x /Rows product that does not fit rather than decoding
// into a buffer sized by something else. Ported from hayro upstream
// (LaurenzV/hayro#1269).
//
// Not redundant with the pixel-limit clamp above: that only runs when a
// limit is configured, and this is about `usize` overflowing on a 32-bit
// target -- which wasm32 is, and we ship there.
//
// `columns` is NOT re-bound here. This commit and the one already on master
// each introduced their own `let columns`, and Rust shadows rather than
// complains, so the rebase produced two bindings of the same value with
// nothing pointing at it.
(columns as usize).checked_mul(rows as usize)?;
let settings = DecodeSettings {
columns,
rows,
end_of_block,
end_of_line: params.get::<bool>(END_OF_LINE).unwrap_or(false),
rows_are_byte_aligned: params.get::<bool>(ENCODED_BYTE_ALIGN).unwrap_or(false),
encoding: if k < 0 {
EncodingMode::Group4
} else if k == 0 {
EncodingMode::Group3_1D
} else {
EncodingMode::Group3_2D { k: k as u32 }
},
invert_black: params.get::<bool>(BLACK_IS_1).unwrap_or(false),
};
struct ByteDecoder {
output: Vec<u8>,
decoded_rows: u32,
buffer: u8,
bit_count: u8,
}
impl ByteDecoder {
fn push_bit(&mut self, white: bool) {
let bit = if white { 1 } else { 0 };
self.buffer = (self.buffer << 1) | bit;
self.bit_count += 1;
if self.bit_count == 8 {
self.output.push(self.buffer);
self.buffer = 0;
self.bit_count = 0;
}
}
fn flush(&mut self) {
if self.bit_count > 0 {
let padded = self.buffer << (8 - self.bit_count);
self.output.push(padded);
self.buffer = 0;
self.bit_count = 0;
}
}
}
impl Decoder for ByteDecoder {
// Upstream replaced push_pixel/push_pixel_chunk with a single
// push_pixels, which hands the whole run to the implementor instead of
// deciding the chunking inside the decoder. The byte-wise fast path is
// the same one we had; it simply lives on this side of the trait now,
// where it can see the output buffer's bit alignment.
fn push_pixels(&mut self, white: bool, count: u32) {
let (prefix, whole_bytes, tail) =
hayro_ccitt::split_run(u32::from(self.bit_count), count);
for _ in 0..prefix {
self.push_bit(white);
}
if whole_bytes > 0 {
let byte = if white { 0xFF } else { 0x00 };
self.output
.extend(iter::repeat_n(byte, whole_bytes as usize));
}
for _ in 0..tail {
self.push_bit(white);
}
}
fn next_line(&mut self) {
self.decoded_rows += 1;
// Flush any remaining bits and align to byte boundary.
self.flush();
}
}
let mut decoder = ByteDecoder {
output: Vec::new(),
decoded_rows: 0,
buffer: 0,
bit_count: 0,
};
// Upstream 0.3.0 takes a reusable DecoderContext instead of settings by
// reference: the context is built once and reset per call rather than
// allocated on every call. One decode here, so it is built inline.
let mut ctx = hayro_ccitt::DecoderContext::new(settings);
let result = hayro_ccitt::decode(data, &mut decoder, &mut ctx);
// If we decoded at least one row, let's be lenient and return what we got.
// See also 0001763.pdf.
if result.is_err() && decoder.decoded_rows == 0 {
return None;
}
if result.is_err() {
crate::leniency::emit(crate::leniency::CCITT_PARTIAL_DECODE);
}
// The pixel limit in Stream::decoded_image was checked against the DECLARED
// /Height, before any of this ran. Reporting decoded_rows -- which is the
// honest count, and the point of the fix above -- lets a file declare a
// small /Height to get past that check and then hand a much larger row count
// to get_components, which allocates at least a u16 per pixel. The fix for
// one hole opened another. (Codex, #1609.)
//
// So the limit is applied again, to what was actually produced.
// Belt and braces: the clamp above bounds what the decoder is asked for, and
// this bounds what it produced. Both are cheap and they fail differently --
// the clamp truncates a hostile image, this refuses one that somehow grew
// past the cap anyway.
if let Some(limit) = image_params.pixel_limit {
let pixels = u64::from(settings.columns).saturating_mul(u64::from(decoder.decoded_rows));
if pixels > u64::from(limit) {
log::warn!(
"CCITT decoded {} rows of {} columns = {pixels} pixels, over the limit {limit}",
decoder.decoded_rows,
settings.columns
);
return None;
}
}
Some(FilterResult {
data: decoder.output,
image_data: Some(ImageData {
alpha: None,
color_space: Some(ImageColorSpace::Gray),
bits_per_component: 1,
width: settings.columns,
// The rows actually decoded -- not `image_params.height`, and not
// `rows` either.
//
// Upstream (LaurenzV/hayro#1269) moved from /Height to /Rows, which
// fixes the common case. It is still an upper bound: a stream that
// ends early, or carries an end-of-block before /Rows, decodes fewer.
// The leniency branch above exists precisely because that happens.
//
// The gap is reachable on purpose. A file declaring a small /Height
// (so the pixel-limit check passes) and a huge /Rows, carrying one
// encoded row, would have get_components allocate and zero-pad
// rows * width samples. decoded_rows is what the buffer holds.
// (Codex, #1609.)
height: decoder.decoded_rows,
}),
})
}
#[cfg(test)]
mod upstream_hardening_tests {
use super::*;
use crate::object::FromBytes;
/// One row of eight white pixels, Group 3 one-dimensional.
///
/// Taken from upstream's own regression fixture for LaurenzV/hayro#1258.
const ONE_ROW_G3: &[u8] = &[0x35, 0x14];
fn params_with(height: u32) -> ImageDecodeParams {
ImageDecodeParams {
height,
..Default::default()
}
}
/// The limit as a stream really carries it: on ImageDecodeParams, set by
/// Stream::decoded_image from the stream's own context.
///
/// The first version of these tests set it on the /DecodeParms dictionary
/// instead, which is where the CCITT decoder used to read it -- and that is
/// precisely the path a stream without /DecodeParms does not have. The test
/// passed while the check it tested was unreachable in production.
fn params_with_limit(height: u32, limit: u32) -> ImageDecodeParams {
ImageDecodeParams {
height,
pixel_limit: Some(limit),
..Default::default()
}
}
/// Ported from LaurenzV/hayro#1269.
///
/// `/Rows` and the image's `/Height` are two numbers from the same file and
/// nothing makes them agree. The decoder stops after `/Rows` rows, so the
/// buffer holds that many -- but the reported height was `/Height`, so a
/// consumer computing `height * stride` walked off the end of the data.
///
/// Upstream's other half of this fix -- sizing the output allocation with a
/// `checked_mul` -- does not apply here: this decoder grows its output as it
/// goes rather than preallocating `columns * height`, so there is no
/// allocation to size wrongly.
#[test]
fn the_reported_height_is_the_number_of_rows_decoded_not_the_declared_one() {
let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
// The image claims four rows; the CCITT parameters say one.
let decoded = decode(ONE_ROW_G3, params, ¶ms_with(4)).unwrap();
let image = decoded.image_data.unwrap();
assert_eq!(
image.height, 1,
"the height must describe the data returned"
);
// The invariant the height exists to support: a consumer reading
// height * stride bytes must not read past what was decoded.
let stride = (image.width as usize).div_ceil(8);
assert!(
decoded.data.len() >= stride * image.height as usize,
"reported {}x{} needs {} bytes, got {}",
image.width,
image.height,
stride * image.height as usize,
decoded.data.len()
);
}
/// The gap between "asked for" and "produced", which `/Rows` alone does not
/// close (Codex, #1609).
///
/// A small `/Height` passes the pixel-limit check upstream of here; a huge
/// `/Rows` then sets the reported height, while the data encodes one row.
/// A consumer sizing a buffer from the reported height allocates and
/// zero-pads a thousand rows for eight bytes of data.
#[test]
fn a_stream_that_stops_early_reports_what_it_produced_not_what_it_promised() {
let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1000 >>").unwrap();
let decoded = decode(ONE_ROW_G3, params, ¶ms_with(1)).unwrap();
let image = decoded.image_data.unwrap();
assert_eq!(
image.height, 1,
"one row was encoded, whatever /Rows claims"
);
let stride = (image.width as usize).div_ceil(8);
assert!(
decoded.data.len() >= stride * image.height as usize,
"reported {}x{} needs {} bytes, got {}",
image.width,
image.height,
stride * image.height as usize,
decoded.data.len()
);
}
/// The usual case must keep working: with no `/Rows`, the image height is
/// still what the decoder is told to produce and still what it reports.
#[test]
fn without_rows_the_image_height_is_still_used() {
let params = Dict::from_bytes(b"<< /K 0 /Columns 8 >>").unwrap();
let decoded = decode(ONE_ROW_G3, params, ¶ms_with(1)).unwrap();
assert_eq!(decoded.image_data.unwrap().height, 1);
}
/// The hole the `decoded_rows` fix opened, and the second check that closes it.
///
/// `Stream::decoded_image` applies the pixel limit to the DECLARED
/// `/Height`, before any decoding. Reporting the decoded row count is the
/// honest answer, and it also means a file can declare a tiny `/Height` to
/// slip past that check and then hand a far larger count to
/// `get_components`, which allocates at least a `u16` per pixel.
/// (Codex, #1609.)
#[test]
fn decoded_rows_are_checked_against_the_pixel_limit_too() {
// Declared height 1, so the check before decoding is satisfied. Eight
// columns against a limit of 4 pixels: the row count is clamped to zero
// before the decoder allocates anything.
let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
let decoded = decode(ONE_ROW_G3, params, ¶ms_with_limit(1, 4));
assert!(
decoded.is_none() || decoded.unwrap().image_data.unwrap().height == 0,
"8 pixels must not be produced under a 4-pixel limit"
);
}
/// And the limit must not fire on an image that fits, or every CCITT image
/// in a document with a limit set would vanish.
#[test]
fn an_image_inside_the_pixel_limit_still_decodes() {
let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
let decoded = decode(ONE_ROW_G3, params, ¶ms_with_limit(1, 64)).unwrap();
assert_eq!(decoded.image_data.unwrap().height, 1);
}
/// Ported from LaurenzV/hayro#1339: `/Rows` below the declared height is a
/// malformed file, and the image is decoded to the height rather than
/// truncated. Chromium does the same.
///
/// Before the port this rendered as a single row out of four -- a sliver
/// where the page has a picture.
#[test]
fn rows_below_the_declared_height_decodes_the_whole_image_anyway() {
let data: Vec<u8> = ONE_ROW_G3.iter().copied().cycle().take(4).collect();
let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
let decoded = decode(&data, params, ¶ms_with(4)).unwrap();
assert_eq!(
decoded.image_data.unwrap().height,
4,
"/Rows 1 against a declared height of 4 must not truncate the image"
);
}
}