forked from 1b-agent/opencadstudio-upstream
web: fix red-team breaks in restore, drop-open, and plot paths
Three attack agents (state-machine tracing, executable input fuzzing,
browser-API review) found two breaks and several degradations; all are
fixed here, with the fuzzer's 22 tests kept as regression suites.
Breaks:
- Untitled tabs (and dot-names like ".dxf") autosave as DXF, but the
restore name defaulted to .dwg — the worker dispatches its reader by
the name's extension, so the only copy of unsaved work could never
be restored and was re-offered forever. recovered_name now uses the
worker's rsplit('.') convention and defaults extensionless names to
dxf, matching new-document serialization.
- " (recovered)" stacked one suffix per crash-restore cycle; the
marker is now stripped idempotently before appending.
- Opening a file (Ctrl+O / OPEN / automation) while a web open was in
flight clobbered it: the in-flight open's bytes were lost and every
queued restore prompt stalled for the session. on_open_file now
refuses while an open is running (web only).
Hardening:
- Escape on the Recover Drawing modal defers instead of deleting the
only snapshot; the Discard button still removes it.
- A cap-rejected restore enqueue warns in-app and re-offers the next
snapshot; the drop queue cap rises to 32.
- Exiting mid-round no longer races snapshot removals (deferrals are
drained, the sweep skips while a round is in flight); closing a tab
with "Don't save" now also removes its web snapshot; the snapshot
key derivation is shared via web_autosave_key.
- The plot-preview overlay no longer leaks a full PDF copy per preview
(the Download closure lives in a replaceable registry).
- A corrupt autosave index.json fails the round/boot instead of
silently resetting every row; plot pages with non-finite or non-
positive sheet dimensions error instead of emitting a NaN MediaBox.
- The drop filter accepts dot-less "DWG"/"dxf" names the worker
parses (same rsplit convention on both gates).
(cherry picked from commit 3eab00bfbf)
Ported onto main (#1717 review): the five standalone tests/*_redteam.rs
files stay out — no test target builds them and they exercised copies of
the functions rather than the code itself.
This commit is contained in:
1 parent
9611f40400
commit
69fa7cae4f
30 files changed
+331
-76
No files matched your search
+29
-5
@@ -454,9 +454,11 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec<u8>], tot
|
||||
let Some(document) = window.document() else {
|
||||
return;
|
||||
};
|
||||
// One preview at a time: a replotted preview replaces its predecessor and
|
||||
// revokes its blob URLs, so repeated previews cannot stack overlays or
|
||||
// leak the images underneath.
|
||||
// One preview at a time: a replotted preview replaces its predecessor,
|
||||
// revokes its blob URLs and DROPS its Download closure (freeing the full
|
||||
// PDF copy the closure captured — a `forget()` would leak one per
|
||||
// preview), so repeated previews cannot stack overlays or leak the
|
||||
// images underneath.
|
||||
const OVERLAY_ID: &str = "ocs-plot-preview";
|
||||
if let Some(previous) = document.get_element_by_id(OVERLAY_ID) {
|
||||
previous.remove();
|
||||
@@ -468,6 +470,11 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec<u8>], tot
|
||||
{
|
||||
let _ = web_sys::Url::revoke_object_url(&url);
|
||||
}
|
||||
// Take the previous closure out so it destructs BEFORE the new preview
|
||||
// stores its own: removing the overlay element already released the JS
|
||||
// handler reference, so this drop is the last one and actually frees
|
||||
// the captured bytes.
|
||||
drop(PREVIEW_DOWNLOAD_CLOSURE.with(|slot| slot.borrow_mut().take()));
|
||||
if page_pngs.is_empty() {
|
||||
// Rasterisation produced nothing: deliver the PDF itself rather than
|
||||
// show an empty frame.
|
||||
@@ -570,13 +577,16 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec<u8>], tot
|
||||
let name = name.to_string();
|
||||
let bytes = pdf_bytes.to_vec();
|
||||
// A plain FnMut stays usable for every click; the one-shot variant
|
||||
// throws on the second click, and users click Download twice.
|
||||
// throws on the second click, and users click Download twice. The
|
||||
// closure is HELD in a registry (not `forget()`ed): replacing or
|
||||
// closing the overlay drops it and frees the captured PDF copy,
|
||||
// while the cloned JS function keeps the live handler working.
|
||||
let closure: wasm_bindgen::closure::Closure<dyn FnMut()> =
|
||||
wasm_bindgen::closure::Closure::new(move || {
|
||||
download_bytes(&name, &bytes);
|
||||
});
|
||||
let click: js_sys::Function = closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
|
||||
closure.forget();
|
||||
PREVIEW_DOWNLOAD_CLOSURE.with(|slot| *slot.borrow_mut() = Some(closure));
|
||||
dl.set_onclick(Some(&click));
|
||||
bar.append_child(&dl).ok();
|
||||
}
|
||||
@@ -612,6 +622,20 @@ fn preview_urls() -> &'static std::sync::Mutex<Vec<String>> {
|
||||
URLS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
|
||||
}
|
||||
|
||||
// The live preview overlay's Download-button closure. The closure captures
|
||||
// a full copy of the PDF bytes, so `forget()`-ing it would leak one PDF per
|
||||
// preview; holding it here lets the next `show_pdf_preview` drop and free
|
||||
// the previous one (the overlay element's removal already released the JS
|
||||
// handler reference). A thread-local `RefCell` rather than a static
|
||||
// `Mutex` like `preview_urls`: `Closure` is not `Send`, and the overlay
|
||||
// only ever lives on the one UI thread.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
thread_local! {
|
||||
static PREVIEW_DOWNLOAD_CLOSURE:
|
||||
std::cell::RefCell<Option<wasm_bindgen::closure::Closure<dyn FnMut()>>> =
|
||||
std::cell::RefCell::new(None);
|
||||
}
|
||||
|
||||
/// Short platform string for bug reports: OS + architecture on the desktop,
|
||||
/// the browser user-agent on the web.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
Reference in new issue
Block a user