Spec: image preview in the Code Editor file pane (GH9729)#10024
Spec: image preview in the Code Editor file pane (GH9729)#10024NorfeldtKnowit wants to merge 10 commits intowarpdotdev:masterfrom
Conversation
Adds product and tech specs under specs/GH9729/ for an in-Warp image preview surface launched from the File Tree, reusing the existing Lightbox overlay component per maintainer guidance in the issue thread. Co-Authored-By: Warp <[email protected]>
|
Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the users @NorfeldtKnowit on file. In order for us to review and merge your code, each contributor must visit https://cla.warp.dev to read and agree to our CLA. Once you have done so, please comment |
|
I'm starting a first review of this spec-only pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
|
@cla-bot check |
|
The cla-bot has been summoned, and re-checked this pull request! |
There was a problem hiding this comment.
Overview
This spec PR defines a Lightbox-based image preview flow for the Code Editor file pane, including product behavior, target resolution changes, sibling navigation, decoder limits, error rendering, and validation coverage.
Concerns
- The sibling-list cap algorithm can drop the clicked image after sorting, contradicting the required behavior and the proposed tests.
- The re-open/replace behavior assumes users can click files behind a Lightbox even though the same spec says the scrim blocks underlying input.
Security
- Eagerly loading and decoding up to 1024 sibling images on open leaves a local-project denial-of-service path despite per-image decode caps; the spec should require lazy loading or explicit decode/read concurrency limits.
Verdict
Found: 1 critical, 2 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| siblings.push(path.to_path_buf()); | ||
| } | ||
| siblings.sort_by(|a, b| natural_cmp(file_name_lossy(a), file_name_lossy(b))); | ||
| siblings.truncate(MAX_SIBLING_IMAGES); |
There was a problem hiding this comment.
🚨 [CRITICAL] Truncating after sorting can drop the clicked image when its natural-sort position is beyond the first 1024 entries, so the Lightbox may open on the wrong file; specify and implement the partition/window-around-clicked algorithm before truncation.
- tech.md: rewrite list_sibling_images_natural_sorted to window around the clicked image before truncation so the clicked image is never dropped in directories larger than MAX_SIBLING_IMAGES. - tech.md: bound parallel sibling decodes to current ± PRELOAD_RADIUS by building only the preload window as Resolved and the rest as Loading; lazy-promote on Left/Right via UpdateLightboxImage. Closes the local-project DoS surface where 1024 parallel decodes could allocate ~262 GB transiently before cache eviction reacted. - product.md: replace the unreachable "replace overlay in place" flow on file-tree clicks with the actual dismiss-then-cold-open behavior driven by LightboxViewEvent::FocusLost. The handler-reuse path stays in tech.md but is correctly scoped to non-focus-stealing dispatch sites (artifacts/screenshots), not file-tree. - tech.md: update Risks and unit-test bullets to match. Co-Authored-By: Warp <[email protected]>
|
Thanks @oz-for-oss. Pushed 🚨 CRITICAL —
/oz-review |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This spec proposes routing File Tree image clicks through the existing Lightbox overlay with sibling navigation, decoder limits, and per-entry error states. The direction is aligned with the issue, but several parts of the product and tech specs are internally inconsistent or underspecified enough that implementation could miss stated success criteria.
Concerns
- Animated GIF/WebP playback is promised, but the proposed Lightbox rendering path does not specify the animation start-time change needed for repainting.
- Lazy preloading requires a concrete path-bearing data model; reconstructing local paths from
descriptionis not reliable with the current Lightbox parameters. - Error handling only covers
AssetState::FailedToLoad, leavingImageType::Unrecognizedfiles in a perpetual loading state despite the spec's error-state requirement. - The end-to-end flow still describes eager resolution/loading of every sibling, contradicting the bounded-preload mitigation.
Security
- Animated GIF/WebP limits must be enforced while iterating frames, not after
collect_frames(), or malicious animated files can allocate all frames before the cap is checked.
Verdict
Found: 0 critical, 5 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
- product.md: scope animated GIF/WebP to first-frame static preview for v1 (continuous playback requires Image::enable_animation_with_start_time wiring + per-frame redraw, pulled out as a follow-up). - tech.md change 3: extend LightboxImage with path: Option<PathBuf> so lazy promotion on Left/Right has a concrete data model; existing call sites (artifacts/screenshots/agent blocks) pass None. - tech.md change 4: replace decoder.into_frames().collect_frames() with a streaming collect_animated_frames_with_limits iterator that bails on running pixel/dimension cap before allocating the next frame, preventing animated-bomb files from materializing every frame before the cap fires. - tech.md change 4: convert Ok(ImageType::Unrecognized) to Err so unrecognized-bytes-but-supported-extension files surface as per-entry Error in the Lightbox instead of spinning forever as Loaded-with-no-size. - tech.md end-to-end flow: rewrite step 6 to describe only the preload window being resolved/queued, with lazy promotion on navigation, matching change 3 (was contradicting the bounded preload by saying every sibling was resolved/queued). - tech.md follow-ups: replace "play/pause control" with explicit "continuous playback via enable_animation_with_start_time" entry. Co-Authored-By: Warp <[email protected]>
|
Thanks for the second pass @oz-for-oss. Pushed 1. Animated GIF/WebP needs 2. Lazy promotion data model is underspecified ( 3. 4. 5. End-to-end flow contradicts the bounded preload ( /oz-review |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This spec routes supported image file clicks from the Code Editor file pane into the existing Lightbox overlay, with sibling navigation, decode limits, error states, and validation coverage.
Concerns
- The animated GIF/WebP decode plan still materializes every frame even though v1 explicitly renders only the first frame, which leaves avoidable memory and DoS exposure in the implementation plan.
- The bounded preload plan still permits roughly 1.2 GB of concurrent transient decode allocation from a local directory click; the spec should require serialization, a stricter global memory budget, or lower caps before implementation.
- The product accessibility requirement is not carried into the technical rendering changes, so the implementation could satisfy the visual behavior while missing the screen-reader label.
Security
- Local project files are attacker-controlled inputs for many workflows; the remaining animated-frame and parallel-decode memory envelopes should be tightened in the design, not left to implementation judgment.
Verdict
Found: 0 critical, 2 important, 1 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| Ok(ImageFormat::WebP) => { | ||
| let decoder = WebPDecoder::new(std::io::Cursor::new(data))?; | ||
| if decoder.has_animation() { | ||
| let frames = collect_animated_frames_with_limits(decoder.into_frames())?; |
There was a problem hiding this comment.
| } | ||
| ``` | ||
|
|
||
| In `Lightbox::render` (`crates/ui_components/src/lightbox.rs`, the per-image render branch around lines 158-176), add an `Error` arm that renders a non-blocking error panel showing the entry's `description` (filename) and the `message`. Do not throw; do not block prev/next. |
There was a problem hiding this comment.
💡 [SUGGESTION] The product spec requires the active filename to be exposed as the image's accessible label, but the tech spec only describes visual error rendering. Add the concrete Lightbox/Image accessibility change and validation so the a11y requirement is implemented.
- Animated decode wastes memory: collapse WebP/GIF animated arms to
decode-first-frame-only since v1 doesn't render animations. The
AnimatedBitmap variant becomes unconstructed; left in enum with a
comment referencing the continuous-playback follow-up.
- Tighten preload envelope: PRELOAD_RADIUS 2->1, MAX_DECODED_PIXELS
64M->32M. Worst-case transient decode allocation drops from
~1.2 GB to ~366 MB per file-tree click. Risks math, validation
fixture sizes, lazy-preload validation step all updated to match.
- A11y label is now wired in tech: change 5 specifies the rendered
Image gets accessible_label = description.unwrap_or("image"),
with parallel labels on Loading and Error variants. New manual
validation step covers VoiceOver announcement.
Co-Authored-By: Warp <[email protected]>
|
Pushed 1. Animated decode still materializes every frame up to the cap (
2. 1.2 GB transient is too generous (
Worst-case transient envelope is now 3. A11y label requirement not implemented in tech (
Added a manual validation step: enable VoiceOver, click This is the third |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This spec defines a focused Lightbox-based image preview flow for File Tree image clicks and covers many product and implementation concerns. Several parts still conflict with the existing Lightbox input model, AssetCache/image decode behavior, and telemetry mapping.
Concerns
- The spec requires underlying panes to be inert while also requiring a click on the File Tree to open another file while the Lightbox is active.
- The responsiveness guarantee is not supported by the proposed AssetCache path because image conversion currently happens on the foreground executor.
- Decoder limits do not cover the encoded local-file read that happens before
try_from_bytes. - The proposed global GIF/WebP first-frame decode would regress an existing animated-image surface.
- Telemetry mapping and preload-count prose need to be made explicit and internally consistent.
Security
- Local files with supported image extensions can still be arbitrarily large encoded inputs; the design needs a pre-read file-size cap, streaming metadata check, or equivalent mitigation before full-file read/decode.
Verdict
Found: 1 critical, 4 important, 1 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| - The image renders centered, fit to the viewport via `Image::new(...).contain()`: aspect ratio preserved, never upscaled past the image's native pixel dimensions. | ||
| - The Lightbox renders as a child of the workspace's main render `Stack`, so the scrim covers the whole workspace (all panes are dimmed, not just the active pane). This matches today's screenshot/artifact Lightbox behavior. | ||
| - Underlying panes remain visible behind the scrim and do not receive input while the Lightbox is open. | ||
| - While bytes are still being read or decoded, the existing loading indicator is shown. Arrow-key navigation remains responsive; an in-flight decode never blocks navigation. |
There was a problem hiding this comment.
ImageType::try_from_bytes runs on the foreground executor after fetch. Add a design change to decode off the UI/foreground executor or weaken this guarantee.
Cuts sibling navigation, the bounded preload window, and the click-to-swap flow that drove the modality contradiction Oz flagged. Adds a unified pre-read size cap and a bounded asset-cache read to close pre-decode DoS surfaces (symlink-to-/dev/zero, TOCTOU). Hardens the global decoder with image::Limits, an explicit animated frame-count and total-pixel budget enforced during iteration, and an SVG intrinsic-dimension cap; frame collection for AnimatedBitmap is unchanged so the changelog continues to animate. Descopes the rendered-image a11y label since GPUI's Image element does not expose an accessibility API today; tracks it as the first follow-up.
|
Thanks for the fourth-pass review @oz-for-oss. Pushed Critical (modality contradiction). Cut sibling navigation, the bounded-preload window, and the click-to-swap flow entirely. v1 is now a single, fully modal, single-image Lightbox: while open, the workspace (file tree, terminals, code editor, tab bar) is inert; opening another file means dismiss-first. That removes the inert-vs-click-through contradiction at its source rather than papering over it. Important (pre-read DoS surface). Added a unified pre-read file-size cap and a bounded Important (animated frame materialization). The previous round's first-frame-only collapse would have regressed the changelog's animated rendering, which I missed. Reverted that: Important (SVG decompression bomb). Added an SVG intrinsic-dimension cap at parse time, so a 200-byte SVG declaring Suggestion (a11y label honesty). Verified against Quick procedural question: my read of the prompt is that the "up to 3 times on the same pull request" budget is aimed at re-review spam where nothing has changed between requests. Does it apply identically when the retrigger follows a substantive code push, or only to re-reviews against the same revision? Happy to leave this for human review either way; mostly want to make sure I am reading the rule correctly. /oz-review |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This spec PR defines a Lightbox-based image preview path for supported file-tree image extensions, including product behavior, implementation integration points, decoder limits, error rendering, telemetry, and validation.
Concerns
- The product spec says there are no global decoder behavior changes, but the tech spec explicitly hardens the shared
ImageType::try_from_bytespath with new global failure modes for over-cap animated and SVG inputs. - The bounded
LocalFileread limits bytes but does not require re-validating that the opened handle is a regular file after the metadata/read TOCTOU window, leaving a local DoS gap for FIFO or special-file replacement.
Security
- The asset-cache read plan should require post-open regular-file validation before reading so a path replacement cannot block indefinitely or bypass the stated special-file rejection contract.
Verdict
Found: 0 critical, 2 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| - While bytes are still being read or decoded, the existing loading indicator is shown. | ||
| - If the file fails the pre-read size cap, fails to read, fails to decode, or exceeds the decoded-pixel cap, the Lightbox shows a non-blocking error state that includes the filename and a short reason. The Lightbox never crashes the workspace. | ||
| - SVG renders via the existing `usvg` + `resvg` pipeline used by `ImageType`. | ||
| - Animated GIF and animated WebP files render their **first frame only**. The Lightbox today never calls `Image::enable_animation_with_start_time`, so animated raster formats already render as a static first frame in this surface; v1 reuses that behavior unchanged. Continuous playback in the Lightbox is tracked as a follow-up. No global decoder behavior changes; the changelog section's animated-image rendering is untouched. |
There was a problem hiding this comment.
ImageType::try_from_bytes path. Reword this to say legitimate animated playback is unchanged, while pathological inputs can now fail globally.
| const MAX_ASSET_LOCAL_FILE_BYTES: u64 = 64 * 1024 * 1024; // matches MAX_PREVIEW_FILE_BYTES | ||
|
|
||
| async { | ||
| let file = async_fs::File::open(&path).await?; |
There was a problem hiding this comment.
take()-reading the path still trusts whatever the path resolves to at read time; after the metadata check, a local process can replace it with a FIFO or special file that blocks indefinitely. Require the asset cache to verify the opened handle is a regular file, such as with post-open file-type validation/no-follow behavior where available, before reading.
- Reword product.md to accurately state that the global ImageType decoder is hardened (frame-count and total-pixel budgets for animated formats, SVG intrinsic-dimension cap) without regressing any in-tree surface; the workspace changelog's animated rendering is unchanged. - Add post-open is_file() validation on the opened handle in the bounded AssetCache read, closing the FIFO/special-file TOCTOU window between the pre-read metadata stat and the open syscall. Add a corresponding race-replace validation case.
|
Thanks for the fifth-pass review @oz-for-oss. Pushed Important (product/tech consistency on global decoder). The honest answer is that change 4 does harden the shared Important / Security (post-open regular-file revalidation). Real TOCTOU gap. Updated change 5 in /oz-review |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
The specs cover the intended Lightbox-based image preview flow and include useful product, implementation, validation, and follow-up detail. The main remaining issues are in the threat model for local image reads and SVG decoding.
Concerns
- The proposed post-open regular-file check does not prevent
openitself from blocking on a FIFO or special-file path introduced by the TOCTOU swap it is meant to defend against. - The SVG plan relies on a 64 MB global byte cap plus an intrinsic-dimension check after
usvgparsing, but does not bound SVG parser CPU/memory before that parse runs on the foreground executor. - One product-spec performance sentence says over-cap files are rejected at pre-read, which conflicts with the tech spec's decode-time dimension and pixel checks.
Security
- A malicious repository file can still cause a denial-of-service if a regular image path is swapped to a FIFO before asset-cache open, because the blocking point happens before the opened-handle metadata check.
- A malicious but under-64 MB SVG can consume parser CPU/memory before the intrinsic-dimension guard runs; the spec should add a content-sniffed SVG byte/complexity limit or otherwise bound parsing.
Verdict
Found: 0 critical, 2 important, 1 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| const MAX_ASSET_LOCAL_FILE_BYTES: u64 = 64 * 1024 * 1024; // matches MAX_PREVIEW_FILE_BYTES | ||
|
|
||
| async { | ||
| let file = async_fs::File::open(&path).await?; |
There was a problem hiding this comment.
file.metadata() runs. Specify a nonblocking/platform-specific open strategy, or an atomic regular-file-only open, that cannot hang on FIFOs or special files before the fstat check.
|
|
||
| Notes on this shape: | ||
|
|
||
| - **One unified pre-read cap.** Earlier drafts split the cap by file extension (`.svg` got a smaller cap). That is bypassable: `try_from_bytes` classifies SVG by content (first byte `<`, see `image_cache.rs:275`), not by extension, so a `.png` whose contents start with `<` would be routed through `usvg` while passing the looser raster cap. v1 uses one cap for everything pre-read; the SVG-specific allocation surface is bounded by the SVG intrinsic-dimension cap added in change 4 instead. |
There was a problem hiding this comment.
usvg::Tree::from_data runs on the foreground executor. Keep a content-sniffed SVG byte/complexity limit or move/bound SVG parsing separately.
|
|
||
| ### Performance posture | ||
|
|
||
| - Image bytes are read on the background executor; the bytes-to-RGBA decode itself runs on the foreground executor (`AssetCache::load_asynchronously` invokes `try_from_bytes` on the foreground executor before publishing the loaded asset). v1 does not change this. The decoder caps below bound the worst-case decode time, and the loading indicator is shown until decode completes; a file that would exceed the caps is rejected at the pre-read step before any decode runs. |
There was a problem hiding this comment.
💡 [SUGGESTION] This says any file exceeding decoder caps is rejected at the pre-read step, but dimension and pixel caps are only discovered during decode. Reword this so the product expectation matches the tech spec's pre-read byte cap versus decode-time dimension/pixel checks.
- Add O_NONBLOCK to the asset-cache open in change 5 so open() of a FIFO does not block before the round-5 post-open is_file() check can run. Document the POSIX semantics that O_NONBLOCK is harmless for regular-file reads, add a Unix-only mkfifo unit test, and note Windows-side handling. - Add an SVG-specific pre-parse byte cap (MAX_SVG_BYTES = 4 MB) and content-sanity prefix check applied before usvg::Tree::from_data is invoked, so a malicious deeply-nested SVG cannot consume parser CPU/memory under the 64 MB raster cap. Add corresponding unit test and manual validation case. - Reword product.md's performance/limits text to distinguish the pre-read size cap (file size, rejects before read) from the decode-time dimension and pixel caps (decoded resolution, rejects during decode). Reconcile Success criteria 5-7 if needed.
|
Thanks for the sixth-pass review @oz-for-oss. Pushed Important / Security (open() blocks on FIFO before is_file check). Real gap; the round-5 post-open check only runs if Important / Security (SVG parser CPU/memory under 64 MB cap). Layered defense in Suggestion (product spec conflates pre-read with decode-time caps). Reworded the Performance posture and Limits sections in /oz-review |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This spec proposes routing supported image-file clicks from the Code Editor file tree into the existing Lightbox overlay, with product behavior, decoder limits, bounded local-file reads, and validation coverage.
Concerns
- The SVG parser DoS mitigation is internally inconsistent: the spec says extension-based caps are bypassable because SVG decode is content-sniffed, but the final bounded-read design keys the tighter SVG cap on
.svgextension. - The global
AssetSource::LocalFileread cap is under-surveyed; existing local markdown/blocklist image rendering and custom theme background images also useLocalFile, so the spec needs to scope or validate the behavior before implementation.
Security
- The extension-keyed SVG byte cap can be bypassed by XML/SVG content under a non-
.svgsupported image extension, allowing up to the raster cap to reachusvgdespite the stated parser-DoS mitigation.
Verdict
Found: 0 critical, 2 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| // intrinsic-dimension check is possible. 4 MB comfortably covers any | ||
| // legitimate icon, diagram, or illustration SVG (real-world SVGs are | ||
| // typically a few hundred KB at most). | ||
| let cap = if path_has_svg_extension(&path) { |
There was a problem hiding this comment.
try_from_bytes classifies SVG by content (<), so XML/SVG bytes in payload.png get the 64 MB raster cap and can still reach usvg; make the tight cap match the parser selection, or use a unified safe cap before SVG sniffing.
|
|
||
| The post-open `is_file()` check is **in addition to**, not a replacement for, `O_NONBLOCK`, the pre-read size cap from change 2, and the `MAX + 1` bounded read. `O_NONBLOCK` ensures `open()` of a FIFO returns immediately so the post-open check actually runs; the post-open check rejects the FIFO before any read is attempted. Change 2 still rejects oversize regular files synchronously before dispatch (avoiding even an open syscall on the obvious-bad cases), and the bounded read still defends against a regular file that grows past the cap between the pre-read stat and the open. The dual-cap design (64 MB for raster, 4 MB for SVG) bounds the bytes fed to `usvg::Tree::from_data` more tightly because XML parsers have super-linear cost in input size. | ||
|
|
||
| `path_has_svg_extension` is a small case-insensitive helper that returns true if the path ends in `.svg`. This intentionally keys on extension, not content sniffing: change 2's resolver already gates the file-tree path on `is_supported_image_file`, so by the time bytes reach this load future the caller has committed to SVG-vs-raster classification. Other `LocalFile` consumers (none today) would also benefit from extension-driven cap selection because the SVG parser cost is the same regardless of caller. |
There was a problem hiding this comment.
LocalFile change is not fully surveyed: local markdown/blocklist images and custom theme backgrounds also create AssetSource::LocalFile, so the spec should either scope the cap to file-tree previews or add acceptance criteria/tests for those existing surfaces.
- Switch the SVG byte cap from extension-keyed to content-keyed in change 5: a 1 KB peek + looks_like_svg_xml at the asset-cache read step picks MAX_SVG_BYTES (4 MB) for any file whose first bytes look XML-shaped, regardless of extension. A 4.5 MB SVG-content file with a .png extension is now capped at 4 MB instead of flowing through the 64 MB raster cap. Reconcile change 4's SVG text and the Pre-read size envelope risk to describe the cap as content-keyed and explain why extension-keying was discarded. Add unit tests local_file_read_caps_svg_content_under_png_extension and local_file_read_uses_raster_cap_for_non_svg_content, and add a manual-validation fixture for the same case. - Survey existing AssetSource::LocalFile consumers (image preview, markdown image embeds in editor/notebooks, custom theme background images, theme deletion preview, settings profile image, blocklist images) and document each consumer's legitimate input envelope against the 64 MB raster / 4 MB SVG content-keyed caps. Keep a single global cap rather than parameterizing per call: no current consumer routinely exceeds those caps. Add a follow-up entry for parameterizing AssetSource::LocalFile with optional max_bytes if a future consumer ever needs higher caps.
|
Thanks for the seventh-pass review @oz-for-oss. Pushed Important / Security (extension-keyed SVG cap is bypassable). Real bypass; the round-6 design picked the cap from path extension, so an Important (global LocalFile cap under-surveyed). Surveyed every current /oz-review |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This spec proposes routing supported image files from the Code Editor file tree into the existing Lightbox and adds decoder/read limits for local image assets. The overall shape is aligned with the linked issue, but there are blocking internal inconsistencies around where the SVG byte cap is applied and how the validation fixtures prove the parser-DoS mitigation.
Concerns
- The product spec says the SVG-specific cap fires at the same point as the pre-read metadata cap, while the tech spec later relies on a content-keyed bounded read after opening the asset.
- The end-to-end flow describes extension-keyed SVG cap selection, contradicting the content-keyed design that is required to prevent SVG content hidden under non-SVG extensions from bypassing the tighter cap.
- The manual validation fixture for the SVG parser-DoS byte cap is below the declared 4 MB cap, so it would not verify the intended pre-parse rejection.
Security
The SVG parser-DoS mitigation is security-relevant. The spec needs one consistent, testable contract for content-keyed SVG byte limiting before implementation.
Verdict
Found: 0 critical, 3 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| - **Bounded read.** The actual file read in the asset cache is bounded by the same cap, so a file that grows after the pre-read metadata check, or a path swapped to a special device or FIFO between the metadata check and the open, cannot bypass the limit. Fires during the read step. | ||
| - **Decode-time dimension and pixel caps.** Rejects files whose declared image **dimensions** or `width × height` **pixel count** exceed fixed maximums. Fires during decode, after the bytes have been read; the file size may be small (and within the pre-read cap) while the decoded image would still exceed the pixel budget. Example: a 3 MB JPEG header that decodes to 100000×100000 pixels passes the pre-read cap but fails this cap. | ||
| - **Animated frame budget.** Rejects animated GIF and animated WebP files whose total frame count or whose summed pixels-across-frames exceed fixed maximums. Fires mid-iteration during decoding so pathological inputs do not first materialize every frame into memory. | ||
| - **SVG-specific pre-read cap.** Rejects SVG files whose on-disk size exceeds a tighter SVG-only cap (smaller than the raster cap), because XML parsers have super-linear cost in input size and a large SVG can consume parser memory disproportionate to its file size. Fires at the same point as the pre-read file-size cap, with a tighter ceiling for `.svg`. |
There was a problem hiding this comment.
| 4. `FileTreeEvent::OpenFile { path, target: ImagePreview, line_col: None }` is emitted, re-emitted as `LeftPanelEvent::OpenFileWithTarget` by `left_panel.rs:758-768`, and handled by `Workspace::open_file_with_target` at `view.rs:5715-5815`. | ||
| 5. The new `ImagePreview` arm (change 2) calls `std::fs::metadata` for the pre-read check (regular-file + size cap). On success it builds a single-element `Vec<LightboxImage>` with `source: Resolved { LocalFile { path } }` and dispatches `WorkspaceAction::OpenLightbox` via `ctx.dispatch_typed_action`. On metadata error, non-regular-file, or oversize file it builds the same single-element vec with `source: Error { message }` (a sanitized constant) and dispatches. | ||
| 6. The `OpenLightbox` handler (`view.rs:21710-21737`) creates `lightbox_view` and focuses it. The scrim covers the workspace; pointer input is intercepted by the scrim; the file tree, terminal panes, code editor panes, and tab bar are all inert. `LightboxView::start_asset_loads` queues `AssetCache::load_asset` for the single `Resolved` entry (no-op for `Error`). | ||
| 7. The asset cache opens the file with `O_NONBLOCK` on Unix (change 5) so that `open()` of a FIFO swapped in via TOCTOU returns immediately rather than hanging waiting for a writer; the post-open `fstat`-based `is_file()` then rejects any non-regular-file descriptor before any byte is read. For regular files, the load future reads up to `cap + 1` bytes via `take()` where `cap` is `MAX_SVG_BYTES` (4 MB) for `.svg` paths and `MAX_ASSET_LOCAL_FILE_BYTES` (64 MB) otherwise, and bails if the cap was exceeded. The bytes are sent through the asset-cache channel and `try_from_bytes` runs on the foreground executor with the static-decode limits, the animated frame budget, the SVG content-sanity prefix check, and the SVG intrinsic-dimension cap from change 4. The Lightbox renders as a child of the workspace's main `Stack` (`view.rs:22739-22740`); the image fits via `Image::new(asset_source).contain()`. Until the bytes have been decoded, the loading indicator is shown. Animated GIF and animated WebP files render their first frame statically because the Lightbox does not call `enable_animation_with_start_time` (no v1 change to this). |
There was a problem hiding this comment.
.svg paths), contradicting change 5's content-keyed design. The normative flow should say the cap is selected by the first-1 KB looks_like_svg_xml peek so SVG content under non-.svg extensions cannot bypass the 4 MB parser limit.
| 9. **Animated formats**: open an animated GIF and an animated WebP from the file tree. Confirm each renders its first frame statically (no continuous playback in the Lightbox). Then navigate to the changelog page in the Resource Center and confirm changelog GIFs/WebPs continue to animate (regression check on change 4). | ||
| 10. **Pathological animated input**: open an animated GIF with 500 frames (above `MAX_ANIMATED_FRAMES`). Confirm the per-entry error state. Open an animated WebP whose frames sum above `MAX_ANIMATED_TOTAL_PIXELS` and confirm the same; observe via memory inspection that the error fires before the full frame set is materialized. | ||
| 11. **SVG**: open a small well-formed SVG and confirm it renders (not raw XML). Open `<svg width="200000" height="200000">...</svg>` and confirm the per-entry error state ("svg dimensions exceed render budget") rather than a multi-GB pixmap allocation. | ||
| 11a. **SVG byte-cap and parser DoS**: generate a `deeply_nested.svg` containing roughly 50000 opened `<g>` elements (file size around 3.5 MB); open it from the file tree and confirm the per-entry error fires from the SVG byte cap before `usvg::Tree::from_data` is ever invoked. With Activity Monitor / `time` / RSS observation, confirm the workspace foreground thread does not spike for hundreds of milliseconds, and total resident memory does not climb to multi-hundred-MB during the dispatch. Then generate a 5 MB binary blob renamed `data.svg` (e.g. `dd if=/dev/urandom of=data.svg bs=1M count=5`) and open it; confirm the per-entry error fires either from the SVG byte cap (if over 4 MB) or from the content-sanity prefix check (if under 4 MB) before the parser runs. |
There was a problem hiding this comment.
MAX_SVG_BYTES = 4 MB, so this fixture will not fail from the SVG byte cap before usvg::Tree::from_data. Make the fixture exceed 4 MB or change the expected failure mode, otherwise the parser-DoS mitigation is not actually validated.
- Reword product.md to describe the SVG cap as firing at the asset-cache content-keyed bounded read, not at the pre-read metadata stage. The pre-read cap is uniform at 64 MB; the SVG cap (4 MB) is content-keyed and applies post-open after a 1 KB peek. - Update tech.md end-to-end flow and surrounding prose to describe the SVG cap selection as content-keyed (peek + looks_like_svg_xml) instead of extension-keyed. Reconciles round-7's content-keyed switch through the full spec narrative. - Bump the SVG parser-DoS manual validation fixture from 3.5 MB (below cap, would not verify rejection) to 4.5 MB (above cap, exercises the bounded-read rejection at the asset-cache step before any parse runs). Add an under-cap counter-fixture and align the existing unit test to use a >4 MB fixture.
|
Thanks for the eighth-pass review @oz-for-oss. Pushed Important (product.md describes SVG cap as pre-read). The cap is post-open, after the first-1-KB content peek; the pre-read metadata cap is uniform at 64 MB across formats because no bytes are read at that point. Reworded the relevant Limits and Performance-posture bullets in Important (end-to-end flow describes extension-keyed cap). End-to-end-flow step 7 in Important (manual fixture below the 4 MB cap). Bumped the SVG parser-DoS fixture in Manual validation step 11a from "around 3.5 MB" (which would have parsed successfully and not exercised the cap) to "4.5 MB above /oz-review |
|
I'm re-reviewing this spec-only pull request in response to a review request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
The spec defines a scoped v1 for previewing supported image files from the Code Editor File Tree in the existing Lightbox overlay, with clear product behavior, integration points, decoder/read limits, validation coverage, and follow-up boundaries. The supplemental security review found the local-file and decoder DoS surfaces are explicitly addressed with pre-read caps, bounded reads, sanitized errors, and decode-time limits.
Concerns
- One non-blocking clarity issue remains: the SVG prefix predicate should either broaden support or explicitly document the supported SVG prelude subset so valid SVG files with comments, DOCTYPE, or other XML prelude forms do not unexpectedly fail the advertised SVG-preview contract.
Verdict
Found: 0 critical, 0 important, 1 suggestions
Approve with nits
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
|
|
||
| 1. **SVG-specific pre-parse byte cap (`MAX_SVG_BYTES = 4 * 1024 * 1024`).** Applied in change 5's bounded read on the asset-cache side: bytes whose first 1 KB peek matches `looks_like_svg_xml` are read with a 4 MB ceiling (`MAX_SVG_BYTES + 1`) instead of the 64 MB raster ceiling. The cap is keyed on the **content** of the file, not its extension, so a non-`.svg` file whose contents are SVG XML is also tightened to 4 MB; an over-cap SVG returns `Err` from the asset-cache read step before bytes reach `try_from_bytes`. 4 MB is roomy for any legitimate icon, diagram, or illustration SVG (real-world SVGs are typically a few hundred KB; even maximalist illustration SVGs rarely exceed 1-2 MB) while preventing a 64 MB SVG from being handed to `usvg`. | ||
|
|
||
| 2. **Content-sanity prefix check on the bytes that reach `try_from_bytes`.** Before `usvg::Tree::from_data` runs, the same `looks_like_svg_xml` helper used by change 5 to pick the byte cap is invoked one more time on the buffer that reached the decoder, and the buffer is rejected if it fails. The check confirms a UTF-8 prefix consistent with XML/SVG: optional UTF-8 BOM, optional whitespace, then `<?xml` or `<svg`. This is the second of two uses of the helper: change 5 calls it on the file-side peek to **pick the cap**, and change 4 calls it on the in-memory buffer to **gate the parser**. Reusing the same predicate keeps the cap signal and the parser-gate signal aligned by construction. The check catches the "binary blob renamed to `.svg`" case (which would otherwise either parse-fail expensively or waste cycles in `usvg`'s error paths) and is cheap (a bounded byte scan, no allocation): |
There was a problem hiding this comment.
💡 [SUGGESTION] Broaden or explicitly scope this SVG-prefix predicate. As written it accepts only BOM/whitespace followed by <?xml or <svg, so valid SVGs that begin with comments, DOCTYPE, or another XML prelude could fail despite the product contract that .svg files render.
Broaden looks_like_svg_xml to accept XML comment and DOCTYPE preludes in addition to <?xml and <svg. Document the supported prelude subset explicitly so legitimate SVG files starting with authoring-tool comments or DOCTYPE declarations are not rejected by the content-sanity gate. Add corresponding unit tests for both new prelude forms.
|
Thanks for the approve-with-nits @oz-for-oss. Pushed Suggestion (SVG prefix predicate broadening). Real concern: legitimate SVGs in the wild often start with authoring-tool comments ( Leaving the PR for human review at this point. The retrigger budget across these passes has been generous and substantive — thanks for the careful read on the threat model in particular (the FIFO open-blocks-before-fstat finding and the content-keyed-vs-extension-keyed SVG cap bypass were both real bugs in the spec that I would not have caught on my own). |
Description
Spec PR for #9729 (Image preview when clicking image files in the File Tree), which is labeled
ready-to-spec. Per the maintainer's guidance in the issue thread (@peicodes: "we think this should be implemented using the Lightbox component, which would open up a preview overlay over your pane with the image"), the spec routes File Tree image clicks through the existingLightboxoverlay rather than introducing a new tab variant.This PR adds:
specs/GH9729/product.md: product spec covering opening, navigation, dismiss/focus restoration, error handling, decoder limits, accessibility, multi-window behavior, and edge cases (hidden files, identity click, file-system mutation during preview).specs/GH9729/tech.md: tech spec grounded in realfile:linereferences, structured per the house convention (Context, Relevant code, Proposed changes, End-to-end flow, Risks and mitigations, Testing and validation, Follow-ups).Scope of the proposal
FileTarget::ImagePreviewvariant, plus a step-0 short-circuit inresolve_file_target_with_editor_choice(app/src/util/openable_file_type.rs) so PNG/JPEG/JPEG/GIF/WebP/SVG are captured ahead of both the markdown/code probe (so SVG no longer opens as raw XML) and the binary fall-through toSystemGeneric(so raster formats no longer hand off to the OS).Workspace::open_file_with_targetarm that scans sibling images via a newlist_sibling_images_natural_sortedhelper (capped atMAX_SIBLING_IMAGES = 1024, hidden-file filter mirrors the clicked file), builds aVec<LightboxImage>ofResolved { LocalFile { path } }entries, and dispatchesWorkspaceAction::OpenLightbox. The existing handler atapp/src/workspace/view.rs:21710-21737already handles "reuse the openLightboxViewviaupdate_params," so no handler change is needed.crates/warpui_core/src/image_cache.rs: introduceimage::Limits(max dimension, max alloc) plus an explicitMAX_DECODED_PIXELScap and anMAX_SVG_BYTESinput cap, so a malformed or maliciously oversize image cannot OOM the process viainto_rgba8()or animated-frame collection.LightboxImageSource::Error { message }variant incrates/ui_components/src/lightbox.rsso per-image read/decode failures render the filename inline rather than spinning forever (the artifacts call site atapp/src/ai/artifacts/mod.rs:362-365works around this today by stuffing "Failed to load" into the description; that workaround drops once the variant exists).What this spec deliberately does not propose
These are enumerated as Follow-ups in
tech.md:Cmd+=/-/0), trackpad pinch-zoom, click-drag pan.Adjacent issues
Linked Issue
Closes #9729 (spec-only; implementation will follow on a subsequent PR per the house spec-driven flow).
ready-to-specorready-to-implement.Screenshots / Videos
N/A; spec-only PR.
Testing
No code changes in this PR. The tech spec's Testing and validation section enumerates:
app/src/util/openable_file_type.rsfor the new resolver branch (each supported extension; precedence over markdown; non-image binaries stillSystemGeneric), the natural-sort helper, the hidden-file filter, and theMAX_SIBLING_IMAGEScap.crates/warpui_core/src/image_cache.rsfor the decoder-limit guard (huge dimensions rejected, normal photos accepted, garbage returnsUnrecognized, oversize SVG rejected).cargo fmt --check,cargo clippy --workspace --all-targets --tests -- -D warnings, andcargo nextest run --no-fail-fast --workspace --exclude command-signatures-v2.Agent Mode