Skip to content

fix(sdg): wire actual_in ports from formal_in, not from CFG ENTRY (#220) - #221

Merged
rahlk merged 1 commit into
mainfrom
fix/220-sdg-formal-in-actual-in
Sep 16, 2026
Merged

rahlk merged 1 commit into
mainfrom
fix/220-sdg-formal-in-actual-in

Conversation

@rahlk

@rahlk rahlk commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #220.

build_actuals wired each actual_in port from the raw reaching-def source. For a
parameter that source is the synthetic CFG ENTRY node, so the graph got
ENTRY -> actual_in:N where it needed formal_in:N -> actual_in:N. build_formals
already had the correct remap; build_actuals did not use it.

Since resolve_value returns the formal_in vertex, every forward walk started at a
vertex with no path to the argument port and stopped inside the caller. taint()
therefore returned a clean exhausted for real flows.

Before / after on the #220 reproducer

1.5.3 this PR
slice_forward("request").total 4 21
flows_to_call False True
paths_between(request -> raw) [] one path, two call boundaries
taint(...).exhausted [('request','raw')] []

This matches what codeanalyzer-typescript 1.6.0 already answers for the identical fixture.

Regression test in test/test_dataflow_sdg.py fails on 1.5.3 and passes here.
Full suite: 520 passed, 6 skipped.

Also bumps to 1.5.4 with a CHANGELOG entry.

The SDG edges were correct: param_in carried actual_in -> formal_in and the
summary edges were present. The DDG feeding them was wired wrong on the way in.

Reaching-def analysis names the synthetic CFG ENTRY node as the definition site
of every parameter, capture and read global. build_formals already remapped that
source onto the matching formal_in vertex. build_actuals runs after it, allocates
the actual_in ports, and wired them straight from _defs_reaching_call_matching,
which returns the raw source. For a parameter that source is ENTRY, and nothing
remapped it, so the graph got ENTRY -> actual_in:N where it needed
formal_in:N -> actual_in:N. The return direction was already correct, and that
asymmetry is the tell.

Because resolve_value returns the formal_in vertex, a forward walk started at a
vertex with no path to the argument port, never crossed PARAM_IN, and stopped
inside the caller. slice_forward stayed in one function, flows_to_call and
flows_to_argument answered False, paths_between returned nothing, and taint()
reported a clean exhausted with complete=True and an empty ledger, which is a
refutation for a flow visible in three lines of source.

Extract the ENTRY-to-formal_in lookup into formal_for(var) on the assembler and
use it from both build_formals and build_actuals, for argument ports and
global-read ports. Fall back to the original source when the variable is not one
of the callable's formals, so no edge is lost.

On the reproducer in #220 a two-boundary flow now resolves end to end and taint
returns the witness. Regression test asserts an actual_in port carrying a
parameter is fed from formal_in and never from ENTRY; it fails on 1.5.3.

Full suite: 520 passed, 6 skipped.

Closes #220
@rahlk
rahlk merged commit 90cb3de into main Sep 16, 2026
@rahlk
rahlk deleted the fix/220-sdg-formal-in-actual-in branch September 16, 2026 05:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SDG: parameter passed as an argument is wired from CFG ENTRY instead of formal_in, breaking all interprocedural value flow

1 participant