Summary
Data-flow taint (FLOWS_TO) and I/O handle resolution (READS_FROM/WRITES_TO) are computed by a single flat, source-order walk with one live taint/handle map. There is no notion of control-flow branches, so a reassignment on one branch of an if/else kills taint globally, and taint introduced on one branch is treated as unconditional. Taint should be a MAY analysis (union across branches): keep taint that survives on any path, kill only when killed on all paths.
This causes both under-reporting (a real flow on the not-taken branch is dropped) and imprecise attribution.
Root cause (grounded)
FlowProcessor.process_flow_for_caller walks the body as a flat pre-order DFS with a single tainted: dict[str, frozenset[HandleBinding]] map (codebase_rag/parsers/flow_access/processor.py:141-167). Branch structure (if/elif/else, try/except, while) is not modeled — nodes are visited in raw source order.
_apply_assignment (processor.py:172-204) unconditionally retaints or kills (tainted.pop(lhs, None)) the target, regardless of the branch it sits in.
IOAccessProcessor.process_io_for_caller has the same source-order, non-path-sensitive handle map (codebase_rag/parsers/io_access/processor.py:74-79, 92-104).
- The current behavior is noted in-code at
flow_access/processor.py:138-140 ("a KILL on one branch drops taint conservatively").
Reproduction
import os
def f(cond):
x = os.getenv("K") # tainted
if cond:
x = "safe" # kill on ONE branch only
print(x) # real flow on the else-path: ENV::K -> STDOUT is MISSED
Source-order walk sees x = getenv, then x = "safe" (kill), then print(x) → x untainted → no ENV::K -> STDOUT edge, even though the else-path leaks.
Suggested direction
Model taint as a MAY (union) analysis over the block structure:
- Build (or traverse) a lightweight CFG / block tree for the body.
- Compute taint per block and union at merge points; a variable is killed only if reassigned to an untainted value on every incoming path.
- Handle resolution (io_access) should follow the same merge semantics.
This is inherently more work than the flat walk and needs care to avoid false positives (e.g. loop back-edges). A regression suite should cover: kill-on-one-branch (must still flow), taint-on-one-branch (must flow), and kill-on-all-branches (must NOT flow).
Summary
Data-flow taint (
FLOWS_TO) and I/O handle resolution (READS_FROM/WRITES_TO) are computed by a single flat, source-order walk with one live taint/handle map. There is no notion of control-flow branches, so a reassignment on one branch of anif/elsekills taint globally, and taint introduced on one branch is treated as unconditional. Taint should be a MAY analysis (union across branches): keep taint that survives on any path, kill only when killed on all paths.This causes both under-reporting (a real flow on the not-taken branch is dropped) and imprecise attribution.
Root cause (grounded)
FlowProcessor.process_flow_for_callerwalks the body as a flat pre-order DFS with a singletainted: dict[str, frozenset[HandleBinding]]map (codebase_rag/parsers/flow_access/processor.py:141-167). Branch structure (if/elif/else,try/except,while) is not modeled — nodes are visited in raw source order._apply_assignment(processor.py:172-204) unconditionally retaints or kills (tainted.pop(lhs, None)) the target, regardless of the branch it sits in.IOAccessProcessor.process_io_for_callerhas the same source-order, non-path-sensitive handle map (codebase_rag/parsers/io_access/processor.py:74-79, 92-104).flow_access/processor.py:138-140("a KILL on one branch drops taint conservatively").Reproduction
Source-order walk sees
x = getenv, thenx = "safe"(kill), thenprint(x)→xuntainted → noENV::K -> STDOUTedge, even though the else-path leaks.Suggested direction
Model taint as a MAY (union) analysis over the block structure:
This is inherently more work than the flat walk and needs care to avoid false positives (e.g. loop back-edges). A regression suite should cover: kill-on-one-branch (must still flow), taint-on-one-branch (must flow), and kill-on-all-branches (must NOT flow).