diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/common/ErrorHandler.java b/symbolic-executor/src/main/java/de/uzl/its/swat/common/ErrorHandler.java
index 18a888a1..95bb5338 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/common/ErrorHandler.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/common/ErrorHandler.java
@@ -50,6 +50,14 @@ public void raiseException(String msg) {
* @param t the throwable to handle
*/
public void handleException(String msg, Throwable t) {
+ // A stack overflow is not a SWAT failure: stop the symbolic tracking (the trace becomes
+ // context loss) and let the error take its course in the target, as without SWAT. Nothing
+ // is logged, the stack is exhausted.
+ Error stackOverflow = StackOverflowGuard.findStackOverflow(t);
+ if (stackOverflow != null) {
+ StackOverflowGuard.trackingLost = true;
+ throw stackOverflow;
+ }
long instCnt = -1;
try {
instCnt = ThreadHandler.getInstructionCount(currentThread().getId());
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/common/StackOverflowGuard.java b/symbolic-executor/src/main/java/de/uzl/its/swat/common/StackOverflowGuard.java
new file mode 100644
index 00000000..df54fa86
--- /dev/null
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/common/StackOverflowGuard.java
@@ -0,0 +1,60 @@
+package de.uzl.its.swat.common;
+
+/**
+ * Records that the JVM stack overflowed during the execution. Deep recursion in the target plus the
+ * frames of the symbolic tracking can exhaust the stack, and a {@link StackOverflowError} can then
+ * strike anywhere: in the target's own frames or inside SWAT while an instruction is processed.
+ *
+ *
+ * - {@link #trackingLost}: a StackOverflowError was raised inside SWAT. The shadow state may be
+ * half updated, so symbolic tracking stops for the rest of the execution; the error is
+ * swallowed, so the target continues as it would without SWAT.
+ *
- {@link #targetOverflowed}: a StackOverflowError reached the target's code. It is set by the
+ * instrumented exception handler of every call site (a PUTSTATIC, no call) before the
+ * exception is reported to SWAT. The path then depends on the stack size of the JVM, which
+ * the task semantics do not fix, so neither a SAFE nor a VIOLATION found on it can be trusted.
+ *
+ *
+ * Both end the symbolic tracking and mark the trace as context loss. The fields are written
+ * without any call, so that they can be set on an exhausted stack; the class is initialised
+ * eagerly by the agent for the same reason. They are global, like the instruction processor whose
+ * state they guard.
+ */
+public final class StackOverflowGuard {
+
+ public static volatile boolean trackingLost = false;
+ public static volatile boolean targetOverflowed = false;
+
+ private StackOverflowGuard() {}
+
+ /** Forces the initialisation of the class (see the class comment). */
+ public static void init() {}
+
+ /** Whether symbolic tracking has stopped because of a stack overflow. */
+ public static boolean isOverflowed() {
+ return trackingLost | targetOverflowed;
+ }
+
+ /**
+ * Returns the StackOverflowError behind {@code t}, or the initialiser error of a class whose
+ * static initialiser it broke, or null if {@code t} is not caused by a stack overflow.
+ */
+ public static Error findStackOverflow(Throwable t) {
+ for (int depth = 0; t != null && depth < 16; depth++, t = t.getCause()) {
+ if (t instanceof StackOverflowError e) {
+ return e;
+ }
+ if (t instanceof ExceptionInInitializerError e
+ && String.valueOf(e.getMessage()).contains("java.lang.StackOverflowError")) {
+ return e;
+ }
+ }
+ return null;
+ }
+
+ /** Clears the state; only for tests that run several executions in one JVM. */
+ public static void reset() {
+ trackingLost = false;
+ targetOverflowed = false;
+ }
+}
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/Transformer.java b/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/Transformer.java
index bb29b91e..aa76fe5d 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/Transformer.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/Transformer.java
@@ -3,6 +3,7 @@
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import de.uzl.its.swat.common.ErrorHandler;
+import de.uzl.its.swat.common.StackOverflowGuard;
import de.uzl.its.swat.common.PrintBox;
import de.uzl.its.swat.common.Util;
import de.uzl.its.swat.common.exceptions.InstrumentationException;
@@ -134,6 +135,10 @@ public static void logMissedClasses(){
@SuppressWarnings("unused")
public static void premain(String agentArgs, Instrumentation inst) {
try {
+ // Initialised here, not on first use: that can be on an exhausted stack, where a
+ // failing static initialiser would break the class for the rest of the run
+ StackOverflowGuard.init();
+ new ErrorHandler();
ThreadHandler.init();
ThreadHandler.addThreadContext(-1, "SymbolicThread", -1); // Main thread used during initialization and instrumentation
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/instruction/InstructionMethodAdapter.java b/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/instruction/InstructionMethodAdapter.java
index b24ee90c..82975e31 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/instruction/InstructionMethodAdapter.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/instruction/InstructionMethodAdapter.java
@@ -1,6 +1,7 @@
package de.uzl.its.swat.instrument.instruction;
import de.uzl.its.swat.common.ErrorHandler;
+import de.uzl.its.swat.common.StackOverflowGuard;
import de.uzl.its.swat.common.exceptions.InstrumentationException;
import de.uzl.its.swat.common.exceptions.SWATAssert;
import de.uzl.its.swat.config.Config;
@@ -1521,6 +1522,23 @@ private boolean isFieldGetFieldMethod(String method) {
return getFieldMethods.contains(method);
}
+ /**
+ * Records a StackOverflowError that reaches the exception handler of a call site in
+ * StackOverflowGuard.targetOverflowed, before the exception is reported to SWAT. Expects the
+ * throwable on top of the operand stack and leaves it there. No call and no branch: on an
+ * exhausted stack a call could replace the exception in flight.
+ *
+ * targetOverflowed |= (throwable instanceof StackOverflowError)
+ */
+ private void addStackOverflowCheck(MethodVisitor mv) {
+ String guard = Type.getInternalName(StackOverflowGuard.class);
+ mv.visitInsn(DUP);
+ mv.visitTypeInsn(INSTANCEOF, "java/lang/StackOverflowError");
+ mv.visitFieldInsn(GETSTATIC, guard, "targetOverflowed", "Z");
+ mv.visitInsn(IOR);
+ mv.visitFieldInsn(PUTSTATIC, guard, "targetOverflowed", "Z");
+ }
+
private void addMethodWithTryCatch(
int opcode, String owner, String name, String desc, boolean itf) {
@@ -1580,6 +1598,7 @@ private void addMethodWithTryCatch(
mv.visitJumpInsn(GOTO, jumpTarget);
mv.visitLabel(handler);
+ addStackOverflowCheck(mv);
addJpushInsn(mv, instrumentationState.incAndGetId());
addJpushInsn(mv, invokeId);
@@ -1656,6 +1675,7 @@ public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object.
mv.visitJumpInsn(GOTO, jumpTarget); // still try catch
mv.visitLabel(handler);
+ addStackOverflowCheck(mv);
addJpushInsn(mv, instrumentationState.incAndGetId());
addJpushInsn(mv, invokeId);
mv.visitMethodInsn(
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/processor/SymbolicInstructionProcessor.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/processor/SymbolicInstructionProcessor.java
index cd419184..5b0e9228 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/processor/SymbolicInstructionProcessor.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/processor/SymbolicInstructionProcessor.java
@@ -3,6 +3,7 @@
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import de.uzl.its.swat.common.ErrorHandler;
+import de.uzl.its.swat.common.StackOverflowGuard;
import de.uzl.its.swat.common.Util;
import de.uzl.its.swat.common.exceptions.SWATAssert;
import de.uzl.its.swat.common.logging.GlobalLogger;
@@ -175,6 +176,10 @@ private boolean isActualMethodNameExpectedMethodName(String actualMethodName, St
*/
@Override
public void processInstruction(Instruction nextInstruction) {
+ // After a stack overflow the shadow state is no longer reliable: stop tracking
+ if (StackOverflowGuard.trackingLost | StackOverflowGuard.targetOverflowed) {
+ return;
+ }
try {
SWATAssert.enforce(!isActive, "Symbolic execution is already active!");
isActive = true;
@@ -350,13 +355,22 @@ && adoptLambdaFrame(visitor.getStack(), expectedCnameFull,
ThreadHandler.setCurrentInstruction(id, ThreadHandler.getNextInstruction(id));
}
+ } catch (StackOverflowError e) {
+ // Only field writes here: the stack is still exhausted. The error is swallowed, so the
+ // target continues as it would without SWAT; the trace is marked as context loss.
+ StackOverflowGuard.trackingLost = true;
} catch (Throwable t) {
- new ErrorHandler().handleException("Error visiting Instruction!", t);
+ if (StackOverflowGuard.findStackOverflow(t) != null) {
+ StackOverflowGuard.trackingLost = true;
+ } else {
+ new ErrorHandler().handleException("Error visiting Instruction!", t);
+ }
} finally {
- long id = Thread.currentThread().getId();
- ThreadHandler.checkThreadContextAbortTimer(id);
+ isActive = false;
+ }
+ if (!(StackOverflowGuard.trackingLost | StackOverflowGuard.targetOverflowed)) {
+ ThreadHandler.checkThreadContextAbortTimer(Thread.currentThread().getId());
}
- isActive = false;
}
/**
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/DTOBuilder.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/DTOBuilder.java
index e272cf71..dacb6da9 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/DTOBuilder.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/DTOBuilder.java
@@ -8,6 +8,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.uzl.its.swat.common.ErrorHandler;
+import de.uzl.its.swat.common.StackOverflowGuard;
import de.uzl.its.swat.common.exceptions.NoThreadContextException;
import de.uzl.its.swat.common.exceptions.NotImplementedException;
import de.uzl.its.swat.common.logging.GlobalLogger;
@@ -148,8 +149,14 @@ private static TraceDTO buildTraceDTO(SymbolicTrace symbolicTrace, StatsStorage
trace.add(new BranchDTO(se.getIid(), se.getInst()));
}
}
- return new TraceDTO(inputs, trace, ufs, buildMissingInvocations(statsStorage), Witness.getNondetReturns(), Witness.getViolation(), symbolicTrace.isSymbolicContextLoss(), symbolicPrecisionLoss, symbolicTrace.isReferenceSemanticChange(),
- symbolicTrace.isSymbolicPrecisionLoss());
+ // Symbolic tracking stopped at a stack overflow: the rest of the execution is not in the trace
+ boolean stackOverflow = StackOverflowGuard.isOverflowed();
+ if (stackOverflow) {
+ logger.warn("Stack overflow during the execution (in SWAT: {}, reached the target: {}): recording symbolic context loss",
+ StackOverflowGuard.trackingLost, StackOverflowGuard.targetOverflowed);
+ }
+ return new TraceDTO(inputs, trace, ufs, buildMissingInvocations(statsStorage), Witness.getNondetReturns(), Witness.getViolation(), symbolicTrace.isSymbolicContextLoss() || stackOverflow, symbolicPrecisionLoss, symbolicTrace.isReferenceSemanticChange(),
+ symbolicTrace.isSymbolicPrecisionLoss(), StackOverflowGuard.targetOverflowed);
}
protected static String encodeCoverage(InstrCoverage instrCoverage) throws JsonProcessingException {
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/dto/TraceDTO.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/dto/TraceDTO.java
index bc99d00f..a9046e05 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/dto/TraceDTO.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/trace/dto/TraceDTO.java
@@ -35,8 +35,12 @@ public class TraceDTO {
// which also covers exact UF-based models, solutions from such a trace are only guesses.
@SuppressWarnings("unused")
private boolean approximateModel = false;
+ // A StackOverflowError reached the target's code: the path depends on the JVM's stack size,
+ // so a violation of this execution is not a verdict (see StackOverflowGuard)
+ @SuppressWarnings("unused")
+ private boolean stackOverflow = false;
- public TraceDTO(ArrayList inputs, ArrayList trace, ArrayList ufs, ArrayList missingInvocations, ArrayList nondetReturns, ViolationDTO violation, boolean symbolicContextLoss, boolean symbolicPrecisionLoss, boolean referenceSemanticChange, boolean approximateModel) {
+ public TraceDTO(ArrayList inputs, ArrayList trace, ArrayList ufs, ArrayList missingInvocations, ArrayList nondetReturns, ViolationDTO violation, boolean symbolicContextLoss, boolean symbolicPrecisionLoss, boolean referenceSemanticChange, boolean approximateModel, boolean stackOverflow) {
this.trace = trace;
this.inputs = inputs;
this.ufs = ufs;
@@ -47,6 +51,7 @@ public TraceDTO(ArrayList inputs, ArrayList trace, ArrayLis
this.symbolicPrecisionLoss = symbolicPrecisionLoss;
this.referenceSemanticChange = referenceSemanticChange;
this.approximateModel = approximateModel;
+ this.stackOverflow = stackOverflow;
}
diff --git a/symbolic-executor/src/test/groovy/de/uzl/its/swat/symbolic/StackOverflowAgentSpec.groovy b/symbolic-executor/src/test/groovy/de/uzl/its/swat/symbolic/StackOverflowAgentSpec.groovy
new file mode 100644
index 00000000..81ddf82a
--- /dev/null
+++ b/symbolic-executor/src/test/groovy/de/uzl/its/swat/symbolic/StackOverflowAgentSpec.groovy
@@ -0,0 +1,116 @@
+package de.uzl.its.swat.symbolic
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import spock.lang.Shared
+import spock.lang.Specification
+
+import javax.tools.ToolProvider
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.concurrent.TimeUnit
+
+/**
+ * L2 (forked agent): a StackOverflowError in the target behaves as without SWAT. The target
+ * recurses as deep as its first input, on a small stack so that the overflow comes quickly; the
+ * second input selects whether main catches the overflow. Run in {@code solver.mode=PRINT}, the
+ * target must produce its own output, and the trace must be sent and marked as context loss and as
+ * stack overflow. Runs via the {@code agentTest} Gradle task (needs the agent jar from
+ * {@code copyJar}).
+ */
+class StackOverflowAgentSpec extends Specification {
+
+ static final Path AGENT_JAR = Path.of("lib", "symbolic-executor.jar").toAbsolutePath()
+ static final Path NATIVE_LIBS = Path.of("..", "libs", "java-library-path").toAbsolutePath()
+ static final Path TARGET_SOURCES = Path.of("src", "test", "resources", "targets", "stackoverflow").toAbsolutePath()
+ static final String DEEP = "1000000000"
+
+ @Shared Path classes
+
+ def setupSpec() {
+ assert Files.isRegularFile(AGENT_JAR): "agent jar missing, run copyJar first: ${AGENT_JAR}"
+ classes = Files.createTempDirectory("swat-stackoverflow-agent-spec")
+ compile(classes)
+ }
+
+ def cleanupSpec() {
+ classes?.toFile()?.deleteDir()
+ }
+
+ def "a shallow recursion is tracked as usual"() {
+ when:
+ def out = run(classes, "100", "0")
+ def trace = parseTrace(out)
+
+ then:
+ out.contains("result 100")
+ !trace.stackOverflow
+ !trace.symbolicContextLoss
+ trace.violation == null
+ }
+
+ def "an overflow caught by the target is handled by the target and recorded in the trace"() {
+ when:
+ def out = run(classes, DEEP, "0")
+ def trace = parseTrace(out)
+
+ then: "the target's handler ran, and SWAT neither crashed nor halted"
+ out.contains("result -1")
+ !out.contains("[SWAT Exception]")
+ !out.contains("NoClassDefFoundError")
+
+ and: "the trace up to the overflow is sent, and cannot lead to SAFE or to a violation"
+ trace.stackOverflow
+ trace.symbolicContextLoss
+ trace.trace.size() > 0
+ }
+
+ def "an uncaught overflow ends the target as without SWAT"() {
+ when:
+ def out = run(classes, DEEP, "1")
+ def trace = parseTrace(out)
+
+ then:
+ !out.contains("result")
+ !out.contains("[SWAT Exception]")
+ !out.contains("[SWAT] Uncaught top-level RuntimeException")
+ trace.violation.throwable == "java.lang.StackOverflowError"
+ trace.stackOverflow
+ trace.symbolicContextLoss
+ }
+
+ private static void compile(Path outDir) {
+ def sources = Files.walk(TARGET_SOURCES).filter { it.toString().endsWith(".java") }.collect { it.toString() }
+ def args = ["-source", "1.8", "-target", "1.8", "-g", "-d", outDir.toString()] + sources
+ def err = new ByteArrayOutputStream()
+ int rc = ToolProvider.getSystemJavaCompiler().run(null, null, err, args as String[])
+ assert rc == 0: "javac failed: ${err}"
+ }
+
+ private static String run(Path classes, String n, String uncaught) {
+ def java = Path.of(System.getProperty("java.home"), "bin", "java").toString()
+ def cmd = [java,
+ "-Xss256k",
+ "-javaagent:${AGENT_JAR}".toString(),
+ "-Djava.library.path=${NATIVE_LIBS}".toString(),
+ "-Dconfig.path=/nonexistent/swat.cfg",
+ "-Dinstrumentation.transformer=SV_COMP",
+ "-Dsolver.mode=PRINT",
+ "-DexitOnError=true",
+ "-DlogShadowStateToConsole=false",
+ "-Dswat.input.I_0=${n}".toString(),
+ "-Dswat.input.I_1=${uncaught}".toString(),
+ "-cp", classes.toString(), "-ea", "Main"]
+ def proc = new ProcessBuilder(cmd).directory(classes.toFile()).redirectErrorStream(true).start()
+ def out = proc.inputStream.text
+ assert proc.waitFor(5, TimeUnit.MINUTES): "target did not finish:\n${out}"
+ return out
+ }
+
+ /** The TraceDTO is the single pretty-printed JSON object whose braces start at column 0. */
+ private static Map parseTrace(String out) {
+ int start = out.indexOf("\n{\n")
+ int end = out.indexOf("\n}\n", start)
+ assert start >= 0 && end > start: "no trace JSON in output:\n${out}"
+ return new ObjectMapper().readValue(out.substring(start + 1, end + 2), Map)
+ }
+}
diff --git a/symbolic-executor/src/test/resources/targets/stackoverflow/Main.java b/symbolic-executor/src/test/resources/targets/stackoverflow/Main.java
new file mode 100644
index 00000000..6f36def0
--- /dev/null
+++ b/symbolic-executor/src/test/resources/targets/stackoverflow/Main.java
@@ -0,0 +1,31 @@
+import org.sosy_lab.sv_benchmarks.Verifier;
+
+/** Recursion as deep as the first input; the second input selects whether main catches an overflow. */
+public class Main {
+ static int depth(int n) {
+ if (n == 0) {
+ return 0;
+ }
+ return 1 + depth(n - 1);
+ }
+
+ public static void main(String[] args) {
+ int n = Verifier.nondetInt();
+ boolean catchOverflow = Verifier.nondetInt() == 0;
+ if (n < 0) {
+ return;
+ }
+ if (!catchOverflow) {
+ System.out.println("result " + depth(n));
+ return;
+ }
+ int r;
+ try {
+ r = depth(n);
+ } catch (StackOverflowError e) {
+ r = -1;
+ }
+ System.out.println("result " + r);
+ assert r == n;
+ }
+}
diff --git a/symbolic-executor/src/test/resources/targets/stackoverflow/org/sosy_lab/sv_benchmarks/Verifier.java b/symbolic-executor/src/test/resources/targets/stackoverflow/org/sosy_lab/sv_benchmarks/Verifier.java
new file mode 100644
index 00000000..2dcbc9ac
--- /dev/null
+++ b/symbolic-executor/src/test/resources/targets/stackoverflow/org/sosy_lab/sv_benchmarks/Verifier.java
@@ -0,0 +1,46 @@
+package org.sosy_lab.sv_benchmarks;
+
+/** Minimal stand-in for the sv-benchmarks Verifier; the SV-COMP transformer redirects the calls. */
+public final class Verifier {
+ public static void assume(boolean condition) {
+ if (!condition) {
+ Runtime.getRuntime().halt(1);
+ }
+ }
+
+ public static boolean nondetBoolean() {
+ return false;
+ }
+
+ public static byte nondetByte() {
+ return 0;
+ }
+
+ public static char nondetChar() {
+ return 0;
+ }
+
+ public static short nondetShort() {
+ return 0;
+ }
+
+ public static int nondetInt() {
+ return 0;
+ }
+
+ public static long nondetLong() {
+ return 0;
+ }
+
+ public static float nondetFloat() {
+ return 0;
+ }
+
+ public static double nondetDouble() {
+ return 0;
+ }
+
+ public static String nondetString() {
+ return "";
+ }
+}
diff --git a/symbolic-explorer/constraint/ConstraintController.py b/symbolic-explorer/constraint/ConstraintController.py
index b00359d9..29e7b7aa 100755
--- a/symbolic-explorer/constraint/ConstraintController.py
+++ b/symbolic-explorer/constraint/ConstraintController.py
@@ -45,6 +45,7 @@ def post(request: ConstraintRequest, endpointID: str = Query(...), traceID: str
nondetReturns = request.nondetReturns
violation = request.violation
approximateModel = request.approximateModel
+ stackOverflow = request.stackOverflow
# Start a new thread to add constraints
thread = threading.Thread(target=ConstraintService.add_constraints, kwargs={
@@ -59,7 +60,8 @@ def post(request: ConstraintRequest, endpointID: str = Query(...), traceID: str
'reference_semantic_change': referenceSemanticChange,
'nondet_returns': nondetReturns,
'violation': violation,
- 'approximate_model': approximateModel})
+ 'approximate_model': approximateModel,
+ 'stack_overflow': stackOverflow})
thread.start()
thread.join() # To ensure trace is added in SV-Comp mode
# Return a response indicating that the request has been accepted
diff --git a/symbolic-explorer/constraint/ConstraintService.py b/symbolic-explorer/constraint/ConstraintService.py
index 7bba9d09..c9171edd 100755
--- a/symbolic-explorer/constraint/ConstraintService.py
+++ b/symbolic-explorer/constraint/ConstraintService.py
@@ -27,7 +27,8 @@ def add_constraints(endpoint_id: str, trace_id: str, trace: List[TraceItem], inp
missing_invocations: List[InvocationItem] = None,
nondet_returns: List[NondetReturnItem] = None,
violation: Optional[ViolationItem] = None,
- approximate_model: bool = False):
+ approximate_model: bool = False,
+ stack_overflow: bool = False):
"""
Adds constraints to the database.
@@ -49,6 +50,8 @@ def add_constraints(endpoint_id: str, trace_id: str, trace: List[TraceItem], inp
violation (ViolationItem): The throwable that escaped the entry method, if any.
approximate_model (bool): Whether an approximate model (a sampled Math function) was used,
so that solutions built from this trace are only guesses.
+ stack_overflow (bool): Whether a StackOverflowError reached the target's code, so that a
+ violation of this execution depends on the JVM's stack size.
Returns:
None: The result is the side effect of adding data to the database.
@@ -71,5 +74,5 @@ def add_constraints(endpoint_id: str, trace_id: str, trace: List[TraceItem], inp
'violation': violation.model_dump() if violation is not None else None,
}
# Adding the trace and inputs to the database for the specified endpoint.
- Database.instance().add_trace(endpoint_id, trace_id, trace_parsed, inputs_parsed, ufs_parsed, symbolic_context_loss, symbolic_precision_loss, reference_semantic_change, missing_invocations_data, witness_data, approximate_model)
+ Database.instance().add_trace(endpoint_id, trace_id, trace_parsed, inputs_parsed, ufs_parsed, symbolic_context_loss, symbolic_precision_loss, reference_semantic_change, missing_invocations_data, witness_data, approximate_model, stack_overflow)
logger.info(f'[CONSTRAINT SERVICE] Added trace {trace_id} to endpoint {endpoint_id}')
diff --git a/symbolic-explorer/data/BinaryExecutionTree/Tree.py b/symbolic-explorer/data/BinaryExecutionTree/Tree.py
index 978dc99e..613a8f53 100755
--- a/symbolic-explorer/data/BinaryExecutionTree/Tree.py
+++ b/symbolic-explorer/data/BinaryExecutionTree/Tree.py
@@ -36,6 +36,9 @@ def __init__(self, endpoint_id: Union[str, int]):
# Traces that used an approximate model (a sampled Math function). A solution built from one
# is a guess, so the next run may take another path than predicted without anything being wrong.
self.approximate_traces: Set[str] = set()
+ # Traces whose execution had a StackOverflowError reach the target's code: their outcome
+ # depends on the JVM's stack size, so a violation they show is not a verdict.
+ self.stack_overflow_traces: Set[str] = set()
# How often the next run did take another path than such a guessed solution predicted.
self.expected_divergences: int = 0
self.uncaught_exceptions: int = 0
diff --git a/symbolic-explorer/data/Database.py b/symbolic-explorer/data/Database.py
index 53bf1882..79bb476e 100755
--- a/symbolic-explorer/data/Database.py
+++ b/symbolic-explorer/data/Database.py
@@ -119,7 +119,8 @@ def add_trace(self, endpoint_id: Union[str, int], trace_id: str, trace: list[Bra
symbolic_context_loss: bool, symbolic_precision_loss: bool,
reference_semantic_change: bool = False, missing_invocations: list = None,
witness_data: dict = None,
- approximate_model: bool = False):
+ approximate_model: bool = False,
+ stack_overflow: bool = False):
endpoint_id = str(endpoint_id)
lock.acquire()
@@ -136,6 +137,7 @@ def add_trace(self, endpoint_id: Union[str, int], trace_id: str, trace: list[Bra
self.tree[endpoint_id].record_precision_loss() if symbolic_precision_loss else None
self.tree[endpoint_id].record_reference_semantic_change() if reference_semantic_change else None
self.tree[endpoint_id].approximate_traces.add(trace_id) if approximate_model else None
+ self.tree[endpoint_id].stack_overflow_traces.add(trace_id) if stack_overflow else None
lock.release()
@@ -187,6 +189,14 @@ def is_approximate_trace(self, endpoint_id: Union[str, int], trace_id: str) -> b
lock.release()
return ret
+ def is_stack_overflow_trace(self, endpoint_id: Union[str, int], trace_id: str) -> bool:
+ """Whether a StackOverflowError reached the target's code in the execution of the trace."""
+ endpoint_id = str(endpoint_id)
+ lock.acquire()
+ ret = trace_id in self.tree[endpoint_id].stack_overflow_traces
+ lock.release()
+ return ret
+
def record_expected_divergence(self, endpoint_id: Union[str, int]):
endpoint_id = str(endpoint_id)
lock.acquire()
diff --git a/symbolic-explorer/driver/SVCompDriver.py b/symbolic-explorer/driver/SVCompDriver.py
index a6c70f11..c412e96e 100644
--- a/symbolic-explorer/driver/SVCompDriver.py
+++ b/symbolic-explorer/driver/SVCompDriver.py
@@ -165,7 +165,12 @@ def determine_next_step(self, status: ExecutionStatus, output: List[str] ) -> Ac
"""Determines the next step based on the execution status."""
if status == ExecutionStatus.SUCCESS:
-
+ # A StackOverflowError reached the target in this execution: its outcome depends on the
+ # JVM's stack size, which the task does not fix, so a violation it shows is not reported.
+ # The trace carries context loss, so the search goes on but cannot end in SAFE.
+ stack_overflow = self.round_has_stack_overflow()
+ if stack_overflow:
+ output = self.drop_violations(output)
for l in output:
if "java.lang.AssertionError: [SWAT]" in l:
# Internal assertion error in the DSE
@@ -192,6 +197,11 @@ def determine_next_step(self, status: ExecutionStatus, output: List[str] ) -> Ac
if len(Database.instance().get_tree(ENDPOINT_ID).symbolic_vars) == 0:
+ if stack_overflow:
+ # The only execution there is overflowed the stack: its outcome is not a verdict
+ logger.info(f'[SVCOMP] No symbolic vars found, but the execution overflowed the stack')
+ self.state.verdict = Verdict.UNKNOWN
+ return Action.REPORTVERDICT
# No symbolic vars found, safe to assume no violation
logger.info(f'[SVCOMP] No symbolic vars found')
self.state.verdict = Verdict.NO_SYMBOLIC_VARS
@@ -208,6 +218,28 @@ def determine_next_step(self, status: ExecutionStatus, output: List[str] ) -> Ac
return Action.REPORTVERDICT
+ def is_violation_line(self, line: str) -> bool:
+ """Whether the executor output line reports a violation of the checked property."""
+ if self.verification_category == VerificationCategory.VALID_ASSERT_PRP:
+ return "java.lang.AssertionError" in line and "java.lang.AssertionError: [SWAT]" not in line
+ if self.verification_category == VerificationCategory.NO_RUNTIME_EXCEPTION_PRP:
+ return "[SWAT] Uncaught top-level RuntimeException in symbolic execution" in line
+ return False
+
+ def drop_violations(self, output: List[str]) -> List[str]:
+ """Removes the violation lines of an execution whose outcome is not a verdict."""
+ violations = [l for l in output if self.is_violation_line(l)]
+ if violations:
+ logger.warning(f'[SVCOMP] Violation after a stack overflow, not reported: {violations[0].strip()}')
+ Database.instance().record_execution_error(ENDPOINT_ID, 'stack_overflow_violation', violations[0].strip())
+ return [l for l in output if not self.is_violation_line(l)]
+
+ def round_has_stack_overflow(self) -> bool:
+ """Whether the trace of this round (if one arrived) reports a stack overflow in the target."""
+ witness_data = Database.instance().get_witness_data(ENDPOINT_ID)
+ return witness_data is not None and Database.instance().is_stack_overflow_trace(
+ ENDPOINT_ID, witness_data['trace_id'])
+
def log_output(self, output: List[str]):
logger.info(f'[SVCOMP] Java Output Begin')
for line in output:
diff --git a/symbolic-explorer/parse/DataTransferObjects.py b/symbolic-explorer/parse/DataTransferObjects.py
index 68be5da1..4c6c9b1b 100644
--- a/symbolic-explorer/parse/DataTransferObjects.py
+++ b/symbolic-explorer/parse/DataTransferObjects.py
@@ -93,6 +93,9 @@ class ConstraintRequest(BaseModel):
referenceSemanticChange: bool = False
# An approximate model (a sampled Math function) was used: solutions from this trace are guesses.
approximateModel: bool = False
+ # A StackOverflowError reached the target's code: the path depends on the JVM's stack size, so a
+ # violation of this execution is not a verdict (the executor also reports context loss).
+ stackOverflow: bool = False
class CoverageRequest(BaseModel):
diff --git a/symbolic-explorer/svcomp/TestcaseStats.py b/symbolic-explorer/svcomp/TestcaseStats.py
index 6f129eaa..f76e7543 100644
--- a/symbolic-explorer/svcomp/TestcaseStats.py
+++ b/symbolic-explorer/svcomp/TestcaseStats.py
@@ -54,6 +54,7 @@ def build_testcase_stats(verdict, category, tree, symbolic_exec_iterations: int,
'symbolic_precision_loss': tree.symbolic_precision_loss,
'reference_semantic_change': tree.reference_semantic_change,
'uncaught_exceptions': tree.uncaught_exceptions,
+ 'stack_overflow_traces': len(tree.stack_overflow_traces),
},
'symbolic_vars': sorted(tree.symbolic_vars),
'missing_invocations': missing,
diff --git a/symbolic-explorer/tests/test_stack_overflow.py b/symbolic-explorer/tests/test_stack_overflow.py
new file mode 100644
index 00000000..377d8652
--- /dev/null
+++ b/symbolic-explorer/tests/test_stack_overflow.py
@@ -0,0 +1,93 @@
+"""
+Unit tests for SVCompDriver.determine_next_step on executions whose trace reports a stack overflow
+in the target: their outcome depends on the JVM's stack size, so a violation they show is not a
+verdict, and an execution without symbolic inputs is not SAFE.
+
+It imports the driver, which needs the explorer's dependencies (z3, fastapi, ...) that the system
+Python lacks, so run it with the harness venv, from symbolic-explorer/:
+ ../targets/sv-comp/scripts/.venv/bin/python3 -m unittest tests.test_stack_overflow -v
+"""
+
+import os
+import sys
+import unittest
+from types import SimpleNamespace
+from unittest import mock
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from driver import SVCompDriver as svcomp
+
+ASSERTION = 'Exception in thread "main" java.lang.AssertionError'
+RUNTIME_EXCEPTION = '[SWAT] Uncaught top-level RuntimeException in symbolic execution'
+
+
+class StackOverflowRoundTest(unittest.TestCase):
+
+ def setUp(self):
+ self.tree = SimpleNamespace(execution_errors=[], symbolic_vars={'I_0'}, uncaught_exceptions=0)
+ self.overflowed_traces = set()
+ def record_execution_error(endpoint_id, kind, message):
+ self.tree.execution_errors.append({'kind': kind, 'message': message})
+ def record_uncaught_exception(endpoint_id):
+ self.tree.uncaught_exceptions += 1
+ db = SimpleNamespace(
+ record_execution_error=record_execution_error,
+ record_uncaught_exception=record_uncaught_exception,
+ get_tree=lambda endpoint_id: self.tree,
+ get_witness_data=lambda endpoint_id: {'trace_id': 't1'},
+ is_stack_overflow_trace=lambda endpoint_id, trace_id: trace_id in self.overflowed_traces)
+ patcher = mock.patch.object(svcomp.Database, 'instance', return_value=db)
+ patcher.start()
+ self.addCleanup(patcher.stop)
+ self.driver = svcomp.SVCompDriver.__new__(svcomp.SVCompDriver)
+ self.driver.state = svcomp.State()
+
+ def step(self, category, output):
+ self.driver.verification_category = category
+ return self.driver.determine_next_step(svcomp.ExecutionStatus.SUCCESS, output)
+
+ def test_assertion_without_stack_overflow_is_a_violation(self):
+ action = self.step(svcomp.VerificationCategory.VALID_ASSERT_PRP, [ASSERTION])
+ self.assertEqual(action, svcomp.Action.REPORTVERDICT)
+ self.assertEqual(self.driver.state.verdict, svcomp.Verdict.VIOLATION)
+
+ def test_assertion_after_stack_overflow_is_not_reported(self):
+ self.overflowed_traces.add('t1')
+ action = self.step(svcomp.VerificationCategory.VALID_ASSERT_PRP, [ASSERTION])
+ self.assertEqual(action, svcomp.Action.SYMBOLICNEXT)
+ self.assertNotEqual(self.driver.state.verdict, svcomp.Verdict.VIOLATION)
+ self.assertEqual([e['kind'] for e in self.tree.execution_errors], ['stack_overflow_violation'])
+
+ def test_runtime_exception_after_stack_overflow_is_not_reported(self):
+ self.overflowed_traces.add('t1')
+ action = self.step(svcomp.VerificationCategory.NO_RUNTIME_EXCEPTION_PRP, [RUNTIME_EXCEPTION])
+ self.assertEqual(action, svcomp.Action.SYMBOLICNEXT)
+ self.assertNotEqual(self.driver.state.verdict, svcomp.Verdict.VIOLATION)
+
+ def test_swat_exception_still_ends_the_run(self):
+ self.overflowed_traces.add('t1')
+ action = self.step(svcomp.VerificationCategory.VALID_ASSERT_PRP, [ASSERTION, '[SWAT Exception]: boom'])
+ self.assertEqual(action, svcomp.Action.REPORTVERDICT)
+ self.assertEqual(self.driver.state.verdict, svcomp.Verdict.UNKNOWN)
+
+ def test_overflow_of_another_round_does_not_count(self):
+ self.overflowed_traces.add('t0')
+ self.step(svcomp.VerificationCategory.VALID_ASSERT_PRP, [ASSERTION])
+ self.assertEqual(self.driver.state.verdict, svcomp.Verdict.VIOLATION)
+
+ def test_no_symbolic_vars_after_stack_overflow_is_unknown(self):
+ self.overflowed_traces.add('t1')
+ self.tree.symbolic_vars = set()
+ action = self.step(svcomp.VerificationCategory.NO_RUNTIME_EXCEPTION_PRP, [])
+ self.assertEqual(action, svcomp.Action.REPORTVERDICT)
+ self.assertEqual(self.driver.state.verdict, svcomp.Verdict.UNKNOWN)
+
+ def test_no_symbolic_vars_without_stack_overflow_is_safe(self):
+ self.tree.symbolic_vars = set()
+ self.step(svcomp.VerificationCategory.NO_RUNTIME_EXCEPTION_PRP, [])
+ self.assertEqual(self.driver.state.verdict, svcomp.Verdict.NO_SYMBOLIC_VARS)
+
+
+if __name__ == '__main__':
+ unittest.main()