refactor(uploads): centralize image decode behind safe helpers + grep-ban (LC-206) #272
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/lc-206-image-decode-safe-helper"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Framing: spike + audit clarified the failure mode
LC-206 was filed as "uploads run the decoder without a
catch_unwindboundary while the avatar path runs with one." Verifying that premise first (the LC-205 discipline) found it wrong about WHY, not about whether the work should happen. All four decoder call sites already sat insidetokio::task::spawn_blocking, andpanic=unwindis confirmed in both Cargo profiles, so tokio'sJoinError::is_panic()already caught every decoder panic today. There was never a missing panic boundary and never a live exploit. The asymmetry instinct was correct; the threat model attached to it (live panic exposure) was not. The work is the same; the framing shifts from urgency to hygiene. Real wins retained: retire four copies of the same spawn_blocking + JoinError boilerplate, prevent a future un-spawn_blocking'd fifth caller structurally, and close the validation gap (the decoder was only ever spike-validated at the 1 MiB avatar cap, never the 10 MiB upload cap).10 MiB spike result (the gate) - clean, Outcome A
Re-ran the LC-78-AVATAR-PROXY hostile corpus scaled to the upload path's 10 MiB ceiling, isolated in
catch_unwind, not through any live route.Zero panics. Promoted into
spike_image_decoder_hostile_corpus.rsso the validation is permanent. The large-valid row carries a caveat comment: it tests allocation-at-scale (36 MB decode buffer), not compression ratio; the high-ratio decompression-bomb vector is covered by the pixel-bomb row, whichimage 0.25rejects at header-read before allocating. Without that comment a future reader could misread "large valid PNG worked" as "decompression bombs are fine."Change
Two thin public async entry points over one shared internal panic boundary (LC-152
outbound_get/outbound_postshape):process_image_safely(path, mime) -> ProcessedImageandprocess_preview_safely(path, mime) -> Vec<u8>sharerun_blocking, which folds aJoinErrorintoSafeImageError::Panicand a normal failure intoSafeImageError::Pipeline.ImagePipelinedrop; bridge-avatar keeps the decode-vs-panic log distinction; admin regen warns-and-continues on a pipeline error and propagates 500 on a panic (the old?-on-JoinError). The thumbnail semaphore stays at the call sites that acquired it (bridge-avatar deliberately does not), so throttling is unchanged.Grep-ban (
lc206_no_raw_image_decode_in_src.rs, LC-152 shape)Raw decoder entry points (
ImageReader::open/new,GifDecoder::new,image::open/load,image::io::Reader) and therun_blockingpanic-boundary seam are forbidden insrc/outside the one full-path allow-list entrysrc/uploads/pipeline.rs.run_blockingis banned exactly as hard as the raw entry points (same posture LC-152 takes withoutbound_unchecked): a#[doc(hidden)]seam is still a bypass ifsrc/can call it, since a direct caller would replicate the boilerplate the helpers exist to retire. The syncprocess_image/preview_from_pathstaypubas the pure-pipeline unit-test seam; visibility is not the enforcement, the grep-ban is. A meta-test assertspipeline.rsstill contains a banned token so the exception cannot silently go dead.Killer test pair (
image_decode_safe_boundary.rs)A synthetic panic through
run_blockingmaps toSafeImageError::Panic(proves the boundary catches regardless of source, durable across an image-crate bump); a malformed image maps toSafeImageError::Pipeline(Decode), NOTPanic(proves the two failure modes stay distinguishable, which the bridge-avatar logging relies on). Both assert the specific variant, never bareis_err().Out of scope (confirmed)
No upstream
image-rsreport (nothing panicked). No security-release question (no shipped live vector - the premise correction makes this clear). Noimage::*broad ban, no decode-only boundary, no helper expansion to resize/convert.ProcessedImagestays the common struct.Test plan
just checkclean both modes. Full suite green both modes: standalone and saas, 128 binaries each, zero failures. No pre-existing failures observed in either run (thelc77_webhook_render_fixturegolden-fixture failures and theroutes_uploadsconcurrent-load flake noted in prior PRs did not reproduce here; neither is touched by this change).🤖 Generated with Claude Code