Skip to content

Resolve the taint graph to a fixed point with sink-reachability pruning - #11951

Merged
danog merged 2 commits into
vimeo:masterfrom
danog:taint/fixed-point-resolution
Sep 15, 2026
Merged

danog merged 2 commits into
vimeo:masterfrom
danog:taint/fixed-point-resolution

Conversation

@danog

@danog danog commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Taint resolution previously ran a fixed 40-round BFS and stopped at a hard-coded nesting limit, so flows longer than 40 hops were silently missed and the round count was unrelated to convergence.

This runs resolution to a true fixed point instead:

  • Fixed-point loop — loop while sources and sinks remain, relying on the existing (id, taints) visited guard (whose state space is finite) to guarantee termination. Removes the resolution-depth limit entirely, so deep taint flows are no longer missed. Propagation semantics are otherwise unchanged.
  • Sink-reachability pruning — restrict resolution to the sub-graph from which a sink is actually reachable, via a backward pass from the sinks. A node from which no sink is reachable can never produce an issue, so propagating into it is wasted work; on real codebases this relevant sub-graph is tiny, which keeps running to convergence cheap. Specialized/unspecialized nodes are linked both by their unspecialized_id field and the id-string separator, so the prune never drops a live node.

Adds tests for a flow deeper than the old 40-hop cap (via assignments and via a chain of distinct specialized calls) and for a sanitized array value that must not be reported.

Part 1 of a 4-PR stack making taint resolution deterministic and correct; the others build on this one.

🤖 Generated with Claude Code

danog and others added 2 commits September 15, 2026 13:29
Taint resolution previously ran a fixed 40-round BFS and stopped at a
hard-coded nesting limit, so flows longer than 40 hops were silently
missed and the round count was unrelated to convergence.

Run to a true fixed point instead: loop while sources and sinks remain,
relying on the existing (id, taints) visited guard -- whose state space
is finite -- to guarantee termination. This removes the resolution-depth
limit entirely, so deep taint flows are no longer missed. Propagation
semantics are otherwise unchanged: each (id, taints) state is explored
exactly as before.

Adds tests for a flow deeper than the old 40-hop cap (via plain
assignments and via a chain of distinct specialized function calls) and
for a sanitized array value that must not be reported.

Co-Authored-By: Claude Opus 4.8 
Restrict resolution to the sub-graph from which a sink is actually
reachable, via a backward reachability pass from the sinks over the
forward edges: a node from which no sink is reachable can never produce
an issue, so propagating taint into it is wasted work. On real codebases
the full taint graph is huge while this relevant sub-graph is tiny,
which is what keeps running to convergence cheap.

Specialized and unspecialized nodes are linked both by their
authoritative unspecialized_id field and by the id-string separator, so
the prune never drops a node whose specialized or de-specialized form
can reach a sink (the resolution walk maps freely between the two).

Co-Authored-By: Claude Opus 4.8 
@danog danog added the release:internal The PR will be included in 'Internal changes' section of the release notes label Sep 15, 2026
@danog
danog merged commit 43068d3 into vimeo:master Sep 15, 2026
57 of 60 checks passed
danog added a commit to danog/psalm that referenced this pull request Sep 22, 2026
Resolve the taint graph to a fixed point with sink-reachability pruning
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:internal The PR will be included in 'Internal changes' section of the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant