Repository navigation
Handle StackOverflowError in the target as the plain JVM would - #78
Closed
nils-loose wants to merge 2 commits into
Closed
nils-loose wants to merge 2 commits into
nils-loose wants to merge 2 commits into
Conversation
Deep recursion plus the frames of the symbolic tracking can exhaust the stack anywhere, including inside SWAT. The second overflow then hit the lazy static initializer of ErrorHandler, which broke the class for the rest of the run, left isActive set and replaced the target's error by a NoClassDefFoundError: the target's own handlers did not run, and the truncated trace was sent without any marker. - ErrorHandler and the new StackOverflowGuard are initialised in premain. - processInstruction swallows a StackOverflowError raised inside SWAT (field writes only) and stops tracking for the rest of the execution; isActive is reset in finally. ErrorHandler passes a stack overflow on instead of halting. - The exception handler of every instrumented call site records a StackOverflowError that reaches the target's code (DUP, INSTANCEOF, IOR into a static field; no call, no branch) before INVOKEMETHOD_EXCEPTION. - The trace of such an execution is marked as context loss, and as stackOverflow when the error reached the target: that path depends on the JVM's stack size, which the task does not fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The executor marks a trace with stackOverflow when a StackOverflowError reached the target's code. Whether such an execution fails an assertion or throws a RuntimeException depends on the JVM's stack size, not on the task, so the driver drops its violation lines (recorded as the execution error stack_overflow_violation) and continues the search; the trace's context loss keeps the run from ending SAFE. An execution without symbolic inputs that overflowed ends UNKNOWN instead of SAFE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose
added a commit
that referenced
this pull request
Oct 19, 2026
_extract_variables: one copy (#82 cherry-picks #72's patch). StrategyService.py, Database.py: #82 walks the Database's tree under the lock; visit_tree passes the solved and #74's retired branches, in #72's 'with lock' style. SVCompDriver.py: #82's shallow snapshot of the target node for check_divergence, with #74's solution record and #80's guess flag; the ingest-failure (#72) and stack-overflow (#78) checks read the Database without copying the tree. DTOBuilder.java: #82's FormulaDumper with #71's SymbolRegistry check. DFS.py: #82 logs the per-node line only with an SA graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose
added a commit
that referenced
this pull request
Oct 19, 2026
SVCompDriver.py: - one C1-only option (#82 and #88 both added it), in #88's EXECUTOR_JVM_OPTIONS, with #88's budget-derived heap; - determine_next_step drops a stack-overflow run's violations (#78) before #88's out-of-memory and resource-limit handling, and reads the Database without copying the tree (#82); - a run stopped by the budget is not checked for divergence; #72's no-trace check applies to the others; - every query, first or retry (#74), runs under the budget's interrupt; - #88's cut-short check joins #72's incomplete-search check, and both apply to every SAFE; - the stats keep the pre-check (#73), divergence retries (#74) and resources (#88). DFS.py: the per-node line is at #88's TRACE level, with the CHOICE label. TestcaseStats.py, test_solver_rlimit.py: both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose
added a commit
that referenced
this pull request
Oct 19, 2026
Combining #72, #74, #78, #82 and #88: the driver tests create #88's budget and verdict lock, and #88's tests get the stack-overflow (#78) and copy-free (#82) Database reads and the choice flag (#76). A run reports one verdict (#88), so the incomplete-search subtests reset the flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
Contributor
Author
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.
What changed and why
When a recursion runs deep, the target's frames plus SWAT's per-instruction frames can use up the JVM stack. The StackOverflowError can then be thrown anywhere, including inside SWAT. Before this PR, the second overflow usually hit the lazy
<clinit>ofErrorHandler, which then failed. That had three effects:NoClassDefFoundError.isActivestayed set.catchdid not run.Which outcome a run got was nondeterministic: either a halt (UNKNOWN), or a truncated trace sent with no marker, which is a false-SAFE hazard (
stackoverflow-hits-errorhandler-clinit).Executor
ErrorHandlerand the newStackOverflowGuardare initialized inpremain, not on first use.processInstructioncatches a StackOverflowError raised inside SWAT using only field writes. It swallows the error, so the target continues as it would without SWAT. Symbolic tracking stops for the rest of the run.isActiveis now reset infinally.ErrorHandlerpasses a stack overflow on (it no longer halts or logs on an exhausted stack). This includes anExceptionInInitializerErrorcaused by an overflow.DUP; INSTANCEOF; GETSTATIC; IOR; PUTSTATIC, with no call and no branch), beforeINVOKEMETHOD_EXCEPTION.symbolicContextLoss, plus the newTraceDTO.stackOverflowflag when the error reached the target.Explorer
stackOverflow, the driver does not report that round's violation lines (an AssertionError under valid-assert, an uncaught RuntimeException under no-runtime-exception). It records them as the execution errorstack_overflow_violationand keeps searching. Context loss still blocks SAFE.stats.jsoncountsstack_overflow_traces.Semantics. StackOverflowError is not a
RuntimeException, so an uncaught one does not violateno-runtime-exception(G ! uncaught(java.lang.RuntimeException)). Whether a path overflows at all depends on the JVM's stack size, which the task does not fix:So an execution in which the target saw an overflow yields neither SAFE nor VIOLATION. A path where only SWAT overflowed and the target finished normally is a real execution: its violations stay valid, and only SAFE is blocked.
-Xssis unchanged. Instrumented reach is about 4.9k frames at 1 MB, 9.4k at 2 MB and 18.9k at 4 MB. A larger stack would not make any verdict sound: every overflow is already context loss. It would also make the deep rounds slower, because trace encoding is quadratic in depth (about 47 s for a 13.6k-branch trace at 1 MB). Finally, it could expose violations like SatPrimes01's that the labels exclude.Measured
Setup:
--sv-comp, 300 s,--mem15g, no SA, againstbaseline-results.jsonl. Runs measured: 1116.executor-stackoverflow,stackoverflow-hits-errorhandler-clinit,stackoverflow-not-modelled-as-program-outcome,executor-slow-or-nonterminating-runanduncaught-oom-huge-nondet-array, plus all of jayhorn-recursive.Result on this branch alone: 0 points gained, 0 lost, no new wrong verdicts. All 1040 baseline-correct runs are still correct.
Why the four runs flip from unknown to stall: the executor no longer halts, so the trace of the overflowing round is sent. On these runs it is longer than 3333 branches, which crashes the recursive tree construction (
recursion-error-tree, fixed byfix/explorer-robustness). Combined withfix/explorer-robustness. I ran the 76 affected runs on two setups: this branch merged withfix/explorer-robustness, andfix/explorer-robustnessalone on the base executor. Combined: +3 points against the baseline. Robustness fix alone: +2. No new wrong verdicts in either.[SWAT Exception]after the overflowThe other differences are 0-point:
On its own the gain is soundness, not points. The overflow no longer replaces the target's error, the target's handlers run, and a truncated trace can no longer be taken for a complete path.
Tests
StackOverflowAgentSpec(agentTest, forked agent with-Xss256k) covers a shallow recursion, an overflow caught by the target, and an uncaught overflow. On the base jar, the caught case prints no result, ends inNoClassDefFoundError(ErrorHandler), and the trace has no context loss.tests/test_stack_overflow.pycovers driver violation suppression, other rounds, a SWAT exception, and no symbolic vars.../gradlew testandagentTestpass. The explorer unit tests pass.Known limits
processInstructionreaches the target as a StackOverflowError at a non-invoke instruction. It is still recorded by the next call-site handler.OutOfMemoryError(uncaught-oom-huge-nondet-array) is the target's own error, not this mechanism, and is unchanged.fix/explorer-robustnessinDatabase.add_trace,ConstraintService.add_constraintsand the no-symbolic-vars check ofSVCompDriver. The conflicts are mechanical: keep both sides and passstack_overflowthrough.🤖 Generated with Claude Code