Skip to content

FLOWS_TO/READS_FROM: path-insensitive walk drops taint killed on only one branch #713

Description

@vitali87

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions