figure a
figure b

1 Introduction

We present Kratos2, a tool for the verification of real-world imperative programs. Kratos2 is a complete rewrite and redesign of Kratos [17], improving and extending it in multiple directions. First, Kratos2 introduces a simple yet expressive intermediate language called K2, with a formally-specified semantics based on Satisfiability Modulo Theories (smt), which is parametric on the underlying smt theory. K2 is expressive enough to capture most of the features of real-world C programs, such as pointers, dynamic memory allocation, floating-point data types, and bit-precise semantics of bounded integers, which the old version of the tool could not handle (being limited to C programs without pointers and recursion, and in which C integers were interpreted as mathematical integers). Kratos2 comes with a separate C front-end c2Kratos that can translate C programs to K2. Second, Kratos2 includes a variety of state-of-the-art verification back-ends based on either symbolic model checking or symbolic execution with sat and smt solvers. Besides reachability properties, Kratos2 also supports various forms of liveness properties, which can be used to encode termination and more complex linear-time temporal properties. Third, Kratos2 implements an interactive interpreter, which can simulate K2 programs using non-deterministic inputs provided either by the user or by external oracles. Kratos2 also supports counterexample reconstruction, another feature not available in the original Kratos.

The new intermediate language K2 enables modular translation of C programs into various verification languages. Namely, Kratos2 can be used for translating C programs into nuXmv [14], vmt  [20], aiger  [9], Btor2  [31], Constrained Horn Clauses (chcs) [11], or Boogie [29] formats. Additionally, Kratos2 comes with a Python api for construction and manipulation of K2 programs, which the users can leverage to implement custom front-ends and generators of K2 programs and also additional translators from K2 to other formalisms.

Although Kratos2 has not been described in a publication until now, it has already been successfully used in several research and industrial projects. In particular, Kratos2 has been used as a back-end for the verification of automotive software in the context of the autosar platform [15, 16]; of C code automatically generated from aadl specifications by the taste development environment [12]; and for verification of C code for railway interlocking systems automatically generated from the specifications in a controlled natural language [1]. Kratos2 has also been used as a benchmark generator to produce symbolic transition systems from C programs [30].

The rest of the paper is structured as follows. The functionalities offered by Kratos2 from the user perspective are described in Sect. 2; Sect. 3 introduces K2, describing its syntax and formal semantics. The internal architecture of Kratos2, with details about its main components, is presented in Sect. 4; implementation notes and experimental evaluation on C programs from the annual software verification competition sv-comp are provided in Sect. 5. Finally, Sect. 6 concludes the paper and presents directions for future developments.

2 Functional View

In this section we provide a high-level overview of the functionalities available in Kratos2. More details will then be provided in the following sections.

An Intermediate Language for Imperative Programs. The core of Kratos2 is built around an idealized language for imperative programs called K2. Unlike common high-level real-world programming languages, K2 has a simple and clean semantics based on first-order logic modulo theories that is fully formally specified. The K2 language, similar in spirit to other intermediate verification languages proposed in the literature such as Boogie [29] or Why3 [26] (although less feature rich than the two), is at the same time simple enough to be easily manipulated and translated into formalisms used by sat-based and smt-based verification back-ends on one hand, and expressive enough to efficiently capture a significant subset of C on the other, as demonstrated also by our experimental results on standard sv-comp benchmarks (see Sect. 5).

Verification of Safety and Liveness with Multiple Back-Ends. Kratos2 implements multiple state-of-the-art verification algorithms based on sat and smt, supporting both bit-precise reasoning over machine integers and floating-point numbers as well as higher-level reasoning based on, e.g., mathematical integers, real numbers, and uninterpreted functions, depending on the combinations of theories used in the input K2 program under analysis. Moreover, Kratos2 supports not only the verification of safety properties (via a reduction to reachability of designated “error” program locations), but it also supports liveness properties such as proving that a specific program location is reached a finite number of times in all executions, or that it is always visited infinitely often in all infinite executions.

A Python api for Program Manipulations. Kratos2 provides a rich and flexible Python api for parsing, printing, and manipulating K2 programs and expressions, which can be used to implement converters from high-level languages to K2 or to directly generate K2 programs from user-specific applications.

A Customizable C Front-End. Kratos2 comes with a front-end for C programs which supports a wide range of customization options for controlling the translation from C to K2. These range from the choice of theories to use to encode C data types (e.g., bit-vectors or unbounded integers), to the use of customized program transformations or the injection of new built-in functions with special meaning (such as special assume, malloc, or memset built-ins). Thanks to its plug-in architecture, the front-end can be easily customized for domain-specific subsets of C, for example to implement special optimization passes that are safe only in the given context, or to automatically inject properties to the code based on specification files (as is, e.g., the case in sv-comp  [3]).

Encoding into Multiple Formalisms. Kratos2 can be used as an encoder or benchmark generator because it can translate imperative programs written in C or in K2 into other formalisms, including symbolic transition systems in nuXmv [14], vmt  [20], aiger  [9] or Btor2  [31] formats, Constrained Horn Clauses (chcs) [11], or other intermediate verification languages like Boogie [29].

Simulation and Symbolic Execution. Finally, Kratos2 can be used as an interpreter, allowing an (interactive) simulation of K2 programs and their symbolic execution, as an alternative to the verification back-ends based on model checking.

3 The K2 Language

In this section we introduce K2, the intermediate verification language used by Kratos2. We present its abstract syntax, formally define its semantics, and discuss its support for safety and liveness properties.

Fig. 1.
figure 1

Abstract syntax of K2 statements and expressions.

Fig. 2.
figure 2

Abstract syntax of K2 programs.

Abstract Syntax. We denote lists of elements with an overbar, i.e., \(\overline{\cdot }\). If \(\overline{a}\) is a list, \(|\overline{a}|\) is its length, and if i is a natural number, \(\overline{a}_i\) is the i-th element of \(\overline{a}\). If e is an element, \(\overline{a} \cdot e\) is the list obtained by appending e at the end of \(\overline{a}\).

Definition 1

(Variables and Functions). A variable is a symbol with an associated sort, as in the multi-sorted first-order logic. A function is a tuple \(\langle f,\overline{a},\overline{r},\overline{l},\overline{\sigma }\rangle \), where:

  • f, a symbol, is the name of the function;

  • \(\overline{a}\), a list of variables, are the formal parameters;

  • \(\overline{r}\), a list of variables, are the return variables;

  • \(\overline{l}\), a list of variables, are the local variables;

  • \(\overline{\sigma }\), a list of statements generated by the grammar of Fig. 1, are the body.

Given a list of variables \(\overline{v}\), we define \(\textsf{syms}(\overline{v}) \) as the corresponding set of symbols. Given a function \(\langle f, \overline{a}, \overline{r}, \overline{l}, \overline{\sigma }\rangle \), we denote with \(\textsf{syms}(f) \) the set \(\textsf{syms}(\overline{a}) \cup \textsf{syms}(\overline{r}) \cup \textsf{syms}(\overline{l}) \). We extend the definition to lists of statements \(\overline{\sigma }\) in the natural way. We now describe K2 programs, whose abstract syntax is shown in Fig. 2.

Definition 2

(Programs). A program P is a tuple \(\langle \overline{g},F,\iota ,e\rangle \), where:

  • \(\overline{g}\), a list of variables, are the global variables;

  • F is a partial mapping from symbols to functions;

  • \(\iota \), a formula, is the constraint on initial states;

  • e, a symbol in \(\textrm{dom}(F)\), is the entry point.

Semantics. We use the standard notions of theory, interpretation, model, and satisfaction from many-sorted first-order logic and smt  [2]. In the following, we assume that we have fixed a theory T with equality that contains at least the sort Bool. Given an interpretation \(\mu \) that is a model for T, we define the evaluation of an expression e (generated by the grammar of Fig. 1) under \(\mu \), denoted \(\mu [e]\), as \(\mu [e] = \mu (v)\) for \(e = {\textbf {var~}} v\) and \(\mu [e] = \mu (o)(\mu [\overline{p}_1], \ldots , \mu [\overline{p}_n])\) for \(e = {\textbf {op~}} o~\overline{p}\) and \(n = |\overline{p}|\).

We denote with \(\mu [v \mapsto e]\) the interpretation that maps v to e, and that agrees with \(\mu \) everywhere else, and with \(\mu [\setminus v]\) any interpretation that agrees with \(\mu \) on all the symbols except v. Finally, if e is of sort Bool, we write \(\mu \models e\) to denote that e evaluates to true under \(\mu \).

Definition 3

(Program states). Pairs \(\langle f, i\rangle \) where f is a function name and i is a natural number are called program locations. A state of a program P is a pair \(s = \langle G, \overline{C}\rangle \) where:

  • G is an interpretation for the global variables of P;

  • \(\overline{C}\) is the current call stack, a list of triples \(\langle f,i,L\rangle \), where \(\langle f, i\rangle \) is a program location and L is an interpretation of \(\textsf{syms}(f) \), i.e., of parameters, return variables, and local variables of F(f).

A state s is initial if and only if \(G \models \iota \), \(|\overline{C}| = 1\) and \(\overline{C}_1 = \langle e,1,L\rangle \) for some L. Given a state s with \(\overline{C}_{|\overline{C}|} = \langle f,i,L\rangle \), we define the current interpretation \(\mu \) for s as \(\mu (v) = G(v)\) for \(v \in \textsf{syms}(\overline{g}) \) and as \(\mu (v) = L(v)\) otherwise.

Fig. 3.
figure 3

Transition rules. In all the rules, \(\mu \) denotes the current interpretation for the left-hand state of the rule.

We define the semantics for programs as a set of transition rules of the form \(s \xrightarrow {\sigma } s'\), where \(s, s'\) are states and \(\sigma \) is a statement. We then call a path of a program P any sequence of transitions (possibly infinite) \(s_0 \xrightarrow {\sigma _0} \ldots \xrightarrow {\sigma _i} s_{i+1} \ldots \) that complies with the transition rules and where \(s_0\) is an initial state.

The rules are shown in Fig. 3. In the definitions, we fix a program \(P=\langle \overline{g}, F, \iota , e\rangle \) and use the following convenience functions, where f is a function name and i a natural number: \(\textsf{arg}(f,i) \) returns the variable \(\overline{a}_i\) of the function F(f); \(\textsf{ret}(f,i) \) returns the variable \(\overline{r}_i\) of the function F(f); \(\textsf{stmt}(f,i) \) returns the statement \(\overline{\sigma }_i\) of F(f); \(\textsf{stmts}(f) \) returns the list of statements \(\overline{\sigma }\) of F(f).

Reachability and Liveness. We then say that a state s is reachable in P iff there exists a finite path \(s_0 \xrightarrow {\sigma _0} \ldots \xrightarrow {\sigma _n} s\) that ends in s. Similarly, a program location \(\langle f,i\rangle \) is reachable iff there exists a path as above in which \(\sigma _n = \textsf{stmt}(f,i) \)Footnote 1. Conversely, if no such path exists, then \(\langle f,i\rangle \) is unreachable. The location \(\langle f,i\rangle \) is infinitely-often reachable iff there exists an infinite path \(s_0 \xrightarrow {\sigma _0} \ldots \xrightarrow {\sigma _i} s_{i+1} \ldots \) in which for all indices j there exists an index \(k > j\) such that \(\sigma _k = \textsf{stmt}(f,i) \). If no such path exists, then \(\langle f,i\rangle \) is eventually unreachable. Finally, we say that \(\langle f,i\rangle \) is live iff it is infinitely-often reachable in all infinite paths of P.

In K2, queries about reachability or liveness of program locations are expressed via annotations of label statements. Annotations are metadata that are attached to statements, in the form of key-value pairs, which do not affect the semantics of the program, but are meant to provide additional information that can be used by tools that manipulate the K2 program. Specifically, Kratos2 uses the following annotations to define properties:

error <id>::

holds iff all labels annotated with the same <id> are unreachable;

notlive <id>::

holds iff all labels annotated with the same <id> are eventually unreachable;

live <id>::

holds iff all labels annotated with the same <id> are live.

These basic properties can be easily used to represent more common higher-level properties of programs, such as assertions and termination. For example, assertions can be reduced to reachability with a combination of assume and jump statements, whereas termination can be checked by adding a final self loop over a label with an attached live annotation. Finally, eventual unreachability can be used to encode arbitrary ltl properties using the standard automata-theoretic approach combined with a symbolic encoding of the accepting automaton such as [22].Footnote 2

3.1 Example

We conclude this section with a simple example of a C program and its equivalent formulation in K2. Both versions are shown in Fig. 4. Most of the code is translated in a fairly direct way (with conditional statements and structured loops translated into nondeterministic jumps constrained by assumptions). However, since in K2, unlike in C, global variables are uninitialized by default, the K2 program contains an additional setup function (called init_and_main in the example) that sets glbl to zero before calling the original main. Another point to highlight is the use of the :error annotation (highlighted in bold) to model the C assertion.

Fig. 4.
figure 4

Example C program and its K2 translation.

4 Architectural View

This section describes the main components of Kratos2 and the flow of information among them. From the high-level point of view, Kratos2 is composed of the front-end c2Kratos, which converts the input C program to the K2 language, and of the core Kratos2, which is responsible for parsing, simplifications, transformations, and verification of K2 code. This separation helps to keep the core Kratos2 simple, as it does not have to handle the complex semantic nuances of C. Moreover, it makes it easy to add front-ends for new languages by writing a separate translator from the language in question to K2.

The front-end c2Kratos reads the input C file, builds its abstract syntax tree (ast) and then builds the corresponding K2 code in two passes. In the first pass, it converts the ast to an extended K2. Compared to the standard K2, the extended K2 also has primitives for pointers, records, complex loops, and compound instructions. These are removed in the second pass, by converting pointers to operations over maps, records to multiple variables, complex loops to sequences of assignments, jump instructions, and assumptions, and compound instructions to sequences of basic assignments to auxiliary variables.

The core Kratos2 consists of several components, whose relationships are visualized in Fig. 5:

cfg builder and simplifier reads the input K2 file and builds the corresponding interprocedural control flow graph (cfg). It then performs several simplifications of the cfg, such as constant propagation and lightweight slicing. The result can be used either by the interpreter, symbolic executor, or one of the encoders. The simplified cfg can also be converted back into a K2 representation.

Interpreter interprets the cfg using the externally provided inputs to guide the execution. The inputs contain new values for all havoc commands and also destination labels for all nondeterministic jump commands. The inputs can be provided by the user, a random generator, or by one of the verification engines. The last option is used for counterexample reconstruction and validation.

Transition system encoder encodes the cfg to a symbolic transition system over a suitable theory. The encoder first inlines all function calls in the program. It then encodes the resulting inlined program using large block encoding [4], which allows encoding larger acyclic subgraphs of the cfg by a single transition formula. The resulting transition system can be verified by one of the available verification back-ends, or converted to a textual representation in one of the available output formats (vmt  [20], nuXmv [14], Btor2  [31], or aiger  [9]).Footnote 3

Fig. 5.
figure 5

Architecture of Kratos2.

chc encoder converts the cfg to a set of Constrained Horn Clauses [11]. In contrast to the transition system encoder, the chc encoder supports interprocedural analysis and recursive functions, encoded as a set of non-linear chc s as described, e.g., in [28].

Symbolic executor implements a classical symbolic execution algorithm with iterative deepening to avoid getting stuck in long uninteresting branches. It supports (possibly recursive) K2 programs over arbitrary combinations of integers, reals, bit-vectors, floats, and arrays.

smt-based engines encompass several smt-based verification algorithms of symbolic transition systems. For reachability properties, Kratos2 implements standard bounded model checking (bmc) [7], k-induction [32], and IC3 with implicit predicate abstraction [18]. For liveness properties, we use a procedure combining liveness-to-safety reduction with ranking functions synthesis [23].

sat-based engines encompass several verification algorithms of finite-state symbolic transition systems. Namely, for transition systems over the theory of bit-vectors and floats, Kratos2 offers bmc, k-induction, and different variants of IC3 [13], working over the bit-blasted Boolean transition system, for both reachability and liveness properties. Additionally, Kratos2 implements a dedicated engine for reachability properties in transition systems over the theory of bit-vectors, floats, and arrays similar to [10, 30].

5 Implementation and Experimental Evaluation

Implementation. Core Kratos2 is implemented in C++ on top of the MathSAT5 [19] smt solver and the nuXmv [14] symbolic model checker. The sat-based verification engine additionally makes use of the MiniSat [25] and CaDiCaL [8] sat solvers. The front-end c2Kratos is implemented in Python and relies on pycparser for parsing of the input C program. Kratos2 is freely available for non-commercial purposes from https://kratos.fbk.eu.

Experimental Setup. We performed an experimental evaluation to answer two research questions: Is the K2 language expressive enough to efficiently represent realistic C programs? Do the engines implemented in Kratos2 offer reasonable performance on realistic verification tasks? To this end, we considered all the C programs from the ReachSafety category of the 2022 edition of the annual software verification competition sv-comp  [3].The category consists of 5400 C programs divided into 12 benchmark families. We compared Kratos2 with VeriAbs  1.4.2 [24] and CPAchecker  2.2 [5], respectively the winner and runner-up of the ReachSafety category of sv-comp 2022. Similarly to the approach used by CPAchecker, we executed Kratos2 in sequential portfolio mode, which successively runs symbolic execution, smt-based IC3, sat-based IC3, and smt-based bmc with predetermined time-outs for each of the engines.

The experiments were performed on several identical pcs equipped with Intel Core i7-8700 cpu @ 3.20 GHz and 32 GiB of ram. Each execution was limited to use a single cpu core, 15 min of cpu time, and 8 GiB of ram. For reliable benchmarking, all experiments were executed using BenchExec [6]. A replication package describing the details of the setup is available at https://doi.org/10.5281/zenodo.7890411.

Table 1. Solved benchmarks by the three compared tools. Column U shows the number of solved unsafe benchmarks, S of safe benchmarks, and W of wrong results.

Results. To answer the first research question, we observe that from the total 5400 benchmarks, only 56 were not converted to K2 by c2Kratos due to unsupported floating point built-ins or features such as variable length arrays.

Fig. 6.
figure 6

Quantile plots of solved benchmarks for all three compared tools in individual benchmark families. The plot shows the number of benchmarks (y-axis) that were solved within the given number of seconds (x-axis).

To answer the second research question, Table 1 shows the numbers of solved benchmarks by the individual tools and quantile plots in Fig. 6 show their running times. The results show that Kratos2 is competitive with CPAchecker on all benchmark families except for eca. It is also competitive with VeriAbs on most benchmark families. There are 23 benchmarks uniquely solved by Kratos2, 48 by CPAchecker, and 1039 by VeriAbs. Moreover, both Kratos2 and VeriAbs produced no wrong results, unlike most other participants of sv-comp.

We remark that CPAchecker is an established and optimized software verifier that regularly scores high in software verification competitions, and that VeriAbs implements algorithm selection heuristics, using both its own custom engines and external state-of-the-art verifiers. As such, it is not surprising that it performs much better than Kratos2 and CPAchecker on some of the families.

We conclude that the K2 language is expressive enough to efficiently capture a significant subset of C used in realistic programs. Furthermore, the verification engines implemented in Kratos2 mostly offer a performance comparable with state-of-the-art software verifiers.

6 Conclusions and Future Work

We have described Kratos2, a mature software verifier for imperative programs written in K2, a new intermediate verification language with a formal semantics based on smt. Kratos2 is a complete rewrite of the original Kratos tool, offering significant extensions in functionalities and performance. The tool has already been successfully applied in various contexts, both industrial and academic.

As future work, we will consolidate the (currently alpha-quality) implementation of the esst algorithm of the original Kratos [21] to handle multithreaded programs with cooperative scheduling. We will also investigate a tighter integration with chc solvers to better handle recursive programs, as well as improved techniques to handle arrays and pointers such as [27, 33]. On the language side, we plan to add support for contracts and pre-/post-conditions via annotations.