Skip to content

Context-Sensitive Andersen Pointer Analysis - #856

Open
fabianbs96 wants to merge 80 commits into
secure-software-engineering:developmentfrom
fabianbs96:f-CtxAndersOTFAA
Open

Context-Sensitive Andersen Pointer Analysis#856
fabianbs96 wants to merge 80 commits into
secure-software-engineering:developmentfrom
fabianbs96:f-CtxAndersOTFAA

Conversation

@fabianbs96

Copy link
Copy Markdown
Member

Adds selective call-string context-sensitivity to the existing AndersenOTFSolver + fixes some bugs/incompleteness in both alias solvers (AndersenOTFAA and union-find-aa).

Context-sensitivity is off by default; with Mode::Off nothing is allocated and the solver behaves exactly as before.

Key Features

Call-string limit fixed at k=1 with 4 selection modes:

Mode Selects
Off (default) nothing; identical to the insensitive solver
Manual only functions matching AllowList
Dynamic AllowList plus functions observed as precision-critical
All every function, until the node budget is reached

AllowList/DenyList are function-name globs to bypass the selection heuristics.

Two budgets bound the cost: MaxContextualNodes (default: 20k) caps context-qualified nodes globally, and MaxContextsPerFunction (default: 32) caps clones of one function.
Mode::Dynamic additionally skips functions above MaxContextualFunctionSize (default: 256).


DISCLAIMER: The work in this branch has been largely AI generated, similar to #842, just with a stronger model.

fabianbs96 and others added 30 commits April 23, 2026 19:40
…ing in AndersenOTFSolver

- grow() may reallocate Nodes; all constraint methods now call every
  grow() before indexing Nodes[X], and snapshot pts sets before any
  addAssignEdge call that fires inside a foreach callback
- onNewPointee snapshots all four constraint lists upfront for the
  same reason
- merge() snapshots NonRep vectors before any addAssignEdge call, and
  retroactively fires load/store/memcopy constraints for Rep's merged
  pts set (previously those constraints were silently dropped for
  already-existing pointees)
- ConnectKnownTargets and checkUnresolvedFPCalls snapshot pts(FPId)
  before iterating: connectCallee->propagate() can grow that set
- handleCall now collects all resolved IDs per argument (not just the
  last one) via SmallVector<ValueId,2> per slot; FPCallRecord::Args
  and connectCallee updated accordingly
- Add dedup guards (LoadDstSet, StoreSrcSet, MemCopyAs{Src,Dst}Set)
  to NodeInfo to avoid redundant constraint firing
- Remove unused NoArgId sentinel and <cstdint> include
- Mark rep() [[nodiscard]]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix OperandOf::operator< (was comparing R2.Inst instead of R1.Inst)
- DeepChainTwoObjectsMerge (context_04_1): three-level id chain with x/y
- RecursiveSelfAlias (context_08): SCC collapsing under self-recursion
- MutualRecursionAlias (context_10_0): Forth↔Back two-way recursion
- ReturnSecondArgContextInsensitive (context_12_1): argretq precision
- FuncPtrCallbackIdentity (context_14_1): OTF resolves indirect call
- RecursionTwoObjectsMerge (context_09_0): recursive with two objects
- MutualRecursionTwoObjects (context_10_1): mutual recursion, two objects
- ThreeWayMutualRecursion (context_11_0): Forth↔Back↔Stop recursion
- ThreeArgReturnQContextInsensitive (context_13_1): three-param function
- FuncPtrCallbackThreeWayMerge (context_14_2): three function pointers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
XXX: Should we allow passing-in an instance of LLVMFunctionDataFlowFacts?
Root-cause was integral stores being found as reaching definition for a ptr-load
fabianbs96 and others added 19 commits August 3, 2026 19:21
addPointee() resolved the pointee through rep(), letting lazy cycle
detection rewrite an abstract object's identity. Merging a cycle is
points-to-correct, but object nodes take part in the constraint graph and
points-to membership is what defines may-alias: once two objects shared a
representative, every pointer to one aliased every pointer to the other.
Context-sensitivity multiplied object nodes and grew that conflated SCC,
 making it a net precision loss.

Storing the canonical id costs nothing: cycle collapsing still drives
propagation, as every constraint helper re-resolves through rep() itself.

Also fixes a latent unsoundness: rep() was applied at insert time while
merge() never rewrote other nodes' points-to sets, so pointers recording
the same object before and after a merge stored different ids and failed
to alias.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gxbHcWF9ZWq82oZP4m6Y1
addFnPtrArgsAsEntries() reads pts(arg) when a declaration call site is
first connected, and the ConnectedCallees guard makes that happen exactly
once. Points-to sets keep growing afterwards, so a function pointer that
reached such an argument later was never discovered and its whole callee
subgraph went unanalyzed.
Record the argument lists and re-check them per outer round, like the
other unresolved-call tables.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gxbHcWF9ZWq82oZP4m6Y1
Turns the six failing tests from d45ecc2 green, plus A3, which the
review had written off as untestable.

- A1: resolveFieldWrite(ValueId) resolves through rep(), so a store whose
  pointer node later collapses into an SCC still poisons its objects.
  Includes the Poisen -> Poison typo.
- A2: a failed addAlias in addPtrAlias now merges the two nodes instead of
  silently dropping the relation. handlePhi interns loop-carried GEPs and
  casts before they are translated, which left them with an empty pts-set.
- A3: merge() queues Rep when the absorbed diff left it with pending pts.
  Reachable via A2's new merge site: addAssignEdge only re-marks Rep's pts
  when it is non-empty, so an empty Rep absorbing a non-empty NonRep
  stranded the whole diff.
- A4: widen handleCall's CSRetVal gate to match handleReturn, add
  ExtractValue/InsertValue (assign edges from the aggregate operand, as
  LLVMPointerAssignmentGraph already does) and AtomicRMW/AtomicCmpXchg
  (store + load). Clang puns pointer atomics through a pointer-sized
  integer, so isPunnedPointerAccess bypasses the integer-type gate when the
  accessed pointer resolves to pointer-holding memory.
- B4: keep a colliding external id as a second name for the node, so a
  caller-supplied ValueCompressor no longer loses one side's alias set.
- B5: sortUnique(Buf) before insertSorted, whose precondition context
  clones routinely violated.

B1 stays as documented behavior: k is still 1, and the test now pins the
merged result so raising k has to break it deliberately.

Not measured yet: A2 is where ptaben recall should improve and A4's
field-insensitive aggregate merging is where precision may drop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compileGlobs silently dropped malformed allow-/deny-list patterns, so a
typo disabled selection for exactly the functions the user asked for --
and in Mode::Manual disabled the feature entirely. It now names the list
and prints the pattern plus the reason to stderr, keeping the rest.

PHASAR_LOG is unsuitable here: logging is off unless the client enables
it and is compiled out without DYNAMIC_LOG, so the report would stay
invisible in a default build.

Also fix the __has_cpp_attribute(lifetimebound) spelling in Macros.h and
mark the AndersenOTFSolver constructor's reference-like parameters
PSR_LIFETIMEBOUND, which catches a temporary Entries container or IRDB
at the call site under clang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured over six programs from a real-world IR corpus, comparing
alias-entry counts and wall time against Mode::Off and Mode::All.

MaxContextsPerFunction 8 -> 32.  Eight was the binding precision
constraint: below the cliff the analysis pays for eight clones and
still merges the remaining callers into the root clone, i.e. the worst
of both.  One program improves by 34% once the cap clears its cliff.

MaxContextualNodes 200'000 -> 20'000.  The old value could never bind
(observed peak 89k).  Fixing the throttle (below) turns it into the
real cost governor; solve time is super-linear in the node count.

MaxLocalMergeFunctionSize 32 -> 0 (off).  Provably inert: 0, 32 and 256
give identical alias counts everywhere.  That tier's only payoff is
formal-vs-formal aliasing inside the body, which buildResult unions
back together across contexts.  Worth revisiting if that is fixed.

The budget could not actually throttle anything: budgetExhausted() was
only consulted in computeIsSelected, which runs once per function and
memoizes before any clone exists, so functions selected in the early
rounds kept minting contexts indefinitely.  calleeContext now
re-checks it.  Allow-listed functions stay exempt there -- selected but
capped at one context is selected in name only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clang lowers pointer traffic through pointer-sized integers -- notably the
atomic builtins -- so gating loads and stores on the value's LLVM type
alone drops the whole chain and leaves the loaded pointer unaliased.

Ports isPunnedPointerAccess from AndersenOTFAA, which admits an integer
access iff the memory it reads from or writes to is a pointer-holding
alloca or global.

Measured over six programs with computeCtxIndSensUnionFindAARaw; all six
recover aliases that were previously missing (alias entries, lower =
more precise, so a rise here is recovered unsoundness):

  bison        99913238 ->  99933230
  lepton      214651764 -> 215856283
  libpcap      17198183 ->  17206453
  lrzip         8801726 ->   8801728
  opencv-core 6955391325 -> 6955724937
  readelf     157857005 -> 158007805

Runtime unchanged within noise.

Unlike Andersen, no escape hatch is needed in the operand traversal: the
Punned flag on forEachOpId only suppresses an early-out that forEachOpId
adds on top of forEachPointerOperand, and handleOperand has no such
early-out.

Two limitations remain, shared with Andersen: forEachPointerOperand still
filters integer operands inside a ConstantExpr walk, and a pointer
laundered through an intermediate integer-typed local breaks the chain at
that local, whose alloca is not pointer-holding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither instruction had a case in dispatch, so an atomic pointer exchange
was silently ignored: nothing was stored into the slot and the exchanged-out
value aliased nothing at all.

Field-insensitively an atomicrmw is a store of the new value plus a load of
the old one; cmpxchg likewise, into its { ty, i1 } result. Mirrors
AndersenOTFAA's handleAtomicAccess. Routing the pair through addEdge lets
the existing store/load delaying treat the slot like any other, so the
delayed edges connect each incoming store to each outgoing load.

Only reaches pointer-typed atomics now that the punning gate is in place;
clang lowers the pointer builtins through a pointer-sized integer.

No change on the six-program corpus: every atomicrmw/cmpxchg there is an
integer refcount update, which the gate still correctly ignores. This is a
completeness fix, not a precision win.

The test expectation differs from AndersenOTFAATest's on one point:
Andersen keeps the two stored objects in separate alias sets, a union-find
cannot, since both reach the same loads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ValueCompressor::addAlias no-ops when the value already owns an id, and
four PAG-builder sites ignored that. Since handlePhi and handleCall intern
operands eagerly, this fires 13-1856 times per program on real code.

Most are benign -- a substitute edge, usually the Assign handlePhi already
added, joins the nodes anyway. The exception is handleLoad's
single-reaching-def MemSSA path, which returns without emitting any edge,
leaving the load aliasing nothing.

addAliasOrEquate falls back to a pair of Assign edges, which every strategy
treats as an equivalence. Both directions are needed:
CallingContextSensUnionFindAA drives the join from the source's contexts.

Measured: bison +28 alias entries, five of six programs unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
buildPAG only called onAddValue for freshly inserted values, so a
pre-populated ValueCompressor shifted every index in the strategies'
per-value tables. Report the already-present ids up front instead, and
assert the ascending-id invariant at the three emplace_back sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LLVMPAGBuilder and AndersenOTFSolver each had their own copy of
isPunnedPointerAccess and of the load/store/atomicrmw/cmpxchg taxonomy.
Move both into LLVMPointerSemantics.h so the two cannot disagree.

That exposed one divergence: Andersen wrapped the multi-def branch of the
MemSSA reaching-defs block in an else, so a single reaching def storing a
ConstantExpr fell back to a plain Load edge instead of assigning from the
expression's leaves. Aligned with the PAG builder; lepton loses 5384
spurious alias entries, the rest of the corpus is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The returned iterator borrows VC, and the strategies borrow CG and IRDB.
Annotating only the call sites is inert, because clang stops tracking at
MaybeUniquePtr / NonNullPtr, so their pointer-taking constructors are
annotated too.

computeUnionFindAA*Raw is left alone: its result owns its data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restore the example tool, which had been replaced by a timing harness.

Drop the review's item ids from test names, test inputs and the design
doc, and reword the comments that still described the defects as open --
all of them are fixed and the tests pass. Remaining findings are deferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fabianbs96 fabianbs96 self-assigned this Aug 14, 2026
@fabianbs96 fabianbs96 added the enhancement New feature or request label Aug 14, 2026
@fabianbs96 fabianbs96 added this to the PhASAR v2610 milestone Aug 15, 2026
@fabianbs96
fabianbs96 marked this pull request as ready for review August 16, 2026 11:16
@fabianbs96
fabianbs96 requested a review from MMory as a code owner August 16, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants