From 0cde6e2d9d36b4e50b2310028beb55882734b993 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 15 Jul 2026 05:47:40 +0200 Subject: [PATCH] =?UTF-8?q?release:=20v3.2.3=20=E2=80=94=20fix=20program-p?= =?UTF-8?q?oints=20cap=20(per-function=20was=20not=20enough)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v3.2.2 cap limited points to 20 PER FUNCTION, but scry-on-scry has ~800 functions, so self-analysis.html was still ~10 MB (deployed page confirmed). The per-function cap alone doesn't bound the page. scry-viz: render a cheap one-row summary for EVERY function + detailed per-point tables only for the first FUNCS_WITH_DETAIL_CAP (12) functions → page stays <1 MB regardless of function count. Page-size test rewritten to synthesize points across 500 functions (not just many points in one). Corrected an inaccurate cap note (per-point data is the library AnalysisResult, not guidance.json). Version 3.2.2 → 3.2.3 + Cargo.lock + CHANGELOG. 24 viz tests, clippy + fmt clean. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 16 +++++ Cargo.lock | 30 ++++----- Cargo.toml | 2 +- crates/scry-analyze-core/Cargo.toml | 18 +++--- crates/scry-analyze-core/src/lib.rs | 2 +- crates/scry-segment/Cargo.toml | 2 +- crates/scry-viz/Cargo.toml | 2 +- crates/scry-viz/src/lib.rs | 97 +++++++++++++++++++---------- 8 files changed, 108 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5845721..3f3a7ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ Versioning: [SemVer 2.0](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [3.2.3] — 2026-07-15 + +Fix: **the v3.2.2 program-points cap was per-function only**, so scry-on-scry's +~800 functions still produced a ~10 MB `self-analysis.html` (800 × 20 rows). + +### Fixed — scry-viz + +- Program points now render a **cheap one-row summary for every function**, and a + detailed per-point table only for the first `FUNCS_WITH_DETAIL_CAP` (12) + functions — so the page stays well under 1 MB regardless of function count. +- The page-size test now synthesizes points across **many functions** (500 × 30), + not just many points in one function, so this regression is caught. +- Corrected an inaccurate cap note: the capped per-point rows are scry's library + `AnalysisResult` output, not the `guidance.json` feed (which carries advisories + + trap verdicts, not per-point invariants). + ## [3.2.2] — 2026-07-15 Headline: **make the dashboard readable.** A four-persona review of the deployed diff --git a/Cargo.lock b/Cargo.lock index 809118e..19f329a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1834,7 +1834,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scry-host-tests" -version = "3.2.2" +version = "3.2.3" dependencies = [ "anyhow", "jsonschema", @@ -1851,18 +1851,18 @@ dependencies = [ [[package]] name = "scry-sai-analyzer" -version = "3.2.2" +version = "3.2.3" dependencies = [ "scry-sai-core", ] [[package]] name = "scry-sai-bits" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-core" -version = "3.2.2" +version = "3.2.3" dependencies = [ "scry-sai-bits", "scry-sai-float", @@ -1880,19 +1880,19 @@ dependencies = [ [[package]] name = "scry-sai-float" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-handle" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-interval" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-lattice" -version = "3.2.2" +version = "3.2.3" dependencies = [ "bitflags", "scry-sai-octagon", @@ -1901,34 +1901,34 @@ dependencies = [ [[package]] name = "scry-sai-octagon" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-pentagon" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-poly" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-provenance" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-segment" -version = "3.2.2" +version = "3.2.3" dependencies = [ "scry-sai-interval", ] [[package]] name = "scry-sai-taint" -version = "3.2.2" +version = "3.2.3" [[package]] name = "scry-sai-viz" -version = "3.2.2" +version = "3.2.3" dependencies = [ "cpp_demangle 0.5.1", "rustc-demangle", diff --git a/Cargo.toml b/Cargo.toml index 302c2a3..7d386ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ default-members = [ # on crates.io matches the release artifacts. The crates.io publish workflow # asserts the pushed `v*` tag equals this version, so a release bump must move # both in lockstep (and the internal path-dep `version = "..."` fields below). -version = "3.2.2" +version = "3.2.3" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/pulseengine/scry" diff --git a/crates/scry-analyze-core/Cargo.toml b/crates/scry-analyze-core/Cargo.toml index e6d88dc..e31bfac 100644 --- a/crates/scry-analyze-core/Cargo.toml +++ b/crates/scry-analyze-core/Cargo.toml @@ -31,7 +31,7 @@ path = "src/lib.rs" # Path deps carry `version` so `cargo publish` rewrites them to the crates.io # coordinate (crates.io rejects path-only deps). The version equals the # workspace version and must be bumped in lockstep with it. -scry-sai-interval = { path = "../scry-interval", version = "3.2.2" } +scry-sai-interval = { path = "../scry-interval", version = "3.2.3" } # Step 2 (DD-012): the analyze body + helpers moved here. wasmparser parses # the input Wasm Core Model module; sha2 digests the module bytes for @@ -43,44 +43,44 @@ sha2 = { workspace = true } # Security-label (taint) lattice for the noninterference analysis (FEAT-009) # and the pure meld<->scry provenance boundary crate (FEAT-002 / DD-002). -scry-sai-taint = { path = "../scry-taint", version = "3.2.2" } -scry-sai-provenance = { path = "../scry-provenance", version = "3.2.2" } +scry-sai-taint = { path = "../scry-taint", version = "3.2.3" } +scry-sai-provenance = { path = "../scry-provenance", version = "3.2.3" } # Octagon relational domain (FEAT-016 slice-2b-ii): carried alongside the # intervals through the structured-CFG fixpoint so a loop counter bounded by a # VARIABLE relation (`i < n`) stays bounded where the interval domain alone # widens it to ⊤. Same pure `#![no_std]` dual-compile crate as scry-interval. -scry-sai-octagon = { path = "../scry-octagon", version = "3.2.2" } +scry-sai-octagon = { path = "../scry-octagon", version = "3.2.3" } # Known-bits × interval-guarded congruence reduced product (FEAT-037 / DD-017): # an additive bit/alignment/stride companion computed in a straight-line-sound # pass, surfaced library-only on `AnalysisResult.bit_facts`. Same pure # `#![no_std]` dual-compile crate as the other domains. -scry-sai-bits = { path = "../scry-bits", version = "3.2.2" } +scry-sai-bits = { path = "../scry-bits", version = "3.2.3" } # Pentagons weakly-relational domain (FEAT-044 / AC-014): intervals + strict # `x < y` facts, the cheap relational layer behind sound out-of-bounds-trap # detection (FEAT-046). An additive guard-recording pass surfaces proven # strict relations library-only on `AnalysisResult.pentagon_facts`. Same pure # `#![no_std]` dual-compile crate as the other domains. -scry-sai-pentagon = { path = "../scry-pentagon", version = "3.2.2" } +scry-sai-pentagon = { path = "../scry-pentagon", version = "3.2.3" } # IEEE-754 float-interval domain (FEAT-047 / AC-022): sound f32/f64 abstraction # with NaN/±inf tracking + round-to-nearest-aware widening. An additive # straight-line pass surfaces sound float intervals library-only on # `AnalysisResult.float_facts`. Same pure `#![no_std]` dual-compile crate. -scry-sai-float = { path = "../scry-float", version = "3.2.2" } +scry-sai-float = { path = "../scry-float", version = "3.2.3" } # Affine Component-Model handle-state lattice (FEAT-049 / MF-007): tracks # own/borrow resource-handle state to flag use-after-drop / double-drop. A # straight-line pass over the canonical-ABI `[resource-drop]` call sites # surfaces findings library-only on `AnalysisResult.handle_findings`. -scry-sai-handle = { path = "../scry-handle", version = "3.2.2" } +scry-sai-handle = { path = "../scry-handle", version = "3.2.3" } # FEAT-058: the linear-memory segmentation domain (content-sensitive memory). # The interpreter tracks per-offset interval content for i32 loads/stores # instead of degrading every load to ⊤. -scry-sai-segment = { path = "../scry-segment", version = "3.2.2" } +scry-sai-segment = { path = "../scry-segment", version = "3.2.3" } [dev-dependencies] # Test-only (the crate is otherwise dep-light + no_std): assemble the .wat diff --git a/crates/scry-analyze-core/src/lib.rs b/crates/scry-analyze-core/src/lib.rs index 2fe9229..17e49df 100644 --- a/crates/scry-analyze-core/src/lib.rs +++ b/crates/scry-analyze-core/src/lib.rs @@ -856,7 +856,7 @@ mod domain { scry_taint::join(a, b) } } -const SCRY_VERSION: &str = "3.2.2"; +const SCRY_VERSION: &str = "3.2.3"; const INVARIANT_SCHEMA_URL: &str = "https://pulseengine.eu/scry-invariants/v1"; /// Default Wasm linear-memory page size (64 KiB). diff --git a/crates/scry-segment/Cargo.toml b/crates/scry-segment/Cargo.toml index d9c5ae2..ad41efd 100644 --- a/crates/scry-segment/Cargo.toml +++ b/crates/scry-segment/Cargo.toml @@ -20,4 +20,4 @@ path = "src/lib.rs" # The per-segment content domain. Path dep carries `version` so `cargo publish` # rewrites it to the crates.io coordinate; the version equals the workspace # version and is bumped in lockstep. -scry-sai-interval = { path = "../scry-interval", version = "3.2.2" } +scry-sai-interval = { path = "../scry-interval", version = "3.2.3" } diff --git a/crates/scry-viz/Cargo.toml b/crates/scry-viz/Cargo.toml index 84c6a7c..e63b32b 100644 --- a/crates/scry-viz/Cargo.toml +++ b/crates/scry-viz/Cargo.toml @@ -22,7 +22,7 @@ path = "src/main.rs" # The only dependency: the published analyzer library. scry-viz is a plain # `std` host tool, so it can read the `AnalysisResult` plain-Rust types and # render them — no WIT, no component, no wasmtime. -scry-sai-core = { path = "../scry-analyze-core", version = "3.2.2" } +scry-sai-core = { path = "../scry-analyze-core", version = "3.2.3" } # Assemble `.wat` inputs to module bytes (so the CLI accepts both .wat and # .wasm); host-only, same dep the test harness uses. wat = { workspace = true } diff --git a/crates/scry-viz/src/lib.rs b/crates/scry-viz/src/lib.rs index 33d39d1..a90bc7c 100644 --- a/crates/scry-viz/src/lib.rs +++ b/crates/scry-viz/src/lib.rs @@ -39,12 +39,19 @@ use scry_analyze_core::{ /// dashboard"). Used in both the `` and the `<h1>`. const HERO_TITLE: &str = "scry — a sound static analyzer for WebAssembly"; -/// Program-points cap: the number of per-function points rendered inline. Above -/// this, a function shows its first `POINTS_PER_FN_CAP` points and a "… showing -/// N of M; full data in the JSON feed" note. This keeps the whole HTML small -/// (the un-capped points section is ~90% of the bytes on scry-on-scry). +/// Program-points cap: the number of per-function points rendered inline in a +/// detailed table. Above this, a function shows its first `POINTS_PER_FN_CAP` +/// points and a "… showing N of M" note. const POINTS_PER_FN_CAP: usize = 20; +/// Cap on the number of FUNCTIONS rendered with a detailed per-point table. +/// scry-on-scry has ~800 functions, so a per-function cap alone still yields a +/// multi-MB dump (800 × 20 rows). All four persona reviews found the raw +/// per-point dump is noise for readers, so every function appears in a cheap +/// one-row summary and only the first `FUNCS_WITH_DETAIL_CAP` get a detailed +/// table; the full per-point invariants are scry's library `AnalysisResult`. +const FUNCS_WITH_DETAIL_CAP: usize = 12; + /// Guidance cap: for every advisory class EXCEPT `DefiniteFault` (always shown /// in full — proven bugs), render at most this many rows, then a "… and N more" /// line. Faults are never elided. @@ -1110,15 +1117,37 @@ fn render_points(s: &mut String, r: &AnalysisResult) { let mut func_indices: Vec<u32> = points.iter().map(|p| p.func_index).collect(); func_indices.sort_unstable(); func_indices.dedup(); - for idx in func_indices { + let n_funcs = func_indices.len(); + + // Compact summary over EVERY function (one cheap row each — small even for + // scry-on-scry's ~800 functions). The detailed per-point tables below are + // capped by function count AND points/function, or the section is a dump. + let _ = write!( + s, + "<p class=\"muted\">{n_funcs} function(s) with program points — summary below; \ + detailed per-point invariants for the first {FUNCS_WITH_DETAIL_CAP}. The full \ + per-point data is scry's library <code>AnalysisResult</code>; the actionable \ + subset is in <code>guidance.json</code>.</p>\ + <table><thead><tr><th>function</th><th>points</th><th>max locals</th>\ + </tr></thead><tbody>", + ); + for &idx in &func_indices { + let fp = points.iter().filter(|p| p.func_index == idx); + let count = fp.clone().count(); + let nloc = fp.map(|p| p.locals.len()).max().unwrap_or(0); + let _ = write!( + s, + "<tr><td>{}</td><td>{count}</td><td>{nloc}</td></tr>", + fn_link(r, idx), + ); + } + s.push_str("</tbody></table>"); + + for &idx in func_indices.iter().take(FUNCS_WITH_DETAIL_CAP) { let heading = match fn_meta(r, idx).and_then(|m| m.name.as_deref()) { Some(n) => format!("func {idx} · {}", name_span(&demangle(n))), None => format!("func {idx}"), }; - // Per-function summary + cap. The un-capped points section is ~90% of - // the bytes on scry-on-scry (16k+ points), so we render a SUMMARY - // (name, #points, #locals) and only the first `POINTS_PER_FN_CAP` - // points per function; the full per-point data lives in the JSON feed. let fn_points: Vec<_> = points.iter().filter(|p| p.func_index == idx).collect(); let n_points = fn_points.len(); let n_locals = fn_points.iter().map(|p| p.locals.len()).max().unwrap_or(0); @@ -1189,8 +1218,8 @@ fn render_points(s: &mut String, r: &AnalysisResult) { if n_points > POINTS_PER_FN_CAP { let _ = write!( s, - "<p class=\"muted\">… showing {} of {} points; full data in the JSON feed \ - (<code>guidance.json</code>).</p>", + "<p class=\"muted\">… showing {} of {} points for this function \ + (full per-point invariants are scry's library output).</p>", POINTS_PER_FN_CAP, n_points, ); } @@ -1914,41 +1943,43 @@ mod tests { #[test] fn points_section_is_capped_and_page_stays_small() { - // Page-size sanity: a function with far more than the cap of program - // points must render only the first `POINTS_PER_FN_CAP`, plus a summary - // and a "showing N of M" note — and the whole page must stay well under - // 1 MB. We synthesize a large point set on a real result (the analyzer's - // own point count depends on its fixpoint, so we don't rely on it). + // Page-size sanity: scry-on-scry has ~800 FUNCTIONS each with points, so + // a per-function cap alone is not enough (800 × 20 rows is still ~10 MB). + // Synthesize points across many functions and assert (a) only the first + // `FUNCS_WITH_DETAIL_CAP` functions get a detailed table, and (b) the + // whole page stays well under 1 MB. let mut r = analyze_wat( "(module (func (export \"run\") (result i32) i32.const 42 i32.const 7 i32.add))", ); let template = r.invariants.points[0].clone(); r.invariants.points.clear(); - for pc in 0..3000u32 { - let mut p = template.clone(); - p.func_index = 0; - p.pc = pc; - r.invariants.points.push(p); + const FUNCS: u32 = 500; + const PTS_PER_FN: u32 = 30; + for f in 0..FUNCS { + for pc in 0..PTS_PER_FN { + let mut p = template.clone(); + p.func_index = f; + p.pc = pc; + r.invariants.points.push(p); + } } - let total = r.invariants.points.len(); - assert!( - total > POINTS_PER_FN_CAP * 5, - "fixture must produce many points (got {total})" - ); + assert_eq!(r.invariants.points.len() as u32, FUNCS * PTS_PER_FN); let html = render_html(&r, "big"); - // The cap note is present … + // Every function appears in the cheap summary line … assert!( - html.contains("full data in the JSON feed"), - "capped points note present" + html.contains("function(s) with program points"), + "per-function summary present" ); + // … but only the first FUNCS_WITH_DETAIL_CAP get a detailed table. + let detailed = html.matches("class=\"fn-points\"").count(); assert!( - html.contains("program point(s)"), - "per-function summary line" + detailed <= FUNCS_WITH_DETAIL_CAP, + "detailed tables capped at {FUNCS_WITH_DETAIL_CAP}, got {detailed}" ); - // … and the page is small despite thousands of points. + // … and the whole page stays under 1 MB despite 15k points / 500 funcs. assert!( html.len() < 1_000_000, - "page must stay under 1 MB even for many points; was {} bytes", + "page must stay under 1 MB across many functions; was {} bytes", html.len() ); }