diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f3a7ae..dab358e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ Versioning: [SemVer 2.0](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [3.2.4] — 2026-07-15 + +Fix (final page-size pass): after v3.2.3 the deployed `self-analysis.html` was +still ~4.9 MB — capping *program points* alone wasn't enough. The other flat +sections (Diagnostics ~1.7 MB, Call graph ~1.7 MB, Trap checks, Functions, gaps, +handle/float/pentagon facts) render one row per entry and scry-on-scry produces +thousands of each. + +### Fixed — scry-viz + +- **Every flat list/table section is now capped** (`SECTION_ROW_CAP` = 100 rows + + a "showing N of M" note): Diagnostics, Call graph (edge table), Trap checks + (POTENTIAL-TRAPs shown first so the cap never hides one), Functions, gaps, + handle faults, float facts, relational guards. +- **The call-graph diagram is skipped for large graphs** — a Mermaid source of + thousands of edges was itself ~1.7 MB. +- **New comprehensive test** `all_flat_sections_capped_page_stays_small` blows up + *every* section to 4,000 entries and asserts the page stays <1 MB — the check + that would have caught the v3.2.2/v3.2.3 partial fixes. The actionable subset of + every section remains available un-capped in the `guidance.json` feed. + ## [3.2.3] — 2026-07-15 Fix: **the v3.2.2 program-points cap was per-function only**, so scry-on-scry's diff --git a/Cargo.lock b/Cargo.lock index 19f329a..e53014c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1834,7 +1834,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scry-host-tests" -version = "3.2.3" +version = "3.2.4" dependencies = [ "anyhow", "jsonschema", @@ -1851,18 +1851,18 @@ dependencies = [ [[package]] name = "scry-sai-analyzer" -version = "3.2.3" +version = "3.2.4" dependencies = [ "scry-sai-core", ] [[package]] name = "scry-sai-bits" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-core" -version = "3.2.3" +version = "3.2.4" dependencies = [ "scry-sai-bits", "scry-sai-float", @@ -1880,19 +1880,19 @@ dependencies = [ [[package]] name = "scry-sai-float" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-handle" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-interval" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-lattice" -version = "3.2.3" +version = "3.2.4" dependencies = [ "bitflags", "scry-sai-octagon", @@ -1901,34 +1901,34 @@ dependencies = [ [[package]] name = "scry-sai-octagon" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-pentagon" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-poly" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-provenance" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-segment" -version = "3.2.3" +version = "3.2.4" dependencies = [ "scry-sai-interval", ] [[package]] name = "scry-sai-taint" -version = "3.2.3" +version = "3.2.4" [[package]] name = "scry-sai-viz" -version = "3.2.3" +version = "3.2.4" dependencies = [ "cpp_demangle 0.5.1", "rustc-demangle", diff --git a/Cargo.toml b/Cargo.toml index 7d386ec..8c9d7ff 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.3" +version = "3.2.4" 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 e31bfac..f87bd3a 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.3" } +scry-sai-interval = { path = "../scry-interval", version = "3.2.4" } # 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.3" } -scry-sai-provenance = { path = "../scry-provenance", version = "3.2.3" } +scry-sai-taint = { path = "../scry-taint", version = "3.2.4" } +scry-sai-provenance = { path = "../scry-provenance", version = "3.2.4" } # 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.3" } +scry-sai-octagon = { path = "../scry-octagon", version = "3.2.4" } # 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.3" } +scry-sai-bits = { path = "../scry-bits", version = "3.2.4" } # 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.3" } +scry-sai-pentagon = { path = "../scry-pentagon", version = "3.2.4" } # 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.3" } +scry-sai-float = { path = "../scry-float", version = "3.2.4" } # 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.3" } +scry-sai-handle = { path = "../scry-handle", version = "3.2.4" } # 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.3" } +scry-sai-segment = { path = "../scry-segment", version = "3.2.4" } [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 17e49df..ec8dc18 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.3"; +const SCRY_VERSION: &str = "3.2.4"; 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 ad41efd..80c8bc0 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.3" } +scry-sai-interval = { path = "../scry-interval", version = "3.2.4" } diff --git a/crates/scry-viz/Cargo.toml b/crates/scry-viz/Cargo.toml index e63b32b..59920d9 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.3" } +scry-sai-core = { path = "../scry-analyze-core", version = "3.2.4" } # 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 a90bc7c..422a3ac 100644 --- a/crates/scry-viz/src/lib.rs +++ b/crates/scry-viz/src/lib.rs @@ -44,6 +44,14 @@ const HERO_TITLE: &str = "scry — a sound static analyzer for WebAssembly"; /// points and a "… showing N of M" note. const POINTS_PER_FN_CAP: usize = 20; +/// Row cap for the flat list/table sections (Diagnostics, Call graph, Trap +/// checks, Functions, gaps, …). scry-on-scry produces thousands of rows in each +/// (Info diagnostics per bounds-check, 1349 call edges, thousands of trap +/// checks); un-capped they are megabytes of noise. Each section shows the first +/// `SECTION_ROW_CAP` rows and a "showing N of M" note; the actionable subset is +/// in the guidance.json feed. +const SECTION_ROW_CAP: usize = 100; + /// 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 @@ -383,7 +391,8 @@ fn render_functions(s: &mut String, r: &AnalysisResult) { .collect(); indices.sort_unstable(); indices.dedup(); - for idx in indices { + let n_funcs = indices.len(); + for idx in indices.into_iter().take(SECTION_ROW_CAP) { let meta = fn_meta(r, idx); let summary = r.function_summaries.iter().find(|f| f.func_index == idx); let stack: Option<&FunctionStack> = @@ -428,7 +437,9 @@ fn render_functions(s: &mut String, r: &AnalysisResult) { yesno(recursive), ); } - s.push_str(""); + s.push_str(""); + cap_note(s, n_funcs, "functions"); + s.push_str(""); } fn render_call_graph(s: &mut String, r: &AnalysisResult) { @@ -441,7 +452,7 @@ fn render_call_graph(s: &mut String, r: &AnalysisResult) { "
| caller | pc | kind | \resolved targets | soundness |
|---|
{} site(s) where scry degraded a function to \u{22a4} (gave up).
{} handle-lifetime fault(s).
{} sound float-interval fact(s).
if region.… showing {SECTION_ROW_CAP} of {total} {what}.
", + ); + } } /// FEAT-030: taint (noninterference) findings. Rendered only when there ARE @@ -1984,6 +2036,45 @@ mod tests { ); } + #[test] + fn all_flat_sections_capped_page_stays_small() { + // The lesson from v3.2.2/v3.2.3: capping ONE section is not enough — the + // deployed self-analysis was still ~5 MB from Diagnostics / Call graph / + // Trap checks. This test blows up EVERY large flat section and asserts + // the whole page stays under 1 MB, so a future uncapped section fails + // here instead of on the deployed site. + let mut r = analyze_wat( + "(module (memory 1) (func $h) \ + (func (export \"run\") (param i32) (result i32) \ + i32.const 4 i32.const 0 i32.store \ + call $h \ + i32.const 10 local.get 0 i32.div_s))", + ); + fn blow