What's new in Capa 1.32.0
A security-hardening release. It closes seven separate soundness gaps found in an audit of the whole compiler and in the hardening pass that followed 1.31.0, each published as its own advisory. Five are information-flow or resource-discipline fixes in the analyzer; two are backend fixes, one of which restored the byte-identical-output promise between the two backends.
What this release is
Capa 1.32.0 is a security-hardening release. Every fix in it tightens a static check or a code generator, so a program that previously compiled (or, on Wasm, compiled to a wrong branch) is now rejected or compiled correctly. Only programs that were already unsound are affected; a correct program compiles exactly as before.
Each of the seven is claimed under the project's stability policy as a security exception and ships as a minor bump rather than a major one, matching every prior soundness fix on the 1.x line. Alongside them the release carries a modularization and test-net effort with no behaviour change, and a move to hash-pinned dependency installs for development and CI. That last one is a build-level property, not a runtime one: the published package stays dependency-free, so it does not change what a downstream consumer installs or runs.
The seven findings
In the release's own order. The severity and the honest scope of each are stated in its advisory; the one-line summaries here are deliberately not a substitute for reading it.
| Finding | What it was |
|---|---|
| Checks did not compose across a callGHSA-hcq6-x556-7r23 | Under @strict_ifc the implicit-flow rule caught a public sink written inline in a secret-conditioned branch, but not one reached through a helper call, so the branch bit leaked. The same blind spot let a secret reach a variable-time operation inside an un-annotated helper under @constant_time, a timing oracle. Both are now flagged at the call site: a hard error under @strict_ifc for the first, and at every tier for the second. |
| Destructuring could launder a secretGHSA-7pf3-h2cq-52wm | A struct-destructuring pattern bound each field with the type it has in the pattern's struct, never the value's, and the pattern's struct name was never checked against the value's type. A public twin of a secret-carrying struct therefore rebound the secret field under a public label. Closed in four faces, including one that leaked silently even under @strict_ifc. |
| match divergence did not raise the pcGHSA-w9m5-7pcc-mq3p | An incomplete earlier fix. A 1.4.0 advisory raised the block's program-counter label after a secret-conditioned divergence for if / while / for, but overlooked match, so a divergence inside a secret-scrutinee arm followed by a public sink leaked the predicate bit. Strict-tier only; no default-tier or output change. |
| Wasm match read dangling memoryGHSA-22qh-gw83-x9mj | On the Wasm backend a pattern string literal appearing nowhere else in the module was interned with no backing data, so the comparison ran against undefined bytes: usually a silent wrong branch, occasionally a spurious match that bypasses an authorization gate. The module still passed wasm-tools validate. The Python backends were never affected, which is what made it dangerous. |
| Double discharge of a borrowed valueGHSA-8q38-5qm9-5q2r | A borrowed (non-consume) linear or typestate parameter was tracked as owned, so returning, aliasing, packing or transitioning it discharged it a second time while the caller still owned it: a double-free with no diagnostic. A cap-bearing struct used after consume was likewise undiagnosed. |
| Laundering through the return channelGHSA-fwph-gx2p-x9cr | A secret introduced inside a callee and returned to the caller, through a field store into a local, a deep field chain, a loop or destructuring binder, or a locally-resolved lambda's result, was not tracked at the field precision the sink check needs, so it reached a public sink clean at both tiers and on both backends. |
| A name shadow diverged between backendsGHSA-ppp5-344c-j7x9 | A binding shadowing an enclosing binding or a module-level const passed --check yet compiled to different behaviour on the two backends (function scope versus lexical capture), so a captured secret was disclosed on one backend while the other crashed or bound a different value. This one broke the byte-identical-output promise directly; the analyzer now rejects the diverging shadows and the lowerer compiles a legal shadow identically on both. |
What ships knowingly unfixed
Two residuals were disclosed as open at the time of the release rather than left silent, each pinned by a test. A trait-typed downcast destructure (s: Shape then let Circle { r } = s) resolved to a trait rather than a struct, so a public-twin downcast was not caught on the Python backend; it was largely closed after 1.32.0, with two root-cause residuals still open. And the --ir backend raised NotImplementedError on struct and or-patterns under --run --ir, where the Wasm backend and the legacy transpiler handled both; that has since been fixed.
Learn more
- The seven security advisories : each dated 2026-08-22, with the severity, the reproduction, and the complete list of residuals and over-reports.
- CHANGELOG : the machine-readable entry for this release, including the internal and no-behaviour-change work.
- Trust model : what the toolchain verifies, what it trusts, and what is outside the threat model.
- Information-flow control : the chapter behind five of the seven findings.