Skip to content

Commit 29cde18

Browse files
Concolic driver (#85)
* connect concolic evaluator to z3 * not all constrains have a solution * guarantee termination && symbolic should consume symstack * i32.or is bitwise or in wasm * exploration tree * some fixes * simplify * when true branch, condition should not be zero * mark endpoint as Finished * refactor * print exploration tree * complete interpreter * fix: put retCont to trail * try bug finding * see if ci works * default is actually good enough * bring back the original test case * ubuntu-24.04 is removed on 2025-04-15 actions/runner-images#11101 * Fix ci (#86) * install clang-11 * try ubuntu-22 * do not generate node in exploration tree if symbolic condition is concrete * still explore all paths in assert_false * we could endwith a fail node
1 parent 70b33e9 commit 29cde18

File tree

10 files changed

+462
-117
lines changed

10 files changed

+462
-117
lines changed

.github/workflows/scala.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
jobs:
1010
build:
1111

12-
runs-on: ubuntu-20.04
12+
runs-on: ubuntu-22.04
1313
defaults:
1414
run:
1515
working-directory: ./
@@ -27,7 +27,7 @@ jobs:
2727
- name: Set up dependencies
2828
run: |
2929
sudo apt-get update
30-
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y git g++ cmake bison flex libboost-all-dev python
30+
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y git g++ cmake bison flex libboost-all-dev 2to3 python-is-python3
3131
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y perl minisat curl gnupg2 locales clang-11 wget
3232
- name: Generate test files (LLVM IR)
3333
run: |
@@ -77,3 +77,4 @@ jobs:
7777
sbt 'testOnly gensym.wasm.TestEval'
7878
sbt 'testOnly gensym.wasm.TestScriptRun'
7979
sbt 'testOnly gensym.wasm.TestConcolic'
80+
sbt 'testOnly gensym.wasm.TestDriver'
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
(module
2+
(type (;0;) (func (param i32 i32) (result i32)))
3+
(type (;1;) (func (param i32)))
4+
(func (;0;) (type 0) (param i32 i32) (result i32)
5+
local.get 0
6+
i32.const 0
7+
i32.le_s
8+
if (result i32) ;; label = @1
9+
i32.const 1
10+
else
11+
local.get 1
12+
i32.const 0
13+
i32.le_s
14+
end
15+
if (result i32) ;; label = @1
16+
i32.const -1
17+
else
18+
local.get 0
19+
local.get 0
20+
i32.mul
21+
local.get 1
22+
local.get 1
23+
i32.mul
24+
i32.add
25+
i32.const 25
26+
i32.eq
27+
if (result i32) ;; label = @2
28+
i32.const 1
29+
else
30+
i32.const 0
31+
call 2
32+
end
33+
end
34+
)
35+
(export "f" (func 0))
36+
(func $real_main
37+
;; TODO: is there a better way to put symbolic values on the stack?
38+
i32.const 0
39+
i32.symbolic
40+
i32.const 1
41+
i32.symbolic
42+
call 0
43+
)
44+
(import "console" "assert" (func (type 1)))
45+
(export "real_main" (func 1))
46+
)

benchmarks/wasm/branch-strip.wat

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
local.get 0
55
i32.const 0
66
i32.le_s
7-
local.get 1
8-
i32.const 0
9-
i32.le_s
10-
i32.or
11-
if (result i32) ;; label = @1
7+
if (result i32) ;; label = @1
8+
i32.const 1
9+
else
10+
local.get 1
11+
i32.const 0
12+
i32.le_s
13+
end
14+
if (result i32) ;; label = @1
1215
i32.const -1
1316
else
1417
local.get 0
@@ -20,7 +23,7 @@
2023
i32.add
2124
i32.const 25
2225
i32.eq
23-
if (result i32) ;; label = @2
26+
if (result i32) ;; label = @2
2427
i32.const 1
2528
else
2629
i32.const 0

benchmarks/wasm/branch-strip1.wat

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
(module
2+
(type (;0;) (func (param i32 i32) (result i32)))
3+
(func (;0;) (type 0) (param i32 i32) (result i32)
4+
local.get 0
5+
i32.const 0
6+
i32.le_s
7+
local.get 1
8+
i32.const 0
9+
i32.le_s
10+
i32.or
11+
if (result i32) ;; label = @1
12+
i32.const -1
13+
else
14+
local.get 0
15+
local.get 0
16+
i32.mul
17+
local.get 1
18+
local.get 1
19+
i32.mul
20+
i32.add
21+
i32.const 25
22+
i32.eq
23+
if (result i32) ;; label = @2
24+
i32.const 1
25+
else
26+
i32.const 0
27+
end
28+
end
29+
)
30+
(export "f" (func 0))
31+
(func $real_main
32+
;; TODO: is there a better way to put symbolic values on the stack?
33+
i32.const 0
34+
i32.symbolic
35+
i32.const 1
36+
i32.symbolic
37+
call 0
38+
)
39+
40+
(export "real_main" (func 1))
41+
)

benchmarks/wasm/branch.wat

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
(func $f (param $x i32) (param $y i32) (result i32)
33
;; if (x <= 0 || y <= 0)
44
(if (result i32)
5-
(i32.or
5+
(if (result i32)
66
(i32.le_s (local.get $x) (i32.const 0))
7-
(i32.le_s (local.get $y) (i32.const 0))
7+
(then (i32.const 1))
8+
(else (i32.le_s (local.get $y) (i32.const 0)))
89
)
910
(then (i32.const -1)) ;; return -1
1011
(else

benchmarks/wasm/branch1.wat

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
(module
2+
(func $f (param $x i32) (param $y i32) (result i32)
3+
;; if (x <= 0 || y <= 0)
4+
(if (result i32)
5+
(i32.or
6+
(i32.le_s (local.get $x) (i32.const 0))
7+
(i32.le_s (local.get $y) (i32.const 0))
8+
)
9+
(then (i32.const -1)) ;; return -1
10+
(else
11+
;; if (x * x + y * y == 25)
12+
(if (result i32)
13+
(i32.eq
14+
(i32.add
15+
(i32.mul (local.get $x) (local.get $x))
16+
(i32.mul (local.get $y) (local.get $y))
17+
)
18+
(i32.const 25)
19+
)
20+
(then (i32.const 1)) ;; return 1
21+
(else (i32.const 0)) ;; return 0
22+
)
23+
)
24+
)
25+
)
26+
27+
;; Optionally export the function
28+
(export "f" (func $f))
29+
)

src/main/scala/wasm/ConcolicDriver.scala

Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import scala.collection.immutable.Queue
99
import scala.collection.mutable.{HashMap, HashSet}
1010

1111
import z3.scala._
12+
import scala.tools.nsc.doc.model.Val
1213

1314
object ConcolicDriver {
14-
def condsToEnv(conds: List[Cond])(implicit z3Ctx: Z3Context): HashMap[Int, Value] = {
15+
def condsToEnv(conds: List[Cond])(implicit z3Ctx: Z3Context): Option[HashMap[Int, Value]] = {
1516
val intSort = z3Ctx.mkIntSort()
1617
val boolSort = z3Ctx.mkBoolSort()
1718

@@ -24,16 +25,57 @@ object ConcolicDriver {
2425
case Add(_) => z3Ctx.mkAdd(symVToZ3(lhs), symVToZ3(rhs)) // does numtype matter?
2526
case Sub(_) => z3Ctx.mkSub(symVToZ3(lhs), symVToZ3(rhs))
2627
case Mul(_) => z3Ctx.mkMul(symVToZ3(lhs), symVToZ3(rhs))
27-
case _ => ???
28+
case Or(_) =>
29+
var result = z3Ctx.mkBVOr(
30+
z3Ctx.mkInt2BV(32, symVToZ3(lhs)),
31+
z3Ctx.mkInt2BV(32, symVToZ3(rhs))
32+
)
33+
z3Ctx.mkBV2Int(result, false)
34+
case _ => throw new NotImplementedError(s"Unsupported binary operation: $op")
2835
}
2936
case SymUnary(op, v) =>
3037
op match {
3138
case _ => ???
3239
}
3340
case SymIte(cond, thenV, elseV) => z3Ctx.mkITE(condToZ3(cond), symVToZ3(thenV), symVToZ3(elseV))
34-
case Concrete(v) => ???
35-
case _ => ???
41+
case Concrete(v) =>
42+
v match {
43+
// todo: replace with bitvector
44+
case I32V(i) => z3Ctx.mkInt(i, intSort)
45+
case I64V(i) => z3Ctx.mkNumeral(i.toString(), intSort)
46+
// TODO: Float
47+
case _ => ???
48+
}
49+
case RelCond(op, lhs, rhs) =>
50+
val res = op match {
51+
case GeS(_) => z3Ctx.mkGE(symVToZ3(lhs), symVToZ3(rhs))
52+
case GtS(_) => z3Ctx.mkGT(symVToZ3(lhs), symVToZ3(rhs))
53+
case LtS(_) => z3Ctx.mkLT(symVToZ3(lhs), symVToZ3(rhs))
54+
case LeS(_) => z3Ctx.mkLE(symVToZ3(lhs), symVToZ3(rhs))
55+
case GtU(_) => z3Ctx.mkGT(symVToZ3(lhs), symVToZ3(rhs))
56+
case GeU(_) => z3Ctx.mkGE(symVToZ3(lhs), symVToZ3(rhs))
57+
case LtU(_) => z3Ctx.mkLT(symVToZ3(lhs), symVToZ3(rhs))
58+
case LeU(_) => z3Ctx.mkLE(symVToZ3(lhs), symVToZ3(rhs))
59+
case Eq(_) => z3Ctx.mkEq(symVToZ3(lhs), symVToZ3(rhs))
60+
case Ne(_) => z3Ctx.mkNot(z3Ctx.mkEq(symVToZ3(lhs), symVToZ3(rhs)))
61+
case Ge(_) => z3Ctx.mkGE(symVToZ3(lhs), symVToZ3(rhs))
62+
case Gt(_) => z3Ctx.mkGT(symVToZ3(lhs), symVToZ3(rhs))
63+
case Le(_) => z3Ctx.mkLE(symVToZ3(lhs), symVToZ3(rhs))
64+
case Lt(_) => z3Ctx.mkLT(symVToZ3(lhs), symVToZ3(rhs))
65+
}
66+
// convert resutl to int
67+
z3Ctx.mkITE(res, z3Ctx.mkInt(1, intSort), z3Ctx.mkInt(0, intSort))
68+
case _ => throw new NotImplementedError(s"Unsupported SymVal: $symV")
69+
}
70+
71+
def getIndexOfSym(sym: String): Int = {
72+
val pattern = ".*_(\\d+)$".r
73+
sym match {
74+
case pattern(index) => index.toInt
75+
case _ => throw new IllegalArgumentException(s"Invalid symbol format: $sym")
76+
}
3677
}
78+
3779
def condToZ3(cond: Cond): Z3AST = cond match {
3880
case CondEqz(v) => z3Ctx.mkEq(symVToZ3(v), z3Ctx.mkInt(0, intSort))
3981
case Not(cond) => z3Ctx.mkNot(condToZ3(cond))
@@ -49,74 +91,93 @@ object ConcolicDriver {
4991
}
5092

5193
// solve for all vars
94+
println(s"solving constraints: ${solver.toString()}")
5295
solver.check() match {
5396
case Some(true) => {
5497
val model = solver.getModel()
5598
val vars = model.getConsts
56-
val env = HashMap()
99+
val env = HashMap[Int, Value]()
57100
for (v <- vars) {
58101
val name = v.getName
59102
val ast = z3Ctx.mkConst(name, intSort)
60103
val value = model.eval(ast)
104+
println(s"name: $name")
105+
println(s"value: $value")
106+
// TODO: support other types of symbolic values(currently only i32)
61107
val intValue = if (value.isDefined && value.get.getSort.isIntSort) {
62-
I32V(value.toString.toInt)
108+
val negPattern = """\(\-\s*(\d+)\)""".r
109+
val plainPattern = """(-?\d+)""".r
110+
val num = value.get.toString match {
111+
case negPattern(digits) => -digits.toInt
112+
case plainPattern(number) => number.toInt
113+
case _ => throw new IllegalArgumentException("Invalid format")
114+
}
115+
I32V(num)
63116
} else {
64117
???
65118
}
66-
// env += (name.toString -> intValue)
67-
???
119+
env += (getIndexOfSym(name.toString) -> intValue)
68120
}
69-
???
70-
// env
121+
println(s"solved env: $env")
122+
Some(env)
71123
}
72-
case _ => ???
124+
case _ => None
73125
}
74126
}
75127

76128
def negateCond(conds: List[Cond], i: Int): List[Cond] = {
77-
???
129+
conds(i).negated :: conds.drop(i + 1)
78130
}
79131

80132
def checkPCToFile(pc: List[Cond]): Unit = {
133+
// TODO: what this function for?
81134
???
82135
}
83136

84137
def exec(module: Module, mainFun: String, startEnv: HashMap[Int, Value])(implicit z3Ctx: Z3Context) = {
85138
val worklist = Queue(startEnv)
86-
// val visited = ??? // how to avoid re-execution
87-
139+
val unreachables = HashSet[ExploreTree]()
140+
val visited = HashSet[ExploreTree]()
141+
// the root node of exploration tree
142+
val root = new ExploreTree()
88143
def loop(worklist: Queue[HashMap[Int, Value]]): Unit = worklist match {
89144
case Queue() => ()
90145
case env +: rest => {
91146
val moduleInst = ModuleInstance(module)
92147
Evaluator(moduleInst).execWholeProgram(
93148
Some(mainFun),
94149
env,
95-
(_endStack, _endSymStack, pathConds) => {
96-
println(s"env: $env")
97-
val newEnv = condsToEnv(pathConds)
98-
val newWork = for (i <- 0 until pathConds.length) yield {
99-
val newConds = negateCond(pathConds, i)
100-
checkPCToFile(newConds)
101-
condsToEnv(newConds)
150+
root,
151+
(_endStack, _endSymStack, tree) => {
152+
tree.fillWithFinished()
153+
val unexploredTrees = root.unexploredTrees()
154+
// if a node is already visited or marked as unreachable, don't try to explore it
155+
val addedNewWork = unexploredTrees.filterNot(unreachables.contains)
156+
.filterNot(visited.contains)
157+
.flatMap { tree =>
158+
val conds = tree.collectConds()
159+
val newEnv = condsToEnv(conds)
160+
// if the path conditions to reach this node are unsatisfiable, mark it as unreachable.
161+
if (newEnv.isEmpty) unreachables.add(tree)
162+
newEnv
163+
}
164+
for (tree <- unexploredTrees) {
165+
visited.add(tree)
102166
}
103-
loop(rest ++ newWork)
167+
loop(rest ++ addedNewWork)
104168
}
105169
)
106170
}
107171
}
108172

109173
loop(worklist)
174+
println(s"unreachable trees number: ${unreachables.size}")
175+
println(s"number of normal explored paths: ${root.finishedTrees().size}")
176+
val failedTrees = root.failedTrees()
177+
println(s"number of failed explored paths: ${failedTrees.size}")
178+
for (tree <- failedTrees) {
179+
println(s"find a failed endpoint: ${tree}")
180+
}
181+
println(s"exploration tree: ${root.toString}")
110182
}
111183
}
112-
113-
object DriverSimpleTest {
114-
def fileTestDriver(file: String, mainFun: String, startEnv: HashMap[Int, Value]) = {
115-
import gensym.wasm.concolicminiwasm._
116-
import collection.mutable.ArrayBuffer
117-
val module = Parser.parseFile(file)
118-
ConcolicDriver.exec(module, mainFun, startEnv)(new Z3Context())
119-
}
120-
121-
def main(args: Array[String]) = {}
122-
}

0 commit comments

Comments
 (0)