Repository navigation
Static property pre-check: SAFE for programs proven free of RuntimeExceptions or AssertionErrors - #73
Closed
nils-loose wants to merge 7 commits into
Closed
nils-loose wants to merge 7 commits into
nils-loose wants to merge 7 commits into
Conversation
ExceptionFreedomCheck analyses every class on the program's classpath with ASM and proves, conservatively, that no RuntimeException can escape Main.main or a static initializer, and whether reachable code creates an AssertionError at all. It tracks nullness, int constants, array lengths, enum ordinals and runtime types through fields, parameters and return values (least fixpoints), treats only modelled library methods as callable and fails on anything it cannot model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For no-runtime-exception and valid-assert, the SV-COMP driver runs the executor jar's ExceptionFreedomCheck before round 0. If it proves the property, round 0 only cross-checks the proof (a violation found there makes the verdict UNKNOWN) and the verdict is SAFE without exploration and without the exploration's soundness downgrades. --no-property-precheck disables it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A static initializer can only run if reachable code initializes its class (new, getstatic, putstatic, invokestatic, or a subclass being initialized), so unrelated classes on the classpath no longer make the check fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Virtual dispatch over-approximates JVM selection: every non-private instance method of the superclass chain and every default method of the superinterfaces counts (package-private overriding, private and maximally-specific default methods were resolved wrongly). - The receiver-escape rule uses a may-alias flag that survives joins. - Main.main's parameter also joins the arguments of calls to main. - ConstantValue only initializes static fields. - ldc of method handles and dynamic constants counts as unknown code. - Classes in java/ are never application classes. - String.concat/toLowerCase/toUpperCase and new StringBuilder(String) are no longer modelled: on JDK 8 they overflow for huge strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A concrete class that implements a library interface may inherit one of its default methods (Iterator.remove, Iterable.forEach, CharSequence.isEmpty), which can throw or call back into the application. Such calls now have unknown targets, unless the interface is known to have no default methods. Platform packages (javax/, jdk/, sun/) are never application classes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class on the classpath in a platform package (java/, javax/, jdk/, sun/, and the JDK 8 packages org/xml/, org/w3c/, org/omg/, org/ietf/, org/jcp/, com/sun/), in a package of the JDK's boot modules, or in the Verifier package other than Verifier and ObjectFactory, is refused or shadowed by the JVM, or would run its static initializer unanalysed. Such programs are no longer proven. 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
…property Combining #73 and #89: after round 0 a proof turned the verdict into SAFE, also for a run that never reached main (no trace), which #89 ends UNKNOWN. The proof now applies only when round 0 sent a trace. The no-trace tests get the Database reads, the pre-check and the budget of the other PRs; #72's robustness test no longer expects no_trace for a failed (ERROR) run, which ends UNKNOWN by itself. 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
A static, whole-program property pre-check for SV-COMP mode.
de.uzl.its.swat.staticcheck.ExceptionFreedomCheck(executor jar, ASM). It analyses every class on the task classpath, entered atMain.main, and proves or fails to prove two facts:exception_free: no RuntimeException can escapeMain.mainor a static initializer that may run. This is theno-runtime-exceptionproperty.assertion_free: reachable code never creates an AssertionError, so no assert can fail. This isvalid-assert.svcomp/PropertyPreCheck.py: the driver runs the check before round 0, as a subprocess with a 60 s timeout. A crash, a timeout or unparsable output proves nothing, and the run proceeds unchanged.precheck_contradictionexecution error is recorded.sa_desync) do not apply to this SAFE, because it does not rest on the exploration.stats.jsongets aproperty_precheckblock.--no-property-precheckdisables the check.Why
LIMITATIONS work item 2 (verdict-downgrade-too-coarse, no-exception-freedom-precheck, property-irrelevant-branches-explored, uncaught-exception-downgrade):
A whole-program proof answers both questions at once, for every execution. That is simpler and safer than refining each downgrade by what is reachable after the context-loss point. The proof does not use the cfg-extraction SA graph: REVIEW-cfg lists ways in which that graph is unsound (B1-B3, S1-S6), and the graph is not in the competition package.
Soundness argument
The SAFE claim needs one thing: the proof holds for all executions of the original program. Exploration events then cannot matter, since they only describe what the exploration covered.
The proof is conservative everywhere. When something is unknown, the check fails.
Closed world. Every call from reachable code (call graph plus the static initializers that reachable code may trigger) must go to one of three places:
Verifiermethods:nondet*exceptnondetObject, andassume.nondetStringis non-null;JdkModelthat never throw a RuntimeException for the stated argument conditions, and never call back into application code.Anything else fails the check, even inside a
catch:invokedynamicother than string concatenation,ldcof method handles, reflection, finalizers. Therefore application code only runs when application code calls it.Instructions. Every instruction that JVMS lets throw a RuntimeException must be proven safe, or be covered by a handler for
Throwable,ExceptionorRuntimeException(orArithmeticExceptionfor division), whose code is checked in turn. Those instructions are:xaload/xastore,arraylength,(multi)newarray,get/putfield, invoke instructions,checkcast,idiv/irem/ldiv/lrem,athrow, monitors andinvokedynamic. The values come from an abstract interpretation (ASMAnalyzer) that tracks:values().length;A caught exception may be rethrown only if no instruction its handler covers can raise a RuntimeException. A thrown value must have an exact type that is not a RuntimeException.
Facts. The facts about fields, parameters and return values are least fixpoints of the joins of all stored, passed or returned values, over all application methods. Reference fields also hold their default null. The exception is a field that every constructor (or the static initializer) assigns in a straight-line, handler-free prefix, in which:
May-throw is a least fixpoint over the call graph. An exception must start at some instruction that is checked locally.
Assumptions:
java/,javax/,jdk/,sun/, or the JDK 8 packagesorg/xml/,org/w3c/,org/omg/,org/ietf/,org/jcp/,com/sun/), in a package of the JDK's boot modules, or in the Verifier package other thanVerifier/ObjectFactory. The JVM would refuse such a class, load the JDK's class instead, or run its static initializer without analysis;Verifiersemantics hold;concat,toLowerCase,new StringBuilder(String)) are excluded;Review. An independent adversarial review ran three rounds. It wrote programs that pass the check but throw, and confirmed each one by running it. It found 11 counterexamples:
main(null);All of them are fixed and are now regression tests. Round 3 found none.
A second, independent review of this PR (
results/review-pr73.md) found 6 more counterexamples. All of them come from one root cause: classpath classes that the JVM loads differently from the model.javax/helpers runs their static initializer without analysis.java/class makes the JVM throw a SecurityException.org.xml.sax), so the JVM loads the JDK's version.These cases now fail the check, and all 6 are regression tests. The review found no task among the 998 that uses such packages. After the fix, the scan still proves the same 426 no-runtime-exception and 3 valid-assert tasks, so the measurement stands unchanged.
Measured
Configuration
--sv-comp, 300 s CPU,--mem15g, no SA, compared againstbaseline-results.jsonl. 1286 runs:Net +470, 235 flips (all UNKNOWN, timeout, stall or OOM to correct SAFE), 0 losses, 0 new wrong verdicts.
The static scan of all 998 tasks:
The median CPU time of a proven run is 2.2 s.
Tests
ExceptionFreedomCheckSpec: 63 compiled programs. 50 may throw and must never be proven; these include every counterexample from both reviews. 13 are proven.tests/test_property_precheck.py: result parsing per property, the command line, disabling, failure and timeout, and the verdict with a proof, including the contradiction case.Known limits
assume, null checks), and no collections or most String methods. Most of securibench, objects (nondetObject), juliet and the algorithms array programs stay unproven.context-loss-swat-internal-helperis not touched, so the StringCompare05 caveat does not apply.🤖 Generated with Claude Code