|
ESBMC
Efficient SMT-based Context-Bounded Model Checker
|
Files | |
| andersen.cpp | |
| andersen.h | |
| Andersen-style (inclusion-based) points-to analysis. | |
| dereference.cpp | |
| dereference.h | |
| The dereferencing code's purpose is to take a symbol with pointer type that is being dereferenced, and translate it to an expression representing the values it read or wrote to/from. | |
| goto_program_dereference.cpp | |
| goto_program_dereference.h | |
| show_value_sets.cpp | |
| show_value_sets.h | |
| value_set.cpp | |
| value_set.h | |
| value_set_analysis.cpp | |
| value_set_analysis.h | |
| value_set_domain.cpp | |
| value_set_domain.h | |
| value_sets.h | |
Developer notes on ESBMC's memory model and the code under src/pointer-analysis/. The directory provides two related services:
The same value_sett data structure is reused by goto-symex (live points-to tracking during symbolic execution) and by value_set_analysist (a flow- and context-insensitive fixpoint computed before symex via the static_analysist<value_set_domaint> framework).
The C standard requires every data object to have a byte representation: arbitrary pointer arithmetic on a char * is well-defined, and memcpy works by copying bytes. A direct encoding of this in SMT — a single byte array covering all storage — is sound but tends to hurt solver performance.
ESBMC instead stores each l1 data object as a single SMT primitive of the object's natural type (bitvector, float, bool, array, structure, or union). The cost of this choice is that the dereferencing code must translate every read or write through a pointer into an access on the primitive backing the pointed-to object — including the cases where the access type, the pointer type, and the primitive's type all disagree.
For example:
A store through baz must be lowered into an access into either foo or bar's SMT primitive, at a non-constant byte offset, writing a char into an int. Three types are in play:
value_setst is the indirection that lets dereferencet consume points-to information without caring whether it came from the static analyser or from the live value_sett carried by a goto-symex state.
These hold throughout the pipeline; the rest of the document assumes them.
The two services above have a compact formal reading. This section gives (a) the grammar of the points-to facts the analysis manipulates, and (b) the transfer functions that map program expressions and assignments onto those facts. Both are exactly what value_set.cpp computes; the function or branch that realises each rule is named alongside it.
A value-set state ρ (the value_sett::values map) is a set of entries, each binding a pointer location to the set of objects it may point at. In EBNF (| alternation, { } zero-or-more, [ ] optional):
This is the syntax value_sett::output prints under --show-value-sets (value_set.cpp:22-108): every object is a <referent, offset, align, type> tuple, except unknown/invalid referents, which print bare. Internally each tuple is an objectt (offset, offset_is_set, offset_alignment) keyed, in the entry's object_mapt, by an index into object_numbering that names the referent. align is objectt::offset_alignment in bytes and is meaningful precisely when offset is symbolic.
When a value set is exported to a client (get_value_set → valuest, value_set.cpp:182-194) each tuple becomes an object_descriptor2t(referent, offset, align) expression, while unknown/invalid stay as unknown2t / invalid2t. That valuest is exactly what dereferencet consumes.
Write ρ ⊢ e ⇓ M for "in state `ρ`, expression `e` evaluates to the object-map `M`" — the relation computed by value_sett::get_value_set_rec. Each rule is tagged with the branch that implements it:
Two properties are worth stressing, both visible in the rules:
An assignment rebinds one location to the value set of its right-hand side (value_sett::assign, value_set.cpp:1074):
The join ⊔ (make_union / insert, value_set.cpp:168, value_set.h:350) is set union on referents; when the same referent carries two offsets it widens conservatively:
So two definite-but-different offsets for one object collapse to a symbolic offset with the coarser alignment — the lattice climbs from "exact offset" to "aligned-but-unknown offset" and finally (via unknown) to "points anywhere."
Each line shows the resulting entry in --show-value-sets notation (<referent, offset, align, type>; * = symbolic offset). The align column is written abstractly as a (a byte alignment lower bound): its concrete value follows the offset_alignment rules and the 8-byte scalar default in get_natural_alignment, and is best read off --show-value-sets for a given program. Only the referent set and the offset are pinned down here:
The longer *baz = 1 walkthrough later in this document (Example walkthrough) shows [IF], [PTR+i], and the join interacting within a single statement.
value_sett maps (l1 identifier, suffix) → entryt. An entryt holds an object_mapt (an unordered_map<unsigned, objectt>) whose keys are indices into the static value_sett::object_numbering and whose values are objectt records carrying:
The suffix uniquely identifies a sub-pointer within an l1 variable (e.g. .field_name for a struct member, [] for an array of pointers — array elements are not tracked individually).
object_numbering and obj_numbering_refset are thread_local so parallel symex threads (e.g. --k-induction-parallel) don't race on the shared numbering.
Key methods:
Entries with identifier prefix value_set::dynamic_object represent heap-allocated storage created during symbolic execution; the symbolic heap-tracking logic ("black magic" in earlier comments) lives in do_free and the dynamic-object handling inside assign_rec.
value_set_analysist (a static_analysist<value_set_domaint>) computes a fixed-point of the points-to map across the goto program. value_set_domaint owns a value_sett * and forwards merge and transform to it. The value_setst abstract base in value_sets.h is the lookup interface used by clients that don't care whether the analysis is the static one or the symbolic-execution one:
where valuest is std::list<expr2tc> containing object_descriptor2t/unknown2t/invalid2t expressions.
dereferencet is stateless w.r.t. the program — it holds only a namespace, a context (for minting new symbols), command-line options, and a back-pointer to a dereference_callbackt. The callback is the interface to the environment hosting the dereference:
goto_program_dereferencet (in goto_program_dereference.{h,cpp}) is the implementation used in front-of-symex passes such as pointer_checks. The symex side has its own dereference_callbackt implementation; both share the same dereferencet code.
dereferencet::modet is a small bitfield:
Predicates (is_read, is_write, is_free, is_internal) compare on the op field only — there is intentionally no operator==.
The four phases listed in dereference.h:
Recurses over the expression looking for dereferences. Special cases:
dereference_expr_nonscalar walks the member/index/typecast/ constant_union chain rooted at base, accumulates the bit-offset via compute_pointer_offset_bits(base, &ns), and on reaching the dereference2t calls dereference(...) with that bit-offset. If the expression accessed traverses a packed struct member without alignment, the mode's unaligned flag is set for the remainder of the walk (unless --no-align-check is set).
Steps:
Failed symbols are fresh free variables named symex::invalid_object<N> (invalid_counter is thread_local to keep names unique under parallel symex). They serve as well-typed placeholder rvalues so the formula remains valid when the access is guaranteed to fail.
build_reference_to is the per-candidate entry point. It:
Provenance: when the dereferenced pointer's type has carry_provenance (CHERI / CHERI-C), bounds_check is given the pointer expression so it can emit provenance-aware bounds.
build_reference_rec is the dispatching switch. It encodes the cross-product of (source kind, destination kind, offset kind) into a single integer key from target_flags:
Each bit is distinct (no flag is zero) so that case labels never collide when OR'd. Source category is read from value->type; destination from the requested type; offset kind from whether offset is a constant_int2t.
Code-typed values and code-typed destinations are returned untouched. A zero-bit destination type returns a gen_zero(type) (or a failed symbol for a memberless union) — this is the fix for issue #723; without it, the recursive paths reach the scalar base case and fail with a spurious width mismatch.
The full dispatch table is the comment block immediately above build_reference_rec in dereference.cpp. In summary:
| src \ dst | scalar (const off) | scalar (dyn off) | struct (const off) | struct (dyn off) | union (const off) | union (dyn off) |
|---|---|---|---|---|---|---|
| scalar | construct_from_const_offset | construct_from_dyn_offset | bitcast if off=0 & widths match | error | bitcast if off=0 & widths match | error |
| struct | construct_from_const_struct_offset | construct_from_dyn_struct_offset | construct_struct_ref_from_const_offset | construct_struct_ref_from_dyn_offset | construct_struct_ref_from_const_offset | construct_struct_ref_from_dyn_offset |
| array | construct_from_array | construct_from_array | construct_struct_ref_from_const_offset_array | construct_struct_ref_from_dyn_offset | construct_struct_ref_from_const_offset_array | construct_struct_ref_from_dyn_offset |
| union | ad-hoc: pick the widest member, recurse | ad-hoc: pick the widest member, recurse | ad-hoc: pick the widest member, recurse | ad-hoc: pick the widest member, recurse | recurse with widest member | recurse with widest member |
construct_struct_ref_* returning a struct only happens when unavoidable (e.g. structure assignment through a char *-backed allocation), and is guarded by aggressive assertions — the preferred path is to dereference directly into the requested scalar field via the lexical chain.
When the access cannot be expressed as a clean projection of the underlying primitive — e.g. a misaligned read, or reading an int from an array of short — the code falls back to byte stitching: extract_bytes produces one expression per byte of the source primitive, stitch_together_from_byte_array concatenates them in target endianness (is_big_endian, derived from config.ansi_c.endianess), and extract_bits_from_byte_array projects the bit-slice asked for. The two overloads of stitch_together_from_byte_array cover, respectively, the fixed-byte-count case and the array-source-with-(possibly-dynamic)-bit-offset case.
Recorded via the callback as the dereferencing code proceeds:
All of these route through dereference_failure(...) → dereference_callback.dereference_failure(...). --no-pointer-check and --no-align-check suppress whole assertion classes (queried inline in dereference.cpp; goto_program_dereferencet::dereference_failure also gates on no-simplify). Setting block_assertions on dereferencet silently drops all dereference-failure assertions and is used for the INTERNAL mode and similar query paths.
The dereferencing code assumes that type_byte_size (in util/) word-aligns all struct fields and inserts trailing padding so that, in well-formed accesses, the requested data never straddles a field boundary in the underlying primitive. Violations of that property surface as alignment failures rather than silently corrupt encodings. The complementary offset_alignment tracked in value_sett::objectt ensures that even non-deterministic offsets carry an alignment lower bound, which the reference builders use to prune cases (especially for array element accesses).
When the dereferencing code cannot produce a sensible reference — unknown points-to, bad base type, etc. — it falls back to a fresh free symbol of the required type, declared in the context passed to dereferencet's constructor. Names are symex::invalid_object<N>, where N is drawn from a thread_local counter. The pre-existing failed_symbol machinery on pointer types (queried via dereference_callbackt::has_failed_symbol) is flagged as legacy in dereference.h and will be removed.
Take the snippet from the intro:
The discussion stops at the expr2tc tree handed to the SMT backend — the actual SMT lowering depends on the chosen solver theory (bitvector vs. floatbv, concat/extract vs. select/store) and is best inspected with esbmc --show-vcc against the program in question.
value_sett::assign(baz, rhs) walks the conditional and unions both arms. The resulting entry for baz (suffix empty, since baz itself is the pointer) has two object map keys, one for foo and one for bar. Both arms carry a constant zero offset:
<obj, offset, alignment, type> is the format value_sett::output prints under --show-value-sets (see value_set.cpp:60-88). offset is * when symbolic; alignment is the byte alignment lower bound from objectt::offset_alignment.
baz += idx is interpreted by value_sett::get_value_set_rec for the right-hand side; the add over a pointer with a non-constant operand collapses both entries' constant offsets into symbolic ones, with offset_alignment recomputed via offset2align. Since the base objects are int (4 bytes natural alignment) but the arithmetic is on a char *, the alignment lower bound drops to 1:
*baz reaches dereference_expr with expr_id == dereference_id, which recurses into the operand under READ mode and then calls dereference(baz, char, guard, WRITE, expr2tc()). There is no lexical member/index chain, so extra_offset is nil.
dereference queries the callback for the points-to set and iterates over the two candidates. For each, build_reference_to:
In build_reference_rec, value->type is int (scalar), type is char (scalar), and the offset is not a constant_int2t, so the flags resolve to flag_src_scalar | flag_dst_scalar | flag_is_dyn_offs. That case dispatches to construct_from_dyn_offset, which goes through stitch_together_from_byte_array: the int primitive is decomposed into four byte expressions via extract_bytes, and the chosen byte is selected by the symbolic offset modulo the width. The resulting expr2tc is the projection of one byte out of foo.
The same construction runs for bar. dereference chains the two results:
For a write, the symex layer turns this rvalue selection into a with-style update on each candidate primitive, guarded by the same same_object2tc predicates plus the surrounding control-flow guard.
The __ESBMC_assume(idx < sizeof(int)) keeps pointer_offset(baz) within range, so the bounds_check calls inside build_reference_to discharge trivially under the solver. Without the assumption, the same code path still runs but the bounds assertion becomes reachable and reportable.
| File | Role |
|---|---|
| value_set.{h,cpp} | Core value-set data structure, assign/get_value_set/make_union. |
| value_set_domain.{h,cpp} | Abstract domain wrapping value_sett for the static analyser. |
| value_set_analysis.{h,cpp} | Whole-program fixpoint over value_set_domaint. |
| value_sets.h | Abstract value_setst interface used by dereferencet clients. |
| dereference.{h,cpp} | dereferencet, dereference_callbackt, mode enum, reference builders. |
| goto_program_dereference.{h,cpp} | goto_program_dereferencet: pre-symex pass that lowers dereferences in a goto program. |
| show_value_sets.{h,cpp} | --show-value-sets pretty-printer for debugging. |