Skip to content

Do not underflow on an empty /W sub-array - #294

Open
youdie006 wants to merge 1 commit into
pdf-rs:masterfrom
youdie006:w-array-empty-underflow
Open

youdie006 wants to merge 1 commit into
pdf-rs:masterfrom
youdie006:w-array-empty-underflow

Conversation

@youdie006

Copy link
Copy Markdown

Font::widths builds the CID width table from the /W array (pdf/src/font.rs:350, and again at :357 for the indirect-reference arm):

let c1 = p.as_usize()?;
if! c1 == 0 {                                    // rejects c1 == usize::MAX
    return Err(PdfError::CidDecode);
}
match iter.next() {
    Some(Primitive::Array(array)) => {
        widths.ensure_cid(c1 + array.len() - 1);

Both c1 and array come straight out of the CIDFont's /W entry, so a document with an empty sub-array -- /W [0 []] -- makes array.len() - 1 wrap.

debug:   thread 'w_array_empty_underflow' panicked at pdf/src/font.rs:350:47:
         attempt to subtract with overflow
release: thread 'w_array_empty_underflow' panicked at library/alloc/src/raw_vec/mod.rs:28:5:
         capacity overflow

In release the double wrap lands on usize::MAX, ensure_cid's checked_sub(0) succeeds, and values.reserve(usize::MAX) aborts. To be precise about the release case: it needs c1 == 0. For c1 >= 1 the two wraps cancel to c1 - 1 and ensure_cid is a no-op, so release is benign there. Debug panics for any c1.

This is a hole in a guard you already wrote

9d83215 ("sanity checks") added the c1 == usize::MAX check five lines above, in the same loop -- so the sum cannot wrap from the top. Nothing covers len() - 1 wrapping from the bottom.

And the callee already does it correctly: Widths::ensure_cid (font.rs:234) uses cid.checked_sub(self.first_char), and Widths::get (font.rs:218) guards cid < self.first_char before subtracting. Only the two call sites compute the argument unchecked. The fix mirrors ensure_cid's own checked_sub.

Verification

cargo test --workspace --no-fail-fast -- 0 failed, matching the pre-existing count. CI (.github/workflows/test.yml) runs exactly cargo test --workspace; there is no fmt or clippy gate. I ran both profiles, since this is an overflow: debug panics on the subtraction, release on the allocation.

Two tests added, one per call site. Mutation-checked separately, anchored to each match arm:

mutation result
revert site A only w_array_empty_underflow fails at font.rs:350, the reference test still passes
revert site B only w_array_empty_underflow_via_reference fails, the direct test still passes

Each mutant is killed by exactly one test, and the reported panic line confirms the mutation landed where I intended.

Behaviour change

A PDF with an empty /W sub-array now returns Ok instead of panicking. No existing test row changes -- the suite has no occurrence of ensure_cid, usize::MAX, or any /W boundary value.

What I did not change

  • The if! c1 == 0 guard itself. It parses as !c1 == 0, i.e. c1 == usize::MAX, which is what it is meant to check -- rewriting it for readability would blur this fix.
  • c1 + i inside the loop and the c1..=c2 arm. Primitive::as_usize only accepts a non-negative Primitive::Integer(i32), so c1 <= i32::MAX and those cannot wrap on a 64-bit target; I had no repro for them.
  • SampledFunction::apply, which is issue Panic (multiply overflow) in SampledFunction::apply #289 and still open, and font.rs:278's self.values[cid - self.first_char], which is already dominated by the cid < self.first_char early return above it.

I noticed #284 (ObjectStream::get_object_slice overflow) and #286 (PsFunc::exec_inner underflow) are both closed and fixed, which is why I sent this as a plain PR rather than asking first.


Disclosure: AI-assisted. I found and prepared this with an AI assistant, and I ran and verified everything above myself.

Font::widths computes c1 + array.len() - 1 for the CID width table. Both
operands come from the /W array in the PDF, so an empty sub-array makes
len() - 1 wrap.

9d83215 added the c1 == usize::MAX guard five lines above, which stops
the sum wrapping from the top; nothing stops len() - 1 wrapping from the
bottom. In debug /W [0 []] panics with 'attempt to subtract with
overflow'; in release the double wrap reaches ensure_cid as usize::MAX
and reserve aborts with 'capacity overflow'.

Widths::ensure_cid and Widths::get already use checked_sub for the same
reason; do the same at the two call sites.
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.

1 participant