diff --git a/README.md b/README.md index 3bbf6943e..16e8afcb4 100644 --- a/README.md +++ b/README.md @@ -198,4 +198,4 @@ One can run the framework tests by running the check task: ## Building -`.\gradlew build -x test` \ No newline at end of file +`./gradlew build -x test` \ No newline at end of file diff --git a/dependencies/soot-analysis-0.2.1-SNAPSHOT-jar-with-dependencies.jar b/dependencies/soot-analysis-0.2.1-SNAPSHOT-jar-with-dependencies.jar index e8b20643a..444c52e27 100644 Binary files a/dependencies/soot-analysis-0.2.1-SNAPSHOT-jar-with-dependencies.jar and b/dependencies/soot-analysis-0.2.1-SNAPSHOT-jar-with-dependencies.jar differ diff --git a/src/main/services/outputProcessors/soot/ConflictDetectionAlgorithm.groovy b/src/main/services/outputProcessors/soot/ConflictDetectionAlgorithm.groovy index 54f28a21e..01683cf88 100644 --- a/src/main/services/outputProcessors/soot/ConflictDetectionAlgorithm.groovy +++ b/src/main/services/outputProcessors/soot/ConflictDetectionAlgorithm.groovy @@ -3,6 +3,7 @@ package services.outputProcessors.soot import util.ProcessRunner import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference /** * Runs a soot algorithm with: @@ -19,23 +20,29 @@ class ConflictDetectionAlgorithm { private SootAnalysisWrapper sootWrapper; private boolean interprocedural; private long depthLimit; - - - ConflictDetectionAlgorithm(String name, String mode, SootAnalysisWrapper sootWrapper, long timeout) { - this(name, mode, sootWrapper, timeout, false, 5); - } - - ConflictDetectionAlgorithm(String name, String mode, SootAnalysisWrapper sootWrapper, long timeout, boolean interprocedural) { - this(name, mode, sootWrapper, timeout, interprocedural, 5); - } - - ConflictDetectionAlgorithm(String name, String mode, SootAnalysisWrapper sootWrapper, long timeout, boolean interprocedural, long depthLimit) { - this.name = name; - this.mode = mode; - this.timeout = timeout; - this.sootWrapper = sootWrapper; - this.interprocedural = interprocedural; - this.depthLimit = depthLimit; + private String callgraph; + private boolean partialResultsOnTimeout; + + // Grace period to let the output thread finish reading buffered output after process is destroyed + private static final long GRACE_PERIOD_MILLIS = 5000L; + + + ConflictDetectionAlgorithm(String name, + String mode, + SootAnalysisWrapper sootWrapper, + long timeout, + boolean interprocedural = false, + long depthLimit = 5, + String callgraph = "SPARK", + boolean partialResultsOnTimeout = false) { + this.name = name + this.mode = mode + this.sootWrapper = sootWrapper + this.timeout = timeout + this.interprocedural = interprocedural + this.depthLimit = depthLimit + this.callgraph = callgraph + this.partialResultsOnTimeout = partialResultsOnTimeout } String getMode() { @@ -54,6 +61,10 @@ class ConflictDetectionAlgorithm { return interprocedural } + void setPartialResultsOnTimeout(boolean partialResultsOnTimeout) { + this.partialResultsOnTimeout = partialResultsOnTimeout + } + @Override String toString() { return "ConflictDetectionAlgorithm{" + @@ -69,6 +80,10 @@ class ConflictDetectionAlgorithm { return depthLimit } + String getCallgraph() { + return callgraph + } + String run(Scenario scenario) { try { println "Running ${toString()}" @@ -83,6 +98,7 @@ class ConflictDetectionAlgorithm { sootConfig.addOption("-entrypoints", scenario.getEntrypoints()); sootConfig.addOption("-depthLimit", this.getDepthLimit()); + sootConfig.addOption("-cg", this.getCallgraph()); return runAndReportResult(sootConfig); } catch (ClassNotFoundInJarException e) { @@ -92,7 +108,10 @@ class ConflictDetectionAlgorithm { protected String runAndReportResult(SootConfig sootConfig) throws InterruptedException, IOException { - String result; + // AtomicReference allows the output thread to safely publish its result to the main thread + // Default is "false": if timeout fires before any [CONFLICT_FOUND] is seen, we record false + AtomicReference atomicResult = new AtomicReference<>("false") + println "Using jar at " + sootConfig.getClassPath() File inputFile = new File(sootConfig.getInputFilePath()); @@ -103,51 +122,57 @@ class ConflictDetectionAlgorithm { Process sootProcess = sootWrapper.executeSoot(sootConfig); - // this is needed because if th waitFor command is called without reading the output - // in some executions the output buffer might get full and block the process - // so we execute both the output reading and the process waiting in parallel + // Reading output and waiting for process must run in parallel to avoid blocking + // when the output buffer fills up before the process finishes Thread processOutputThread = new Thread(new Runnable() { @Override void run() { - result = hasSootFlow(sootProcess); + atomicResult.set(hasSootFlow(sootProcess)); } }) - processOutputThread.start(); // start processing the output + processOutputThread.start(); boolean executionCompleted = true; if (timeout > 0) { - // wait for the execution to end setting a timeout executionCompleted = sootProcess.waitFor(timeout, TimeUnit.SECONDS) } - // if the timeout has been reached if (!executionCompleted) { - processOutputThread.interrupt(); // cancel the output reading thread - print ("Execution exceeded the timeout of " + timeout + " seconds") - result = "timeout"; - } else { - processOutputThread.join(); + println "Execution exceeded the timeout of ${timeout} seconds" + // Destroy the process first so its output stream closes, allowing the reader thread to exit + sootProcess.destroy(); + + if (partialResultsOnTimeout) { + // Wait for the reader thread to finish consuming any buffered output that was + // already in the pipe before the process was destroyed + processOutputThread.join(GRACE_PERIOD_MILLIS) + String partial = atomicResult.get() + println "Result at timeout: ${partial}" + return partial + "-timeout" + } else { + processOutputThread.interrupt(); + } + return "timeout"; } - - // force destroy process - // if we don't use this command some processes will keep running and consuming a lot of memory - // even after the analysis execution ends + processOutputThread.join(); + // Force destroy to prevent zombie processes that keep consuming memory sootProcess.destroy(); - return result; + return atomicResult.get(); } private String hasSootFlow(Process sootProcess) { - String result = "error" - - sootProcess.getInputStream().eachLine { - println it; - if (it.stripIndent().startsWith("Number of conflicts:")) { - result = "true" - } else if (it.stripIndent() == "No conflicts detected") { - result = "false" + String result = "false" + try { + sootProcess.getInputStream().eachLine { + println it; + if (it.stripIndent() == "[CONFLICT_FOUND]") { + result = "true" + } } + } catch (IOException ignored) { + // Stream closed because the process was destroyed (timeout case) — return whatever was found so far } return result } diff --git a/src/main/services/outputProcessors/soot/Main.groovy b/src/main/services/outputProcessors/soot/Main.groovy index 5a61a3003..f0d8f304c 100644 --- a/src/main/services/outputProcessors/soot/Main.groovy +++ b/src/main/services/outputProcessors/soot/Main.groovy @@ -40,6 +40,11 @@ class Main { sootRunner.setDetectionAlgorithms(configureDetectionAlgorithms(appArguments, sootWrapper)) } + if (appArguments.isPartialResultsOnTimeout()) { + println "Setting partial results on timeout" + sootRunner.configurePartialResultsOnTimeout(true) + } + sootRunner.executeAnalyses(outputPath) if (appArguments.isReport()) { @@ -76,7 +81,7 @@ class Main { private static ArrayList configureDetectionAlgorithms(Arguments appArguments, SootAnalysisWrapper sootWrapper) { List detectionAlgorithms = new ArrayList(); - + String callgraph = appArguments.getCallgraph(); long timeout = appArguments.getTimeout() long depthLimit = appArguments.getDepthLimit(); if (appArguments.getDfIntra()) { @@ -89,26 +94,25 @@ class Main { detectionAlgorithms.add(new ConflictDetectionAlgorithm("Confluence Intra", "dfp-confluence-intraprocedural", sootWrapper, timeout, false,depthLimit)) } if (appArguments.getCfInter()) { - detectionAlgorithms.add(new ConflictDetectionAlgorithm("Confluence Inter", "dfp-confluence-interprocedural", sootWrapper, timeout, true,depthLimit)) + detectionAlgorithms.add(new ConflictDetectionAlgorithm("Confluence Inter", "dfp-confluence-interprocedural", sootWrapper, timeout, true,depthLimit, callgraph)) } if (appArguments.getOaIntra()) { detectionAlgorithms.add(new ConflictDetectionAlgorithm("OA Intra", "oa", sootWrapper, timeout, false,depthLimit)) } if (appArguments.getOaInter()) { - detectionAlgorithms.add(new ConflictDetectionAlgorithm("OA Inter", "ioa", sootWrapper, timeout, true,depthLimit)) + detectionAlgorithms.add(new ConflictDetectionAlgorithm("OA Inter", "ioa", sootWrapper, timeout, true,depthLimit, callgraph)) } if (appArguments.getOaIntraWithoutPA()) { detectionAlgorithms.add(new ConflictDetectionAlgorithm("OA Intra Without Pointer Analysis", "oa-without-pa", sootWrapper, timeout, false,depthLimit)) } if (appArguments.getOaInterWithoutPA()) { - detectionAlgorithms.add(new ConflictDetectionAlgorithm("OA Inter Without Pointer Analysis", "ioa-without-pa", sootWrapper, timeout, true,depthLimit)) + detectionAlgorithms.add(new ConflictDetectionAlgorithm("OA Inter Without Pointer Analysis", "ioa-without-pa", sootWrapper, timeout, true,depthLimit, callgraph)) } - if (appArguments.getDfpIntra()) { detectionAlgorithms.add(new NonCommutativeConflictDetectionAlgorithm("DFP-Intra", "dfp-intra", sootWrapper, timeout, false,depthLimit)) } if (appArguments.getDfpInter()) { - detectionAlgorithms.add(new NonCommutativeConflictDetectionAlgorithm("DFP-Inter", "dfp-inter", sootWrapper, timeout, true,depthLimit)) + detectionAlgorithms.add(new NonCommutativeConflictDetectionAlgorithm("DFP-Inter", "dfp-inter", sootWrapper, timeout, true,depthLimit, callgraph)) } if (appArguments.getCd()) { detectionAlgorithms.add(new NonCommutativeConflictDetectionAlgorithm("CD", "cd", sootWrapper, timeout, false,depthLimit)) diff --git a/src/main/services/outputProcessors/soot/NonCommutativeConflictDetectionAlgorithm.groovy b/src/main/services/outputProcessors/soot/NonCommutativeConflictDetectionAlgorithm.groovy index 760e6c54d..fb4666443 100644 --- a/src/main/services/outputProcessors/soot/NonCommutativeConflictDetectionAlgorithm.groovy +++ b/src/main/services/outputProcessors/soot/NonCommutativeConflictDetectionAlgorithm.groovy @@ -1,6 +1,5 @@ package services.outputProcessors.soot - /** * Runs a soot algorithm twice, once with: * left -> source @@ -8,21 +7,19 @@ package services.outputProcessors.soot * and once with: * left -> sink * right -> source - * This is used for algorithms that are non commutative, that means different conflicts can be found running from - * left to right and right to left + * This is used for algorithms that are non-commutative, meaning that + * running left→right may produce different conflicts than right→left. */ class NonCommutativeConflictDetectionAlgorithm extends ConflictDetectionAlgorithm { - NonCommutativeConflictDetectionAlgorithm(String name, String mode, SootAnalysisWrapper sootWrapper, long timeout) { - super(name, mode, sootWrapper, timeout) - } - - NonCommutativeConflictDetectionAlgorithm(String name, String mode, SootAnalysisWrapper sootWrapper, long timeout, boolean interprocedural) { - super(name, mode, sootWrapper, timeout, interprocedural) - } - - NonCommutativeConflictDetectionAlgorithm(String name, String mode, SootAnalysisWrapper sootWrapper, long timeout, boolean interprocedural, long depthLimit) { - super(name, mode, sootWrapper, timeout, interprocedural, depthLimit); + NonCommutativeConflictDetectionAlgorithm(String name, + String mode, + SootAnalysisWrapper sootWrapper, + long timeout, + boolean interprocedural = false, + long depthLimit = 5, + String callgraph = "SPARK") { + super(name, mode, sootWrapper, timeout, interprocedural, depthLimit, callgraph) } @Override @@ -33,16 +30,23 @@ class NonCommutativeConflictDetectionAlgorithm extends ConflictDetectionAlgorith @Override String run(Scenario scenario) { try { - SootConfig configLeftRight = buildSootConfig(scenario.getLinesFilePath(), scenario); - SootConfig configRightLeft = buildSootConfig(scenario.getLinesReverseFilePath(), scenario); + println "Running ${toString()}" - println "Running left right " + toString(); - String leftRightResult = super.runAndReportResult(configLeftRight); + // LEFT → RIGHT + String filePathLeftRight = scenario.getLinesFilePath() + SootConfig configLeftRight = buildSootConfig(filePathLeftRight, scenario) - println "Running right left " + toString(); - String rightLeftResult = super.runAndReportResult(configRightLeft); + println "Running left → right ${toString()}" + String leftRightResult = runAndReportResult(configLeftRight) - return "${leftRightResult};${rightLeftResult}"; + // RIGHT → LEFT + String filePathRightLeft = scenario.getLinesReverseFilePath() + SootConfig configRightLeft = buildSootConfig(filePathRightLeft, scenario) + + println "Running right → left ${toString()}" + String rightLeftResult = runAndReportResult(configRightLeft) + + return "${leftRightResult};${rightLeftResult}" } catch (ClassNotFoundInJarException e) { return "not-found;not-found"; } @@ -50,8 +54,11 @@ class NonCommutativeConflictDetectionAlgorithm extends ConflictDetectionAlgorith private SootConfig buildSootConfig(String filePath, Scenario scenario) { SootConfig config = new SootConfig(filePath, scenario.getClassPath(), super.getMode()); + config.addOption("-entrypoints", scenario.getEntrypoints()); config.addOption("-depthLimit", this.getDepthLimit()); + config.addOption("-cg", getCallgraph()) + return config; } diff --git a/src/main/services/outputProcessors/soot/README.md b/src/main/services/outputProcessors/soot/README.md index ba656324b..599a5138d 100644 --- a/src/main/services/outputProcessors/soot/README.md +++ b/src/main/services/outputProcessors/soot/README.md @@ -60,6 +60,8 @@ Options: -t,--timeout Run -t time: time limit for each analysis (default: 240) -depthLimit Sets the depth limit on accessing methods when performing Overriding Assignment Interprocedural, Direct Flow Interprocedural and Confluence Interprocedural analyses. Default = 5 -printDepthSVFA Print depth in SVFA analysis + -cg,--callgraph Select call graph algorithm [CHA, RTA, VTA, SPARK] + -prt,--partial-results-on-timeout When a soot analysis times out, capture and record the partial results ``` For example: diff --git a/src/main/services/outputProcessors/soot/RunSootAnalysisOutputProcessor.groovy b/src/main/services/outputProcessors/soot/RunSootAnalysisOutputProcessor.groovy index 45b829968..2e8bdd481 100644 --- a/src/main/services/outputProcessors/soot/RunSootAnalysisOutputProcessor.groovy +++ b/src/main/services/outputProcessors/soot/RunSootAnalysisOutputProcessor.groovy @@ -66,6 +66,12 @@ class RunSootAnalysisOutputProcessor implements OutputProcessor { } } + void configurePartialResultsOnTimeout(boolean partialResultsOnTimeout) { + for (ConflictDetectionAlgorithm algorithm : detectionAlgorithms) { + algorithm.setPartialResultsOnTimeout(partialResultsOnTimeout); + } + } + void processOutput() { // check if file generated by FetchBuildsOutputProcessor exists println "Executing RunSootAnalysisOutputProcessor" diff --git a/src/main/services/outputProcessors/soot/arguments/ArgsParser.groovy b/src/main/services/outputProcessors/soot/arguments/ArgsParser.groovy index ea2d4fd57..ccbfdcfa8 100644 --- a/src/main/services/outputProcessors/soot/arguments/ArgsParser.groovy +++ b/src/main/services/outputProcessors/soot/arguments/ArgsParser.groovy @@ -37,6 +37,8 @@ class ArgsParser { this.cli.pd(longOpt: 'pessimistic-dataflow', "Run pessimistic-dataflow") this.cli.report(longOpt: 'report', "Run report results for experiment using -icf -ioa -idfp -pdg") this.cli.r(longOpt: 'reachability', "Run reachability") + this.cli.cg(longOpt: 'callgraph', args: 1, argName: 'algorithm', "Call graph algorithm [CHA, RTA, VTA, SPARK]") + this.cli.prt(longOpt: 'partial-results-on-timeout', "When a soot analysis times out, capture and record the partial results found up to that point instead of discarding them") } Arguments parse(args) { @@ -117,5 +119,15 @@ class ArgsParser { if (this.options.r) { args.setReachability(true) } + if (this.options.cg) { + String algorithm = options.cg?.toString()?.trim()?.toUpperCase() + if (!algorithm || !["CHA", "RTA", "VTA", "SPARK"].contains(algorithm)) { + throw new IllegalArgumentException("Invalid callgraph algorithm: ${options.cg}") + } + args.setCallgraph(algorithm) + } + if (this.options.prt) { + args.setPartialResultsOnTimeout(true) + } } } diff --git a/src/main/services/outputProcessors/soot/arguments/Arguments.groovy b/src/main/services/outputProcessors/soot/arguments/Arguments.groovy index b7b7be9e7..f25c9b2c8 100644 --- a/src/main/services/outputProcessors/soot/arguments/Arguments.groovy +++ b/src/main/services/outputProcessors/soot/arguments/Arguments.groovy @@ -23,6 +23,8 @@ class Arguments { private long timeout private long depthLimit private boolean printDepthSVFA + private String callgraph + private boolean partialResultsOnTimeout Arguments() { // set the default values for all parameters isHelp = false @@ -47,6 +49,8 @@ class Arguments { timeout = 240 printDepthSVFA = false depthLimit = 5 + callgraph = "SPARK" + partialResultsOnTimeout = false } boolean getOaIntraWithoutPA() { @@ -225,4 +229,19 @@ class Arguments { void setTimeout(long timeout) { this.timeout = timeout } + + String getCallgraph() { + return callgraph + } + void setCallgraph(String callgraph) { + this.callgraph = callgraph + } + + boolean isPartialResultsOnTimeout() { + return partialResultsOnTimeout + } + + void setPartialResultsOnTimeout(boolean partialResultsOnTimeout) { + this.partialResultsOnTimeout = partialResultsOnTimeout + } } \ No newline at end of file