Skip to content
Development Update: Mid-September 2026

Development Update: Mid-September 2026

September 20, 2026

Following the early-September update, 231 commits landed on master between 4 and 20 September. Here are the changes a user will notice.

The C++ standard-library headers ESBMC could not include. Because ESBMC compiles C++ with -nostdinc++, a header with no operational model is a PARSING ERROR on the #include alone, whether or not the program uses anything from it. Twenty headers closed that way this fortnight: <numbers> (#7638), <concepts> (#7639), <version> (#7647), <charconv> (#7649), <cfenv>, <cinttypes>, <cwctype> and six more C wrappers (#7799), <ratio> (#7804), <execution>, <codecvt> and <scoped_allocator> (#7909), <latch>, <semaphore> and <stop_token> (#7912), <flat_map>, <flat_set> and <syncstream> (#7914), and <regex> and <mdspan> (#7915). <ranges>, <format>, <print>, <coroutine>, <generator>, <barrier> and <stdfloat> are what remain.

Two of these are worth reading the small print on, because being includable is not the same as being modelled. <regex> models no matching at all: a regular expression engine unrolled into SSA is not something a solver can carry, and a partial engine would give wrong verdicts on the patterns it mishandles, so every function that reports a match answers nondeterministically and a property that depends on a particular pattern matching is reported FAILED rather than proved. <syncstream> likewise transfers no characters, and <charconv> omits the floating-point overloads for the same reason — an approximate shortest-round-trip formatter returns wrong digits silently, where an absent overload stays the compile error it already was. The C++ Support page now says which is which per header.

Invariant synthesis for affine loops. k-induction cannot prove a property needing a relation between a loop counter and an accumulator: the interval domain is non-relational, so at the loop head it knows the counter’s range and nothing tying the accumulator to it. --synthesise-loop-invariants recognises affine counter/accumulator loops and emits the closed form as a LOOP_INVARIANT (#7479), which the existing havoc schema asserts before it assumes — so a wrong candidate fails a claim rather than producing an unsound proof. It trades bug-finding for proving: over regression/{esbmc,esbmc-unix,k-induction,loop-invariants} it fires on 87 of 2716 files, of which 9 lose a bug that bounded BMC finds, and no file gains a false proof or a false alarm. Opt-in, off by default, and documented under Loop Invariants.

--loop-invariant-check can refute again. The UNKNOWN downgrade added in #7491 was decided by a claim’s position — everything downstream of the havoc — which is right while the abstraction admits the claim holding and wrong when the invariant pins the counterexample outright, the case the mode exists to serve. ESBMC now asks the solver whether the claim can hold at all on a feasible abstract path before downgrading; UNSAT means no abstract state satisfies it, and since the concrete states are a subset, the violation is real (#7626). Two further fixes in the same area: a do-while head is the first instruction of the body rather than a guard, so the combined pass was copying an empty body and discharging the inductive step against no iteration at all (#7497); and storage written through a dereference has no symbol to havoc, so the pointee is now havocked through the pointer and resolved against symex’s own value set (#7518).

A verdict that said UNKNOWN after finding a bug. Under --multi-property — explicit, or implied by --parallel-solving or --all-witnesses--k-induction and --incremental-bmc keep going past a violation, but the exhausted-k exit ignored what they had recorded: a program whose loop cannot be unwound printed its counterexamples and then ended VERIFICATION UNKNOWN with exit status 0. That exit now reports the recorded violation (#7913). Relatedly, the C library idiom (void)((c) || (assert(0), 0)) was folded into ASSERT c by matching any two-statement branch and erasing the second statement unchecked; under --multi-property, which keeps checking past a failed assertion, that lost real code (#7917).

An undefined conversion that verified clean. C11 6.3.1.4p1 makes a floating-point to integer conversion undefined when the integral part is not representable in the destination type. ESBMC’s existing cast check was gated on --int-encoding, so the default bitvector mode had none and reported VERIFICATION SUCCESSFUL on (long long)1e300. --overflow-check now covers it (#7622); --no-fp-conversion-check turns just that check off, as SV-COMP needs, whose no-overflow property is about signed-integer arithmetic only.

Concurrency: two schedules that were never explored. Partial-order reduction keyed only static and heap-typed objects, so a mutex or datum in main’s frame handed to a worker through a pointer had no key in either thread: MPOR pruned the only schedule reaching the bug, and a write to such a local was not even a context-switch point, so --no-por missed it too. Every address-taken local is now keyed as a global is (#7826). Separately, race instrumentation put a call inside the atomic block guarding its own accesses whenever the result was stored in shared memory or the call went through a global function pointer, so races inside the callee were never reported and racy programs verified SUCCESSFUL (#7768).

Memory model. A write or free through a pointer the value set cannot resolve exhaustively now fails when the pointer is none of the recorded targets — freed globals, stack objects and heap interiors were previously missed (#7773). The alignment check reads the object’s base alignment rather than assuming the base carries the access width, so a pointer laundered out of a packed struct no longer reads as aligned (#7721). A flexible array member has size zero as C17 6.7.2.1p18 requires, rather than the storage of a one-element array (#7774). And ::operator new(n) with a non-constant n allocates n bytes instead of one, which had made every in-bounds access through the returned pointer an out-of-bounds report (#7651).

GNU vector extensions. Four defects, each of which aborted or silently dropped a cast: a vector comparison was typed bool, so v4i m = a == b; assigned a one-bit value to a 128-bit vector and the solver aborted on the width mismatch (#7904); dereferencing a pointer to a vector aborted in the pointer analysis, where the read and write sides disagreed about what index2t admits (#7920); the last statement of a GNU statement expression was decayed to &x[0] although C never decays a vector (#7921); and a cast between vector types was dropped entirely, so (v4u)c verified as c (#7922).

C front end. __builtin_{add,sub,mul}_overflow and the carry builtins arrive unlowered, unlike the typed family clang expands itself, and with no body returned nondeterministic results behind a warning (#7586). __atomic_test_and_set and __atomic_clear were the only two GCC atomic builtins the front end did not name (#7658). __bf16 and __mfp8 are modelled, so #include <immintrin.h> no longer fails under clang 22 (#7894); _Complex int z = {1, 2} converts (#7801); and an integer sentinel pointer — 0xffffffffffffffffUL into a void *, as preprocessed kernel and CIL sources write it — is accepted outside --sv-comp as well, since GCC accepts it and refusing was a false rejection of input a mainstream toolchain compiles (#7741).

Diagnostics instead of aborts. An uncaught exception is reported at the raise it came from, one property per raise site, rather than at line 0 under global once a second disagreeing site cleared the anchor (#7770). An initializer list the front end cannot model reports its type, arity and location instead of tripping a bare assert (#7702). A target clang cannot map — --ppc-macos — is reported as PARSING ERROR rather than a SIGSEGV (#7759). A zero-width dereference fails loudly in release builds instead of reading bytes[-1] inside ESBMC (#7737). And ESBMC warns when an option value names another option, which had silently made --show-loops a witness filename (#7538).

Python runs faster. The operational models are precompiled to a GOTO binary at build time, the way c2goto builds the C library, instead of being re-converted from AST JSON and re-lowered on every run (#7747, #7778): a trivial run drops from 3.26 s to 1.80 s and the Python regression suite from 2952 s to 1932 s. A module’s AST is parsed on first lookup rather than at startup (#7777), model functions the program cannot reach are dropped before GOTO conversion (#7780), and the advisory mypy check nobody asked for is now opt-in behind --python-typecheck.

Python correctness. A union mixing a scalar with a container was narrowed to its leftmost member, which folded a comparison against the returned list to false and its negation to true — a proof of a false property (#7877, #7880). A module CPython imports but ESBMC has no AST for — sys, or a stdlib file the resolver filters out — no longer aborts the run, and sys has a data-only model (#7677); a module that fails to compile or whose body raises is reported for what it is rather than escaping as a CPython traceback with no verdict (#7681). An inherited @staticmethod called through an instance binds its arguments to the right slots (#7781). random.choice and random.sample dispatch on the sequence type instead of running the int-list model over a str or a tuple and reporting a dereference failure (#7676). And a tagged (dynamically-typed) scalar can now be passed as a function argument, stored in a list, and compound-assigned or negated (#7573, #7708, #7834).

NumPy. A 2-D array parameter keeps its full shape through the C-ABI row-pointer decay, so .shape, .ndim, .size and numpy.transpose / .T / .transpose() read it rather than the decayed 1-D type — this was the one NumPy shape that produced a silently wrong array value rather than an explicit rejection. Sorting and searching gained row and column views and 2-D arrays with an axis argument (#7722).

CUDA. The model’s device list was never populated, so every cudaSetDevice failed, no current device was tracked, and a kernel launched on one GPU with memory from another verified. The model now keeps a per-host-thread current device, tags each cudaMalloc with its device, and asserts at every launch that each pointer argument is host memory, the current device’s, or an enabled peer’s (#7772).

Solver. The Bitwuzla backend moved to Bitwuzla’s C++ API, where Term and Sort release themselves, after a first pass fixed the leaks that needed no API change (#7507, #7508). --overflow-check on (__int128)a * (__int128)b for two long longs generated a VCC whose encoding doubles the destination width to 256 bits; a same-signedness widened multiply cannot overflow when the destination is at least w1 + w2 bits, so the check folds without reaching the solver, where it had OOM’d under Bitwuzla and timed out under Z3 and Boolector (#7843). A function’s address can no longer coincide with SIG_DFL, SIG_ERR or SIG_IGN, which C11 7.14p3 requires (#7800).

Two SV-COMP fixes. Flattening a pointer into an untyped byte object and reading it back went through the integer-to-pointer path, which rebuilds a pointer from an address alone — and an address does not identify one, so a key an aws-c-common harness stored came back as a different pointer and ESBMC reported a false alarm on a task whose expected verdict is true (#7895). And a violation witness named every local but not the value that made the property fail when that value was read through a pointer into an object the program never wrote, so a validator had nothing to replay — which turns a correct false into no points (#7893).

The website documentation has been updated to match. As always, the full list is in the commit history — and if you hit a bug or a missing feature, we would love an issue report.