---
title: 'Symbolic Sandbox: A Runtime for Binary Symbolic Execution'
url: https://www.emergentmind.com/topics/symbolic-sandbox
type: topic
---

# Symbolic Sandbox: A Runtime for Binary Symbolic Execution

A symbolic sandbox is an accurate and extensible runtime framework for symbolic execution of binary code, constructed upon formal input–output semantics of a target instruction set architecture (ISA). The approach pioneered in BinSym avoids the pitfalls of traditional IR-based symbolic execution by leveraging machine-readable, mechanized ISA specifications to provide exact semantics for each instruction. This enables direct, path-sensitive symbolic execution at the binary level, without the semantic ambiguities or maintenance overheads associated with manual instruction lifters or English-language ISA descriptions. BinSym achieves its extensibility and correctness by parameterizing all instruction semantics over a small set of stateful primitives, defined as operation constructors in a free monad, and by instantiating symbolic values as paired concrete words and SMT solver ASTs [2404.04132].

## 1. Formal Representation of ISA Semantics

BinSym builds upon the LibRISCV Haskell library, which comprehensively encodes the RV32I core and M/A/C extensions as algebraic data types and generalized algebraic data types (GADTs) describing both bit-vector expressions and primitive stateful effects. Each RISC-V instruction acquires a direct mechanized definition, e.g.,

```
instrSemantics BEQ = do
  rs1 ← decodeRS1 >>= readRegister
  rs2 ← decodeRS2 >>= readRegister
  imm ← decodeImmB
  runIfTrue (rs1 `Eq` rs2) $ writePC (pc `Add` imm)
```

LibRISCV’s expressions constitute a freely-generated algebra $E$, with constructors such as $\mathsf{FromInt}$, $\mathsf{ZExt}$, $\mathsf{Add}$, $\mathsf{Eq}$, etc. All stateful ISA effects (register/memory access, control flow) are represented as a GADT $\mathsf{Operations}\;v\;r$, with typical constructors:

- $\mathsf{ReadRegister} :: v \to \mathsf{Operations}\;v\;v$
- $\mathsf{WriteRegister} :: v \to v \to \mathsf{Operations}\;v\;()$
- $\mathsf{LoadMem}$, $\mathsf{StoreMem}$, $\mathsf{RunIf}$, $\mathsf{WritePC}$

A generic interpreter for $\mathsf{Operations}$, using standard free-monad techniques, yields a RISC-V emulator. BinSym reuses this machinery, instantiating $v$ as a *concolic* value (concrete bit and symbolic Z3 AST) and hooking $E$ to the Z3 backend instead of native unboxed integer code.

#### Key Definitions

- Let $\sigma \in \Sigma$ (RISC-V machine state): $\sigma = (\mathsf{PC} : \mathsf{Expr},\;\mathsf{R} : \mathsf{RegName} \to \mathsf{Expr},\; \mathsf{M} : \mathsf{Addr} \to \mathsf{Expr})$
- Symbolic value $v = (c : \mathbb{N},\, s \in \mathsf{AST})$
- Path condition $\pi \in \mathcal{P}$: finite set of Z3 Boolean ASTs

## 2. Symbolic States and Expressions

The symbolic sandbox maintains a symbolic state $S = (\sigma, \pi)$, where:
- $\sigma$: the complete RISC-V machine state, with all registers, memory, and program counter as pairs of concrete values and SMT ASTs.
- $\pi$: the path condition, a finite conjunction of Boolean ASTs (each corresponding to a control-flow decision).

The core expression type $E.\mathsf{Expr}\;v$ is interpreted by:
```
evalE :: E.Expr (Concolic v) → Z3.MonadZ3 AST
```
Each constructor (e.g., $\mathsf{Add},\,\mathsf{Eq}$) is dispatched to the corresponding Z3 bit-vector operator:
- $\mathsf{evalE(Add\;e_1\;e_2)} = \mathsf{mkBvadd(evalE\;e_1,\,evalE\;e_2)}$
- $\mathsf{evalE(Eq\;e_1\;e_2)}  = \mathsf{mkEq(evalE\;e_1,\,evalE\;e_2)}$

Distinctively, each concolic value pairs a concrete word for decoding and an optional AST for symbolic path reasoning, eliminating the overhead of symbolic opcode resolution and ensuring precise branching semantics.

## 3. Inference Rules for Core Instructions

Instruction semantics are extractible into standardized inference rules, mapping symbolic state transitions. Each semantic primitive is mapped directly to a monad action, avoiding IR translation:

- **BEQ (Conditional Branch):**
  
  Let $e_1 = \sigma(rs1)$, $e_2 = \sigma(rs2)$, $c = (e_1 = e_2)$, and $pc_0 = \sigma(\mathsf{PC})$.

  **Branch taken:**
  $$
    \langle \text{BEQ } rs1,rs2,imm \rangle, (\sigma,\pi)
    \implies (\sigma', \pi')\quad
    \text{where}\,\sigma' = \sigma[\mathsf{PC} \mapsto pc_0+imm], \;\pi' = \pi \cup \{c\}
  $$

  **Not taken:**
  $$
    \langle \text{BEQ } rs1,rs2,imm \rangle, (\sigma,\pi)
    \implies (\sigma'', \pi'')\quad
    \text{where}\,\sigma'' = \sigma[\mathsf{PC} \mapsto pc_0+4], \;\pi'' = \pi \cup \{\neg c\}
  $$

- **ADD:**
  $$
    \langle \text{ADD } rd,rs1,rs2 \rangle, (\sigma,\pi)
    \implies (\sigma', \pi)\quad
    \text{where}\,\sigma' = \sigma[\mathsf{R}(rd) \mapsto \sigma(rs1)+\sigma(rs2),\, \mathsf{PC} \mapsto \sigma(\mathsf{PC})+4]
  $$

- **LW (Load Word):**
  Let $a = \sigma(rs) + \mathsf{imm}$, $v = \mathsf{LoadMem32}(a)$,
  $$
    \langle \text{LW } rd,\mathsf{imm}(rs) \rangle,(\sigma,\pi)
    \implies (\sigma',\pi)\quad
    \sigma' = \sigma[\mathsf{R}(rd)\mapsto v,\,\mathsf{PC}\mapsto \sigma(\mathsf{PC})+4]
  $$

All state transitions are mediated directly by the monadic $\mathsf{Operations}$, ensuring the sandbox runs strictly according to the mechanized ISA semantics.

## 4. Architecture and Execution Framework

The sandbox relies upon the following layered runtime:

1. **ISA parser:** Direct import of LibRISCV (or similarly formalized ISA), exposing functions:
   ```
   instrSemantics :: Instr → Free (Operations v) ()
   ```
   This provides canonical instruction semantics as specified in the source ISA, with no hand-written lifter.

2. **Semantics generator:** A generic free-monad interpreter parameterized over $v$ (concrete or concolic) and supporting a mapping from $E.\mathsf{Expr}$ to Z3 ASTs.

3. **Symbolic executor:** Maintains a worklist of symbolic states $S_i = (\sigma_i, \pi_i)$. Each step proceeds as follows:
   - Decode $\mathsf{PC}$ concretely, yielding a 32-bit opcode.
   - Lookup $\mathsf{instrSemantics}$ for the decoded instruction.
   - Run the concolic interpreter to compute successor states.
   - When encountering $\mathsf{RunIf}$, spawn two branches extending $\pi$ with either guard.
   - Query Z3 with each resulting $\pi_i \wedge \neg(\text{taken branch})$ to generate distinguishing inputs.

The pipeline proceeds as:

```
[Elf/RV32 binary]
    ↓
[σ₀ concrete start state]
    ↓
pickState(S₀)
    ↓
decode@Concrete(PC₀) ⇒ rawBits → decodeLibrary → instrSemantics DSL
    ↓
interpret symMonad → { (σ₁,π₁), (σ₂,π₂) }
    ↓
for each new leaf, πᵢ is sent to Z3 as “πᵢ ∧ ¬(discarded branch)” to extract fresh input for the next path.
```

## 5. Extensibility and Multi-ISA Support

Extending the symbolic sandbox to new ISAs requires only providing a module (e.g., `MyFancyISA.hs`) that defines:

1. $\mathsf{E.Expr}$ constructors for any new ops.
2. An $\mathsf{Operations'}$ GADT listing requisite stateful actions.
3. Monad definitions of instruction semantics, e.g.,
   ```
   instrSemantics MyOP1 = do
     decode operands ...
     emit RunIf / LoadMem' / WriteReg ...
   ```

The symbolic interpreter is generic over any $\mathsf{Operations}'$ instance. At startup, multiple ISA specs may be loaded and merged:

```
let spec1 = loadLibRISCV
let spec2 = loadMyFancyISA
symbolicspec = mergeSpecs [spec1, spec2]
runSymbolic symbolicspec initialState
```

This design ensures that no solver-specific or execution-engine code requires modification to support additional ISAs.

## 6. Case Study: Evaluation and Bug Discovery

A comparative evaluation was performed on BinSym versus three open-source RISC-V symbolic engines:
- SymEx-VP (direct RV32 exec in SystemC)
- BinSec (DBA-lifted static exec)
- angr (VEX-lifted dynamic exec in Python)

Benchmark tasks included base64-encode, bubble-sort, is-prime, insertion-sort, and uri-parser, each given a fixed array of symbolic bytes. Each tool was run five times per benchmark using Docker+Z3, exploring all feasible paths (100%).

Table: Benchmark Runtimes (seconds)

| Tool      | base64 | sort-bubble | is-prime | insertion | uri-parser |
|-----------|--------|-------------|----------|-----------|------------|
| BinSym    | 169    | 52          | 98       | 67        | 54         |
| SymEx-VP  | 217    | 44          | 129      | 122       | 67         |
| BinSec    | 229    | 82          | 136      | 128       | 95         |
| angr      | 32*    | 256         | 207      | 393       | 322        |

*angr on base64 explored only 125 of 6250 feasible paths; all others covered 100%.

Key findings:
- BinSym solved the suite ~30% faster than SymEx-VP, ~40% faster than BinSec, and ~5× faster than angr.
- angr failed to discover 6125/6250 feasible base64 paths due to a branch encoding error.

Five previously undocumented bugs were identified in angr’s RISC-V front-end:
1. BEQ immediate miscalculation (offset error)
2. I-type immediate SEXT-ZEXT confusion (incorrect sign)
3. Omitted decoding of certain CSR `funct3` values
4. Absent load-word alignment check disabled unaligned branches
5. SRA implemented as logical shift

Each was detected as a failing symbolic path, producing minimal counterexamples.

## 7. Performance, Trade-offs, and Limitations

BinSym’s symbolic sandbox demonstrates competitive performance with minimal decoding overhead due to direct Z3 AST manipulation and elimination of IR lifters. However:
- Using boxed bit-vectors may be suboptimal compared to potential direct-bitblast backends.
- Concolic values circumvent the opcode decoding issue but complicate verification for fully symbolic address ranges; future work is anticipated on lazy arrays or advanced heap models.
- The free-monad interpreter introduces minor per-operation overhead, but most instructions require only a direct $\mathsf{evalE}$ call and single register update.

Proposed extensions include Coq or HOL4 proofs of $instrSemantics$ correctness (relative to golden models such as Sail), hardware-peripheral modelling for firmware support, and ISA augmentation (e.g., RV64, CHERI, ARMv8) by plugging additional GADTs into the same interpreter.

For experimental reproducibility, code and benchmarks are released at https://github.com/agra-uni-bremen/binsym; all requirements are distributed for re-execution under Guix [2404.04132].

Source: https://www.emergentmind.com/topics/symbolic-sandbox