Skip to content

Python: cache exposed shared SSA adjacency relations - #22421

Draft
yoff wants to merge 5 commits into
github:yoff/python-shared-cfg-dataflow-flipfrom
yoff:yoff-python-stage-amplification
Draft

yoff wants to merge 5 commits into
github:yoff/python-shared-cfg-dataflow-flipfrom
yoff:yoff-python-stage-amplification

Conversation

@yoff

@yoff yoff commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

The shared SSA implementation documents that language adapters must cache predicates they expose. Python's shared-SSA adapter exposed AdjacentUses::firstUse, adjacentUseUse, and useOfDef without restoring that cache boundary, unlike the legacy ESSA implementation.

This two-commit dependent draft:

  1. adds a passing semantic contract diagnostic for all three exposed relations, including exact projection/expansion mismatch counts;
  2. adds cached to the Python AdjacentUses module and its three public predicates.

The diagnostic intentionally has no MISSING/SPURIOUS annotation and no wall-time assertion. The defect is duplicate evaluator specialization with identical semantic output, so relation equality is the stable red-state equivalent and timing would be machine-dependent.

Why the adapter cache

On exact historical Salt, the same 6,313,793-row liveAtExit fixed point was evaluated twice per endpoint query. Equivalent recursive plans received distinct RA hashes because one inherited an unrelated cached-empty sentinel while the other used a literal empty base.

Caching at the Python adapter boundary restores the documented shared-SSA contract and is narrower than caching the 6.31M-row generic liveness relation for every language instantiation.

Current-head measurement

Base: exact #21925 head 1a8e317b4a328bea1059453a2ab3ba6eada09e3f.
Database: exact saltstack/salt@d036b1177efeec175164571e9cc07b52cddf7844.
Mode: three matched, prewarmed, serial -j1 repeats.

Endpoint query Baseline evaluator median Fixed evaluator median Paired median reduction Joined tuples Recursive runs
unsafe deserialization 51.294s 45.103s 6.428s 821,363,441 -> 763,107,771 12,955 -> 10,956
modification of default value 42.377s 34.238s 8.247s 617,442,427 -> 531,778,114 11,692 -> 8,677

Every repeat retained zero endpoint rows and BQRS hash 2a514e093aae140a14f6bf77beebe1ad. Peak dependent relation size was unchanged at 15,216,060 tuples.

Historical exact relation controls retained 483,922 definitions, 169,921 phi inputs, 390,548 first uses, 475,226 adjacent uses, and 123,231 semantic call edges, with zero left-only or right-only rows. Historical Salt recovery was 16.5% and 19.9%. Cold prewarm evaluator time was neutral (106.609s -> 106.620s), so this is a warm-query optimization rather than a claimed cold-cache speedup.

Tests

  • codeql query format --check-only for the changed QL files
  • python/ql/test/library-tests/dataflow-new-ssa/AdjacentUsesContract.ql
  • python/ql/test/library-tests/dataflow-new-ssa/SsaTest.ql

The broader requested Airflow and Python test matrix was not started after the coordinator requested the final current-head result without additional experiments; this remains a draft for that reason.

Stack

This PR is based directly on yoff/python-shared-cfg-dataflow-flip (#21925 head). It neither includes nor depends on sibling drafts #22116 (legacy CFG/ESSA unpin) or #22407 (capturedJumpStep binding rewrite).

No DCA was launched.

yoff and others added 5 commits August 20, 2026 10:32
Flips the Python dataflow trunk from the legacy CFG (semmle/python/Flow.qll)
and legacy ESSA SSA (semmle/python/essa/*) to the new shared CFG facade
(semmle.python.controlflow.internal.Cfg) and the new SSA adapter
(semmle.python.dataflow.new.internal.SsaImpl), both introduced
additively in the preceding PRs in this stack.

This is the trunk-flip equivalent of the original draft PR github#21894 (kept
around as documentation), rebased on top of the four preparatory PRs:

  P1: Remove AstNode.getAFlowNode() and rewrite callers (github#21919).
  P2: Qualify Flow.qll's AST references with Py:: prefix (github#21920).
  P3: Add new shared-CFG-backed control flow graph (github#21921).
  P4: Add new shared-SSA-backed SSA adapter (github#21923).

The Python dataflow library (semmle/python/dataflow/new/) now imports
the new CFG facade and SSA adapter. All CFG-typed predicates
(ControlFlowNode, CallNode, BasicBlock, NameNode, AttrNode, ...) are
qualified with the Cfg:: prefix; SSA references switch from
EssaVariable/EssaDefinition to SsaImpl::Definition/SourceVariable.

GuardNode is redesigned to use the new CFG's outcome-node model
(isAfterTrue / isAfterFalse) instead of the legacy ConditionBlock +
flipped indirection. Only BarrierGuard<...> is preserved as public
API.

Framework files (Bottle, FastApi, Django, Tornado, Pyramid, Stdlib,
...) are updated to take CFG nodes from the new facade.

A handful of dataflow consistency tweaks for the new CFG:
- Augmented-assignment targets are treated as both load and store.
- 'from X import *' produces uncertain SSA writes for unknown names.
- CFG nodes are canonicalised so dataflow does not see equivalent
  pre/post-order pairs as distinct nodes.

Two AST tweaks for the new CFG:
- AstNodeImpl: omit PEP 695 type-parameter names from
  FunctionDefExpr / ClassDefExpr children.
- ImportResolution: drop the legacy essa import.

Test churn (~175 files): reblessed library- and query-test .expected
files reflect slightly different CFG granularity, different toString
output, and a handful of true alert deltas in security queries.

Verification: all 367 lib + src + consistency-queries compile clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The `Cfg::ControlFlowNode` facade re-exports the shared CFG library's
`dominates`/`strictlyDominates` predicates, which are declared
`bindingset[this, that]` + `pragma[inline_late]` and are meant to be used
as bound-pair membership checks. The facade wrappers dropped these
annotations (using plain `pragma[inline]`), so even though the only
callers — the `with` / `async with` taint steps in DataFlowPrivate.qll
and TaintTrackingPrivate.qll — bind both endpoints, the optimizer was
free to materialise `Cfg::ControlFlowNode.strictlyDominates/1` as a full
O(nodes^2) relation over the (larger) shared-CFG node set.

On some projects this dominated analysis time entirely (DCA showed e.g.
ICTU/quality-time and biosimulations regressing ~75-160x). Restoring
`bindingset[this, other]` + `pragma[inline_late]` on the wrappers turns
the predicate back into a bound-pair check and is result-preserving (only
binding annotations change, the predicate body is unchanged).

Reproduced on ICTU/quality-time: full python-security-extended suite went
from stalling >20min on `strictlyDominates` to completing in ~6min; all
ControlFlow and dataflow/coverage library tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the public expression adapter and apply the canonical QL annotation ordering required by the formatter.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 529363f5-bc7d-4f0b-9f47-e03ba9aa0cdf
Exercise the three public AdjacentUses relations and compare them with their internal projection or recursive expansion contracts. The cache-placement defect changes evaluator specialization and work rather than semantic results, so a semantic contract snapshot is the stable red-state equivalent; wall-time assertions would be machine-dependent and flaky.

This intentionally records no MISSING or SPURIOUS rows: the expected invariant is exact relation equality before and after cache placement changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 21ab8585-861f-42c9-a834-451604646c6b
The shared SSA module requires language adapters to cache predicates that they expose. The Python adapter exposed firstUse, adjacentUseUse, and useOfDef without restoring that cache boundary, unlike the legacy AdjacentUses implementation.

On exact historical Salt, the missing boundary caused the same 6,313,793-row liveAtExit fixed point to be evaluated twice. The equivalent plans received distinct RA hashes (c6bc8xgji0uv6seurbhesjqd315 versus fabf1xs3jb6t67a2buq2iv8iof4 for unsafe deserialization, and c6bc8xgji0uv6seurbhesjqd315 versus 8270excv27ldlfrtk19ou81d206 for modification-of-default-value) because one inherited an unrelated cached-empty sentinel while the other used a literal empty base.

Cache the three Python adapter relations rather than generic liveness. This restores the documented shared-SSA contract at the narrow language boundary and avoids imposing a 6.31M-row generic cache on every language instantiation.

On current head 1a8e317 with exact saltstack/salt@d036b117, three matched prewarmed -j1 repeats reduced median evaluator time from 51.294s to 45.103s for unsafe deserialization and from 42.377s to 34.238s for modification-of-default-value. Median paired reductions were 6.428s and 8.247s. Joined tuples fell by 58,255,670 and 85,664,313; recursive pipeline runs fell by 1,999 and 3,015. Both queries retained the identical empty endpoint hash 2a514e093aae140a14f6bf77beebe1ad in every repeat.

Historical exact controls also retained 483,922 definitions, 169,921 phi inputs, 390,548 first uses, 475,226 adjacent uses, and 123,231 semantic call edges with zero left-only or right-only rows. Historical Salt evaluator recovery was 16.5% and 19.9%. Cold prewarm evaluator time was neutral (106.609s to 106.620s), so this is a warm-query optimization rather than a claimed cold-cache speedup.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 21ab8585-861f-42c9-a834-451604646c6b

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant