Repository navigation
fix(core, analyzer): Add hard field limits - #419
Merged
Merged
Conversation
Saloed
force-pushed
the
saloed/field-limit
branch
from
October 23, 2026 12:48
b0af90c to
f6e7de3
Compare
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 23, 2026 22:40
c4d6d0c to
c938b70
Compare
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 23, 2026 22:43
c938b70 to
340ba2b
Compare
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 23, 2026 22:46
340ba2b to
8270065
Compare
Contributor
Author
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 24, 2026 17:19
8270065 to
da053aa
Compare
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 24, 2026 18:52
da053aa to
1d9d576
Compare
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 24, 2026 18:54
1d9d576 to
e043fdf
Compare
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 25, 2026 10:08
e043fdf to
a4551a9
Compare
…limit Replaces the separate field limiter with an adaptation of the existing iterative-deepening (ID) fact-depth ladder in NormalMethodAnalyzer. Depth measure. The ID gate (edgeExceedLimit) now measures field depth via FactAp.exceedsFieldLimit instead of FactAp.depth. Field depth is the maximum, over every access path of a fact, of the number of field-like accessors on that path: FieldAccessor, ElementAccessor ([*]) and AnyAccessor ([any]) each count 1; taint marks, the final accessor, class statics, value, type-info group and type-info accessors are not counted. The old measure, Tree AccessNode.maxDepth, counted every accessor and charged +10,000 for each node owning an [any] child, so a single [any] pushed a fact out of reach of the ladder for 10,000 rounds. Tree AccessNode precomputes maxFieldDepth in its init block; AccessPath walks its chain with early exit; Automata runs a monotone longest-path relaxation that stops once the limit is exceeded; Cactus treats a cycle through a counted accessor as unbounded. Ladder. INITIAL_ALLOWED_FACT_DEPTH is now 1 and the comparison is strict: an F2F initial edge or F2F summary is delayed when its initial fact or its final fact has field depth above the current limit (the previous +2 slack for the final fact is gone). Which edges the ladder gates is unchanged (initial F2F edges and F2F summaries); zero-fact edges are not gated. Cap. With --field-limit N the analyzer's limit is min(ladder limit, N), also at construction and on resetApManager. An edge over the limit while the analyzer is already at the cap is delayed forever: it is neither stored in the delayed lists nor does it register the analyzer as delayed. Registering it would make TaintAnalysisUnitRunnerManager resume the unit at every quiescence, the unit would replay the same edges without progress, and the analysis would livelock until the IFDS timeout. Each registration therefore implies the analyzer is below the cap, and each resume strictly raises its limit, so every analyzer registers at most N times and analysisCompletion fires once nothing is left to admit. Without --field-limit the ladder is unbounded as before. Dropped edges are counted in a LongAdder in the runner manager, which logs `Field limit N: dropped K edges over the limit` at the end of every analysis phase. The option is plumbed from the Kotlin CLI (--field-limit) through CommonAnalysisOptions, TaintAnalyzerOptions, TaintAnalyzer, TaintAnalysisUnitRunnerManager, TaintAnalysisUnitRunner and MethodAnalyzerStorage to NormalMethodAnalyzer; the Go CLI gets `opentaint scan --field-limit N` (0 = unlimited). Trace resolution. MethodTraceResolver's TraceBuilder.addPredecessor drops predecessor entries whose trace edges exceed the field limit, so the backward search cannot deepen preconditions without bound through [any]-containing facts. This gate also covers source (zero-fact) trace edges, which the forward ladder does not gate. Tests: FieldLimitTest (field depth of Tree facts) and FieldLimitAnalysisTest (a depth-3 flow through a callee summary and a callee initial fact is found unlimited and with limit 3 and dropped with limit 2; a depth-1 flow is found with limit 1 and dropped with limit 0). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The ladder only gated initial fact-to-fact edges and fact-to-fact summaries, so facts that originate at sources (zero-to-fact) or grow inside a method were never checked: on conductor with --field-limit=4 the full scan still ran into the memory guard at ~9M events. addSequentialEdge now delays (below the cap) or drops (at the cap) any edge whose initial or final facts exceed the current limit, the delayed edges are replayed on every limit raise, and summaries of every edge kind are gated. Automata and Cactus field depth counted any cycle through a field-like accessor as unbounded, so with every edge gated those facts were never admitted even without a field limit (9 Automata analysis tests failed). A cycle now counts once, like the cycle-free Cactus depth. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ing the whole edge
A Tree fact is a set of access paths, and an edge was delayed (below the
cap) or dropped (at the cap) as soon as any one of its paths went over
the current field-depth limit. A fact that merged a shallow path with a
deep one therefore lost the shallow path as well. This happens when a
callee's summary delivers ret.{$, value.$} as a single tree and the
caller then stores it into a field.
Every analyzer gate (initial F2F edges, sequential edges of all kinds,
summaries) now goes through admitEdgeWithinLimit:
- an initial fact over the limit delays or drops the whole edge, as before;
- final fact max field depth <= limit: the edge proceeds unchanged
(checked first, so this path allocates nothing);
- final fact min field depth > limit: the whole edge is delayed or dropped;
- otherwise the final fact is split. The edge with the within-limit part
proceeds, and an edge with the rest part goes to the gate's delayed
list (or is dropped and counted at the cap). A delayed rest edge is
replayed through the same gate, so it can split again at the next rung.
ZeroToZero edges are never gated.
FinalFactAp.splitFieldLimit(limit) returns FieldLimitSplit(within, rest).
Together within and rest represent exactly the paths of the original
fact. Every path in within has field depth <= limit, every path in rest
has field depth > limit, and an empty part is null. A path ends at a
final or abstract node.
Tree: AccessNode.fieldLimitWithin keeps every node reachable with counted
depth <= limit, along with its final/abstract flags and deep-accessor
exclusion. It drops child subtrees whose field-like accessor would push
the depth over the limit. AccessNode.fieldLimitRest keeps only the cut
branches (whole), rooted at the same base. The prefix nodes become plain
structure with no final/abstract flag and no deep exclusion, so a node
that is terminal at depth == limit stays in within while its deeper
children move to rest. Both parts are built with the existing
transformAccessors/manager.create helpers and keep the base and
exclusions. AccessNode.allFieldPathsExceed computes min > limit on
demand, with early exit, only when maxFieldDepth > limit. No new
per-node field is stored.
Automata and Cactus use the interface default, which is all-or-nothing:
(this, null) within the limit, (null, this) otherwise.
Tests: FieldLimitTest covers the split (branch partition, fast paths,
terminal node at the limit with deeper children, [any] counting 1,
marks/final not counting, abstract leaves with deep exclusions, base
exclusions). FieldLimitAnalysisTest adds mixedDepthFlow: a callee
returns a Leaf tainted both at its base and at .value. The caller stores
it into holder.leaf, and two sinks read holder.leaf and holder.leaf.value.
With --field-limit 1 only the shallow sink is reported (on the previous
commit neither was), and without a limit both are reported.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…th without a field limit Without --field-limit the backward trace gate was inactive, and the start-to-final search concretises forward [any] facts into every ordering of distinct fields: a stored `x.[any]` fact contains all of them, so the contains-entry check never rejects one. On conductor the spring dispatcher loop inserts one summary field per pass and the search never closes. The gate now uses the field limit when it is set, and otherwise the highest fact-depth ladder level reached in the phase plus one. The slack is needed because a single forward [any] counts as one field but a trace may have to spell out two concrete fields under it (the starred clean and any-field-source cleaner tests fail with no slack). The reached level is logged as `Fact depth ladder reached N` at the end of each phase. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t at a fixed depth The trace gate dropped every predecessor whose facts exceeded a fixed depth (the field limit, or the reached ladder level plus one), which needed a hand-tuned slack because a single forward [any] fact may have to be spelled out as several concrete fields. The trace builder now runs its own iterative deepening. It starts at the last reached forward ladder level (capped by the field limit) and delays, rather than drops, a predecessor whose facts exceed the current depth. When the queue drains, the search stops and discards the delayed predecessors if a start entry was reached; otherwise it raises the depth by one and re-admits the delayed predecessors that now fit. The full-trace rebuild for a known start waits for that particular start, so it cannot stop at a shallower different one. Shortest witnesses are found first, and a search that reaches a start never expands deeper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… simplify field depth 1. Rename the option to "iteration limit": the analyzer CLI flag is now --iteration-limit and the Go CLI forwards `opentaint scan --iteration-limit` as --iteration-limit=N (only when > 0). The plumbing is renamed through CommonAnalysisOptions, TaintAnalyzerOptions, TaintAnalyzer, the runner manager, the runner, MethodAnalyzerStorage and NormalMethodAnalyzer, as are the analysis test, its sample and rule ids. The option caps the iterative-deepening fact depth; the fact-level field-depth API keeps its names. 2. AccessGraph measures field depth as the number of field-depth accessors in the graph (edge keys), replacing the longest-path traversal. 3. AccessPath and the Cactus AccessPathWithCycles precompute the field count per node next to size, making exceedsFieldLimit an O(1) comparison. 4. AccessTree nodes precompute minFieldDepth (shallowest terminal path) next to maxFieldDepth, both as Short so the node layout does not grow. splitFieldLimit, fieldLimitWithin and fieldLimitRest prune whole subtrees with the min/max bounds; allFieldPathsExceed is gone. 5. FinalFactAp.splitFieldLimit has no default; the access graph and cactus implement the all-or-nothing split explicitly until a precise split exists. 6. AnalysisRunner no longer exposes handleEdgeDroppedOverFieldLimit or reachedFactDepthLimit. Over-limit drops are not counted, and the trace resolver starts its backward deepening from the method analyzer's own capped fact depth limit. 7. The runner manager and runner only pass the iteration limit through; the drop counter, the limit report and the reached-ladder tracking are removed, keeping the runner/manager API surface unchanged apart from the parameter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
misonijnik
force-pushed
the
saloed/field-limit
branch
from
October 25, 2026 12:52
a4551a9 to
bf6fba9
Compare
Contributor
Author
misonijnik
approved these changes
Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.