mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 20:18:06 -06:00
da9cefa33cf9d86a866dbaa675ed659bfaef4635
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9d6552291b |
Issue #1296: hayro-syntax applies SASLprep to AES-256 passwords (PdfCraft patch 16) (#1345)
Revisions 5-6 of the standard security handler key the file on the
password processed with SASLprep (ISO 32000-2 7.6.4.3.3, Algorithm 2.A
step a). pdfcraft-crypt writes and reads it so, but the vendored
hayro-syntax skipped the step ("TODO: Do the above.") and used the UTF-8
bytes as typed. A password that SASLprep changes (a decomposed "cafe" +
U+0301, a no-break space, fullwidth letters typed by an IME in full-width
mode) was wrong for the renderer: Protect with such an open password was
rolled back ("the password is incorrect"), and an AES-256 file with such a
password did not open in a session with it, although pdfcraft-cos
accepted it.
- hayro-syntax (vendored patch 16) tries an R5/R6 password in its SASLprep
form (as pdfcraft-crypt computes it: as given if SASLprep refuses it,
UTF-8, at most 127 bytes), then, when that differs, as given.
stringprep, already in the tree through pdfcraft-crypt, is an optional
dependency enabled by the std feature; without std nothing changes.
- Tests: hayro-syntax crypto::pdfcraft_password_tests (candidate list and
R5/R6 dictionaries built from prepared passwords) and two engine tests
(Protect and reopen, and pdfcraft-cos-encrypted files) for the three
cases, with precomposed "caf\u{E9}" as the control. The dictionary test
and both engine tests fail without the fix.
Fixes #1296.
|
||
|
|
dd046a25b2 |
M2: stream page content past the 256 MiB cap (item 1 of storytold/pdfcraft#623) (#797)
* M2: stream page content past the 256 MiB cap (item 1 of storytold/pdfcraft#623) Page content was decoded whole, and the decode stopped at MAX_DECODED_STREAM (256 MiB), so the last ~10 MB of the #623 sample was dropped without a warning. Top-level page content is now read a window at a time, so the decoded content is never all in memory: - the content tokenizer reads windows and holds back an instruction that runs past one; Reader records reads that touch the end of the data, including reads made through clones, so an instruction is only decided once it has been seen whole; - the Flate decoder is resumable and keeps its own 32 KiB history, so it pauses at any output size and back-references do not depend on what the caller has kept; - page content may decode to max(256 MiB, 64 x its compressed size), counting each distinct stream once; content that reaches that budget is cut and the page reports ContentTruncated, as it did not before; - forms, tiling patterns, Type 3 glyph procedures and generic streams keep their existing budgets and caps. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * M2: Avoid duplicate page content raw reads * M2: Document page content byte reuse --------- Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> |
||
|
|
9c2d9c02bf |
fix(crypt): R2–R4 passwords with non-ASCII characters open (PDFDocEncoding) (#905)
Revisions 2–4 of the standard security handler take the password in PDFDocEncoding (ISO 32000-2 §7.6.4.3.2, Algorithm 2). PdfCraft authenticates an encrypted file twice, and neither did that: - pdfcraft-crypt took Latin-1 and dropped every other character, so "ı" (0x9A) and "€" (0xA0) vanished and "ılık€" became "lk"; - the renderer (vendored hayro-syntax, "TODO: Convert to PDFDocEncoding") used the UTF-8 bytes, so any non-ASCII password failed, even "Çok güzel", which Latin-1 and PDFDocEncoding write alike; - the engine handed the renderer an owner-recovered user password as Latin-1 characters, which the renderer then read as UTF-8 again. Files that qpdf or Acrobat protected with such passwords could not be opened with the right password. - pdfcraft-crypt encodes R2–R4 passwords in PDFDocEncoding and, for a password with characters PDFDocEncoding lacks (ğ, ş, İ), also tries its UTF-8 bytes, which qpdf writes for those. The PDFDocEncoding table moves here from pdfcraft-cos (which depends on this crate) and cos uses it. - hayro-syntax (vendored patch 14) tries an R2–R4 password as given, then in PDFDocEncoding. - The engine passes a recovered user password to the renderer as UTF-8 when its bytes are UTF-8 and as PDFDocEncoding otherwise, so the renderer gets exactly those bytes back. Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
74be522c18 |
hayro-syntax: cap the nesting of direct arrays and dictionaries (#772)
Skipping a direct array or dictionary, and the lenient repair of a dictionary that has an object where a key belongs, recursed once per level with no bound, so deep nesting in a trailer entry or a content stream overflowed the stack. Count the depth on the Reader and fail beyond 128 levels like any other malformed object. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
d94da2038a |
M2: pages with /UserUnit are shown at their size in points, as in Acrobat (#722)
* M2: pages with /UserUnit are shown at their size in points, as in Acrobat /UserUnit gives the size of a page's user-space unit in points; PdfCraft ignored it everywhere except the measuring tools, so a /UserUnit 2 page was shown, extracted, printed and edited at half its size, and the measuring tools disagreed with the rest of the app. The value is now read as Acrobat reads it (the page's own entry, a number from 1 up, at most 75,000; anything else counts as 1) in two places: - vendored hayro-syntax Page (patch 13): render_dimensions and initial_transform give the page in points, so rendering, tiles, PageInfo and the text layer follow; a page without it keeps exactly the transform it had. - pdfcraft-model Page: user_unit(), and display_size/view_matrix in points, so print, header/footer, watermark, background, added content and measuring agree with the screen. The measure crate drops its own arithmetic. Added items record the /UserUnit they were written for; items without the record (written before) read back converted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: an added item's recorded /UserUnit is clamped like the page's An infinite or huge recorded /UserUnit made the scale factor 0 and collapsed the item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
72b3b6171a | hayro-syntax: decouple content tokenization from slices (#706) | ||
|
|
dbd0748177 |
Reduce large-PDF memory growth (#307) (#419)
* M14: bound large-PDF memory use (#307) Inspect large documents lazily, share parser/resource metadata, bound full-save and raster retention, and cache signature candidate discovery safely. Preserve upstream rendering deduplication, pixel alignment, signature previews, and organizer insertion behavior, with regression coverage for their interaction. Validated with 1,336 workspace tests, 193 parser tests, strict workspace Clippy, 30 WASM crate checks, 1x/2x GUI protocols, and same-PDF workflow comparisons. * cos: drop the writer cache-policy probe and its test-only hooks in production paths * cos: the temporary reader keeps parsed objects until a count or owned-bytes budget Since #552 a stream read from the file is a view into the input, so clearing the whole parse cache after every read saved little memory and cost most of the full-save slowdown (every object parsed twice, indirect /Length objects re-parsed per stream). Clear only past OBJECTS entries or STREAM_BYTES of data of the cache's own (decrypted streams, strings). * cos: the writers refuse a document opened with a stream limit (read-only inspection) * cos: a full save fails rather than writing null when an object reads differently the second time * render: parsing is not timed by the watchdog; timing tests with wide margins A slow first parse (a huge or damaged file) made the whole pool fail every page for its lifetime. Parsing, shared or private, is now its own untimed stage, as on main before the shared parser; rendering is timed from when parsing ends. The init-timeout tests become one deterministic test (a 2 s parse against a 10 ms limit, shared and private). Watchdog tests use limits and injected delays ~10x apart instead of 25-50 ms against real renders. Also: saturating retired-worker count, main's stop-flag test runs on private parsers (the first shared timeout is a private retry), set_queue documents that unlisted results are dropped, and retire() documents the permanent per-worker-parser fallback. * render: page numbers in the lazy page tree use checked conversion * render: the lazy inspector is compared with lopdf on named destinations and object streams too * ui: on-screen pages always render; pages turned back to stay cached; stable thumbnail tags - Visible pages and tiles are always admitted and uploaded (the viewport bounds them); the byte and count limits decide only how much prefetch fits. A HiDPI two-up view or a 5K viewport left pages on 'Rendering...' for good. - Pages no longer demanded stay cached, nearest the current page first, while the 128 MiB allowance and the 24-page count have room: paging back no longer re-renders. - Thumbnail density follows the demand count rounded up to a power of two, so rows scrolling in and out of an organize grid don't change every thumbnail's tag and re-render it. - A suspended (background) tab drains its pool's finished results instead of holding them. - Organize drag-and-drop ends on the primary button only. - Tests: the stale-result test uses valid pixels and checks nothing was uploaded; the organizer wheel test runs 40 steps; regressions for the first three fixes. * vendor: mark the hayro-syntax lock and SegmentList patches and list them as (8) and (9); clippy 1.99 as_chunks in a raster test * render: clippy 1.99 — contains over iter().any() in a raster test --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1399d38ea3 |
Fix: High-precision float mantissa overflow in hayro-syntax, causing warped display of some PDFs (#199) (#427)
* hayro-syntax: fix float mantissa overflow on high-precision coordinates (#199) jsPDF outputs double-precision floating-point coordinates with 16+ decimal places in /MediaBox. In hayro-syntax, read_inner accumulated float mantissa digits into u64 using wrapping arithmetic. Integer mantissas exceeding u64::MAX wrapped modulo 2^64, severely corrupting page dimensions (e.g., 3874.96 pt became 185.61 pt, rendering as a warped ribbon). Accumulation now uses checked arithmetic: trailing fractional digits exceeding u64 capacity are safely ignored without incrementing decimal_shift. This preserves full IEEE-754 f64 precision while preventing overflow for numbers with arbitrary numbers of decimal places. Includes: - Unit test in hayro-syntax with reported values and a 50-digit fraction. - Synthetic PDF regression test in pdfcraft-render verifying inspect(). - Vendored patch (6) documentation in vendor/README.md. Fixes #199 * Never crash: mesh shadings can't exhaust memory or run for minutes `cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in place) found a page whose Coons patch shading (type 6), a 147-byte stream with 1-bit coordinates decoded onto +-40000, took the renderer past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by pixel over each triangle's device-space bounding box into a map, with nothing but the u16 cast bounding it: up to 65536 x 65536 entries for one triangle. A synthetic single patch passed 6 GB in six seconds; ten thousand triangles that each cover half a 1000 x 1000 page ran for minutes. Two rounds of review of earlier versions found more: every patch was cut into 722 triangles (about 150 KB) before anything limited them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever, and one with 4294967295 read the whole stream into one row; a mesh painted a thousand times was triangulated a thousand times; and the first caps cut content without a word. Vendored hayro-interpret patch (18): - `ShadingPattern::encode_within(area)`: the vendored hayro renderer passes the area it will actually draw (the path's bounding box on the canvas, which it already computes) plus a pixel for texels at a fractional edge, and triangles are sampled only inside it, so the map holds at most that area's pixels. - `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per triangle in it. A triangle's bounding box is about twice its area and a sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and small dense ones (a patch in a few pixels is 722 triangles) still shade completely; overlapping ones stop. - Coons and tensor-product patches are triangulated one at a time. - Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19 vertices per row. - A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200 mesh is 29 million) mesh triangles in all, reset per page like the existing paint budget. - Each of these cuts logs a warning. `encode()` keeps its signature; with no area it samples within a fixed 2^24 visits. Synthetic regression tests in render: - `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle mesh decoded onto +-40000 render and the rest of the page draws; a patch on the page, the part of the huge one that covers it, and a patch filling a 4 x 4 pt rectangle as a pattern are all shaded. - `many_overlapping_mesh_triangles_finish`: 10,000 triangles each covering half of a 1000 x 1000 page finish, and are drawn. - `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches beside the page, and a lattice with `/VerticesPerRow 0`. Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a mutation: no visit budget stalls past 60 s; no clip leaves the covering patch unshaded; no per-triangle allowance leaves the small patch unshaded; triangulating all patches first runs out of memory (killed at 5.8 GB); no rows check runs out of memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: James Locke <336218001+jcl-bit@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
e88def3465 |
Never crash: decompression bombs can't exhaust the renderer's memory (#411)
* Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
a5202e11fc |
M2: normalize JPEG2000 samples without overflowing (#575)
* M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * ROADMAP: keep the JPX entry with this session's entries --------- Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
e15e5ebce0 |
Checkboxes with an indirect /AP /N state dictionary are drawn (#180) (#181)
hayro-syntax recovered a stream with a wrong /Length by scanning ahead for the bytes `stream`. An indirect appearance-state dictionary (`<< /Yes … /Off … >>`, as Quartz writes a checkbox's `/AP /N`) was read as whatever stream came later in the file. That dictionary has no `/BBox`, so the appearance was dropped and the widget was skipped. `stream` now has to be the next token. A wrong `/Length` still recovers, and the renderer already selects the `/AS` state once `/N` is a dictionary. Fixes #180 Co-authored-by: Eri Bastos <eb@fluxmail.xyz> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
d3db99818f |
Rename PrintCraft to PdfCraft (#174)
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the PrintCraft settings and crash-recovery folders to the new name, and both apps read settings saved under the old storage key, so upgrades keep recent files, preferences and unsaved work. The signing test fixtures keep their PrintCraft subject names (they are binary). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2f364665b8 |
Never crash: the fuzz job's eleven hangs finish (parser retries, inline images, Type 3 fan-out)
Every hang the nightly fuzz job found (11 findings, 8 source files) now finishes in 0.1-2 s; each cause has a synthetic regression test that times out without its fix. - cos (7 findings, our own parser): a stream whose /Length pointed at itself, with no endstream, made every parse attempt fail and each attempt reload the same object: ~2^32 parses. Objects being loaded are a seen-set, /Length values are memoised per load, and the shifted-header retry only runs when the header is shifted. - hayro-syntax (3): deciding where an inline image's data ends re-parsed the rest of the stream, nesting through every inline image; 50 images ran for over ten minutes. The look-ahead no longer nests and is bounded (256). - hayro-interpret (1): a Type 3 glyph showing eight glyphs of its own font, sixteen deep. Nested paints share a per-page budget (50,000). The pdf.js corpus sweep matches its baseline (963 ok, same 20 known). xtask check writes its report under CARGO_TARGET_DIR too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a5d830bf25 |
Never crash: predictor rows and stroke widths from the input can't exhaust memory
The second full nightly fuzz run on main found two more: - hayro-syntax computed a Flate/LZW predictor row as columns × bits in wrapping arithmetic: /Columns 9223372036854775807 asked for 2^61 bytes and aborted on any machine. Row widths are now saturating usize (colors × bits also overflowed u8), a zero width is refused (it divided by zero), and a row longer than the data is not allocated (empty output, as before). - hayro stroked /LW 9223372036854775807 as given, and stroke expansion allocated 10 GB. Widths are clamped to 4 × (canvas width + height) device pixels, which covers the same pixels. Both findings now run in 8 MB. Synthetic regression test in render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6058068266 |
Never crash: absurd image-mask and CCITT sizes are refused before decoding
The first full nightly fuzz run on main (now that the runner survives) found two 4 GiB allocations, caught by the 4 GiB child cap: - an inline stencil mask claiming /W 4294967295 was decoded in hayro-interpret before the renderer's MAX_IMAGE_PIXELS check could skip it (locally 9.6 GB, 42 s). Vendored patch image_size_ok: images and masks over 2^28 pixels are not decoded. - a CCITT image whose /DecodeParms claimed /Columns 4294967295 (with a small /Width) sized the decoder's line buffers (locally 4.3 GB). Vendored patch ccitt_size_ok in hayro-syntax: at most 2^20 columns and 2^28 pixels. Both findings now run in 7 MB. Synthetic regression test in render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2849ad8922 |
M2.6: vendor hayro-syntax 0.7.2 with a page-tree cycle guard (stack overflow on /Kids loops in object streams)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |