Skip to content

fix(gif): cap decoded gif dimensions and frame count - #241

Merged
LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/gif-decode-bomb
Sep 22, 2026
Merged

LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/gif-decode-bomb

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Severity Medium, category security. Location: crates/rustmotion-components/src/gif.rs:129

Impact

decoder.width()/height() come straight from the GIF logical-screen descriptor (2 bytes each, max 65535). A ~200-byte crafted GIF declaring 65535×65535 makes line 129 request a single 17.2 GB allocation before a single pixel is decoded. Worse, the while loop then pushes a full-canvas clone per animation frame (line 141) with no frame-count limit — a 4000×4000 canvas with 500 frames is 32 TB of Vec<u8> — and the result is inserted into the global gif_cache() (renderer/assets.rs:32) which is never evicted. A scenario whose gif src points at such a file aborts the render process (Rust allocation failure = abort, not a catchable error), which is a denial of service on any batch/CI renderer fed third-party scenarios or media.

Fix

Before allocating, reject canvases above a configured budget: check canvas_w as u64 * canvas_h as u64 * 4 against a cap (and compare against the video's own dimensions — nothing larger can be usefully drawn) and return None with a warning, exactly like the existing decode-failure arms. Add a frame-count cap to the while loop, and prefer storing only the deltas or re-decoding on demand rather than cloning the whole canvas per frame.

Evidence the audit read

let canvas_w = decoder.width() as u32;
    let canvas_h = decoder.height() as u32;

    let mut frames: Vec<(Vec<u8>, u32, u32)> = Vec::new();
    let mut cumulative_times: Vec<f64> = Vec::new();
    let mut accumulated = 0.0;
    let mut composed = vec![0u8; canvas_w as usize * canvas_h as usize * 4];

    while let Ok(Some(frame)) = decoder.read_next_frame() {
        ...
        frames.push((composed.clone(), canvas_w, canvas_h));

Stacked on perf/video-frame-cache-budget, which carries the previous finding of this workstream. GitHub shows only this finding's diff; merge in order.

Part of the September 2026 audit remediation chantier. Refs #220 (RM-23).

@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 21, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 21, 2026
@LeadcodeDev
LeadcodeDev force-pushed the perf/video-frame-cache-budget branch from 2eeb8b8 to e10ffb1 Compare September 22, 2026 06:10
@LeadcodeDev
LeadcodeDev force-pushed the perf/video-frame-cache-budget branch from e10ffb1 to f6a17df Compare September 22, 2026 08:34
@LeadcodeDev
LeadcodeDev force-pushed the perf/video-frame-cache-budget branch from f6a17df to 5e0f4c1 Compare September 22, 2026 08:44
@LeadcodeDev
LeadcodeDev changed the base branch from perf/video-frame-cache-budget to chantier/audit-2026-09 September 22, 2026 08:53
`decoder.width()/height()` come straight from the GIF logical-screen descriptor (2 bytes each, max 65535). A ~200-byte crafted GIF declaring 65535×65535 makes line 129 request a single 17.2 GB allocation before a single pixel is decoded. Worse, the `while` loop then pushes a *full-canvas clone* per animation frame (line 141) with no frame-count limit — a 4000×4000 canvas with 500 frames is 32 TB of `Vec<u8>` — and the result is inserted into the global `gif_cache()` (`renderer/assets.rs:32`) which is never evicted. A scenario whose `gif` src points at such a file aborts the render process (Rust allocation failure = abort, not a catchable error), which is a denial of service on any batch/CI renderer fed third-party scenarios or media.

Refs #220
@LeadcodeDev
LeadcodeDev merged commit e1381bd into chantier/audit-2026-09 Sep 22, 2026
LeadcodeDev added a commit that referenced this pull request Sep 22, 2026
`decoder.width()/height()` come straight from the GIF logical-screen descriptor (2 bytes each, max 65535). A ~200-byte crafted GIF declaring 65535×65535 makes line 129 request a single 17.2 GB allocation before a single pixel is decoded. Worse, the `while` loop then pushes a *full-canvas clone* per animation frame (line 141) with no frame-count limit — a 4000×4000 canvas with 500 frames is 32 TB of `Vec<u8>` — and the result is inserted into the global `gif_cache()` (`renderer/assets.rs:32`) which is never evicted. A scenario whose `gif` src points at such a file aborts the render process (Rust allocation failure = abort, not a catchable error), which is a denial of service on any batch/CI renderer fed third-party scenarios or media.

Refs #220
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant