Releases · 1.32.0

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.

No design record for this release. Unlike 1.30.0, 1.30.1 and 1.31.0, which each document a single fix in depth, 1.32.0 bundles seven distinct findings. There is no combined technical design record page for it; the per-finding depth lives in the seven advisories linked below, which carry the reproductions, the mechanism and the residuals.

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.

FindingWhat it was
Checks did not compose across a callGHSA-hcq6-x556-7r23Under @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-52wmA 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-mq3pAn 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-x9mjOn 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-5q2rA 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-x9crA 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-j7x9A 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.

Warnings still do not block. The information-flow default tier warns and the program still runs; it is @strict_ifc that turns a finding into a build-stopping error. The @constant_time checks are hard errors at every tier. Gate on strict mode in CI if you want the information-flow guarantee enforced.

Learn more

Keep going