Skip to content

fix(core, analyzer): Backward analysis - #405

Draft
Saloed wants to merge 82 commits into
mainfrom
saloed/backward-main
Draft

Saloed wants to merge 82 commits into
mainfrom
saloed/backward-main

Conversation

@Saloed

@Saloed Saloed commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

No description provided.

Saloed and others added 30 commits September 30, 2026 21:18
JIRMethodAnalysisContext only needs the analysis phase and the manager
params from its manager. Introduce the JIRAnalysisManagerBase interface
exposing exactly those, implement it in JIRAnalysisManager, and make the
context open with an overridable methodCallFactMapper so a second
(backward) manager can reuse the context. Forward behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c802d1143d5cc49a2c981ecae301cb2525e8a038)
Add package org.opentaint.dataflow.jvm.ap.ifds.backward: a second
TaintAnalysisManager that runs the unchanged IFDS engine over the
reversed application graph, where facts are sink demands.

- JIRBackwardAnalysisManager mirrors JIRAnalysisManager (params, phase
  selection via SelectedTaintRulesProvider, relevant rule ids, context
  reset) and reaches forward-only components (alias analysis, variable
  reachability, JApplicationGraph downcast) through graph.reversed.
  Alias analysis is seeded with the forward method entry. isReachable is
  always true; valid exit facts are Argument/This/ClassStatic only.
- JIRBackwardMethodCallFactMapper: call->start maps the result variable
  to Return only, otherwise forward mapping; exit->return maps callee
  Argument/This back to the caller arguments/receiver.
- Entry-point resolver drops the exceptional exit; start FF, summary
  handler, side-effect handler and trivial precondition objects.
- JIRBackwardTaintRules: sink rules -> demand seeds from the positive
  mark literals of the rewritten condition, and demand -> source matches
  (call, method exit, method entry, static field), shared by the call and
  sequent flow functions.
- JIRBackwardFindingTracker: thread-safe source findings, unconditional
  sinks and demand seeds, exposed by the manager.
- Sequent and call flow functions are Phase 1 stubs: correct for the zero
  fact (zero propagation, sink seeds, unconditional sinks), Unchanged for
  facts.

TaintSourceActionPreconditionEvaluator now takes the FactReader interface
so a FinalFactReader demand can be matched and its abstraction
refinement recorded; the trace preconditions keep passing
InitialFactReader, so their behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3a8f98356d6dda3894508203fbefb2eb88a47770)
BackwardAnalysisTest builds the single-exit JIR graph, reverses it and
drives JIRBackwardAnalysisManager through TaintAnalysisUnitRunnerManager
directly (FullScan, reset ApManager, run), returning the tracker records,
analysed methods and per-statement facts. It provides assertSourceReached
and assertNoSourceReached.

AnalysisTest exposes findEntryPoint, createRulesProvider,
createAnalysisGraph and SingleLocationUnit to subclasses instead of
keeping them inline in runAnalysis; forward runs are unchanged.

BackwardSmokeTest on SimpleDataFlowSample checks that the sink call seeds
one demand on the caller local and that every callee is entered from its
exit, and that a method-entry sink seeded at JMethodEnterInst is mapped
back to the caller argument through the summary. The source-reach
assertion is disabled until the phase 2 flow functions land.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 38aeff474440a3cd751c43fb52f67a367077338b)
Add the observed reversed-graph behaviour, manager and context details,
mapper type checks, corrections found against the code (resolution
failure has no start base in propagateUnresolvedCallFact, source
evaluator generalised to FactReader, pass-through precondition evaluator
cannot read FinalFactAp demands), the JIRBackwardTaintRules helper API,
tracker API, new known limitations and the phase 1 status/test harness.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7cf71a8e1dc4ebe4d5f8c61535609727f6054563)
TaintPassActionInverseEvaluator is a FinalFactAp sibling of
TaintPassActionEvaluator: a demand on a pass rule's 'to' position becomes a
demand on its 'from' position (CopyAllMarks rebuilds the delta after 'to' at
'from', CopyMark maps to·M·$ to from·M·$). Reads go through FinalFactReader,
so abstract demands are refined exactly like forward facts. The trace-side
TaintPassActionPreconditionEvaluator is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e625803e212ebfd7d6b26ea7f69f96e3ad386aa9)
Replaces the Phase 1 stub for facts:
- relevance check and call-to-start through the backward fact mapper with the
  real result variable;
- source matches recorded as findings, conditional sources emit their
  positive mark literals as new demands;
- cleaners are inverted by running the forward cleaner on the demand itself:
  removed alternatives are dropped, surviving demands continue;
- constructors keep the demand call-to-return, as forward;
- resolution failure overrides the three hooks to know the start base: non
  Return demands are kept, pass rules (plus defaultGetModel) are inverted with
  TaintPassActionInverseEvaluator, the generated demands are filtered by the
  call's cleaners and mapped callee to caller;
- every reader refinement is merged into the emitted edges and reported via
  addSideEffectRequirement, like forward.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2416b6ea38483e63e3b5fc7690dc59382c1e7d92)
BackwardCallSample holds call-only flows (no sequent FF needed): direct
sink(source()), library pass-through with default and user CopyMark rules,
a demand through a callee argument heap effect, argument/result/receiver
cleaners, a conditional source and negative cases. The callee return-value
flow needs the sequent FF and is disabled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6425cac27cc8c22888b51c0a0e8fa097965d5a23)
Records cleaner inversion (forward cleaner applied to the demand), pass
inversion (TaintPassActionInverseEvaluator, mark conditions treated as
satisfied), refinement plumbing, resolution-failure overrides, documented
omissions and the Phase 2a test coverage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 04f8ca713277779180b020b68e40f6cdb470b2fb)
Replace the Phase 1 fact-level stub with the full statement semantics of
spec section 4: assignments (copy/cast with forward type filters, field,
static and array reads by prepending, binary operands, kill on const/new/
other), strong field and two-level static writes with forward-identical
abstraction splitting, weak array writes, return/throw rebasing, and
may-alias weak moves for field and array writes using the alias state
before the statement.

Rules: exit sources are matched at return before Return is rebased,
static-field sources at x = C.f, entry sources at JMethodEnterInst; reader
refinements from abstract demands are propagated into the emitted edges.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8d00202797cb9ff31b6b26c5710ee4ddf1fb7047)
BackwardSequentFlowTest runs the backward engine end to end with an
entry-point source and a call sink, so the demand only crosses sequent
statements: copies, casts, fields, arrays, statics, binary ops,
self-referential forms, aliases through a heap indirection, branches,
loops and a method-exit sink, each with negative counterparts.

BackwardSequentFlowFunctionTest checks the exact Sequent sets for abstract
FactToFact demands (refinement on field, element and static accessors),
concrete ZeroToFact demands and exit-source matching at return.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 1262dbaface5061e4716aaa423e5a5c34bec098b)
Add section 4.1 (callback structure, type filters, constants, write
procedures, alias direction and translation, observed JIR shapes) and
4.2 (status and test inventory) to the backward analysis spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 566119fa4ab8dbd881e5b28063c2db4c6f28f112)
The callee-return and smoke source-reach cases now pass with the
sequent and call flow functions merged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit bfac792fa2002fdf6d7a8e5907e018c425a1be1c)
ContainsMarkOnAnyField (a ContainsMark on a position with the AnyField
modifier) produced no demand, so every AnyField sink was silent unless
it also had a BaseOnly alternative whose mark sat at the root. Forward
matches such a literal when the fact carries the mark at any path below
the position, including the position itself. The demand is now seeded
as position.M and position.[any].M, which the flow functions move and
match like any other demand. Source condition demands use the same
expansion.

Found by the backward/forward differential test (CleanerDslControlFlow
sequentialMarks, cleanThenRetain; CleanerDsl helper controls and
recursive any-only source).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b26cbd4845e10fb45f43311370066435a6f8f6cb)
Forward aliases the facts a call creates (source results, pass-through
results, summary facts with a memory effect) through
forEachAliasAfterCallStatement: a fact on a call local b is copied to
every alias z.g1..gn of b that persists through the call. Backward
ignored this, so a demand on z after the call never reached the call's
effect on b. For StringBuilder chains (sb.append(x).append(b)) the
demand on sb never became a demand on the intermediate receiver.

The call flow function now also processes, for every call local b
(receiver, arguments, result) and every alias of b persisting through
the call whose base is the demand's base, the demand read through the
alias accessors and rebased to b. The derived demand goes through the
same source matching, cleaners and call-to-start as the original one,
as JIRMethodCallPrecondition does for aliased trace facts. Reading
through an abstract demand refines it via the fact reader. A demand
irrelevant to the call is still kept unchanged.

Found by the backward/forward differential test
(JavaDataFlowReachabilityTest chainedAppend, namedReturn).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 05bdbd0e6012ab997ce0e15c5dae03abeea5e3c4)
BackwardForwardDifferentialTest replays every case of
JavaDataFlowReachabilityTest, KotlinDataFlowReachabilityTest,
MultiReturnDataFlowTest, CleanerDslAnalysisTest,
CleanerDslControlFlowAnalysisTest and CleanerFieldSensitivityAnalysisTest
(re-declared in ForwardSuiteCases, with the same useDefaultConfig and
unroll settings as the forward suite). Forward runs once per case on the
full config. Backward findings name a mark, not a sink, so each case's
sinks are split into groups whose demanded marks are pairwise distinct
and backward runs once per group: the marks it reports must equal the
marks of the group's sinks that forward reached. Tree mode additionally
checks forward against the forward suite's expectation.

Accepted divergences carry their mechanism in the table: lambdaCaptureFlow
(lambda resolution is forward-only) and the star-path/Exact-cleaner
interaction, evidenced both ways by CleanerStarDualSample.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a842d1cf4eb68008d54f7cd1fffb5d718205968c)
Forward reports at most one vulnerability per sink statement across the
rules of a config, so a group of sinks sharing a call (the cleaner DSL
checkpoints) under-reports in the modes where several rules fire there.
The forward reference is now one isolated forward run per sink rule, as
for the route-oracle benchmark.

Automata, BaseOnly and BaseOnlyField subclasses compare backward with
forward in the same mode; a rule also agrees when backward matches the
forward suite's Tree-verified expectation, since forward in these modes
has its own mode-specific results (Automata drops the flatMap and
recursive any-only flows its IFDS facts reach; BaseOnlyField forward
over-reports 300 matrix groups). Tree stays strict. Divergences now pin
the backward value and are checked to still differ from the reference,
so a stale entry fails. New entries: Automata keeps the star demand
through an Exact cleaner (root-level mark FP), BaseOnlyField seeds
root demands with an open field tail (cleaner/field-write FP), and the
forward-suite flatMap known false negative that backward reports.

Cactus is not run: forward cannot execute there (AccessCactus.equalTo
is not implemented).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 990739bea0821027c0580f19456099428a8bfe51)
New section 11: the replay harness (sink groups, per-rule forward runs,
mode references, self-checking divergence pins, the star/cleaner
evidence sample), the Tree results (609 agree, 11 fixed, 8 accepted
groups), the two fixes, the star path vs Exact cleaner mechanism, the
access-path mode matrix for the existing backward suite and the
differential (Automata, BaseOnly, BaseOnlyField, plus a Cactus record
against the suite expectation), and forward observations made on the
way. Sections 5, 6a and 9 are updated for the call-site aliases and the
any-field seeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8414c2cf2d60c18f7a5b377d84ebd86a9e2e03c5)
The backward entry-point resolver dropped JMethodExitExceptionalInst, and
the single-exit graph links throws only to it, so statements that lead
only to a throw (a branch ending in a throw, a rethrowing catch handler,
a callee that always throws) were never analysed. Forward analyses them
and reports sinks there; it only ignores summaries at the exceptional
exit.

Keep every reversed entry point. An exceptional entry point starts only
the Zero fact: the start flow function drops caller demands there, since
forward never returns a callee fact to its caller along an exception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b20cc7ae30e28b708132a41186c00230e3ba90e5)
callSiteAliasDemands required the demand base to be a local variable, so
a demand rooted at an argument, `this` or a static field never reached
the call local aliasing it (`b = h.sb; b.append(p); sink(h.sb...)` with
`h` a parameter). Forward's forEachAliasAfterCallStatement only needs
the call local to be a local; the alias base may be a local, an
argument, `this` or ClassStatic, and only constant alias bases are
skipped, which a demand never has. Drop the restriction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 79a816b875df3564f23805e637d2b8473312181f)
…emands

Forward applies entry-point sources on the zero fact of every analysed
method and, at the exit, dropArgumentsLocalTaintMarks removes the marks
assigned on method enter from zero-to-fact facts rooted at an argument
or `this`. Only a mark directly at the root is removed (TaintMarkRemover
accepts any path below a field), so `arg1.sb.M` still reaches the
caller while `arg0.M` does not.

Backward matched entry sources on every demand at JMethodEnterInst,
including a caller's demand on the argument itself, and so reported
`entryHandler(s); sink(s)` with the entry rule on entryHandler, which
forward does not. Record an entry-source finding unless the edge's
initial fact (the caller demand at the exit) is rooted at an argument or
`this` and starts with a mark assigned by an entry rule of the method.
Zero-to-fact edges and initial Return / ClassStatic bases always record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a399b3107a09f4bee208905cfeca6fa926472f04)
A source match on an abstract demand only refines it (the reader records
the exclusion, the concrete initial fact is created by the engine later).
The refinement travelled on the emitted demands, so when nothing was
emitted after the match, e.g. an exit source on `return "c"` or a void
return, it was lost and the concrete edge that records the finding never
existed. Report it as a SideEffectRequirement on fact-to-fact edges, as
the call flow function already does; zero and ND edges keep the
can't-refine check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f2b6543e97cf5d1bbd28fbbf7da8b72a84b7e368)
JIRTaintAnalysisContext.handlePhase records the id of every rule queried
in Prescan into relevantRuleIds, which later phases pass to
TaintRulesProvider.selectRules. Forward queries sinks and sources on the
zero fact, but backward matches sources only on demands, and Prescan has
none, so no source id was recorded and a semgrep-backed provider
(SemgrepRuleProvider.reduceTaint) would drop the taint rule in FullScan.

On the zero fact in Prescan, query the call, method-exit, method-entry
and static-field source rules, as forward does. Cleaners and pass rules
are queried only on facts in both directions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit bc4d5ee241bb68d4c4637a4901b0cde1aebd0ce3)
The backward summary handler used the default handleSummary, which never
calls createSideEffectRequirement. Forward's JIRMethodCallSummaryHandler
emits one whenever applying a summary refines the caller's initial fact,
which is how the refinement reaches the caller's own callers. Mirror that
override. Call aliases are left out: backward inverts call-site aliases
on the demand in the call flow function before it enters the callee.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit db2bf2955fd8e5a8b951ae1afa2e0e5e72d8dfab)
…llFact

MethodCallFlowFunction.Default.propagateUnresolvedCallFact had no
startFactBase, so the backward call flow function re-implemented the
three resolution-failure adapters of Default and kept an unreachable
override that threw. Default now passes the start base through; the
forward JIR and Go implementations ignore it. The backward flow function
implements propagateUnresolvedCallFact directly and inherits the
adapters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4c212023a5a221b9d79e7a48b066ed9314d4bfe6)
JIRBackwardMethodCallFactMapper copied the forward mapping. Delegate to
JIRMethodCallFactMapper and override only the differences: exit to
return answers nothing for Return, call to start maps a demand on the
result variable to Return, and valid exit facts are Argument, This and
ClassStatic.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 9c0641ba85b8a719a622b932e3beb0b3fd984bb0)
BackwardDemandSeed is a debugging and test aid, yet one was kept for
every sink seed, with its fact, for the whole run. Record seeds only when
the manager is created with recordDemandSeeds = true; the test harness
enables it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 93cfb41a76a49f14ef5ad5f67459f014b13d445c)
…ionMatcher

ForwardSuiteCases.sinkMarks counted marks under Not, which the backward
analysis never demands, so such a sink would need an impossible
finding. Track the polarity and keep positive marks only. Reuse
AnalysisTest's functionMatcher (now a companion function) instead of a
copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c705c050ed31796b0efdcb275f788be31b09be55)
BackwardRegressionTest (sample BackwardRegressionSample) checks each case
against the expected result in both directions: sinks on paths that end
in a throw, the heap effect of an always-throwing callee, a loop without
exit, call-site aliases rooted at an argument, `this`, a local and a
static field, entry sources of a callee reaching the caller through the
argument root (no), an argument field, the return value and a static
field (yes), and an exit source on a method returning a constant.

With stagedRuleSelection the harness wraps the provider in
RuleIdSelectingProvider, which honours selectRules like the semgrep
provider, and runs Prescan before FullScan; the prescan cases check that
call, entry, exit and static-field source rules survive the selection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4f214d60661d1478360d5c8f1d17bb9cea8c56fa)
Section 1: the exceptional exit is a Zero-only entry point; the earlier
"exceptional flow ignored, as in forward" rationale was wrong, forward
analyses statements before a throw and only ignores summaries at the
exceptional exit. Section 3: the fact mapper delegates to forward.
Section 4: entry-source findings versus forward's
dropArgumentsLocalTaintMarks, and the refinement kept when a matched
demand dies. Section 5: call-site aliases of any base and the start fact
base passed to propagateUnresolvedCallFact. Section 6: side-effect
requirements on summary application. Section 6a: prescan source
registration and why it is needed. Sections 8 and 9: gated demand seeds,
exceptional flow and rule selection. Section 10: rewritten for the
current tests and harness.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fc59485794af3e308aae6f0961bac316740d84a3)
Main has no shallow-scan phase, no actionable-rule selection and no
BaseOnly access-path modes. Select rules the way JIRAnalysisManager does
on main, and drop the BaseOnly differential runs and divergence entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Saloed and others added 29 commits September 30, 2026 21:19
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since the flow functions subclass the forward ones, the backward inversion
of `C.f = x` reused the forward static write, which clears nothing: it
tests the field accessor against a fact that starts with the class-static
accessor. The demand on ClassStatic.<C>.f therefore survived an overwrite
with a constant (forward has the same weak flow step but drops the finding
in trace resolution).

The backward write now clears <C> from ClassStatic and f from the <C>
subtree with two forward RefAccess writes and restores the rest under
<C>; the demand still moves to x through the forward static read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Backward starts a method at its forward exits, and the single-exit graph
connects only returns and throws to them, so a region that never exits
(an infinite loop after a sink) was never visited and its sinks were never
seeded.

JIRBackwardNonExitingStarts computes, in the forward method graph, the
statements reachable from the entry that reach no exit and returns one
representative of every bottom strongly connected component of that
region; every statement of the region is backward-reachable from one of
them. The backward entry-point resolver adds them to the reversed graph's
entry points, and the start flow function treats them like the
exceptional exit: Zero only, no caller demands and no end demands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drop BackwardSinkOccurrence and BackwardRunResult from the generic layer.
BackwardTaintAnalysisManager now only creates the backward manager and
prepares the next run (prepareNextBackwardRun returns the run's timeout or
null when done); TaintAnalyzer loops over it and reads the engine's
vulnerabilities.

The JVM backward manager wraps its rules in JIRBackwardSinkSelection, which
returns sink rules only for the selected (statement, rule) pairs of a
restricted run, so seeding no longer checks restrictions.
JIRBackwardSinkAttribution holds the discovery / disjoint-mark group /
isolated-run planning, the time-budget fallbacks and reports the confirmed
occurrences to the engine's TaintSinkTracker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… rule provider

One backward run replaces the discovery/group/isolation protocol.
JIRBackwardTaintRulesProvider swaps the rules: unconditional sources become
sinks at the same method and position, sinks become sources that seed their
condition demands, conditional sources turn a demand on their product into
demands on their condition. The forward rule code (JIRMethodCallTaintUtil,
JIRSequentTaintUtil, applyTaintRules, the exit rule steps) applies them, so a
finding is an ordinary sink report at the source statement.

Exit-sink demands carry zero-edge marks that do not leave the method through
its entry, keeping forward's zero-edge-only exit sinks. End-fact requirements
become an extra condition on the derived rule, triggered by the end demands.
Derived sinks take their id and meta from TaintRulesProvider.sinkMetaForSource
(the semgrep automaton of the source). Sink attribution, sink selection, the
finding tracker, the end-requirement helper and prepareNextBackwardRun are
removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In backward mode findings sit at source statements with derived rule ids, so
assertReachable checks only that a finding exists and exact finding sets are
compared as empty vs non-empty (assertFindings). Forward assertions are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Backward behaves as if no sink had trackFactsReachAnalysisEnd: derived
rules no longer carry the requirement literal, no end demands are seeded
at method exits, and the prescan requirement collection and the
analysisEndMethods plumbing are removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The mark-free cubes of a method-exit sink stay a sink of the backward
provider, reported when Zero reaches a return or throw, like the
unconditional call and entry sinks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Negative samples are not checked in backward mode; tests whose negatives
report in backward are marked with the cause.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…conditional

When every action of a resolved cleaner is an Exact RemoveMark(M, P) and the
resolved condition is exactly the disjunction of the ContainsMark(M, P)
checks derived from those actions (P, plus P.[e] for array and Object
positions), the condition becomes True. Removing an absent mark is a no-op,
so forward removes the same marks. Any-field positions, String positions
(the removal also clears <string-bytes>), RemoveAllMarks and
ExactAndAnyField reach are left conditional.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lling

The backward rule provider keeps a cleaner only with the mark-free cubes of
its condition and drops it when every cube needs a mark, so the call flow
function runs the forward cleaner step unchanged. JIRBackwardStarUnroller
and its wiring are removed.

An Exact cleaner of a value again removes a whole any-field star demand on
it: CleanerDslAnalysisTest matrix AnyField-Plain-* and field-store-any, and
CleanerDslControlFlowAnalysisTest sequenceNestedAfterPlainSink-m1, are not
reported in backward mode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RemoveMark cannot remove [any] unless its own position has [any]. The
forward cleaner step treats the [any] at the cleaned position as possibly
empty, so an Exact RemoveMark(M, x) deleted a whole star demand x.[any]M
although forward keeps x.fM. JIRBackwardTaintCleanActionEvaluator runs the
forward step and, for an Exact RemoveMark whose position has no [any], adds
the demand's P.[any] subtree back unchanged. ExactAndAnyField, [any]
positions and RemoveAllMarks keep the forward behaviour.

Recovers the backward any-field-sink false negatives (CleanerDslAnalysisTest
AnyField-Plain matrix and field-store-any, CleanerDslControlFlowAnalysisTest
sequenceNestedAfterPlainSink-m1). The kept star over-approximates when the
mark sits on the cleaned value itself: the matrix with a plain source now
reports Plain-Plain-AnyField-field-depth0 in backward.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eir actions

Replace the exact-disjunction check with a condition rewriter. Every
RemoveMark(M, P) checks ContainsMark(P, M) on the fact before removing, so
it is a no-op when that literal is false and the literal can be assumed. The
condition is put in NNF, assumed literals become True and their negations
False, and the result is folded (constants, flattening, duplicates). Since a
literal is implied only for its own action, the rewrite is kept only when
assuming each action's literal alone yields the same condition.

Cleaners with an action that implies no literal keep their condition:
RemoveAllMarks, any-field positions and String positions (the removal also
clears <string-bytes>). ExactAndAnyField reach performs the same presence
check and is now rewritten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replace JIRBackwardNonExitingStarts (extra Zero-only entry points at one
representative of every bottom SCC of the non-exiting region) with
JIRBackwardExitWiringGraph, a wrapper of the forward application graph.
On every methodGraph request (no caching) it marks, in a BitSet over
instruction indices, the statements from which an exit is reachable by
walking predecessors from the exit points, and gives every other statement
an extra forward edge to JMethodExitNormalInst (successors += normal exit,
predecessors(normal exit) += those statements).

The backward manager builds its method inst graph from
JIRBackwardExitWiringGraph(graph.reversed).reversed, so that code is
backward-reachable from the normal exit. The special entry points and the
zero-only start for them are gone; zero-only stays for the exceptional
exit. The generic engine and TaintAnalyzer are unchanged.

Consequence: a caller demand entering at the normal exit also flows into
code that never returns. This only over-approximates and cannot lose a
finding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…onditions

A condition literal is implied only by its own RemoveMark action, so a
cleaner with several actions is resolved as one cleaner per action, each
condition is simplified against that action alone, and cleaners with equal
simplified conditions are joined again. resolveMethodRule now returns a
list of rules. The split is equivalent: applyCleaner evaluates every
applicable cleaner's condition on the same fact before applying actions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…itions

Resolve rules with flatMap. RemoveMark on a String position now assumes
its ContainsMark literal like any other position. An any-field removal
assumes both ContainsMarkOnAnyField on the base (the form an any-field
condition resolves to) and ContainsMark on the position.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… exit

The exceptional exit is a zero-only start, so sinks in code that never
returns are still seeded, while caller demands entering at the normal
exit no longer flow into it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…marks

A rule condition is put in NNF; its positive mark literals form one set
regardless of And/Or structure, negated mark literals count as True, and
the rest of the condition is folded with every mark literal replaced by
True. Every rule kind is swapped uniformly: mark-free sources become
sinks, sources with marks become conditional sources, sinks with marks
become seeding sources, mark-free sinks stay sinks, cleaners with marks
are dropped. Entry-point and static-field sources and method-entry sinks
with marks are now derived too and applied by the backward sequent flow
function at JMethodEnterInst and at static reads.

The $zero-edge shadow marks and their stripping at JMethodEnterInst are
removed; exit-sink demands reach callers like any other demand.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… context

Replace JIRBackwardTaintRulesProvider with JIRBackwardTaintAnalysisContext,
which extends the forward JIRTaintAnalysisContext and derives the backward
rules from the forward rules prepared at the same statement. A prepared
condition that is True (or has no positive mark literal) gives the
unconditional role, an expression with positive mark literals gives the
conditional role with all of them as demanded marks, so mixed conditions
such as `A or M@P` keep their mark-free branch. Static-field rules are
served by the context, so the sequent flow function no longer builds them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ent roles

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bclasses

No forward class is open for backward any more and no backward class extends
a forward class. The backward components implement the engine interfaces and
hold forward instances: the manager delegates to its own JIRAnalysisManager,
the taint context wraps a forward JIRTaintAnalysisContext through the
extracted JIRTaintRuleContext interface, the method context is a plain
JIRMethodAnalysisContext with the backward taint context and fact mapper, and
the flow functions and summary handler delegate to forward instances.

Forward variation points are parameters with the forward default: the
sequent FF's transfer (JIRSequentTransfer), the call FF's clean-action
evaluator argument, the clean evaluator's removeFinalFact, the summary
handler's withCallAliases, the context's methodCallFactMapper and the
manager's relevantRuleIds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The forward sequent flow function had to become internal only because an
internal interface cannot declare its members internal. Public members
keep the forward class as it is on main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…orward steps

The backward sequent flow function is a standalone MethodSequentFlowFunction
that calls the forward steps directly with roles swapped: a read x = y.f
moves the demand on x through moveIntoField, a write y.f = x applies
clearWrittenField and then fieldRead of y.f into x, and a base move kills
the demand on the target and rebases it to the source. The rule hooks at
return, static reads and method entry are unchanged. JIRSequentTransfer is
removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…position

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d sequent flow function

The backward sequent flow function now applies
JIRStatementSummary.buildReversed through the core transfer, like the
forward one applies build. Return and throw are plain reversed moves; the
exit rules run at JMethodExitNormalInst / JMethodExitExceptionalInst via the
forward exit rule hooks, as in forward. Entry and static-field rule hooks
are unchanged. The backward flow function is cached per statement.

- buildReversed keeps the per-base type filters.
- In the reversed build a weak (element) write keeps its written path
  refinable (a/{[e]} -> a, a.[e] -> a.[e]) instead of the plain identity,
  so an abstract demand on the array is refined down to the element and
  reaches the stored value (BackwardEdgeCaseTest arrayFilledInCallee).
- Constants carry no facts in JIRStatementSummary.
- The forward exit rule hooks and FactRefiner are public; the sequent FF
  cache holds any MethodSequentFlowFunction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Saloed
Saloed force-pushed the saloed/backward-main branch from 09aa1dd to b253972 Compare October 1, 2026 07:42
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.

1 participant