Bug Description
Seven defects on the read path — reached through from_bytes, render_page and the tagged-PDF structure tree.
Five are crashes. A malformed shading function, a negative image-mask dimension or a zero-width /Mask panics inside render_page, on a file the crate opened without complaint. A caller cannot defend against that with a Result.
Two are nondeterminism. Neither errors, neither logs, and both return a plausible answer — just not the same one twice. A damaged file recovers a different page tree between reconstructions, and an annotation without a normal appearance renders one of two images depending on hash order.
Both nondeterminism cases vary within one process, not only between runs, because Rust seeds each HashMap instance separately.
Summary
| # |
Location |
Trigger |
Today |
Should be |
| 1 |
rendering/mesh_shading.rs:965 |
shading function FunctionType 3 with /Domain [] |
panic: index out of bounds |
skip the shading |
| 2 |
rendering/mesh_shading.rs:1089 |
FunctionType 0 whose /Size exceeds its sample stream |
panic: multiply with overflow |
reject the function |
| 3 |
rendering/separation_renderer.rs:3062 |
/ImageMask with /Width -1 /Height -1 |
panic: multiply with overflow |
reject the dimensions |
| 4 |
rendering/page_renderer.rs:4178 |
image whose /Mask sub-image has /Width 0 |
panic: subtract with overflow |
skip the mask, paint the image |
| 5 |
rendering/page_renderer.rs:7750 |
annotation /AP with /D or /R but no /N |
identical bytes render two different images |
draw nothing (§12.5.5) |
| 6 |
xref_reconstruction.rs:545 |
damaged file with two /Type /Pages inside object streams |
different page tree run to run |
same bytes, same page tree |
| 7 |
structure/parser.rs:538 |
/K 4294967301 (2³²+5) |
wraps to MCID 5, aliasing the real one |
skip the out-of-range id |
Rows 1–4 panic in any build — the indexing and arithmetic here are checked regardless of profile.
Steps to Reproduce
Rows 1–4. Build each malformed construct into a one-page PDF and call render_page. Every file parses cleanly; the failure is at paint time.
Row 5. Render the same bytes 64 times in one process and count distinct outputs. You get 2. With /N present you get 1.
Row 6. Reconstruct a damaged file containing two /Type /Pages objects packed into object streams, 64 times in one process, recording which tree is chosen.
Row 7. Give a marked-content reference /K 4294967301 alongside a genuine MCID 5 on the same page, then read back the page's marked-content ids.
Observed on v0.3.77 — verbatim output
Rows 1–5, cargo test --features rendering --test malformed_render_input:
test stitching_function_with_empty_domain_renders ... FAILED
test sampled_function_size_larger_than_stream_renders ... FAILED
test zero_width_mask_sub_image_renders ... FAILED
test separation_image_mask_negative_dimensions_render ... FAILED
test annotation_appearance_without_normal_entry_draws_nothing ... FAILED
test annotation_appearance_with_normal_entry_is_deterministic ... ok
panicked at src/rendering/mesh_shading.rs:965:32
panicked at src/rendering/mesh_shading.rs:1089:13
panicked at src/rendering/page_renderer.rs:4178:68
panicked at src/rendering/separation_renderer.rs:3062:23
annotation_appearance_without_normal_entry_draws_nothing:
identical bytes produced 2 distinct renders left: 2 right: 1
test result: FAILED. 2 passed; 5 failed
The two passes are controls: the /N-present annotation is already deterministic, and a stencil-mask variant I could not drive to the faulty branch.
Row 6, 64 reconstructions of the same bytes in one process:
test objstm_recovery_picks_the_same_page_tree_every_run ... FAILED
recovered page tree varies between runs:
[Some(3), Some(4), Some(3), Some(3), Some(3), Some(4), Some(3), Some(3), Some(4),
Some(4), Some(3), Some(3), Some(3), Some(4), Some(4), Some(3), Some(3), Some(4),
Some(4), Some(3), Some(3), Some(3), Some(3), Some(3), Some(4), Some(4), Some(3),
... ]
Row 7, cargo test --test structure_mcid_range:
test k_mcid_above_u32_max_is_skipped_not_wrapped ... FAILED
left: [5, 5] right: [5]
test negative_k_mcid_is_skipped_not_wrapped ... FAILED
left: [4294967295, 5] right: [5]
test out_of_range_mcr_mcid_is_skipped_not_wrapped ... FAILED
left: [5, 5] right: [5]
test in_range_k_mcids_are_unaffected ... ok
test result: FAILED. 1 passed; 4 failed
[5, 5] is the aliasing — the out-of-range id has landed on the page's real MCID 5, so two references now point at one id.
Expected Behavior
Malformed values should not panic. Rows 1–4 all read a value straight out of the file and use it in arithmetic with no range check. An error or a skipped construct is the right outcome.
Row 5 needs the fallback removed, not ordered. ISO 32000-1 §12.5.5 makes /N the normal appearance; /D and /R show only while the pointer presses or hovers. Drawing one of those on a static page is wrong in every iteration order, so sorting the map would just freeze a specific wrong answer.
Rows 6 and 7 should be deterministic. The same bytes should recover the same page tree, and an out-of-range marked-content id should be skipped rather than truncated onto a valid one.
Actual Behavior
Rows 1–4 panic inside render_page on a file that opened without error.
Row 5 returns one of two images depending on hash order, with no way for a caller to know which.
Row 6 returns a different page tree between reconstructions of the same bytes, so a damaged file can extract different content on consecutive runs.
Row 7 truncates the id onto a real one, leaving two marked-content references pointing at the same id.
Environment
- pdf_oxide version: 0.3.77
- OS: Linux x86_64 (Ubuntu 20.04)
- Rust version: stable (rustc 1.97.1)
Checklist
AI disclosure: drafted with AI assistance and reviewed before filing. Every Observed block is real console output captured against v0.3.77, not reconstructed. Leaving the box above unticked rather than claiming otherwise.
Bug Description
Seven defects on the read path — reached through
from_bytes,render_pageand the tagged-PDF structure tree.Five are crashes. A malformed shading function, a negative image-mask dimension or a zero-width
/Maskpanics insiderender_page, on a file the crate opened without complaint. A caller cannot defend against that with aResult.Two are nondeterminism. Neither errors, neither logs, and both return a plausible answer — just not the same one twice. A damaged file recovers a different page tree between reconstructions, and an annotation without a normal appearance renders one of two images depending on hash order.
Both nondeterminism cases vary within one process, not only between runs, because Rust seeds each
HashMapinstance separately.Summary
rendering/mesh_shading.rs:965FunctionType 3with/Domain []rendering/mesh_shading.rs:1089FunctionType 0whose/Sizeexceeds its sample streamrendering/separation_renderer.rs:3062/ImageMaskwith/Width -1 /Height -1rendering/page_renderer.rs:4178/Masksub-image has/Width 0rendering/page_renderer.rs:7750/APwith/Dor/Rbut no/Nxref_reconstruction.rs:545/Type /Pagesinside object streamsstructure/parser.rs:538/K 4294967301(2³²+5)Rows 1–4 panic in any build — the indexing and arithmetic here are checked regardless of profile.
Steps to Reproduce
Rows 1–4. Build each malformed construct into a one-page PDF and call
render_page. Every file parses cleanly; the failure is at paint time.Row 5. Render the same bytes 64 times in one process and count distinct outputs. You get 2. With
/Npresent you get 1.Row 6. Reconstruct a damaged file containing two
/Type /Pagesobjects packed into object streams, 64 times in one process, recording which tree is chosen.Row 7. Give a marked-content reference
/K 4294967301alongside a genuine MCID 5 on the same page, then read back the page's marked-content ids.Observed on v0.3.77 — verbatim output
Rows 1–5,
cargo test --features rendering --test malformed_render_input:The two passes are controls: the
/N-present annotation is already deterministic, and a stencil-mask variant I could not drive to the faulty branch.Row 6, 64 reconstructions of the same bytes in one process:
Row 7,
cargo test --test structure_mcid_range:[5, 5]is the aliasing — the out-of-range id has landed on the page's real MCID 5, so two references now point at one id.Expected Behavior
Malformed values should not panic. Rows 1–4 all read a value straight out of the file and use it in arithmetic with no range check. An error or a skipped construct is the right outcome.
Row 5 needs the fallback removed, not ordered. ISO 32000-1 §12.5.5 makes
/Nthe normal appearance;/Dand/Rshow only while the pointer presses or hovers. Drawing one of those on a static page is wrong in every iteration order, so sorting the map would just freeze a specific wrong answer.Rows 6 and 7 should be deterministic. The same bytes should recover the same page tree, and an out-of-range marked-content id should be skipped rather than truncated onto a valid one.
Actual Behavior
Rows 1–4 panic inside
render_pageon a file that opened without error.Row 5 returns one of two images depending on hash order, with no way for a caller to know which.
Row 6 returns a different page tree between reconstructions of the same bytes, so a damaged file can extract different content on consecutive runs.
Row 7 truncates the id onto a real one, leaving two marked-content references pointing at the same id.
Environment
Checklist
AI disclosure: drafted with AI assistance and reviewed before filing. Every Observed block is real console output captured against v0.3.77, not reconstructed. Leaving the box above unticked rather than claiming otherwise.