Browse / github.com/storytold/wordcraft / patch
#3612: clamp the object drag preview texture to the GPU limit
[N-02] Object drag preview can create a texture larger than the GPU limit, and wgpu panics outside the command guard Reported from a read-only security and robustness audit (AUDIT.md, findings N-02) of WordCraft at eae2421, re-checked against upstream main [`00bc6a4b`](https://github.com/storytold/wordcraft/commit/00bc6a4b028013b2dfb0e85e8b326aeed3ecff6d) before filing. **Severity:** High (crash of the UI thread). **Status:** Confirmed in code and dependency source at eae2421 (not executed); the code is unchanged at `00bc6a4b`, and the dependency versions are the same (`egui-wgpu` 0.36.2, `wgpu` 30.0.1 in `Cargo.lock`). Related: #2772 (same wgpu sink, reached through `ui.resize`). ## Evidence The
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #4578, open)
- Base
2cf1d6c7ae94723a45bd6e7e8c884fe375148750- Size
- +26 −1 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
78fc8ad2f75d5d6a9f99ff9d
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/objects.rs crates/ui-egui/src/objects/tests.rs
@@ -513,6 +513,14 @@ fn shows_pixels(d: &ObjectDrag) -> bool { (d.picture && d.object.spin.is_identity()) || d.drag.grab == Grab::Move || (d.drag.grab == Grab::Rotate && d.object.text_box.is_none()) } +/// The pixels-per-page-point for a `w` x `h` preview, lowered so neither side exceeds the GPU's+/// `max_side` (the texture is painted into the object's rect, so a smaller one is only stretched).+fn preview_scale(px: f32, w: f32, h: f32, max_side: usize) -> f32 {+ let room = (max_side.min(wordcraft_render::MAX_SIDE as usize).max(2) - 1) as f32;+ let px = if px.is_finite() { px } else { 1.0 };+ px.min(room / w.max(h).max(1.0))+}+ /// `o` drawn alone over its rotated bounds, as a texture, at the canvas's `scale` (screen points /// per page point). fn preview(app: &WordApp, ctx: &egui::Context, layout: &DocLayout, o: &ObjectHit, scale: f32) -> Option<egui::TextureHandle> {@@ -522,7 +530,13 @@ fn preview(app: &WordApp, ctx: &egui::Context, layout: &DocLayout, o: &ObjectHit opts.dark = app.session.view.dark_mode; let items = wordcraft_layout::display::object_display(&app.session.doc, page, obj, o.rect, o.text_box, &opts.display); let b = o.bounds();- let img = wordcraft_render::render_items(&app.session.doc, &items, b.x, b.y, b.w, b.h, scale * ctx.pixels_per_point(), &opts);+ let max_side = ctx.input(|i| i.max_texture_side);+ let px = preview_scale(scale * ctx.pixels_per_point(), b.w, b.h, max_side);+ let img = wordcraft_render::render_items(&app.session.doc, &items, b.x, b.y, b.w, b.h, px, &opts);+ // A texture over the device limit panics wgpu outside `Session::run`; fall back to the outline.+ if img.width as usize > max_side || img.height as usize > max_side {+ return None;+ } let ci = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_straight()); Some(ctx.load_texture("object-drag", ci, egui::TextureOptions::LINEAR)) }@@ -659,3 +659,14 @@ fn rotation_drag_keeps_text_boxes_outline_only_until_their_text_can_rotate() { assert_eq!(selected(h.state()).unwrap().1.frame().unwrap().2.rot, 90.0); assert_eq!(h.state().session.undo_depth(), depth + 1); }++#[test]+fn drag_preview_scale_stays_within_the_gpu_texture_limit() {+ // A 14,400 pt picture at 2x on a GPU limited to 8,192 px.+ let px = preview_scale(2.0, 14_400.0, 100.0, 8192);+ assert!((14_400.0f32 * px).ceil() <= 8192.0);+ assert_eq!(preview_scale(2.0, 100.0, 50.0, 8192), 2.0);+ for bad in [f32::NAN, f32::INFINITY, 0.0] {+ assert!((100.0f32 * preview_scale(bad, 100.0, 100.0, 0)).ceil() <= 2.0);+ }+}