Abstract 1 Introduction 2 Virtual Recency Abstraction for Relational Analyses 3 Termination of the Virtual Recency Abstraction 4 Soundness of the Virtual Recency Abstraction 5 Modular Reuse of Existing Analysis Components 6 Evaluation 7 Related Work 8 Conclusion References

The Virtual Recency Abstraction

Sven Keidel ORCID Fraunhofer SIT | ATHENE, Darmstadt, Germany    Raphaël Monat ORCID Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRIStAL, F-59000 Lille, France    Sebastian Erdweg ORCID KIT, Karlsruhe, Germany
Abstract

Abstract interpreters enable sound static analysis, but are hard to develop. In recent years, researchers have proposed a component-based approach to developing abstract interpreters, where different parts of the abstract domain (e.g., numeric, call frame, heap) are handled by isolated components. This works well as long as components do not share or expose their internal state: Any state update that is locally sound is also globally sound. However, some abstract domains require shared state, most prominently relational abstract domains, which use symbolic expressions such as 2x+5 to represent abstract values. As the relational component performs a strong update of x, the abstract value 2x+5 can change non-monotonically, which breaks soundness. We propose a novel solution to this problem: A virtual recency abstractions that decouples the relational component, supports strong updates, and allows recursion. We prove that the virtual recency abstraction restores soundness: Any shared state wrapped in our virtual recency abstraction may be locally updated non-monotonically, while global soundness persists. We applied our approach to develop the first relational WebAssembly analysis, reusing many components from an existing inter-procedural abstract interpreter. Furthermore, we evaluate the recall, precision, and scalibility of this analysis to demonstrate the practicality of the virtual recency abstraction.

Keywords and phrases:
Relational Numerical Analysis, Recency Abstraction
Copyright and License:
[Uncaptioned image] © Sven Keidel, Raphaël Monat, and Sebastian Erdweg; licensed under Creative Commons License CC-BY 4.0
2012 ACM Subject Classification:
Software and its engineering Automated static analysis
Supplementary Material:
Software  (Artifact): https://doi.org/10.5281/zenodo.19704087
Supplementary Material:
Software  (ECOOP 2026 Artifact Evaluation approved artifact): https://doi.org/10.4230/DARTS.12.1.3
Editors:
Robbert Krebbers and Alexandra Silva

1 Introduction

Static analyzers are automated tools that extract information from computer programs without running them. They are used in compilers, integrated development environments, security scanners, bug trackers, and software verification. However, developing sound and precise static analyses for modern programming languages is a delicate balancing act: The analysis needs to faithfully encode the complexity of the language semantics and employ complex abstract domains.

One approach to reduce the development complexity of static analyses is to modularize their implementation, such that independent analysis components capture different aspects of the language semantics and the abstract domain. Such component-based design not only reduces the analysis’ code complexity, it also enables reuse of language-independent components [18, 32, 33], allows swapping out components for more precise or higher performance components [29, 32, 27], and allows proving components sound independently from each other [35]. For example, a modular WebAssembly analysis includes analysis components for the byte-addressable memory, global variables, function tables, exceptions, the operand stack, and call frame of local variables [8]. And a Java call-graph analyses may be implemented modularly with analysis components for reflection, threads, and serialization [28], which are notoriously difficult to analyze [36, 1, 48].

Developing static analyses from modular analysis components works well when components do not share state. Specifically, when the concretization of a decomposed abstract domain is compositional γ(A1××An)=γ1(A1)××γn(An), then each analysis component is responsible for its own soundness only. This is the case in all prior work on analysis components. However, we found that some analysis components A0 require shared state S, such that γ(A0(S)×A1××An)=γ0(A0(S))×γ1(A1,S)××γn(An,S). This means, we need to co-develop A0 with all other analysis components, which breaks modularity.

Consider relational abstractions [15, 40], which infer relationships between program variables to improve precision. For example, the relationship 0𝚒𝚗𝚍𝚎𝚡<𝚊𝚛𝚛𝚊𝚢.𝚕𝚎𝚗𝚐𝚝𝚑 proves safe accesses to 𝚊𝚛𝚛𝚊𝚢 via 𝚒𝚗𝚍𝚎𝚡. Relational abstractions require shared state for the analysis component that manages local variables, such as the call frame. This is because relational analyses use symbolic expressions to represent abstract values, so that variable symbols have to be resolved by other analysis components, too. For example, consider a relational analysis for WebAssembly (Wasm) using separate components for the operand stack and the local variables:

Statement Analysis 1 (Unsound) Analysis 2 (Imprecise) Analysis 3 (Non-Modular)
Stack Local Vars. Stack Local Vars. Stack Local Vars.
;; 𝚢[1,5] [] 𝚢[1,5] [] 𝚢[1,5] [] 𝚢[1,5]
1: (local.set $x $y) [] 𝚡=𝚢 [] 𝚡=𝚢 [] 𝚡=𝚢
2: (local.get $x) [𝚡] 𝚡=𝚢 [𝚡] 𝚡=𝚢 [𝚡] 𝚡=𝚢
3: (local.set $x (i32.add $y $y)) [𝚡] 𝚡=2𝚢 [𝚡] 𝚢𝚡2𝚢 [𝚡1] 𝚡=2𝚢,𝚡1=y
4: (local.get $x) [𝚡,𝚡] 𝚡=𝚢 [𝚡,𝚡] 𝚡=𝚢 [𝚡,𝚡1] 𝚡=𝚢
5: (i32.eq) [𝗍𝗋𝗎𝖾] [] [𝖿𝖺𝗅𝗌𝖾]

The analyzed program corresponds to x := y; x == {x := y + y; x} in an imperative language. The key point is that one operand of the comparison == is looked up before the second assignment happens, whereas the second operand is looked up after the second assignment. Hence, for x0, the comparison must yield 𝖿𝖺𝗅𝗌𝖾. Let’s see how a component-based relational analysis can handle this program.

We consider three alternative analysis designs. Analysis 1 pushes the symbolic expression 𝚡 on the stack to represent the abstract value corresponding to variable $x (Line 2). When re-assigning 𝚡 (Line 3), the component responsible for local variables changes its internal state using a strong update to obtain 𝚡=2𝚢. However, the component responsible for the stack was relying on the previous state for the locals. Indeed, Analysis 1 subsequently infers that the comparison must yield 𝗍𝗋𝗎𝖾, which is unsound. The analysis could have prevented this unsoundness if it had avoided the strong update of 𝚡. Analysis 2 shows this behavior: It performs a weak update and assigns 𝚡 to y2y. This yields a sound result, but is unnecessarily imprecise. To retain precision, the analysis must perform a strong update, but it may not break the stack component. To this end, Analysis 3 introduces a fresh variable 𝚡1 to remember the old binding of 𝚡 when doing the strong update. But this only helps when the analysis also updates the internal state of the other components. In Analysis 3, when the component for local variables handles an assignment, it also must adapt the internal state of the stack component. This is sound and precise, but not modular and requires the two components to be co-developed and co-maintained.

None of the three analyses was able to deliver sound and precise analysis results, unless we give up on component-based analysis design. However, the problem we illustrated not only occurs in the operand stack, but in every component that accesses shared state. For relational analysis, this means any component that handles values must coordinate with the call frame to resolve symbolic expressions. In WebAssembly, this includes for example components for value-carrying exceptions, which we would rather not have to redesign. But, we also found that shared state can occur within the call-frame component itself. Specifically, when analyzing recursive functions, each function call spawns a fresh call frame, while the caller’s call frame awaits. A strong variable update in the callee is necessary for precision, but it may not affect the variables of the caller, although they carry the same name and make use of the same relational domain. Moreover, we cannot allocate fresh variables to remember the old bindings like Analysis 3 did above, because this precludes a fixed-point for recursive functions.

We solve the above problems by introducing the virtual recency abstraction for local variables, inspired by the original recency abstraction for heaps [3]. The original recency abstraction distinguishes the most recent allocation at a memory location from all prior allocations at this location with two kinds of abstract addresses: a𝗋𝖾𝖼𝖾𝗇𝗍 and a𝗈𝗅𝖽. We adapt this technique for local variables, so that we get precise strong updates for 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 yet sound weak updates for 𝚡𝗈𝗅𝖽. However, when 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 is re-assigned, the recency abstraction usually requires a complete rewrite of the analysis state to retire 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 references and replace them with 𝚡𝗈𝗅𝖽 instead. This is not only a performance bottleneck, it is also non-modular because analysis components need to be aware of recency addresses contained within them. We avoid this problem by adding a new layer of abstraction over recency addresses, similar to virtual and physical memory in an operating system [19]. Specifically, we define symbolic expressions over virtual addresses, which are then mapped to physical addresses by an address translation. The physical addresses must be bounded to ensure termination; we use recency addresses 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 and 𝚡𝗈𝗅𝖽 for that. However, the virtual addresses space can grow freely and virtual addresses do not need to be retired, which elevates the need for rewriting the analysis state.

For example, consider again our Wasm example from above and let us illustrate how our new virtual recency abstraction works. The crucial step is the re-assignment in statement 3.

Statement Virtual Recency Abstraction
Stack Local Vars. Address translation Relations
;; 𝚢[1,5] [] 𝚢𝚢1 𝚢1𝚢r 𝚢r[1,5]
1: (local.set $x $y) [] 𝚡𝚡1,𝚢𝚢1 𝚡1𝚡r,𝚢1𝚢r 𝚡r=𝚢r,𝚢r[1,5]
2: (local.get $x) [𝚡𝟷]
3: (local.set $x (i32.add $y $y)) [𝚡1] 𝚡𝚡2,𝚢𝚢1 𝚡1𝚡o,𝚢1𝚢r,𝚡2𝚡r 𝚡o=𝚢r,𝚢r[1,5],𝚡r=2𝚢r
4: (local.get $x) [𝚡2,𝚡1]
5: (i32.eq) [𝖿𝖺𝗅𝗌𝖾]

The virtual recency abstraction handles statement 3 by (i) retiring 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 and replacing it by 𝚡𝗈𝗅𝖽 in the address translation and relational state, (ii) creating a fresh virtual address 𝚡2 and assigning it 𝚡𝗋𝖾𝖼𝖾𝗇𝗍, and (iii) initializing 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 with a strong update to 2𝚢𝗋𝖾𝖼𝖾𝗇𝗍. All the while, the reference 𝚡1 in stack remains valid, and the test 𝚡1=𝚡2 eventually fails, because y2y for y>0. Specifically, note that the analysis result is sound and precise, and we never needed to make breaking changes to the stack component. Indeed, later in the paper, we develop theories to show that analyses using the virtual recency abstraction for local variables are terminating and sound, and they can be defined modularly using analysis components.

To demonstrate the practicality of our solution and the reusability of analysis components, we apply the virtual recency abstraction to develop a flow-sensitive inter-procedural relational analysis for the research language Tip [42] and for WebAssembly 1.0 [26]. Tip is a simple imperative language with functions, loops, and recursion. WebAssembly (Wasm) is a modern low-level language for the web, that is statically-typed and features structured control-flow. We implement a new component for the call frame that supports strong-updates by using the virtual recency abstraction, which we use for both languages. Our Wasm analysis soundly and precisely approximates 32 and 64-bit integer operation with overflow handling, IEEE754 floating-point operations with special value handling (,,0.0,NaN), conversion operations between numeric types, assignment to local and global variables with strong-updates, the operand stack, and all other features of the Wasm language. We constructed the Wasm analysis by instantiating an existing analysis template for Wasm [8] called a generic interpreter. Generic interpreters capture analysis-independent functionality of a language and can be instantiated to obtain different analyses and also concrete interpreters [35, 6]. We instantiate the generic interpreter with new analysis components we developed specifically for the relational analysis, while soundly reusing existing components for Wasm’s operand stack, function tables, exceptions, and failures.

To demonstrate the practicality of the virtual recency abstraction, we evaluate recall, scalability, and precision of the relational Wasm analysis on the Wasm 1.0 specification test suite, 7 compiled C programs from the benchmarks game suite, and 9 existing and 22 custom precision tests. We find that our Wasm analysis on the Wasm 1.0 specification test suite has a high recall of 100% for Polyhedra [15], 94% for Octagons [40], and 100% for Intervals [11]. Furthermore, on 7 compiled C programs of the benchmarks game suite with 255 to 1120 lines of code, the Wasm analysis has an average running time of 22s for Polyhedra, 13s for Octagons, and 1.3s for Intervals. Lastly, on the precision tests our Wasm analysis achieves 63% precision for Polyhedra, 57% for Octagons, and 29% for Intervals.

In summary, we make the following contributions:

  • We introduce the virtual recency abstraction that enables strong updates for abstract interpreters with shared state (Section 2).

  • We prove that analyses based on the virtual recency abstraction terminate (Section 3).

  • We prove that the virtual recency abstraction allows to soundly reuse existing analysis components as-is (Section 4).

  • We evaluate the modularity of the virtual recency abstraction by developing relational analyses for Tip and Wasm 1.0 that reuse existing analysis components (Section 5).

  • We demonstrate the practicality of the relational Wasm analysis by evaluating recall, scalability, and precision (Section 6).

2 Virtual Recency Abstraction for Relational Analyses

To showcase the relevance of our approach, we start by presenting some analysis definitions and their shortcomings: unsoundness (Section 2.1), non-termination (Section 2.2), or non-modularity (Section 2.3). Then, we introduce our solution (Section 2.4), and explain how the virtual recency abstraction enables sound strong updates in relational analyses, while reusing existing modular analysis components as-is. Our solution will build upon various components of the alternative analyses.

2.1 Naive Strong Updates to Local Variables (Unsound)

We start with a relational analysis that performs naive strong updates on local variables. Local variables are managed in the call frame. The concrete call frame is a mapping from local variables of a function to their assigned values (𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾=𝖬𝖺𝗉[𝖵𝖺𝗋,𝖵𝖺𝗅]). For the sake of a simpler presentation, we focus on languages with integer values for now (𝖵𝖺𝗅=), but our relational analyses for Tip and WebAssembly have to abstract over values of different types (Section 5). A relational analysis abstracts call frames with a numerical relational abstraction over local variables such as convex Polyhedra [15] or Octagons [40] (𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^1=𝖱𝖾𝗅[𝖵𝖺𝗋,]).111We use hats to distinguish abstract definitions from their same named concrete definitions without a hat. The number in the subscript after call frames distinguishes different versions of the call frame introduced throughout this section. In case of convex Polyhedra, 𝖱𝖾𝗅[𝖵𝖺𝗋,] contains linear equalities and inequalities over variables in the program (a1𝚡1+an𝚡nc). Octagons sacrifice some precision compared to Polyhedra by limiting equalities and inequalities to two variables with unit coefficients (±𝚡1±𝚡2c), in order to gain an exponential performance speedup. The inner working of these abstract domains is not relevant here and we refer interested readers to the primary sources listed above.

The abstract call frame defines three key operations: variable assignment, variable lookup, and assertion checking.

Operation 𝖺𝗌𝗌𝗂𝗀𝗇^(x,v,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒) strongly updates variable x within the relational abstract domain by overwriting its prior value. Operation 𝗅𝗈𝗈𝗄𝗎𝗉^(x,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒) returns a symbolic reference to variable x if it is bound in 𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒 and otherwise. Operation 𝖺𝗌𝗌𝖾𝗋𝗍^(𝑐𝑜𝑛𝑠,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒) asserts if 𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒 does or does not satisfy a given constraint 𝑐𝑜𝑛𝑠. These operations are defined on numeric values abstracted with symbolic numeric expressions over local variables (𝖵𝖺𝗅^1=𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖵𝖺𝗋]) and boolean values abstracted with symbolic boolean expressions (𝖡𝗈𝗈𝗅𝖵𝖺𝗅^1=𝖱𝖾𝗅𝖢𝗈𝗇𝗌[𝖵𝖺𝗋]). Symbolic expressions are computed by the analysis as overapproximations of program expressions. For example, the abstract interpreter evaluates program expression 𝚊𝚋𝚜(𝚡) to symbolic expression 𝚡 in case x0 and 𝚡𝖭𝖾𝗀𝖺𝗍𝖾(𝚡) otherwise.

With these call frame operations we reproduce the unsoundness problem of 𝖠𝗇𝖺𝗅𝗒𝗌𝗂𝗌1 in the first example of Section 1:

Abstract Operations 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^1 𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^1]
1: c1𝖺𝗌𝗌𝗂𝗀𝗇(𝚢,𝖢𝗈𝗇𝗌𝗍([1,5]),[]) [𝚢[1,5]] []
2: c2𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝚢,c1) [𝚡=𝚢,𝚢[1,5]] []
3: s1𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c2),[]) [𝚡=𝚢,𝚢[1,5]] [𝚡]
4: c3𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝖠𝖽𝖽(𝚢,𝚢),c2) [𝚡=2𝚢,𝚢[1,5]] [𝚡]
5: s2𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c3),s1) [𝚡=2𝚢,𝚢[1,5]] [𝚡,𝚡]
6: 𝖺𝗌𝗌𝖾𝗋𝗍(𝖤𝗊(𝗉𝖾𝖾𝗄(s2,1),𝗉𝖾𝖾𝗄(s2,2)),c3) = 𝗍𝗋𝗎𝖾

The abstract stack is a list of abstract values, where operation 𝗉𝗎𝗌𝗁:𝖵𝖺𝗅^1×𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^1]𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^1] pushes an element on top of the stack and operation 𝗉𝖾𝖾𝗄:𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^1]×𝖵𝖺𝗅^1 retrieves the element on the stack at a given index. The assertion in the last line confirms that the top two elements on the stack are equal in the latest call frame c3. This is unsound because the value of variable 𝚡 has been overwritten line 4, so we would expect that top two elements on the stack refer to different values and are not equal. However, because both elements on the stack refer to the same symbolic expression 𝚡, the assertion holds. The problem is that the naive strong update overwrites the value of 𝚡, however, this also changes the value of symbolic expression 𝚡 on the stack, which is unsound.

A sound solution would be to weakly update local variables, joining newly assigned values into prior values: 𝖺𝗌𝗌𝗂𝗀𝗇^(x,v,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)=𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒[xv]. With weak updates, c3 in the example above would be [𝚢𝚡2𝚢,y[0,5]] and the assertion would return . This is sound, but imprecise because weak updates loose many benefits that relational analyses have over non-relational, making the performance trade-off not worth it.

2.2 Constant References to Local Variables (Non-Terminating)

The previous subsection has shown that naive strong updates to local variables also changes the value of references to these variables in other analysis components, which is unsound. This problem can be solved by implementing a call frame in which references to variables remain constant throughout the analysis. This is achieved by allocating a fresh reference – i.e, an abstract address – whenever a variable assignment is analyzed. This way references to variables via addresses remain constant.

To ensure that the analysis never runs out of addresses and can always assign new values to a freshly-generated addresses, the analysis defines addresses as the variable name paired with a natural number (𝖠𝖽𝖽𝗋^2=𝖵𝖺𝗋×). These abstract addresses are added to the call frame (𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2=𝖬𝖺𝗉[𝖵𝖺𝗋,𝒫(𝖠𝖽𝖽𝗋^2)]×𝖱𝖾𝗅[𝖠𝖽𝖽𝗋^2,]) [51] and to abstract values (𝖵𝖺𝗅^2=𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖠𝖽𝖽𝗋^2], 𝖡𝗈𝗈𝗅𝖵𝖺𝗅^2=𝖱𝖾𝗅𝖢𝗈𝗇𝗌[𝖠𝖽𝖽𝗋^2]). The powerset (𝒫(𝖠𝖽𝖽𝗋^2)) ensures that call frames can be joined if different addresses have been assigned to the same variable.

The operations on the new call frame are now defined as follows:

𝗅𝗈𝗈𝗄𝗎𝗉:𝖵𝖺𝗋×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2𝖵𝖺𝗅^2×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2
𝗅𝗈𝗈𝗄𝗎𝗉(x,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒@(𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒,_))=
𝐢𝐟(x𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒)𝗃𝗈𝗂𝗇({aa𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒(x)},𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)𝐞𝐥𝐬𝐞(,)
𝖺𝗌𝗌𝗂𝗀𝗇:𝖵𝖺𝗋×𝖵𝖺𝗅^2×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2
𝖺𝗌𝗌𝗂𝗀𝗇(x,v,(𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒^,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒^))=
𝐥𝐞𝐭𝑎𝑑𝑑𝑟=𝖿𝗋𝖾𝗌𝗁(x)𝐢𝐧(𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒^[x{𝑎𝑑𝑑𝑟}],𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒^[𝑎𝑑𝑑𝑟v])

Operation 𝗅𝗈𝗈𝗄𝗎𝗉 returns a reference to the addresses assigned to a local variable. If the call frame assigns multiple addresses to the variable, helper function 𝗃𝗈𝗂𝗇:𝒫(𝖵𝖺𝗅^)×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2𝖵𝖺𝗅^×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2 allocates a fresh address and joins the value of the other addresses into the fresh address. Operation 𝖺𝗌𝗌𝗂𝗀𝗇 allocates a fresh address to hold the new value, which avoids overwriting values of symbolic expressions that reference the variable.

Going back to our running example with this new call frame, we now obtain a sound result: the strong update to variable 𝚡 does not change the value of symbolic expression 𝚡1 on the stack.

Abstract Operations 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^2 𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^2]
1: c1𝖺𝗌𝗌𝗂𝗀𝗇(𝚢,𝖢𝗈𝗇𝗌𝗍([1,5]),[]) [𝚢{𝚢1}],[𝚢1[1,5]] []
2: c2𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝚢1,c1) [𝚡{𝚡1},𝚢{𝚢1}],[𝚡1=𝚢1,𝚢1[1,5]] []
3: s1𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c2),[]) -"- [𝚡1]
4: c3𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝖠𝖽𝖽(𝚢1,𝚢1),c2) [𝚡{𝚡2},𝚢{𝚢1}], [𝚡1]
[𝚡2=2𝚢1,𝚡1=𝚢1,𝚢1[1,5]]
5: s2𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c3),s1) -"- [𝚡2,𝚡1]
6: 𝖺𝗌𝗌𝖾𝗋𝗍(𝖤𝗊(𝗉𝖾𝖾𝗄(s2,1),𝗉𝖾𝖾𝗄(s2,2)),c3) = 𝖿𝖺𝗅𝗌𝖾

Unfortunately, the allocation of fresh addresses created a termination problem: the number of addresses that the analysis allocates is unbounded and hence the analysis does not terminate.

2.3 Recency Abstraction for Local Variables (Non-Modular)

The previous subsection has shown that assigning new values to freshly-generated addresses to keep references to addresses constant, created a termination problem. To solve this termination problem, the set of addresses must be finite and addresses must be reused on assignment. This can be done by adapting the recency abstraction [3], which traditionally has been used to handle dynamic memory allocation with a good precision-performance ratio. The recency abstraction distinguishes the most recent allocation of an object (x𝗋𝖾𝖼𝖾𝗇𝗍) from all prior allocations (x𝗈𝗅𝖽). Whenever a new allocation is performed for the same object, x𝗋𝖾𝖼𝖾𝗇𝗍 is first retired with all other prior allocations, to make it usable through strong updates for the newly allocated object.

Here, we use the recency abstraction to abstract the addresses from the previous subsection (𝖠𝖽𝖽𝗋^2=𝖵𝖺𝗋×) into a finite set of addresses (𝖠𝖽𝖽𝗋^3=𝖵𝖺𝗋×{𝗋𝖾𝖼𝖾𝗇𝗍,𝗈𝗅𝖽}) as shown with the following abstraction function:

Thanks to the recency abstraction, we can perform strong updates on recent local variables, while soundly referring to old values.

While the lookup and assertion operations stay mostly the same for the new call frame, variable assignment changes as follows:

𝖺𝗌𝗌𝗂𝗀𝗇:𝖵𝖺𝗋×𝖵𝖺𝗅^3×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^3×𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^3]𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^3×𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^3]
𝖺𝗌𝗌𝗂𝗀𝗇(x,v,(𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒),𝑠𝑡𝑎𝑐𝑘)=
𝐥𝐞𝐭𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒=𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒[x{x𝗋𝖾𝖼𝖾𝗇𝗍}]
𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒=𝖿𝗈𝗅𝖽(𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒,x𝗋𝖾𝖼𝖾𝗇𝗍,x𝗈𝗅𝖽)[x𝗋𝖾𝖼𝖾𝗇𝗍v]
𝑠𝑡𝑎𝑐𝑘=[v[x𝗋𝖾𝖼𝖾𝗇𝗍/x𝗈𝗅𝖽]v𝑠𝑡𝑎𝑐𝑘]
𝐢𝐧((𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒),𝑠𝑡𝑎𝑐𝑘)

Operation 𝖺𝗌𝗌𝗂𝗀𝗇^(x,v,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒,𝑠𝑡𝑎𝑐𝑘) joins the prior recent value x𝗋𝖾𝖼𝖾𝗇𝗍 into address x𝗈𝗅𝖽 and then strongly updates address x𝗋𝖾𝖼𝖾𝗇𝗍 to the newly assigned value v. The retirement of address x𝗋𝖾𝖼𝖾𝗇𝗍 into x𝗈𝗅𝖽 is performed by the relational 𝖿𝗈𝗅𝖽 operator [23]. Afterwards, operation 𝑎𝑠𝑠𝑖𝑔𝑛^ broadcasts this retirement to all other components of the analysis: this is a crucial step, which would otherwise make the analysis unsound. In particular, all references to addresses x𝗋𝖾𝖼𝖾𝗇𝗍 on the stack are renamed into x𝗈𝗅𝖽.

We obtain a sound result on the running example:

Abstract Operations 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^3 𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^3]
1: c1,s1𝖺𝗌𝗌𝗂𝗀𝗇(𝚢,𝖢𝗈𝗇𝗌𝗍([1,5]),[],[]) [𝚢{𝚢𝗋𝖾𝖼𝖾𝗇𝗍}],[𝚢𝗋𝖾𝖼𝖾𝗇𝗍[1,5]] []
2: c2,s2𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝚢𝗋𝖾𝖼𝖾𝗇𝗍,c1,s1) [𝚡{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚢{𝚢𝗋𝖾𝖼𝖾𝗇𝗍}], []
[𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚢𝗋𝖾𝖼𝖾𝗇𝗍,𝚢𝗋𝖾𝖼𝖾𝗇𝗍[1,5]]
3: s3𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c2),s2)    -"- [𝚡𝗋𝖾𝖼𝖾𝗇𝗍]
4: c3,s4𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝖠𝖽𝖽(𝚢𝗋𝖾𝖼𝖾𝗇𝗍,𝚢𝗋𝖾𝖼𝖾𝗇𝗍),c2,s3) [𝚡𝗋𝖾𝖼𝖾𝗇𝗍=2𝚢𝗋𝖾𝖼𝖾𝗇𝗍,𝚡𝗈𝗅𝖽=𝚢𝗋𝖾𝖼𝖾𝗇𝗍]
[𝚡{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚢{𝚢𝗋𝖾𝖼𝖾𝗇𝗍}], [𝚡𝗈𝗅𝖽]
5: s5𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c3),s4)    -"- [𝚡𝗋𝖾𝖼𝖾𝗇𝗍,𝚡𝗈𝗅𝖽]
6: 𝖺𝗌𝗌𝖾𝗋𝗍(𝖤𝗊(𝗉𝖾𝖾𝗄(s5,1),𝗉𝖾𝖾𝗄(s5,2)),c3) = 𝖿𝖺𝗅𝗌𝖾

The key difference to before is that the assignment of variable 𝚡 in line 4 does not overwrite the old value referenced by a symbolic expression on the stack. Instead, the call frame assigns the new value to address 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 and refers to the old value with 𝚡𝗈𝗅𝖽. This way the assertion in the last line confirms that the top two values on the stack are not equal, because one refers to 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 and the other to 𝚡𝗈𝗅𝖽.

Unfortunately, the recency abstraction causes a new modularity problem: the retirement of address 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 into 𝚡𝗈𝗅𝖽 needs to be broadcast and applied to all analysis components of the analysis state. This is not only slow, but also breaks the interface of analysis components, which now need to implement an address retirement operation. Furthermore, existing soundness proofs for these components become invalid and need to be reestablished for relational abstractions.

2.4 Virtual Recency Abstraction (Sound, Modular, Terminating)

To summarize, naive strong updates are unsound for relational abstractions, because they unsoundly change value to references in other analysis components. Assigning new values to freshly-generated addresses to keep references to other addresses constant is sound, but does not terminate because the number of addresses is unbounded. The recency abstraction is a finite abstraction over the infinite set of freshly-generated addresses and enables strong updates of local variables, however, the retirement of addresses requires to replace addresses in other analysis components, which is non-modular. We solve the final modularity problem by combining the recency abstraction with the generation of fresh addresses. Specifically, we add an extra layer of indirection between the two address types, similar to virtual memory in operating systems [19]. Therefore, we call this the Virtual Recency Abstraction.

The virtual recency abstraction defines a mapping between virtual addresses (𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋=𝖵𝖺𝗋×, addresses from Section 2.2) and symbolic expressions. Virtual addresses are first mapped to physical addresses (𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋=𝖵𝖺𝗋×{𝗋𝖾𝖼𝖾𝗇𝗍×𝗈𝗅𝖽}, addresses from Section 2.3), which are then used as variables in the relational domain. We call the mapping between virtual and physical addresses the address translation (𝖠𝖽𝖽𝗋𝖳𝗋𝖺𝗇𝗌^=𝖬𝖺𝗉[𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋,𝒫(𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋)]). The call frame maps variables to virtual addresses, includes the address translation from virtual to physical addresses, and defines the relational abstract domain over physical addresses:

𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4=𝖬𝖺𝗉[𝖵𝖺𝗋,𝒫(𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋)]×𝖬𝖺𝗉[𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋,𝒫(𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋)]×𝖱𝖾𝗅[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋,]
𝗅𝗈𝗈𝗄𝗎𝗉:𝖵𝖺𝗋×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4𝖵𝖺𝗅^4×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4
𝗅𝗈𝗈𝗄𝗎𝗉(x,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒@(𝑓𝑟𝑎𝑚𝑒,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒))=
𝐢𝐟(x𝑓𝑟𝑎𝑚𝑒)𝐥𝐞𝐭𝑣𝑖𝑟𝑡𝐴𝑑𝑑𝑟=𝖿𝗋𝖾𝗌𝗁(x)𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠=𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠[𝑣𝑖𝑟𝑡𝐴𝑑𝑑𝑟v𝑓𝑟𝑎𝑚𝑒(x)𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠(v)]𝐢𝐧(𝑣𝑖𝑟𝑡𝐴𝑑𝑑𝑟,(𝑓𝑟𝑎𝑚𝑒,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒))
𝐞𝐥𝐬𝐞(,)
𝖺𝗌𝗌𝗂𝗀𝗇:𝖵𝖺𝗋×𝖵𝖺𝗅^4×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4
𝖺𝗌𝗌𝗂𝗀𝗇(x,v,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)=
𝐥𝐞𝐭(v,(𝑓𝑟𝑎𝑚𝑒,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒))=𝗍𝗈𝖯𝗁𝗒𝗌𝖤𝗑𝗉𝗋(v,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)
𝑣𝑖𝑟𝑡𝐴𝑑𝑑𝑟=𝖿𝗋𝖾𝗌𝗁(x)
𝑓𝑟𝑎𝑚𝑒=𝑓𝑟𝑎𝑚𝑒[x{𝑣𝑖𝑟𝑡𝐴𝑑𝑑𝑟}]
𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠=𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠[xn{x𝗈𝗅𝖽}xn𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠][𝑣𝑖𝑟𝑡𝐴𝑑𝑑𝑟{x𝗋𝖾𝖼𝖾𝗇𝗍}]
𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒=𝖿𝗈𝗅𝖽(𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒,x𝗋𝖾𝖼𝖾𝗇𝗍,x𝗈𝗅𝖽)[x𝗋𝖾𝖼𝖾𝗇𝗍v]
𝐢𝐧(𝑓𝑟𝑎𝑚𝑒,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒)
𝐰𝐡𝐞𝐫𝐞𝗍𝗈𝖯𝗁𝗒𝗌𝖤𝗑𝗉𝗋:𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋]×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋]×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4
𝖺𝗌𝗌𝖾𝗋𝗍:𝖡𝗈𝗈𝗅𝖵𝖺𝗅^4×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4𝖡𝗈𝗈𝗅^×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4
𝖺𝗌𝗌𝖾𝗋𝗍(𝑐𝑜𝑛𝑠,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)=
𝐥𝐞𝐭(cons,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒@(_,_,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒))=𝗍𝗈𝖯𝗁𝗒𝗌𝖢𝗈𝗇𝗌(𝑐𝑜𝑛𝑠,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)
𝐢𝐧{(,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)if 𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒 satisfies 𝑐𝑜𝑛𝑠 and 𝗇𝗈𝗍(𝑐𝑜𝑛𝑠)(𝗍𝗋𝗎𝖾,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)if 𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒 satisfies 𝑐𝑜𝑛𝑠(𝖿𝖺𝗅𝗌𝖾,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)if 𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒 satisfies 𝗇𝗈𝗍(𝑐𝑜𝑛𝑠)
𝐰𝐡𝐞𝐫𝐞𝗍𝗈𝖯𝗁𝗒𝗌𝖢𝗈𝗇𝗌:𝖱𝖾𝗅𝖢𝗈𝗇𝗌[𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋]×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4𝖱𝖾𝗅𝖢𝗈𝗇𝗌[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋]×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4
Figure 1: Operations of the Virtual Recency Abstraction.

We now define the abstract operations for the call frame of the virtual recency abstraction (Figure 1). Operation 𝗅𝗈𝗈𝗄𝗎𝗉(x,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒) returns a fresh virtual address that refers to all physical addresses that correspond to x within the address translation. Operation 𝖺𝗌𝗌𝗂𝗀𝗇(x,v,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒) first converts the virtual addresses contained in value v to physical addresses with helper function 𝗍𝗈𝖯𝗁𝗒𝗌𝖤𝗑𝗉𝗋. It then points variable x to a freshly generated virtual address, assigns this virtual address to x𝗋𝖾𝖼𝖾𝗇𝗍 in the address translation, folds x𝗋𝖾𝖼𝖾𝗇𝗍 into x𝗈𝗅𝖽 in the relational abstract domain, and finally assigns the converted value to x𝗋𝖾𝖼𝖾𝗇𝗍. Helper function 𝗍𝗈𝖯𝗁𝗒𝗌𝖤𝗑𝗉𝗋 replaces virtual addresses in symbolic expression with physical addresses, such that the resulting expression can be assigned to the relational abstract domain. In case virtual address xn points to multiple physical addresses {x𝗋𝖾𝖼𝖾𝗇𝗍,x𝗈𝗅𝖽}, 𝗍𝗈𝖯𝗁𝗒𝗌𝖤𝗑𝗉𝗋 joins x𝗋𝖾𝖼𝖾𝗇𝗍 into x𝗈𝗅𝖽 in the relational abstract domain and replaces virtual address xn with physical address x𝗈𝗅𝖽 in the symbolic expression. Similarly, operation 𝖺𝗌𝗌𝖾𝗋𝗍 first converts constraints over virtual addresses to constraints over physical addresses and afterwards decides if the resulting constraint is satisfied by the relational abstract domain.

To demonstrate that the virtual recency abstraction solves the unsoundness and modularity problem, we again reproduce the running example:

Abstract Operations 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4 𝖲𝗍𝖺𝖼𝗄^[𝖵𝖺𝗅^4]
1: c1𝖺𝗌𝗌𝗂𝗀𝗇(𝚢,𝖢𝗈𝗇𝗌𝗍([1,5]),[]) [𝚢{𝚢1}],[𝚢1{𝚢𝗋𝖾𝖼𝖾𝗇𝗍}],[𝚢𝗋𝖾𝖼𝖾𝗇𝗍[1,5]] []
2: c2𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝚢1,c1) [𝚡{𝚡1},𝚢{𝚢1}], []
[𝚡1{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚢1{𝚢𝗋𝖾𝖼𝖾𝗇𝗍}],
[𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚢𝗋𝖾𝖼𝖾𝗇𝗍,𝚢𝗋𝖾𝖼𝖾𝗇𝗍[1,5]]
3: s3𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c2),[])    -"- [𝚡1]
4: c3,s4𝖺𝗌𝗌𝗂𝗀𝗇(𝚡,𝖠𝖽𝖽(𝚢1,𝚢1),c2) [𝚡{𝚡2},𝚢{𝚢1}], [𝚡1]
[𝚡2{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚡1{𝚡𝗈𝗅𝖽}],
[𝚡𝗋𝖾𝖼𝖾𝗇𝗍=2𝚢𝗋𝖾𝖼𝖾𝗇𝗍,𝚡𝗈𝗅𝖽=𝚢𝗋𝖾𝖼𝖾𝗇𝗍]
5: s5𝗉𝗎𝗌𝗁(𝗅𝗈𝗈𝗄𝗎𝗉(𝚡,c3),s4)    -"- [𝚡2,𝚡1]
6: 𝖺𝗌𝗌𝖾𝗋𝗍(𝖤𝗊(𝗉𝖾𝖾𝗄(s5,1),𝗉𝖾𝖾𝗄(s5,2)),c3) = 𝖿𝖺𝗅𝗌𝖾

The analysis in this example is sound because the assignment in line 4 does not overwrite the prior value of virtual address 𝚡1, but moves it from physical address 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 to 𝚡𝗈𝗅𝖽. The new value for virtual address 𝚡2 is then written to a physical address 𝚡𝗋𝖾𝖼𝖾𝗇𝗍. Furthermore, the analysis is modular, because virtual address 𝚡1 is retired to 𝚡𝗈𝗅𝖽 by adjusting the address translation, without the need to change addresses on the stack.

To summarize, the virtual recency abstraction solves all the issues of soundness, termination and modularity illustrated in the previous subsections. The next sections will provide further details on the features of our virtual recency abstraction:

Termination

The virtual recency abstraction terminates, thanks to an ordering of the call frame based on physical addresses, which is a finite set. We detail this claim in Section 3.

Soundness

The virtual recency abstraction enables sound strong updates by assigning to 𝗋𝖾𝖼𝖾𝗇𝗍 addresses and retiring references to 𝗈𝗅𝖽. We prove soundness formally in Section 4.

Modularity

The virtual recency abstraction solves the modularity problem, allowing to reuse existing analysis components as-is. For example, our relational analysis for WebAssembly reuses existing analysis components for the stack, memory, global variables, function tables, exceptions, and failures, which were originally designed for a non-relational analysis (Section 5).

In the following parts of the paper, we keep the callframe abstraction described in this subsection: 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^=𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^4.

3 Termination of the Virtual Recency Abstraction

In this section, we address the termination of analyses that use the virtual recency abstraction. Specifically, we describe a procedure that terminates analyses despite the infinite supply of virtual addresses.

3.1 Terminate Infinite Ascending Chains on Call Frames

One problem that threatens termination of the analysis is the unbounded number of virtual addresses in the call frame. To solve this problem, we define an ordering on call frames that does not compare virtual addresses directly. Instead, the ordering maps virtual addresses to physical addresses, which is a finite set.

(𝑓𝑟𝑎𝑚𝑒1,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠1,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒1)𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^(𝑓𝑟𝑎𝑚𝑒2,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠2,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒2)
 iff (𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠1𝑓𝑟𝑎𝑚𝑒1)𝖬𝖺𝗉[𝖵𝖺𝗋,𝒫(𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋)](𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠2𝑓𝑟𝑎𝑚𝑒2)
𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒1𝖱𝖾𝗅[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋,]𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒2

This pre-order on call frames is not anti-symmetric (c1c2 does not imply c1=c2), e.g.,

c1=([𝚡{𝚡3}],[𝚡3{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚡2{𝚡𝗈𝗅𝖽},],[𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽10𝚡𝗈𝗅𝖽100])
c2=([𝚡{𝚡4}],[𝚡4{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚡3{𝚡𝗈𝗅𝖽},],[𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽10𝚡𝗈𝗅𝖽100])

The lack of anti-symmetry is crucial for the virtual recency abstraction, because normalizing call frames to a unique representation would require updating virtual addresses in analysis components, which is not modular (Section 2.3).

Another problem that threatens termination are infinite ascending chains on relational abstract domains (𝖱𝖾𝗅[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋,]) contained within the call frame. To this end, we define a widening operator on call frames that applies a widening operator on the contained relational abstract domain. A widening operator [12] :L×LL computes an upper bound of its operands xxyy and terminates an infinite ascending chain x1x2x3 when folded over it: x1x1x2(x1x2)x3 The widening operator on call frames is defined as follows:
(𝑓𝑟𝑎𝑚𝑒1,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠1,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒1)𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^(𝑓𝑟𝑎𝑚𝑒2,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠2,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒2)= 𝐥𝐞𝐭𝑓𝑟𝑎𝑚𝑒=[x𝑓𝑟𝑎𝑚𝑒1(x)𝑓𝑟𝑎𝑚𝑒2(x)x𝑓𝑟𝑎𝑚𝑒1x𝑓𝑟𝑎𝑚𝑒2] 𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠=[v𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠1(v)𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠2(v)v𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠1v𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠2] 𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒=𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒1𝖱𝖾𝗅[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋,]𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒2 𝐢𝐧(𝑓𝑟𝑎𝑚𝑒,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒)

This widening operator terminates an infinite ascending chain of call frames 𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒1𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒2, because the address frame after mapping virtual addresses to physical addresses is a lattice of finite height (𝖬𝖺𝗉[𝖵𝖺𝗋,𝒫(𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋)]). Furthermore, the relational abstract domain terminates by its own widening operator.

Lastly, we describe a fixpoint algorithm that iterates a monotone analysis function f^:PP over a pre-order (P,,) with least element and ensures termination with a widening operator :P×PP. The fixpoint algorithm iterates (xi)i defined by x1= and xn+1=xnf^(xn) until the ascending chain terminates after p steps with xpxp1. This final iteratee xp soundly overapproximates the least fixpoint of a concrete semantics function f:LL over lattice (L,):

Theorem 1 (Fixpoint Iteration with Widening over Pre-Orders [10, Theorem 34]).

Let f^:PP be a monotone analysis function over pre-order (P,,) that soundly overapproximates the monotone f:LL over lattice (L,) such that for a monotone concretization γ:PL it holds xP.γ(f^(x))f(γ(x)).

  1. 1.

    The fixpoint iteration (xi)i defined above reaches a post fixpoint xpf^(xp).

  2. 2.

    Every such post fixpoint xp overapproximates the least fixpoint of f: γ(xp)𝗅𝖿𝗉(f).

Proof.

Proofs can be found in our supplementary material and [10, Theorem 34].

3.2 Terminate Infinite Ascending Chains on Values and Analysis States

Another problem that threatens the termination of the analysis is the unbounded number of abstract values (𝖵𝖺𝗅^=𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋]). To solve this problem, the ordering on abstract values does not compare symbolic expressions v1,v2 syntactically, but compares them semantically by evaluating them in the given call frames c1,c2:
(v1,c1)𝖵𝖺𝗅^(v2,c2) iff c1=𝖺𝗌𝗌𝗂𝗀𝗇^(𝑓𝑟𝑒𝑠ℎ,v1,c1)c2=𝖺𝗌𝗌𝗂𝗀𝗇^(𝑓𝑟𝑒𝑠ℎ,v2,c2)c1𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^c2

For example, expressions 𝖠𝖽𝖽(𝚡1,𝚢1) and 𝖬𝗎𝗅(𝚡1,𝖢𝗈𝗇𝗌𝗍(2)) are indistinguishable by the value ordering in call frame (,[𝚡1{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚢1{𝚢𝗋𝖾𝖼𝖾𝗇𝗍}],[1𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚢𝗋𝖾𝖼𝖾𝗇𝗍10]).

The widening operator on abstract values takes two values and two call frames as input.

𝖵𝖺𝗅^:(𝖵𝖺𝗅^×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^)×(𝖵𝖺𝗅^×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^)𝖵𝖺𝗅^×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^

The full definition of the operator is given in Definition 2 in the supplementary material. In this section, we discuss the operator at a high-level. Depending on the expressions contained in the abstract values, the operator implements different widening strategies:

  • In case the expressions are both constant, i.e. contain no variables, each expression is evaluated in the respective call frame and the resulting intervals are widened.

  • In case the expressions are structurally equal, i.e. differ only in the recency of their addresses, the widening operator attempts a structural join. This happens often when the fixpoint algorithm widens two analysis states and handling this case avoids adding addresses to the relational abstract domain to hold the join of expressions. For example, the analysis of loop for(int i = 0; ...; i++) requires to widen two expressions for the incrementation of the index variable 𝚒: 𝙰𝚍𝚍(𝚒n1,𝖢𝗈𝗇𝗌𝗍(1)) where 𝚒n1 is old and 𝙰𝚍𝚍(𝚒n,𝖢𝗈𝗇𝗌𝗍(1)) where 𝚒n is recent. The widening operator will join these two expressions into 𝙰𝚍𝚍(𝚒n+1,𝖢𝗈𝗇𝗌𝗍(1)), where 𝚒n+1 mapped to 𝗋𝖾𝖼𝖾𝗇𝗍 and 𝗈𝗅𝖽 by the address translation.

  • In all other cases the operator assigns expressions v1,v2 to a join address 𝑐𝑡𝑥k in the respective call frame.

    (v1,(𝑓𝑟𝑎𝑚𝑒1,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠1,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒1))𝖵𝖺𝗅^(v2,(𝑓𝑟𝑎𝑚𝑒2,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠2,𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒2))=
    𝐥𝐞𝐭𝑐𝑡𝑥k=𝖺𝗅𝗅𝗈𝖼(𝑒𝑥𝑝𝑟1,𝑒𝑥𝑝𝑟2)𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒1=(𝑓𝑟𝑎𝑚𝑒1,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠1[𝑐𝑡𝑥k{𝑐𝑡𝑥𝗈𝗅𝖽}],𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒1[𝑐𝑡𝑥𝗈𝗅𝖽v1])𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒2=(𝑓𝑟𝑎𝑚𝑒2,𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠2[𝑐𝑡𝑥k{𝑐𝑡𝑥𝗈𝗅𝖽}],𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒2[𝑐𝑡𝑥𝗈𝗅𝖽v2])𝐢𝐧(𝑐𝑡𝑥k,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒1,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒2)

    The context of join address 𝑐𝑡𝑥n is decided by function 𝖺𝗅𝗅𝗈𝖼, which is an additional parameter of the widening operator. Depending on where operator 𝖵𝖺𝗅^ is called, different contexts may be allocated. For example, when two stack entries are widened, the allocator might return a context 𝖲𝗍𝖺𝖼𝗄(𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛,𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑡𝑖𝑜𝑛,𝑓𝑢𝑛𝑐𝑡𝑖𝑜𝑛) to indicate the position on the stack, the current instruction, and function where the widening occurred.

Finally, we describe a widening operator 𝖲𝗍𝖺𝗍𝖾^ on compound analysis states, whose type 𝖲𝗍𝖺𝗍𝖾^ we do not specify for generality. Since two compound analysis states S,S with respective call frames c1,c1 contain many values v1vn and v1vn that need to be widened pair-wise, we fold operator 𝖵𝖺𝗅^ over all values (vi,ci)𝖵𝖺𝗅^(v1,ci)=(vi′′,ci+1,ci+1) where 1in and finally apply the widening operator on the two resulting call frames cn𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^cn. This terminates infinite ascending chains of compound analysis states because abstract values are compared by evaluating them in respective call frames, which themselves are widened during every step.

3.3 Terminating Analysis of Recursive Functions

In this subsection, we describe how the virtual recency abstraction terminates the analysis of recursive functions. To ensure termination of the analysis, we use an existing fixpoint algorithm [34], which we illustrate at the example of the recrusive sum function:

The graph to the top-right shows a trace of the analysis, where (𝚜𝚞𝚖@𝑐𝑡𝑥,c)c can be read as function 𝚜𝚞𝚖 in calling context 𝑐𝑡𝑥 under input call frame c evaluates to output call frame c. We use short-hand notation for the frame and address translation, e.g., [𝚡1,2𝚡𝗈𝗅𝖽] stands for [𝚡1{𝚡𝗈𝗅𝖽},𝚡2{𝚡𝗈𝗅𝖽}]. The fixpoint algorithm first recursively analyzes function sum, widening consecutive input call frames after the function parameters have been assigned, until they do not grow any further at c4c3. The algorithm then iterates between the recurrent recursive calls 𝚜𝚞𝚖@[7,7,7,3] and 𝚜𝚞𝚖@[7,7,3] until the widened output call frames c5,c5c6,(c5c6)c7 do not grow anymore. Once the iteration is finished, the algorithm returns to the top-most call with final result:

[𝚊𝗋𝖾𝖼𝖾𝗇𝗍𝚡𝗈𝗅𝖽𝚡𝗋𝖾𝖼𝖾𝗇𝗍0,𝚡𝗋𝖾𝖼𝖾𝗇𝗍𝚡𝗈𝗅𝖽1,𝚋𝗋𝖾𝖼𝖾𝗇𝗍=𝚢𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽+𝚢𝗈𝗅𝖽𝚡𝗈𝗅𝖽]

The inequalities 𝚊𝗋𝖾𝖼𝖾𝗇𝗍𝚡𝗈𝗅𝖽𝚡𝗋𝖾𝖼𝖾𝗇𝗍0 show that parameter 𝚡 decreases from 𝚊 down to 0. Furthermore, 𝚢𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽+𝚢𝗈𝗅𝖽𝚡𝗈𝗅𝖽 show that the last result is the sum of any 𝚡 with any result 𝚢, which is greater than any input 𝚡𝗈𝗅𝖽.

4 Soundness of the Virtual Recency Abstraction

In this section, we prove that existing analysis components remain sound when instantiated relational values in form of symbolic expressions. We prove this by showing that abstract components remain sound overapproximations of concrete components on all operations of the abstract call frame. To prove soundness, we first define concretization functions on abstract call frames, values, and compound analysis states.

4.1 Concretizations on Call Frames, Values, and Analysis States

Typically, soundness would be proven with respect to Galois connections [12]. However, some relational abstract domains, such as polyhedra, do not enjoy Galois connections [41, Paragraph “Absence of Galois Connection”]. The problem is that the abstraction function α, that computes the best possible approximation of a concrete property in the abstract domain, may not exist. Instead, we prove soundness with respect to monotone concretization functions γ from abstract domain to concrete domain, which is also used in mechanized soundness proofs [31]. The full definitions of the concretization functions can be found in Definitions 3-5 in the supplementary material. Here we discuss the concretizations at a high-level by example.

The concretization function for abstract call frames is the composition of two functions:

This two-step-concretization is needed because the concretization for values evaluates them in 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾. Concretization γ𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^ expands the codomain of the relational abstraction to virtual addresses and then concretizes it. This is necessary to assign virtual addresses on the same physical address their own individual values. Concretization γ𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾 maps to the result of the first to regular concrete call frames. To illustrate the concretization on call frames, consider the following example:

c=([𝚡{𝚡3}],[𝚡1{𝚡𝗈𝗅𝖽},𝚡2{𝚡𝗈𝗅𝖽},𝚡3{𝚡𝗋𝖾𝖼𝖾𝗇𝗍}],[𝚡𝗈𝗅𝖽<𝚡𝗋𝖾𝖼𝖾𝗇𝗍])
γ𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^(c)=i<k,j<k{([𝚡{𝚡3}],[𝚡1i,𝚡2j,𝚡3k])}{([𝚡{𝚡3}],[𝚡11,𝚡21,𝚡32]),([𝚡{𝚡3}],[𝚡11,𝚡22,𝚡33]),}
γ𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾(γ𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^(c))={[𝚡i]i}={[𝚡1],[𝚡0],[𝚡1]}

The concretization function for abstract values (𝖵𝖺𝗅^=𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋]) evaluates symbolic expressions in a given 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾:

γ𝖵𝖺𝗅^:𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾𝖵𝖺𝗅^𝒫(𝖵𝖺𝗅)

Symbolic expressions are not evaluated in 𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^ because this would loose the relations between variables, which makes the soundness proof meaningless [43]. To illustrate the concretization of abstract values, consider the interpretation of (x3x1)[1,3] in a call frame cγ𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^(c):
c=(,[𝚡11,𝚡21,𝚡32]) γ𝖵𝖺𝗅^(c)(𝖬𝗎𝗅(𝖲𝗎𝖻(𝚡3,𝚡1),𝖢𝗈𝗇𝗌𝗍([1,3])))={(21)1,(21)2,(21)3}={1,2,3}

Next, we describe the concretization of analysis components. To stay as generic as possible, we do not require any structure of analysis components. The only requirement is that analysis must be parametric in the type of values (𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^[𝖵𝖺𝗅^]), such that they can be reused in a relational analysis. Because components are parametric in the type of values, their concretization function is also parameterized by a concretization function for values:

γ𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^:V^.(V^𝒫(𝖵𝖺𝗅))(𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^[V^]𝒫(𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍))

As an example, consider the concretization function for a stack analysis component:

𝖲𝗍𝖺𝖼𝗄^[𝑉𝑎𝑙]=𝖫𝗂𝗌𝗍[𝑉𝑎𝑙]𝖲𝗍𝖺𝖼𝗄[𝑉𝑎𝑙]=𝖫𝗂𝗌𝗍[𝑉𝑎𝑙]
γ𝖲𝗍𝖺𝖼𝗄^:𝑉𝑎𝑙^,𝑉𝑎𝑙.(𝑉𝑎𝑙^𝒫(𝑉𝑎𝑙))𝖲𝗍𝖺𝖼𝗄^[𝑉𝑎𝑙^]𝒫(𝖲𝗍𝖺𝖼𝗄[𝑉𝑎𝑙])
γ𝖲𝗍𝖺𝖼𝗄^(γ𝑉𝑎𝑙^)(𝑠𝑡𝑎𝑐𝑘^)={𝑠𝑡𝑎𝑐𝑘𝗌𝗂𝗓𝖾(𝑠𝑡𝑎𝑐𝑘)=𝗌𝗂𝗓𝖾(𝑠𝑡𝑎𝑐𝑘^)i.𝑠𝑡𝑎𝑐𝑘[i]γ𝑉𝑎𝑙^(𝑠𝑡𝑎𝑐𝑘^[i])}

To use the concretization function for stacks, we apply it to the concretization function for abstract values:

γ𝖲𝗍𝖺𝖼𝗄^(γ𝖵𝖺𝗅^(,[𝚡11,𝚡21,𝚡32]))([𝚡1,𝚡3])={[1,2]}

Finally, we describe the concretization function on compound analysis states. The analysis state consists of multiple components together with the call frame of the virtual recency abstraction:

𝖲𝗍𝖺𝗍𝖾=𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍1×𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍n×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾
𝖲𝗍𝖺𝗍𝖾^=𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^1[𝖵𝖺𝗅^]×𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^n[𝖵𝖺𝗅^]×𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^

The concretization function γ𝖲𝗍𝖺𝗍𝖾^:𝖲𝗍𝖺𝗍𝖾^𝒫(𝖲𝗍𝖺𝗍𝖾) partially applies a concretized call frame cγ𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^(c^) to the concretization function for values to obtain a concretization functions on components: γ𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^i[𝖵𝖺𝗅^](γ𝖵𝖺𝗅^(𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)):𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^i[𝖵𝖺𝗅^]𝒫(𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍i).

4.2 Soundness Theorem

Now we are ready to state the main soundness theorem. The theorem guarantees that existing analysis components can be soundly reused in a relational analysis that uses the virtual recency abstraction.

Theorem 2 (Modular Soundness of the Virtual Recency Abstraction).

Given a modular analysis consisting of the components 𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^1,𝖢𝗈𝗆𝗉𝗈𝗇𝖾𝗇𝗍^n,𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^. All analysis components remain sound overapproximations of their concrete counterparts on all operations of the call frame of the virtual recency abstraction:

(𝑐𝑜𝑚𝑝𝑠,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)γ𝖲𝗍𝖺𝗍𝖾^(𝑐𝑜𝑚𝑝𝑠^,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒^),vγ𝖵𝖺𝗅^(v^,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒),
bγ𝖡𝗈𝗈𝗅𝖵𝖺𝗅^(b^,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒),x𝖵𝖺𝗋.
(𝑐𝑜𝑚𝑝𝑠,π2(𝗅𝗈𝗈𝗄𝗎𝗉(x,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)))γ𝖲𝗍𝖺𝗍𝖾^(𝑐𝑜𝑚𝑝𝑠^,π2(𝗅𝗈𝗈𝗄𝗎𝗉^(x,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒^)))
(𝑐𝑜𝑚𝑝𝑠,𝖺𝗌𝗌𝗂𝗀𝗇(x,v,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒))γ𝖲𝗍𝖺𝗍𝖾^(𝑐𝑜𝑚𝑝𝑠^,𝖺𝗌𝗌𝗂𝗀𝗇^(x,v^,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒^))
(𝑐𝑜𝑚𝑝𝑠,π2(𝖺𝗌𝗌𝖾𝗋𝗍(b,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒)))γ𝖲𝗍𝖺𝗍𝖾^(𝑐𝑜𝑚𝑝𝑠^,π2(𝖺𝗌𝗌𝖾𝗋𝗍^(b^,𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒^))).

Proof.

The proof is in the supplementary material accompanying this paper.

5 Modular Reuse of Existing Analysis Components

In this section, we evaluate the modularity of the virtual recency abstraction by using it to develop relational analyses for two languages and report on the reuse of existing analysis functionality. Specifically, we developed analyses for the research language Tip and WebAssembly 1.0.

5.1 Relational Analysis for Tip

Tip [42] is a small imperative research language with functions, conditionals, loops, integers, references, and records.

We developed a relational analysis for Tip in Scala in the Sturdy framework by reusing code of an existing non-relational analysis for Tip.222We intend to submit an artifact including the code of the analyses. Figure 3 shows a Venn diagram that reports what analysis functionality was reused and what new code needed to be implemented. Furthermore, the diagram shows which analysis functionality is specific to Tip and which is language-independent. Our evaluation shows that 86.1% of the code of the analysis is either reused as-is or new code that is language-independent. We demonstrate that the code is language-independent by reusing it in our WebAssembly analysis.

Figure 2: Reuse and language-independence of analysis functionality in our relational Tip analysis. Lines of code (loc) were measured with cloc (https://github.com/AlDanial/cloc). Percentages are computed with respect to the total 6360 loc of the relational Tip analysis.

Generic Interpreter.

We now describe in more detail the different parts of the Tip analysis, which parts could be reused, and are language-independent. We implemented the relational analysis for Tip by instantiating an existing generic interpreter for Tip. A generic interpreter captures analysis-independent language semantics and can be used to derive different analyses and even a concrete semantics [35]. The generic interpreter for Tip has been used to derive a non-relational interval analysis. Our relational analysis reuses the code of the generic interpreter as-is, because the generic interpreter is parametric in the values, addresses, and effects.

To derive an analysis, the generic interpreter must be instantiated with analysis components [33]. Analysis components describe analysis functionality, such as the virtual recency abstraction for call frames or abstract operations on integers. To this end, we distinguish analysis components for values (called value components) and analysis components for effects (called effect components).

Value Components.

The Tip language has boolean, integer, reference, function, and record values. The abstraction for abstract values joins values of different types to . Numeric operators support detection of integer overflows. To summarize, for the implementation of the relational Tip analysis, we reused code for analyzing functions, and record values, and implemented new code for analyzing integer, boolean, and reference values.

Effect Components.

The Tip language features a call frame of local variables, a heap of dynamically-allocated values, a console for printing and reading user input, and abort execution due to a program failure.

To analyze the call frame and the heap, we refactor the virtual recency abstraction from Section 2:

𝖢𝖺𝗅𝗅𝖥𝗋𝖺𝗆𝖾^[𝐶𝑡𝑥,𝑉𝑎𝑙]=𝖬𝖺𝗉[𝖵𝖺𝗋,𝒫(𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋[𝐶𝑡𝑥])]×𝖱𝖾𝖼𝖾𝗇𝖼𝗒𝖲𝗍𝗈𝗋𝖾[𝐶𝑡𝑥,𝑉𝑎𝑙]
𝖧𝖾𝖺𝗉^[𝐶𝑡𝑥,𝑉𝑎𝑙]=𝖱𝖾𝖼𝖾𝗇𝖼𝗒𝖲𝗍𝗈𝗋𝖾[𝐶𝑡𝑥,𝑉𝑎𝑙]
𝖱𝖾𝖼𝖾𝗇𝖼𝗒𝖲𝗍𝗈𝗋𝖾[𝐶𝑡𝑥,𝑉𝑎𝑙]=𝖬𝖺𝗉[𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋[𝐶𝑡𝑥],𝒫(𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋[𝐶𝑡𝑥])]×𝖬𝖺𝗉[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋[𝐶𝑡𝑥],𝑉𝑎𝑙]×𝖱𝖾𝗅[𝖯𝗁𝗒𝗌𝖠𝖽𝖽𝗋[𝐶𝑡𝑥],]

Specifically, we split the abstract call frame into the address frame and a new abstraction called recency store. The recency store contains the address translation and relational abstract domain. The same recency store is also used to analyze the heap, which allows relations to span over local and heap-allocated variables. Furthermore, we added a map to the recency store that holds non-relational values, which is also used to store meta-data of numeric variables such as the their types. When looking up from the call frame, the value stored in the non-relational map is joined with the value in the relational abstract domain, if there is any.

For example, analyzing the Tip program l1: x = console.input(); l2: y = alloc x results in the following abstract call frame:
𝑎𝑑𝑑𝑟𝐹𝑟𝑎𝑚𝑒: [𝚡{𝚡1},𝚢{𝚢1}] 𝑎𝑑𝑑𝑟𝑇𝑟𝑎𝑛𝑠: [𝚡1{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚢1{𝚢𝗋𝖾𝖼𝖾𝗇𝗍},𝖨𝗇𝗉𝗎𝗍(l1)1{𝖨𝗇𝗉𝗎𝗍(l1)𝗋𝖾𝖼𝖾𝗇𝗍}, 𝖠𝗅𝗅𝗈𝖼(l2)1{𝖠𝗅𝗅𝗈𝖼(l2)𝗋𝖾𝖼𝖾𝗇𝗍}] 𝑛𝑜𝑛𝑅𝑒𝑙𝑆𝑡𝑜𝑟𝑒: 𝑟𝑒𝑙𝑆𝑡𝑜𝑟𝑒: [𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝖨𝗇𝗉𝗎𝗍(l1),𝖠𝗅𝗅𝗈𝖼(l2)𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗋𝖾𝖼𝖾𝗇𝗍]

The relational abstract domain used in the abstract call frame is implemented in the Apron library [29]. Apron provides a unified interface for relational abstract domains: changing its underlying domains does not require changes to the high-level analysis. Apron supports various domains such as Polyhedra [15] and Octagons [40].

To summarize, for the implementation of the relational Tip analysis, we reused code for console input and output, and failure, and implemented new code for the call frame of the virtual recency abstraction and the heap.

Fixpoint Algorithm.

Our relational Tip analysis reuses a fixpoint algorithm for big-step abstract interpreters that handles both loops and recursive functions [34], demonstrating that the reused fixpoint algorithm also works for relational abstractions. The vast majority of the fixpoint algorithm is language-independent and reused (717 loc). A smaller part is Tip-specific and reused (73 loc) and only 9 loc are not reused.

5.2 Relational Analysis for WebAssembly

WebAssembly (Wasm for short) is a modern low-level language for the web that is supported in all major browsers. The language is statically-typed, features an operand stack, a byte-addressable heap, global and local variables, function tables, modules, structured control-flow and exceptions. Furthermore, Wasm 1.0 has four types of values: 32 and 64-bit integers and 32 and 64-bit IEEE754 floating-point numbers.

We developed a relational analysis for Wasm 1.0 in the Sturdy framework by reusing code of an existing non-relational analysis for Wasm [8] and code developed for the relational Tip analysis. Figure 3 reports what analysis functionality was reused and what new code needed to be implemented. Our relational analysis demonstrates that the Wasm generic interpreter and many existing non-relational analysis modules could be soundly reused as-is. Our evaluation shows that 82.2% of the code of the analysis is either reused as-is or new code that is language-independent.

Figure 3: Reuse and language-independence of existing functionality in our relational Wasm analysis. Percentages are computed with respect to the total 11389 loc of the relational Wasm analysis.

Value Components.

Wasm has values of four types: 32 and 64-bit integers and 32 and 64-bit IEEE 754 floating point numbers. Our Wasm analysis uses the following abstraction for values:

𝖵𝖺𝗅^=𝖨𝟥𝟤𝖵𝖺𝗅(𝖵𝖨𝟥𝟤)𝖨𝟨𝟦𝖵𝖺𝗅(𝖵𝖨𝟨𝟦)𝖥𝟥𝟤𝖵𝖺𝗅(𝖵𝖥𝟥𝟤)𝖥𝟨𝟦𝖵𝖺𝗅(𝖵𝖥𝟨𝟦)
𝖵𝖨𝟥𝟤=𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖠𝖽𝖽𝗋^,𝖳𝗒𝗉𝖾]𝖱𝖾𝗅𝖡𝗈𝗈𝗅[𝖠𝖽𝖽𝗋^,𝖳𝗒𝗉𝖾]
𝖵𝖨𝟨𝟦=𝖵𝖥𝟥𝟤=𝖵𝖥𝟨𝟦=𝖱𝖾𝗅𝖤𝗑𝗉𝗋[𝖠𝖽𝖽𝗋^,𝖳𝗒𝗉𝖾]
𝖳𝗒𝗉𝖾=𝖨𝟥𝟤𝖳𝗒𝗉𝖾𝖨𝟨𝟦𝖳𝗒𝗉𝖾𝖥𝟥𝟤𝖳𝗒𝗉𝖾𝖥𝟨𝟦𝖳𝗒𝗉𝖾

The type includes information about the bit-width (32 and 64-bit) as well as how the number is represented in the relational abstract domain (integer, real). The type information is used when detecting integer overflow and floating-point special values.

The Wasm analysis reuses integer operations developed for the Tip analysis by generically lifting operations on 𝖱𝖾𝗅𝖤𝗑𝗉𝗋 to operations on Wasm’s 𝖵𝖺𝗅^. 𝖨𝟥𝟤𝖵𝖺𝗅 values can either be a numeric expression (𝖱𝖾𝗅𝖤𝗑𝗉𝗋) or a boolean expression (𝖱𝖾𝗅𝖡𝗈𝗈𝗅). This is because Wasm encodes boolean true as 32-bit integer 1 and false as 0. However, to represent the result of a boolean operator more precisely, our analysis returns a relational boolean expression. The conversion of a boolean expression to a numeric expression is only performed if absolutely necessary, for example, if a boolean expression is applied to a non-boolean numeric operator.

To analyze Wasm’s floating-point numbers, we developed language-independent operations that approximate the IEEE754 floating-point standard. The floating-point operations are similar to integer operations in that they first create relational expression and then check for floating-point special values that need to be added (,0.0,𝖭𝖺𝖭,).

Additionally, Wasm allows converting each of its four numeric types into another other. Conversions to integer types can be signed or unsigned. To analyze these numeric conversions, we implemented 12 conversion operations in 256 loc.

To summarize, our relational Wasm analysis reuses code for relational integer operations, but required new code for floating-point operations and numeric conversion operations.

Effect Components.

Wasm features local and global variables, an operand stack, a byte-addressable heap, function tables, jumps, exceptions, and failures. The Wasm analysis implements local variables by reusing the call frame of the virtual recency abstraction. The operand stack, function tables, jumps, exceptions, and failures are implemented by reusing existing analysis components of Sturdy.

To analyze global variables, we developed a new relational abstraction similar to the call frame that uses the 𝖱𝖾𝖼𝖾𝗇𝖼𝗒𝖲𝗍𝗈𝗋𝖾, extended so that local variables on the call frame can be referenced by their index within the frame, whereas global variables must be reference by their name.

To analyze the byte-addressable heap, we developed a new relational abstraction inspired by Balakrishnan’s abstraction for the memory of x86 executables [2]. Our byte-memory abstraction reads in information of the static memory layout generated by LLVM (if available), including information about global variables, stack, and heap. On stores and loads, the abstract byte-memory determines for the given numeric address to which abstract memory location the address belongs. In case the numeric address cannot be resolved to a unqiue address, the address is assigned to dynamic heap contexts, which may overlap other statically-known memory regions.

To enable relations that span Wasm local variables, global variables, operand stack positions, and byte memory locations, we implemented the following context type for virtual and physical:
𝖠𝖽𝖽𝗋^=𝖵𝗂𝗋𝗍𝖠𝖽𝖽𝗋[𝖢𝗍𝗑] 𝖢𝗍𝗑=𝖫𝗈𝖼𝖺𝗅(𝑐𝑎𝑙𝑙𝐹𝑟𝑎𝑚𝑒𝐼𝑑𝑥:𝖨𝗇𝗍,𝑓𝑢𝑛:𝖥𝗎𝗇𝖼𝗍𝗂𝗈𝗇)𝖦𝗅𝗈𝖻𝖺𝗅(𝑎𝑑𝑑𝑟:𝖨𝗇𝗍)𝖲𝗍𝖺𝖼𝗄(𝑠𝑡𝑎𝑐𝑘𝑃𝑜𝑠:𝖨𝗇𝗍,𝑝𝑟𝑜𝑔𝑟𝑎𝑚𝑃𝑜𝑠:𝖫𝖺𝖻𝖾𝗅,𝑓𝑢𝑛:𝖥𝗎𝗇𝖼𝗍𝗂𝗈𝗇)𝖡𝗒𝗍𝖾𝖬𝖾𝗆𝗈𝗋𝗒(𝑚𝑒𝑚𝑜𝑟𝑦𝐴𝑑𝑑𝑟:𝖬𝖾𝗆𝗈𝗋𝗒𝖠𝖽𝖽𝗋)𝖮𝗍𝗁𝖾𝗋(𝑝𝑟𝑜𝑔𝑟𝑎𝑚𝑃𝑜𝑠:𝖫𝖺𝖻𝖾𝗅,𝑡𝑦𝑝𝑒:𝖳𝗒𝗉𝖾)

While relational variables based on 𝖫𝗈𝖼𝖺𝗅 and 𝖦𝗅𝗈𝖻𝖺𝗅 contexts are proactively allocated and added to the relational abstract domain, the other contexts are only allocated if necessary in joins of relational expressions. For example, context 𝖲𝗍𝖺𝖼𝗄 is only allocated at a join point where the joined stacks contain different relational expressions at the same position that cannot be joined any other way. In all other places relational variables based on context 𝖮𝗍𝗁𝖾𝗋 are allocated, for example, for when joining expressions in the analysis of a non-linear integer expression like 𝗆𝖺𝗑.

To summarize, our relational Wasm analysis reuses effect components for local variables, the operand stack, function tables, jumps, exceptions, and failures. For better precision, we implemented new effect components for global variables and the byte-addressable heap.

6 Evaluation

In this section, we assess the praticality of the virtual recency abstraction by empirically evaluating our relational analysis for WebAssembly that uses it. In this evaluation, we answer the following research questions about our relational Wasm analysis.

  • RQ1: Does the relational Wasm analysis yield sound analysis results?

  • RQ2: Does the relational Wasm analysis scale to realistic programs?

  • RQ3: What is the precision of the relational Wasm analysis?

6.1 Soundness of the Relational Wasm Analysis

While we already proved the soundness of our approach in Section 4, we here empirically evaluate the soundness of our relational Wasm analysis to rule out implementation bugs. To this end, we measure the recall on the Wasm 1.0 specification test-suite.333https://github.com/WebAssembly/spec/tree/main/test/core The test-suite is typically used to evaluate the conformance of concrete Wasm interpreters and tests all primitive Wasm instructions with all of their edge-cases. The test-suite consists of 71 individual test files, from which we execute 16517 assert_return and assert_trap assertions (we skipped parser tests, type checker tests, etc). For each assertion, we check if the analysis result soundly overapproximates the concrete interpretation result.

Table 1: Recall of our Wasm analysis measured on the Wasm 1.0 specification test-suite.
Category Assertions Polyhedra Octagons Intervals
Control Flow 832 100.00% 40.26% 100.00%
Conversions 593 100.00% 100.00% 100.00%
Floats 12197 100.00% 100.00% 100.00%
Functions 339 100.00% 50.74% 100.00%
Global Vars. 48 100.00% 0.00% 100.00%
Integers 909 100.00% 100.00% 100.00%
Local Vars. 93 100.00% 56.99% 100.00%
Memory 757 100.00% 93.13% 100.00%
Modules 606 100.00% 83.99% 100.00%
Stack 143 100.00% 34.27% 100.00%
Total 16517 100.00% 94.01% 100%

We repeated the experiment for Apron’s implementation of Polyhedra [15] (Polka), Octagons [40] (Octagon) and Intervals [11] (Box). The results are shown in Table 1. For Polyhedra and Octagons, there are 4 assertions in float_exprs.wast that run out of memory, which we count as a top analysis result, which is sound. For Octagons, 989 (6.0%) of assertions were approximated unsoundly. After investigating the issue, we found that all unsound approximations stem from the same Wasm code that is shared between the tests. The code assigns values to multiple local floating-point variables at once, which triggers a bug in the underlying Apron library. We have reported this bug to the Apron developers. This bug did not occur in any of our other experiments, which use different Wasm programs.

RQ1: The relational Wasm analyses yield sound results for Polyhedra and Intervals, but revealed a soundness bug in Apron for Octagons. We conclude that our approach enables sound relational analyses for Wasm.

6.2 Scalability of the Relational Wasm Analysis

The scalability of relational analyses is a general concern due to the higher computational complexity of relational abstract domains. We inherit this complexity in our relational Wasm analysis, yet want to demonstrate that it still scales to realistic Wasm programs. To this end, we evaluate the running time of the relational Wasm analysis on 7 compiled C programs from the benchmarks game suite[20]. From the benchmark suite of 9 programs, we excluded k-nucleotide and pidigits because these contain arrays of structs, which our Wasm analysis does not currently support. The 7 remaining programs have 546 lines Wat code on average (Figure 4).

We compiled the C programs with clang-19 --target=wasm32 -O3 -g to Wasm and linked our own C standard library stub for which we implemented summaries within our analysis. We ran the experiment on Ubuntu 24.04 with OpenJDK 21.0.9 (100GB heap space, 1GB stack space) on a machine with an AMD Ryzen Threadripper PRO 5975WX CPU. For each analyzed program, we warmed up the JVM with 3 runs of the analysis and then collected the running times over 5 runs with 3 seconds breaks for garbage collection.

Refer to caption
Refer to caption
Figure 4: Lines of Wat code of analyzed programs measured with cloc (left) and average running times of the relational Wasm analysis per program (right).

The average running time per program are shown in Figure 4. As to be expected, Polyhedra are slowest as their worst-case execution time and space complexity is exponential [15]. Octagons are on average 7x faster and Intervals 11.6x faster than Polyhedra. The execution time of the relational analyses is heavily influenced by the number of local variables in the code, which in turn depends on the compiler optimization level: at higher optimization levels, the compiler moves more program data from the bytememory to local variables. For example, mandelbrot has 49 local variables and nbody has 67 local variables. Since the number of variables affects the number of dimensions of the relational abstract domain, these programs have the highest analysis times.

RQ2: The relational Wasm analyses scale to realistic Wasm programs, compiled from C source code.

6.3 Precision of the Relational Wasm Analysis

To enable the relational analysis of recursive functions, we introduced the virtual recency abstraction, which uses a single relational variable 𝚡𝗈𝗅𝖽 for all previous values of a variable 𝚡. This can lead to precision loss compared to a non-relational analysis. For example, consider the following code that increments variable x three times:

(func $plus_three (local $x i32)
(local.set $x (call $itv (i32.const 0) (i32.const 10))) ;; 𝚡𝗋𝖾𝖼𝖾𝗇𝗍[0,10]
(local.set $x (i32.add (local.get $x) (i32.const 1))) ;; 𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽+1,𝚡𝗈𝗅𝖽[0,10]
(local.set $x (i32.add (local.get $x) (i32.const 1))) ;; 𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽+1,𝚡𝗈𝗅𝖽[0,11]
(local.set $x (i32.add (local.get $x) (i32.const 1))) ;; 𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽+1,𝚡𝗈𝗅𝖽[0,12]
(call $assert (i32.le_s (i32.const 3) (local.get $x)))) ;; Fails because 𝚡𝗋𝖾𝖼𝖾𝗇𝗍[0,13]

A non-relational interval analysis assigns [0,10]+[1,1]+[1,1]+[1,1]=[3,13] to the final value of x and is able to prove both assertions. In contrast, the virtual recency abstraction assigns 𝚡𝗋𝖾𝖼𝖾𝗇𝗍=𝚡𝗈𝗅𝖽+1 with 𝚡𝗈𝗅𝖽[0,12], which fails the first assertion. This begs the question: Is the precision loss of the virtual recency abstraction so severe that it negates the precision advantage of relational abstract domains?

To answer this question, we measure the precision of our relational Wasm analysis in Sturdy and compare it to state-of-the-art C analyzers Goblint 2.7.1 [52] and Mopsa 1.1 [32]. For this experiment, we created a benchmark of 35 precision tests, which we ported to Wasm and C to be able to compare the analysis results. Of the 35 tests, 9 tests were taken from existing literature [41, 42, 39, 17] and 26 tests we implemented ourselves. The benchmark consists of 13 tests of common mathematical operations (linear arithmetic, maximum, absolut value, sine, etc.), 11 tests with variable reassignment of which 8 tests include loops, and 11 tests of common recursive functions (recursive identity, factorial, fibonacci, gaussian sum, even odd). Each test, like plus_three shown above, consists of a sequence of instructions with one or more assertions of numerical properties. The complete code of the benchmarks can be found in the supplementary material.

Table 2: Precision advantage of relational abstract domains within the virtual recency abstraction.
Sturdy (Wasm) Goblint (C) Mopsa (C)
Benchmark Tests Poly. Oct. Itv. Poly. Oct. Itv. Poly. Oct. Itv.
Literature 9 44% 33% 0% 67% 67% 11% 56% 33% 0%
Own Non-Rec. 16 88% 81% 31% 81% 75% 38% 63% 56% 19%
Own Recursive 10 40% 60% 60% 20% 30% 20% 0% 0% 0%
Total 35 63% 63% 31% 60% 60% 26% 43% 34% 9%

We executed Goblint444goblint --conf=examples/very_precise.json --set ana.context.widen true (Polyhedra, Octagons, Intervals) and MOPSA555mopsa-c -config=c/cell-pack-rel-itv.json (Polyhedra, Octagons) and mopsa-c -config=c/cell-itv.json (Intervals) with precise analysis configuration options and checked the console output if assertions could not be proven. Furthermore, we disabled sound overflow wrap-around in our Wasm analysis for better comparability to C’s signed integers, for which overflow is undefined behavior. The results of the experiment are shown in Table 2. While the virtual recency abstraction looses some precision, especially on the tests taken from literature, the relational abstractions polyhedra and octagons retain their precision advantage over non-relational intervals. Furthermore, on the benchmark the virtual recency abstraction analyzed recursive functions more precisely in contrast to Goblint which analyzes recursive function partially context-sensitively [21] and MOPSA which finitely inlines recursive functions.

RQ3: While we may loose some precision due to the virtual recency abstraction, our relational Wasm analyses were able to analyze recursive programs and have a precision advantage over a non-relational interval analyses.

7 Related Work

Recency Abstraction.

Balakrishnan and Reps [3] introduced the recency abstraction, which allows precise analyses of object initialized in loops by discriminating the last allocated object (called “recent”) from others (called “old”). This is a highly popular allocation abstraction which has been extensively used and studied [30, 37, 46, 44]. However, whenever a new object is allocated, this abstraction needs to retire the previously recent object with the other old ones. We have shown in Section 2.3 that this retirement operation is non-modular: every abstract domain needs to support this retirement operation, breaking standard interfaces. One of the core contributions of this paper is to introduce a virtual recency abstraction, which is modular thanks to a new layer of virtual addresses, drawing from operating system design [19]. Additionally, we rely on the virtual recency abstraction to allocate variables: our analysis can refer to the values of a program variable at a previous program point, or in a different callstack. An in-depth study of variations in allocation strategies and their effect on performance and precision of the analysis is left as future work.

Relational analysis of languages without an operand stack (C, Python, …).

The analysis frameworks Astrée [14], Verasco [31], Mopsa [32], and Goblint [52, 47] implement relational analyses for C and other languages without an operand stack. Relational analyses for such languages can, under certain restrictions, strongly update local variables without running into a soundness problem. Strong updates to local variables is only sound if nothing in these analyses refers to an old state of local variables and thus updating local variables cannot be observed elsewhere. The exception to this are recursive functions where a strong update to a local variable can change the value in a recursive parent call frame. To solve this problem, fully context-sensitive analyses inline recursive calls up to a certain depth and tag variables in different call frames with the call string. This way each recursive call frame has its own fresh set of variables and strong updates never affect local variables in parent call frames. However, adding new variables to a relational abstract domain can be costly, as for example the time and memory complexity for Polyhedra grows exponentially with the number of variables. Furthermore, full context-sensitivity only works for recursive functions that can be fully explored by recursive unrolling up to a certain depth, but fails for example to analyze the recursive factorial function 𝖿𝖺𝖼𝗍(n) for 0n, which would require infinite unrolling. More recent work extends Goblint to handle recursive functions partially context-sensitively by annotating local variables with a context [21]. However, this can create scalability problems if too many contexts are distinguished.

In comparison, the virtual recency abstraction analyzes recursive functions while reusing the same set of variables. Specifically, variable updates in a recursive call retire virtual addresses in a parent call frame, ensuring that their value does not change non-monotonically. Furthermore, our relational analyses explore recursive functions fully by widening the analysis state at recursive calls with the state at parent calls. This ensures that every recursive call chain either terminates naturally or has a recurrent recursive call. In case of a recurrent recursive call, the fixpoint algorithm avoids recursing deeper by iterating between both occurences of the recurrent recursive call [34].

Relational analyses for languages with an operand stack (assembly, bytecode, …).

Many relational analyses for languages with an operand stack circumvent the problem of unsound strong updates by first translating to an intermediate language without a stack. Fähndrich and Logozzo [22] describe a relational analysis for .NET’s bytecode representation CIL. They eliminate the operand stack before the analysis is executed, but they do not describe in detail what language they translate CIL programs into. Ballabriga et al. [4] developed a relational analysis for ARM assembly. As the first step ARM’s stack is eliminated by translating it to a three-address code called MEMP, which is then analyzed. A problem is that the translation to three-address code introduces new variables for values that previously where stored on the stack. Their analysis adds all these variables to the relational abstract domain, which increases its size and could lead to scalibility problems. Bau et al. [5] analyze a stack-based smart contract language called Michelson. Instead of translating Michelson to an intermediate language without a stack, they analyze Michelson directly. Specifically, they introduce new variables into the relational abstract domain that represent a value at a positions on the stack at a position in the program. This solves the problem of unsound strong updates, but at the cost of introducing many new variables into the relational abstract domain.

In comparison, our relational analysis for Wasm analyzes the language directly without a preprocessing step. Our analysis approximates the operand stack with a list of abstract values, which are symbolic expressions over local variables. This avoids introducing new variables into the relational abstract domain for each stack position and program location. New variables are only introduced if necessary at control-flow joins in the program when the two stacks contains different symbolic expressions that cannot be joined any other way. Furthermore, our Wasm analysis solves the problem of unsound strong updates by using the virtual recency abstraction. The virtual recency abstraction ensures that symbolic expressions on the operand stack do not change its value non-monotonically in case of strong updates.

x = rand(0,10);
x = x + 1;
x = x + 1;
x = x + 1;
(a) Program.
 
x1 = rand(0,10);
x2 = x1 + 1;
x3 = x2 + 1;
x4 = x3 + 1;
(b) SSA translation.
 
SSA:[𝚡1[0,10],𝚡2=𝚡1+1,𝚡3=𝚡2+1]
Virtual Recency Abstraction:
[𝚡{x4}],[𝚡4{𝚡𝗋𝖾𝖼𝖾𝗇𝗍},𝚡3,2,1{𝚡𝗈𝗅𝖽}],
[0𝚡𝗈𝗅𝖽12,x𝗋𝖾𝖼𝖾𝗇𝗍=x𝗈𝗅𝖽+1]
(c) Analysis states at the end of the program.
Figure 5: Comparison between relational analyses for SSA languages and the virtual recency abstraction.

Relational analysis of languages with static single assignment (LLVM, …).

Languages with static single assignment (SSA) assign variables only once [16]. SSA partly solves the unsoundness problem of strong updates for relational analyses, because variables are never reassigned and thus their value does not change non-monotonically during analysis. For example, the SeaHorn verification framework [24, 9] implements an abstract interpreter Crab that analyzes CrabIR, an intermediate representation for LLVM [25]. However, SSA still does not solve the problem for recursive functions because recursive calls reuse the same set of local variables. Furthermore, the translation from a high-level language to SSA creates many new variables, that when added to the relational abstract domain, blow up its size and could cause scalibility issues.

Figure 5 compares relational analyses for an SSA language to the virtual recency abstraction at the example of a small C program. The program translated to SSA uses a fresh variable for each reassignment of x. The relational analysis of the SSA program adds each variable to the relational abstract domain and thus remembers each intermediate value of x. The interval of 𝚡4 after the last assignment is [3,13]. In comparison, the virtual recency abstraction retires recent virtual addresses of x on each reassignment. This is less precise than the SSA analysis: the interval of 𝚡𝗋𝖾𝖼𝖾𝗇𝗍 after the last assignment is [1,13]. However, the virtual recency abstraction only requires 2 relational variables instead of 4 variables. In the future, we want to investigate the precision and performance tradeoff of the virtual recency abstraction more thoroughly.

Modular implementation of static analyses.

Prior works have modularized different aspects of static analyses:

Generic Interpreters

A generic interpreter captures the semantics of the analyzed language without referring to analysis-specific details [50, 35, 6, 38, 31]. The generic interpreter can be instantiated to derive both concrete and abstract interpreters.

Reusable Analysis Modules

Analyses are implemented modularly by reusing existing language-independent analysis components [29, 18, 33, 32, 27, 28, 45].

Abstract Domain Combinators

Abstract domains can be modularly composed with domain combinators to improve their precision, e.g., reduced products [13] or communication channels [14, 31].

Fixpoint modularity

Fixpoint algorithms can be modularized by different iteration strategies [7], by composing them from language-independent fixpoint combinators [34], or by relying on an external solver [49].

Our relational analyses in Section 5 combine all these types of modularization to achieve a high level of reuse and improve the maintainability of the analyses. To this end, we build the analyses within the Sturdy framework [35, 33, 34]. So far, Sturdy only supported non-relational numerical abstract domains and we needed to implement infrastructure to support relational abstract domains. Furthermore, our analyses reuse existing generic interpreters for Tip and Wasm [8] that up until were only instantiated with non-relational abstract domains. Finally, our analyses reuse existing language-specific fixpoint algorithms that have been modularly composed of language-independent fixpoint combinators [34].

8 Conclusion

This work shows how to integrate relational abstract domain within interpreter modular analyses platforms. We tackled the issue of shared state between abstract domains through the introduction of the virtual recency abstraction. We have shown our approach to be sound and terminating. We have implemented our approach within an existing modular framework without needing to change interfaces. This approach has permitted us to develop the first relational analysis for WebAssembly. In our evaluation, we find that the Wasm has a high recall of 100% for Polyhedra, 94% for Octagons, and 100% for Intervals. Furthermore, in our performance experiments, the Wasm analysis has average running times of 22s for Polyhedra, 13s for Octagons, and 1.3s for Intervals. Lastly, on the precision tests our Wasm analysis achieves 63% precision for Polyhedra, 57% for Octagons, and 29% for Intervals. These expriments demonstrate the practical applicability of the virtual recency abstraction.

References

  • [1] Cyrille Artho and Armin Biere. Applying static analysis to large-scale, multi-threaded java programs. In 13th Australian Software Engineering Conference (ASWEC 2001), 26-28 August 2001, Canberra, Australia, pages 68–75. IEEE Computer Society, 2001. doi:10.1109/ASWEC.2001.948499.
  • [2] Gogul Balakrishnan and Thomas W. Reps. Analyzing memory accesses in x86 executables. In Evelyn Duesterwald, editor, Compiler Construction, 13th International Conference, CC 2004, Held as Part of the Joint European Conferences on Theory and Practice of Software, ETAPS 2004, Barcelona, Spain, March 29 - April 2, 2004, Proceedings, volume 2985 of Lecture Notes in Computer Science, pages 5–23. Springer, 2004. doi:10.1007/978-3-540-24723-4_2.
  • [3] Gogul Balakrishnan and Thomas W. Reps. Recency-abstraction for heap-allocated storage. In Kwangkeun Yi, editor, Static Analysis, 13th International Symposium, SAS 2006, Seoul, Korea, August 29-31, 2006, Proceedings, volume 4134 of Lecture Notes in Computer Science, pages 221–239. Springer, 2006. doi:10.1007/11823230_15.
  • [4] Clément Ballabriga, Julien Forget, Laure Gonnord, Giuseppe Lipari, and Jordy Ruiz. Static analysis of binary code with memory indirections using polyhedra. In Constantin Enea and Ruzica Piskac, editors, Verification, Model Checking, and Abstract Interpretation - 20th International Conference, VMCAI 2019, Cascais, Portugal, January 13-15, 2019, Proceedings, volume 11388 of Lecture Notes in Computer Science, pages 114–135. Springer, 2019. doi:10.1007/978-3-030-11245-5_6.
  • [5] Guillaume Bau, Antoine Miné, Vincent Botbol, and Mehdi Bouaziz. Abstract interpretation of michelson smart-contracts. In Laure Gonnord and Laura Titolo, editors, SOAP ’22: 11th ACM SIGPLAN International Workshop on the State Of the Art in Program Analysis, San Diego, CA, USA, 14 June 2022, pages 36–43. ACM, 2022. doi:10.1145/3520313.3534660.
  • [6] Martin Bodin, Philippa Gardner, Thomas P. Jensen, and Alan Schmitt. Skeletal semantics and their interpretations. Proc. ACM Program. Lang., 3(POPL):44:1–44:31, 2019. doi:10.1145/3290357.
  • [7] François Bourdoncle. Efficient chaotic iteration strategies with widenings. In Dines Bjørner, Manfred Broy, and Igor V. Pottosin, editors, Formal Methods in Programming and Their Applications, International Conference, Akademgorodok, Novosibirsk, Russia, June 28 - July 2, 1993, Proceedings, volume 735 of Lecture Notes in Computer Science, pages 128–141. Springer, 1993. doi:10.1007/BFB0039704.
  • [8] Katharina Brandl, Sebastian Erdweg, Sven Keidel, and Nils Hansen. Modular abstract definitional interpreters for webassembly. In Karim Ali and Guido Salvaneschi, editors, 37th European Conference on Object-Oriented Programming, ECOOP 2023, July 17-21, 2023, Seattle, Washington, United States, volume 263 of LIPIcs, pages 5:1–5:28. Schloss Dagstuhl – Leibniz-Zentrum für Informatik, 2023. doi:10.4230/LIPIcs.ECOOP.2023.5.
  • [9] Guillaume Brat, Jorge A. Navas, Nija Shi, and Arnaud Venet. IKOS: A framework for static analysis based on abstract interpretation. In Dimitra Giannakopoulou and Gwen Salaün, editors, Software Engineering and Formal Methods - 12th International Conference, SEFM 2014, Grenoble, France, September 1-5, 2014. Proceedings, volume 8702 of Lecture Notes in Computer Science, pages 271–277. Springer, 2014. doi:10.1007/978-3-319-10431-7_20.
  • [10] Patrick Cousot. Abstracting induction by extrapolation and interpolation. In Deepak D’Souza, Akash Lal, and Kim Guldstrand Larsen, editors, Verification, Model Checking, and Abstract Interpretation - 16th International Conference, VMCAI 2015, Mumbai, India, January 12-14, 2015. Proceedings, volume 8931 of Lecture Notes in Computer Science, pages 19–42. Springer, 2015. doi:10.1007/978-3-662-46081-8_2.
  • [11] Patrick Cousot and Radhia Cousot. Static determination of dynamic properties of programs. In Proceedings of the 2nd International Symposium on Programming, Paris, France, pages 106–130. Dunod, 1976.
  • [12] Patrick Cousot and Radhia Cousot. Abstract interpretation: A unified lattice model for static analysis of programs by construction or approximation of fixpoints. In Robert M. Graham, Michael A. Harrison, and Ravi Sethi, editors, Conference Record of the Fourth ACM Symposium on Principles of Programming Languages, Los Angeles, California, USA, January 1977, pages 238–252. ACM, 1977. doi:10.1145/512950.512973.
  • [13] Patrick Cousot and Radhia Cousot. Systematic design of program analysis frameworks. In Alfred V. Aho, Stephen N. Zilles, and Barry K. Rosen, editors, Conference Record of the Sixth Annual ACM Symposium on Principles of Programming Languages, San Antonio, Texas, USA, January 1979, pages 269–282. ACM Press, 1979. doi:10.1145/567752.567778.
  • [14] Patrick Cousot, Radhia Cousot, Jérôme Feret, Laurent Mauborgne, Antoine Miné, David Monniaux, and Xavier Rival. Combination of abstractions in the astrée static analyzer. In Mitsu Okada and Ichiro Satoh, editors, Advances in Computer Science - ASIAN 2006. Secure Software and Related Issues, 11th Asian Computing Science Conference, Tokyo, Japan, December 6-8, 2006, Revised Selected Papers, volume 4435 of Lecture Notes in Computer Science, pages 272–300. Springer, 2006. doi:10.1007/978-3-540-77505-8_23.
  • [15] Patrick Cousot and Nicolas Halbwachs. Automatic discovery of linear restraints among variables of a program. In Alfred V. Aho, Stephen N. Zilles, and Thomas G. Szymanski, editors, Conference Record of the Fifth Annual ACM Symposium on Principles of Programming Languages, Tucson, Arizona, USA, January 1978, pages 84–96. ACM Press, 1978. doi:10.1145/512760.512770.
  • [16] Ron Cytron, Jeanne Ferrante, Barry K. Rosen, Mark N. Wegman, and F. Kenneth Zadeck. Efficiently computing static single assignment form and the control dependence graph. ACM Trans. Program. Lang. Syst., 13(4):451–490, 1991. doi:10.1145/115372.115320.
  • [17] Matthias Dangl, Stefan Löwe, and Philipp Wendler. Cpachecker with support for recursive programs and floating-point arithmetic - (competition contribution). In Christel Baier and Cesare Tinelli, editors, Tools and Algorithms for the Construction and Analysis of Systems - 21st International Conference, TACAS 2015, Held as Part of the European Joint Conferences on Theory and Practice of Software, ETAPS 2015, London, UK, April 11-18, 2015. Proceedings, volume 9035 of Lecture Notes in Computer Science, pages 423–425. Springer, 2015. doi:10.1007/978-3-662-46681-0_34.
  • [18] David Darais, Matthew Might, and David Van Horn. Galois transformers and modular abstract interpreters: reusable metatheory for program analysis. In Jonathan Aldrich and Patrick Eugster, editors, Proceedings of the 2015 ACM SIGPLAN International Conference on Object-Oriented Programming, Systems, Languages, and Applications, OOPSLA 2015, part of SPLASH 2015, Pittsburgh, PA, USA, October 25-30, 2015, pages 552–571. ACM, 2015. doi:10.1145/2814270.2814308.
  • [19] Peter J. Denning. Virtual memory. ACM Comput. Surv., 2(3):153–189, 1970. doi:10.1145/356571.356573.
  • [20] Isaac Gouy Dough Bagley, Brent Fulgham. The computer language benchmarks game. https://benchmarksgame-team.pages.debian.net/benchmarksgame/, 2004–2026. Accessed: 2026-04-23.
  • [21] Julian Erhard, Johanna Franziska Schinabeck, Michael Schwarz, and Helmut Seidl. When to stop going down the rabbit hole: Taming context-sensitivity on the fly. In Raphaël Monat and Cindy Rubio-González, editors, Proceedings of the 13th ACM SIGPLAN International Workshop on the State Of the Art in Program Analysis, SOAP 2024, Copenhagen, Denmark, 25 June 2024, pages 35–44. ACM, 2024. doi:10.1145/3652588.3663321.
  • [22] Manuel Fähndrich and Francesco Logozzo. Static contract checking with abstract interpretation. In Bernhard Beckert and Claude Marché, editors, Formal Verification of Object-Oriented Software - International Conference, FoVeOOS 2010, Paris, France, June 28-30, 2010, Revised Selected Papers, volume 6528 of Lecture Notes in Computer Science, pages 10–30. Springer, 2010. doi:10.1007/978-3-642-18070-5_2.
  • [23] Denis Gopan, Frank DiMaio, Nurit Dor, Thomas W. Reps, and Shmuel Sagiv. Numeric domains with summarized dimensions. In Kurt Jensen and Andreas Podelski, editors, Tools and Algorithms for the Construction and Analysis of Systems, 10th International Conference, TACAS 2004, Held as Part of the Joint European Conferences on Theory and Practice of Software, ETAPS 2004, Barcelona, Spain, March 29 - April 2, 2004, Proceedings, volume 2988 of Lecture Notes in Computer Science, pages 512–529. Springer, 2004. doi:10.1007/978-3-540-24730-2_38.
  • [24] Arie Gurfinkel, Temesghen Kahsai, Anvesh Komuravelli, and Jorge A. Navas. The seahorn verification framework. In Daniel Kroening and Corina S. Pasareanu, editors, Computer Aided Verification - 27th International Conference, CAV 2015, San Francisco, CA, USA, July 18-24, 2015, Proceedings, Part I, volume 9206 of Lecture Notes in Computer Science, pages 343–361. Springer, 2015. doi:10.1007/978-3-319-21690-4_20.
  • [25] Arie Gurfinkel and Jorge A. Navas. Abstract interpretation of LLVM with a region-based memory model. In Roderick Bloem, Rayna Dimitrova, Chuchu Fan, and Natasha Sharygina, editors, Software Verification - 13th International Conference, VSTTE 2021, New Haven, CT, USA, October 18-19, 2021, and 14th International Workshop, NSV 2021, Los Angeles, CA, USA, July 18-19, 2021, Revised Selected Papers, volume 13124 of Lecture Notes in Computer Science, pages 122–144. Springer, 2021. doi:10.1007/978-3-030-95561-8_8.
  • [26] Andreas Haas, Andreas Rossberg, Derek L. Schuff, Ben L. Titzer, Michael Holman, Dan Gohman, Luke Wagner, Alon Zakai, and J. F. Bastien. Bringing the web up to speed with webassembly. In Albert Cohen and Martin T. Vechev, editors, Proceedings of the 38th ACM SIGPLAN Conference on Programming Language Design and Implementation, PLDI 2017, Barcelona, Spain, June 18-23, 2017, pages 185–200. ACM, 2017. doi:10.1145/3062341.3062363.
  • [27] Dominik Helm, Florian Kübler, Michael Reif, Michael Eichberg, and Mira Mezini. Modular collaborative program analysis in OPAL. In Prem Devanbu, Myra B. Cohen, and Thomas Zimmermann, editors, ESEC/FSE ’20: 28th ACM Joint European Software Engineering Conference and Symposium on the Foundations of Software Engineering, Virtual Event, USA, November 8-13, 2020, pages 184–196. ACM, 2020. doi:10.1145/3368089.3409765.
  • [28] Dominik Helm, Tobias Roth, Sven Keidel, Michael Reif, and Mira Mezini. Unimocg: Modular call-graph algorithms for consistent handling of language features. In Maria Christakis and Michael Pradel, editors, Proceedings of the 33rd ACM SIGSOFT International Symposium on Software Testing and Analysis, ISSTA 2024, Vienna, Austria, September 16-20, 2024, pages 51–62. ACM, 2024. doi:10.1145/3650212.3652109.
  • [29] Bertrand Jeannet and Antoine Miné. Apron: A library of numerical abstract domains for static analysis. In Ahmed Bouajjani and Oded Maler, editors, Computer Aided Verification, 21st International Conference, CAV 2009, Grenoble, France, June 26 - July 2, 2009. Proceedings, volume 5643 of Lecture Notes in Computer Science, pages 661–667. Springer, 2009. doi:10.1007/978-3-642-02658-4_52.
  • [30] Simon Holm Jensen, Anders Møller, and Peter Thiemann. Type analysis for javascript. In Jens Palsberg and Zhendong Su, editors, Static Analysis, 16th International Symposium, SAS 2009, Los Angeles, CA, USA, August 9-11, 2009. Proceedings, volume 5673 of Lecture Notes in Computer Science, pages 238–255. Springer, 2009. doi:10.1007/978-3-642-03237-0_17.
  • [31] Jacques-Henri Jourdan, Vincent Laporte, Sandrine Blazy, Xavier Leroy, and David Pichardie. A formally-verified C static analyzer. In Sriram K. Rajamani and David Walker, editors, Proceedings of the 42nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL 2015, Mumbai, India, January 15-17, 2015, pages 247–259. ACM, 2015. doi:10.1145/2676726.2676966.
  • [32] Matthieu Journault, Antoine Miné, Raphaël Monat, and Abdelraouf Ouadjaout. Combinations of reusable abstract domains for a multilingual static analyzer. In Supratik Chakraborty and Jorge A. Navas, editors, Verified Software. Theories, Tools, and Experiments - 11th International Conference, VSTTE 2019, New York City, NY, USA, July 13-14, 2019, Revised Selected Papers, volume 12031 of Lecture Notes in Computer Science, pages 1–18. Springer, 2019. doi:10.1007/978-3-030-41600-3_1.
  • [33] Sven Keidel and Sebastian Erdweg. Sound and reusable components for abstract interpretation. Proc. ACM Program. Lang., 3(OOPSLA):176:1–176:28, 2019. doi:10.1145/3360602.
  • [34] Sven Keidel, Sebastian Erdweg, and Tobias Hombücher. Combinator-based fixpoint algorithms for big-step abstract interpreters. Proc. ACM Program. Lang., 7(ICFP):955–981, 2023. doi:10.1145/3607863.
  • [35] Sven Keidel, Casper Bach Poulsen, and Sebastian Erdweg. Compositional soundness proofs of abstract interpreters. Proc. ACM Program. Lang., 2(ICFP):72:1–72:26, 2018. doi:10.1145/3236767.
  • [36] Davy Landman, Alexander Serebrenik, and Jurgen J. Vinju. Challenges for static analysis of java reflection: literature review and empirical study. In Sebastián Uchitel, Alessandro Orso, and Martin P. Robillard, editors, Proceedings of the 39th International Conference on Software Engineering, ICSE 2017, Buenos Aires, Argentina, May 20-28, 2017, pages 507–518. IEEE / ACM, 2017. doi:10.1109/ICSE.2017.53.
  • [37] Percy Liang, Omer Tripp, Mayur Naik, and Mooly Sagiv. A dynamic evaluation of the precision of static heap abstractions. In William R. Cook, Siobhán Clarke, and Martin C. Rinard, editors, Proceedings of the 25th Annual ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, OOPSLA 2010, October 17-21, 2010, Reno/Tahoe, Nevada, USA, pages 411–427. ACM, 2010. doi:10.1145/1869459.1869494.
  • [38] Sébastien Michelland, Yannick Zakowski, and Laure Gonnord. Abstract interpreters: A monadic approach to modular verification. Proc. ACM Program. Lang., 8(ICFP):602–629, 2024. doi:10.1145/3674646.
  • [39] Antoine Miné. Domaines numériques abstraits faiblement relationnels. PhD thesis, Palaiseau, Ecole polytechnique, 2004.
  • [40] Antoine Miné. The octagon abstract domain. High. Order Symb. Comput., 19(1):31–100, 2006. doi:10.1007/S10990-006-8609-1.
  • [41] Antoine Miné. Tutorial on static inference of numeric invariants by abstract interpretation. Found. Trends Program. Lang., 4(3-4):120–372, 2017. doi:10.1561/2500000034.
  • [42] Anders Møller and Michael I. Schwartzbach. Static program analysis, October 2018. Department of Computer Science, Aarhus University, http://cs.au.dk/˜amoeller/spa/.
  • [43] Raphaël Monat. Static type and value analysis by abstract interpretation of Python programs with native C libraries. (Analyse statique, de type et de valeur, par interprétation abstraite, de programmes Python utilisant des librairies C). PhD thesis, Sorbonne University, Paris, France, 2021. URL: https://tel.archives-ouvertes.fr/tel-03533030.
  • [44] Raphaël Monat, Abdelraouf Ouadjaout, and Antoine Miné. Value and allocation sensitivity in static python analyses. In Paddy Krishnan and Christoph Reichenbach, editors, Proceedings of the 9th ACM SIGPLAN International Workshop on the State Of the Art in Program Analysis, SOAP@PLDI 2020, London, UK, June 15, 2020, pages 8–13. ACM, 2020. doi:10.1145/3394451.3397205.
  • [45] Raphaël Monat, Abdelraouf Ouadjaout, and Antoine Miné. A multilanguage static analysis of python programs with native C extensions. In Cezara Dragoi, Suvam Mukherjee, and Kedar S. Namjoshi, editors, Static Analysis - 28th International Symposium, SAS 2021, Chicago, IL, USA, October 17-19, 2021, Proceedings, volume 12913 of Lecture Notes in Computer Science, pages 323–345. Springer, 2021. doi:10.1007/978-3-030-88806-0_16.
  • [46] Jihyeok Park, Xavier Rival, and Sukyoung Ryu. Revisiting recency abstraction for javascript: towards an intuitive, compositional, and efficient heap abstraction. In Karim Ali and Cristina Cifuentes, editors, Proceedings of the 6th ACM SIGPLAN International Workshop on State Of the Art in Program Analysis, SOAP@PLDI 2017, Barcelona, Spain, June 18, 2017, pages 1–6. ACM, 2017. doi:10.1145/3088515.3088516.
  • [47] Simmo Saan, Michael Schwarz, Kalmer Apinis, Julian Erhard, Helmut Seidl, Ralf Vogler, and Vesal Vojdani. Goblint: Thread-modular abstract interpretation using side-effecting constraints - (competition contribution). In Jan Friso Groote and Kim Guldstrand Larsen, editors, Tools and Algorithms for the Construction and Analysis of Systems - 27th International Conference, TACAS 2021, Held as Part of the European Joint Conferences on Theory and Practice of Software, ETAPS 2021, Luxembourg City, Luxembourg, March 27 - April 1, 2021, Proceedings, Part II, volume 12652 of Lecture Notes in Computer Science, pages 438–442. Springer, 2021. doi:10.1007/978-3-030-72013-1_28.
  • [48] Joanna C. S. Santos, Reese A. Jones, and Mehdi Mirakhorli. Salsa: static analysis of serialization features. In Wytse Oortwijn, editor, FTfJP 2020: Proceedings of the 22nd ACM SIGPLAN International Workshop on Formal Techniques for Java-Like Programs, Virtual Event, USA, July 23, 2020, pages 18–25. ACM, 2020. doi:10.1145/3427761.3428343.
  • [49] Helmut Seidl and Ralf Vogler. Three improvements to the top-down solver. Math. Struct. Comput. Sci., 31(9):1090–1134, 2021. doi:10.1017/S0960129521000499.
  • [50] Ilya Sergey, Dominique Devriese, Matthew Might, Jan Midtgaard, David Darais, Dave Clarke, and Frank Piessens. Monadic abstract interpreters. In Hans-Juergen Boehm and Cormac Flanagan, editors, ACM SIGPLAN Conference on Programming Language Design and Implementation, PLDI ’13, Seattle, WA, USA, June 16-19, 2013, pages 399–410. ACM, 2013. doi:10.1145/2491956.2491979.
  • [51] Olin Grigsby Shivers. Control-flow analysis of higher-order languages or taming lambda. Carnegie Mellon University, 1991.
  • [52] Vesal Vojdani, Kalmer Apinis, Vootele Rõtov, Helmut Seidl, Varmo Vene, and Ralf Vogler. Static race detection for device drivers: the goblint approach. In David Lo, Sven Apel, and Sarfraz Khurshid, editors, Proceedings of the 31st IEEE/ACM International Conference on Automated Software Engineering, ASE 2016, Singapore, September 3-7, 2016, pages 391–402. ACM, 2016. doi:10.1145/2970276.2970337.