What's new in Capa 1.30.1
A precision release. It removes two spurious information-flow reports on a read of a public sibling field of a struct whose other field was stored a secret. No leak is closed, so it ships as a PATCH and carries no security advisory. Nothing is added and nothing is removed; the only change accepts more programs, so it cannot break a 1.x program.
What this release is
Capa 1.30.1 is a precision fix, and only that. Two leak-free field-store shapes that 1.30.0 over-reported are now accepted at every tier. Because it removes false positives and closes no leak, it does not use the security exception the earlier information-flow releases used; it ships under the plain patch rule ("fixes a bug so the actual behaviour matches the documented behaviour"). The direction of the only change is accept-more, so no correct 1.x program can break.
Concretely, 1.30.0 flagged two cases where nothing actually leaked:
- A caller reading a public sibling field of a struct whose other field a callee stored a secret into. fill(bag, secret) does bag.secret_field = secret, then reading the clean bag.note raised a warning and, under @strict_ifc, a hard error.
- A closure re-reading a clean sibling field of a struct whose other field was stored into after the closure was defined. let f = fun() => box.note, then box.secret_field = secret, then f() flagged the sibling read.
What changed, in plain terms
Capa's information-flow checker tracks where a secret goes. When a secret is pushed into a container field of a struct, the checker already keyed that taint on an access path, the (root, field-path) pair naming exactly which part of the struct is dirty, so a read of a different field stayed clean. A plain field store, bag.secret_field = secret, did not get that precision: it raised the label of the whole struct, so any later read of that struct, including a disjoint public sibling, looked tainted.
Capa 1.30.1 gives a direct field store the same field sensitivity a container mutation already had: the store is keyed on the (root, field-path) it writes, so a read of a disjoint sibling path is no longer tainted by it. The keying applies only when the store is rooted directly at the binding within a tracked field-path bound; an aliased, renamed, or over-long root keeps the conservative whole-value carrier, so no cross-function whole-value flow loses coverage.
Before and after
Below, fill stores the secret into bag.secret_field. main then reads only the public bag.note, which never held the secret:
const TOKEN: @secret String = "s3cr3t"
type Bag { secret_field: String, note: String }
fun fill(bag: Bag, secret: @secret String)
bag.secret_field = secret
fun main(stdio: Stdio)
var bag: Bag = Bag { secret_field: "", note: "public" }
fill(bag, TOKEN)
stdio.println(bag.note)On 1.30.0 the read of the clean sibling raised a warning by default, and a hard error under @strict_ifc, though the program only ever prints public:
$ capa --check sibling.capa
sibling.capa:11:19: warning: information-flow: a @secret value reaches Stdio.println (argument 1), a public sink that sends data out of the program. Route it through declassify(value, reason: "...") if this disclosure is intended.
11 | stdio.println(bag.note)
^
sibling.capa: ok (4 items, 14 expressions typed, 7 bindings)
$ capa --run sibling.capa
publicOn 1.30.1 the disjoint sibling read is clean at every tier, and the program runs exactly as before, printing public on both backends:
$ capa --check sibling.capa
sibling.capa: ok (4 items, 14 expressions typed, 7 bindings)
$ capa --run sibling.capa # Python backend
public
$ capa --run --wasm sibling.capa # Wasm backend
publicAdding @strict_ifc() is the same story: 1.30.0 turned the sibling read into a build-stopping error; on 1.30.1 the strict build passes.
An honest scope
This is a precision fix. It removes false positives; it closes no leak. Every leak 1.30.0 flagged stays flagged on 1.30.1, a warning by default and a hard error under @strict_ifc, on both backends:
- A read of the stored field itself (bag.secret_field) stays flagged. Verified: on 1.30.1 that read still warns.
- A whole or getter read of the struct, and passing the whole struct to a callee that sinks the stored path, stay flagged.
- A store into an interior node read back through a descendant path stays flagged.
Two limits stay open, each disclosed rather than silent:
- A closure reading the captured struct through a whole or method receiver (a bare use, a method like bag.reveal(), or an argument) cannot be resolved to a field path, so it still observes every taint on the root and still over-reports a clean sibling. This is a sound over-report, never a missed leak; only the direct field read was made precise here.
- A struct held as a container value or element (a Map value or a List element, mutated through its own binding and read via .get(...)) is a pre-existing different-root points-to residual: it leaks on 1.30.0 and 1.30.1 alike and is not introduced here.
Learn more
- Technical design record : the deep engineering companion to this page, for contributors and auditors: the model, the mechanism in the analyzer with file and line references, the alternatives rejected, how it was verified, and every residual.
- Advisory: cross-function whole-struct read : where the cross-function field-store sibling over-report is disclosed and now recorded as resolved.
- Advisory: lambda-flow sensitivity : where the closure clean-sibling over-report is disclosed and now recorded as resolved for the direct field read.
- Information-flow control : the chapter that introduces @secret, the sinks, and declassify.
- CHANGELOG : the machine-readable entry for this release.