Releases · 1.30.1

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:

sibling.capa
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 1.30.0
$ 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
public

On 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 1.30.1
$ 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
public

Adding @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.
No advisory. Because this release closes no leak, it ships with no security advisory. The two 2026-08-10 advisories that disclosed these shapes as sound over-reports now record them as resolved (for the direct field-read case); the machine-readable entry is in the CHANGELOG.

Learn more

Keep going