T3-5 · Symbolic Execution

Symbolic Execution with Assertion Annotations

QCP allows users to add assertion annotations between program statements to help complete verification. Loop invariants are one important kind of such annotation. When the QCP symbolic executor encounters an assertion annotation, it checks whether the strongest postcondition obtained so far implies the assertion manually provided by the user.

Symbolic Execution with an Added Assertion in swap

Consider the swap function again.

void swap(int * px, int * py)
/*@ With x y
    Require store_int(px, x) * store_int(py, y)
    Ensure store_int(px, y) * store_int(py, x) */
{
  int t;
  t = * px;
  * px = * py;
  * py = t;
}

After the first assignment, the program state satisfies the following assertion, which is also the strongest postcondition obtained by QCP:

store_int(&t, x) *
store_ptr(&px, px_37) *
store_ptr(&py, py_40) *
store_int(px_37, x) *
store_int(py_40, y)

Here px_37 and py_40 represent the initial values px@pre and py@pre. Looking at the rest of the program, we know that the current value stored at *px is no longer needed. At this program point we can assert a weaker property:

void swap(int * px, int * py)
/*@ With x y
    Require store_int(px, x) * store_int(py, y)
    Ensure store_int(px, y) * store_int(py, x) */
{
  int t;
  t = * px;
  /*@ Assert
        store_int(&t, x) *
        store_ptr(&px, px@pre) *
        store_ptr(&py, py@pre) *
        has_int_permission(px@pre) *
        store(py@pre, y) */
  * px = * py;
  * py = t;
}

Adding this assertion means that whenever execution reaches this point, the program state must satisfy the stated property. Subsequent verification then relies only on this property. Even if the stronger original property is true, later verification does not need to use it. QCP therefore generates the following VC and continues symbolic execution from the user-provided assertion.

store_int(&t, x) *
store_ptr(&px, px_37) *
store_ptr(&py, py_40) *
store_int(px_37, x) *
store_int(py_40, y)
|-- store_int(&t, x) *
    store_ptr(&px, px_37) *
    store_ptr(&py, py_40) *
    has_int_permission(px_37) *
    store_int(py_40, y)

The following two screenshots compare the symbolic execution state before and after executing this assertion. After the assertion is processed, the Normal assertion list contains the has_permission predicate introduced by the annotation.

image-3-5-1
image-3-5-1
image-3-5-2
image-3-5-2

Assertion Annotations After Symbolically Executing an if Statement

The following max3 function computes the maximum of three integers.

int max3(int x, int y, int z)
/*@ Require emp
    Ensure __return >= x &&
           __return >= y &&
           __return >= z */
{
  int t;
  if (x < y) {
    t = y;
  }
  else {
    t = x;
  }
  /*@ Assert t >= x@pre && t >= y@pre &&
             x == x@pre && y == y@pre && z == z@pre */
  if (t < z) {
    return z;
  }
  else {
    return t;
  }
}

This function contains two if statements. As before, after symbolically executing the first if, QCP obtains two Normal assertions.

image-3-5-3
image-3-5-3

If QCP then symbolically executes the assertion annotation after this if, it generates two VCs: each of the two Normal assertions must imply the manually inserted assertion. Written in basic separation-logic notation, the two VCs are:

x_54 < y_51 &&
store_int(&t, y_51) *
store_int(&x, x_54) *
store_int(&y, y_51) *
store_int(&z, z_48)
|--
exists t_64,
t_64 >= x_54 &&
t_64 >= y_51 &&
store_int(&t, t64) *
store_int(&x, x_54) *
store_int(&y, y_51) *
store_int(&z, z_48)
x_54 >= y_51 &&
store_int(&t, y_51) *
store_int(&x, x_54) *
store_int(&y, y_51) *
store_int(&z, z_48)
|--
exists t_64,
t_64 >= x_54 &&
t_64 >= y_51 &&
store_int(&t, t64) *
store_int(&x, x_54) *
store_int(&y, y_51) *
store_int(&z, z_48)

In the IDE, after the assertion is processed, the two branch assertions are merged into the user-provided assertion, and symbolic execution proceeds from there.

image-3-5-4
image-3-5-4