Martin Mohr

Systematic Approaches to Advanced Information Flow Analysis

# Systematic Approaches to Advanced Information Flow Analysis –

and Applications to Software Security

Martin Mohr

Systematic Approaches to Advanced Information Flow Analysis – and Applications to Software Security

# Systematic Approaches to Advanced Information Flow Analysis – and Applications to Software Security

by Martin Mohr

Karlsruher Institut für Technologie Institut für Programmstrukturen und Datenorganisation (IPD)

Systematic Approaches to Advanced Information Flow Analysis – and Applications to Software Security

Zur Erlangung des akademischen Grades eines Doktor-Ingenieurs von der KIT-Fakultät für Informatik des Karlsruher Instituts für Technologie (KIT) genehmigte Dissertation

von Martin Mohr

Tag der mündlichen Prüfung: 6. Mai 2022 Erster Gutachter: Prof. Dr.-Ing. Gregor Snelting Zweiter Gutachter: Prof. Dr.-Ing. Christian Hammer

### **Impressum**

Karlsruher Institut für Technologie (KIT) KIT Scientific Publishing Straße am Forum 2 D-76131 Karlsruhe

KIT Scientific Publishing is a registered trademark of Karlsruhe Institute of Technology. Reprint using the book cover is not allowed.

www.ksp.kit.edu

*This document – excluding parts marked otherwise, the cover, pictures and graphs – is licensed under a Creative Commons Attribution-Share Alike 4.0 International License (CC BY-SA 4.0): https://creativecommons.org/licenses/by-sa/4.0/deed.en*

*The cover page is licensed under a Creative Commons Attribution-No Derivatives 4.0 International License (CC BY-ND 4.0): https://creativecommons.org/licenses/by-nd/4.0/deed.en*

Print on Demand 2023 – Gedruckt auf FSC-zertifiziertem Papier

ISBN 978-3-7315-1275-2 DOI 10.5445/KSP/1000155035

# **Contents**




*With a little help from my friends*. . . The Beatles

# **Danksagung**

Zunächst einmal möchte ich mich bei meinem Doktorvater und Erstgutachter Prof. Gregor Snelting dafür bedanken, dass er mir so viele Freiheiten bei meiner Forschung und der Ausarbeitung meiner Dissertation gelassen hat, aber auch für seine Unterstützung, sein Vertrauen und seine Geduld. Ich danke auch Prof. Christian Hammer für seine Bereitschaft, als Zweitgutachter dieser Arbeit tätig zu sein.

Ich danke der Deutschen Forschungsgemeinschaft, die diese Arbeit im Rahmen des Projektes "Zuverlässig sichere Software-Systeme" (SPP 1496, Sn11/12-1/2/3) teilweise finanziert hat. Ebenso möchte ich Prof. Heiko Mantel und den Mitarbeiterinnen und Mitarbeitern seines Lehrstuhls dafür danken, dass sie das Projekt so professionell organisiert und sich dafür eingesetzt haben, die Zusammenarbeit der verschiedenen, deutschlandweit verteilten Forschungsgruppen zu fördern. Ferner danke ich den Kolleginnen und Kollegen, mit denen ich im Rahmen des Projektes zusammenarbeiten durfte. Insbesondere danke ich Prof. Markus Müller-Olm und seinen Mitarbeitern Sebastian Kentner, Benedikt Nordhoff und Alexander Wenner für die gute Zusammenarbeit in unserem Teilprojekt "Information Flow Control for Mobile Components" (Sn11/12-1/2/3).

Danken möchte ich auch meinen ehemaligen Kollegen am Lehrstuhl von Prof. Snelting, namentlich meinen Programmanalytiker-Kollegen Jürgen Graf, Martin Hecker und Simon Bischof, sowie Joachim Breitner, Matthias Braun, Sebastian Buchwald, Andreas Fried, Sebastian Graf, Andreas Lochbihler, Denis Lohner, Manuel Mohr, Sebastian Ullrich, Maximilian Wagner und Andreas Zwinkau. Die insgesamt sieben Jahre, die ich mit ihnen zusammen arbeiten durfte, werden mir immer in guter Erinnerung bleiben. Ich erinnere mich gern an die vielen interessanten Fachdiskussionen, die gute und professionelle Zusammenarbeit, aber auch an unsere sonstigen gemeinsamen Aktivitäten wie Film- oder Spieleabende, Kinogänge, Programmier-, Doktorhut- und Musikprojekte. Allen eben genannten bin ich außerdem dafür dankbar, dass sie mit mir ihr umfangreiches Fachwissen über Programmiersprachen, Compilerbau, Theorembeweiser und Programmanalyse geteilt haben, sowie für ihre Unvoreingenommenheit, ihre positive Problemlösermentalität, ihren Humor und ihre Offenheit. Ich danke meinen Kollegen insbesondere auch dafür, dass sie mir Feedback und Korrekturvorschläge zu Teilen meiner Dissertation gegeben haben. Ich danke auch Brigitte Sehan-Hill und Andreas Ladanyi, die durch ihre Arbeit im Hintergrund dafür gesorgt haben, dass wir wissenschaftlichen Mitarbeiter uns auf Forschung und Lehre konzentrieren konnten. Auch möchte ich an dieser Stelle Martin Armbruster, Tobias Blaschke, Stephan Gocht und Maik Wiesner dankend erwähnen, die uns im Rahmen von Abschlussarbeiten und Hilfstätigkeiten bei der Weiterentwicklung von Joana unterstützt haben.

Dankbar bin ich auch meinen Eltern Dorothea Mohr-Andrich und Harald Mohr. Sie haben mich immer gefördert und unterstützt, mir Vertrauen geschenkt, mich bestärkt und die von mir eingeschlagenen Wege nie in Frage gestellt. Ohne ihre liebevolle Unterstützung hätte ich wohl nie eine Promotion in Informatik angestrebt. Meinen Geschwistern Katrin Wünsch, Stephan Mohr und Anneliese Mohr danke ich für ihren Rückhalt und ihre bedingungslose Unterstützung.

Besonderer Dank gilt meiner langjährigen Partnerin und Ehefrau Claudia Lienau-Mohr für ihre Liebe, ihr Verständnis und dafür, dass sie mit mir alle Höhen und Tiefen überstanden und mich immer wieder aufgebaut hat. Ohne ihren Einsatz und die Opfer, die sie bringen musste, hätte ich diese Arbeit nicht beenden können. Auch Claudias Eltern möchte ich dafür danken, dass sie mir durch ihren Einsatz die Zeit für die Beendigung meiner Dissertation geschenkt haben und dabei nie an mir gezweifelt haben.

Ich danke Dorothea Jansen, die mich fachlich und moralisch unterstützt hat. Trotz gut gefüllten Alltags fand sie nicht nur die Zeit, Teile meiner Arbeit Korrektur zu lesen und mir wertvolles Feedback zu geben, sondern war und ist auch immer eine Ansprechperson und eine sehr gute Freundin für mich.

Weiterer Dank gilt Johannes Bechberger, Jan Betzing, Susanne Eckhardt, Melanie Heßmer, Stephan Mohr, Friederike Morfeld, Joachim Morfeld, Sebastian Schneider, Martin Wilde, Daniel Wünsch und Katrin Wünsch, die mir ebenfalls Feedback und Korrekturvorschläge gegeben haben.

Münster, Oktober 2021 *Martin Mohr*

*With a little help from my friends*. . . The Beatles

# **Acknowledgments**

First of all, I would like to thank my advisor Prof. Gregor Snelting for giving me so much freedom in my research and in the preparation of my thesis, but also for his support, trust, and patience. I also thank Prof. Christian Hammer for his willingness to contribute the second review of this thesis. I thank the German Research Foundation, who partially funded this work within the scope of the project "Reliably Secure Software Systems" (RS<sup>3</sup> , SPP 1496). Particularly, I would like to thank Prof. Heiko Mantel and the staff of his chair for organizing the project in such a professional way and for their efforts to promote cooperation between the various research groups across Germany. I would also like to thank the colleagues with whom I have collaborated within RS<sup>3</sup> . In particular, I thank Prof. Markus Müller-Olm and the members of his research staff Sebastian Kentner, Benedikt Nordhoff and Alexander Wenner for the good collaboration in our sub-project "Information Flow Control for Mobile Components" (Sn11/12-1/2/3).

I thank my former colleagues at the chair of Prof. Snelting, namely the other program analysts Jürgen Graf, Martin Hecker, and Simon Bischof, as well as Joachim Breitner, Matthias Braun, Sebastian Buchwald, Andreas Fried, Sebastian Graf, Andreas Lochbihler, Denis Lohner, Manuel Mohr, Sebastian Ullrich, Maximilian Wagner and Andreas Zwinkau. I will always have fond memories of the seven years that I worked with them – particularly of the many interesting discussions, the good and professional cooperation, but also of our other joint activities such as movie and game nights, programming and music projects, as well as the numerous "doctoral hat construction sprints". I am also grateful to them for sharing their extensive knowledge of programming languages, compiler construction, theorem provers and program analysis with me as well as for their impartiality, positive problem-solving mentality, humor, and open-mindedness. I particularly thank those colleagues who where willing to provide feedback and suggest corrections to parts of my dissertation.

I also thank Brigitte Sehan-Hill and Andreas Ladanyi, whose organizational and technical work in the background made sure that the research staff members could concentrate on research and teaching, as well as Martin Armbruster, Tobias Blaschke, Stephan Gocht, and Maik Wiesner, who helped us in the context of their theses and other support activities in the further development of Joana.

I am grateful to my parents Dorothea Mohr-Andrich and Harald Mohr. They have always encouraged and supported me, gave me confidence, encouraged me and never questioned the paths I took. Without their loving support, I would probably never have pursued a doctorate in computer science. I thank my siblings Katrin Wünsch, Stephan Mohr and Anneliese Mohr for their backing and unconditional support.

Special thanks go to my longtime partner and wife Claudia Lienau-Mohr for her love, understanding and for the fact that she overcame all the ups and downs with me and built me up again and again. Without her commitment and the sacrifices she had to make, I would not have been able to finish this work. I would also like to thank Claudia's parents for giving me the time to finish my dissertation and for never doubting me.

I thank Dorothea Jansen, who supported me professionally and morally. Despite a fully packed everyday life, she not only found the time to proofread parts of my work and give me valuable feedback, but was and still is always a contact person and a very good friend for me.

Further thanks go to Johannes Bechberger, Jan Betzing, Susanne Eckhardt, Melanie Heßmer, Stephan Mohr, Friederike Morfeld, Joachim Morfeld, Sebastian Schneider, Martin Wilde, Daniel Wünsch and Katrin Wünsch, who provided me with feedback and suggestions for corrections.

Münster, October 2021 *Martin Mohr*

# **Zusammenfassung**

Bei der *statischen Programmanalyse* geht es darum, Eigenschaften von Programmen abzuleiten, ohne sie auszuführen. Zwei wichtige statische Programmanalysetechniken sind zum einen die *Datenflussanalyse* auf *Kontrollflussgraphen* und zum anderen *Slicing* auf *Programmabhängigkeitsgraphen* (PAG). In dieser Arbeit berichte ich über Anwendungen von Slicing und Programmabhängigkeitsgraphen in der Softwaresicherheit. Außerdem schlage ich ein Analyse-Rahmenwerk vor, welches Datenflussanalyse auf Kontrollflussgraphen und Slicing auf Programmabhängigkeitsgraphen verallgemeinert. Mit einem solchen Rahmenwerk lassen sich neue PAGbasierte Analysen systematisch ableiten, die über Slicing hinausgehen.

Eine wichtige Anwendung von PAG-basiertem Slicing liegt in der Softwaresicherheit, genauer in der *Informationsflusskontrolle*. Bei dieser geht es darum sicherzustellen, dass ein gegebenes Programm keine vertraulichen Informationen über öffentliche Kanäle preisgibt bzw. öffentliche Eingaben keine kritischen Berechnungen beeinflussen können. Der Lehrstuhl Programmierparadigmen am KIT entwickelt seit einiger Zeit Joana, ein Werkzeug zur Informationsflusskontrolle für Java, das unter anderem auf Programmabhängigkeitsgraphen und Slicing basiert. Von 2011 bis 2017 war der Lehrstuhl am Schwerpunktprogramm 1496 "Zuverlässig sichere Softwaresysteme" (engl. Reliably Secure Software Systems, RS<sup>3</sup> ) der Deutschen Forschungsgemeinschaft (DFG) beteiligt.

Im ersten Teil dieser Arbeit gebe ich einen Überblick über Beiträge des Lehrstuhls zu RS<sup>3</sup> , an denen ich beteiligt war. Diese Beiträge umfassen zum einen die Erweiterung eines mit PAG-basierten Techniken überprüfbaren Informationsflusskontrollkriteriums für nebenläufige Programme, und zum anderen eine Reihe von Anwendungen von Joana in der Softwaresicherheit.

Im zweiten Teil meiner Doktorarbeit schlage ich vor, Datenflussanalysen auf Kontrollflussgraphen und Slicing auf Programmabhängigkeitsgraphen zu einer gemeinsamen, verallgemeinerten Analysetechnik zu vereinheitlichen. Eine solche Vereinheitlichung ermöglicht beispielsweise neue Analysen auf PAGs, die über bestehende PAG-basierte Ansätze hinausgehen. Darüber hinaus können für die Instanzen der allgemeinen Analysetechnik bestimmte formale Garantien gegeben werden, welche die Korrektheitsargumente, wie sie u.a. für bestehende PAG-basierte Analysen gegeben wurden, vereinfachen.

Zunächst stelle ich ein allgemeines Graphmodell sowie ein allgemeines Analyse-Rahmenwerk vor und zeige, dass sich sowohl Datenflussanalyse auf Kontrollflussgraphen als auch Slicing auf Programmabhängigkeitsgraphen darin ausdrücken lassen.

Anschließend zeige ich, dass sich Instanzen des allgemeinen Analyse-Rahmenwerkes durch Ungleichungssysteme beschreiben lassen. Hierbei greife ich auf klassische Ansätze zurück und passe diese geeignet an.

Ich gebe außerdem Algorithmen zur Lösung der zuvor aufgestellten Ungleichungssysteme an. Diese kombinieren klassische Lösungsalgorithmen für Datenflussanalysen mit einer Erreichbarkeitsanalyse.

Schließlich beschreibe ich eine Instanziierung der allgemeinen Analysetechnik für Programmabhängigkeitsgraphen. Ich stelle eine Implementierung in Joana vor und evaluiere diese anhand von realen Programmabhängigkeitsgraphen und einiger Beispielanalysen.

Die Hauptthesen meiner Arbeit lauten wie folgt:

1. PAG-basierte Informationsflusskontrolle ist nützlich, praktisch anwendbar und relevant.

2. Datenflussanalyse kann systematisch auf Programmabhängigkeitsgraphen angewendet werden.

3. Datenflussanalyse auf Programmabhängigkeitsgraphen ist praktisch durchführbar.

# **Abstract**

*Static program analysis* is concerned with deriving properties of computer programs without executing them. Two important static program analysis techniques are *data-flow analysis on control-flow graphs* and *slicing on program dependence graphs (PDGs)*. In this thesis, I report on applications of slicing and program dependence graphs to software security. Moreover, I propose a framework that generalizes both data-flow analysis on control-flow graphs and slicing on program dependence graphs. Such a framework enables to systematically derive data-flow-like analyses on program dependence graphs that go beyond slicing.

One important application of PDG-based slicing lies in the field of software security, more specifically in *information flow control*. The goal of information flow control is to verify that a given program does not leak confidential information to public channels, or, respectively, that public input cannot influence critical computations.

The programming paradigms group at KIT develops Joana, a PDG-based information flow control tool for Java that employs program dependence graphs and slicing. From 2011 to 2017, our group participated in the priority program "Reliably Secure Software Systems" (RS<sup>3</sup> , SPP 1496) of the German Research Foundation (DFG).

In the first part of this dissertation I give an overview of the contributions of the programming paradigms group to RS<sup>3</sup> in which I participated. These contributions include, on the one hand, the extension of an information flow control criterion for concurrent programs that can be checked with PDG-based techniques, and, on the other hand, a number of applications of Joana in software security.

In the second part of my dissertation, I present a unification of data flow analysis on control flow graphs and slicing on program dependence graphs into a common, general analysis technique. Such a unification enables new analyses on PDGs that go beyond existing PDG-based approaches. In addition, for instances of the general analysis technique, certain formal guarantees can be given for instances of the general analysis technique, which simplify correctness proofs such as those given for existing PDGbased analyses.

First, I introduce a general graph model as well as a general analysis framework and show that data-flow analysis on control-flow graphs as well as slicing on program dependence graphs can be expressed in this framework.

Then, I show that instances of the general analysis framework can be described by monotone constraint systems. For this, I resort to traditional approaches and adapt them appropriately.

I also present algorithms for solving the constraint systems set up earlier. These algorithms combine traditional solution approaches for data flow analyses with a reachability analysis.

Finally, I describe an instantiation of the general analysis technique for program dependence graphs: I present an implementation in Joana and evaluate it using real programs and a selection of example analyses. In summary, the main theses of my dissertation are:

1. PDG-based information flow control is useful, practically applicable and relevant.

2. Data-flow analysis can be systematically applied to program dependence graphs.

3. Data-flow analysis on PDGs can be practically conducted.

*Dear Sir or Madam, will you read my book? It took me years to write, will you take a look?* <sup>T</sup>he <sup>B</sup>eatles **1**

# **Introduction**

*Program analysis* is a branch of computer science that is concerned with the derivation of a given computer program's properties. *Static program analysis* [130, 76], or static analysis for short, is a sub-branch of program analysis that considers techniques to derive properties of a given program without executing it.

This thesis is located within the field of static program analysis. More specifically, it considers *automatic* static program analysis techniques, i.e. static analyses that can execute without human interaction.

Two notable static analysis techniques, which are important to this thesis, are *data-flow analysis* [101] and *program slicing* [165, 166, 58]. The basic idea of data-flow analysis is to analyze how a given program transforms data along its executions. Slicing was originally developed to aid programmers in debugging. Its goal is, given a program *p*, to extract a sub-program of *p* that behaves equivalently to *p* with respect to a given observation. Such a sub-program is also called a *slice*.

Both data-flow analysis and program slicing can be conducted on a graph representation of the given program. Data-flow analysis typically uses the program's *control-flow graph* [12], whereas an established approach to program slicing employs the *program dependence graph* (PDG) [58, 93, 137]. These two graph representations concentrate on different aspects of a program.

The control-flow graph focuses on the actual executions of the program and how control is transferred between its statements. Its nodes can be thought of as the program's statements, while an edge between a statement *s*<sup>1</sup> and another statement *s*<sup>2</sup> means that *s*<sup>2</sup> may be executed directly after *s*1 . A program dependence graph on the other hand materializes the *dependencies* between the program's variables and statements. There are

**Figure 1.1:** A small code snippet **(a)** with its control-flow graph **(b)** and its program dependence graph **(c)** – node labels correspond to line numbers; in Figure 1.1c, data and control dependencies are represented by solid and dashed edges, respectively.

two major kinds of dependencies. A *data dependency* arises between two statements *s*<sup>1</sup> and *s*<sup>2</sup> if *s*<sup>1</sup> writes a value that *s*<sup>2</sup> reads. A *control dependency* describes that *s*<sup>1</sup> controls whether *s*<sup>2</sup> is executed or not. Figure 1.1 shows an example for control-flow graphs and program dependence graphs.

As has been shown by Ferrante et al. [58], PDGs can be used for program slicing: Giving a node *n* in a PDG *G*, called *slicing criterion*, the (PDG-based) backwards slice of *n* consists of all nodes from which *n* is reachable in *G*. Analogously, the forward slice of *n* consists of all nodes which are reachable from *n*.

Both control-flow graphs and program dependence graphs can also be used to represent programs with multiple procedures. These variants and the analyses that process them are called *interprocedural* [154, 93].

# **1.1 Applications to Software Security**

The first main part of this thesis is concerned with applications of static analysis techniques such as PDGs and slicing to software security. The goal of static analysis in software security (which I will call *static security analysis* in the following) is usually to analyze a given program or system with respect to some desirable security property.

In software security, there are three desirable classes of properties of a given system. *Confidentiality* means that no sensitive information is disclosed

```
1 int h = inputPIN(); // HIGH
2 print(h); // LOW
3 if (h > 4711) {
4 print("OK"); // LOW
5 } else {
6 print("FAIL") // LOW
7 }
8 print("A") // LOW
```
**Figure 1.2:** Simple examples for illegal information flows

to a public or untrusted channel, *integrity* describes the property that no untrusted input can influence critical computations and, finally, *availability* states that data is always accessible if necessary [17]. This thesis focuses on confidentiality and integrity, which both can be generically formulated as an *information-flow property*. Basically, such a property demands that *high input cannot influence low output*. In the following, I briefly explain this in more detail. Information-flow properties assume that a program contains *(information) sources* and *(information) sinks*. Sources are typically points in the program where data is imported from the outside, whereas sinks are points in the program where it emits output, e.g. where it exports data to the outside. An information flow between a source and a sink manifests itself if a change in the data that a source imports can lead to a change in the output that a sink generates. A typical information-flow property demands that the given program does not contain any illegal information flows. In order to distinguish between legal and illegal information flows, sources and sinks are usually labeled with some form of sensitivity level, in the simplest case *high* and *low*. An information flow is called *illegal* if it starts in a high source and ends in a low sink.

Simple examples for legal and illegal information flows are shown in Figure 1.2. On the one hand, this code snippet contains illegal information flows from the high source in line 1 to the low sinks in lines 2, 4, and 6, respectively. On the other hand, no information flows from line 1 to line 8. The class of static security analyses that are concerned with verifying information-flow properties is also called *information flow control* [53].

As Snelting et al. [157] noted, slicing can be used to perform static information flow control. The basic idea is as follows: A (backwards) slice of a program with respect to a public sink contains all parts from which information may flow to the given sink. Therefore, if such a slice does not contain any secret source, there is no information flow between any secret source and the public sink.

Moreover, as mentioned before, program dependence graphs turn out to be an appropriate program representation to perform slicing. In particular, they reduce the task of computing a slice to a form of graph reachability. A slice can be obtained by traversing the graph and collecting all nodes that are connected to a given node via a chain of edges.

Extending upon these ideas, the programming paradigms group<sup>1</sup> at KIT developed Joana [104], an information flow control tool for Java. First, given a Java application, Joana builds a program dependence graph. Then, the user can annotate sources and sinks on this graph and use Joana to perform various slicing-based static information flow control checks.

From 2011 to 2017, the programming paradigms group participated in the priority program "Reliably Secure Software Systems" (RS<sup>3</sup> ) of the German Research Foundation (DFG). The main thesis of RS<sup>3</sup> was that classical mechanism-based approaches to software security like authentication and access control need to be complemented with property-oriented approaches such as information flow control [4].

In chapter 4 of this thesis, I will give an overview of the achievements of the programming paradigms group within the RS<sup>3</sup> project. Our group extended the theoretical foundations of Joana, participated in a number of collaborations, and applied Joana to various scenarios within the field of software security.

# **1.2 Systematic Approaches to Advanced Information Flow Analysis**

The second part of my thesis is concerned with a generalization of both data-flow analysis on control-flow graphs and slicing-based techniques on program dependence graphs. One motivation for this is that a generalized analysis framework enables data-flow-like analyses on program dependence graphs and therefore can extend the toolkit of PDG-based tools like Joana by a family of powerful analyses. Moreover, such a generalization

<sup>1</sup>https://pp.ipd.kit.edu

is also theoretically interesting because it clarifies the relation between the two techniques, enables the re-use of formal guarantees and simplifies the development of new slicing-based techniques.

While they have different purposes and have developed independently, data-flow analysis on control-flow graphs and slicing on program dependence graphs share a fairly large amount of similarities. Both operate on a graph that represents the program to be analyzed and obtain their result by some form of propagation along an appropriate set of paths on the given graph. Moreover, both techniques face the same challenge for programs with multiple procedures. To analyze such programs properly, it is crucial to only consider paths where procedure calls return to the sites that they were actually called from. Data-flow analyses are usually conducted in order to derive properties of the set of a given program's executions by propagating pieces of data along abstract representations of these executions. Traditionally, they are expressed using a generic framework, for which general formal guarantees can be given [101, 154, 106]. Moreover, they can be systematically derived from program semantics [45]. In this sense, data-flow analysis can be thought of as executing the program using an abstraction of the real program semantics that concentrates only on those aspects of interest. Thus, data-flow analysis can represent and process fairly complex data.

Slicing on program dependence graphs is a form of reachability analysis. While it appears to be expressible as a very simple data-flow-like analysis – the information that slicing propagates is "this node is reachable"<sup>2</sup> – slicing can in fact *not* be cast as an instance of ordinary data-flow frameworks. This is because slicing requires a richer setting than such frameworks.

In the following, I briefly discuss the setting for data-flow analysis and then contrast it with the one for slicing.

Firstly, interprocedural data-flow analyses track the flow of data beginning in a *main* procedure from which every node is assumed to be reachable.

Secondly, because data-flow analyses are supposed to consider abstractions of actual program executions, they naturally only follow *descending* controlflow paths that begin in entry of the *main* procedure. A control-flow path π is called *descending*, if π only contains returns from procedures for which π also contains the corresponding call.

<sup>2</sup>This will be detailed in chapter 3.

In summary, interprocedural data-flow analyses on control-flow graphs traverse descending paths that begin in the entry of the *main* procedure and thus reach every node in the graph.

In contrast, PDG-based slicing operates with different assumptions.

While data-flow analysis on control-flow graphs always starts propagation with a fixed node for which it is known per se that every node is reachable from it, PDG-based slicing starts from *an arbitrary node of interest*. Naturally, it cannot be assumed that this node is reachable in the PDG from the entry of *main*. Moreover, the node for which the slice is to be computed can be situated in some arbitrary part of the PDG. Hence, in order to compute a complete slice for this node, it is necessary to not only consider descending paths, but also paths that contain unmatched returns.

To sum up, the difference between the two techniques can be described best as follows: While data-flow analysis assumes that every node is reachable and computes a value for it, the task of PDG-based slicing is *to compute the very reachability information that data-flow analysis assumes.*

Hence, a generalization of both data-flow analysis on control-flow graphs and PDG-based slicing has to account for the two aspects that the two techniques lay their respective focus on: On the one hand, it needs to determine which nodes are reachable (like slicing does) and on the other hand, it needs to compute some result for every reachable node (like data-flow analysis does).

The second part of my thesis develops this idea. I propose a generalization of both data-flow analysis and slicing. Firstly, my generalization comprises a general graph model that covers both control-flow graphs and program dependence graphs. Secondly, it provides a framework for generalized data-flow analysis on this graph model. With this framework, both data-flow analyses and slicing-based analyses are expressible. My generalized data-flow framework combines the strengths of both classical data-flow analysis and PDG-based slicing. Like data-flow analysis, it allows to express data with complex structure. Like PDG-based slicing, it takes the node from which propagation is supposed to start as additional input and also follows paths with unmatched returns. I will show that classical approaches to interprocedural data-flow analysis are transferable to my generalized framework. Moreover, I present general algorithms that compute solutions to generalized data-flow analysis problems. These algorithms combine the classical solution algorithms for data-flow analyses [102, 130] with reachability analysis and a well-known approach to interprocedural slicing [93, 137].

# **1.3 Main Theses and Contributions**

The main theses of this dissertation are:

**Main Thesis 1: PDG-based information flow control is useful, practically applicable and relevant.** I report on several applications of Joana within the field of security analysis. These applications were the result of my collaborations with other research groups within the RS<sup>3</sup> project.

# **Main Thesis 2: Data-flow analysis can be systematically applied to program dependence graphs.**


**Main Thesis 3: Data-flow analysis on PDGs can be practically conducted.** Within the scope of this work, I have implemented my algorithms in Joana and have evaluated them on real program dependence graphs. Thus, I demonstrate that the presented approaches not only enjoy pleasant theoretical properties but are also practically feasible and useful.

# **1.4 Organization of This Thesis**

The organization of this thesis is depicted in Figure 1.3.

Chapter 2 compiles the basic notions and theories that are relevant throughout this thesis. In particular, it gives an introduction to basic order and fixed-point theory and, based upon that, a presentation of monotone constraint systems and the classical worklist-based algorithm to solve such systems. Monotone constraint systems form the theoretic foundation of data-flow analysis as it is presented and used in this thesis. The solving algorithm serves as the basis for algorithms that are developed in later chapters.

After the general foundations were laid in chapter 2, chapter 3 prepares for the two main topics of this thesis. Its first part, which ranges from 3.1 to 3.3, gives an introduction to several concepts and techniques employed in static program analysis. Specifically, it introduces both data-flow analysis on interprocedural control-flow graphs and context-sensitive slicing on interprocedural program dependence graphs. Moreover, it introduces information flow control and discusses its relation to slicing. Finally, section 3.4 prepares the reader for chapter 4. It gives an overview of Joana, the information flow control tool developed by the programming paradigms group. In particular, it explains various data-flow analysis techniques that Joana employs in order to properly compute program dependence graphs for object-oriented languages such as Java.

After chapter 3 has provided the necessary program analysis background, chapter 4 reports on the contributions of the programming paradigms group to RS<sup>3</sup> . These contributions consist of (a) advancements of PDGbased checks for concurrent non-interference and (b) applications of Joana to various scenarios and collaborations.

The chapters following chapter 4 lay out the second main topic of my thesis: The development of a general interprocedural framework that subsumes

**Figure 1.3:** Visualization of the organization of this thesis – including the relation to its title

both data-flow analysis on control-flow graphs and context-sensitive slicing on program dependence graphs.

In chapter 5, which directly builds on the first part of chapter 3, I derive a graph model that subsumes both control-flow graphs and program dependence graphs and, based on this model, a general data-flow framework. In addition, I discuss a variety of example analyses that can be expressed within this framework.

Chapter 6 is concerned with the characterization of solutions to the problems posed by instances of the data-flow framework in chapter 5. To accomplish this task, I employ monotone constraint systems. I demonstrate that both the functional approach and the call-string approach can be applied to my general data-flow framework and that – under appropriate assumptions – both solve the problem with maximal precision. Because the unrestricted call-string approach in general does not allow a practical solution algorithm, I also consider a technique that enables practical algorithms and maintains correctness (while sacrificing precision). In particular, this technique generalizes the well-known technique that was proposed for interprocedural data-flow analysis on control-flow graphs.

In chapter 7, I describe algorithms for solving the constraint systems given in chapter 6. These algorithms combine a general worklist-based approach with a reachability analysis. For the functional approach, the algorithms that I obtain look like generalized versions of the well-known algorithms for context-sensitive slicing.

In chapter 8, I present an implementation of the algorithms derived in chapter 7, describe an evaluation of the implementation and discuss the evaluation results.

Chapter 9 gives a critical discussion of the work developed in the preceding chapters and relates it to the existing literature.

Finally, in chapter 10, I recapitulate the contents of this thesis and give an outlook on possible future work.

**Usage of Personal Pronouns** At this point, I want to briefly make clear the convention that I apply concerning the usage of personal pronouns in this dissertation.

Generally, it is similar to earlier dissertations [39]: I will mainly use the first person singular. In proofs, I use the plural form in order to invite the reader to conduct them with me. An exception is chapter 4, where I report about research that was conducted by multiple persons, including me. This is why I use the plural form "we" there. Generally, I choose to deviate from these rules whenever I think that it is necessary.

*My foundations were made of clay.*

# <sup>E</sup>ric <sup>C</sup>lapton **2 Foundations**

# **2.1 Sets and Relations**

I assume in the following that the reader is familiar with basic set theory. This section is supposed to clarify the notations and conventions that I apply in this thesis.

For a given set *A*, I write 2*<sup>A</sup> de f* = {*B* | *B* ⊆ *A*} for the *power set* of *A*.

For two sets *A* and *B*, *A* × *B de f* = {(*a*, *b*) | *a* ∈ *A* ∧ *b* ∈ *B*} is the *cartesian product* of *A* and *B*. Elements (*a*, *b*) ∈ *A* × *B* are also called *pairs*. For the purposes of this thesis, I consider cartesian products associative, that is I identify *A* × (*B* × *C*) with (*A* × *B*) × *C*. Hence, the parentheses can generally be omitted.

The cartesian product can also be considered for *n* ∈ **N** sets *A*<sup>1</sup> , . . . , *An*:

$$A\_1 \times \dots \times A\_n = \prod\_{i=1}^n A\_i \stackrel{def}{=} \{ (a\_1, \dots, a\_n) \mid \forall 1 \le i \le n. \, a\_i \in A\_i \}$$

Elements (*a*<sup>1</sup> , . . . , *an*) of ∏︁*<sup>n</sup> i*=1 ∏︁ *A<sup>i</sup>* are called *n-tuples*. A special case is *n* = 0: *n i*=1 *Ai* is defined to be the set that consists only of the *empty tuple* {()}. In the following, I want to consider the special case of relations between two sets that I also call *binary relations*.

A binary relation *R* ⊆ *A* × *B* is called

1. *left-unique* if

∀*x* ∈ *A*.∀*x* ′ ∈ *A*.∀*y* ∈ *B*.(*x*, *y*) ∈ *R* ∧ (*x* ′ , *y*) ∈ *R* =⇒ *x* = *x* ′ 2. *right-unique* if

$$\forall \mathbf{x} \in A. \forall y \in B. \forall y' \in B. (\mathbf{x}, y) \in R \land (\mathbf{x}, y') \in R \implies y = y'$$

3. *left-total* if

$$\forall \mathbf{x} \in A. \exists y \in B. (\mathbf{x}, y) \in \mathbb{R}^n$$

4. *right-total* if

$$\forall y \in B. \exists x \in A. (x, y) \in R.$$

The *domain* of a binary relation *R* is

$$dom(R) \stackrel{def}{=} \{ a \in A \mid \exists b \in B. \ (a, b) \in R \}$$

The *image* of a binary relation *R* is

$$\dim(\mathcal{R}) \stackrel{def}{=} \{ b \in \mathcal{B} \mid \exists a \in A. \ (a, b) \in \mathcal{R} \}$$

If *R* is right-unique and *a* ∈ *dom*(*R*), then I write *R*(*a*) for the one and only *b* ∈ *B* such that (*a*, *b*) ∈ *R*. Analogously, if *R* is left-unique and *b* ∈ *dom*(*R*), then I write *R* −1 (*b*) for the one and only *a* ∈ *A* such that (*a*, *b*) ∈ *R*. If *R* ⊆ *A* × *B* is right-unique, then *R* is also called *partial function from A to B*. For the set of partial functions from *A* to *B*, I use the notation *A* →*<sup>p</sup> B*. For *f* ∈ *A* →*<sup>p</sup> B*, I also write *f* : *A* →*<sup>p</sup> B*. If *R* is additionally left-total, then *R* is also called *function from A to B*. By *A* → *B*, I mean the set of functions from *A* to *B* and for *f* ∈ *A* → *B*, I also write *f* : *A* → *B*. A function *R* is called


Every binary relation *R* ⊆ *A* × *B* can be assigned a function *f<sup>R</sup>* : *A* → 2 *B*, defined by

$$f\_{\mathcal{R}}(a) \stackrel{def}{=} \{ b \in \mathcal{B} \mid (a, b) \in \mathcal{R} \}.$$

Occasionally, I will use *R* and *f<sup>R</sup>* interchangeably, that is I will consider relations *R* ⊆ *A* × *B* as functions *A* → 2 *B*.

# **2.2 Complete Lattices, Fixed Point Theory Theory and Monotone Constraint Systems**

In this section, I recall the basic notions that data-flow analysis builds upon and show generic algorithms that can be used to perform data-flow analyses. Particularly, I define monotone constraint systems and show how to solve them.

In subsection 2.2.1, I recall and clarify the basic notions and compile important results from the literature. In subsection 2.2.2, I introduce monotone constraint systems and characterize their solutions. In subsection 2.2.3, I present algorithms for solving monotone constraint systems.

# **2.2.1 Partial Orders and Fixed-Points**

**Partial Orders.** A *partial order* is a tuple (*L*,≤) which consists of a set *L* and a relation ≤ ⊆ *L* × *L* with the following properties:


Let (*L*,≤) be a partial order and *A* ⊆ *L*. Then *x* is called *minimal in A* if ∀*y* ∈ *A*.*x* ̸≥ *y*. Moreover, *x* ∈ *A* is called *least element of A* if ∀*y* ∈ *A*. *x* ≤ *y*. Note that least elements do not need to exist but are unique if they do, while minimal elements neither need to exist nor need to be unique. However, if *A* has a least element, then this element is necessarily minimal. Plus, if *A* is finite and non-empty, it always has minimal elements.

**Bounds.** An element *u* ∈ *L* is called *upper bound* of *A* ⊆ *L* if

$$\forall a \in A. \; a \le u.$$

*Upper*(*A*) denotes the set of upper bounds of *A*. *u* ∈ *L* is called *least upper bound* of *A* if *u* is an upper bound of *A* and if

$$\forall u' \in \mathcal{U}proper(A). \; u \le u'.$$

I also write ⨆︁ *A* for the least upper bound of *A*. *l* ∈ *L* is called *lower bound* of *A* if

$$
\forall a \in A. \ l \le a.
$$

*Lower*(*A*), or ⨅︁ *A*, respectively, denotes the set of lower bounds of *A*. *l* ∈ *L* is called *greatest lower bound* of *A* if *l* is a lower bound of *A* and if

> ∀*l* ′ ∈ *Lower*(*A*). *l* ′ ≤ *l*.

In the following, I compile some basic facts about least upper bounds and greatest lower bounds. Since they do not need to exist, I apply the usual convention for equations about partially defined objects: If I state an equation of the form *x* = *y*, I mean that either both *x* and *y* exist and coincide or that neither exists.

Least upper bounds and greatest lower bounds are dual to each other in the following sense:

$$\text{(2.1)}\qquad\qquad\forall A\subseteq L.\bigsqcup A=\bigsqcup\ulcorner\ulcorner\urcorner\urcorner\urcorner\urcorner\urcorner\urcorner\urcorner\urint\urleft\sharp\left(A\right)\left(\begin{array}{c}\end{array}\right)$$

$$\text{(2.2)}\qquad\qquad\forall A\subseteq L.\prod A = \bigsqcup L\\\text{Lower}(A)$$

Assume that *<sup>A</sup>* = {*a*, *<sup>b</sup>*} and that ⨆︁ *A* exists. Then we define

*a*<sup>1</sup> ⊔ *a*<sup>2</sup> *de f* = ⨆︂ {*a*1 (2.3) , *a*2} *de f*

$$(2.4) \qquad \qquad \qquad a\_1 \sqcap a\_2 \overset{def}{=} \biguplus \{a\_1, a\_2\}$$

This defines partial binary operations ⊔,⊓ : *L* × *L* →*<sup>p</sup> L*. Easy calculations show that both ⊔ and ⊓ are associative and commutative, that is

(2.5) ∀*a*, *b* ∈ *L*. *a* ⊔ *b* = *b* ⊔ *a* ∧ *a* ⊓ *b* = *b* ⊓ *a*

$$(\text{2.6})\qquad \forall a, b, c \in \text{L.}\ a \sqcup (b \sqcup c) = (a \sqcup b) \sqcup c \wedge a \sqcap (b \sqcap c) = (a \sqcap b) \sqcap c.$$

These properties of ⊔ and ⊓ justify to extend both operations to arbitrary finite sets:

$$(\text{2.7}) \qquad \qquad \qquad a\_1 \sqcup \dots \sqcup a\_n \stackrel{def}{=} \bigsqcup \{a\_1, \dots, a\_n\}.$$

$$(2.8) \qquad \qquad a\_1 \sqcap \dots \sqcap a\_n \overset{def}{=} \biguplus \{a\_{1'}, \dots, a\_n\}.$$

14

**Monotone Functions.** For two partial orders (*L*,≤) and (*L*,≤ ′ ), I call a function *f* : *L* → *L* ′ *monotone*, if

$$(2.9) \qquad \qquad \forall l\_1, l\_2 \in L. \, l\_1 \le l\_2 \implies f(l\_1) \le' f(l\_2)$$

Given a partial order (*L*,≤) and a monotone function *f* : *L* → *L*, we can consider elements of *L* that behave specially with respect to *f*. I call *x* ∈ *L*


The sets of reductive elements, extensive elements and fixed-points are denoted by *Ext*(*f*), *Red*(*f*) and *Fix*(*f*), respectively.

If *Fix*(*f*) has a least element, i.e. if there is *x* ∈ *Fix*(*f*) with the property ∀*y* ∈ *Fix*(*f*). *x* ≤ *y*, then I call x *least fixed-point of f* and write it as *l f p*(*f*).

**Complete Lattices.** A *complete lattice* is a partial order (*L*, ≤) in which every subset has a least upper bound. In particular, it has a least element ⊥ *de f* = ⨆︁ ∅ = ⨅︁ *L* and a greatest element ⊤ *de f* = ⨆︁ *L* = ⨅︁ ∅.

**Theorem 2.1** (Knaster-Tarski, cf.[161, Theorem 1])**.** *Let f* : *L* → *L be a monotone function on a complete lattice* (*L*,≤)*. Then Fix*(*f*) *is not empty and* (*Fix*(*f*),≤) *is a complete lattice. In particular, we have*

$$\bigsqcup F\text{ix}(f) = \bigsqcup E\text{x}(f)$$

*and*

⨅︁ *Fix*(*f*) = ⨅︁ *Red*(*f*)

Theorem 2.1 reduces the problem of finding the least reductive point to the problem of finding the least fixed point. Unfortunately, it is not constructive, in the sense that it gives no recipe of how to find the least fixed point. However, with some modifications, a constructive result can be given in the form of such a recipe. Before I present this result, I introduce some auxiliary definitions.

**Chains and Chain conditions.** A *chain* in (*L*,≤) is a subset *C* ⊆ *L* such that

$$\forall x, y \in \mathbb{C}. \, x \le y \lor y \le \infty.$$

With *Chains*(*L*) I denote the set of chains in *L*. A sequence (*l i* )*i*∈**<sup>N</sup>** is called *ascending chain* if

$$\forall i, j \in \mathbb{N}. \, i \le j \implies l\_i \le l\_j$$

I denote the ascending chains of *L* with *Asc*(*L*). A sequence (*l i* )*i*∈**<sup>N</sup>** is called *descending chain* if

$$\forall i, j \in \mathbb{N}. \, i \le j \implies l\_i \ge l\_j.$$

I denote the descending chains of *L* with *Desc*(*L*).

It is important to consider the different natures of chains on the one side and ascending and descending chains on the other side. Any ascending or descending chain can be assigned a chain as follows: Every sequence can be considered a function *l* : **N** → *L*. Hence, we can consider the image *im*(*l*) of *l* and observe that *im*(*l*) is a chain if *l* is an ascending or descending chain.

Conversely, for a given chain *C*, any monotone function

$$l: (\mathbb{N}, \le\_{\mathbb{N}}) \to L$$

with *C* = *im*(*L*) can justify to regard *C* as ascending chain and any monotone function *l* : (**N**,≥**N**) → *L* with *C* = *im*(*L*) can justify to regard *C* as descending chain.

However, there are chains which cannot be considered as ascending or descending chains. Take for example the set **Z** of integers with its natural ordering and consider the set 2**Z** of even integers. It is easy to see that 2**Z** forms a chain in **Z** but there is no monotone function *l* : **N** → **Z** with *im*(*l*) = 2**Z** (neither for ≤**<sup>N</sup>** nor for ≥**N**).

A partial order satisfies the *ascending chain condition*, if every ascending chain eventually stabilizes:

$$(\text{ACC}) \qquad \forall (l\_i)\_{i \in \mathbb{N}} \in \text{Acc}(L) \\ \exists n\_0 \in \mathbb{N}. \forall n \ge n\_0. \ l\_n = l\_{n\_0}$$

A partial order satisfies the *descending chain condition*, if every descending chain eventually stabilizes:

$$(\text{DCC}) \qquad \forall (l\_i)\_{i \in \mathbb{N}} \in \text{Des} \, c(L) \\ \exists n\_0 \in \mathbb{N}. \, \forall n \ge n\_0. \, l\_n = l\_{n\_0}$$

If *C* is a finite chain, then the cardinality |*C*| is also called the *height of C*. *L* is said to have *finite height* if there is *n* ∈ **N** such that every chain has a height of at most *n*. The smallest such *n* (if it exists) is also called the *height of L* and is written as *height*(*L*).

*L* is said to have *no infinite chains*, if all chains *C* ⊆ *L* are finite.

There is a subtle difference between partial orders of finite height and partial orders with no infinite chains. Figure 2.1b illustrates the difference.

**Figure 2.1: (a)**: Relationship between different chain conditions – **(b)**: proof sketch for why the inclusion between "finite height" and "no infinite chains" is proper

As I elaborated on above, ascending and descending chains are special cases of chains. However, with regard to the respective chain conditions, there is a strong connection.

**Theorem 2.2** ([50, p. 2.40])**.** *A partial order has no infinite chains if and only if it satisfies both the ascending and the descending chain condition.*

**Chain-Complete Partial Orders.** A *chain-complete partial order*(CCPO) is a partial order in which every chain in *L* has a least upper bound. In particular, a CCPO has a bottom element ⊥ = ⨆︁ ∅, since ∅ is a chain. Let (*L*, ≤) and (*L* ′ , ≤ ′ ) be two CCPOs. Then *f* : *L* → *L* ′ is called *continuous*, if *f* is monotone and for every chain *C* in *L* we have *f*( ⨆︁ *C*) = ⨆︁ *f*(*C*).

**Theorem 2.3** (Fixed-point theorem of Kleene)**.** *If* (*L*,≤) *is a CCPO and f* : *L* → *L is continuous, then f has a least fixed-point that is characterized as follows:*

$$\operatorname{lfp}(f) = \bigsqcup\_{i \in \mathbb{N}} f^i(\bot)$$

*Proof.* See, e.g., [28, Theorem 6.3.2]. □

Theorem 2.3 not only gives a constructive characterization of the least fixed-point, it also enables a powerful proof technique that I will make use of later. This proof technique is formalized in the following lemma (cf.[28, Theorem 6.3.5]).

**Lemma 2.4** (fixed-point induction principle)**.** *Consider a continuous function f* : *L* → *L on a CCPO L. Furthermore, let A* ⊆ *L be a subset of L which has the following closure properties:*


$$3. \; \forall a \in L. \; a \in A \implies f(a) \in A$$

*Then l f p*(*f*) ∈ *A.*

*Proof.* Due to the assumptions about *L* and *f*, we can apply Theorem 2.3 and obtain

$$\operatorname{lfp}(f) = \bigsqcup\_{i \in \mathbb{N}} f^i(\bot)$$

With the help of properties 1 and 3 we can show by complete induction on *i* ∈ **N**:

$$\forall i \in \mathbb{N}. \, f^i(\bot) \in A$$

Finally, by property 2, we get

$$\operatorname{lfp}(f) = \bigsqcup\_{i \in \mathbb{N}} f^i(\bot) \in A$$

□

Every complete lattice is a CCPO. However, the fixed-point theorem of Kleene is not applicable under the assumptions of Theorem 2.1. For general complete lattices, not all monotone functions are continuous. However, if we restrict the lattice, monotonicity and continuity coincide and Kleene's fixed-point theorem becomes applicable.

**Lemma 2.5.** *If* (*L*,≤) *is a CCPO which satisfies* (ACC)*, then every monotone function f* : *L* → *L is continuous.*

*Proof.* Let *f* : *L* → *L* be monotone and assume that *C* ⊆ *L* is a chain. We must show that

$$f(\bigsqcup \mathcal{C}) = \bigsqcup f(\mathcal{C})$$

Due to the monotonicity of *f*, it is clear that ⨆︁ *f*(*C*) ≤ *f*( ⨆︁ *C*), so it suffices to show

$$f(\bigsqcup \mathbf{C}) \le \bigsqcup f(\mathbf{C})$$

This is easy to see if *C* is finite, so we assume that *C* is infinite. First we observe that ⨆︁ *C* ∈ *C*, otherwise we could construct an ascending sequence (*ci* )*i*∈**<sup>N</sup>** with ∀*i* ∈ **N**. *c<sup>i</sup>* ∈ *C* and ∀*i*, *j* ∈ **N**. *i* < *j* =⇒ *c<sup>i</sup>* < *c<sup>j</sup>* in contradiction to *L* satisfying (ACC).

But ⨆︁ *C* ∈ *C* implies *f*( ⨆︁ *C*) ∈ *f*(*C*) and this implies *f*( ⨆︁ *C*) ≤ ⨆︁ *f*(*C*). □

**Corollary 2.6.** *If* (*L*,≤) *is a CCPO which satisfies* (ACC) *and f* : *L* → *L is monotone, then the least fixed-point of f is characterized as follows:*

$$\text{(2.10)}\qquad\qquad lfp(f) = \bigsqcup\_{i \in \mathbb{N}} f^i(\bot).$$

*In particular, there is an n* <sup>∈</sup> **<sup>N</sup>** *such that l f p*(*f*) *coincides with f <sup>n</sup>* (⊥)*:*

$$\text{(2.11)}\tag{2.11}$$

$$\text{(2.11)}\tag{2.12}$$

*Proof.* By Lemma 2.5, any monotone function on *L* is continuous. Hence, the first claim follows by Theorem 2.3.

Next, we show (2.11). For this, consider the set

$$\mathbb{K}\_f \stackrel{def}{=} \{ f^{\vec{i}}(\bot) \mid i \in \mathbb{N} \}$$

19

If *K<sup>f</sup>* is finite, we choose *n* as the greatest number such that *f n* (⊥) ∈ *K<sup>f</sup>* . This is always possible because *K<sup>f</sup>* is not empty. For this choice of *n*, (2.10) implies (2.11).

Now consider the case that *K<sup>f</sup>* infinite. We take a look at the observation made in the proof of Lemma 2.5: It said that ⨆︁ *C* ∈ *C* for every chain *C* ⊆ *L*, provided that *L* satisfies (ACC). Plus, an easy inductive argument shows that *K<sup>f</sup>* is indeed a chain. Hence, we may conclude that

$$\bigsqcup \mathsf{K}\_f \in \mathsf{K}\_f.$$

In other words, there must be *n* ∈ **N** such that

$$\bigsqcup\_{i \in \mathbb{N}} f^i(\bot) = f^n(\bot)\_{\prime}$$

as desired. □

**Corollary 2.7.** *Let L be a CCPO which satisfies* (ACC) *and f* : *L* → *L be a monotone function on L. Furthermore, let A* ⊆ *L be a subset of L which has the following closure properties:*

*1.* ⊥ ∈ *A*

$$\text{2. } \forall B \in \text{Chains}(L). \text{ } B \subseteq A \implies \bigsqcup B \in A$$

$$3. \; \forall a \in L. \; a \in A \implies f(a) \in A.$$

*Then l f p*(*f*) ∈ *A.*

*Proof.* This follows from Lemma 2.5 and Lemma 2.4. □

The ascending chain condition also comes in handy when we want to show that a partial order is a complete lattice. With (ACC) it is enough to show the existence of a bottom element and that every finite subset has a least upper bound. This is formalized by Lemma 2.8.

**Lemma 2.8.** *Let* (*L*, ≤) *be a partial order that satisfies* (ACC)*. Then the following statements are equivalent:*


*Proof.* This follows from [50, p. 2.41]. □

**Some Constructions** I want to conclude this section with the compilation of some examples of complete lattices, which will play a role later in this thesis.

For any set *A*, the power set 2 *<sup>A</sup>* with respect to set inclusion forms a complete lattice (2 *<sup>A</sup>*,⊆), called the *power set lattice of <sup>A</sup>*. If *<sup>A</sup>* is finite, then the power set lattice of *A* has finite height. If *A* is infinite, then neither (ACC) nor (DCC) are satisfied.

If (*L*<sup>1</sup> ,≤<sup>1</sup> ) and (*L*2,≤2) are two partial orders, then the cartesian product (*L*<sup>1</sup> × *L*2,≤) forms a partial order where ≤ is the relation

$$(\mathfrak{x}, \mathfrak{y}) \le (\mathfrak{x}', \mathfrak{y}') \overset{def}{\Longleftrightarrow} \mathfrak{x} \le\_1 \mathfrak{x}' \land \mathfrak{y} \le\_2 \mathfrak{y}'$$

Moreover,


This can be generalized to an arbitrary finite number of complete lattices. If (*L*,≤) is a partial order and *A* is any set, then the set of total functions *A* → *L* is a partial order with

$$f \le g \iff \forall x \in A. \; f(x) \le g(x)$$

If *L* is a complete lattice, then *A* → *L* is, too. Moreover, if *L* satisfies (ACC) and *A* is finite, then *A* → *L* satisfies (ACC). Conversely, if *A* → *L* satisfies (ACC), then either *L* contains only one element or *A* is finite and *L* satisfies (ACC).

If (*L*<sup>1</sup> ,≤<sup>1</sup> ) and (*L*2,≤2) are partial orders, then the space *L*<sup>1</sup> →*mon L*<sup>2</sup> of monotone functions is a partial order via the relation defined above. Plus, if *L*<sup>2</sup> is a complete lattice, then *L*<sup>1</sup> →*mon L*<sup>2</sup> is also a complete lattice. That is, for any set *A* ⊆ *L*<sup>1</sup> →*mon L*<sup>2</sup> of monotone functions, the least upper bound defined by

$$(\bigsqcup A)(\mathfrak{x}) = \bigsqcup \{ f(\mathfrak{x}) | f \in A \}$$

is also monotone.

# **2.2.2 Monotone Constraint Systems**

In this section, I introduce *monotone constraint systems*. These systems are fundamental for program analysis and in particular for data-flow analysis, since they are expressive enough to describe abstractly how a program propagates values.

# **2.2.2.1 Syntax**

I start with a set *X* of variables and a set F of function symbols. Every function symbol has an *arity a*(*f*) ∈ **N**. Particularly, I allow function symbols with *a*(*f*) = 0, which I also call *constants*.

The set *Expr*(*X*, F ) of expressions over *X* and F is defined inductively as follows:

1. *X* ⊆ *Expr*(*X*, F )

2. If *f* ∈ F with *a*(*f*) = *n* and *t*<sup>1</sup> , . . . ,*t<sup>n</sup>* ∈ *Expr*(*X*, F ), then *f*(*t*<sup>1</sup> , . . . ,*tn*) ∈ *Expr*(*X*, F ).

A *monotone constraint* (or constraint for short) has the form *x* ≥ *t* where *x* is a variable and *t* ∈ *Expr*(*X*, F ) is some expression over *X* and F .

A *(monotone) constraint system* is a set C of constraints.

With *FV*(*t*) I denote the variables occurring in *t* ∈ *Expr*(*X*, F ). I refer to *FV*(*t*) as the set of *free variables in t*.

Let C be a monotone constraint system and *c* = *x*≥*t* ∈ C. Then I define *lhs*(*c*) *de f* = *x* and *rhs*(*c*) *de f* = *t* and refer to *lhs*(*c*) and *rhs*(*c*) as the left-hand side and right-hand side of *c*, respectively.

With *Vars*(C), I denote the set of left-hand sides of constraints in C, i.e.

$$(\text{2.12})\qquad\qquad\qquad\text{Vars}(\mathcal{C})=\{\text{lbs}(\mathcal{c})\mid \mathcal{c}\in\mathcal{C}\}$$

For a constraint *c* ∈ *C* with *x* = *lhs*(*c*), I also say that *c defines x* and refer to *c* as *defining constraint* for *x*.

**Definition 2.9.** *Let* C *be a constraint system.*

*1. I define* 〜 ⊆ C × C *by <sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* 〜 *<sup>x</sup>* ′ ≥ *t* ′ *if x* ∈ *FV*(*t* ′ )*. With* 〜<sup>⋆</sup> *I denote the reflexive-transitive closure of* 〜 *and with* 〜<sup>+</sup> *the transitive closure.*

*2. For sets* C<sup>1</sup> *,* <sup>C</sup><sup>2</sup> ⊆ C *of constraints I write* <sup>C</sup><sup>1</sup> 〜 <sup>C</sup><sup>2</sup> *if*

$$
\exists c\_1 \in \mathcal{C}\_1. \; \exists c\_2 \in \mathcal{C}\_2. \; c\_1 \sim c\_2.
$$

<sup>C</sup><sup>1</sup> 〜<sup>⋆</sup> <sup>C</sup><sup>2</sup> *and* <sup>C</sup><sup>1</sup> 〜<sup>+</sup> <sup>C</sup><sup>2</sup> *have the respective meaning. Instead of* {*c*} 〜 <sup>C</sup>2*, I also write <sup>c</sup>* 〜 <sup>C</sup><sup>2</sup> *(analogously for* <sup>C</sup><sup>2</sup> <sup>=</sup> {*c*}*). For* <sup>C</sup><sup>0</sup> ⊆ C *I denote with* <sup>C</sup> ⋆ 0 *the set* {*<sup>c</sup>* ∈ C : <sup>C</sup><sup>0</sup> 〜<sup>⋆</sup> *<sup>c</sup>*} *(*<sup>C</sup> + 0 *is defined analogously).*

*3. For x* ∈ *X I denote with De f*(*x*) *the set of constraints with x on the left-hand side.*

*4. I overload* 〜 *to* 〜<sup>⊆</sup> *<sup>X</sup>* <sup>×</sup> *X as follows*

$$\mathfrak{x} \sim \mathfrak{x}' \iff Def(\mathfrak{x}) \sim \mathcal{D}ef(\mathfrak{x}').$$

〜⋆<sup>⊆</sup> *<sup>X</sup>* <sup>×</sup> *X and* 〜+<sup>⊆</sup> *<sup>X</sup>* <sup>×</sup> *X have the respective meaning for sets of variables.*

## **2.2.2.2 Semantics**

In the following, I define what it means for a constraint system to be satisfied. In a nutshell, I assign every occurring expression a monotone function on an appropriately chosen partially ordered set *L*. This allows to evaluate the left-hand side and the right-hand side of a constraint and to determine whether the left-hand side is greater than or equal to the right-hand side, with respect to the partial order on *L*.

Let (*L*,≤) be a partially ordered set.

A *variable assignment* is a function ψ : *X* → *L*. Furthermore, I assign every function symbol *f* ∈ F with *a*(*f*) = *n* a monotone *interpretation* α(*f*) : *L <sup>n</sup>* <sup>→</sup>*mon <sup>L</sup>*. Now I inductively define the interpretation ⟦*t*⟧ : (*<sup>X</sup>* <sup>→</sup> *<sup>L</sup>*) <sup>→</sup> *<sup>L</sup>* of expressions:


By straight-forward induction I can show Lemma 2.10, which states basic but important properties of ⟦·⟧ and *FV*.

**Lemma 2.10.** *Let t* ∈ *Expr*(*X*, F ) *be an expression. Then the following statements hold:*


$$\forall \mathbf{x} \in FV(t). \,\psi(\mathbf{x}) \le \psi'(\mathbf{x}).$$

*then*

⟦*t*⟧(ψ) <sup>≤</sup> ⟦*t*⟧(<sup>ψ</sup> ′ ).

*In particular:* ∀*x* ∈ *FV*(*t*). ψ(*x*) = ψ ′ (*x*) *implies* ⟦*t*⟧(ψ) = ⟦*t*⟧(ψ ′ )*.*

A variable assignment <sup>ψ</sup> : *<sup>X</sup>* <sup>→</sup> *<sup>L</sup> satisfies a constraint <sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* if <sup>ψ</sup>(*x*) <sup>≥</sup> ⟦*t*⟧(ψ). ψ *satisfies a constraint system* if it satisfies all *c* ∈ C. I this case, I call ψ a *solution* of C. ψ is called *least solution* of C if ψ ≤ ψ ′ for all solutions ψ ′ of C. Given a constraint system C I define the *corresponding functional*

$$F\_{\mathcal{C}} : (X \to L) \to (X \to L)$$

by

$$F\_{\mathcal{C}}(\psi)(\mathfrak{x}) = \bigsqcup \{ \llbracket t \rrbracket(\psi) | \mathfrak{x} \succeq t \in \mathcal{C} \}$$

For the moment, I implicitly require that *F*<sup>C</sup> is well-defined, i.e. that all least upper bounds on the right-hand side exist.

An easy calculation shows that *F*<sup>C</sup> is monotone. Furthermore, there is a strong connection between the solutions of C and the reductive points of *F*C:

**Lemma 2.11.** ψ *is a solution of* C *if and only if F*C(ψ) ≤ ψ*.*

*Proof.* " ⇐= ": Assume *F*C(ψ) ≤ ψ and let *x*≥*t* ∈ C. Then, by definition of *F*C,

$$F\_{\mathcal{C}}(\psi)(x) \ge \|t\|(\psi)$$

Since *F*C(ψ) ≤ ψ, this implies

$$
\psi(x) \ge \|t\| (\psi).
$$

Hence, ψ is a solution of C.

" <sup>=</sup><sup>⇒</sup> ": If we have <sup>ψ</sup>(*x*) <sup>≥</sup> ⟦*t*⟧(ψ) for every *<sup>x</sup>*≥*<sup>t</sup>* ∈ C, then

$$\forall \mathbf{x} \in \mathcal{X}. \psi(\mathbf{x}) \ge \bigsqcup \{ \llbracket t \rrbracket(\psi) \vert \mathbf{x} \ge t \in \mathcal{C} \} = F\_{\mathcal{C}}(\psi)(\mathbf{x})$$

which is equivalent to <sup>ψ</sup> <sup>≥</sup> *<sup>F</sup>*C(ψ). □

The connection between the solutions of C and the reductive points of *F*<sup>C</sup> makes the theory developed in section 2.2 available. The least solution exists and coincides with the least fixed-point of *F*<sup>C</sup> if


If *L* is a CCPO and *F*<sup>C</sup> is well-defined and continuous (which is for example the case if *L* is a complete lattice satisfying the ascending chain condition), then the least solution is characterized by

$$\operatorname{Ifp}(F\_{\mathcal{C}}) = \bigsqcup\_{i \in \mathbb{N}} F\_{\mathcal{C}}^{i}(\bot).$$

# **2.2.3 Solving Monotone Constraint Systems**

Now we know how monotone constraint systems look like and what they mean. In the following, I show algorithms that can solve them under some common conditions.

Let C be a monotone constraint system over a complete lattice *L* that satisfies the ascending chain condition. Then Kleene's fixed-point theorem (Corollary 2.7) suggests a simple algorithm to compute the least solution of C, which is presented in Algorithm 1.

Algorithm 1 is indeed very simple. From previous considerations, it can easily be seen that Algorithm 1 must terminate and that upon termination, the value of A is indeed the least fixed-point of F<sup>C</sup> and therefore the least solution of C. On the other hand, it is very inefficient: *F*C(A) is computed by computing *F*C(A(*x*)) for all *x* ∈ *X* and *F*C(A)(*x*) is computed by evaluating ⟦*t*⟧(A) for all *<sup>t</sup>* such that *<sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* ∈ C and computing their supremum. In effect, all constraints are evaluated and this is re-iterated for all constraints, even if only one value changed.

More efficient algorithms can be obtained by carefully tracking the constraints that need to be updated. For every constraint *x* ≥ *t*, according to Lemma 2.10, ⟦*t*⟧ only depends on *FV*(*t*), i.e. it can only change if the value of at least one *y* ∈ *FV*(*t*) has changed. Conversely, this means: If we re-evaluate A(*x*) and it changes, then afterwards we only have to consider those constraints where *x* occurs on the right-hand side, i.e. the constraints **Algorithm 1:** Simple algorithm to compute the least solution of a monotone constraint system


of the form *y* ≥ *t* such that *x* ∈ *FV*(*t*), or, in the spirit of Definition 2.9, which are related to *<sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* via <sup>C</sup>'s 〜-relation.

We now can give an improved version of Algorithm 1, which is shown in Algorithm 2. This algorithm does not apply *F*<sup>C</sup> globally but considers each constraint individually.

Like Algorithm 1, Algorithm 2 maintains a function A : *X* → *L* which is initialized to ⊥ for all *x* and then updated incrementally. Additionally, it maintains a list of constraints which have to be considered later – the so-called *worklist<sup>3</sup>* . This list initially contains all constraints, which ensures that every constraint is considered at least once. In the iteration phase, the worklist is processed as follows: First, a constraint *x* ≥ *t* is removed. Then, the algorithm checks whether the current value of A satisfies this constraint. If this is the case, the constraint can be discarded and A is left unchanged. If this is not the case, then A is updated. After the update, A satisfies *x* ≥ *t*. Lastly, all constraints which may be influenced by *x*, i.e. all *x* ′ ≥ *t* ′ such that *x* 〜 *x* ′ , are inserted into the worklist. This ensures that every constraint that may have been violated by the recent update of A will be considered again later.

<sup>3</sup> I use the term work*list* here although the algorithms treat *W* like a set. The reason is that I do not want to deviate from the literature too much.

**Algorithm 2:** Worklist algorithm for computing the least solution of a monotone constraint system (adapted from [130, Table 6.1])

```
Input: a finite monotone constraint system C
  Result: the least solution of C
1 W ← ∅
2 foreach x≥t ∈ C do
3 W ← W ∪ {x ≥ t}
4 A(x) ← ⊥
5 while W ≠ ∅ do
6 x ≥ t ← remove(W)
7 new ← eval(t, A)
8 if A(x) ̸≥ new then
9 A(x) ← A(x) ⊔ new
10 foreach x
                 ′ ≥ t
                     ′
                      such that x ≥ t 〜 x
                                       ′ ≥ t
                                           ′ do
11 W ← W ∪ {x
                       ′ ≥ t
                           ′
                           }
12 return A
```
By considering each constraint at least once and ensuring that a constraint is considered again if the value of an influencing variable has changed, it can be shown that Algorithm 2 indeed computes the least solution of C, as stated by Theorem 2.12.

**Theorem 2.12** (cf. Lemma 6.4 in [130])**.** *If* C *is finite and L satisfies the ascending chain condition, then Algorithm 2 computes the least solution of* C*.*

Note that Algorithm 2 does not specify how elements are inserted into the worklist and how they are removed. This means that the algorithm is correct no matter in which order the constraints are processed, so that it can be further improved by optimizing the evaluation order.

Instead of maintaining constraints in a worklist, one can also maintain variables instead of constraint. This leads to Algorithm 3. Here, the worklist contains all the variables whose value needs to be updated.

**Theorem 2.13.** *If* C *is finite and L satisfies the ascending chain condition, then Algorithm 3 computes the least solution of* C*.*

*Proof.* We prove the claim as in the proof of [130, Lemma 6.4] in three steps:

**Algorithm 3:** A variable-oriented variant of Algorithm 2

```
Input: a finite monotone constraint system C
  Result: the least solution of C
1 foreach x≥t ∈ C do
2 A(x) ← ⊥
3 W ← W ∪ {x}
4 while W ≠ ∅ do
5 x ← remove(W)
6 old ← A(x)
7 forall x≥t ∈ C do
8 A(x) ← A(x) ⊔ ⟦t⟧(A)
9 if A(x) ≠ old then
10 foreach x
                   ′ ≥ t
                       ′ ∈ C such that x ∈ FV(t
                                          ′
                                           ) do
11 W ← W ∪ {x
                         ′
                         }
12 return A
```

$$\forall i \in \mathbb{N}. \; \mathcal{R}\_i \le lfp(F\_C)$$

3. Upon termination, we have

$$\mathcal{A} \ge lfp(F\_C)$$

For the first two steps, we refer to the proof of [130, Lemma 6.4]. For the third step, we prove that the loop in lines 4–11 maintains the following invariant:

$$(\mathbf{Inv}) \,\,\forall \mathbf{x} \ge t \in \mathcal{C}. \,\, x \notin \mathcal{W} \implies \mathcal{A}(\mathbf{x}) \ge \|t\|(\mathcal{A}).$$

First we note that this invariant proves our claim: Upon termination, the worklist is empty. The invariant **(Inv)** implies then that every constraint is satisfied.

Next we show that **(Inv)** holds before the first loop iteration. But this is clear since after the initialization loop in lines 1–3 has finished, every variable occurring on the left-hand side of a constraint in C is contained in *W*. Hence, the premise of **(Inv)** is false, so **(Inv)** holds before the first loop iteration.

Next we show that **(Inv)** is preserved by each iteration of the loop in lines 4–11. So, consider the *i*-th loop iteration. Let A*Old* and *WOld* be the values of A and *W* at the beginning and A*New* and *WNew* be the values of A and *W* at the end of the *i*-th iteration, respectively. We assume that **(Inv)** holds at the beginning of the *i*-th iteration and show that it still holds at the end. So, consider any constraint *<sup>x</sup>*≥*<sup>t</sup>* ∈ C with *<sup>x</sup>* <sup>∉</sup> *<sup>W</sup>New*. We distinguish two cases:

1. *<sup>x</sup>* <sup>∉</sup> *<sup>W</sup>Old*: Then *<sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* was satisfied at the beginning of the *<sup>i</sup>*-th loop iteration and *x* cannot have been removed in this iteration, which means that A(*x*) is not touched. Moreover, no variable from *FV*(*t*) can have been touched in this iteration: Otherwise, line 11 would have been executed and *x* ∈ *WNew*. Hence, *x* ≥ *t* is still satisfied at the end of the iteration.

2. *<sup>x</sup>* <sup>∈</sup> *<sup>W</sup>Old*: Since *<sup>x</sup>* <sup>∉</sup> *<sup>W</sup>New*, *<sup>x</sup>* must be the variable which is processed in the *i*-th iteration. Then, at some point, line 8 is executed for *x* ≥ *t*, so that *x* ≥ *t* is satisfied afterwards. Now we make three observations that together allow us to conclude that *x* ≥ *t* is still satisfied at the end of the *i*-th iteration: First, we see that line 8 is the only place in the loop where A(*x*) is modified. Secondly, A is only modified for variable *x* and no other variable. Finally, we make the observation that no variable from *FV*(*t*) can have been touched in this iteration: If that were the case, then this would necessarily entail *x* ∈ *FV*(*t*). But then line 11 would be executed for *x* ≥ *t* and we would have *x* ∈ *WNew* (which we have not).

From these three observations, it follows that the *i*-th iteration only changes *Analysis*(*x*) and leaves ⟦*t*⟧(*Analysis*) unchanged. Together with the fact that line 8 changes *Analysis*(*x*) upwards, we can conclude that *x* ≥ *t* is still satisfied at the end of the *i*-th iteration.

This concludes the proof that **(Inv)** is preserved by the loop in lines 4–11. □

# **2.3 Inductive Definitions**

In later chapters, I will use inductive definitions at various places. Furthermore, I will use induction principles derived from the respective definition. In this section, I make these notions precise by applying the theoretic foundations compiled in section 2.2. A general version of the following definitions and results can be found in the literature [7].

To improve presentation, I abbreviate (*x*<sup>1</sup> , . . . , *xn*) ∈ *B <sup>n</sup>* as **<sup>x</sup>** <sup>∈</sup> *<sup>B</sup> n* , for a given set *B* and *n* ∈ **N**. Moreover, for **x** ∈ *B n* , I write **x***<sup>i</sup>* to denote the *i*-th component of **x**.

**Definition 2.14.** *Let X be an arbitrary set. An* operator *(on X) is a partial function f* : *X <sup>n</sup>* <sup>→</sup> *<sup>X</sup> for some <sup>n</sup>* <sup>∈</sup> **<sup>N</sup>***, which is also called the* arity *of <sup>f</sup> and which is written as ar*(*f*)*.*

**Definition 2.15.** *For a set* F *of operators (on a set X), I say that A* ⊆ *X* is closed under F *if*

$$(2.13)\qquad \forall f \in \mathcal{F}. \,\forall \underline{\mathbf{x}} \in \mathcal{X}^{ar(f)}. \,\underline{\mathbf{x}} \in A^{ar(f)} \cap dom(f) \implies f(\underline{\mathbf{x}}) \in A^{ar(f)}$$

Usually, I will specify F by giving a list of closure properties of the form (2.13).

Mostly for layout reasons, I express (2.13) in the following way:

$$(2.14) \qquad \qquad \frac{\underline{\mathbf{x}}\_1 \in A \quad \dots \quad \underline{\mathbf{x}}\_{ar(f)} \in A \quad \underline{\mathbf{x}} \in dom(f)}{f(\underline{\mathbf{x}}) \in A}$$

Occasionally, I will omit the "∈ *A*"'s if they are clear from the context. I will also omit other assertions that restrict elements to membership in a given set if these assertions do not restrict the elements more than other assertions. I will also omit quantifiers and assume that all free variables are universally quantified.

Using (2.13) or (2.14) not only specifies an operator set on *X* but also a canonical subset of *X*. I introduce this set in the following.

**Definition 2.16.** *Let* F *be a set of operators on the set X. I say that A* ⊆ *X is inductively defined by* F *if*


**Remark 2.17.** *For every set X and every set* F *of operators on X, there is exactly one subset A* ⊆ *X that is inductively defined by* F *.*

*Proof.* Define *A* by

$$\text{(2.17)}\qquad\qquad A \stackrel{def}{=} \bigcap \{ B \subseteq X \mid B \text{ is closed under } \mathcal{F} \}$$

Then it can be easily seen that *A* satisfies the two conditions in Definition 2.16. Now let *A*<sup>1</sup> ⊆ *X* and *A*<sup>2</sup> ⊆ *X* be two subsets of *X* that are inductively defined by F . Then by (2.15), both *A*<sup>1</sup> and *A*<sup>2</sup> are closed under F . Hence, by (2.16), we have *A*<sup>1</sup> ⊆ *A*<sup>2</sup> and *A*<sup>2</sup> ⊆ *A*<sup>1</sup> . Due to anti-symmetry of <sup>⊆</sup>, it follows that *<sup>A</sup>*<sup>1</sup> <sup>=</sup> *<sup>A</sup>*2. □

Because of (2.16), one can also say that the set that is inductively defined by F is *the least subset that is closed under* F

For an inductively defined set *A* ⊆ *X*, the following proof principle can be applied.

**Theorem 2.18.** *Let A* ⊆ *X be inductively defined by* F *and let P* : *X* → {*true*, *f alse*} *be a statement about elements of X. Suppose that we can show*

$$(2.18) \qquad \forall f \in \mathcal{F}. \,\forall \underline{\mathbf{x}} \in \mathbf{X}^{ar(f)}. \,\underline{\mathbf{x}} \in dom(f) \land \bigwedge\_{i=1}^{ar(f)} P(\underline{\mathbf{x}}\_i) \implies P(f(\underline{\mathbf{x}})).$$

*Then* ∀*a* ∈ *A*. *P*(*a*)*.*

*Proof.* Define *C<sup>f</sup>* ,*C*<sup>F</sup> : 2 *<sup>X</sup>* <sup>→</sup> <sup>2</sup> *<sup>X</sup>* by

$$\text{(2.19)}\qquad\qquad\mathsf{C}\_{f}(\mathsf{B})\stackrel{def}{=}\{f(\underline{\mathbf{x}})\mid\underline{\mathbf{x}}\in\mathsf{B}^{ar(f)}\cap\operatorname{dom}(f)\}$$

$$\text{(2.20)}\qquad\qquad\mathsf{C}\_{\mathcal{F}}(\mathsf{B}) \stackrel{\text{def}}{=} \bigcup\_{f \in \mathcal{F}} \mathsf{C}\_{f}(\mathsf{B}).$$

Then it is easy to see that

1. *<sup>C</sup>*<sup>F</sup> is a monotone function on the complete lattice (<sup>2</sup> *<sup>X</sup>*,⊆).

2. The subsets of *X* that are closed under F are exactly the reductive points of *C*<sup>F</sup> .

From Theorem 2.1, we know that

$$\operatorname{lfp}(\mathsf{C}\_{\mathcal{F}}) = \bigcap \operatorname{Red}(\mathsf{C}\_{\mathcal{F}}) .$$

Hence, *l f p*(*C*<sup>F</sup> ) is the least subset of *<sup>X</sup>* that is closed under <sup>F</sup> . Another basic observation is that for all A ⊆ 2 *<sup>X</sup>* we have

$$\mathcal{C}\_{\mathcal{F}}(\bigcup \mathcal{A}) = \bigcup\_{A \in \mathcal{A}} \mathcal{C}\_{\mathcal{F}}(A)$$

In particular, *C*<sup>F</sup> is a continuous function on the CCPO <sup>2</sup> *<sup>X</sup>*. Hence, we can use Lemma 2.4 to give a proof for Theorem 2.18. Abbreviate *l f p*(*C*<sup>F</sup> ) as *<sup>A</sup>* and define

$$\mathcal{P} \stackrel{def}{=} \{ \mathcal{B} \subseteq A \mid \forall b \in \mathcal{B}. P(b). \}$$

Then we need to show *A* ∈ P. By Lemma 2.4, it suffices to show

(2.21) ∅ ∈ P

$$\text{(2.22)}\qquad\qquad\forall\mathcal{B}\subseteq\mathtt{2}^{X}.\mathcal{B}\subseteq\mathcal{P}\implies\bigcup\mathcal{B}\in\mathcal{P}$$

$$\text{(2.23)}\qquad\qquad\forall\mathcal{B}\subseteq\mathcal{X}.\mathcal{B}\in\mathcal{P}\quad\implies\mathcal{C}\_{\mathcal{F}}(\mathcal{B})\in\mathcal{P}$$

The first two properties are easy to see. Now consider (2.23). Let *B* ∈ P. We need to show *<sup>C</sup>*<sup>F</sup> (*B*) ∈ P, that is

$$\bigcup\_{f \in \mathcal{F}} \{ f(\underline{\mathbf{x}}) \mid \underline{\mathbf{x}} \in \mathcal{B}^{ar(f)} \cap dom(f) \} \in \mathcal{P}^r$$

By (2.22), it suffices to show that

$$\{ f(\underline{\mathbf{x}}) \mid \underline{\mathbf{x}} \in \mathcal{B}^{ar(f)} \cap dom(f) \} \in \mathcal{P}^r$$

for all *f* ∈ F . For this, it is sufficient to show

$$\forall f \in \mathcal{F}. \,\forall \underline{\mathbf{x}} \in \mathcal{B}^{ar(f)} \cap dom(f). \, P(f(\underline{\mathbf{x}})) $$

So let *f* ∈ F and **x** ∈ *B ar*(*f*) <sup>∩</sup> *dom*(*f*). From *<sup>B</sup>* ∈ P we can derive

$$\bigwedge\_{i=1}^{ar(f)} P(\mathbf{x}\_i)$$

and by (2.18),

$$P(f(\underline{\mathbf{x}})) \in \mathcal{B}\_{\prime}$$

follows, as desired. □

In this thesis, I will at several points consider a set *A* that is defined in some non-inductive way. Then, I will specify a set F of operators and propose that *A* is inductively defined by F . In order to show this, by Remark 2.17, I only need to show that *A* is the least set that is closed under F . The following theorem formalizes this argument.

**Theorem 2.19.** *Let X be a set,* F *a set of operators on X and A* ⊆ *X. Furthermore, let X*<sup>0</sup> ⊆ *X be the least subset of X that is closed under* F *. In order to show that A is inductively defined by* F *, it su*ffi*ces to show the following two statements.*

*A is closed under* F .(2.24)

*A* ⊆ *X*0*, where X*<sup>0</sup> *is the least subset of X that is closed under* F .(2.25)

*Proof.* (2.24) is exactly the same as (2.15). (2.25) is a reformulation of (2.16) with regards to the representation (2.17). Hence, (2.24) and (2.25) indeed imply together that *A* is inductively defined by F according to Definition 2.16. □

# **2.4 Symbol Sequences**

Let *E* be a finite set. I will refer to *E* as *alphabet* and to the elements of *E* as *letters*, or *symbols*, respectively.

$$E^n \stackrel{def}{=} \prod\_{i=1}^n E$$

is the set of sequences with items in *E* and length *n*. In particular, *E* 0 contains exactly one element ϵ that I also call *the empty sequence*.

$$E^{\star} \stackrel{def}{=} \bigcup\_{i \ge 0} E^i$$

is the set of all sequences with items in *E*.

I will use some abbreviating notations: I will use an interval notation for ranges of integers. For instance [*i*, *j*] ⊆ **N** is meant to be the set of non-negative integers which are ≥ *i* and ≤ *j*, the notation for open and half open intervals ]*i*, *j*[, [*i*, *j*[ and ]*i*, *j*] have respective meaning. For a sequence π ∈ *E* <sup>⋆</sup> of symbols from *E*, π *<sup>i</sup>* and <sup>|</sup>π<sup>|</sup> denote the *<sup>i</sup>*-th item in <sup>π</sup> and the length of π, respectively. Moreover, I define π [*i*,*j*] , π [*i*,*j*[ , π ]*i*,*j*] and π ]*i*,*j*[ as the sub-sequence of π that is obtained by taking only the items of the respective intervals. So, for example, if π = π 0 · . . . · π *n*−1 , then π [*i*,*j*] = π*<sup>i</sup>* · . . . · π*<sup>j</sup>* . π <*i* is short for π [0..*i*[ , π >*i* is short for π ]*i*,|π|−1] , π <sup>≤</sup>*<sup>i</sup>* and π <sup>≥</sup>*<sup>i</sup>* are defined analogously. Generally, I consider π ′ a sub-sequence of π if and only if π ′ = π *I* for some interval *I* ⊆ *range*(π). Moreover, although I will mostly treat π *<sup>I</sup>* as a sequence of its own right, I consider it to also implicitly contain the interval *I* and the sequence π from which it was extracted. This avoids ambiguities for cases in which there are multiple occurrences of a sub-sequence in a sequence. For instance, for π = *abab*, the sub-sequence *ab* could be represented as both π [0,1] and π [2,3] . By also incorporating the interval, I specify which occurrence of *ab* I mean. Both π and *I* will be clear from the context, unless stated otherwise. For a sub-sequence π ′ = π *<sup>I</sup>* of π, I denote with *range*π(π ′ ) := *I* the range of indices which need to be selected from π to obtain π ′ . If I omit the index, then I mean the full range of π, so *range*(π) = {0, ...|π| − 1}.

Lastly, *Pre f ixes*(π) := {π <*i* | *i* ∈ *range*(π)} is the set of all prefixes of π.

Sequences can be concatenated. For π ∈ *E <sup>m</sup>*, π ′ ∈ *E n* , π ′′ *de f* = π · π ′ is the sequence of length *m* + *n* with π ′′<|π<sup>|</sup> = π and π ′′≥|π<sup>|</sup> = π ′ . This defines a binary operation on *E* <sup>⋆</sup>, i.e. a function

$$\cdot : E^{\star} \times E^{\star} \to E^{\star} \text{ .}$$

This operation is associative, that is (π<sup>1</sup> · π2) · π<sup>3</sup> = π<sup>1</sup> · (π<sup>2</sup> · π3) and has ϵ as neutral element, that is we have π · ϵ = ϵ · π = π. Moreover, for every π ∈ *E* <sup>⋆</sup>, if we split *range*(π) into adjacent intervals *I*<sup>1</sup> , . . . , *I<sup>k</sup>* that only have endpoints in common, then we can write

$$
\pi = \pi^{I\_1} \cdot \dots \cdot \pi^{I\_k} \cdot
$$

Specifically, by considering each symbol in a sequence π as a sequence of its own right, then we can write

$$
\pi = \pi^0 \cdot \ldots \cdot \pi^{|\pi|-1}
$$

# **2.5 Directed Graphs**

The following definitions are standard and can be found in any text book about graph theory [44, 55].

**Definition 2.20.** *A* directed graph *is a pair G* = (*N*, *E*) *where N is a set of* nodes *and E* ⊆ *N* × *N is a binary relation over N whose elements are called* edges*.*

**Definition 2.21** (edge labels)**.** *An edge-labeled directed graph is a tuple G* = (*N*, *E*, *L*, *l*) *such that* (*N*, *E*) *is a directed graph and*

$$l: E \to L$$

*is a function from edges to a finite, non-empty set L of* labels*. I assume that L always contains the empty label* τ*.*

For *e* ∈ *E*, I will write *n <sup>e</sup>*<sup>→</sup> *<sup>n</sup>* ′ if *e* = (*n*, *n* ′ ) or, for labeled edges, if ∃*l*.(*n*, *l*, *n* ′ ) ∈ *E*. Given a (labeled) edge *n <sup>e</sup>*<sup>→</sup> *<sup>n</sup>* ′ , *src*(*e*) = *n* and *tgt*(*e*) = *n* ′ denote *the source* and *target* of *e*, respectively.

For a given sequence π ∈ *E* <sup>⋆</sup>, π ≠ ϵ, I define *start* and *end* as follows:

$$\begin{aligned} \text{(2.26)} \end{aligned} \qquad \text{start}(\pi) = \text{src}(\pi^0)$$

$$\text{(2.27)}\qquad\qquad\qquad\qquad\text{end}\qquad\qquad\text{end}(\pi)=\text{tg}(\pi^{|\pi|-1})\qquad\qquad\qquad\qquad\text{(2.27)}$$

A *path* is an edge sequence π ∈ *E* <sup>⋆</sup> with the property

$$\forall 1 \le i \le |\pi| - 1. \operatorname{src}(\pi^i) = t \operatorname{gt}(\pi^{i-1}).$$

We can characterize the paths in *G* as follows.

The set *Paths<sup>G</sup>* ⊆ *N* × *N* × *E* <sup>⋆</sup> is inductively defined by the following rules:

$$\begin{aligned} \left( \begin{array}{c} \mathsf{PATH-EMPT} \\ \hline \end{array} \right) & \xrightarrow{-} \begin{array}{c} \mathsf{P} \\ \left( \begin{array}{c} \mathsf{s} , \mathsf{s} , \mathsf{e} \end{array} \right) & \xrightarrow{-} \begin{array}{c} \mathsf{P} \\ \end{array} \\\\ \left( \begin{array}{c} \mathsf{PATH-EETEN} \\ \end{array} \right) & \xrightarrow{\left( \begin{array}{c} \mathsf{s} , \mathsf{t} , \mathsf{n} \right) \in \mathsf{P} \mathsf{aths}\_{G} \\ \end{array} & \xrightarrow{\left( \begin{array}{c} \mathsf{s} , \mathsf{t} \\ \end{array} \right) & \xrightarrow{\left( \begin{array}{c} \mathsf{s} \\ \end{array} \right) & \xrightarrow{} \mathsf{P} \mathsf{aths}\_{G} \\ \end{array} \end{aligned} $$

Instead of (*s*, *t*, π) ∈ *PathsG*, I also write π ∈ *PathsG*(*s*, *t*) and refer to *Paths<sup>G</sup>* as function *N* × *N* → 2 *E* ⋆ .

It is easy to see that *Paths<sup>G</sup>* relates the pairs (*s*,*t*) ∈ *N* × *N* to the paths that start in *s* and end in *t*, as formalized in Lemma 2.22. Occasionally, I will consider *Paths<sup>G</sup>* not only as a relation but also as a subset of *E* <sup>⋆</sup> by identifying it with ⋃︁ *<sup>s</sup>*,*t*∈*N*{π ∈ *E* <sup>⋆</sup> <sup>|</sup> (*s*,*t*, <sup>π</sup>) <sup>∈</sup> *PathsG*}. It will be clear from context, which of the two I mean.

**Lemma 2.22.** *For all* π ∈ *E* <sup>⋆</sup> *and all <sup>s</sup>*,*<sup>t</sup>* <sup>∈</sup> *<sup>N</sup> the following two statements are equivalent:*

(1) (*s*, *t*, π) ∈ *Paths<sup>G</sup>*

(2) π *is a path and* (*s* = *t* ∧ π = ϵ ∨ *start*(π) = *s* ∧ *end*(π) = *t*)

*Proof.* " =⇒ " can be seen by induction on (*s*,*t*, π) ∈ *PathsG*, " ⇐= " can be shown by induction on the length of sequences in *E* <sup>⋆</sup>. □

Lastly, I state three elementary properties of paths that I will make use of later.

**Lemma 2.23.** *If* π ∈ *PathsG*(*s*,*t*) *and* π ′ ∈ *PathsG*(*t*,*t* ′ )*, then* π · π ′ ∈ *PathsG*(*s*, *t* ′ )*.*

*Proof.* Fix *s*, *t* ∈ *N* and π ∈ *PathsG*(*s*, *t*). Then we can show

∀π ′ ∈ *E* <sup>⋆</sup>. <sup>∀</sup>*<sup>t</sup>* ′ ∈ *N*. π ′ ∈ *PathsG*(*t*, *t* ′ ) =⇒ π · π ′ ∈ *PathsG*(*s*, *t* ′ )

by induction on the length of π ′ . □

**Lemma 2.24.** *For all* π, π ′ ∈ *E* <sup>⋆</sup>*, the following statement holds: If* π · π ′ ∈ *PathsG*(*s*,*t*)*, then there is t* ′ ∈ *N such that* π ∈ *PathsG*(*s*,*t* ′ ) *and* π ′ ∈ *PathsG*(*t* ′ , *t*)*.*

36

*Proof.* Fix π ′ ∈ *E* <sup>⋆</sup> and *<sup>t</sup>* <sup>∈</sup> *<sup>N</sup>*. Then we can show

$$\begin{aligned} \forall \pi \in E^\star. \forall s \in \mathcal{N}. \pi \cdot \pi' \in \text{Paths}\_G(s, t) \\ \implies \exists t' \in \mathcal{N}. \pi \in \text{Paths}\_G(s, t') \land \pi' \in \text{Paths}\_G(t', t) \end{aligned}$$

by induction on the length of π. □

**Remark 2.25.** *If* π *is a path and i*, *j* ∈ *range*(π)*, then* π [*i*,*j*] *is a path.*

*Proof.* This is clear by definition. □

*My independence seems to vanish in the haze.*

# <sup>T</sup>he <sup>B</sup>eatles **3**

# **Program Dependence Graphs for Object-Oriented Programs**

This chapter introduces the reader to common terminology and patterns of thought used in static program analysis. Moreover, it describes several techniques that enable Joana to analyze security properties of Java programs. This prepares the reader for chapter 4, which shows several applications of the information flow control tool Joana to software security. This chapter plays another important role: It describes two fundamental and widely used static program analysis techniques, namely *data-flow analysis on control-flow graphs* and *slicing on program dependence graphs*. These two techniques, which were developed more or less independently, face similar issues that have been solved with analogous approaches. In later chapters, I will present a common generalization of data-flow analysis and slicing that combines the strengths of both techniques.

The following sections are structured as follows. In section 3.1, I give an overview of basic aspects of static program analysis and introduce several central concepts that play important roles throughout this chapter. After that, section 3.2 and section 3.3 present data-flow analysis on control-flow graphs and slicing on program dependence graphs, respectively.

Finally, in section 3.4 I describe Joana and show how it can be used to verify non-interference for Java programs. In this last section, I put an emphasis on the explanation of several techniques that are particularly important for the analysis of the programming language features of Java and also play a role in chapter 4.

# **3.1 Principles of Static Program Analysis**

Program Analysis is concerned with techniques that allow to derive information about a given program and its properties. Roughly, program analysis techniques can be grouped into dynamic techniques and static techniques. Dynamic techniques generate information while executing the program [75], while static techniques aim to analyze a program without executing it [130, 76].

Another criterion that can be used to classify program analyses is whether they are automatic or not. As the name suggests, an automatic program analysis is usually another program that takes a given program as input, performs some algorithm on it and outputs its analysis result. In contrast, non-automatic techniques are either fully manual or *interactive*, that is they generally employ automatic techniques but query the user if they cannot complete their task in an automatic fashion.

In this thesis, I focus on automatic and static program analysis techniques such as *data-flow analysis* [101] and *static program slicing* [165, 166, 58]. In the following, I assume that all analyses are static and automatic, unless explicitly stated otherwise. Furthermore, I assume that the programs under analysis are written in a Turing-complete language.

Two important concepts in program analysis are *soundness* and *precision*<sup>4</sup> . Before I explain these two concepts, I first introduce some formalisms: Let ϕ be a property of programs, that is a given program P either can satisfy ϕ, written P |= ϕ, or not, written P ̸|= ϕ. The property ϕ can for example make a statement about P's semantics, i.e. about how P transforms states or can state that P is secure in some sense.

Now let A be a program analysis whose goal is to analyze programs with respect to ϕ. Formally, we can imagine A as a function that takes a program P as input and outputs either "P satisfies ϕ" or "P does not satisfy <sup>ϕ</sup>". We write the former as P ⊢<sup>A</sup> <sup>ϕ</sup> and the latter as <sup>P</sup> <sup>⊬</sup><sup>A</sup> <sup>ϕ</sup>.

Note that the result of A a priori has nothing to do with whether P actually satisfies ϕ or not. It is merely a consequence of the formal reasoning performed by A.

The notions of soundness and precision establish a connection between |= and ⊢A:

<sup>4</sup> In the area of formal systems, precision is also called *completeness*.

1. A is said to be *sound* with respect to ϕ if

$$
\mathcal{P} \models\_{\mathcal{R}} \phi \implies \mathcal{P} \models \phi.
$$

That is, if A concludes that P satisfies ϕ, then this is indeed the case.

2. A is said to be *complete* with respect to ϕ if

$$
\mathcal{P} \vdash \phi \implies \mathcal{P} \vdash\_{\mathcal{A}} \phi.
$$

That is, if P has property ϕ, then A is able to derive that.

If A is sound, then it can give the guarantee that a program actually satisfies a property. If it is additionally complete, then it is actually able to *decide* ϕ. Unfortunately, there can be no static automatic analysis for a non-trivial property of programs written in a Turing-complete programming language that is both sound and complete [142]. Hence, analysis designers must make compromises, i.e. analyses usually are usually not sound or not complete (or neither of the two).

In the context of static program analysis, completeness is commonly also referred to as *precision*. Throughout this thesis, I use both terms interchangeably.

It is common to focus on soundness and sacrifice precision, especially in security analyses where one aims to give strong guarantees for programs. However, if an analysis is not precise, then it may raise *false alarms*, e.g. report that a program is insecure although it is not. This can undermine the credibility of an analysis.

Hence, another goal of program analysts is to minimize false alarms, i.e. make their analyses *as precise as possible*. Note that, although precision in theory is a binary property that can be either true or false, in this thesis I use it as a property that has multiple degrees, so that it can also be used comparatively or even quantified. Given two analyses A<sup>1</sup> and A2, one can say that A<sup>1</sup> is at least as precise as A<sup>2</sup> if

$$\langle \mathcal{P} \mid \mathcal{P} \vdash \phi \land \mathcal{P} \models \mathcal{A}\_1 \; \phi \rangle \subseteq \langle \mathcal{P} \mid \mathcal{P} \vdash \phi \land \mathcal{P} \models \mathcal{A}\_2 \; \phi \rangle.$$

This defines a partial order on the set of analyses that can be used to compare different analyses.

There are different trade-offs that can be made with respect to analysis precision. In the following, I give an overview of a selection of them

```
int z = input();
x = 2;
if (z * z + z + 1 == 0) {
  x = 3;
}
             (a)
                                      int z = input();
                                      x = 2;
                                      if (z * z - 9 == 0) {
                                         x = 3;
                                      }
                                                   (b)
```
**Figure 3.1:** Example for the impact of value-sensitivity

that play some role in this thesis. For illustration, I will use the example property *upon program termination, variable x always has the value 2* that I write as γ*x*.

One trade-off static analyses can make is how precisely they handle values, for example numbers and algebraic identities.

Consider the two programs in Figure 3.1. Since the quadratic function *z* <sup>2</sup> + *z* + 1 does not have any integral zeros, it is relatively easy to see that x is always 2 upon termination of the program in Figure 3.1a, hence γ*x* holds for it. However, γ*x* is not satisfied by the program in Figure 3.1b, since *x* is set to 3 if *z* = 3. A program analysis that is sound with respect to γ*x* but does not reason about algebraic identities will fail to verify γ*x* for the program on the left. There are analysis techniques that can deal with such challenges[128], but the techniques that I consider in this thesis usually do not reason about values beyond the use of limited constant propagation [126].

Another aspect of analysis precision is *flow-sensitivity*, a property that refers to the ability of an analysis to take the order of statements into account.

As an example, it is clearly the case that the program P<sup>1</sup> in Figure 3.2a satisfies γ*x*, while program P<sup>2</sup> in Figure 3.2b does not. Now consider an analysis A that is sound with respect to γ*x*. Then it must be the case that <sup>P</sup><sup>2</sup> <sup>⊬</sup><sup>A</sup> <sup>γ</sup>*x*, because <sup>P</sup><sup>2</sup> ̸|<sup>=</sup> <sup>γ</sup>*x*. Now, if <sup>A</sup> is flow-insensitive, then it usually yields the same result for P<sup>1</sup> , as they only differ in the order of statements. Hence, a flow-insensitive analysis is usually not able to verify γ*<sup>x</sup>* for P<sup>1</sup> . In contrast, it may be the case that a flow-sensitive analysis may consider P<sup>1</sup> and P<sup>2</sup> as different programs.

*Context-sensitive* program analyses consider not only the program statements, but also take their *execution context* into account. Examples for the execution context of a statement include the site from which a procedure

```
x = 3
x = 2
  (a)
               x = 2
               x = 3
                 (b)
                                void main() {
                                  y = f(3);
                                  x = f(2);
                                }
                                void f(int a) {
                                  return a;
                                }
                                      (c)
                                                          void main() {
                                                            x = f(2);
                                                            x = f(3);
                                                          }
                                                          void f(int a) {
                                                            return a;
                                                          }
                                                                (d)
```
**Figure 3.2:** Illustration of different sensitivities: Programs a and b cannot be distinguished by a flow-insensitive analysis, programs c and d cannot be distinguished by a context-insensitive analysis

was called (in programs with multiple procedures), or the object on which a method is called (in object-oriented programs).

For example, it clearly can be seen that the program in Figure 3.2c satisfies γ*x*, while the program in Figure 3.2d does not. However, a contextinsensitive program analysis that is sound with respect to γ*x* cannot verify Figure 3.2c: It has to "merge" the calls in lines 2 and 3 and deem it possible that f may also return 3, otherwise it would not be able to reject the program in Figure 3.2d.

# **3.2 Data-Flow Analysis on Control-Flow Graphs**

In this section, I introduce *control-flow graphs*, a classical data structure of static analysis and *data-flow analysis*, which is an important, generic operation on control-flow graphs that forms the essence of many static program analyses.

This section is structured as follows: First, I introduce control-flow graphs in subsection 3.2.1. After that, I explain how data-flow analysis works in subsection 3.2.2.

The two subsections are structured analogously: First, they consider programs without procedures and then show how the respective formalism can be extended to the interprocedural case.

**Figure 3.3:** An example program and its control-flow graph

# **3.2.1 Control-Flow Graphs**

Control-flow graphs are a classical program representation on which many program analyses operate. A control-flow graph of a given program P is a directed graph that represents the possible control-flow between P's statements and predicates.

# **3.2.1.1 Intraprocedural Control-Flow Graphs**

An example of a simple program and its control-flow graph can be seen in Figure 3.3. Its nodes represent the program's statements and predicates. Directed edges connect nodes with those nodes which may immediately follow them in an execution.

There are two kinds of control-flow. The control-flow from line 1 to line 2 is unconditional: After the read operation has been executed, control is always transferred to the following **if** statement.

The control-flow from line 2 to line 5, however, is conditional. Line 5 is only executed if the predicate in line 2 is evaluated to **true**. If it is evaluated to **false**, line 3 is executed instead.

It is also a common assumption that control-flow graphs have a unique entry node from which all nodes in the control-flow graph are reachable and a unique exit node which can be reached by all nodes. Next, I introduce control-flow graphs more formally.

**Definition 3.1** (control-flow graph, [65, based on Definition 2.1])**.** *Given a procedure (or procedure-less program)* P*, a control-flow graph (CFG) is an edge-labeled directed graph with two distinguished nodes s*,*e* ∈ *N, written G* = (*N*, *E*,*s*,*e*, *L*, *l*)*, with the following properties:*


The labels are used to model conditional flows. As they do not play an important role in this thesis, I will mostly ignore them in the following. For intraprocedural graphs that are considered in isolation, a classical assumption is that for all *n* ∈ *N* there is a path which starts at *s* and ends at *n*.

In the literature there is some variation in the concrete representation of control-flow graphs. Some authors [12, 106, 130] use a *node-oriented* notation, in which the nodes of the control-flow graph represent the statements, while others [46, 153, 127] use an *edge-oriented* notation, in which the edges are annotated with the statements. Also, there are some differences among the node-oriented notations: some of them use a different node for each statement, while others use nodes for each *basic block*, which are linear chains of statements.

I use a node-oriented notation of control-flow graphs in this thesis. Moreover, I will only consider basic blocks with one statement, unless I deviate from this convention explicitly.

# **3.2.1.2 Interprocedural Control-Flow Graphs**

For programs with multiple procedures, control-flow graphs have to be adapted in order to model the procedure calls properly. Again, there are several notions in the literature, which differ slightly. In most of them, interprocedural control-flow graphs are families of intraprocedural control-flow graphs for each procedure. They mainly differ in the exact way in which they model calls and whether the procedural control-flow graphs are connected or not.

My definition follows notations used by De Sutter et al. [51] and Hammer [86].

**Definition 3.2.** *Let Proc be a finite set of procedures with main* ∈ *P. An interprocedural control-flow graph (ICFG) is a quadruple*

$$G = ((G\_p)\_{p \in \operatorname{Proc} \prime} E\_{\operatorname{call} \prime} E\_{\operatorname{ret} \prime} \Phi),$$


Figure 3.4 shows how procedure calls can be modeled: Each call is represented by two nodes, one for the call itself and one for the point just after the call to which the called procedure returns. I call this point the

**Figure 3.4:** Additional control-flow structure for procedure calls

*return site*. A *call edge ecall* connects the call node with the entry node of the callee and a *return edge eret* connects the exit node of the callee to the return site. These two edges correspond to each other, i.e. Φ(*ecall*) = *eret*.

# **3.2.2 Data-Flow Analysis**

In this subsection, I introduce *data-flow analysis*, a classical static program analysis technique. My presentation roughly follows textbook literature [152, 130] and classic articles [102, 82, 101, 46] on the topic.

Roughly, data-flow analysis gathers information about the possible executions of a program. This information can then be used in subsequent analyses and program transformations.

Before I introduce data-flow analysis in the following, I want to consider the paths of control-flow graphs more closely.

A usual assumption about control-flow graphs, which I also make in this thesis, is that they are sound. This means that, given the control-flow graph *G* of a program P, every execution of P is represented by a path in *G*. Note however, that this representation is not always exact. For example, a common simplifying assumption is that all outgoing edges of a given node might be taken by some program execution, regardless of the path that an execution had taken before.

An example of what this means can be seen in Figure 3.5.

Assuming that statement S does not change the outcome of b it is clear that no program execution can take the path 1 → 2 → 6 → 7. Since 1 → 2 is only traversed if b is evaluated to **true** at 1 and S does not change the outcome of <sup>b</sup>, any execution that traverses 1 → 2 must traverse 6 → 9 after that. Analyses that take into account the histories of paths are also called *path-sensitive*. Several publications on path-sensitive analysis can be found in the literature [89, 54, 35, 49]. In this thesis, I will only consider path-*in*sensitive analyses.

# **3.2.2.1 Intraprocedural Data-Flow Analysis**

In the following, I concentrate on intraprocedural data-flow analyses that only consider a single procedure. The notions that I introduce however are also useful for interprocedural data-flow analysis, which I will describe in subsubsection 3.2.2.2.

A *data-flow framework* is a pair (*L*, *F*) that specifies the general structure of the data-flow analysis to be performed. The set *L* describes how information look like, while *F* is a set of functions on *L* which transform this information.

A data-flow analysis associates each node *n* in a given control-flow graph with some property of the control-flow paths from the graphs start node to *n*.

**Figure 3.5:** A code snippet and its control-flow graph

The elements of *L* are used to represent properties like "*a* is definitely 42", "the value of *b* is unknown" or "variable *c* has the value assigned to it in line 5". Such properties can be partially ordered with respect to the amount of information they provide. For example, if *x* represents the property "*a* is definitely 42" and *y* represents the property "*a* has an unknown value", then *x* can be considered to provide more information than *y*, which is formally expressed by *x* ≤ *y*. Because we want to propagate information along the paths of a given control-flow graph, we need a way to combine values coming from different paths. The value resulting from a combination of *x*, *y* ∈ *L* should provide at least as much information as the combined values but can provide no more information, in order to be safe. So a good candidate for such a combination is the *least upper bound* or *joins* of *x* and *y*, In order to have a well-defined structure it is customary to require *L* to have least upper bounds for arbitrary subsets *A* ⊆ *L*. In other words, *L* is assumed to be a *complete lattice* (see page 15).

The transfer functions are abstractions of the program's statements' effect on properties<sup>5</sup> . They are assumed to be *information-preserving*, that is, if *x* provides more information than *y*, then the same should hold for the transformed values. Formally, this means that transfer functions are *monotone*. Additionally, it is customary to require that *F* enjoys some closure properties: Usually, one assumes that *F* contains the identity function and is closed under composition and arbitrary joins. Since the set of monotone functions *L* → *L* has all these properties, *F* can in theory be assumed to contain all monotone functions. In practice however, this set is usually "too large" in the sense that it contains more functions than actually needed for the given data-flow analysis. Hence, one aims to find more precise descriptions of *F* that allow for effective or even efficient representations. Classically, a data-flow framework is thought to be independent of the control-flow graphs they work on. To actually perform a data-flow analysis on a given control-flow graph, a data-flow framework needs to be *instantiated*.

Hence, a *data-flow instance* is a quintuple (*L*, *F*, *G*, ρ, *init*) that adds to a data-flow framework (*L*, *F*) a control-flow graph *G* = (*N*, *E*,*s*,*e*) connects the two using (a) an *initial information init* ∈ *L* that represents the properties

<sup>5</sup>The area of *abstract interpretation* is concerned with the systematic derivation of transfer functions from program semantics [46].

which hold at *G*'s entry node *s* and (b) a function ρ : *E* → *F*. This function associates each edge<sup>6</sup> *e* with a transfer function ρ(*e*) that describes the effect of the statement *src*(*e*) on the properties in *L* and *init* ∈ *L*. Instead of ρ(*e*), I also write *fe*.

By induction, transfer functions can be extended to the paths of *G*:

$$\begin{aligned} f\_{\mathfrak{e}} &= id \\ f\_{\pi \cdot \mathfrak{e}} &= f\_{\mathfrak{e}} \circ f\_{\pi} \end{aligned}$$

Due to the closure properties of *F*, all *f*π are elements of *F*.

The functions *f*π describe how properties are transformed along controlflow paths. We are interested in the properties which hold at each node, no matter which path was taken. For this purpose, we take the least upper bound of all *f*<sup>π</sup> and apply this function the initial information *init*. The result of this operation is also called the *merge-over-all paths* solution

$$(\text{3.1}) \qquad \qquad MOP(n) = \bigsqcup\_{\pi \in \text{Paths}\_G(n)} f\_{\pi}(init)\_{\pi}$$

where *PathsG*(*n*) is the set of paths in *G* from *s* to *n*.

The function *MOP* is in some sense the ideal solution of the given dataflow analysis problem, so the goal of data-flow analysis is to compute *MOP*. Note that *MOP* cannot be computed directly using (3.1), since (3.1) potentially merges over infinitely many paths. Also, there are data-flow instances for which *MOP* is not computable at all [101]. However, there are sufficiently interesting and useful data-flow frameworks for which it is possible to compute a *safe over-approximation* of the corresponding *MOP*

<sup>6</sup> It may seem odd that in the formalism I use in this thesis, statements are represented by nodes but, in contrast, transfer functions are associated with edges. This "hybrid" variant of data-flow analyses, however, can also be found in the literature [82, 136], just like the "pure" variants that either associate both statements and transfer functions with nodes [102, 100, 10, 106] or edges [46, 153], respectively. All three variants appear to be equivalent. My decision is mostly for pragmatic reasons: Although I prefer to associate transfer functions with edges, I want to describe control-flow graphs and program dependence graphs uniformly and acknowledge that in previous work [58, 93], program dependence graphs are derived from control-flow graphs in which the nodes represent statements. Hence I inherit node-oriented control-flow graphs from these earlier presentations.

solution. A safe over-approximation is a function *A* : *N* → *L* that has the property

$$\forall \text{2.2} \qquad \forall n \in \mathbb{N}. A(n) \ge \text{MOP}(n).$$

This property is equivalent to

$$(\text{3.3}) \qquad \forall n \in \text{N. } \forall \pi \in \text{Paths}\_{\text{G}}(n). A(n) \ge f\_{\pi}(init). A$$

Property (3.3) says that for every *n* ∈ *N* and every π ∈ *PathsG*(*n*), *A*(*n*) can provide no more information than *f*π(*init*). This is safe in the sense that no piece of information coming from a path ending in *n* is left out of *A*(*n*). A program optimization (or any other program transformation) that solely relies on the information provided by *A* never changes a program's behavior, provided that the transfer functions make sure that the program semantics is abstracted faithfully.

For instance, suppose that a compiler aims to identify variables that always have the same values in order to substitute read accesses by the constant value. A possible data-flow analysis for this would then propagate along a control-flow path π whether the value of variable *x* stays the same on π (and the value itself, if applicable). Then *A*(*n*) says something about whether *x* always has the same value on *any* control-flow path ending in *n* and provides this value, if applicable. But then *A*(*n*) has to integrate the information of *all* paths ending in *n*. Otherwise, *A*(*n*) could express that *x*'s value is always the same up until *n* but ignores some of the paths where the value of *x* is indeed different, which means that *A*(*n*) makes an unsafely wrong statement about the program under analysis. Clearly, the optimization step that substitutes accesses to *x* with the value computed by *A* would result in a program that differs in behavior from the original program.

A trivial safe over-approximation of *MOP* is the function which returns ⊤ for every *n*. This is of course safe because ⊤ provides no information at all. For our example, *A*(*n*) = ⊤ would mean that variable *x* may assume different values during program execution, even if this is not the case. Clearly, this may also be a wrong statement about the program under analysis but this time it can be considered safe because *A*(*n*) provides no information that can be exploited by the subsequent optimization step.

This leads to a notion of *precision* specialized to data-flow analysis: For two safe over-approximations *A* and *B*, *A* is at least as precise as *B* if ∀*n* ∈ *N*. *A*(*n*) ≤ *B*(*n*). In other words, for every *n* ∈ *N*, *A* needs to provide at least as much information as *B*.

In general, one aims to obtain a safe over-approximation for *MOP* which provides as much information as possible. One way of obtaining such a solution is to solve the following system of *monotone constraints*:

# **Constraint System 3.1.**

$$A(s) \ge \text{init}$$
  $m \xrightarrow{\varepsilon} n \implies A(n) \ge f\_{\varepsilon}(A(m))$ 

The idea of this system is to build up *MOP* "edge by edge".

By grouping together constraints with the same left-hand side, we can transform this constraint system to a system with one constraint per node:

$$A(n) \ge F\_n((A(m))\_{m \in \mathcal{N}})\,.$$

Each *Fn* : *L* <sup>|</sup>*N*<sup>|</sup> <sup>→</sup> *<sup>L</sup>* is monotone.

The whole constraint system can be described by one constraint

$$(3.4)\tag{3.4}$$

$$A \ge F(A)$$

where

(3.5) *F* : (*N* → *L*) → (*N* → *L*)

$$F = (F\_n)\_{n \in \mathcal{N}}$$

$$(\text{3.7})\qquad\qquad\qquad F\_n(A) = \bigsqcup\_{m \stackrel{\mathcal{C}}{\Longrightarrow} n} f\_\mathcal{E}(A(m))$$

Since *L* is a complete lattice, *N* → *L* is, too. Moreover, *F* : (*N* → *L*) → (*N* → *L*) is monotone.

In order to solve the constraint system, we need to find a function *A* : *N* → *L* that satisfies (3.4).

The theory of complete lattices tells us that this is always possible: Theorem 2.1 implies that (3.4) has a unique least solution, that is a solution *A*<sup>0</sup>

$$\begin{array}{c} MOP(n4) = \underbrace{f\_3(f\_1(init))}\_{MFP(n4)} \sqcup \underbrace{f\_3(f\_2(init))}\_{f\_3(f\_2(init))},\\ MFP(n4) = f\_3(\underbrace{f\_1(init)}\_{MFP(n4)} \sqcup \underbrace{f\_3(f\_2(init))}\_{f\_3(f\_2(init))}),\end{array}$$

**Figure 3.6:** Illustration of the effect of non-distributivity on the difference between *MOP* and *MFP*, based on Constraint System 3.1

such that *A*<sup>0</sup> ≤ *A* for every solution *A* of (3.4). So, *A*<sup>0</sup> is the most precise solution of (3.4). In the context of data-flow analysis, *A*<sup>0</sup> is also referred to as *Minimal Fixpoint*, or *MFP* for short.

Furthermore, it can be shown that *A*<sup>0</sup> is a safe over-approximation of *MOP*:

$$A\_0 \ge MOP.$$

Moreover, Theorem 2.13 states that *A*<sup>0</sup> can be computed using Algorithm 3, provided that *L* satisfies the ascending chain condition (see page 16). Note however, that *MFP* in general does not coincide with *MOP*. This is only the case if all the transfer functions enjoy a property that is called *distributivity*: A function *f* : *L* → *L* is distributive, if

$$\forall A \subseteq L. f(\bigsqcup A) = \bigsqcup f(A)$$

Whereas *MOP* first applies all transfer functions along the different paths, *MFP* applies joins at each node, for results along the incoming edges. In non-distributive instances, it is impossible to "pull joins out of" functions, which prevents *MFP* from coinciding with *MOP*. This is illustrated in Figure 3.6.

As an example of intraprocedural data-flow analysis, I want to discuss *reaching definitions*. This is a standard data-flow analysis applied in compilers and can also be used to compute data dependencies (see paragraph 3.3.2.1.1).


**Figure 3.7:** The constraint system and its least solution for the reaching definition analysis applied to the example from Figure 3.3

Given a control-flow graph *G* = (*N*, *E*,*s*,*e*), a *definition* of a variable *x* is a node *n* ∈ *N* which represents an assignment statement *x* := *e*. Let *De f*(*x*) ⊆ *N* be the definitions of variable *x* and assume for simplicity that each statement can define at most one variable. A definition *n may reach* a node *n* ′ if there is a path from *n* to *n* ′ on which there is (apart from *n*) no further definition of the variable defined by *n*.

For *n* ∈ *N*, the *reaching definitions of n* are the definitions which may reach *n*.

The data-flow framework for reaching definitions consists of (2 *<sup>D</sup>*, *F*) where *D* ⊆ *N* is the set of definitions in *N*, partially ordered by ⊆. *F* consists of functions of the form <sup>λ</sup>*A*. (*<sup>A</sup>* <sup>−</sup> *<sup>K</sup>*) <sup>∪</sup> *<sup>G</sup>* where *<sup>K</sup>*, *<sup>G</sup>* <sup>⊆</sup> *<sup>D</sup>* are sets<sup>7</sup> of definitions. It can easily be verified that *F* contains the identity function and is closed under composition and joins.

Let *m <sup>e</sup>*<sup>→</sup> *<sup>n</sup>* be an edge in *<sup>G</sup>*. Then *<sup>m</sup>*'s effect on the reaching definitions can be described as follows: If *m* is not a definition, then every definition which reaches *m* also reaches *n*. Therefore, all definitions reaching *m* are propagated to *n*. If *m* is a definition of variable *x*, then every definition of some variable *x* ′ ≠ *x* is propagated to *n*, but since *m* (re-)defines *x*, all previously reaching definitions of *x* are deleted and replaced by *m*. Formally, the edge transfer functions *fe* are defined by

$$f\_{\mathcal{C}} = \begin{cases} \lambda A. \, A & \text{if } m \text{ does not define any variable } \mathbf{x} \\ \lambda A. \, (A - Def(\mathbf{x})) \cup \{m\} & \text{if } m \text{ is a definition of } \mathbf{x} \end{cases}$$

Since initially no variable is defined, the initial information *init* is ∅. For the example program from Figure 3.3, the monotone constraint system resulting from the corresponding data-flow instance and its least solution is shown in Figure 3.7.

# **3.2.2.2 Interprocedural Data-Flow Analysis**

Like intraprocedural data-flow analysis operates on intraprocedural control-flow graphs, interprocedural data-flow analysis operates on interprocedural control-flow graphs.

**3.2.2.2.1 Context Problem** Interprocedural control-flow graphs introduce a source of imprecision, which I already discussed in section 3.1. It is caused by a main benefit of having procedures in the first place, namely by the fact that procedures can be called from multiple call sites.

As an example, consider the program in Figure 3.8a. It contains a function f that is called from two call sites. Its control-flow graph is shown in Figure 3.8b. The graph contains for example the path π<sup>1</sup> : 1 → 2 → 7 → 8 → 9 → 3, which corresponds to the normal execution of the given

.

<sup>7</sup>Data-flow analyses where the transfer functions can be expressed in this way are also called gen/kill or bit vector analyses.

**Figure 3.8:** A small program with its interprocedural control-flow graph with Φ(2 → 7) = 9 → 3 and Φ(4 → 7) = 9 → 5

program. However, another path is π<sup>2</sup> : 1 → 2 → 7 → 8 → 9 → 5. This path does not correspond to any execution because it does not respect the semantics of procedure calls: When a procedure is called, the site from which the call is performed is pushed to the *call stack*. Once the procedure is finished, the call is popped off the call stack and execution continues from that call site. The path π<sup>2</sup> obviously does not respect this semantics: No execution that enters <sup>f</sup> through 2 → 7 leaves it through 9 → 5.

Relating calls and returns to one another is the main task of the correspondence function Φ. In the example, Φ is given by Φ(2 → 7) = 9 → 3 and Φ(4 → 7) = 9 → 5.

This leads to the notion of *interprocedurally valid paths*. Intuitively, an interprocedurally valid path is a path that respects the semantics of procedure calls. Validness can be defined with the help of Φ: We say that a path π is valid if Φ(*ecall*) = *eret* for all pairs (*ecall*,*eret*) on path such that *eret* is the return that finishes the procedure call performed by *ecall*.

Consider the path π<sup>1</sup> in the example. The return edge 9 → 3 finishes the call that is started by 2 → 7 and the return edge 9 → 5 finishes the call that is started by 4 → 7. According to the definition of Φ, we have Φ(2 → 7) = 9 → 3 and Φ(4 → 7) = 9 → 5, hence π<sup>1</sup> is valid.

By contrast, π<sup>2</sup> is not valid, since 9 → 5 finishes the call that is started by <sup>2</sup> <sup>→</sup> 7 but we have <sup>Φ</sup>(<sup>2</sup> <sup>→</sup> <sup>7</sup>) <sup>≠</sup> <sup>9</sup> <sup>→</sup> 5.

In chapter 5, I will consider valid paths in a more general context. In particular, I will make precise what I mean by "*eret* finishes the call that is started by *ecall*".

# **3.2.2.2.2 Two Approaches for tackling the Context Problem**

From the previous considerations, it is clear that if we perform data-flow analysis on interprocedural control-flow graphs like we do on intraprocedural control-flow graphs, the result is overly imprecise: Even if *MFP* coincides with *MOP*, the result is still imprecise since *MOP* merges over too many paths. To increase precision, we can consider a version of *MOP* that ignores paths that are definitely invalid. To define that, we let *VP*(*n*) be the set of valid paths that start in *smain* and end in *n*. Now let (*L*, *F*, *G*, ρ, *init*) be a data-flow instance.

Then we can define the *merge-over-all-valid-paths MOVP* as

$$(3.8) \qquad \qquad MOVP(n) \stackrel{def}{=} \bigsqcup\_{\pi \in VP(n)} f\_{\pi}(init)$$

Sharir and Pnueli [154] presented two approaches to compute *MOVP* and showed that these two approaches compute under certain assumptions the same solution. In the following two sub-paragraphs, I give a short summary on both of these approaches. I will also consider both approaches in chapter 6 and chapter 7 in a more general setting and in more detail.

**Call-String Approach** The idea of the call-string approach is to extend the constraint system that describes *MOP* by an additional stack component. Every time a call is encountered, this call is pushed onto the stack and each time a return is encountered, it can be checked whether it corresponds to the call at the top of the stack. Constraints are only generated for corresponding call-return pairs.

More formally, the constraint system describes a function

$$A: N \times S \to L$$

where *S* = *E* ⋆ *call* is the set of all call stacks. The intraprocedural constraints naturally extend the constraint system given for intraprocedural data-flow analysis. Constraints for call and return edges not only apply the transfer functions but also manipulate the call stack according to procedure calling semantics, using the usual stack operations *push*, *pop* and *top*, with the properties

$$\begin{aligned}push(e,\sigma) &= e \cdot \sigma\\pop(e \cdot \sigma) &= \sigma\\top(e \cdot \sigma) &= e \end{aligned}$$

The empty stack is denoted by ϵ. The full constraint system C*Stack* looks as follows:

### **Constraint System 3.2.**

$$A(s, \mathfrak{e}) \ge \mathfrak{init}$$

$$m \xrightarrow{\varepsilon} n \land e \in \mathbb{E}\_{\mathrm{intra}} \implies A(n, \sigma) \ge f\_{\mathfrak{e}}(A(m, \sigma))$$

$$m \xrightarrow{\varepsilon} n \land e \in \mathbb{E}\_{\mathrm{call}} \implies A(n, \mathrm{push}(e, \sigma)) \ge f\_{\mathfrak{e}}(A(m, \sigma))$$

$$\begin{array}{l} m \xrightarrow{\varepsilon} n \land e \in \mathbb{E}\_{\mathrm{ret}} \land \sigma \ne \mathfrak{e} \\ \land \Phi(\mathrm{top}(\sigma)) = e \end{array} \implies A(n, \mathrm{pop}(\sigma)) \ge f\_{\mathfrak{e}}(A(m, \sigma))$$

Note the additional precondition for the return constraints: No constraint is generated if Φ(*top*(σ)) ≠ *e*. This ensures that the resulting function *A* does not mix up calling contexts.

Like in the intraprocedural case, the above defined constraint system C*Stack* has a least solution *A*0.

To ensure comparability with *MOVP*, we define

$$\tilde{A}\_0(n) \stackrel{def}{=} \bigsqcup\_{\sigma \in \mathcal{S}} A\_0(n, \sigma).$$

Then it turns out that *A*̃ <sup>0</sup> <sup>≥</sup> *MOVP* and that *<sup>A</sup>*̃ <sup>0</sup> = *MOVP* under some additional assumptions. However, there is one hitch: Unlike the intraprocedural constraint system, Constraint System 3.2 cannot be computed by the usual method, particularly if the program contains recursive calls.

**Figure 3.9:** Sketch of the idea of the functional approach

The reason simply is that Constraint System 3.2 is not guaranteed to be finite.

The usual solution is to restrict the depth of the stacks. So instead of computing a function in *N* × *E* ⋆ *call* <sup>→</sup> *<sup>L</sup>*, we compute a function in *N* × *E* ≤*k call* <sup>→</sup> *<sup>L</sup>* and an adjusted *push* function

$$push\_k(e, \sigma) = (e \cdot \sigma)^{\leq k}$$

that discards the lowermost item on the stack σ if σ already has *k* elements. By using *push<sup>k</sup>* instead of *push*, Constraint System 3.2 is always finite, its least solution *A* (*k*) 0 can be computed using the usual method and still has the property *A*̃ <sup>0</sup> ≥ *MOVP*. However, note that discarding parts of the stack does not yield a fully context-sensitive analysis.

**Functional Approach** The *functional approach* uses the following idea, which is illustrated in Figure 3.9: Suppose that we have for each procedure p a transfer function *fp* that faithfully describes a complete traversal of a procedure p's control-flow graph. Then we can obtain a context-sensitive constraint system as follows: For normal intra-procedural edges, we use the usual constraints. For each call site c of p we can use *fp* to describe the effect of a call of f at c with a constraint like (1) in Figure 3.9b.

Additionally, a constraint like (2) ensures that the data-flow information is propagated from each call site to the entry of p.

Now, because of constraint (1), no constraint of the form

$$A(r) \ge f\_{e\_{\text{net}}}(A(t))$$

is needed to propagate data-flow information back to the return site r. Such a constraint would introduce the context problem because the data-flow information at t subsumes all paths to t. This includes in particular the paths which come from call sites other than c. Information along these paths is not supposed to be propagated to r. Constraint (1) avoids this problem by propagating the information at c through the whole procedure p.

It remains to solve the sub-problem of providing the *fp*. The idea is to describe them by a monotone constraint system, just like the final solution of the overall data-flow analysis. Note however that the *fp* do not represent single data-flow information but describe how data-flow information is transformed. This means that the *fp* do not live in *L* but in *F*.

As I already mentioned above, any *fp* should faithfully describe a complete traversal of p. More formally, this means that the *fp* incorporate the effects of traversing a certain class of paths from p's entry to its p's exit, the so-called *same-level paths*. A same-level path is a path that leaves every called procedure at the right call site and, additionally, ends in the same procedure in which it started. Hence a same-level path from p's entry to p's exit can be considered a complete traversal of p, as it may also occur in a real execution.

The sets *SL*(*sp*, *t*) of same-level paths from *s<sup>p</sup>* to *t* ∈ *N<sup>p</sup>* are defined inductively for procedure entries *s<sup>p</sup>* and nodes *t* ∈ *N<sup>p</sup>* of the same procedure. In order to avoid that call sites are mixed up, only corresponding call and return edges can be appended.

$$(\text{3.9})\qquad\qquad\epsilon\in SL(s\_{p\prime}s\_{p})$$

$$\text{(3.10)}\qquad \pi \in \text{SL}(\text{s}\_{p'}, t) \land t \stackrel{\varepsilon\_{\text{int}}}{\\\rightarrow} \text{e}\_{p'} \implies \pi \cdot e\_{\text{int}\mathbf{r}} \in \text{SL}(\text{s}\_{p'}, t')$$

$$\begin{aligned} \text{(3.11)}\\ \Longrightarrow & \quad \pi \in SL(s\_{p'}t) \land t \overset{\varepsilon\_{\text{call}}}{\leftrightarrow} s\_{p'} \land \pi' \in SL(s\_{p'}, e\_{p'}) \land e\_{p'} \overset{\Phi(\varepsilon\_{\text{call}})}{\leftrightarrow} t''\\ \Longrightarrow & \quad \pi \cdot e\_{\text{call}} \cdot \pi' \cdot \Phi(e\_{\text{call}}) \in SL(s\_{p'}t'') \end{aligned}$$

Using the same-level paths, we can now specify what we expect of the *fp*: The *fp* shall incorporate the effects of traversing any same-level path:

$$f\_p \geq \bigsqcup\_{\pi \in SL(\mathbf{s}\_p \mathcal{L}\_p)} f\_{\pi}$$

60

The constraint system for the *fp* can defined along the same-level paths as follows:

### **Constraint System 3.3.**

$$\text{(3.12)}\tag{3.12}$$

*t e*→ *t* ′ ∧ *e* ∈ *Eintra* =⇒ *X*(*sp*, *t* ′ (3.13) ) ≥ *f<sup>e</sup>* ◦ *X*(*sp*, *t*) *t <sup>e</sup>call* <sup>→</sup> *<sup>s</sup><sup>p</sup>* ′ ∧*ecall* ∈ *Ecall* ∧*e<sup>p</sup>* ′ <sup>Φ</sup>(*ecall*) <sup>→</sup> *<sup>t</sup>* ′ =⇒ *X*(*sp*, *t* ′ )<sup>≥</sup> *<sup>f</sup>*Φ(*ecall*) ◦ *X*(*s* ′ *p* ,*e<sup>p</sup>* ′)◦ *fecall* (3.14) ◦*X*(*sp*, *t*)

Both Constraint System 3.3 and the one sketched in Figure 3.9b are finite, even in the presence of recursion. This means that, if the complete lattice *N* × *N* → *F* satisfies the ascending chain condition, it can be solved by Algorithm 3. Note however that this additional condition restricts the practical applicability of the functional approach in comparison to the restricted call-string approach: It can only be applied to data-flow frameworks in which not only the complete lattice *L* but also the function space *F* satisfies the ascending chain condition. However, if the functional approach is applicable, it obtains a fully context-sensitive solution.

# **3.3 Slicing on Program Dependence Graphs**

In this section, I introduce *slicing*, another important static program analysis technique, and *program dependence graphs*, a data-structure that reduces slicing to graph reachability.

This section is organized as follows: First, I explain the general idea of slicing in subsection 3.3.1. In subsection 3.3.2, I introduce program dependence graphs, first for the intraprocedural case and subsequently for the interprocedural case. Finally, in subsection 3.3.3 I show how program slicing can be performed on program dependence graphs. Specifically, I consider an approach to obtain context-sensitive slices on interprocedural program dependence graphs.

# **3.3.1 Slicing**

*Program slicing* was introduced by Weiser [165] as a technique for focusing on specific parts of a program. For a given program P, a slice is defined with respect to a *slicing criterion* which consists of a program location *l* and a variable *x*. Given such a slicing criterion *c* = (*x*, *l*), a *valid* slice with respect to *c* is any sub-program P′ of P which produces the same behaviour with respect to *c* as P. This means that if P and P′ are started in the same state and both terminate, then P and P′ cannot be distinguished by just looking at the values of *x* at each respective execution of location *l*. It is desirable to have an automatic procedure which can always find slices of the smallest possible size. Due to decidability reasons, such a procedure cannot exist, but Weiser [165] describes a procedure to obtain a valid program slice that is fairly small. The idea is roughly to traverse the program's control-flow graph backwards from the given slicing criterion (*x*, *l*) and include in the slice every statement which may have an influence on the value of *x* in *l*. Essentially, Weiser's procedure transitively follows the data and control dependencies ending in *x* backwards.

# **3.3.2 Program Dependence Graphs**

*Program Dependence Graphs* [58] (PDGs) are another program representation used in program analysis. Roughly, PDGs model the dependencies between the statements and expressions of a program.

Ferrante, Ottenstein and Warren [58] introduced the program dependence graph as a program representation which makes control dependencies and data dependencies explicit. On this representation, program slicing can be expressed as a graph traversal. A slice thus obtained is indeed valid [140].

# **3.3.2.1 Intraprocedural Program Dependence Graphs**

For intraprocedural programs, there are two main kinds of dependencies: *data dependencies* and *control dependencies*. In the following, I will briefly explain data and control dependencies. After that, I will show how Program Dependence Graphs are extended for programs with multiple procedures.

**Figure 3.10:** Visualization of data dependencies

**3.3.2.1.1 Data Dependencies** Data dependencies capture the flow of data inside a program. A statement *defines* a variable if it writes a value to it and *uses* a variable if it reads the current value of that variable and then uses this value, for example to define other variables or to evaluate a branching condition.

For a statement (or CFG node, respectively) *s* I denote with *de f*(*s*) the set of variables defined by *s* and with *use*(*s*) the set of variables used by *s*.

Basically, a statement or expression *s*<sup>2</sup> is data-dependent on statement *s*<sup>1</sup> if there is a variable *x* such that (1) *s*<sup>1</sup> defines *x*, (2) *s*<sup>2</sup> uses *x* and (3) there is a control-flow path between *s*<sup>1</sup> and *s*<sup>2</sup> that does not define *x*. Such a situation is illustrated in Figure 3.10. Definition 3.3 gives a formal definition.

**Definition 3.3** (data dependencies)**.** *Let G* = (*N*, *E*,*s*,*e*) *be a control-flow graph. n* ∈ *N is data-dependent on m* ∈ *N, written m* →*dd n, if there is x* ∈ *de f*(*m*) ∩ *use*(*n*) *and there is a path* π ∈ *PathsG*(*m*, *n*) *such that* ∀1 ≤ *i* < <sup>|</sup>π| − 1. *<sup>x</sup>* <sup>∉</sup> *de f*(<sup>π</sup> *i* )*.*

Figure 3.11 shows the data dependency graph of the program from Figure 3.3.

For example, there is a data dependency from line 5 to line 7 because line 5 defines the variable a, line 7 uses a and a is not overwritten between line 5 and line 7. In contrast, line 5 and line 12 are not connected by a data dependency: Though line 12 uses a to define c, it definitely does not use the value of a from line 5, since a is overwritten in line 10.

Data dependencies can be computed using the reaching definitions analysis described earlier.

**3.3.2.1.2 Control Dependencies** The other kind of dependencies in a program dependence graph are *control dependencies*. The intuition behind control dependencies is illustrated in Figure 3.12. Control dependencies capture that a node *m* ∈ *N* is the "latest" node that "decides" whether (or how often) a node *n* is executed or not. This means that if program execution traverses *m*, then it can either proceed to a branch from which *m* can be bypassed or to a branch from which the execution of *n* is inevitable. Throughout this thesis, I assume the classical definition by Ferrante et al. [58], which I present here for reference. Ferrante et al. also propose an efficient algorithm for computing control dependency graphs that constructs the post-dominator tree using a fast algorithm presented by Lengauer and Tarjan [119].

**Definition 3.4** ([58], Definitions 2 and 3)**.** *Let G* = (*N*, *E*,*s*,*e*) *be a control-flow graph and m*, *n* ∈ *N.*

*1. Node n post-dominates m if*

$$\forall \pi \in \text{Paths}\_{\mathbb{G}}(m, e), n \in \text{nodes}(\pi)$$

*2. Node n is control-dependent on m, written m* →*cd n, if*

*a) there is a path* π ∈ *Paths<sup>G</sup> from m to n such that n post-dominates every node in* π *(except for m and n)*

*b) n does not post-dominate m.*

Figure 3.13 shows the control dependence graph of the example from Figure 3.3.

**Figure 3.11:** Data dependency graph for the example from Figure 3.3

**Figure 3.12:** Illustration of control-dependencies

**Figure 3.13:** Control dependence graph of the program in Figure 3.3 – note that for well-formedness reasons an additional synthetic control-flow edge between the entry node and the exit node is assumed

# **3.3.2.2 Interprocedural Program Dependence Graphs**

In the following, I describe briefly how Program Dependence Graphs look like for programs with multiple procedures. The standard approach has been described by Horwitz et al. [93].

In this approach, interprocedural PDGs are constructed from intraprocedural PDGs in a similar way as interprocedural control-flow graphs are constructed from intraprocedural control-flow graphs. An interprocedural PDG consists of a PDG for every procedure. These *procedure dependence graphs* are enriched with additional nodes and edges which model the call itself and the passing of parameters from caller to callee and the passing of return values from callee to caller. This modelling assumes call by value. First of all, a *call dependence* connects the call node at the call site and the entry method of the callee. This is a special control dependence

**Figure 3.14:** The interprocedural PDG of the example in Figure 3.8a

that captures the intuition that the call node "decides" whether the callee is called or not.

Moreover, for every parameter of a procedure *p*, there is a *formal-in* parameter node, and for its return value, there is a *formal-out* node. At each call site of *p*, there is an *actual-in* node for each of *p*'s parameters and an *actual-out* node for *p*'s return value. Formal and actual parameter nodes are connected via *parameter-in* and *parameter-out* edges, which model passing of parameters from caller to callee and of the return values from callee to caller. A procedure call can be thought of to be preceded by a series of assignment statements which assign the actual parameter values to special variables which only the callee has access to. After the procedure has finished, it copies its return value to a special variable which only the caller has access to. In this sense, parameter-in and parameter-out edges can be seen as special interprocedural data dependencies. Parameter passing is then modeled using a *parameter-in* edge which connects the actual-in parameter node at the call site with the formal-in node in the callee. Additional *parameter-out* edges model how data flows from a formal-out parameter node of a procedure to its counterpart in the caller. Consider for example the interprocedural PDG in Figure 3.14: Procedure f has one formal-in parameter node for its parameter z and one formal-out parameter node for its return value. Both are connected to their counterparts at each of the two call sites of f.

In the following, I consider call dependencies and parameter-in edges as *call edges* and refer to all call edges as *Ecall*. Analogously, I consider parameter-out edges as return edges and use *Eret* to refer to the set of parameter-out edges.

# **3.3.3 PDG-based Slicing**

As I already mentioned in the beginning, program dependence graphs make explicit the dependencies that are traversed implicitly during Weiser's slicing procedure [165]. Hence, PDGs reduce the slicing problem to mere graph traversal.

# **3.3.3.1 PDG-based Intraprocedural Slicing**

**Algorithm 4:** A simple intraprocedural backward slicer – upon termination, we have *W* = *BSintra*(*s*)

```
Input: a PDG G = (N, E) with intraprocedural edges Eintra, sli-
        cing criterion s ∈ N
 Result: intraprocedural slice BSintra(s)
1 W ← {s}
2 while W ≠ ∅ do
3 n ← remove(W)
4 foreach m → n ∈ Eintra do
5 W ← W ∪ {m}
6 return W
```
For single procedures, PDG-based slicing works as follows: Given a PDG *G* = (*N*, *E*) and some node *n*, the *backwards-slice* is the set *BS*(*n*) ⊆ *N* of all nodes that reach *n* in the PDG:

$$BS(n) \stackrel{def}{=} \{ \mathbf{s} \in \mathcal{N} \mid \mathbf{s} \to\_G^\* n \}.$$

Analogously, the *forward-slice* is the set *FS*(*n*) ⊆ *N* of all nodes that is reachable by *n* in the PDG:

$$FS(n) \stackrel{def}{=} \{ s \in \mathbb{N} \mid n \to\_G^\* s \}.$$

*BS* and *FS* are related by the property

$$s \in BS(n) \iff n \in FS(s).$$

and hence are dual to each other. Algorithm 4 shows a simple algorithm that computes an intraprocedural backward slice of *s* ∈ *N*. It is very easy to modify Algorithm 4 such that it computes an intraprocedural forward slice.

# **3.3.3.2 PDG-based Interprocedural Slicing**

Interprocedural slicing operates on an interprocedural program dependence graph, just like intraprocedural slicing operates on an intraprocedural program dependence graph.

**3.3.3.2.1 Context Problem** Similar to data-flow analysis, interprocedural slicing faces the problem that not all paths in an interprocedural program dependence graph represent a valid chain of dependencies. In effect, if *BS* and *FS* are not adapted to the interprocedural case, they yield context-insensitive slices, which are usually too big.

For example, Figure 3.15 shows a simple backwards slice of the return value of the second call of f from Figure 3.14. This slice also contains the actual parameter of the first call of f.

However, the only path from the actual parameter of the first call of f to the return value of the second call of f is interprocedurally invalid – just like the path that enters f through the first call site and leaves it through the second call site.

**3.3.3.2.2 Context-Sensitive Interprocedural Slicing** The context problem for interprocedural slicing can be tackled in similar ways as the context problem for interprocedural data-flow analysis. Agrawal and Guo [9] consider a call-string-based approach that supports call-strings of unlimited length. However, Krinke [110] observes that this approach is indeed

incorrect. He proposes a fixed version and also considers call-strings with limited depths.

Horwitz, Reps and Binkley [92, 93] propose an approach to interprocedural slicing that resembles the functional approach for interprocedural data-flow analysis and thus avoids the context problem. First, in a pre-processing step the program dependence graph is extended with additional *summary edges*. A summary edge is added between an actual-in node and an actualout node of the same call site if a corresponding formal-in/formal-out pair is connected by a chain of intraprocedural edges or summary edges. The resulting graph is called *System Dependence Graph*.

In comparison to the original presentation [92, 93], the runtime performance of the pre-processing step can be improved by introducing additional bookkeeping and trading space for speed, as shown by Reps, Horwitz, Mooly and Rosay [137]. My presentation in this thesis concentrates on the improved version.

The actual slicing can then be performed on the System Dependence Graph using an algorithm that operates in two phases. Each phase basically consists of a simple graph reachability approach that skips either parameter-in or parameter-out edges. Both phases use summary edges to traverse call sites. This way, the approach avoids traversing call and return

**Figure 3.15:** Context-insensitive backwards slice of the actual-out ret parameter of the second call of f

edges individually – the root cause of the context problem, as I mentioned earlier.

Similar to the functional approach to interprocedural data-flow analysis described in section 3.2.2.2.2, summary edges describe a complete traversal of the called procedure and the System Dependence Graph is nothing more than the integration of these additional helper transfer functions into the Program Dependence Graph.

More generally, the PDG edges themselves also can be viewed as transfer functions. The information they propagate is mere reachability. In this sense, the two-phase slicer can be understood as the solution algorithm for a data-flow analysis instance with very simple transfer functions. I will discuss the similarities between and differences of data-flow analysis and slicing in more detail in subsection 3.3.4.

In the following, I briefly describe the summary edge algorithm and the two-phase slicer.

**Summary Edges** The summary edge computation algorithm proposed by Reps et al. [92, 93] is shown in Algorithm 5. The rough idea is to compute all node pairs for which there is a *same-level path* in the given program dependence graph. To define same-level paths for program dependence graphs, some modifications are necessary: For one, we need to use parameter-in edges instead of call edges and parameter-out edges instead of return edges. Moreover, the correspondence function needs to be a correspondence *relation*, since in the presence of multiple parameters, there may be more than one parameter-out edge that corresponds to a given parameter-in edge. In chapter 5, I will present a definition that applies to both control-flow graphs and program dependence graphs.

The algorithm maintains two sets of node pairs (*v*, *w*) where *w* is a formalout node and *v* is an arbitrary node of the same procedure. If (*v*, *w*) ∈ *PathEdge*, then there is a same-level path between *v* and *w*. If (*v*, *w*) ∈ *W*, then there is a same-level path between *v* and *w* and (*v*, *w*) has been discovered for the first time.

Initially, for every formal-out node *w*, the pair (*w*, *w*) is contained in both *W* and *PathEdge*. This is because the empty path is a same-level path.

In the main iteration, the algorithm removes a pair (*v*, *w*) from *W* and reacts to two relevant cases for (*v*, *w*):

**Algorithm 5:** Summary Edge Algorithm proposed by Reps et al.– compare [137, Figure 5]

```
Input: a PDG G
  Result: summary egdes in G
1 PathEdge ← ∅
2 SummaryEdge ← ∅
3 W ← ∅
4 foreach w ∈ FormalOuts(G) do
5 PathEdge ← PathEdge ∪ {(w, w)}
6 W ← W ∪ {(w, w)}
7 while W ≠ ∅ do
8 (v, w) ← remove(W)
9 if v is a formal-in node then
10 foreach x
               param−in → v, w
                         param−out → y do
11 if x and y belong to the same call-site then
12 SummaryEdge ← SummaryEdge ∪ {x → y}
13 foreach a such that (y, a) ∈ PathEdge do
14 if x → a ∉ PathEdge then
15 PathEdge ← PathEdge ∪ {(x, a)}
16 W ← W ∪ {(x, a)}
17 else
18 foreach x → v ∈ Eintra do
19 if (x, w) ∉ PathEdge then
20 PathEdge ← PathEdge ∪ {(x, w)}
21 W ← W ∪ {(x, w)}
22 if v is an actual-out node then
23 foreach (x, v) ∈ SummaryEdge do
24 if (x, w) ∉ PathEdge then
25 PathEdge ← PathEdge ∪ {(x, w)}
26 W ← W ∪ {(x, w)}
27 return SummaryEdge
```
**Figure 3.16:** The interprocedural PDG of the example in Figure 3.8a with summary edges

1. If *v* is a formal-in node, then a same-level path has been completed. The algorithm then adds a summary edge between every corresponding actual-in/actual-out pair (*x*, *y*). In this context, this means that there is a parameter-in edge from *x* to *v*, a parameter-out edge from *w* to *y* and that *x* and *y* belong to the same call site. Now that there is an additional edge between *x* and *y*, it may be the case that some pair (*y*, *a*) can be extended to (*x*, *a*). Because of this, for all already discovered pairs (*y*, *a*), (*x*, *a*) is added to *W* if it has not been discovered before. This ensures that the call site "notices" that propagation can be continued.

2. If *v* is not a formal-in node, then every incoming intraprocedural edge *x* → *v* of *v* is processed; since there is a same-level path between *v* and *w* and there is an incoming edge *x* → *v*, there is a same-level path between *x* and *w*. Hence, (*x*, *w*) is added to *PathEdge* and *W* if it was encountered for the first time. If *v* is an actual-out node, then the algorithm additionally processes the already discovered summary edges.

Applied to Figure 3.14, Algorithm 5 starts at the formal-out return node of f and traverses f's PDG backwards until it arrives at f's formal-in node for z. Now, a complete same-level path has been discovered and Algorithm 5 inserts a summary edge between the actual-in node for x and the actual-out return node at each of the two call sites of f. This results in the system dependence graph shown in Figure 3.16.

**The Two-Phase Slicer** The idea of the two-phase slicer, which can be seen in Algorithm 6, bears some similarity to the idea of the functional approach: With summary edges, procedure calls can be skipped.

**Algorithm 6:** Backwards two-phase slicer proposed by Horwitz et al. [92]

```
Input: a PDG G = (N, E); E consists of the intraprocedural edges
         Eintra and Ecall, Eret as described on page 67; Esum is the set
         of summary edges as computed by Algorithm 5; slicing
         criterion s
 Result: context-sensitive backwards slice of s
1 S ← ∅
```

```
2 W1 = {s} // phase 1: only ascend to callers
3 while W1 ≠ ∅ do
4 n ← remove(W1
                   )
5 S ← S ∪ {n}
6 foreach m
              e→ n ∈ E do
7 if e ∈ Eintra ∪ Esum ∪ Ecall then
8 W1 ← W1 ∪ {m}
9 else
10 // e ∈ Eret and m is an exit or formal-out node
11 W2 ← W2 ∪ {m}
12 // phase 2: only descend to callees
13 while W2 ≠ ∅ do
14 n ← remove(W2)
15 S ← S ∪ {n}
16 foreach m → n ∈ Eintra ∪ Esum ∪ Eret do
17 W2 ← W2 ∪ {m}
18 return S
```
This way, simple graph reachability can be used to obtain a slice that respects calling contexts. Note, however, that unlike with data-flow analysis, we are not interested in paths from the entry of the main procedure but actually want to start at an arbitrary place in the given graph. But this means that it does not suffice to descend into called procedures. It is also necessary to ascend to calling procedures. For this purpose, the slicer proposed by Horwitz et al. consists of two phases: The first phase only ascends to calling procedures and the second phase only descends into called procedures.

Figure 3.17 shows how Algorithm 6 is applied to Figure 3.14. The slicing criterion is the actual-out return node of the second call of f. In Figure 3.17a, we see the state after the completion of the first phase of Algorithm 6: At this point, the slice contains all nodes with bold frame. Moreover, the actual-out return node of the second call of f is contained in *W*2 so that phase 2 will start with that node.

Phase 2 then descends into f. The result of this can be seen in Figure 3.17b. It can easily be seen that it is more precise than a context-insensitive slice, since unlike the slice in Figure 3.15, it does not contain the actual parameter of the first call of f.

# **3.3.4 Relation of PDG-based Slicing and Data-Flow Analysis**

In the following, I investigate the relation between PDG-based slicing and data-flow analysis. My observations are

1. slicing can be cast as a very simple data-flow problem,

2. this data-flow problem can be solved with basically the same techniques, however, different assumptions have to be made, and

3. particularly, summary-based two-phase slicing can be seen as an application of a modified functional approach to interprocedural data-flow analysis

After having identified slicing as a special case of data-flow analysis, we can generalize it and obtain arbitrary data-flow analyses on program dependence graphs. These analyses can be solved for instance with the functional approach. This will be the subject of chapters 5, 6 and 7.

**Figure 3.17:** Applying the two-phase slicer to the example in Figure 3.14; the slicing criterion (with dark gray background) is the actual-out return node of the second call of f; nodes in slice after the respective phase have light gray background

In the following presentation, I fix a node *s* ∈ *N* and consider the forward slice *FS*(*s*) of *s*. Note that I knowingly ignore the fact that Algorithm 5 and Algorithm 6 traverse the graph *backwards*. In contrast, I only consider forward traversals and, in particular, I pretend that Algorithm 5 and Algorithm 6 traverse the graph forward. I showed these algorithms in their original version that was motivated by applications such as debugging, where backward slices are natural. However, in general the propagation direction does not matter and can easily be changed.

# **3.3.4.1 PDG-based Slicing as a Data-Flow Problem**

As I already mentioned earlier, slicing can be described as a kind of reachability analysis. As such, it can easily be cast as a data-flow problem. Mainly, we will have to adapt the way in which the constraint systems are generated from the given graph.

Let *L* = {⊥, ⊤} with ⊥ < ⊤ and let *F* = {λ*x*.⊥, λ*x*.*x*} with the usual partial order. Then (*L*, *F*) is a data-flow framework.

An instance of this framework is given by the PDG *G*, the initial information *init* = ⊤ and ρ(*e*) = *id*.

For simplicity, I consider the special case *reachability from a given node<sup>8</sup> s*.

**Intraprocedural case** For π ∈ *PathsG*(*s*,*t*), we have *f*<sup>π</sup> = *id*. Hence, with

$$F\_{\text{intra}}(t) = \bigsqcup\_{\pi \in \text{Paths}\_G(\mathbf{s}, t)} f\_{\pi}(\mathbf{init}),$$

we have *Fintra*(*t*) = ⊤ if and only if *t* ∈ *FSintra*(*s*) (where *FSintra* is the intra-procedural forward slice of *s*). The function *Fintra* is similar to *MOP* (see (3.1) on page 50), but merges over a different path set. I will examine the difference later and ignore it for now.

*Fintra* can be described by a monotone constraint system that is very similar to the one used in intra-procedural data-flow analysis:

### **Constraint System 3.4.**

$$\begin{aligned} X(s) &\geq \acute{m}\dot{t} \\ X(t) &\geq X(t') \; for \; t' \xrightarrow{\varepsilon} t. \end{aligned}$$

As can easily be seen, this constraint system actually precisely characterizes *Fintra*: Its least solution *X* can be used to extract the intra-procedural forward-slice *FSintra* of *s*:

$$FS\_{intra} = \{ n \in N \mid \underline{X}(n) = \top \}.$$

**Interprocedural case** In the interprocedural case, we want to compute

$$F\_{inter}(t) = \bigsqcup\_{\pi \in VP(s, t)} f\_{\pi}(init).$$

This is similar to *MOVP* (see (3.8) on page 57). However, there are two differences:

<sup>8</sup> In later chapters, I consider data-flow problems whose *MOP* functions take two parameters.

$$\begin{array}{c} n\_3 \stackrel{\pi\_3}{\smile\_2} n\_4\\ n\_1 \stackrel{\pi\_2}{\smile\_2} n\_2 \\ \uparrow\_{\text{return}}\\ s \stackrel{\pi\_1}{\smile\_2} n\_0 \end{array} \begin{array}{c} n\_3 \stackrel{\pi\_3}{\smile\_3} n\_4\\ n\_5 \stackrel{\text{\tiny all}}{\smile\_4} n\_6 \\ n\_5 \stackrel{\pi\_4}{\smile\_5} n\_6 \\ \end{array}$$

**Figure 3.18:** Illustration of a valid PDG path: The π*<sup>i</sup>* are same-level paths. All *n<sup>i</sup>* are reachable from *s* by a valid path.

1. *Finter* does not merge over all valid paths starting in *smain* and ending in *t* but over those that start in *s* (similar to *Fintra*).

2. *Finter* refers to a different notion of *validness*, which I describe in the following.

For interprocedural control-flow graphs, two usual assumptions are

1. that every procedure *p* has an entry node *sp* and each of *p*'s node is same-level reachable from *sp*, and

2. that there is a main procedure *main* and for each procedure *p*, *sp* is reachable from *smain* by a *descending* path, that is a path that consists of a series of same-level paths interspersed with call edges.

In interprocedural data-flow analysis, one usually is interested in proper executions that indeed start at*smain* and proceed until a given program point is reached. This is why *MOVP*(*t*) merges over all valid paths from *smain* to *t*. For slicing, this is different: As we are interested in reachability from *s*, we only want to consider the valid paths that start in *s* and not necessarily in *smain*. Moreover, since *s* may lie anywhere in the program, the notion of validness employed here can not only consider descending paths, like in interprocedural control-flow graphs, but also has to include paths that have an *ascending* prefix. Analogously to a descending path, an ascending path can be imagined as a series of same-level paths interrupted by return edges. A valid path is an ascending path followed by a descending path. An illustration is given in Figure 3.18.

Now we want to describe *Finter* by a series of monotone constraint systems. We use the functional approach, since it is applicable to the reachability framework and we want to maximize precision. Recall that the idea of the functional approach is to avoid traversing call and return edges at the same time by first solving a helper system that describes the effect of completely traversing procedures from entry to exit.

**Helper System for Same-Level Reachability** The helper system is similar to the helper system for interprocedural data-flow analysis on page 61. However, for PDGs we have to make a few modifications: For one, procedures may have multiple entries, namely the actual procedure entry and the formal-in parameter nodes, and also multiple exits, namely the actual procedure exit and the formal-out parameter nodes. Correspondingly, call sites may have multiple call nodes (the actual call node and the actual-in parameter nodes) and multiple return nodes (the actual return node and the actual-out parameter nodes). In particular, the correspondence relation Φ in this case relates actual-ins and actual-outs that belong to the same call site and therefore in general is not a function, but rather an arbitrary relation.

With these modifications, the helper constraint system that describes same-level reachability looks as follows:

# **Constraint System 3.5.**

$$(3.15) \qquad \qquad \qquad X(n,n) \ge id\_n$$

$$(\text{3.16})\qquad\qquad t \xrightarrow{\varepsilon} t' \land e \in E\_{intra}$$

$$\implies X(n, t') \ge X(n, t)$$

$$\begin{aligned} (\text{3.17}) \quad & (e\_{\text{call}}, e\_{\text{ret}}) \in \Phi \land m \stackrel{e\_{\text{call}}}{\rightarrow} n\_0 \land n\_1 \stackrel{e\_{\text{ret}}}{\rightarrow} t \\ \implies \text{X}(n, t) \ge \text{X}(n\_0, n\_1) \circ \text{X}(n, m) .\end{aligned}$$

With uniqueness assumptions of entries, exits, calls and returns in place, Constraint System 3.5 reduces to Constraint System 3.3.

The least solution of this system is a function *XSL* : *N* × *N* → *F* with *X*(*s*, *t*) = *id* if and only if *t* is same-level reachable from *s*.

In the following, I show how to use *XSL* to actually compute *Finter* and hence *FS*(*s*). The approach exploits a fundamental property about valid paths:

*Every valid path* π ∈ *VP*(*s*,*t*) *is either ascending or there is n* ∈ *Ncall so that* π *can be split up into* π<sup>1</sup> · π<sup>2</sup> *such that* π<sup>1</sup> *is an ascending path from s to n and* π2 *is a non-empty descending path from n to t.*

I will also call the second kind of valid paths *non-ascending*.

As a consequence of the above mentioned property, *Finter* can be obtained in two steps as illustrated by Figure 3.19. They can be described intuitively as follows:

1. The first step computes the solution along the ascending paths starting with *s*.

2. The second step starts at the nodes *n* ∈ *Ncall* that are reachable from *s* by an ascending path and extends the solution along descending paths.

**Computing Reachability Along Ascending Paths** This first step works like the intra-procedural case but additionally propagates the reachability information along return edges and uses *XSL* for propagating from actual-ins to corresponding actual-outs.

)

### **Constraint System 3.6.**


$$\begin{aligned} (\text{3.20}) \qquad &(e\_{call}, e\_{ret}) \in \Phi \land m \stackrel{\varepsilon\_{call}}{\xrightarrow{\varepsilon\_{call}}} n\_0 \land n\_1 \stackrel{\varepsilon\_{ret}}{\xrightarrow{\varepsilon\_{ret}}} t \\ \implies &X\_{ASC}(t) \ge \underline{X}\_{SL}(n\_0, n\_1) \circ X\_{ASC}(m) \end{aligned}$$

The least solution *XASC* has the property

*<sup>X</sup>ASC*(*t*) <sup>≠</sup> ⊥ ⇐⇒ *<sup>s</sup>* reaches *<sup>t</sup>* using an ascending path(3.21)

**Extending Reachability Along Descending Paths** The second step starts at the call nodes that are reachable by ascending paths from *s* and extends the reachability solution along descending paths. Again, it works like the intra-procedural case but additionally propagates the reachability information along call edges and uses *XSL* for propagating from actual-ins to corresponding actual-outs.

### **Constraint System 3.7.**

$$(\text{3.22})\qquad\qquad a \in N\_{\text{call}} \land a \xrightarrow{\mathcal{e}} t \land e \in E\_{\text{call}}$$

$$\implies \text{X}\_{\text{NASC}}(t) \ge \underline{\text{X}}\_{\text{ASC}}(a)$$
 
$$\therefore \quad \text{F}\_{\text{A}} \qquad \stackrel{\text{} \longrightarrow \text{F}\_{\text{A}}}{\longrightarrow} \quad \text{A} \quad \text{\text{A}} \quad \stackrel{\text{} \mathcal{E}\_{\text{A}}}{\longrightarrow} \quad \text{A}$$

$$\begin{aligned} \text{(3.23)} \qquad &e \in E\_{intra} \cup E\_{call} \land t' \xrightarrow{\iota} t\\ \implies \text{X}\_{NASC}(t) \ge \text{X}\_{NASC}(t') \end{aligned}$$

$$\begin{aligned} \text{(3.24)} \qquad & \qquad (e\_{\text{call}}, e\_{\text{ret}}) \in \Phi \land m \stackrel{e\_{\text{call}}}{\twoheadrightarrow} n\_0 \land n\_1 \stackrel{e\_{\text{ret}}}{\twoheadrightarrow} t \\ \implies & X\_{\text{NASC}}(t) \ge \underline{X}\_{SL}(n\_0, n\_1) \circ X\_{\text{NASC}}(m) \end{aligned}$$

The least solution *XNASC* has the property

*<sup>X</sup>NASC*(*t*) <sup>≠</sup> ⊥ ⇐⇒ *<sup>s</sup>* reaches *<sup>t</sup>* using a non-ascending path .(3.25)

**Putting the Steps Together** With the fundamental property of valid paths and properties (3.21) and (3.25), we can characterize *Finter* as *XASC* ⊔ *XNASC*.

$$\text{(3.26)}\qquad\qquad\qquad\qquad\quadF\_{\text{inter}}=\underline{X}\_{\text{ASC}}\sqcup\underline{X}\_{\text{NASC}}$$

### **3.3.4.2 Comparisons of the Algorithms**

After having compared the specification side of data-flow analysis and slicing, I examine the algorithm side more closely.

Roughly, one can say that each of the slicers described so far, namely Algorithm 4, Algorithm 5 and Algorithm 6, correspond to one or more of the constraint systems that I just showed, in the sense that they compute a representation of the least solution for significant parts of them.

In particular, the summary edges used by Algorithm 6 can be considered as procedural effect functions that are used to safely skip procedure calls, or, for PDGs, transitions from actual-in nodes to actual-out nodes. Conceptually, all pairs of actual-in and actual-out nodes at the same call site are connected by edges. The presence of a summary edge between such an actual-in node *m* and an actual-node out *n* encodes whether the transfer function for the edge between *m* and *n* is λ*x*.*x* or λ*x*.⊥. In the former case, reachability is propagated, in the latter case propagation stops. The constraint systems that I showed so far can also be solved with appropriate instantiations of Algorithm 3. In the following, I investigate the differences between the slicers and Algorithm 3.

First, when looking at the constraint systems, we see that they are *too large*: They contain a lot more constraints than the ones that are actually needed to compute their respective result. An example for this for the intraprocedural case can be seen in Figure 3.20: Suppose that the PDG contains a node *s* ′ that is not reachable from *s* but like *s* has an outgoing edge to *t*. Then both edges are incorporated in Constraint System 3.4 but only the edge from *s* to *t* is relevant. The other constraint systems have a similar problem.

The reason is that they cannot already incorporate reachability information as *their purpose is to characterize that very information*. An unmodified version of Algorithm 3 would solve the complete systems and in particular process large parts that are actually not relevant. The slicers however only explore the relevant parts of the respective constraint systems, namely the ones that are reachable from the initial constraints that do not have variables on the right-hand side.

**Figure 3.20:** Illustration for the way in which the constraint systems for slicing – for example Constraint System 3.4 – contain irrelevant constraints

A striking similarity between Algorithm 3 and the slicing algorithms is that they all use worklists to keep track of the items that have yet to be processed. However, they differ in what I want to call *workflow policy*. A workflow policy answers the following questions and thus determines how the algorithm operates on the worklist to finish its task:


Figure 3.21 illustrates the differences in the worklist policies of Algorithm 3 and the slicing algorithms.

As shown in Figure 3.21a, Algorithm 3 puts an item onto the worklist if one of its predecessors has changed in an earlier iteration. Then, when an item *x* is taken off the worklist and processed, its value is updated with the value of its predecessors. If the value has changed, all its successors are put on the worklist.

The slicers have a slightly different worklist policy, as illustrated in Figure 3.21b. There, an item is put on the worklist if its value has been changed in an earlier iteration but this has not been propagated yet. Then when an item *x* is taken off the worklist and processed, all its successors are updated with respect to the value of *x*. All successors whose value changes by this are put on the worklist.

**Figure 3.21:** Illustration of the difference between (a) Algorithm 3 and (b) Algorithm 4 – the node highlighted in gray is assumed to have just been taken off the worklist and about to be processed

Another difference between Algorithm 3 and the slicers is that Algorithm 3 requires that all variables are processed at least once. For this, the algorithm initially puts all variables on the worklist. In contrast, the slicing algorithms initialize the worklist only with those variables that are defined by initial constraints.

In short, one can say that the slicers integrate a reachability analysis of the constraint system into their solution process. This may seem like a pointless observation: For one, reachability analysis is exactly what the slicers do. Secondly, data-flow analysis makes several reachability assumptions and therefore has no necessity to additionally perform reachability analysis. However, this observation is actually helpful for generalizing slicing to arbitrary data-flow analysis: Since slicing does not make the reachability assumptions of data-flow analysis, its generalization also does not make them. *Therefore, in order to perform data-flow analysis on program dependence graphs that includes slicing as a special case, we need solution algorithms that integrate slicing into its solution processing!* I will present such algorithms in chapter 7, along with the theory that explains why they work and what exactly they do. These algorithms can be used to perform data-flow analysis not only on program dependence graphs but also on controlflow graphs that do not satisfy reachability assumptions such as the ones mentioned on page 77.

# **3.4 J**oana**: PDG-based Information Flow Control for J**ava

In this section, I describe the Joana framework in more detail.

Joana heavily uses the T.J. Watson Libraries for Analysis (WALA), a program analysis framework for Java bytecode [141].

Using WALA, Joana applies a wide variety of program analysis techniques in order to construct program dependence graphs and to verify various non-interference-like properties of a given Java application. Apart from the techniques I have shown so far in section 3.2 and section 3.3, Joana can also handle modern programming language features.

The goal of this section is to give the reader a rough understanding of how Joana works so that they are equipped to follow the details of chapter 4. For a more thorough description of the inner workings of Joana, I refer the reader to earlier publications [78, 65, 86].

In subsection 3.4.1, I describe the connection between information flow control and slicing that is exploited by Joana. After that, I go into some of the features of Java and describe some of the techniques that Joana applies in order to treat them appropriately. Specifically, subsection 3.4.2 considers dynamic dispatch, subsection 3.4.3 looks at the challenges posed by exceptions and, lastly subsection 3.4.4 shows how objects are represented and handled by Joana.

Note that I will not discuss concurrency in this chapter but in chapter 4, particularly in section 4.2.

# **3.4.1 Slicing and Information Flow Control**

Slicing has a strong connection with non-interference and, hence, information flow. In the following, I give a cursory description of the intuition behind this connection. For my explanation, I use *batch-job terminationinsensitive non-interference* (BTINI) [22] in a very simple setting.

Consider a deterministic program P which defines and uses only two variables *h*, *l*. Variable *h* is thought of to contain secret values that are not supposed to influence the variable *l*, which is assumed to be publicly accessible.

A suitable instantiation of BTINI then demands that

$$\forall \sigma, \sigma' \in \Sigma. \sigma(l) = \sigma'(l) \land \mathcal{P} \Downarrow \sigma \land \mathcal{P} \Downarrow \sigma' \implies \llbracket \mathcal{P} \| (\sigma)(l) = \llbracket \mathcal{P} \| (\sigma')(l) \rangle$$

where Σ is the set of all program states and *P* ⇓ σ means that P terminates for initial state σ.

Now assume that there is a valid slice P′ of P with respect to the value of *l* at the end of P that does not contain any use of *h*. Then it can be shown that P is non-interferent. A rough and intuitive argument for this goes as follows: Fix a valid slice P′ of P that does not contain any use of *h* and let σ, σ ′ ∈ Σ with σ(*l*) = σ ′ (*l*) and assume that P ⇓ σ and P ⇓ σ ′ . Since P′ is a valid slice of P with respect to the final value of *l*, we can conclude that P′ also terminates on σ and that the final state P′ (σ) coincides with P(σ) on *l*:

$$\mathcal{P}' \Downarrow \sigma \land \mathcal{P}'(\sigma)(l) = \mathcal{P}(\sigma)(l).$$

The same argument can be made for σ ′ :

$$
\mathcal{P}' \Downarrow \sigma' \land \mathcal{P}'(\sigma')(l) = \mathcal{P}(\sigma')(l).
$$

Moreover, since (a) P′ does not contain any use of *h* and therefore only uses *l* itself and (b) σ(*l*) = σ ′ (*l*), it must be P′ (σ)(*l*) = P′ (σ ′ )(*l*). Together, it follows that

$$\mathcal{P}(\sigma)(l) = \mathcal{P}'(\sigma)(l) = \mathcal{P}'(\sigma')(l) = \mathcal{P}(\sigma')(l).$$

For PDG-based slicing, this basic idea can be exploited to give formal proofs for the general case. Horwitz et al. [91] show that program dependence graphs adequately capture program execution behaviour. Extending this work, Reps et al. [140] show the *Slicing Theorem* that states that PDG-based program slices are indeed valid, i.e. that a program and its slices exhibit the same execution behavior with respect to the slicing criterion.

Snelting, Robschink and Krinke apply the Slicing Theorem to argue that non-interference according to Goguen and Meseguer [71, 70] can be verified using PDGs and slicing [157].

Wasserrab [164] shows the correctness of PDG-based slicing and applies this result to show that slicing can be used to verify BTINI for sequential programs with multiple procedures.

For concurrent programs, simple properties such as BTINI are not sufficient anymore. I will consider non-interference properties for concurrent programs in section 4.2.

# **3.4.2 Dynamic Dispatch**

In subsubsection 3.2.1.2, I discussed how interprocedural control-flow graphs are built by constructing the control-flow graphs of each procedure and then connecting them appropriately. This works fine for simple languages in which all call targets can be resolved *statically*, that is at compile-time.

Modern programming languages, however, usually support some form of *late binding*, i.e. that names are not resolved statically at compile-time but dynamically at run-time. In the context of procedures (which are also called *methods* in object-oriented languages), late binding is also known as *dynamic dispatch*: For a dynamically dispatched method, there may exist multiple implementations and in contrast to statically dispatched methods, the actually executed implementation is not selected at compile-time but deferred to runtime.

In Java, the programmer can declare *classes* and two types of methods: *Static methods* are associated with the class itself and therefore independent of any instances of this class. They are dispatched statically, that is calls of them are resolved at compile-time.

The other type of methods are *instance methods*: They are associated with every instance of the class individually<sup>9</sup> .

For instance methods, Java makes it possible to provide multiple implementations through its class inheritance mechanism which allows methods to be *overridden* – that is, a sub-class can re-declare a method declared in its superclass and provide another implementation for it.

For the call of an instance method, the bytecode only specifies the *static* call target, that is the call target which is derivable at compile-time from static type information. The method which is actually called is then resolved dynamically at run-time using the actual type of the receiver object.

An example for this can be seen in Listing 3.1: The method call in line 25 is dispatched dynamically. The static call target is A::f, however at runtime B::f is called since the runtime type of parameter a is B.

For a static analysis this means that method calls in general cannot be resolved uniquely at compile-time but must be approximated. In order for the static analysis to be sound, this has to be an *over-*approximation. The example Listing 3.1 also illustrates that it is crucial for a sound program analysis to capture all possible call targets of dynamically dispatched method calls. If it does not, it may miss important program behaviour: For example, Listing 3.1 contains an information leak: the secret value that is read in line 18 is printed to a public console in line 8. An information flow analysis that does not detect B::f as possible call target for the call in line 25 would miss this illegal information flow.

A static program analysis that aims for a safe over-approximation of the actual program behaviour must therefore in particular over-approximate the possible targets of every instance method call.

Hence, in the interprocedural control-flow graph multiple outgoing call edges from the same call target are allowed. With the definition of

<sup>9</sup> In Java there are also **private** methods which are only accessible from within the same class and therefore are also bound statically. But I ignore them here for simplicity.

```
1 class A {
2 void f(int x) {
3 //do nothing
4 }
5 }
6 class B extends A {
7 void f(int x) {
8 print(x);
9 }
10 }
11 class C extends A {
12 void f(int x) {
13 print(42);
14 }
15 }
                              16 class Main {
                              17 static void main() {
                              18 int secret = readPIN();
                              19 A a = new A();
                              20 B b = new B();
                              21 run(secret, b);
                              22 run(secret, b);
                              23 }
                              24 static void run(int x, A a) {
                              25 a.f(x);
                              26 }
                              27 }
```
**Listing 3.1:** An example which shows why dynamic dispatch must be handled correctly

interprocedural control-flow graphs presented in Definition 3.2 on page 46, this is no problem: The correspondence relation Φ discussed earlier relates call *edges* with return *edges* and not just nodes and, thus, also incorporates the call target in the identification of a call.

There exist several program analysis techniques for the approximation of dynamically dispatched method calls that I will discuss briefly in the following paragraphs. Since Java allows that class loading is deferred to runtime, they all assume that all classes are available for static analysis.

All of these techniques construct a directed graph, the *call graph* which reflects the calling structure of a given program. Its nodes correspond to the program's procedures and there is an edge from *p* to *p* ′ if *p* may call *p* ′ . Call graphs can be obtained both dynamically and statically. In this work, I only consider static call graphs and because of that I will omit the qualifier *static* from now on.

The analysis techniques that I will describe mainly differ in their *precision*, that is, their ability to approximate the possible targets of a call as closely as possible.

Precise resolution of dynamic dispatch is an important and critical feature for any static analysis aimed at a language like Java and in particular for static information flow analysis tools like Joana.

For one, the precision of dynamic dispatch resolution directly affects the precision of a static analysis: Suppose that the object b in line 22 is an instance of C instead of B. Then the program in Listing 3.1 would be secure because the secret is never printed. However, an analysis that fails to exclude B::f as possible call target in line 25 is also not able to rule out the execution of line 8.

Secondly, precise handling of dynamic dispatch is also important for scalability of a sound analysis: Consider a statement such as p.equals(q). Then a sound analysis has to assume that p may be an instance of any available class and that the call p.equals(q) resolves to every available implementation of equals – unless it is able to incorporate additional information about p. If such information is not exploited, the resulting call graph may be substantially bigger than necessary, practically prohibiting any further analysis for scalability reasons.

Particularly, the applications of Joana that I present in chapter 4 (see e.g. section 4.3 or section 4.7) rely on precise handling of dynamic dispatch.

**Figure 3.22:** Call graphs for the program from Listing 3.1 resulting from the application of different analyses

# **3.4.2.1 Class Hierarchy Analysis**

Perhaps the simplest non-trivial analysis for approximating dynamic dispatch is *class hierarchy analysis (CHA)* [52]. This analysis considers the inheritance relationships between classes and resolves a dynamically dispatched method call to the static call target C::f to all methods D::f such that D is a subclass of C that overrides method f.

Clearly, for class-based languages like Java, this rule is sound: If D' is an arbitrary class that is not a subclass of C, then no method D::f can be a valid call target for any call with static target C::f.

For the example in Listing 3.1, using CHA would resolve the call in line 25 to the possible call targets A::f, B::f and C::f. This results in the call graph depicted in Figure 3.22a. Note however that C::f is considered as a possible runtime call target even though C is never instantiated.

# **3.4.2.2 Rapid Type Analysis**

*Rapid Type Analysis* (RTA) [23] is an improvement of CHA which additionally takes instantiation into account: If *c* is a call site with static call target C::f and D is a subclass of C that implements f and is instantiated somewhere in the program, then D::f is considered a possible call target for *c*.

Implementations of RTA usually require a main entry point and use an iterative approach to compute the classes which are instantiated and the methods reachable according to the above rule.

RTA is obviously still sound for languages like Java: D::f cannot be called at runtime if D is never instantiated. Moreover, RTA always delivers a result that is at least as precise as CHA: If CHA does not consider D::f as possible runtime call target, then neither does RTA.

For the example in Listing 3.1, RTA finds out that C is not instantiated. Therefore, it resolves the call in line 25 to the possible call targets A::f and B::f, resulting in the call graph in Figure 3.22b. This shows that RTA can be more precise than CHA.

However, RTA still cannot rule out the case that A::f is called since A is instantiated.

# **3.4.2.3 Points-To Analysis**

Points-to analysis [16, 88, 158] is a general technique which aims to determine the possible runtime values of pointer variables. Among its numerous applications, points-to analysis can in particular be used to resolve static call targets [84].

Points-to analysis is concerned with the computation of *points-to graphs*, i.e. relations *PT* ⊆ P × I where P is a finite set of *abstract pointers* and I is a finite set of *abstract instances*.

For an abstract pointer *p*,

$$PT(p) \stackrel{def}{=} \{ i \mid (p, i) \in PT \}$$

is also called the *points-to set of p*. The commonly used intuitive meaning for *o* ∈ *PT*(*p*) is that *p may point to o at runtime* or, conversely, the intuitive meaning of *o* ∉ *PT*(*p*) is that *p* definitely does not point to *o* at runtime. This is specifically useful for call graph construction: in order to stay sound, one wants to rule out impossible call targets<sup>10</sup> .

With points-to information available, static call targets can be resolved as follows: Let *c*. *f*(*o*<sup>1</sup> , . . . , *on*) be a call site with static call target C::f. Then D::f may be an actual runtime call target if *c* may point to a *D* object. This is still sound: If *c* definitely never points to any *D* object, then D::f definitely is not a call target. Furthermore, points-to analysis always delivers a result which is at least as precise as the result of RTA: If *D* is not instantiated at all, then no reference can point to any *D* object.

Lastly, for the example in Listing 3.1, points-to analysis can find out that a in line 25 does not point to any instance of runtime type A or C. Hence, it can rule out A::f and C::f as runtime call targets for the call in line 25. This results in the call graph depicted in Figure 3.22c.

As I already mentioned, points-to analysis is a powerful analysis technique. Joana not only uses it to resolve dynamic dispatch but also for alias analysis. I will look at this more closely in subsection 3.4.4.

# **3.4.3 Exceptions**

*Exceptions* are Java's mechanism and language construct for handling errors at runtime. If a program encounters an erroneous state or condition, it can *throw* an exception that can be *caught* at some other place to handle the error gracefully. In Java, there are two kinds of exceptions. *Explicit* exceptions have to be declared, thrown and caught explicitly. The other kind, *implicit* exceptions, are thrown by the Java runtime environment in

<sup>10</sup>Note that the notion *p may point to o at runtime* does not say anything about whether this means "sometime at runtime" or whether this statement is bound to a specific point of the program. Further note that it not necessarily means that *p* actually will point to *o* at some point at runtime. It only means that it is not the case that *p* definitely does not point to *o* at runtime (bound to some specific point or not).

certain situations for which there is no sensible reaction. This includes environmental problems like memory shortage or input/output errors, but also the failure of single instructions because of programming errors. For example, a field access of the form a.x = y may fail because a is **null**, or an array access a[i] = o may fail because i is out of the bounds of a.

An overview of Java's language constructs in connection with exceptions is given in Figure 3.23.

From the point of view of a static information flow analysis, exceptions are challenging: On the one hand, they have to be properly dealt with in order to capture every possible program behavior. In particular, bytecode instructions like field or array accesses may cause exceptions that are not apparent from the program's bytecode. Therefore, a static information flow analysis must model the behavior of these bytecode instructions carefully. On the other hand, exceptions have to be handled with sufficient precision in order to not introduce too much spurious control-flow which in turn may cause many false alarms [103]. Particularly, Joana's precision is heavily affected by its handling of exceptions. For example, the successful verification of the case studies described in section 4.3 would not have been possible without precise exception handling.

Like Joana's exception analysis, I focus in the following on implicit exceptions that are caused by programming errors.

```
int readFile(File f)
throws IOException (1) {
 if (!f.exists()) {
   throw new IOException(); (2)
 }
 ...
}
int foo() {
 int[] arr = new int[5];
 ...
 return arr[6]; (3)
}
                                       void bar(File f) {
                                         try { (4)
                                           int x = readFile(f);
                                         } catch (IOException e) {
                                            (5)
                                         }
                                         println(foo()); (6)
                                       }
```
**Figure 3.23:** Overview of exceptions in Java: checked exceptions have to be declared (1), thrown (2) and handled (4)/(5); unchecked exceptions are thrown implicitly by the JVM and do not have to be handled (3)/(6)

**Figure 3.24:** Example for an information leak through exceptions

**Figure 3.25:** program dependence graph for the example in Figure 3.24 – the path from the high access in line 3 to 12 is highlighted in bold

Figure 3.24a shows an example for information flow that solely occurs because of implicit exceptions. The control-flow graph can be seen in Figure 3.24b and the program dependence graph in Figure 3.25.

With the assumption that the return values of inputPIN and inputLOW are not statically determinable, the program in Figure 3.24a has a control-flow graph like the one depicted in Figure 3.24b: In particular, this control-flow graph has a control-flow edge from line 10 to the exit node since the field access in line 10 may lead to a crash if a is **null**. If it fails, line 12 is not executed because the program crashes with a NullPointerException. This means that the value of secret influences whether line 12 is executed or not.

Consequently, the program dependence graph contains a control dependency from line 9 to line 12 that, together with the data dependency from line 3 to 9, constitutes a path from line 3 to line 12.

The example in Figure 3.24 shows that exceptions can induce a significant amount of additional control-flow and, thus, additional control dependencies. The control-flow from line 10 to the exit for example induces control-dependencies both from line 10 to 12 and from 9 to 12. If there were statements after line 12, then all these statements would also be control-dependent on both 9 and 10.

However, assume that inputLOW is never **null**. Then a cannot be null and therefore the program cannot crash. If static analysis cannot prove that inputLOW is never **null**, then it will report a false alarm. Therefore it can be beneficial to perform additional analyses that enable to safely rule out control-flow due to exceptions, e.g. by proving that certain pointer variables cannot be **null**.

Joana performs an analysis that rules out impossible null pointer exceptions, both intraprocedurally and interprocedurally. Its capabilities are illustrated in Figure 3.26:


Details about Joana's null pointer analysis can be found in the dissertation of Graf [78].

Apart from that, within the scope of the RS<sup>3</sup> project11, we also integrated an analysis to rule out exceptions due to out-of-bounds array accesses. I will briefly go into that in section 4.3.

<sup>11</sup>I will introduce and explain RS<sup>3</sup> in chapter 4.

```
1 class A {int x;}
2 void foo() {
3 int benign = inputLOW();
4 A a = null;
5 A b = new A();
6 if (benign > 1742) {
7 a = new A();
8 }
9 if (a != null) {
10 a.x = 42;
11 }
12 b.x = 42;
13 bar(b);
14 baz(b);
15 baz(null);
16 }
17 void bar(A a) {a.x = 42;}
18 void baz(A x) {a.x = 42;}
```
**Figure 3.26:** The capabilities of Joana's null pointer analysis

# **3.4.4 Objects**

For static information flow analysis, objects pose a number of challenges:


Figure 3.27 shows two example that illustrate these challenges. In Figure 3.27a we see a small program that stores high and low data within the same object, but in different fields. Hence, the print-statement in line 8 does not reveal high data. Figure 3.27b shows why it is important to handle aliasing properly: The print statement in line 10 obviously leaks the secret that was just stored in a1.x. Since after line 6, a1 and a3 refer to the same object, line 14 is illegal as well. In contrast, line 12 does not leak secret information since a1 and a2 refer to different objects and therefore line 8 does not influence the value of a2.x.

Joana handles all the examples shown in Figure 3.27 properly by carefully incorporating objects into its PDG representation. The full details can be found in the dissertations of Hammer [86] and Graf [78].

# **3.4.4.1 Heap Dependencies**

Central to this approach is the notion of *heap dependencies* that can be defined with the help of points-to analysis. I have already explained the basic intuition behind points-to analysis in subsection 3.4.2 and will go a bit deeper into it in subsubsection 3.4.4.3.

Heap dependencies can occur between statements that store to or read from the heap. A statement *s*<sup>2</sup> is called *heap-dependent* on a statement *s*<sup>1</sup> if *s*<sup>2</sup> may use a heap location that *s*<sup>1</sup> may have defined. For example, a

```
1 class A {int x; int y}
2 void foo() {
3 A a = new A();
4 a.y = 0;
5 int high = inputPIN();
6 a.x = high;
7 int low = a.y;
8 print(low); // OK
9 }
          (a) fields
                          1 class A {int x;}
                          2 void bar() {
                          3 A a1 = new A();
                          4 A a2 = new A();
                          5 a2.x = 0;
                          6 A a3 = a1;
                          7 int high = inputPIN();
                          8 a1.x = high;
                          9 int out1 = a1.x;
                         10 print(out1); // ILLEGAL
                         11 int out2 = a2.x;
                         12 print(out2); // OK
                         13 int out3 = a3.x;
                         14 print(out3); // ILLEGAL
                         15 }
                                   (b) aliasing
```
**Figure 3.27:** Two small example programs that illustrate aspects of objects that need to be handled properly by a static information flow analysis

statement *y* = *a*. *f* is heap-dependent on a statement *b*.*g* = *z*, if the fields *f* and *g* are the same and if *a* and *b* may point to the same object.

For illustration, consider Figure 3.28 and Figure 3.29, respectively.

Figure 3.28 shows the data dependency graph of the code example in Figure 3.27a. The statement low = a.y is heap-dependent on a.y = 0 since the former reads from the same heap location that a.y = 0 has written to. Also note that there is no heap dependency between the statements from a.x = high to low = a.y. Although they both access the object that is pointed to by a, they refer to different primitive fields within a. In Java, if an object has two fields of primitive type<sup>12</sup> with different names, they are known to reside in different memory locations.

Another example can be seen in Figure 3.29, which shows the data dependency graph of the code example in Figure 3.27b.

This example shows three heap dependencies. Most notably, the statement out3 = a3.x is heap-dependent on a1.x = high. This is because the points-to sets of a1 and a3 coincide and therefore have a non-empty intersection. Hence, we conclude that a3.x in out3 = a3.x may refer to the same heap location as a1.x in a1.x = high.

Note that the data dependency graphs in Figure 3.28 and Figure 3.29 are simplified. In fact, Joana represents field access not only by a single node but by multiple nodes. Mainly this is done to be able to distinguish between different information flow caused by field accesses. For example, the operation that reads an object's field's value from the heap is represented

**Figure 3.28:** Data dependency graph of Figure 3.27a

<sup>12</sup>The term *primitive type* is used in Java for non-object types like **int**, **double**, **char** or **boolean**.

**Figure 3.29:** Data dependency graph of Figure 3.27b

by a structure as depicted in Figure 3.30. This structure consists of four nodes: One node for the actual instruction, two nodes for the base object and the field, respectively, and an additional artificial exit node. Such a representation makes it possible to distinguish between three kinds of information flow: For one, there are data dependencies from the base object and the field to the actual instruction (the instruction uses both the base object and the field to read its value). Secondly, a data-heap dependency to the field node represents information flow through the heap. And last but not least, the field access operation may fail because the base object is **null**.

**Figure 3.30:** Joana's PDG node structures corresponding to the operation that reads an object's field's value from the heap (taken from [78, Figure 2.31])

**Figure 3.31:** How Joana incorporates objects into its parameter passing structures

Only the base object and the exit node are involved in this exceptional information flow.

# **3.4.4.2 Propagating Heap Access Across Procedures**

In order to also model field accesses across procedural boundaries, Joana incorporates them in the structures that represent interprocedural parameter passing. For every method m, additional formal parameter nodes represent the field accesses performed by m or by any method called directly or indirectly by m.

For this, Joana performs an *interprocedural side-e*ff*ect analysis*: For each method, all its field accesses are collected and summarized as formal parameter nodes: Each of these additional parameter nodes represents read or write access to a set of heap locations. Moreover, Joana relates parameter nodes by *parameter-structure edges*: Roughly, parameter node *p* is connected to a parameter node *p* ′ , if *p* ′ represents a field of *p*. More technically, parameter node *p* is connected to a parameter node *p* ′ if the heap locations of *p* ′ contain a field that can be obtained by dereferencing an object represented by *p*.

Consider Figure 3.31 for a simple example: Method bar writes the field x of its parameter a. Hence, bar has a formal-out parameter node for the field a.x. This node is propagated to callers, so for each of a's call sites, there is an actual-out parameter node corresponding to the formal-out parameter node for a.x. The formal-in node for bar's parameter a is connected to the formal-out node for a.x, since the latter represents accesses to the field x of a.

# **3.4.4.3 Points-to Analysis**

For the remainder of this section, I take a closer look at points-to analysis, which is not only a tool for constructing precise call graphs but also a key ingredient for analyzing information flows across the heap.

Points-to analysis as used by Joana is not a single, fully-determined analysis but rather a family of possible concrete points-to analyses where various aspects can be configured. Every choice has consequences with respect to the Joana's precision and runtime performance. Hence, the choice of points-to analysis is important for practical applications of Joana and deserve explanation. Particularly, I will illustrate that for demanding analysis clients such as information flow analysis, there is no perfect choice of points-to analysis.

**Abstractions** As I already mentioned in subsubsection 3.4.2.3, points-to analysis is concerned with the computation of *points-to graphs*, which are one-to-many relations between abstractions of pointer variables and abstraction of concrete object instances. These abstractions can be thought of as some kind of description generated from the static information available in the program's code.

Consider as an example Figure 3.32: Since n is a parameter, potentially infinite List objects are created in the loop. A common approach for points-to analyses is to represent all these objects by one abstract instance, described by something like "any instance of List that is instantiated in line 10". Furthermore, the potentially infinitely many incarnations of the local pointer variable pr are represented by one abstract pointer variable, described by something like "the pointer variable pr at any point in the method List::create".

**Sensitivities** Like all static analyses, points-to analyses are subject to several precision trade-offs. In the following, I look more closely at some of them.

```
1 class List {
2 int d;
3 List prev;
4 }
                  5 class Foo {
                  6 List create(int n) {
                  7 List cur = null;
                  8 for (int i = 0; i < n; i++) {
                  9 List pr = cur;
                  10 cur = new List();
                  11 cur.d = i;
                  12 cur.prev = pr;
                  13 }
                  14 return cur;
                  15 }
                  16 }
```
**Figure 3.32:** A code snippet that illustrates the abstractions in points-to analysis

**Flow-sensitivity** Recall the general descriptions in section 3.1: A flowsensitive points-to analysis takes into account the order of statements, whereas a flow-insensitive points-to analysis does not. Flow-insensitive points-to analyses usually compute one global points-to graph for the whole program, whereas flow-sensitive points-to analyses result in a separate points-to graph for each statement.

Figure 3.33 shows an example which highlights the effect of flow-sensitivity in points-to analysis: We see two code snippets there which are identical up to statement order. Flow-insensitive points-to analysis computes the same points-to graphs for both of them, whereas flow-sensitive analysis results in different points-to graphs.

Joana employs flow-insensitive points-to analysis. Some of the resulting precision loss is recovered by using *Static single assignment form* (SSA) [47, 38, 42], an intermediate representation which is widely used in compilers and program analysis tools. Specifically, it is employed by WALA and Joana which is why I want to describe it briefly in the following. The key property of SSA form is that every variable is assigned to at most once. This simplifies some program analyses. For example, the reaching definitions analysis described before becomes simpler because definitions do not need to be deleted anymore.

Every program can be transformed into SSA form. This transformation is usually performed on the given program's control-flow graph. The idea is to introduce a separate copy for each definition of a variable. At join

**Figure 3.33:** Two code snippets (upper part) and their points-to graphs (lower part) – c and d show the flow-sensitive points-to graphs at the end of the code in a and b, respectively. The points-graph in e results from flow-insensitive points-to analyses of both snippets.

**Figure 3.34:** Example showing a simple program and its SSA form

points, where several control-flow paths meet, special statements called Φ functions have to be inserted. An easy example is given in Figure 3.34: The statement y<sup>3</sup> = Φ(y1, y2) means that y<sup>3</sup> is either y<sup>1</sup> or y2, depending on which control-flow path was taken before. Figure 3.35 shows the control-flow graph of the running example in SSA form. At node 9, three Φ

statements<sup>13</sup> have to be inserted. For simplicity, I allow all these statements to be contained in the same basic block and assume that they are executed at the beginning of each loop iteration, just before the loop predicate is evaluated.

Consider Figure 3.36 for an example of how SSA form can affect points-to precision. It shows the code snippets of Figure 3.33 in SSA form and their flow-insensitive points-to graphs. It can be seen that the points-to graphs

**Figure 3.35:** The control-flow graph from Figure 3.3 in SSA form

<sup>13</sup>I use the symbol Φ here to be consistent with the literature. This is not to be confused with the correspondence function for interprocedural control-flow graphs.

differ and are almost the same as the flow-sensitive graphs depicted in Figure 3.33c and Figure 3.33d, respectively. Due to SSA form, it is made explicit which definition of a is used in the last two lines. Since there are two local variables for a now (one for each definition), there are also two points-to sets.

**Equality-Based vs. Subset-Based** Another precision trade-off for points-to analyses is whether they are *equality-based* [159] or *subset-based* [16]. Equality-based points-to analyses do not take into account the direction of assignments, whereas subset-based points-to analyses do. An example for this can be found in Figure 3.37. The two code snippets only differ in whether b is assigned to a or vice versa. Since subset-based points-to analysis is sensitive to this difference, it produces two different points-to graphs, whereas equality-based points-to analysis produces the

**Figure 3.36:** Effect of SSA form on flow-insensitive points-to analysis – The upper part shows the code snippets from Figure 3.33 in SSA form and the lower part the respective flow-insensitive points-to graph

same points-to graph for both snippets. Joana uses subset-based points-to analyses.

**Figure 3.37:** Subset-based (c, d) vs. equality-based (e) points-to analysis

**Context-Sensitivity** Finally, I want to consider the effect of contextsensitivity on points-to analyses and their client analyses, specifically on the heap dependency graph construction performed by Joana.

As already explained generally in section 3.1, a context-sensitive analysis not only analyzes the individual statements of a program but also takes into account their *execution context*14. For points-to analyses, the execution context can include


<sup>14</sup>Note that context-sensitive points-to analysis is usually not *fully* context-sensitive. This can be compared to the call string approach subsubsection 3.2.2.2 with limited stack depth.

Usually, the context information is incorporated in the abstract descriptions of pointers. The kind of used context information can have a profound effect on client analyses, especially on Joana's PDG construction algorithm.

This is illustrated by the example programs in Figure 3.38. Both programs are secure, since the value they print is not affected by the secret input. Let us first consider Figure 3.38a in more detail. If Joana analyzes this program with context-insensitive analysis, it yields a heap dependency graph like depicted in Figure 3.39a. This is caused by the fact that (a) the points-to analysis performed by Joana is flow-insensitive, (b) after sideeffect analysis is performed for each method, the same access summary is propagated to the callees without adapting it to the respective call site and

```
1 class A {
2 int x;
3
4
5
6 }
7 class CM1 {
8 void foo(int high) {
9 A a1 = new A(); // o1
10 a1.x = high;
11 A a2 = new A(); // o2
12 a2.x = 0;
13 modify(a1);
14 modify(a2);
15 int low = a2.x;
16 println(low);
17 }
18 void modify(A a) {
19 a.x++;
20 }
21 }
              (a)
                                1 class A {
                                2 int x;
                                3 void modify() {
                                4 a.x++;
                                5 }
                                6 }
                                7 class CM2 {
                                8 void foo(int high) {
                                9 A a1 = new A(); // o1
                               10 a1.x = high;
                               11 A a2 = new A(); // o2
                               12 a2.x = 0;
                               13 A a = random()>0.5?a1:a2;
                               14 a.modify();
                               15 int low = a2.x;
                               16 println(low);
                               17 }
                               18
                               19
                               20
                               21
                                             (b)
```
**Figure 3.38:** Two example programs showing the effect of context-sensitivity in points-to analysis on information flow analysis

**Figure 3.39:** Relevant section of the heap dependency graph of Figure 3.38a with different pointer analyses

(c) with context-insensitive points-to analysis, the parameter a of method modify is described by the same abstract pointer, regardless of the call site. Since both o1 and o2 may be passed to modify, a may point to both of them. Hence, according to the points-to information, the field access in line 19 may access both o1.x and o2.x. This information is propagated to both call sites of modify and, based on this information, Joana adds a heap dependency: one from line 10 to the actual-in node for a.x at the call in line 14 and one from the actual-out node for a.x at the call in line 13 to line 15. This constitutes a PDG path from line 10 to 15.

In contrast, such a mix-up does not occur when Joana analyzes the example with 1-CFA. Here, a heap dependency graph such as the one depicted in Figure 3.39b is obtained. The reason is that 1-CFA uses two different abstract pointers for the parameter a of modify, one for each call site. This way, both calls can be treated as if they called different methods, which each operate exclusively on o1 and o2, respectively. In effect, the access summaries become more precise, Joana does not add the spurious heap dependencies, so that line 10 and line 15 are not connected via heap dependencies. Note that object-sensitive points-to analysis yields the same result as context-insensitive points-to because modify is only called on one object.

However, there are also examples where no *k*-CFA helps: Such an example is shown in Figure 3.38b. Again, this program is secure as it always prints 0, regardless of the high value.

**Figure 3.40:** Relevant section of the heap dependency graph of Figure 3.38b with different pointer analyses

As the return value of random() cannot be statically determined, the pointsto set of a contains both o1 and o2, with any points-to analysis. Hence both o1 and o2 may be the receiver object of the call in line 14. Both contextinsensitive points-to and *k*-CFA merge o1 and o2 into the points-to set of the **this** pointer in modify. In particular *k*-CFA uses only one abstract pointer for **this**, since modify only has one call site. This leads to multiple spurious heap dependencies and a false alarm, as can be seen in Figure 3.40a.

In contrast, object-sensitive points-to analysis uses two different abstract pointers <o1,**this**> and <o2,**this**> for **this** and can distinguish between the call on o1 and the call on o2. The points-to set of <o1,**this**> only contains o1 and the points-to set of <o2,**this**> only contains <o2,**this**>.

Technically, Joana treats o1::modify and o2::modify as two different methods. Hence, it also computes two distinct access summaries that are both propagated to the only call site. In effect, Joana is able to keep the accesses on o1 and o2 separate. The heap dependency graph, which is shown in Figure 3.40b, does not contain the spurious heap dependencies of Figure 3.40a and hence also not the unnecessary heap dependency path.

```
1 class A {
2 B b;
3 void init(int x) {
4 B b = new B();
5 // <o1::init, o3>,
6 // <o2::init, o3>
7 b.x = x;
8 this.b = b;
9 }
10 }
11 class B {
12 int x;
13 }
                                13 class C {
                                14 void foo(int high) {
                                15 A a1 = new A(); // o1
                                16 A a2 = new A(); // o2
                                17 A a = random()>0.5?a1:a2;
                                18 a.init(high);
                                19 B b = a2.b;
                                20 int low = b.x;
                                21 print(low);
                                22 }
                                23
                                24 }
```
**Figure 3.41:** Effects of a context-sensitive heap model on Joana's PDG construction

Additional context information can not only improve the representation of abstract pointers, but also of abstract instances. A context-sensitive pointsto analysis that incorporates the context information in its representation of abstract instances is said to employ a *context-sensitive heap model*. With a context-sensitive heap model, more instances can be distinguished, which again can be beneficial for client analyses like Joana's PDG construction, especially when dealing with nested object structures.

Consider the example in Figure 3.41. We assume that Joana uses objectsensitive points-to analysis with a context-sensitive heap model. Because of object-sensitive abstract pointers, the two possible calls of A::init in line 18 are treated as calls to two different methods, o1::init and o2::init. This additional context information is used to split up the creation site of B in A::init. In o1::init, the local variable b points to <o1::init, o3> and in o2::init, it points to <o2::init, o3>. Hence the write accesses to b.x in line 7 can be separated and Joana is able to conclude that the heap location accessed in line 20 does not contain high information.

The examples I just presented not only show that context-sensitivity can have a positive effect on Joana's precision but also that there is no perfect choice of points-to analysis. Indeed, Figure 3.38a is an example that profits from the use of 1-CFA, whereas with object-sensitive points-to, Joana reports a false alarm. In contrast, Joana can verify the security of Figure 3.38b only with object-sensitive points-to, whereas no *k*-CFA provides enough context information to eliminate false alarms. Figure 3.41 illustrates the same phenomenon for context-sensitive heap models: Joana does not report an illegal flow with object-sensitive points-to but no *k*-CFA is sufficient for that. With a slight variation of Figure 3.41, we yield an example analogous to Figure 3.38a whose security can be proven with 1-CFA, while object-sensitivity has no positive effect in comparison to context-insensitive points-to.

**Performance Impact of Points-to Analysis** The examples also give an idea of another important effect of the choice of points-to analysis, namely the effect on the runtime performance of Joana. In my explanation of the examples, I mentioned two important aspects of Joana's modelling of objects and handling of points-to analysis:


These two aspects can cause a substantial amount of PDG nodes representing field accesses, in particular at parameter nodes at call sites. For example, this may lead to a significant rise in the memory and runtime performance of summary edge computation.

Therefore, the choice of the points-to analysis and how Joana incorporates points-to information into its structures plays an important role for applications such as those presented in chapter 3. For example, the case study described in section 4.7 could only be verified with object-sensitive points-to, and the PDG construction times differed significantly between different choices of points-to analysis. I will discuss this in section 4.7.

*Listen, do you want to know a secret?*

# <sup>T</sup>he <sup>B</sup>eatles **4**

# **Applications of J**oana **to Software Security**

In this chapter, I report on several applications of Joana within the scope of the *Reliably Secure Software Systems* priority program (RS<sup>3</sup> ) that ran from 2010 to 2017 and was funded by the German Research Foundation (DFG). In particular, I will focus on the contributions of the programming paradigms group of Prof. Dr.-Ing Gregor Snelting at KIT.

The chapter is organized as follows. In section 4.1 I give a general introduction into the RS<sup>3</sup> project and its motivations. After that, I describe various collaborations with other groups of RS<sup>3</sup> . These collaborations can be generally split into three categories:

	- **–** In section 4.3, I describe the *Security in E-Voting* scenario [41] and how Joana was combined with an interactive theorem prover to prove certain cryptographic properties of a prototypical electronic voting system.
	- **–** In section 4.4, I describe the reference scenario *Software Security for Mobile Devices* [20] and particularly how Joana was extended to show information flow properties of Android applications.
	- **–** We took part in the development of the *RS*<sup>3</sup> *Information Flow Language*(RIFL), a language that makes it possible to specify security requirements in a tool-independent, language-independent and machine-readable way. I describe RIFL and Joana's support for it in section 4.5.
	- **–** As an application of RIFL, several RS<sup>3</sup> research groups developed ifspec, a benchmark suite for information flow security. Together with two other tools, Joana and its Android variant Jodroid can be evaluated using ifspec. I go into the details of ifspec in section 4.6.
	- **–** In section 4.7, I report on the SHRIFT approach, which shows how a static information flow control tool like Joana can be applied to improve the performance, precision and thus the usability of system-wide usage control.
	- **–** Last but not least in section 4.8 I shortly report on a collaboration with the *Application-oriented Formal Verification* group at KIT that presents an approach to the modular security verification of component-based systems in which Joana was used to lower the burden for a first-order theorem prover.

# **4.1 General Description and Motivation of RS**<sup>3</sup>

In this subsection, I give an overview over the motivation, goals and structure of the RS<sup>3</sup> priority program. This overview is based on the descriptions that were given on the website [6] of RS<sup>3</sup> and on excerpts of the program's proposal, which also can be found on its website [4, 2]. The main thesis of RS<sup>3</sup> was that there is a need for complementing traditional approaches to IT security in order to give reliable security guarantees for complex software systems.

**RS**<sup>3</sup> In classical IT security approaches, mechanisms like authentication, cryptographic protocols are used to ensure that only *trusted* entities (e.g. programs) may perform actions in a given system. Trust is usually provided by some form of certificate that uses cryptographic signatures to prove the given entity's identity and integrity. Additionally, access control ensures that a given entity may only perform allowed actions.

Together, these techniques create a zone that is to a great extent protected from unknown and potentially malicious code. However, even with such a zone it remains unclear what guarantees can be given with respect to security. Once an entity has entered the trusted zone, it may perform all allowed actions. For example, it may combine these actions to do harm or to disclose information which are not supposed to be disclosed. In consequence, trust-based and mechanism-oriented approaches cannot protect from malicious entities that are falsely trusted.

Therefore, such approaches need to be complemented by *property-oriented solutions*: Such solutions concentrate on (a) formalizing the security requirements of a given system in the form of properties and (b) providing methods for verifying that the given system enjoys these properties.

Focusing on properties and their verification offers a number of advantages: Firstly, security properties can be rigorously analyzed (e.g. for contradictions). Moreover, precise and well-defined security guarantees can be given if a system can be rigorously shown to satisfy a given property.

Therefore, the main goal of RS<sup>3</sup> was to develop concepts and techniques that enable trustworthy certification of system-wide, technical security requirements and which adequately respect the semantics of programs. In order to achieve this goal, RS<sup>3</sup> was driven by three guiding themes:

1. the development of precisely defined security properties; such properties enable to formalize and hence reason about security, requirements for a given system, just like it is common for functional requirements.

2. the development of program analysis methods and tools for the verification of security properties; ideally, these techniques are sound, scalable and usable, and

3. the development of concepts for understanding and certifying security aspects in complex software systems.

# **4.2 The Sub-Project "Information Flow Control for Mobile Components"**

*Information Flow Control For Mobile Components* (IFC4MC) was a sub-project within RS<sup>3</sup> that comprised the programming paradigms group at KIT and the Software Construction and Verification group at the university of Münster.

I focus on the KIT side of the projects. From our point of view, IFC4MC was concerned with three main topics:

1. information flow properties that are suitable for modern program structures like concurrent programs


In the following, I give an overview of the achievements in the first two items. The first two items were first tackled by Giffhorn [65, 66]. The third item is considered in detail in the dissertation of Graf [78].

# **4.2.1 Information Flow Properties for Concurrent Programs**

The security properties that we were mainly concerned with are *noninterference-like*. Generally, non-interference-like properties demand that high input does not influence low-observable program behavior. For sequential and deterministic batch-like programs, this essentially means that if the program is applied to two states that only differ in high variables, the result states also only differ in high variables.

However, this is insufficient for advanced programming language features like concurrency. In contrast to sequential programs, *concurrent*, or *multithreaded* programs are composed of multiple *threads*, sub-programs that run simultaneously and may or may not be executed on multiple processors. A *scheduler* periodically distributes threads to the available processors. Consequently, if no particular scheduler is assumed, concurrent programs have to be considered as *non-deterministic* in the sense that a given input may lead to multiple possible program behaviors. This has to be accounted for when designing appropriate security properties.

Figure 4.1a and Figure 4.1b show examples that illustrate different types of possible information leaks in concurrent programs.

Figure 4.1a contains one simple explicit (A) and one simple implicit (B) leak. These two types of information flow also can occur in sequential programs. The example also contains a third leak (C) that can also be considered an explicit information flow but crosses thread borders: It may occur since statement (D) may be executed by thread t1 before the main threads executes statement (C).

Another type of leak is shown in Figure 4.1b. This example neither contains an explicit nor an implicit information leak. However, there are two output statements which may be observed by a low attacker and whose execution order may reveal something about high data: Observe that t2 is delayed by a loop whose execution time directly depends on the high input pin. Hence, if we assume a scheduler that after each step chooses the next active thread by fair dice roll, then the larger the pin is, the more likely it is that t1 executes its output statement before t2 does.

The notion that captures such considerations is *probabilistic non-interference*. The idea here is to consider the probability distribution of possible program behaviors. Probabilistic non-interference then demands that if two inputs

```
main:
 pin = input(HIGH)
 spawn t1
 output(LOW, 42 * pin + 17) (A)
 if (pin > 0) {
   output(LOW, pin) (B)
 }
 output(LOW, x) (C)
t1:
 x = pin (D)
                    (a)
                                                    main:
                                                      spawn t1
                                                      pin = input(HIGH)
                                                      while (pin > 0) {
                                                        pin--
                                                      }
                                                      spawn t2
                                                    t1: output(LOW, 0)
                                                    t2: output(LOW, 1)
                                                            (b)
```
**Figure 4.1:** Examples for different types of leaks that can occur in concurrent programs: a shows explicit information flows within (A) and across (C,D) thread borders and an implicit information flow (B); b shows a truly probabilistic leak: the larger the value of pin the more likely it is that 0 is output before 1

only differ in high parts, then the probabilities of the resulting lowobservable program behavior is the same.

Let *Tr* be the set of program behaviors, also called *traces*, and *I* the set of inputs. A trace can be thought of to be a sequence of operations that accurately describes what a program does and how the memory contents develop. Some operations are used for input and output. We assume that input and output operations (also called input events and output events, respectively) use different channels for high and low observers.

An input *i* ∈ *I* leads to multiple possible traces *t* ∈ *Tr*(*i*) ⊆ *Tr*. Each of these traces has an occurrence probability<sup>15</sup> *P<sup>i</sup>* (*t*).

An attacker does not see the full trace but only its so-called *low-observable* part. This is modeled by a function *E<sup>L</sup>* : *Tr* → *Tr* that strips off the parts of a trace that cannot be observed by a low attacker, like output events on high channels or high parts of the memory.

The attacker also only sees some part of the input, namely those input events that operate on the low channel. To model this, we overload *E<sup>L</sup>* to *E<sup>L</sup>* : *I* → *I* that strips off high parts of inputs.

Hence, if program P is run with input *i* and exhibits program behavior *t*, then a low observer only sees the low part *i<sup>L</sup>* = *EL*(*i*) of that input and observes that P exhibits *t<sup>L</sup>* = *EL*(*t*). From this, they can conclude that the input must have been some *i* ′ ∈ *E* −1 *L* (*iL*) and that some *t* ′ ∈ *E* −1 *L* (*tL*) must have been executed.

Now, probabilistic non-interference aims to ensure that the attacker cannot learn anything from that. The argument goes as follows: Assume that the attacker knows the probabilities *P<sup>i</sup>* ′(*E* −1 *L* (*tL*)) for all *i* ′ ∈ *E* −1 *L* (*iL*) and also assume that these probabilities differ. In other words, there are *i*0, *i*<sup>1</sup> ∈ *E* −1 *L* (*iL*) with *Pi*<sup>0</sup> (*E* −1 *L* (*tL*)) > *Pi*<sup>1</sup> (*E* −1 *L* (*tL*)). Then the attacker could conclude that it is more likely that the full input is *i*<sup>0</sup> than *i*<sup>1</sup> . In order to deprive the attacker of this possibility, probabilistic non-interference demands that *Pi*<sup>0</sup> (*E* −1 *L* (*tL*)) = *Pi*<sup>1</sup> (*E* −1 *L* (*tL*)) for all *i*0, *i*<sup>1</sup> ∈ *E* −1 *L* (*iL*).

The example in Figure 4.1b clearly violates probabilistic non-interference: The attacker may conclude from the low output 01 that the pin was probably large and from 10 that it was probably small.

<sup>15</sup>In general, *P<sup>i</sup>* assigns probabilities to *sets* of traces. We assume a countable set of traces, hence all *P<sup>i</sup>* are discrete probability distributions so that *P<sup>i</sup>* is fully specified by specifying it for single traces.

```
main:
  spawn t1
  spawn t2
t1: output(LOW, 0)
t2: output(LOW, 1)
      (a)
                                       main:
                                         spawn t1
                                         spawn t2
                                         pin = input(HIGH)
                                         while (pin > 0) {
                                           pin--
                                         }
                                       t1: output(LOW, 0)
                                       t2: output(LOW, 1)
                                             (b)
```
**Figure 4.2:** a: An example which passes Giffhorn's criterion but not LSOD – b: Giffhorn's criterion allows access to high input before the first lowobservable non-determinism occurs

Probabilistic non-interference is in general hard to verify directly as it requires to know probability distributions of traces which is why Giffhorn also considered sufficient criteria for it. One such criterion is *low-security observational determinism* (LSOD) [169], which essentially demands that a program only has one low-observable low-behavior for a given low-part of the input. If LSOD holds, then the probability distributions become very simple and probabilistic non-interference can be easily verified.

Central to LSOD is the observation that non-determinism manifests itself in the form of *conflicts*. Two operations form a conflict if they may be executed in multiple orders. For instance, if two statements *s*<sup>1</sup> and *s*<sup>2</sup> are composed concurrently then the scheduler may decide to run either of them first so that both the execution orders *s*<sup>1</sup> ,*s*<sup>2</sup> and *s*2,*s*<sup>1</sup> are possible. Apart from the absence of explicit and implicit leaks, LSOD also demands that there is *no* conflict of low-observable events. For instance, the example Figure 4.1b is clearly rejected by LSOD since it contains two conflicting low-output statements.

One particularly pleasant property of LSOD is that it is *schedulerindependent*. This means that no assumptions on the scheduler are necessary for probabilistic non-interference to hold. Therefore, a respective security certificate can be re-used when changing the environment.

In his dissertation [65], Giffhorn showed that LSOD can be checked using program dependence graphs.

**Figure 4.3:** Idea of the RLSOD improvement

However, LSOD is also very restrictive: It essentially forbids any lowobservable non-determinism, which is often the very motivation for designing a system in a concurrent way in the first place. So, even a program that does not access high data, like the one in Figure 4.2a, violates LSOD if it contains conflicts.

This is why Giffhorn also proposed a slight improvement on LSOD. His idea was to allow conflicts if they are not influenced by high data. One example of this can be seen in Figure 4.2b.

In later work [40, 31], we took this as a starting point for improvements on LSOD. The central idea here was to push the "influence sphere" of high events on conflicts even further into the direction of conflicts. We observed that, under some assumptions about the scheduler, more conflicts can be considered benign. Key to this observation is the notion of *common dynamic ancestors (cda)*: For two statements *m*, *n*, a statement *c* is called *common dynamic ancestor* of *m* and *n* if (a) *c* is a dominator for *m* and *n*, i.e. if every control-flow path to *m* or *n* must traverse *c* first and (b) if *c* is guaranteed to never execute concurrently to *m* and *n*. Hence, a common dynamic ancestor of *m* and *n* is any point in the program which is guaranteed to be executed before *m* and *n*.

Now the idea of RLSOD, which is sketched in Figure 4.3, is as follows: If *c* is a common dynamic ancestor of two conflicting statements *m* and *n*, any statement *s* that is guaranteed to be executed before *c* can only delay both *m* and *n* but cannot determine in which order *m* and *n* are executed. Hence *s* can safely be allowed to be influenced by high input. Consequently, it

```
main:
 pin = input(HIGH)
 while (pin > 0) {
   pin--
 }
spawn t1
spawn t2
t1: output(LOW, 0)
t2: output(LOW, 1)
```
**Listing 4.1:** An example which passes RLSOD but not Giffhorn's criterion

suffices to check that no statement *s* that lies on some control-flow path between *c* and *m* or *c* and *n* may be influenced by high input.

As an example, consider the program in Listing 4.1: It contains a lowobservable conflict after high data is accessed, hence it is rejected by Giffhorn's criterion. However, this high access is guaranteed to execute before t1 is spawned – a point in the program that is a common dynamic ancestor of the two output statements. Such high access is allowed by RLSOD and hence the conflict can be considered benign. As we showed in Bischof et al. [31], this consideration works with every common dynamic ancestor. Indeed, the RLSOD check can be specified with respect to a function *cda* which assigns every two statements *m*, *n* a common dynamic ancestor. The closer *cda*(*m*, *n*) is to *m* and *n*, the larger the portion of the program on which influence of high input is allowed can be and, hence, the more precise the check becomes.

An imprecise yet safe choice is to always use the start point of the program as *cda*. With this choice of the *cda* function, the RLSOD check becomes essentially Giffhorn's criterion.

# **4.2.2 Modeling and Analyzing Concurrency in Program Dependence Graphs**

Basically, PDGs for multi-threaded programs can be obtained by computing a PDG for each thread and connecting these sub-PDGs using *interference edges*, an additional kind of edge that captures inter-thread data dependencies.

**Figure 4.4:** PDG of Figure 4.1a with interference edges

To illustrate this kind of dependency, consider again Figure 4.1a. The information leak in statement (C) occurs because statement (D) may be executed before statement (C) and therefore the high value pin is first written into the variable x and then output to a low channel. This is a special kind of data dependency that crosses thread borders and is called *interference dependency*. The PDG of Figure 4.1a can be found in Figure 4.4. As defined formally by Giffhorn [65, Definition 3.8], statement *s*<sup>2</sup> is interference-dependent on *s*<sup>1</sup> , if *s*<sup>2</sup> may use a value which *s*<sup>1</sup> computes and *s*<sup>1</sup> and *s*<sup>2</sup> *may happen in parallel*. Two statements *s*<sup>1</sup> and *s*<sup>2</sup> *may happen in parallel* if it is both possible that *s*<sup>1</sup> is scheduled before *s*<sup>2</sup> and that *s*<sup>2</sup> is scheduled before *s*<sup>1</sup> .

Due to decidability reasons, may-happen-in-parallel information must be approximated. To yield a sound analysis, this approximation is conservative in the sense that the statically computed relation *MHP* has the property:

> *s*<sup>1</sup> and *s*<sup>2</sup> may happen in parallel =⇒ *MHP*(*s*<sup>1</sup> ,*s*2)

That is, if ¬*MHP*(*s*<sup>1</sup> ,*s*2), then only one execution order of *s*<sup>1</sup> and *s*<sup>2</sup> is allowed. Conversely, however, it may be the case that *MHP*(*s*<sup>1</sup> ,*s*2) holds even though *s*<sup>1</sup> and *s*<sup>2</sup> can only occur in one particular execution order. In his dissertation [65], Giffhorn describes a fairly sophisticated MHP analysis that takes into account (a) definite execution orders that can be inferred from the program's control-flow and (b) thread creation and joining.

This MHP analysis can be further improved by also taking *concurrency control mechanisms* like *locks* into account:

For example, in Java one can use *synchronization on objects' monitors*, a simple locking mechanism to achieve mutual exclusion, to ensure that for critical sections one thread at a time can be active. Threads that are about to enter such a critical section while another thread is active have to wait until the active thread is finished. This way, the possible interleavings can be restricted.

A MHP analysis that takes into account locking will consider less statements to run in unspecified order and hence be more precise.

One static analysis formalism which can model concurrency and in particular locks is *dynamic push-down networks* (DPNs) [37, 118]. Roughly, DPNs represent programs with multiple threads by a series of call stacks and are able to model dynamic thread creation, unbounded recursion and finite abstractions of thread-local and procedure-local state. Moreover, using tree automata techniques, DPNs can be used to check whether a multi-threaded program has lock-sensitive executions with given properties [63], like MHP information. Furthermore, by iterated analysis [131], DPNs can also be used to compute interference dependencies directly.

We combined DPNs and PDGs to remove interferences which in fact do not occur due to locking [80]. This is especially beneficial in situations where locking is actually used to impose definite execution orders.

# **4.3 Reference Scenario "Security in E-Voting"**

In this section, I describe the reference scenario *Security in E-Voting*. This description is based on various RS<sup>3</sup> publications [5, 41, 115]. First, I give a short motivation. After that, I give an overview of the contributions of the reference scenario overall and the contributions of the programming paradigms group in particular.

# **4.3.1 Motivation**

In recent years, more and more elections are conducted electronically. This includes national and municipal elections, as well as elections within associations, societies, and companies. There are two main categories of such systems: The first kind consists of electronic voting machines like recording electronic voting systems and scanners that are usually installed in polling stations. The other kind is remote electronic voting systems that are used by voters to vote over the internet using their own devices like desktop computers or smart-phones.

Since elections are a critical part of democracies, it is crucial that they are held in a way that satisfies some basic properties. Two such properties are:

Privacy The system ensures that the voters' votes remain confidential.

Verifiability Voters have the possibility to check that their choices have been properly counted.

In traditional elections, these properties are usually sufficiently ensured by providing voting booths that are not observable from outside or by making the counting public.

E-voting systems also aim for such properties and ideally, the developers of E-Voting systems can be presumed to be benevolent. However, it is also true that E-Voting systems are complex hardware and software systems and as in all such systems programming errors can hardly be avoided. Verification techniques and procedures are therefore used to ensure that these systems enjoy particular security properties.

In this reference scenario, we consider the verification of privacy properties of Java programs that use cryptographic operations, where the final aim is to provide strong cryptographic guarantees on the code of a fully fledged remote e-voting system which is designed to provide confidentiality of the votes.

# **4.3.2 CVJ Framework**

As a first step, Küsters, Truderung Graf and Scapin proposed the *CVJ framework* for the cryptographic analysis of Java programs that use cryptographic primitives [116, 114]. This framework enables existing tools that can check non-interference properties forJava programs, but a priori cannot deal with cryptography, to establish cryptographic indistinguishability properties at the code level.

The CVJ framework combines techniques from program analysis and *universal composability* [43, 134, 113], a well-established concept in cryptography. CVJ works in two steps. The idea of the first step, which is

**Figure 4.5:** Visualization of the general approach employed to verify the security of the E-Voting system [115, Figure 1]

illustrated on the left side of Figure 4.5, is to check non-interference properties for the Java program to be analyzed where cryptographic operations such as encryption are performed within so-called *ideal functionalities*.

A given <sup>J</sup>ava program <sup>P</sup>*real* (the box on the left in Figure 4.5) that uses real cryptographic primitives is transformed into a <sup>J</sup>ava program <sup>P</sup>*ideal* (the box in the middle of Figure 4.5), where the real cryptographic primitives are replaced by idealized primitives. These idealized primitives provide guarantees in face of unbounded adversaries and can often be formulated without probabilistic operations. Therefore, they can be analyzed by tools that cannot deal with security notions specific to cryptography (probabilities, polynomially bounded adversaries). The results of the CVJ framework imply that if P*ideal* is non-interferent, then the original <sup>J</sup>ava program <sup>P</sup>*real* (using actual cryptographic operations) enjoys strong cryptographic indistinguishability.

In addition to the reduction of a cryptographic verification task to an ordinary non-interference check, the CVJ framework also consists of a second step that is illustrated on the right side of Figure 4.5 and tackles the problem that the systems to be analyzed are often *open*: They interact, for example, with an untrusted (and unspecified) network. Analysis tools such as Joana however can only deal with *closed* Java programs, in this case the combination of the open system with one particular environment. Therefore, the CVJ framework also provides a proof technique that enables program analysis tools to verify non-interference properties of open systems. Such an open system is non-interferent if the combination of this system with any environment (which is closed) is non-interferent in

**Figure 4.6:** Visualization of the hybrid approach

the ordinary sense. According to the CVJ framework, it is not necessary to check ordinary non-interference for *all* environments, rather it suffices to establish non-interference for a carefully designed family of environments. These environments only differ in their inputs, so that they can be expressed as one parameterized environment (see box on right-hand side of Figure 4.5).

Graf [78] shows that this works in particular for PDG-based tools such as Joana.

# **4.3.3 The Hybrid Approach**

In summary, the CVJ framework in principle enables a static analysis tool that can verify unrestricted non-interference to perform cryptographic analyses on programs using cryptographic primitives. However, an automatic tool like Joana cannot be sound and completely precise at the same time. Joana in particular may report *false alarms*, i.e. falsely reject a program which is actually non-interferent.

To remedy this, Küsters et al. propose the *hybrid approach* [115], which combines the strengths of automatic information-flow analysis tools with the strengths of interactive theorem provers. This was joint work of the groups of Küsters, Snelting and Beckert, to which I contributed the Joana part of the verification of the case study. In our work [115], we demonstrate the hybrid approach on a case study, a small prototypical e-voting system. In this case study, we combined Joana with KeY [11], a theorem-prover for Java.

Figure 4.6 illustrates how the hybrid approach works: The task is to verify non-interference of a given (Java-like) program and we assume that the security of this program as-is cannot be verified using a given automatic tool. The hybrid approach now works in two steps:

1. Additional code is added to the program which makes explicit to the automatic tool that the falsely reported illegal flow is in fact not present.

2. It is shown that the modifications of step 1 satisfy the requirements for a *conservative extension*. Apart from certain syntactical restrictions, this means that the essential behavior of the program has not been changed by the modification. This boils down to verifying a functional property of the given program, a task that can be performed using an interactive theorem prover.

For reference, I cite the definitions of extensions and conservativity here.

**Definition 4.1** (Extension [115, Definition 1])**.** *Let P* = *P*[⃗*x*] *be a deterministic and closed (Jinja*+ *<sup>16</sup>) program. An* extension of *P is a program P* ′ = *P* ′ [⃗*x*] *obtained from P in the following way. First, a new component M is added to P consisting of some number of classes with the following properties:*

*(i) the methods and fields of the classes in M are static,*

*(ii) the arguments and the results of the methods of M are of primitive types,*

*(iii) the methods of M do not refer to classes defined in P (in particular, no methods and fields of P are used in M),*

*(iv) all potential exceptions are caught inside M,*

*(v) all methods of M always terminate.*

*Second, P is extended by adding statements of the following form in arbitrary places within methods of P:*

<sup>16</sup>*Jinja* [105] is a java-like programming language which is equipped with a formal semantics to make it accessible for reasoning with a theorem prover. *Jinja*+ [116] extends Jinja by various features which are useful in the context of the CVJ framework.

*(a) (output to M)*

$$\text{(4.1)}\tag{4.1}$$

*where C is a class in M with a (static) method f and e*<sup>1</sup> , . . . ,*en are expressions without side e*ff*ects.*

$$(b) \text{ (input from M)}$$

$$(4.2)\tag{4.2}$$

$$r = \mathbb{C}. f(e\_1, \dots, e\_n)\_{r}$$

*where C is a class in M, C*. *f is a (static) method with some (primitive) return type* τ*, e*<sup>1</sup> , . . . ,*en are expressions as above, and r is an expression that evaluates without side e*ff*ects to a reference of type* τ*. (Such an expression can, for example, be a variable or an expression of the form o*.*x, where o is an object with field x.)*

**Definition 4.2** (Conservative extension [115, Definition 2])**.** *An extension P* ′ [⃗*x*] *of P*[⃗*x*] *is called a* conservative extension *of P*[⃗*x*]*, if for all initial values* ⃗*a of high variables* ⃗*x the following is true in the run of P* ′ [⃗*a*]*: Whenever a statement of the form* (4.2) *is executed, it does not change the value of r. That is, the value of r right* before *the execution of the assignment coincides with the value returned by the method call C*. *f*(*e*<sup>1</sup> , . . . ,*en*)*. As such, statement* (4.2) *is redundant.*

Consider the example in Listing 4.2. We assume the security policy that the initial value of secret must not influence the final value of pub and that bar does not violate this policy. Then the program is secure: Although secret is added to the result of the method foo in line 12, it actually has no influence on it. This is because line 12 is only executed if secret==0, but has no effect in this case. However, a static information flow tool like Joana reports an illegal flow because it does not reason about values and assumes that the final value of b (and therefore the final value of pub) may be influenced by the initial value of secret. Now consider Listing 4.3. Here, the program from Listing 4.2 has been modified in such a way that Joana is able to verify non-interference: The value of b is saved (line 12) before it is possibly incremented (line 13) and overwritten afterwards (line line 14). Using a local killing definitions analysis [78], Joana is able to detect that b is indeed overwritten in line 14 and can correctly verify that the modified program is secure. According to the hybrid approach, it remains to be shown that Listing 4.3 is a conservative extension of Listing 4.2. It is easy to see that Listing 4.3 is an extension of Listing 4.2. It remains to be shown

```
1 class Example {
2 static public int pub;
3 static private int a;
4 public static void main(int secret) {
5 a = 42;
6 bar(secret);
7 int b = foo(secret);
8 pub = b;
9 }
10 static int foo(int secret) {
11 int b = a;
12 if (secret==0) b+=secret;
13 return b;
14 }
15 static void bar(int secret) {
16 ...
17 }
18 }
```
### **Listing 4.2:** A secure program for which Joana reports a false alarm (adapted from [115], p. 309)

that this extension is indeed conservative. This boils down to proving that in line 14 the value returned by M.get() equals the value of b just before the execution of line 14. This can be done for example with an interactive theorem prover like KeY.

# **4.3.4 Case Study: E-Voting Machine**

Within the scope of our work [115], we demonstrated the hybrid approach on a small prototypical e-voting machine, using Joana as automatic information flow control tool and KeY as a theorem prover for the subsequent proof of conservativity.

We extended the program conservatively and proved a non-interference property with Joana (and the CVJ framework). The size of the conservative extension was 934 lines of code (LoC). The non-interference property that Joana had to verify essentially says that the voters' choices have no

```
1 class Example {
2 static public int pub;
3 static private int a;
4 public static void main(int secret) {
5 a = 42;
6 bar(secret);
7 int b = foo(secret);
8 pub = b;
9 }
10 static int foo(int secret) {
11 int b = a;
12 M.set(b);
13 if (secret==0) b+=secret;
14 b = M.get();
15 return b;
16 }
17 static void bar(int secret) {
18 ...
19 }
20 }
21 class M {
22 static int x;
23 public static void set(int n) { x=n; }
24 public static int get() { return x; }
25 }
```
**Listing 4.3:** An extension of the program in Listing 4.2 which makes the absence of illegal information flow explicit

influence to the low output of the system<sup>17</sup> . Joana could verify noninterference of this program in about 18 seconds on a standard laptop (Core i5 2.5GHz, 8GB RAM). To conduct the analysis, we wrote a small driver program (about 60 LoC) which sets various configuration options of Joana, initiates the PDG construction, identifies and annotates the appropriate nodes in the PDG, and triggers the information flow analysis.

For full details, I refer the interested reader to the original article [115].

Apart from the adaptions to obtain the conservative extension, the further small adaptions of the e-voting machine were necessary for Joana to verify the system. In the following, I want to elaborate a bit on these adaptions.

```
1 for( int i=0; i<N; ++i ) {
2 switch( actions[i] ) {
3 case 0: // next voter votes
4 if (voterNr<numberOfVoters) {
5 int choice = secret ? choices0[voterNr]:choices1[voterNr];
6 vm.collectBallot(choice);
7 ++voterNr;
8 }
9 break;
10 [...]
11 }
```
**Listing 4.4:** A code snippet from the case study of [115] which needed to be adapted

Listing 4.4 shows a critical code snippet from the case study which needed to be changed. The code is responsible for selecting a series of votes according to a secret bit. It processes two arrays of votes and, depending on secret, one of these arrays is chosen to be the array of votes that is processed subsequently.

The problem for Joana is that it does not reason about values or array bounds. As a consequence, it must assume that voterNr may be out of the bounds of the arrays choices0 and choices1. This means that both branches of the statement in line 5 may throw an ArrayIndexOutOfBoundsException. Hence, all the program's statements after line 5 are control-dependent on both branches of 5.

Furthermore, both branches are control-dependent on the secret bit. As a consequence, every statement which is executed after line 5, including public outputs, is dependent on secret, so that Joana is not able to prove any reasonable non-interference property.

However, the code snippet could be modified as shown in Listing 4.5. Here, both possible choices are loaded from the two arrays in lines 5 and 6 before the actual decision is made in line 7.

Since we assume a single-threaded environment, neither choices0 nor choices1 can change after lines 5/6, so that the code snippet in Listing 4.5

```
1 for(int i=0; i<N; ++i ) {
2 switch( actions[i] ) {
3 case 0: // next voter votes
4 if (voterNr<numberOfVoters) {
5 int choice0 = choices0[voterNr];
6 int choice1 = choices1[voterNr];
7 int choice = secret ? choice0 : choice1;
8 vm.collectBallot(choice);
9 ++voterNr;
10 }
11 break;
12 [...]
13 }
14 }
```
**Listing 4.5:** A code snippet from Listing 4.4 with a small but critical modification

is equivalent to the one in Listing 4.4. Moreover, the fatal chain of dependencies described before is prevented: Lines 5 and 6 may still throw an ArrayIndexOutOfBoundsException but this is independent of secret. Plus, line 7 does not throw any exception.

Listing 4.6 shows another critical code snippet that needed to be adapted. The method is called only with valid values of votersChoice, i.e. with values between 0 and numberOfCandidates - 1, where the field numberOfCandidates coincides with votersForCandidates.length. Formally, an exception is thrown if votersChoice is outside the desired range, but this actually never happens for this program. Consequently, since votersChoice is within the bounds of votesForCandidates, the array access succeeds.

For Joana, there are two problems here. Firstly, whether the exception is thrown depends on the value votersChoice. This is a problem since

```
1 public int collectBallot(int votersChoice) throws InvalidVote {
2 if ( votersChoice < 0 || votersChoice >= numberOfCandidates ) {
3 throw new InvalidVote();
4 }
5 votesForCandidates[votersChoice]++;
6 }
```
**Listing 4.6:** Another critical code snippet from the E-Voting Machine case study

votersChoice depends on the secret bit and Joana does not know here that votersChoice is actually within bounds. Hence, as before, the program may possibly crash dependent on the secret bit, which precludes any sensible non-interference property. The other problem is very similar: Joana assumes that the array access may fail and since votersChoice is considered secret, Joana must assume that the program may crash depending on the secret value.

Our solutions to these problems were

1. remove lines 2 and 3; and

2. surround line 5 with a **try**..**catch**-block (with empty catch clause) which catches all Throwables, which effectively suppresses all possible exceptions which may occur there.

It remained to show that the method indeed never throws any exception if called with valid values of votersChoice. This was done during the KeY proof phase.

Apart from the two code snippets that I just discussed there were also others which prevented Joana from showing the desired non-interference property because of possibly invalid array accesses. This motivated us to introduce a simple array analysis into the WALA framework18. This analysis was implemented by a student researcher under my supervision. It is based on the ABCD analysis by Bodík et al. [34] and can prove at least for simple cases that array accesses are valid and that a crash cannot happen19. It was integrated into WALA in 2016 [69] and Joana can be configured to use it. Examples of what the analysis can and cannot recognize can be found on Github [67, 68].

# **4.3.5 Spec Slicing**

Within the scope of the case study, we observed in the KeY proof part that there are situations in which a given KeY specification only covers a small part of the program or, in other words, significant parts of the given

<sup>18</sup>In section 3.4, I mentioned that Joana makes heavy use of WALA for analyzing Java bytecode.

<sup>19</sup>Note that due to time constraints the analysis could not be applied to the E-Voting reference scenario.

program are irrelevant to the specification. KeY specifications tend to be very complex and detailed and their proof may require a considerable amount of manual interaction. This motivated us to develop a method to decrease the amount of proof work that needs to be performed.

**Figure 4.7:** Visualization of spec slicing

We devised a general technique which we call *spec slicing* [41]. The idea behind spec slicing is illustrated in Figure 4.7: If parts of the program do not influence the final state with respect to the proof obligation, they can be safely removed and functional verification can be performed on the simpler program.

Verification of this simpler program can then be performed without any loss of precision but with possibly much less effort. The identification and removal of irrelevant program parts can be performed by an automatic tool like Joana. We already applied this technique to the E-Voting machine mentioned above: Parts of its implementation perform mere logging, which does not affect the voting result and therefore does not have any influence on the overall functional property which had to be verified. Using Joana, we gained a simpler but equivalent program without logging, which we verified using KeY to establish functional correctness for the whole program.

# **4.4 Reference Scenario "Software Security for Mobile Devices"**

The programming paradigms group also participated in the RS<sup>3</sup> Reference Scenario "Software Security for Mobile Devices" (SSMD). The goal of this reference scenario was to develop an app store that offers apps with security guarantees. The reference scenario combined several security techniques developed in the scope of RS<sup>3</sup> , including static analysis with PDGs and type systems, secure modeling and runtime enforcement. In the following, I describe the context of the reference scenario and its main result, the RS<sup>3</sup> certifying app store. After that, I focus on the contributions of the programming paradigms group. In the scope of this scenario, we developed Jodroid, an extension of Joana with support for the specialties of Android apps. I will describe the specifics of Jodroid and will elaborate on the challenges which have already been addressed and also on the challenges which remain open until this point.

# **4.4.1 Motivation**

In today's world, smartphones and other mobile devices are ubiquitous. They are used to store and process a wide variety of personal and sensitive data, including contacts, location, financial and health information and hence can be considered security-critical infrastructure.

The most commonly used mobile operating system is Android, with a market share of 86.6% at the end of 2019 [1] and currently over 2.8 million apps in its app store [18]. The Android ecosystem offers a number of security mechanisms, such as sandboxing of applications and a permission system restricting access to critical resources. Moreover, applications available on Google Play<sup>20</sup> are scanned to detect malicious behavior. At the same time, Android still has problems with security violations [162] These commonly take the form of the application revealing the user's sensitive information [133] or behaving in a way that is unexpected and harmful to the user [150].

<sup>20</sup>*Google Play* is Android's native app store.

This suggests that the security mechanisms employed in the Android ecosystem are not sufficient for enabling security-aware end users of Android devices to reliably enforce their personal security requirements. The RS<sup>3</sup> reference scenario "Software Security for Mobile Devices" aimed to offer a solution to these security problems by proposing an app store that provides user-definable security guarantees by integrating several of static and dynamic program analysis and security enforcement techniques.

# **4.4.2 The RS**<sup>3</sup> **Certifying App Store**

The artifact of the SSMD scenario was the *RS*<sup>3</sup> *certifying app store*. Its architecture is shown in Figure 4.8. Basically, it follows a client-server architecture.

**Figure 4.8:** Architecture of the RS<sup>3</sup> certifying app store [20, Fig. 1]

The client consists of an app store app that the user can use to download the Android applications, configure information flow policies (see Figure 4.9a) and run and view the results of various information flow analyses (see Figure 4.9b).

The app store integrates the different approaches of the various groups which contributed to the reference scenario. These approaches range from static analyses like PDGs and slicing or type systems to dynamic analyses combined with runtime enforcement.

Joana, or more specifically its Android variant Jodroid, is integrated in the server component: The user can specify an information flow policy and send an analysis request to the server. The server then computes the app's PDG and checks whether the given flow policy can be verified by using a PDG-based security check. The internal format of the specification is largely similar to the *RS*<sup>3</sup> *information flow language* that I will consider in section 4.5. There, I will also explain how Joana can be used to verify such policies.

**(a)** client-side policy editor

**(b)** diagram showing the information flows of an app

**Figure 4.9:** Screenshots from the client-side app store app of the RS<sup>3</sup> certifying app store (compare [120, Figure 1(b), Figure 1(d)] and [20, Figure 2])

# **4.4.3 J**odroid**: J**oana **for Android**

In the following, I describe Jodroid, an extension of Joana to support Android apps. The following text is largely based on an earlier publication[123]. The initial version of Jodroid has been implemented within the scope a diploma thesis under the supervision of our group [33]. The goal of extending Joana to handle Android apps poses several challenges. Among these challenges are the following:

1. Although Android apps are developed in Java, they are not compiled to Java bytecode but to Android's own Dalvik bytecode.

2. Standard Java applications use a single entry point (main), but Android apps have a multitude of possible entry points which are triggered by the Android system throughout the execution of the app

3. Android apps employ *intents*, a message-passing mechanism to exchange data and start external apps' components, which requires to also analyze information flows between apps.

These challenges are not specific to Android. For example, many Java applications with graphical user interfaces (GUI) also have multiple entry points which handle user input. However, different frameworks require different models specifying how these entry-points are used and Joana has no naturally built-in mechanism to specify such models21. Similar considerations can be made for intents: intents are comparable to other message-passing mechanisms which are commonly found in client-serverapplications but currently Joana does not provide a general mechanism which applies to a wide variety of message-passing mechanisms. We therefore consider our work on extending Joana to Android as a starting point to addressing these more general challenges.

In the following, I present the work we have already done to address the challenges just sketched. In section subsubsection 4.4.3.1, I give a short overview of architectural aspects of Android apps, in subsubsection 4.4.3.2, I outline how we address the above mentioned challenges. After that, I conclude in subsubsection 4.4.3.3 by giving an outlook on future work.

# **4.4.3.1 Overview of Android Applications**

In the following, I briefly discuss the architecture of Android applications. This overview is largely based on Android's API documentation [72]. An Android application usually consists of multiple, loosely coupled components. In the simplest case, these components can either be *Activities*, *Broadcast Receivers*, *Services* or *Content Providers*. I now give a quick summary of what these components do and which roles they play.

<sup>21</sup>This problem has been tackled in a bachelor's thesis under my supervision [19].

	- *Activities:* An activity is an application component that provides a screen with which users can interact in order to do something. Each activity is given a window in which to draw its user interface.
	- *Broadcast Receivers:* A broadcast receiver responds to system-wide broadcast announcements. Many broadcasts originate from the system, but can also be initiated by arbitrary app components. Broadcast receivers do not display a user interface. Typically, a broadcast receiver is just a "gateway" to other components and is intended to do a very minimal amount of work. For instance, it might initiate a service to perform some work based on the event.
	- *Services:* A service runs in the background to perform long-running operations or to perform work for remote processes. It does not provide a user interface. Another component, such as an activity, can start the service and let it run or bind to it in order to interact with it, using a special kind of inter-process communication.
	- *Content Providers:* Content providers manage access to a structured set of data. They encapsulate the data, and provide mechanisms for defining data security. Content providers are the standard interface that connects data in one process with code running in another process.

Android components use *intents* to exchange messages with each other. In particular, intents are used to start components. Intents can be *explicit*

**Figure 4.11:** Overview of the architecture of Joana

or *implicit*. Explicit intents specify a receiver, whereas implicit intents leave it up to the Android system and/or the user to resolve their receiver. Figure 4.10 (taken from Android's API documentation [73]) shows how Android processes an implicit intent.

# **4.4.3.2 Approach**

In this section, I explain how we address the challenges I mentioned in subsection 4.4.3.

**D**alvik **front-end** Figure 4.11 shows the general architecture of Joana. As can be seen in Figure 4.11, the PDG builder of Joana only depends on WALA's analysis results and hence is decoupled from WALA's front-end. As a consequence, we only needed to adapt WALA's front end to be able to process Android apps. For this, we integrated the WALA front end code of SCanDroid [62], a security analysis tool which is also based on WALA. This was a first step to extend Joana to handle also Android apps.

**Life cycle modelling.** Classic Java applications have a single entry point and every execution of the application starts with an invocation of this method. Clearly, this assumption is not met by Android apps: As we already mentioned, Android apps have multiple callbacks, which may be triggered by the user or the Android system as a reaction to certain events. However, the order and the way these callbacks are triggered is not arbitrary, but follows certain rules. More specifically, the components of an Android app are driven by their *life cycles*. The life cycle of an activity can be seen in Figure 4.12.

**Figure 4.12:** Lifecycle of an activity (see [74, Figure 1])

It is not an option to run a separate analysis for each entry point, since there may be information flows which only occur if multiple callbacks are executed in sequence. Listing 4.7 (adapted version of a sample from DroidBench [61]) presents an example.

```
1 public class MyActivity extends Activity {
2 static String addr =
3 "http://www.google.de/search?q=";
4 void onCreate(Bundle savedInstanceState) {
5 telephonyManager = (TelephonyManager)
6 getSystemService(
7 Context.TELEPHONY_SERVICE
8 );
9 /** retrieve secret data of telephone (source) */
10 imei = telephonyManager.getDeviceId();
11 /** extend request by secret data */
12 addr = URL.concat(imei);
13 }
14 void onStart() {
15 super.onStart();
16 try{
17 url = new URL(addr);
18 conn = (HttpURLConnection) url
19 .openConnection();
20 conn.setRequestMethod("GET");
21 conn.setDoInput(true);
22 /** send request to network (sink) */
23 conn.connect();
24 } catch(Exception ex){}
25 }
26 }
```
**Listing 4.7:** Example for an information flow across entry points

When onCreate() is executed, line 10 reads the IMEI of the phone and later, upon the invocation of onStart(), line 23 sends the IMEI to a server on the internet. However, such an information flow would not be detected if onStart() and onCreate() were each analyzed in isolation, since neither calls the other but both are called by the Android framework.

To cover such flows as well, our approach synthesizes an entry method that simulates the Android framework by invoking all callbacks of the given app.

```
1 public class MyActivity extends Activity {
2 static String URL=
3 "http://www.google.de/search?q=";
4 void onCreate(Bundle savedInstanceState){
5 ...
6 conn.connect();
7 ...
8 }
9 void onStart() {
10 ...
11 imei = telephonyManager.getDeviceId();
12 URL = URL.concat(imei);
13 }
14 }
```
In order to lose not too much precision, we take the life cycles of the app's components into account. Consider again Figure 4.12: When onCreate() is called, either the activity has just been launched, or the app's process has been destroyed and re-created. In either case, onCreate() is called on a fresh heap which cannot have been influenced by any other of the activity's entry points. Thus, it is safe to assume that none of the activity's entry points is called before onCreate. An example of how this assumption can be exploited to rule out impossible information flows and thus leads to increased precision is shown in Listing 4.8: In this variant of Listing 4.7, the source is contained in onStart() and the sink is contained in onCreate(). Since onCreate is never executed after onStart, the sink cannot be influenced by the source.

**Intents.** Our model also provides basic support for intents.

In order to incorporate the intents an application may react to, the application's manifest is inspected, the possible intent targets are resolved and appropriate method calls are inserted into the artificial entry method. Similarly, our approach handles intents which may be issued during the execution of the application and whose target can be resolved to a component within the same application. Listing 4.9 shows an example of an activity ShareActivity which declares the kinds of intents that it reacts to. It does so by using an *intent filter*. An intent filter specifies a set of possible intents an activity may react to. There are three aspects that can be used to specify intents: actions, categories and data. For example, in Listing 4.9 the activity ShareActivity can react to intents which specify android.intent.action.SEND as action, belong to the category android.intent.category.DEFAULT and send data of type text/plain. Note that in general, an intent filter may declare multiple action, category and data items. If an intent filter declares multiple action items, it matches all intents that match at least one of the declared action items. The same rule applies to categories and data. In Listing 4.10, we see exemplary code which issues an intent matching the intent filter depicted in Listing 4.9. Jodroid handles such code in the following way: It analyzes the app's manifest and records for each activity the possible intents it may react to.

Now suppose that during call graph construction, a piece of code like Listing 4.10 is encountered. Jodroid then inspects the object passed as parameter in the call in line 9. If the action can be resolved statically and matches the intent filter of a given activity, the call is interpreted as a call to the onCreate method of that activity.

This analysis can be improved by a static approximation of strings. Such an approximation was implemented in the scope of a bachelor's thesis under my supervision [167] and can be integrated into Jodroid.

```
<activity android:name="ShareActivity">
 <intent-filter>
 <action android:name="android.intent.action.SEND"/>
 <category
   android:name="android.intent.category.DEFAULT"/>
   <data android:mimeType="text/plain"/>
 </intent-filter>
</activity>
```
**Listing 4.9:** An exemplary section of an app's manifest where a component declares that it reacts to certain intents – taken and adapted from the Android documentation [73]

```
1 public void foo() {
 2 Intent s = new Intent();
 3 s.setAction(Intent.ACTION_SEND);
 4 s.putExtra(Intent.EXTRA_TEXT, "secret");
 5 s.setType("text/plain");
 6 // Verify that the intent will resolve to an
 7 //activity
 8 if (s.resolveActivity(getPackageManager()) != null) {
 9 startActivity(s);
10 }
11 }
```

```
Listing 4.10: Example of how to invoke an activity using an implicit intent – taken
             and adapted from [73]
```
# **4.4.3.3 Limitations and Future Work**

Now, I elaborate on the work that is left to do.

At the moment, Jodroid cannot handle callbacks of graphical user interfaces. The graphical user interfaces of Android apps are typically described in separate files and these files also reference the callbacks which are invoked on user input, e.g. when a button is pressed.

Hence, to also cover GUI callbacks, they have to be extracted from the separate files and integrated into the artificial main method appropriately. Another current limitation is that Jodroid only analyzes information flows inside single apps, but no information flows between different apps. This could be achieved by simply analyzing all the apps simultaneously. However, such an analysis would have to be re-done each time an app is added. Additionally, the analysis would have to be adapted in order not to assume that all the apps under analysis share a single heap (normally, different apps run in different virtual machines and hence have separate heaps).

An alternative, more modular approach is outlined in Figure 4.13: First, the intra-app flows in each single application are analyzed and summaries are generated from these analysis results. After that, a *communication graph* is built by connecting one app's summary with another app's summary if one of the former app's components may trigger one of the latter app's components by issuing an intent. The paths of such a graph represent the possible information flows between the apps.

# **4.5 RIFL**

In this section, I report on RIFL ("RS<sup>3</sup> Information Flow Language") [57, 25], a joint effort of multiple researchers within the RS<sup>3</sup> project. The goal was to develop a language in which security requirements can be expressed. One main design goal for RIFL was to be tool-independent, i.e. not be tailored to a specific information-flow analysis. Tool-independence enables to create case studies that are suitable for multiple tools, so that multiple tools can be evaluated, compared and possibly even combined. As a consequence of its tool-independence, RIFL is a semi-formal language: It has a formally defined syntax with a specific intuition behind it, but no fixed security semantics.

In the following, I describe the syntax of RIFL and the intended meaning of a RIFL specification. For this, I will base on the technical report on RIFL 1.1 [25], to which I also contributed. Furthermore, I will report on Joana's

**Figure 4.13:** Possible approach to capture inter-app flows

support for RIFL and in particular will describe how a RIFL specification maps to an information flow query for Joana.

As an application of RIFL, we also developed a benchmark suite for information flow analysis tools. I will describe this benchmark suite and some results from it in section 4.6.

# **4.5.1 Description of RIFL**

In this subsection, I describe the syntax of RIFL and shed some light on the intended meanings behind it. To keep the description brief, I will refrain from showing the syntax explicitly but rather show examples on which I explain the different elements. For further information, I refer the interested reader to the technical report on RIFL 1.1 [25], which contains all details.

Intuitively, a security requirement specification describes the allowed information flows within a program. Such a description usually consists of


A RIFL specification roughly consists of these ingredients. The syntax of RIFL is XML-based and a Document Type Definition (DTD) is provided. In order to enable re-use, RIFL is separated into a language-independent part, which can be re-used for each concrete supported programming language and a language-dependent part, which provides the respective specifics for the supported languages. Currently,< the supported languages are Java Source Code (JSC),Java Bytecode (JBC) and Dalvik Bytecode (DBC). Joana and its Android-variant Jodroid support both Java Bytecode and Dalvik Bytecode. In my descriptions, I will focus on Java Bytecode. The DBC front-end is syntactically identical to JBC and the JSC front-end only differs in the notation of method and field signatures.

In the following, I will describe the different parts of a RIFL specification. This description is based on the technical report on RIFL 1.1 [25].

# **4.5.1.1 Interface Specification**

The interface specification declares where a program imports and exports information. It specifies where in the program code a program reads input from the environment (*sources*) and where it provides output (*sinks*) to the environment.

**Figure 4.14:** RIFL's program model .

RIFL also provides a grouping mechanism. Sources and sinks can be grouped into *categories* and categories can be organized in a hierarchy: Categories may either contain sources or sinks or further categories.

A rough sketch of RIFL's program model can be seen in Figure 4.14. A program is basically regarded as a black-box which interacts with its environment (for example, the operating system) through a well-defined interface. This interface can be used to get data from outside (source) or provide output to the environment (sink). A RIFL specification describes exactly the parts of the environment which are used as sources and sinks for the program.

An interface specification consists of multiple *assignables*. An assignable has an identifier which is called a *handle* and contains either a *source*, a *sink* or a *category*. The sources and sinks themselves are language-dependent since they refer to explicit locations in the program's code and are explained later. A category has an identifier, its *name*, and may contain arbitrarily many sources, sinks or further categories.

It may appear redundant to have identifiers both for assignables and categories. But this has the simple reason that an assignable may consist of a single source or sink. Sources and sinks themselves do not have an identifier. Instead, their containing assignable provides one through its handle. The identifiers of categories and assignables are important since they are used for referring from other parts of a RIFL specification.

Figure 4.15 shows the logical structure of an exemplary interface specification. The corresponding RIFL representation can be found Listing 4.11. The sources and sinks in this example are simplified. Their concrete structure will be discussed later.

The specification declares the three handles fileshandle, HTTPhandle and HTTPShandle. HTTPhandle and HTTPShandle each consist of one sink (sendViaHTTP and sendViaHTTPS, respectively). The third handle fileshandle consists of the single category files which contains one bare sink storeToTmpFile and two sub-categories file-sources and file-sinks, in which the source loadFromFile and the sink storeToFile are located.

**Figure 4.15:** Logical structure of an exemplary interface specification – the actual RIFL specification snippet can be found in Listing 4.11 – handles are represented by octagons, categories by rectangles and sources/sinks by ovals

```
<interfacespec>
 <assignable handle="locationhandle">
   <category name="location">
     <source name="getGPS" />
     <source name="getNetworkLocation" />
   </category>
 </assignable>
 <assignable handle="fileshandle">
   <category name="files">
     <category name="file-sources">
       <source name="loadFromFile"/>
     </category>
     <category name="file-sinks">
       <sink name="storeToFile" />
     </category>
     <sink name="storeToTmpFile"/>
   </category>
 </assignable>
 <assignable handle="HTTPhandle">
   <sink name="sendViaHTTP" />
 </assignable>
 <assignable handle="HTTPShandle">
   <sink name="sendViaHTTPS" />
 </assignable>
</interfacespec>
```
**Listing 4.11:** RIFL representation of the exemplary interface specification from Figure 4.15

# **4.5.1.2 Security Domains and Flow Relation.**

As usual in the information-flow world, RIFL uses *security domains* to model different levels of confidentiality. A *flow relation* 〜 <sup>⊆</sup> *<sup>D</sup>* <sup>×</sup> *<sup>D</sup>* over the set *D* of security domains is used to describe the allowed flows. Formally, a flow between security domains *d*<sup>1</sup> and *d*<sup>2</sup> is allowed according to the given RIFL specification iff *d*<sup>1</sup> 〜 *d*2.

In RIFL, both the security domains and the flow relation are specified by declaring mere lists. RIFL aims to make as much explicit as possible. All pairs of domains which are related via 〜 have to be listed explicitly. All domains occurring in the flow relation have to be declared in the <domain> section. The only implicit assumption made about the flow relation is that it is reflexive. Consequently, transitive relations like lattices may lead to a considerable rise of verbosity in a RIFL specification.

The specifications shown in Listing 4.12 describe a diamond lattice: On the left-hand side, four security domains are declared, whereas the right-hand side specifies the diamond lattice structure on them.


**Listing 4.12:** Specification of security domains and a flow relation in RIFL

Since RIFL makes the implicit assumption that the specified flow relation is reflexive, declarations such as

```
<flow from="low" to="low"/>
```
need not be declared. However, it is neccessary to declare

```
<flow from="low" to="high"/>
```
since flow relations in RIFL do not need to be transitive.

### **4.5.1.3 Escape Hatches.**

For the sake of completeness, I mention here that RIFL also supports a form of *declassification* [144], namely *what-declassification*. This is realized by the usage of *escape hatches* [143]. An escape hatch specifies that certain information may be declassified to a given security domain.

Joana also supports a kind of declassification, *where-*declassification [87]. It does however not support the what-declassification mechanism implemented by RIFL. Joana does not reject a RIFL specification containing escape hatches, it rather ignores the escape hatches and hence treats such a specification as if they were not there. Consequently, it checks the compliance of the given program with a stricter policy, which does not hurt soundness.

For details on how escape hatches work in RIFL, I refer the interested reader to the technical report of RIFL 1.1 [25].

# **4.5.1.4 Domain Assignment.**

RIFL's domainassignment describes a mapping of the declared sources and sinks to the declared security domains. It employs the handles of declared assignables to refer to the sources and sinks declared within that assignable. In particular, if the assignable contains a category, then all sources and sinks declared within that category (directly or indirectly) are referred to by the handle of the assignable.

Listing 4.13 shows an example for a domain assignment, assuming the flow relation from Listing 4.12 and the interface specification of Listing 4.11. The third assign declaration refers to the assignable with the handle fileshandle and hence assigns the security domain low to all sources and sinks contained in fileshandle, namely storeToTmpFile, storeToFile and loadFromFile.

```
<domainassignment>
 <assign handle="locationhandle" domain="high" />
 <assign handle="HTTPShandle" domain="high" />
 <assign handle="fileshandle" domain="low" />
 <assign handle="HTTPhandle" domain="low" />
</domainassignment>
```
**Listing 4.13:** Exemplary domain assignment in RIFL

# **4.5.1.5 Sources and Sinks**

Sources and sinks in RIFL are language-specific, i.e. which kinds of sources and sinks are available and the concrete syntax depend on the concrete programming language. In RIFL, there are specializations for Java Source Code, Java Bytecode and Dalvik Bytecode. In the following, I will focus on Java and Dalvik Bytecode since the JBC and the JSC front-ends of RIFL support the same kinds of sources and sinks and only differ in the syntax of method and field identifiers. Note that the Java Bytecode and Dalvik Bytecode front-ends are syntactically identical, therefore I will only consider the Java Bytecode front-end.

# **4.5.1.6 Method Parameters and Return Values.**

In Figure 4.16, we see two views on Java methods that may be employed when thinking about their role in specifying sources and sinks.

**Figure 4.16:** Two views on methods

On the left, we see an *internal view*. This is the view which is used for application-internal methods which are called from the environment. Examples for this include the main method of a simple Java application or any callback of an Android app. When such an application-internal method is called from the environment, its parameters can be used to pass information from the environment to the application. In other words, using the intuitions of RIFL, any parameter of an application-internal method can be considered a source. Conversely, when the application-internal method finishes, it passes its return value to the environment. In other words, the return value of an application-internal method can be considered a sink. On the right-hand side of Figure 4.16, we see the external view on a method. This view is to be employed for external environment methods which are called from the application. Examples for such usage of library methods include writing into or reading from files. Here, the application uses the parameters of the external methods to pass information to the environment (for example, the next line of text to be written) and the return value to import information from the environment (the next line from a text file). Since both views are valid in their respective context, method parameters and return values can be both specified as sources and sinks. Table 4.1 summarizes the different usages of method parameters and return values as sources and sinks.


**Table 4.1:** Method parameters and return values as sources or sinks

# **4.5.1.7 Heap Locations.**

A Java application may exchange information with the environment not only through method parameters and return values but also through the heap. RIFL supports the specification of the following kinds of heap locations:

**Object fields / static fields** Fields of objects and static fields can be specified both as sources and sinks. The specification of an object field is to be understood in an object-insensitive way. That means that if the object field f of the class C is specified as a source, then o.f is considered a source for all instances o of class C. Static fields are specified in the same way as object fields.

**Content and length of arrays** Arrays are treated like objects with two special fields content and length. That is, it is possible to e.g. specify the contents of every **int**[] array as source or sink.

**Fields of objects received as parameters of methods** If an application method receives its parameters from the outside through a parameter of non-primitive type (i.e. an object), it may be unsuitable to treat the parameter itself as a source since it may only be a reference to heap locations which contain the actual information. Therefore, RIFL provides the possibility to not only specify parameters but also reachable fields as sources. Note that, until version 1.1, RIFL does not allow to specify fields reachable from parameters of external method calls or from return values of internal or external methods.

# **4.5.1.8 Exceptions.**

In Java, exceptions constitute an implicit channel of information. This especially applies to the communication between an application and its environment through internal or external methods. Therefore, RIFL makes it possible to specify exceptions as sources and/or sinks. The intuition is that a method not only returns its ordinary return value but also whether it has terminated abnormally and also the type of the occurred exception. Applying the intuition expressed in Figure 4.16, RIFL considers exceptions as sources for external methods and as sinks for internal methods. This enables to express for example that a given parameter may not influence whether a given application-internal method terminates abnormally.

# **4.5.2 J**oana**'s Support for RIFL**

In the following, I will explain how Joana's RIFL front-end works. In particular I will carry out how a RIFL specification is interpreted by Joana and how the translation of sources and sinks is performed. I will, as in most parts of this thesis, consider only sequential programs.

Joana implements RIFL 1.1 in most parts. RIFL's declassification is not supported since Joana only supports a form of *where-declassification* [86] but no *what-declassification*. Furthermore, Joana currently does not support the specification of an array's length as source or sinks. However, this has only implementation reasons and should be fixable with reasonable effort. The major part of Joana's RIFL front-end consists of translating a RIFL specification <sup>S</sup> = (*D*,〜, *Src*, *Snk*, *dom*) into queries answerable by <sup>J</sup>oana. The objective is to check whether the RIFL specification S is satisfied. Intuitively, a source *s* may influence a sink *t* only if information classified as *dom*(*s*) is allowed to influence information classified as *t*. More formally, this can be expressed as

**Algorithm 7:** Routine for checking a RIFL policy

**Input:** a program *p*, a set *Src* of sources, a set *Snk* of sinks, a RIFL specification with flow relation 〜 **Result:** whether one of the given sources may influence one of the given sinks although it must not according to 〜 **<sup>1</sup> foreach** *s* ∈ *Src* **do <sup>2</sup> foreach** *t* ∈ *Snk* **do <sup>3</sup> if** *dom*(*s*) ̸〜 *dom*(*t*) <sup>∧</sup> *s possibly influences t* **then <sup>4</sup> return** *false* **<sup>5</sup> return** *true*

<sup>∀</sup>*<sup>s</sup>* <sup>∈</sup> *Src*. <sup>∀</sup>*<sup>t</sup>* <sup>∈</sup> *Snk*. *<sup>s</sup>* possibly influences *<sup>t</sup>* <sup>=</sup><sup>⇒</sup> *dom*(*s*) 〜 *dom*(*t*).

In RIFL, sources and sinks lie at the boundary between the application and the environment. At a source, information enters the application from the environment and at a sink it leaves the application to the environment. With ordinary sources and sinks, it is not possible in RIFL to specify sources or sinks which completely lie within the application. Furthermore, once information is outside the application, it cannot be tracked by Joana anymore. So if it leaves the application through a sink and immediately enters it again unmodified through a source, it is treated like a fresh piece of information with no connection to the piece of information that left the application just before.

But if there can be no intermediate steps within the application, it is sufficient to consider each pair (*s*,*t*) of sources and sinks individually. A check routine is shown in Algorithm 7. It receives a program and a RIFL specification and returns whether it can be verified that the program satisfies the RIFL specification. If the assigned domains of *s* and *t* are related, no checking is neccessary: Even if there were an information flow between *s* and *t*, that would be permitted since their security domains relate. Hence, an actual check is performed just for those (*s*,*t*), where *<sup>s</sup>* ̸〜 *<sup>t</sup>*.

It remains to implement the check whether a source *s possibly influences* a sink *t* using Joana. This is done in a two-step process:

1. Translate *s* into a set *NSrc* of PDG nodes and *t* into a set *NSnk* of PDG nodes.

2. *s* cannot influence *t* if

$$\text{(4.3)}\tag{4.3}$$

$$\text{(4.3)}\tag{4.4}$$

Wasserrab has shown [164, Theorem 6.1] that a check like the one expressed by (4.3) is sufficient for guaranteeing non-interference. Note that such a check can be replaced by other more sophisticated checks like RLSOD.

In the special case that the flow relation 〜 on *D* forms a lattice, we can also perform a single instance of Hammer's IFC check [86] instead of performing an individual check for each source-sink-pair.

In the following, I describe how the first step of the process sketched above is performed, namely how RIFL sources and sinks are translated to PDG nodes.

# **4.5.2.1 Mapping of method parameters, return values and exceptional return values**

Joana's interprocedural PDG representation has special-purpose nodes for method parameters and the return values, both at the caller's and the callee's side. Hence, these kinds of RIFL sources and sinks can be mapped directly to PDG nodes. The specifics are summarized in Table 4.2. By using the term "root parameter", I acknowledge the fact that Joana not only has parameter nodes for the parameters themselves but also for fields reachable from parameters which are read or written within the method, either directly by the method itself or indirectly by called methods. Every parameter node represents a sets of heap locations which may be modified or read. Parameter nodes are connected via *parameter structure edges*: *p* is connected to *q* via a parameter structure edge if a heap location represented by *q* may be obtained by dereferencing one of *p*'s heap locations using a field access operation. More details about this can be found in the PhD thesis of Graf [78].

# **4.5.2.2 Mapping of static fields and object fields**

Static and object fields do not have a single counterpart in Joana's PDG representation. Instead they are mapped to all corresponding heap access


**Table 4.2:** Method parameters and return values as sources or sinks on Joana's layer


**Table 4.3:** Static and non-static fields as sources or sinks on Joana's layer

operations. The specifics are summarized in Table 4.3. Joana retains sufficient information in its PDG for identifying all reading or writing accesses of a given field and also whether these accesses are static or not. For example, if an object field A.f is specified as a source, then all non-static heap read operations on A.f are located.

How such a field read operation is represented in the PDG can be seen in Figure 4.17. One of the nodes in this structure represents the actual access to the heap (highlighted in blue). This node is selected as a source.

Accordingly, if an object field A.f is specified as a sink, then first all write operations to this field are located and the actual field node in the corresponding PDG structure (see Figure 4.18) is selected.

Static fields are handled analogously.

# **4.5.2.3 Mapping of arrays.**

RIFL allows for the specification of the content and length of arrays as sources and sinks. Since Joana's RIFL front-end does not support lengths of arrays as sources or sinks, I only consider the contents of an array.

static field-get (v1 = A.f) field-get (v1 = v2.f)

**Figure 4.17:** Joana's PDG node structures corresponding to the various heap read operations (taken and adapted from [78, Figure 2.31]) – the node to which a particular kind of source is mapped is highlighted in blue.

Joana models arrays as classes with exactly one field for the contents of the array. Individual array cells are not distinguished. Hence, array contents can be handled analogously to object fields. The only difference is that one has to be coarser when selecting the appropriate instructions: RIFL only distinguishes arrays by element type. For example, it can be specified that all **int** arrays are sources. In such a case, all reads on **int** arrays are located and for each of them the actual content access node is selected (see the bottom of Figure 4.17 and Figure 4.18, respectively).

static field-set (A.f = v1)

# **4.6** ifspec**: An Information-Flow Security Benchmark Suite**

In the scope of our work on RIFL, we developed ifspec, a benchmark suite for information flow analysis tools. This was joint work with RS<sup>3</sup> researchers from the groups of Mantel at TU Darmstadt and Beckert at KIT, with support from many other RS<sup>3</sup> researchers. In the following, I will describe the motivation behind and the structure of ifspec. Furthermore, I will show some of the results we have produced using ifspec. The following text is based on the resulting publication [85], to which I contributed Joana support.

Benchmark suites exist for various areas of computer science, like compiler research, SAT/SMT solving, theorem proving or model checking. They allow for the evaluation of a tool or technique with respect to different quality metrics like performance, correctness, precision or completeness. With such evaluable quality metrics, it is possible to compare different tools. Hence, benchmark suites can be regarded as driving forces of innovation and technical progress.

In a benchmark suite that assesses some form of correctness, samples should contain some kind of specification of the expected behavior of the benchmarked tool. For example, benchmark suites for compilers usually consist of sample programs to be compiled together with several test cases for the correctness of the compiled programs.If the compiled program fails one of these test cases, there is evidence that the compiler does not behave correctly. Hence, it is crucial to have such test cases in order for benchmark suites that are evaluated with respect to correctness.

Ideally, the expectation is specified *formally*, so that it can be read and processed automatically. For instance, the SPEC compiler benchmark suites contain test drivers, which execute the compiled samples and compare their actual outputs with expected values.

Formal specifications of expectations can also be found in benchmark suites for SMT solving. Here the expectation concerns for example the satisfiability of a given instance. SMT-Lib requires benchmark instances to state their solvability in the metadata [24, §3.9.3].

In the area of information-flow security, we found two benchmark suites: SecuriBench [148] and DroidBench [21, 61]. SecuriBench consists of several web applications found "in the wild" with known vulnerabilities. Later, SecuriBench Micro [149] was distilled from SecuriBench. SecuriBench Micro consists of 122 very small web applications, each of which focuses on a small set of vulnerabilities.

Although SecuriBench and SecuriBench Micro were developed to benchmark tools for the analysis of vulnerabilities in web applications, they are also suitable for information-flow analysis tools since web vulnerabilities can also be interpreted from an information-flow security perspective. Indeed, SecuriBench Micro has been used to evaluate information-flow analyzers targeting Java (e.g. [168]).

Arzt et al. [21] presented *DroidBench* as a benchmark suite for comparing their taint-analysis tool FlowDroid with existing tools for Android. The original version consists of 35 small Android apps, each of which focuses on some feature of Android. Of these 35 apps, 25 are insecure and 10 are secure (according to an intuitive specification). Later, DroidBench was extended considerably: The current DroidBench 2.0 consists of 119 apps, 99 of which are insecure and 20 of which are secure.

For a benchmark suite in information-flow security, such a formal specification consists of two parts: A formal specification of the security requirements for the given sample and the *ground truth*, a short information whether the given program satisfies this specification. Then, an information-flow can be evaluated automatically as follows: First, it is fed with the program and the requirements specification and performs its security analysis. Then it outputs whether it deems the given program secure or insecure with respect to the given specification. This output is then compared with the expected output.

Neither SecuriBench Micro nor DroidBench provides a machine-readable specification of the information-flow requirements. Instead, they provide some hints in the comments of their samples.

With ifspec, we provide a collection of samples together with a machinereadable specification so that tools can be evaluated and compared automatically. We use RIFL to provide formal security requirements and a short text file to indicate whether the program satisfies these formally specified security requirements or not.

In detail, each sample consists of the following data:

**Sample Kernel** The *sample kernel* comprises the program itself together a RIFL specification and a ground truth. The RIFL specification describes formally what it means for this program to be secure. The ground truth specifies whether the given program is expected to satisfy the RIFL specification.

**Sample Meta-Information** The *sample meta-information* provides further descriptions of the sample. For one, it associates the sample with a number of *tags*, which serve as a categorization mechanism for the samples. Another meta-information is the minimal RIFL version that a benchmarked tool must support to parse the RIFL specification. Lastly, since RIFL has no formal semantics, each sample needs to provide some *security semantics* that have been considered when classifying a sample as secure or insecure. For the classification of the samples in ifspec, we consider four formal security properties: termination-insensitive non-interference for the Abstract Dalvik Language (TIN-ADL) [121], sequential and probabilistic non-interference (SN/PN) [31], and the flow\*-predicate [27]. These are sufficient for Joana, Jodroid, Cassandra and KeY, the four tools considered in this work. The security property TIN-ADL is enforced by Cassandra[121], SN/PN is enforced by Joana as well as Jodroid for sequential (resp. concurrent) programs, and the flow\*-predicate is enforced by KeY[27].

**Sample Interpretation** The purpose of the *sample interpretation* is to provide convincing arguments that the sample kernel is meaningful. It consists of three parts:


The core of ifspec consists of 80 samples. The core samples have been provided by RS<sup>3</sup> researchers over the course of several years. Apart from that, ifspec also comprises the 122 original samples of SecuriBench Micro and 30 additional samples that were derived from them.

As an extension, ifspec incorporates a machine-processable version of DroidBench 2.0. A second extension comprises examples whose RIFL specification make use of declassification.

To demonstrate the usefulness of ifspec, we ran four information-flow tools on its samples and reported and discussed our findings.

In the following, I summarize these discussions, with a focus on Joana's and Jodroid's results.

We use terms that are commonly used to assess properties of classification tools. In the following, I introduce these terms. An information-flow tool that processes a sample produces two possible *analysis results*: Either it considers the sample secure or it considers it insecure. We refer to the former as a *positive* result (as in *the analysis could not find a potential information flow*) and to the latter as a *negative* result (as in *the analysis found a potential information flow*).


**Table 4.4:** The four possible analysis results when rated with respect to the ground truth

Apart from that and as I already elaborated on, each of ifspec's samples comes with a *ground truth* which specifies the sample as *secure* or *insecure*. Now, if we compare the analysis result with the actual ground truth, we yield four possible combinations that can be thought of as "rated analysis results", i.e. the analysis result together with the assessment whether the analysis result matches the sample's ground truth.

The four possible combinations are listed in Table 4.4.

This can also be expressed in terms of the formalisms introduced in section 3.1. Evaluating the analyses on a number of samples can be thought of evaluating empirically the four sets


for a property ϕ that expresses that the given program is secure w.r.t. to the specification<sup>22</sup> .

Let #*S* be the number of secure samples and #*I* be the number of insecure samples, respectively. For a fixed analysis tool, we use #*TP*, #*FP*, #*TN*, #*FN* to refer to the number of true positive, false positive, true negative and false negative analysis results, respectively. Analogously, let #*P* be the number of positive analysis results and #*N* be the number of negative analysis results. Hence, we have

$$\#\mathbb{S} = \#TN + \#FP$$

<sup>22</sup>Note that ϕ has to express security instead of *in*security due to our usage of soundness.

#*I* = #*TP* + #*FN* #*P* = #*TP* + #*FP* #*N* = #*TN* + #*FN*

Based on these numbers, several quantities can be derived that can be useful in assessing an analysis tool. In our paper about ifspec, we used two of them, *recall* and *precision*. These two quantities also have been used to assess other information flow analysis tools like *FlowDroid* [21]<sup>23</sup> .

• The *recall* measures how many insecure samples yield a positive analysis result.

$$recall = \frac{\#TP}{\#I} = \frac{\#TP}{\#TP + \#FN}$$

The recall always lies between 0 and 1. A recall of 0 means that the analysis tool never returns a positive result for any insecure sample, whereas a recall of 1 means that the analysis tool yields a positive result for all insecure samples. Looking at the formula, we see that the recall is low if the number of false negatives is high. In this sense, one could say that the recall is a measure of soundness of an analysis tool, at least for the given set of samples.

• The *precision* measures how many samples with positive analysis result are actually insecure.

$$precision = \frac{\#TP}{\#P} = \frac{\#TP}{\#TP + \#FP}$$

The precision also lies between 0 and 1. A precision of 0 means that the analysis never returns a positive result for an insecure sample or,

<sup>23</sup>Note that there are other metrics that may be more adequate in assessing an information flow tools' actual precision than the one defined in the following. However, also note that the metrics we picked are supposed to be an example for evaluations that can be performed with ifspec. Hence, such a discussion lies outside of the scope of this work.

conversely, that all its positive results are false positives. Conversely, a precision of 1 means that the analysis tool has no false positives. In this sense, one could say that this quantity is a measure for actual the precision of an analysis tool, at least for the given set of samples.


**Legend:** JSC=Java source code, JBC=Java bytecode, DBC=Dalvik bytecode

**Figure 4.19:** Overview of benchmark results – cf. [85, Fig. 5]

Figure 4.19 shows the results of running the four analysis tools Cassandra, KeY, Joana and Jodroid on ifspec. It lists the respective numbers of true positives, true negatives, false positives and false negatives and also the recall and precision values derived from these numbers.

Note that apart from returning a normal result, an analysis tool may also crash for a given sample. This may be due to a bug but also due to the fact that the sample uses a feature that cannot be treated by the respective analysis tool. We interpreted such cases as *positive* results and call them *soundly over-approximated*, or *soap* for short. The number of soap samples are reported separately for each analysis tool, both the total number and how many of false positives and true positives where due to sound over approximation (denoted as +*n*).

In the following, I want to discuss Joana's and Jodroid's results in more detail. Discussions of the other tools' results can be found in our article on ifspec [85].

The results of Joana match the ground truths for 174 of the samples in ifspec. The 50 false positives are mainly caused by the fact that Joana over-approximates actual program behavior. For instance, Joana does not reason about values and does not rule out control flow which is actually impossible due to algebraic invariants. Other sources of imprecision include array handling (Joana does not distinguish between different cells of the same array) and exceptional control flow.

The eight false negatives are due to two reasons. Seven false negatives are caused by the usage of reflection: Joana tries to handle reflective code but leaves it unresolved if it fails in doing so. The resulting PDG is then incomplete. The second reason is that Joana models static initializers improperly: In one example, the leak is caused by the fact that in Java, class initializers are executed lazily. Joana on the other hand assumes that all class initializers are executed upfront and hence misses the leak because it assumes that the leaking statement is executed at a time when no secret information is available yet.

The benchmarking results for Jodroid show differences in 11 samples. These appear to be caused by Jodroid's Dalvik frontend, which not only reads in the bytecode but also performs simple intraprocedural analyses on it. In three examples, Joana could deliver a result while Jodroid crashed. In five examples, Joana did not report a flow and JoDroid did. Possible reasons for this may include differences in the handling of static initializers and the analysis of exceptional control-flow. Three more differences appear to stem from a bug in Jodroid's modelling of multidimensional arrays.

We also ran Jodroid on the 119 DroidBench samples that are integrated into IFSpec. JoDroid delivered the expected results on 67 of them (54 true positives, 13 true negatives) and unexpected results on 52 samples (seven false positives, 45 false negatives) – this corresponds to a recall of 54.6% and a precision of 88.5%. The false negatives shed light on Jodroid's limits which I already elaborated on in subsection 4.4.3 (in particular subsubsection 4.4.3.3): It currently only has rudimentary support for Android features like intents and dynamic broadcast receivers and does not detect entry points corresponding to graphical interfaces. Also, the results clearly show that the stubs we used for Jodroid are insufficient as they do not reflect the dependencies of the actual library methods.

# **4.7 SHRIFT– System-Wide HybRid Information Flow Tracking**

In the following, I report on SHRIFT, a collaboration with the group of Pretschner at TU Munich. The general idea of the SHRIFT approach is to use static information flow analysis to improve the precision and runtime performance of a usage control and enforcement system. We also implemented the approach using Joana and demonstrated it on a case study.

The following summary is largely based on the resulting publication [122]. I concentrate on the motivation for this work and a brief description on its approach, with a focus on aspects of my contribution that were not covered by the paper. For detailed results and their discussion, I refer the interested reader to the original article.

# **4.7.1 Background and Motivation**

*Usage control* [132] is an extension of access control. Apart from the question "who is allowed to access this data?" it is also concerned with usage policies ("how is this data allowed to be used after access has been granted?"), data flow tracking mechanisms ("what is allowed to happen to this data?" or "what must happen to this data?") and runtime enforcement mechanisms ("what happens if the policy is violated?").

A major challenge for effective usage control is the fact that data may exist in different representations and/or on different system layers. For example, "don't copy this picture" may mean *don't send an e-mail to which this picture is attached* in an e-mail client, *don't copy this file* on the operating system layer or *don't copy&paste this picture* in an image editor.

As a possible solution to this challenge, it has been proposed (a) to model usage control polices in a representation-independent language and (b) to track and enforce these policies on multiple layers of abstraction using a distributed approach [135]. However, multiple monitors running in parallel and communicating with each other may incur a significant runtime overhead and it may be the case that monitors are not available for every layer of abstraction.

A remedy for the absence of a dedicated monitor is to rely on conservative estimation: For example, if a dedicated monitor for a process is not available, an OS-level monitor would treat the process as a "black box" and assume that every output of the process may result from any sensitive input this process has come in touch with. However, this may lead to a phenomenon called *label creep*: Due to over-approximation, the system falsely assumes that a piece of data is compromised with high data and prevents necessary actions on it because according to the system's policy, they are not allowed. In the worst case, this may lead to an unusable system.

# **4.7.2 Approach**

SHRIFT aims to improve on the "black box"-approach described above. Using a static information flow analysis, we compute an over-approximation of the data flows between the sources and the sinks a given application imports data from and exports data to, respectively. Then the application is instrumented with a lightweight runtime monitor which, instead of performing full data-flow tracking, consults the result of the static analysis phase every time a sink is executed to report to the OS-level monitor a list of sources which may have contributed to the piece of data which is exported by the sink.

This way, SHRIFT may increase precision in comparison with the "black box"-approach and at the same time reduce the runtime overhead of a dedicated application runtime monitor communicating with the system's monitor.

We provided and exemplified an implementation of the SHRIFT approach by using Joana as the static information flow analysis. Moreover, we evaluated our approach in terms of precision gain with respect to the black box approach and performance gain with respect to a fully dynamic analysis. Our evaluation showed that by employing a lightweight runtime monitor using the result of a static information flow analysis like Joana, it is possible to obtain a significant performance gain in comparison with a fully dynamic approach and being more precise than the black box approach while at the same time retaining a reasonable amount of soundness.

We applied our approach to the following example24, which is visualized in Figure 4.20:

*A company enforces the policy* "upon logout, delete every local copy of customer data" *to prevent clerks from working with outdated material. Upon every login, a clerk must download from a central server a fresh version of the customer data he is interested in. In this setting, a clerk uses the* JZip *application to compress multiple customer data (E, F) into a single archive file (*File 3*), which he then sends to the company server using* JFTP*.*

This example illustrates that precision is crucial: If data-flow tracking is imprecise, then not only customer data but also additional resources which are vital to the system's functionality, such as the Zipper's configuration

<sup>24</sup>Here, I show a slightly adapted version of the scenario description in the original paper [122, page 372].

**Figure 4.20:** Example scenario on which we demonstrated our SHRIFT approach (taken from [122, p. 372])

file, may be deleted. Moreover, it is important that the usage control system is able to distinguish the two different channels FTP works with: A data channel is used for the data to be sent (in this case the zipped customer data), whereas a control channel is used for commands and credentials. With the black box approach, once customer data has been read, every write operation is assumed to contain customer data. Hence, even the credentials written to the control channel by the client and the database are subject to deletion.

In the following, I describe how Joana was used to execute the static analysis phase.

Input for the static analysis is a list of sources and sinks to consider. It is important to notice that usually the list of descriptors is provided to the analysis by a security expert. In general, this list may depend on the application under analysis, since an application can e.g. use JNI to call its own native libraries.

In general, to be independent from the concrete application, we represent sources and sinks as pairs (*m*, *p*) where *m* is a method and *p* is a parameter of *m*. In the following, we call such pairs *descriptors*. A descriptor (*m*, *p*) represents parameter *p* of every invocation of *m* in the application code. It is notated in the format which is also used by Java's class file format25. For example, the descriptor (FileInputStream.read([B), param 1) represents the byte array passed as first parameter to any call of the method read(**byte**[]) of class FileInputStream.

Consider the code snippet shown in Listing 4.14, taken from our running example. Here, we want Joana to consider the first parameter of the

<sup>25</sup>See e.g. *The* Java *Virtual Machine Specification, Java SE 8 Edition, §4.3.3*.

call at line 10 as a source and the first parameter of the call at line 11 as a sink. To a certain extent, the descriptors have to be chosen manually, e.g. by reading the API documentation and then deciding which methods/parameters are relevant. For example, one may consider all variants of FileOutputStream.write() together with appropriate parameters as sinks.

```
1 FileOutputStream fos = new FileOutputStream(file);
2 ZipOutputStream zos = new ZipOutputStream(fos);
3 List<String> fileList = this.generateFileList();
4 byte[] buffer = new byte[1024];
5 for (String file : fileList) {
6 ZipEntry ze = new ZipEntry(file);
7 zos.putNextEntry(ze);
8 FileInputStream in = new FileInputStream(file);
9 int len;
10 while ((len = in.read(buffer)) > 0)
11 zos.write(buffer, 0, len);
12 in.close();
13 }
```
### **Listing 4.14:** Java code fragment for Zipper application

However, it may be the case that applications do not invoke source or sink methods directly. Consider again Listing 4.14: In line 2, a FileOutputStream is wrapped into a ZipOutputStream. When line 11 is executed, ultimately FileOutputStream.write() will be called, but not directly from application code. To also cover such cases, the descriptors have to be more general. One approach is to list every method manually and explicitly. This may be error-prone because methods may be missed. We chose another approach: We list only methods of the most general I/O classes java.io.InputStream, java.io.OutputStream, java.io.Reader and java.io.Writer. After that, they are extended automatically using the following rule: *If* (*m*, *p*) *is a source descriptor and m*′ *overrides m, then also* (*m*′ , *p*) *is a descriptor*. This rule can be implemented by analyzing the class hierarchy of the given program.

Still, this may not suffice. For example, JZip also contains a call to the method Properties.load() which takes an input stream as parameter and uses it to fill a properties table. This method is not included by the above rule because Properties itself is not an I/O class. For this reason, the descriptors are again extended automatically by the following rule: *If* (*m*, *p*) *is a descriptor, m*′ *may call m and p* ′ *is a parameter of m*′ *, then* (*m*′ , *p* ′ ) *is also a descriptor.* This rule can be implemented using a call graph of the application, which is also built and used during PDG construction, so it can be reused here.

Once the descriptors have been extended, they can be used to find the locations of the sources and sinks in the application and map them to appropriate PDG nodes.

Joana then computes the outcome of this phase: a table that lists, for each sink, all the sources that may influence this sink. An example is depicted in listing 4.15.

```
1 <source>
2 <id>Source1</id>
3 <location>JZip.zipIt(Ljava/lang/String;Ljava/lang/String;)V:191</location>
4 <signature>java.io.FileInputStream.read([B)I</signature>
5 <return/>
6 </source>
7 <source>
8 <id>Source2</id>
9 </source>
10 ...
11 <sinks>
12 <sink>
13 <id>Sink1</id>
14 </sink>
15 <location>JZip.zipIt(Ljava/lang/String;Ljava/lang/String;)V:185</location>
16 <signature>java.util.zip.ZipOutputStream.write([BII)V</signature>
17 <param index="1"/>
18 </sink>
19 <flows>
20 <sink id="Sink1">
21 <source id="Source1"/>
22 </sink>
23 </flows>
```
**Listing 4.15:** Static analysis report listing sinks, sources and their dependencies

# **4.7.3 Results**

In the following, I give a summary of the results of our evaluation of the SHRIFT approach. I concentrate on the Joana part.

As Zipper application, we used *JZip*, a simple command-line application written by us that uses the built-in ZIP functionality of the Java standard library and Apache Commons CLI [60] (Version 1.2) to implement command-line features. Without libraries, it has 293 LoC.

The FTP client *JavaFTP* was downloaded from SourceForge [109]. It does not use any libraries apart from Java's standard library and consists of 2082 LoC.

All measurements were conducted on a system with a 2.6 GHz Xeon-E5 CPU and 3GB of RAM. We ran Joana on JZip and JavaFTP with four different points-to analyses. For each points-to analysis, we considered two variants with respect to control dependencies. In variant DI ("direct and indirect flows"), Joana built the normal PDG, including controldependencies. In variant D ("only direct flows") it built a PDG without control dependencies. This was realized by first building the normal graph, removing control-dependencies and summary edges from this graph and finally re-computing the summary edges.

Each run consists of four steps: First, Joana built the call graph of the given program and extended the given sources and sinks as described in subsection 4.7.2. Then, Joana built the program dependence graph and identified sources and sinks in it according to the extended lists. Finally, Joana used context-sensitive slicing to count the number of source-sinkpairs that were connected in the PDG.

# **4.7.3.1 Performance**

Table 4.5 shows the overall time that Joana took for each configuration, along with rough estimates of the sizes of the respective PDGs. The times for the D and DI variants are aggregated by reporting the time for the slower variant. Note that although the D variant takes more time for the PDG construction, it may take less time in the slicing phase since there are less edges – the overall time for the D variant may therefore be roughly the same or even smaller. The graph sizes are reported for the DI variant. We can see that the points-to analysis has a massive impact on the graph size and also on the overall time needed to perform the analysis. For object-sensitive points-to, the number of edges can be 6.4-6.6 times as high as for 0-1-CFA, resulting in an overall time that is 6.7-6.9 times as high.

# **4.7.3.2 Precision**

Table 4.6 shows the number of source-sink-connections for both examples for all considered configurations and variants, which we call *flows* for


**Table 4.5:** Overall time of static analysis phase and PDG sizes for JavaFTP and JZip

short. Moreover, Table 4.6 gives a simple and coarse estimation of the precision gain obtained. This value is computed as

$$precision = 1 - \frac{\text{\#flows}}{\text{\#sources} \cdot \text{\#sinks}}$$

and compares the respective static analysis result to a conservative black box approach where every source is assumed to possibly flow to every sink. Such an approach would correspond to a static analysis with a *precision* of 0. Hence, the higher the *precision* value, the larger the precision gain of using the respective static analysis is<sup>26</sup> .

According to this measure, we see that the choice of points-to analysis is crucial for Joana's precision and has to be adapted to the application under analysis. For JavaFTP in the DI variant, object-sensitive points-to analysis makes the analysis 6.6 times as expensive as 0-1-CFA, without

<sup>26</sup>Note however, that the term "precision" is not entirely appropriate, since we deliberately give up soundness in the D configurations. In [122], we argue why it may be appropriate to ignore control dependencies for our application.

<sup>27</sup>The actual sources and sinks may vary with different points-to analyses because Joana uses points-to analysis for call graph construction and particularly for the identification of reachable code. Also, [122] reports 84% for JZip/object-sensitive/D, possibly due to a typo in the paper.


**Table 4.6:** precision values for JavaFTP and JZip<sup>27</sup>

any precision gain. Also 2-CFA does not have any advantage over 1-CFA although its runtime is 2.4 times as high. For JZip in the DI variant, on the other hand, the higher costs seem to pay off. For the D variants, the precision gain of a more precise points-to analysis is more visible. Joana uses points-to analysis not only to construct its call graph but also heavily for the computation of the heap dependencies. Hence it is plausible that a more precise points-to results in more precise heap dependency and therefore data dependency graph.

# **4.8 Modular Verification of Information Flow Security in Component-Based Systems**

In a collaboration with the group of Beckert at KIT [83], we also applied Joana in the area of information flow security verification of componentbased systems.

In an instantiation of the approach proposed in the article, we used Joana to verify user-provided service-level security properties. From these properties, first-order formulas are generated that express that componentlevel security follows from service-level security and that system-level security follows from component-level security. These formulas can be discharged using a first-order theorem prover like KeY.

The approach is both modular with respect to services and components and with respect to service-level security properties. This not only means that the security of the whole can be derived from the security of its parts but also that service-level properties verified by Joana can be re-used to show different overall security properties.

We applied the approach to a case study, in which we verified the security of a system implemented in Java.

# **4.9 Summary and Conclusion**

In this chapter, I gave an overview of some of the activities within the RS<sup>3</sup> priority program, with a focus on the achievements of the program paradigms group and the sub-project *Information Flow for mobile components*. In section 4.2, I described the advances in our work on *probabilistic noninterference*, particularly how we developed *relaxed low-security observational determinism* (RLSOD), a criterion that (a) can be verified using PDGs and control-flow checks, (b) improves the precision (under certain scheduling assumptions) on earlier criteria based on observational determinism and (c) enforces probabilistic non-interference.

After that, I reported on seven collaborations within RS<sup>3</sup> in which Joana was involved.

In section 4.3, I elaborated on how Joana and the KeY theorem prover can be combined to verify cryptographic properties of a prototypical E-Voting system.

In section 4.4, I presented Jodroid, an extension of Joana for Android apps and showed how it was integrated into the *RS*<sup>3</sup> *certifying app store*, the artifact of the RS<sup>3</sup> reference scenario *Software Security for mobile devices*.

Subsequently, in section 4.5, I reported on the joint work on the *RS*<sup>3</sup> *Information Flow Language* (RIFL), a language to specify information-flow properties in a language- and tool-neutral way. Then, I described Joana's RIFL support in subsection 4.5.2. An application of RIFL was shown in section 4.6: RIFL was used to provide an information-flow security benchmark suite that is also fully supported by Joana.

Last but not least, I elaborated on two other collaborations that showed how a static information flow control tool like Joana can be applied (a) to improve the precision and performance of usage control (section 4.7) and (b) to aid a theorem prover in the verification of information-flow properties for component-based systems (section 4.8).

All in all, this chapter demonstrated that our progress and our collaborations within the RS<sup>3</sup> project contributed to the establishment of PDG-based static analysis techniques in the realm of security analyses. Joana is a matured tool whose theoretical foundation has been firmly stabilized. It can be applied to a wide variety of scenarios, ranging from advanced security checks of mobile apps over the verification of cryptographic security properties to the improvement of usage control systems and the simplification of theorem proving approaches to the verification of component-based systems. With Joana's support for RIFL and ifspec, a well-founded baseline can be drawn that should drive and foster the state-of-the art of static security analysis tools in the future.

*And in the end, the love you take is equal to the love you make.*

# <sup>T</sup>he <sup>B</sup>eatles **5**

# **A Common Generalization Of Program Dependence Graphs and Control-Flow Graphs**

In chapter 3, I considered the interprocedural versions of slicing on program dependence graphs (PDGs) and data-flow analysis on control-flow graphs. In subsection 3.3.4, I identified similarities and argued that slicing on PDGs can be considered as a very simple data-flow analysis instance.

In this chapter, I am going to further develop these ideas. I will introduce *interprocedural graphs*(IGs), a general graph model that is less restricted than interprocedural control-flow graphs (ICFGs), yet still enables data-flow analysis. Both interprocedural control-flow graphs and interprocedural program dependence graphs can be considered as IGs. This makes available a whole range of data-flow analyses for PDGs.

IGs generalize ICFGs as defined in subsubsection 3.2.1.2 in several aspects. For one, the procedures of ICFGs each have only one entry and one exit. IGs lift this restriction and allow for multiple entries and exits. Secondly, in an ICFG, each call has exactly one corresponding return. In contrast, IGs allow for arbitrary corresponding relations between calls and returns. A third aspect in which IGs generalize ICFGs is that ICFGs – at least in the context of classical data-flow analysis – usually make some reachability assumptions. For data-flow analysis on IGs, these assumptions are not neccessary.

The data-flow analysis variant I introduce in this chapter is also a generalization of its counterpart on interprocedural control-flow graphs that I already considered in subsubsection 3.2.2.2. These generalizations are neccessary to properly consider slicing as a data-flow analysis.

As I have carved out in subsection 3.3.4, two generalizations are necessary to make this idea work. In the following, I am going to briefly describe them.

The first generalization is concerned with the notion of *interprocedurally valid paths*. Remember that subsubsection 3.2.2.2 introduced this notion to characterize the properties of interprocedural paths considered by a data-flow analysis (cf. page 56).

Intuitively, an interprocedurally valid path π respects call semantics. This amounts to two properties. Firstly, if a procedure is called on π and it returns later, then the call site to which it returns must match the call site from which the call started in the first place. Secondly, if a procedure returns on π, then π must also contain a matching call. The notions developed in this chapter only require that the first property is satisfied. This matches the notion of interprocedural validness that one usually makes on PDGs to define slicing.

The second generalization is concerned with the fact that a slice is defined usually not with respect to a fixed entry point but rather with respect to an arbitrary node. In subsection 5.4.2, this leads to a version of the objective function *MOVP* that has – unlike the version in subsubsection 3.2.2.2 (cf. equation (3.8)) – not one argument, but two.

This chapter is organized in four parts. In section 5.1, I develop the notion of *valid sequences* and important variants. I base these notions on classic results from the theory of balanced parenthesis. My approach is to first introduce intuitive definitions and then derive inductive definitions from them. After that, in section 5.2, I present interprocedural graphs and my notion of valid paths, which is based on the theory developed so far. Next, section 5.3 presents data-flow analysis on interprocedural graphs. Finally, I conclude this chapter in section 5.4 by showing a variety of use cases for data-flow analysis on IGs, including already existing PDG-based approaches and several graph-theoretic notions.

# **5.1 Nesting Properties of Symbol Sequences**

In this section, I develop the notion of *validness* for symbol sequences. Valid sequences form the basis of valid paths, which I will define in section 5.2 along with interprocedural graphs.

Validness is concerned with symbol sequences that essentially consist of opening and closing parentheses and connects two different ways of assigning closing parentheses to opening parentheses. One of them counts parentheses to assign every opening parenthesis at most one closing parenthesis such that the part between the two is *balanced*. The other way is expressed as a correspondence relation that is used to relate opening parentheses and closing parentheses that are compatible. Validness then demands that if two parentheses match, then they must be compatible with respect to the correspondence relation.

This section is organized as follows: In subsection 5.1.1, I introduce the *matching relation* that assigns each opening parenthesis in a given sequence at most one closing parenthesis and vice versa. This relation makes use of *balanced sequences*, which in turn can be characterized using a formalization of the process of counting parentheses. It turns out that relation-theoretic properties of the matching relation can be used to characterize two different kinds of *partially balanced sequences* that become important in defining the building blocks of valid sequences and paths. Partially-balanced sequences are introduced in subsection 5.1.2. After that, subsection 5.1.3 is concerned with the second kind of assigning opening and closing parentheses to each other: It introduces a relation that specifies which parentheses are compatible. Using this relation, I define *valid sequences*. Additionally, I introduce valid counterparts for (partially- )balanced sequences. Finally, after I have introduced valid sequences in subsection 5.1.3, I derive inductive definitions for valid sequences and their partially-balanced variants in subsection 5.1.4.

# **5.1.1 Balanced Sequences**

In this section, I introduce *balanced sequences*. I do this according to Knuth [107] with the help of two functions which formalize the process of counting parentheses.

In the following I consider symbol sequences over *E* = *Eintra* ∪ *Ecall* ∪ *Eret* where *Eintra*, *Ecall* and *Eret* are pairwise disjoint. Elements of *Ecall* are called *opening parentheses*, *call symbols* or just *calls*. Elements of *Eret* are called *closing parentheses*, *return symbols*, or just *returns*. Elements of *Eintra* are called *inner symbols*. Later, the opening parentheses will model calls of procedures and the closing parentheses will model returns from procedures. The inner symbols will later model intraprocedural edges. With *Callpos*(π) := {*i* ∈ *range*(π) | π*<sup>i</sup>* ∈ *Ecall*} and *Retpos*(π) := {*i* ∈ *range*(π) | π*<sup>i</sup>* ∈ *Eret*} I denote the set of *call* and *return positions* in π, respectively.

**Definition 5.1.** *Let* π ∈ *E* <sup>⋆</sup> *be a symbol sequence.*

*1. The* content *c*(π) *of* π *is defined as follows:*

$$c(\boldsymbol{\pi} \cdot \boldsymbol{e}) = 0$$

$$c(\boldsymbol{\pi} \cdot \boldsymbol{e}) = \begin{cases} c(\boldsymbol{\pi}) + 1 & \text{if } e \in E\_{\text{call}} \\ c(\boldsymbol{\pi}) - 1 & \text{if } e \in E\_{\text{ret}} \\ c(\boldsymbol{\pi}) & \text{if } e \in E\_{\text{intra}} \end{cases}$$

*2. The* deficiency *d*(π) *of* π *is defined as follows:*

$$d(\pi \cdot e) = \begin{cases} \max\{d(\pi), -c(\pi)\} & \text{if } e \in E\_{\text{call}} \cup E\_{\text{intra}}\\ \max\{d(\pi), -c(\pi) + 1\} & \text{if } e \in E\_{\text{ret}} \end{cases}$$

Given a symbol sequence π, the function *c* computes the difference between the number of call symbols and the number of return symbols of a given symbol sequence, wheres *d* computes the maximal shortage of call symbols among the prefixes of π. I give some examples to illustrate how *c* and *d* work.

**Example 5.2.** *Let Eintra* = {⋆}*, Ecall* = {(} *and Eret* = {)}*.*

*1. For* π<sup>1</sup> = ()(⋆)*, we have*


*2. For* π<sup>2</sup> = ()))*, we have*


*3. For* π<sup>3</sup> = (()())*, we have*


# *4. For* π<sup>4</sup> =)()((⋆*, we have*


*5. For* π<sup>5</sup> = ()(⋆(⋆*, we have*


Remark 5.3 and Lemma 5.4 formalize important properties of *c* and *d*.

**Remark 5.3.** *c is additive: c*(π<sup>1</sup> · π2) = *c*(π<sup>1</sup> ) + *c*(π2)*.*

*Proof.* This follows by induction on π2. □

**Lemma 5.4.** *For every* π ∈ *E* <sup>⋆</sup>*, we have*

$$1. \ c(\pi) = |\mathsf{Call}pos(\pi)| - |\mathsf{Retpos}(\pi)|.$$

	- For the third claim, we observe that (1) ϵ is always a prefix of π, (2) *c*(ϵ) = 0 (by definition of *c*) and conclude from the second claim that (3) *d*(π) ≥ −*c*(ϵ) = 0.

• Since π is a prefix of itself, we may apply the second claim and obtain *d*(π) ≥ −*c*(π) or, equivalently, −*d*(π) ≤ *c*(π). For *d*(π) = 0, this implies *c*(π) ≥ 0. □

Now I define what it means for a symbol sequence to be *balanced*. Intuitively, in a balanced sequence we can match the call positions with the return positions in a *well-nested* fashion.

However, initially I define balancedness as a mere property about the counts of opening and closed parentheses in a symbol sequence. Balancedness has two requirements: Firstly, there must be as many call symbols as return symbols in π. Secondly, for every prefix of π there cannot be more return symbols than call symbols. The first property guarantees that a bijective function between the call positions and the return positions is possible, while the second ensures that it is always possible to map each call position to a *later* return position.

Going back to Example 5.2, we see that π<sup>1</sup> and π<sup>3</sup> are intuitively balanced, whereas π2, π<sup>4</sup> and π<sup>5</sup> are not: π<sup>2</sup> has more return symbols than call symbols, while π<sup>5</sup> has more call symbols than return symbols. Moreover, as π<sup>4</sup> starts with a return symbol, it cannot be extended to a balanced sequence.

The examples show that the balancedness of sequences can indeed by characterized using *c* and *d*.

**Definition 5.5.** *A sequence* π ∈ *E* <sup>⋆</sup> *is called* balanced *if c*(π) = *d*(π) = 0*. The set of balanced symbol sequences is written as Bal*(*E*)*.*

The following lemma gives an easy characterization of balancedness which we will make use of later.

**Lemma 5.6.** *A sequence* π ∈ *E* <sup>⋆</sup> *is balanced i*ff *<sup>c</sup>*(π) = <sup>0</sup> *and <sup>c</sup>*(θ) <sup>≥</sup> <sup>0</sup> *for all prefixes* θ *of* π*.*

*Proof.* This follows easily from Definition 5.5 and Lemma 5.4. □

# **5.1.1.1 The Matching Relation**

Now I introduce a relation νπ which relates opening and closing positions in a symbol sequence π to each other. The relation was also introduced by 0 1 2 3 4 5 6 7 8 9 10 11 (︂ (︂ *e*1 *e*2 )︂ (︂ *e*<sup>3</sup> *e*<sup>4</sup> *e*5 )︂ *e*6 )︂

**Figure 5.1:** An example sequence with its matching relation. The *e<sup>i</sup>* are inner symbols, there is one call symbol "(" and one return symbol ")". Connected symbols are related by the matching relation.

Knuth [107], albeit not formally. An example for the intuition behind ν<sup>π</sup> can be seen in Figure 5.1.

**Definition 5.7.** *For a sequence* π ∈ *E* <sup>⋆</sup> *we define the* matching relation ν<sup>π</sup> ⊆ *Callpos*(π) × *Retpos*(π) *by:*

(*i*, *j*) ∈ ν<sup>π</sup> ⇐⇒ *i* < *j* ∧ π*<sup>i</sup>* ∈ *Ecall* ∧ π*<sup>j</sup>* ∈ *Eret* ∧ π ]*i*,*j*[ *is balanced*

In the following, I am going to state three important properties of ν<sup>π</sup> that reflect the intuitive expectations toward a properly defined matching relation.

Firstly, Theorem 5.8 states that νπ never relates a call position to multiple return positions or, conversely, a return position to multiple call positions (compare Knuth[107, p. 271]).

**Theorem 5.8.** *For any symbol sequence* π ∈ *E* <sup>⋆</sup>*,* ν<sup>π</sup> *is left- and right-unique.*

*Proof.* We show left- and right-uniqueness separately.

right-uniqueness: Let (*i*, *j*) ∈ ν<sup>π</sup> and (*i*, *j* ′ ) ∈ νπ. Assume, for the purpose of contradiction, that *j* ≠ *j* ′ . Without loss of generality, we may assume that *j* < *j* ′ . Since π ]*i*,*j* ′ [ is balanced, we know that *d*(π ]*i*,*j* ′ [ ) = 0. Because π ]*i*,*j*] is a prefix of π ]*i*,*j* ′ [ , we get *c*(π ]*i*,*j*] ) ≥ 0 by Lemma 5.4. However, since π*<sup>j</sup>* ∈ *Eret* and π ]*i*,*j*[ is balanced (which means that *c*(π ]*i*,*j*[ ) = 0), we also have:

$$c(\pi^{[i,j]}) = c(\pi^{[i,j]}) - 1 = 0 - 1 = -1$$

This is a contradiction, so the assumption must be false and it must be *j* = *j* ′ .

**Figure 5.2:** Illustration of the different cases of well-nestedness of ν<sup>π</sup>

left-uniqueness: Let (*i*, *j*) ∈ νπ, (*i* ′ , *j*) ∈ νπ. Assume, for the purpose of contradiction, *i* ≠ *i* ′ . Without loss of generality, assume *i* < *i* ′ . Since π ]*i*,*j*[ is balanced, we have *c*(π ]*i*,*j*[ ) = 0. Since both π ]*i*,*i* ′ [ and π ]*i*,*i* ′ ] are prefixes of π ]*i*,*j*[ , we have *c*(π ]*i*,*i* ′ [ ) ≥ 0 and *c*(π ]*i*,*i* ′ ] ) ≥ 0 by Lemma 5.4. In fact, since π*<sup>i</sup>* ′ ∈ *Ecall*, we even have *c*(π ]*i*,*i* ′ ] ) = *c*(π ]*i*,*i* ′ [ ) + 1 > 0. But then, using Remark 5.3, we can conclude

$$c(\pi^{]i',j[}) = c(\pi^{]i,j[}) - c(\pi^{]i,i'[}) < c(\pi^{]i,j[}) = \mathbf{0}\_{\prime\prime}$$

so that *c*(π ]*i* ′ ,*j*[ ) < 0, which contradicts the balancedness of π ]*i* ′ ,*j*[ .

Secondly, the one-sided totality properties of ν<sup>π</sup> can be used to assess whether π is balanced or not. This is the statement of Theorem 5.9.

□

**Theorem 5.9.** *For any symbol sequence* π ∈ *E* <sup>⋆</sup>*, the following conditions are equivalent:*


The third property, which is given in Theorem 5.10, states that νπ relates call and return positions in a *well-nested* fashion.

More specifically, the respective sections between two matching position pairs never overlap.

For an illustration, see Figure 5.2. Given two pairs (*i*, *j*),(*i* ′ , *j* ′ ) ∈ ν<sup>π</sup> of call and return positions that are related by νπ, the corresponding index ranges [*i*, *j*] and [*i* ′ , *j* ′ ] are either disjoint or one of them is contained in the other.

**Theorem 5.10.** *Given* π ∈ *E* <sup>⋆</sup>*, assume that* (*i*, *j*),(*i* ′ , *j* ′ ) ∈ νπ*. Then one of the following statements is true:*


Proofs for Theorem 5.9 and Theorem 5.10 can be found in section A.1 and Theorem 5.9, respectively.

# **5.1.2 Partially-Balanced Sequences**

Theorem 5.9 suggests that the totality properties of ν<sup>π</sup> can be used to classify sequences with respect to their balancedness properties. This motivates the following definition.

**Definition 5.11.** *We denote by Le f t*(*E*) ⊆ *E* <sup>⋆</sup> *the set of symbol sequences* π *such that* ν<sup>π</sup> *is left-total and by Right*(*E*) ⊆ *E* <sup>⋆</sup> *the set of symbol sequences* π *such that* ν<sup>π</sup> *is right-total. If* π ∈ *Le f t*(*E*) ∪ *Right*(*E*)*, we also call* π partially balanced*.*

**Remark 5.12.**

*Le f t*(*E*) ∩ *Right*(*E*) = *Bal*(*E*)

*Proof.* This follows from Theorem 5.9. □

# **5.1.3 Valid Sequences**

The property of balancedness only considers call and return symbols in terms of their numbers. In particular, in a balanced path it is only ensured that every call symbol is matched by a return symbol and vice versa. However, it is not ensured that return symbols also *correspond* to their matching call symbols.

$$
\pi \colon \begin{array}{ccc}
0 & 1 & 2 & 3 \\
\hline
\end{array}
$$

**Figure 5.3:** A balanced but invalid symbol sequence – positions related by ν<sup>π</sup> are connected

As a simple example, consider the symbol sequence depicted in Figure 5.3. Here there are two distinct call symbols **(** and **[** and two distinct return symbols **)** and **]**. We find that ν<sup>π</sup> = {(0, 3),(1, 2)}, hence π is balanced. But the symbols at the positions related by νπ do not belong together. To exclude such sequences, I introduce a *correspondence relation* Φ ⊆ *Ecall* × *Eret* that specifies which call symbols belong to which return symbols. In the example, the obvious choice for Φ is {(**(**, **)**),(**[**, **]**)}.

**Definition 5.13.** *Let* π ∈ *E* <sup>⋆</sup> *be a symbol sequence.*

π *is called* valid *if*

∀(*i*, *j*) ∈ *Callpos*(π) × *Retpos*(π).(*i*, *j*) ∈ ν<sup>π</sup> =⇒ (π *i* , π *j* ) ∈ Φ

*I will denote the set of valid symbol sequences as Val*(*E*)*.*

Basically, this property says that if a call has a matching return (in terms of position in the path), this return actually corresponds to the call according to the relation. The sequence from our example is invalid, since (0, 3) ∈ ν<sup>π</sup> but (π 0 , π 3 ) ∉ Φ.

In the following, I fix a correspondence relation Φ ⊆ *Ecall* × *Eret*.

**Definition 5.14.** *Let* π ∈ *E* <sup>⋆</sup> *be a symbol sequence.*

*1.* π *is called* ascending *if* π *is valid and* νπ *is left-total.*


*4. With AscSeq*(*E*)*, DescSeq*(*E*)*, SLSeq*(*E*)*, I denote the sets of ascending, descending and same-level sequences, respectively.*

**Example 5.15.** *Let Eintra* = {⋆}*, Ecall* = {<*a*, <*<sup>b</sup>* } *and Eret* = {>*a*, >*<sup>b</sup>* }*, Furthermore, assume that* Φ = {(<*a*,>*a*),(<*<sup>b</sup>* ,>*<sup>b</sup>* )} *and consider*

$$\begin{aligned} \pi\_1 &\stackrel{def}{=} <\_a >\_b\\ \pi\_2 &\stackrel{def}{=} <\_a <\_b >\_b >\_a\\ \pi\_3 &\stackrel{def}{=} <\_a <\_b >\_b >\_a >\_b \end{aligned}$$

$$
\pi\_4 \stackrel{def}{=} <\_a <\_a >\_a <\_b >\_b
$$

$$
\pi\_5 \stackrel{def}{=} >\_b >\_b <\_a <\_a
$$

*1. Then* π<sup>1</sup> *is invalid: We have* (0, 1) ∈ νπ<sup>1</sup> *, but* (<*a*,>*<sup>b</sup>* ) ∉ Φ*.*

*2. We have Callpos*(π2) = {0, 1}*, Retpos*(π2) = {2, 3} *and* νπ<sup>2</sup> = {(0, 3),(1, 2)}*. Now it is easy to see that* π2 *is valid and both left- and right-total. Hence,* π2 *is a same-level sequence.*

*3. We have Callpos*(π3) = {0, 1} *and Retpos*(π3) = {2, 3, 4}*. Moreover, we have* νπ<sup>3</sup> = {(0, 3),(1, 2)}*. This means that* π<sup>3</sup> *is valid and* νπ<sup>3</sup> *is left-total, but not right-total, since there is no i* ∈ *Callpos*(π3) *with* (*i*, 4) ∈ νπ<sup>3</sup> *. Hence,* π<sup>3</sup> *is ascending, but not same-level.*

*4. Analogously, we see that* π<sup>4</sup> *is valid and that* νπ<sup>4</sup> ⊆ {0, 1, 3} × {2, 4} *is right-total, but not left-total. Hence,* π<sup>4</sup> *is descending, but not same-level.*

*5. Finally,* νπ<sup>5</sup> = ∅ ⊆ {0, 1} × {2, 3}*, so* π<sup>5</sup> *is trivially valid but neither left- nor right-total. Hence,* π5 *is neither ascending nor descending.*

Since the left- and right-totality of νπ is equivalent to the balanced-ness of π, the same-level property can also be expressed using balanced-ness.

**Theorem 5.16.** π ∈ *E* <sup>⋆</sup> *is same-level if and only if it is balanced and valid.*

*Proof.* By definition, π is same-level if and only if it is valid and ν<sup>π</sup> is bijective. By Theorem 5.9, this is the case if and only if π is valid and balanced. □

In the following, I show that valid, ascending and descending sequences are closed under taking contiguous sub-sequences. This becomes clear relatively quickly, if we think about cases in which the respective totality properties are maintained: Taking an arbitrary sub-sequence of π can only remove or shift the positions appearing in νπ. Hence, validness is maintained by this operation. Left-totality may be destroyed, if we take a suffix and potentially remove a return position by this. However, taking prefixes maintains right-totality. Hence, we can conclude that descending sequences are closed under taking prefixes. Analogously, right-totality is maintained by taking suffixes since taking a suffix only removes return positions and does not hurt the matching for the remaining return positions. Hence, ascending sequences are closed under taking suffixes.

For the proof of Theorem 5.18, I need a technical lemma, which I state and prove before I proceed with Theorem 5.18.

**Lemma 5.17.** *If* π = π<sup>1</sup> · π<sup>2</sup> · π<sup>3</sup> ∈ *E* <sup>⋆</sup>*, then*

$$1. \ \mathsf{range}\_{\pi}(\pi\_2) = \{ i + |\pi\_1| \mid i \in \mathsf{range}(\pi\_2) \}.$$

$$\text{2. } \forall i, j \in \text{range}(\pi\_2). \ (i, j) \in \nu\_{\pi\_2} \iff (i + |\pi\_1|, j + |\pi\_1|) \in \nu\_{\pi\_1}$$

*Proof.* We observe that

$$
\pi\_2 = \pi^{\left[|\pi\_1|, |\pi\_1| + |\pi\_2| - 1\right]}\_{\prime \prime}
$$

which is equivalent to

$$\forall i \in range(\pi\_2). \ \pi\_2^i = \pi^{|\pi\_1| + i}.$$

From this, both claims can be proven easily. □

**Theorem 5.18.** *The following statements are true.*

*1. Valid sequences are closed under taking sub-sequences, in particular under taking su*ffi*xes and prefixes.*

*2. Ascending sequences are closed under taking su*ffi*xes.*

*3. Descending sequences are closed under taking prefixes.*

*4. Taking a prefix of an ascending sequence or a su*ffi*x of a descending sequence yields a valid sequence.*

*Proof.* 1. If π is valid and π ′ = π [*i*,*j*] is a sub-sequence of π, then π ′ still has the validity property, as can easily be seen.

2. Let π be ascending and π <sup>≥</sup>*<sup>j</sup>* be a suffix of <sup>π</sup>. Write *<sup>n</sup>* <sup>=</sup> <sup>|</sup>π|. Let *k* ∈ *range*(π ≥*j* ) be a call position in π ≥*j* . Then (π ≥*j* ) *<sup>k</sup>* = π *k*+*j* . Because ν<sup>π</sup> is left-total, there is a return position *l* ∈ *range*(π) such that (*k* + *j*, *l*) ∈ νπ. From this, it follows that *l* ≥ *k* + *j*. Hence, we can write *l* = (*l* − *j*) + *j*, so that *l* − *j* ∈ *range*(π ≥*j* ). Then, (*k* + *j*, *l*) ∈ ν<sup>π</sup> implies (*k*, *l* − *j*) ∈ ν π <sup>≤</sup>*<sup>j</sup>* by Lemma 5.17. Thus, we have shown that ν π <sup>≥</sup>*<sup>j</sup>* is left-total.

3. Let π be descending and π <sup>≤</sup>*<sup>j</sup>* be a prefix of π. Let

$$l \in \operatorname{Retpos}(\pi^{\le j}) \subseteq \operatorname{Retpos}(\pi).$$

be a return position in π ≤*j* . Due to right-totality of νπ, we find a *k* ∈ *Callpos*(π) such that (*k*, *l*) ∈ νπ. This means in particular that *k* < *l*. With *l* ≤ *j* we get *k* ≤ *j* so that we can conclude (*k*, *l*) ∈ ν π ≤*j* . This proves that ν π <sup>≤</sup>*<sup>j</sup>* is left-total.

4. Both ascending and descending paths are valid and according to the first statement, valid paths are closed under taking suffixes and prefixes.

□

# **5.1.4 Inductive Definitions**

In this subsection, I derive inductive definitions for the various classes of sequences that I have introduced so far. I start with characterizing balanced, left-total and right-total and finally all sequences inductively. Afterwards, I consider their valid counterparts and give inductive definitions for same-level, ascending, descending and valid sequences.

Proofs for the following three theorems can be found in section A.3.

**Theorem 5.19.** *Bal*(*E*) *is the least subset X of E* <sup>⋆</sup> *with the following properties:*

$$(Bal1)\ \frac{\pi \in X}{\varepsilon \in X} \quad (Bal2)\ \frac{\pi \in X \qquad e \in E\_{\text{intra}}}{\pi \cdot e \in X}$$

$$(Bal3)\ \frac{\pi \in X \qquad \pi' \in X \qquad e\_{\text{call}} \in E\_{\text{call}} \qquad e\_{\text{ret}} \in E\_{\text{ret}}}{\pi \cdot e\_{\text{call}} \cdot \pi' \cdot e\_{\text{ret}} \in X}$$

**Theorem 5.20.** *Le f t*(*E*) *is the least subset X of E* <sup>⋆</sup> *which has the following properties:*

$$(Left1)\ \frac{\pi}{\epsilon \in X} \quad (Left2)\ \frac{\pi \in X \qquad e \in E\_{intra} \cup E\_{ret}}{\pi \cdot e \in X}$$

$$(Left3)\ \frac{\pi \in X \qquad \pi' \in Bal(E) \qquad e\_{call} \in E\_{call} \qquad e\_{ret} \in E\_{ret}}{\pi \cdot e\_{call} \cdot \pi' \cdot e\_{ret} \in X}$$

189

**Theorem 5.21.** *Right*(*E*) *is the least subset X of E* <sup>⋆</sup> *which has the following properties:*

$$(\text{Right1})\xrightarrow[\epsilon\in X]{} (\text{Right2})\xrightarrow[\pi\in\text{A}]{} \pi\cdot\text{E}\to\text{E}\_{\text{intra}}\cup\text{E}\_{\text{call}}$$

$$(\text{Right3})\xrightarrow[\pi\in\text{A}]{} \frac{\pi\in\text{X}\qquad\pi'\in\text{B} \newline al}{\pi\cdot e\_{\text{call}}\cdot\pi'\cdot e\_{\text{ret}}\in\text{X}}\qquad e\_{\text{ret}}\in\text{E}\_{\text{ret}}$$

Next, Theorem 5.22 that every sequence can be split up into a left- and right-total sequence in a specific way. This is a specific version of a theorem already considered by Knuth (cf. [107, Lemma 1]). I am going to apply this property in chapter 7.

**Theorem 5.22.** *For every symbol sequence, there is i* ∈ *range*(π) *such that every symbol sequence* π ∈ *E* <sup>⋆</sup> *can be split up into* π = π <*i* · π ≥*i such that*

$$(5.1)\tag{5.1}$$

$$(5.2) \tag{5.2}$$

$$(\text{5.3})\qquad\qquad\pi\notinLeft(E)\implies i\in\mathbb{C}allpos(\pi).$$

*Proof.* Define the set of unfinished call positions in π as

$$\mathcal{M}\_{call} \stackrel{def}{=} \{ i \in \mathbb{C} allpos(\pi) \mid \forall j \in range(\pi). \ (i, j) \notin \nu\_{\pi} \}.$$

Now we make a case distinction on whether U*call* is empty or not. If U*call* = ∅, then π ∈ *Le f t*(*E*). Then we can choose *i* = |π|.

Now consider the case that <sup>U</sup>*call* <sup>≠</sup> <sup>∅</sup>. Let *<sup>i</sup>* ∈ U*call* be the least element of U*call*. Now we show

$$\text{(1)}\tag{1}\tag{1}\tag{2}$$

$$\pi\_1 \stackrel{def}{=} \pi^{$$

$$
\pi\_2 \stackrel{def}{=} \pi^{\geq i} \in \operatorname{Right}(E).
$$

(1) Let *i* ′ ∈ *Callpos*(π<sup>1</sup> ) ⊆ *Callpos*(π). Then *i* ′ < *i* and, due to the choice of *i*, there must be *j* ∈ *range*(π) such that (*i* ′ , *j*) ∈ νπ. This implies π ]*i* ′ ,*j*[ is balanced and hence that *j* ≤ *i*, since otherwise π ]*i* ′ ,*j*[ would contain the unmatched call π*<sup>i</sup>* , in contradiction to the balancedness of π ]*i* ′ ,*j*[ . In addition to *<sup>j</sup>* <sup>≤</sup> *<sup>i</sup>* we also observe *<sup>j</sup>* <sup>≠</sup> *<sup>i</sup>*, since <sup>π</sup> *<sup>i</sup>* <sup>∈</sup> *<sup>E</sup>call* and <sup>π</sup> *<sup>j</sup>* <sup>∈</sup> *<sup>E</sup>ret* and *Ecall* ∩ *Eret* = ∅. Thus, we have *j* ∈ *range*(π<sup>1</sup> ) and therefore (*i* ′ , *j*) ∈ νπ<sup>1</sup> .

(2) We show π<sup>2</sup> ∈ *Right*(*E*) by induction on π<sup>2</sup> ∈ *E* <sup>⋆</sup>. This is clear for π = ϵ. So let π<sup>2</sup> = π ′ 2 ·*e*. Our induction hypothesis says that π ′ 2 ∈ *Right*(*E*) and we have to show that π<sup>2</sup> ∈ *Right*(*E*) as well. We proceed with a case distinction on *e*. If *e* ∈ *Eintra* ∪ *Ecall*, then we have π<sup>2</sup> ∈ *Right*(*E*) by Theorem 5.21. So we assume *e* ∈ *Eret*. Let *j* ∈ *Retpos*(π2). We have to find *i*<sup>0</sup> ∈ *Callpos*(π2) such that (*i*0, *j*) ∈ νπ<sup>2</sup> . This follows from the induction hypothesis if *j* ∈ *Retpos*(π ′ 2 ), so we may assume *j* = |π2| − 1. In this case, we choose *i*<sup>0</sup> as the greatest unmatched call position in π2. Such a position must exist since *i* is an unmatched call position in π and therefore in π2. Furthermore, we have *i*<sup>0</sup> < *j*. This is because *Ecall* ∩ *Eret* = ∅ and π *j* 2 = *e* ∈ *Eret*. Finally, we have to show that π ]*i*0 ,*j*[ 2 is balanced. By Theorem 5.9, it suffices to show that π ]*i*0 ,*j*[ 2 contains neither unmatched call positions nor unmatched returns positions. We show the two claims separately:


□

**Corollary 5.23** (cf. [107], Lemma 1)**.** *Every symbol sequence* π ∈ *E* <sup>⋆</sup> *can be split up into* π = π<sup>1</sup> · π<sup>2</sup> *such that* νπ<sup>1</sup> *is left-total and* νπ<sup>2</sup> *is right-total.*

*Proof.* This is a direct consequence of Theorem 5.22. □

# **5.1.4.1 Inductive Definitions for valid sequences**

Before I actually derive and prove correct inductive definitions for the valid variants of partially-balanced sequences, I compile a couple of observations about the closure properties of same-level, ascending, descending and valid sequences. Proofs can be found in section A.3.

Firstly, I observe that appending inner symbols does not do any harm.

**Lemma 5.24.** *1. If* π ∈ *AscSeq*(*E*) *and e* ∈ *Eintra* ∪*Eret, then* π·*e* ∈ *AscSeq*(*E*)*.*


Secondly, appending a same-level sequence destroys neither validness nor the respective totality property of the matching relation.

**Lemma 5.25.** *Let*π ∈ *Val*(*E*) *and*π ′ ∈ *SLSeq*(*E*)*. Then the following statements hold:*


Lastly, same-level sequences are closed under surrounding with a corresponding call-return pair.

**Lemma 5.26.** *If* π ∈ *SLSeq*(*E*)*, ecall* ∈ *Ecall, eret* ∈ *Eret and* (*ecall*,*eret*) ∈ Φ*, then ecall* · π ·*eret* ∈ *SLSeq*(*E*)*.*

With these three observations in mind, it is now relatively clear how the inductive definitions for the partially-balanced sequences have to be modified in order to obtain inductive definitions for their valid counterparts. All we have to do is adapt the respective clause that is concerned with appending balanced sequence to make sure that it maintains validity.

**Theorem 5.27.** *The same-level sequences are the least subset X of E* <sup>⋆</sup> *with the following closure properties:*

$$\left(\text{s1-SEQ}\_{\text{empty}}\right) \frac{\left(\text{s1-SEQ}\_{\text{empty}}\right)}{\varepsilon \in X} \quad \left(\text{s1-SEQ}\_{\text{intr}}\right) \frac{\pi \in X \qquad e \in \mathcal{E}\_{\text{intra}}}{\pi \cdot e \in X}$$

$$\left(\text{s1-SEQ}\_{\text{inter}}\right) \frac{\pi \in X \quad \pi' \in X \quad e\_{\text{call}} \in \mathcal{E}\_{\text{call}} \quad e\_{\text{ret}} \in \mathcal{E}\_{\text{ret}} \quad \left(e\_{\text{call}}, e\_{\text{ret}}\right) \in \Phi\right)$$

*Proof.* By Theorem 2.19, we have to show that

1. *SLSeq*(*E*) satisfies sl-seq*empty*,sl-seq*intra* and sl-seq*inter*.

2. *SLSeq*(*E*) is contained in the least subset *X*<sup>0</sup> ⊆ *E* <sup>⋆</sup>that satisfies sl-seq*empty*,sl-seq*intra* and sl-seq*inter*.

We prove the two claims separately.

1. *SLSeq*(*E*) satisfies sl-seq*empty*, because ϵ is balanced by *Bal*1 and trivially valid. Moreover, with Lemma 5.24 we see that *SLSeq*(*E*) also satisfies sl-seq*intra*. Finally, Lemma 5.25 and Lemma 5.26 imply sl-seq*inter*.

2. By structural induction on π ∈ *Bal*(*E*) we show

$$\forall \pi \in \text{Bal}(E). \; \pi \in \text{Val}(E) \implies \pi \in X\_0.$$

This implies *SLSeq*(*E*) = *Bal*(*E*) ∩ *Val*(*E*) ⊆ *X*0.

*Bal*<sup>1</sup> If <sup>π</sup> <sup>=</sup> <sup>ϵ</sup>, then <sup>π</sup> <sup>∈</sup> *<sup>X</sup>*<sup>0</sup> by sl-seq*empty*.

*Bal*2 Let π = π ′ ·*e* with π ′ ∈ *Bal*(*E*) and *e* ∈ *Eintra*. Assume that π ∈ *Val*(*E*). Then π ′ ∈ *Val*(*E*) by Theorem 5.18. This implies π ′ ∈ *X*<sup>0</sup> by induction hypothesis. With sl-seq*intra*, we get π = π ′ ·*e* ∈ *X*0.

*Bal*3 Let π = π ′ · *ecall* · π ′′ · *eret* with π ′ , π ′′ ∈ *Bal*(*E*), *ecall* ∈ *Ecall*, *eret* ∈ *Eret* and assume that π ∈ *Val*(*E*). Choose *i*, *j* such that π <sup>&</sup>lt;*<sup>i</sup>* = π ′ , π *<sup>i</sup>* = *ecall*, π ]*i*,*j*[ = π ′′ and π *<sup>j</sup>* <sup>=</sup> *<sup>e</sup>ret*. Then we observe that (*i*, *<sup>j</sup>*) <sup>∈</sup> <sup>ν</sup><sup>π</sup> by definition. Since π ∈ *Val*(*E*), it follows that (*ecall*,*eret*) ∈ Φ. Furthermore, by Theorem 5.18, both π ′ and π ′′ are valid. Application of the induction hypothesis to π ′ and π ′′ yields π ′ , π ′′ <sup>∈</sup> *<sup>X</sup>*0. Now we can apply sl-seq*inter* obtain that π = π ′ ·*ecall* · π ′′ ·*eret* ∈ *X*0.

**Theorem 5.28.** *The set of ascending sequences is the least subset X of E* <sup>⋆</sup> *with the following properties:*

$$\left(\mathsf{Asc-SEQ}\_{\mathsf{Empty}}\right)\frac{\pi}{\mathfrak{e}\in X} \quad \left(\mathsf{Asc-SEQ}\_{\mathsf{Asc}}\right)\frac{\pi\in X \qquad \mathfrak{e}\in E\_{\mathsf{intra}}\cup E\_{\mathsf{ret}}}{\pi\cdot\mathsf{e}\in X}$$

$$\pi\_{\text{(asc-sezQ}\_{\text{sl}})} \frac{\pi \in \mathcal{X} \text{ } \pi' \in \text{SLSeq}(E) \text{ } e\_{\text{call}} \in \mathcal{E}\_{\text{call}} \text{ } e\_{\text{ret}} \in \mathcal{E}\_{\text{ret}} \text{ } (e\_{\text{call}}, e\_{\text{ret}}) \in \Phi}{\pi \cdot e\_{\text{call}} \cdot \pi' \cdot e\_{\text{ret}} \in \mathcal{X}}$$

*Proof.* By Theorem 2.19, we have to show that

1. *AscSeq*(*E*) satisfies asc-seq*empty*,asc-seq*asc* and asc-seq*sl*, and

2. *AscSeq*(*E*) is contained in the least subset *X*<sup>0</sup> ⊆ *E* <sup>⋆</sup> that satisfies asc-seq*empty*,asc-seq*asc* and asc-seq*sl*.

We prove the two claims separately.

1. *AscSeq*(*E*) satisfies asc-seq*empty*, because <sup>ϵ</sup> <sup>∈</sup> *Le f t*(*E*) by *Le f t*<sup>1</sup> and <sup>ϵ</sup> is trivially valid. Moreover, with Lemma 5.24 we see that *AscSeq*(*E*) also satisfies asc-seq*asc*. Finally, Lemma 5.26 and Lemma 5.25 imply that *AscSeq*(*E*) satisfies asc-seq*sl*.

2. Let *X*<sup>0</sup> be the least subset of *E* <sup>⋆</sup> that has the closure properties asc-seq*empty*,asc-seq*asc* and asc-seq*sl*. By structural induction on <sup>π</sup> <sup>∈</sup> *Le f t*(*E*) we show

$$\forall \pi \inLeft(E). \,\pi \in Val(E) \implies \pi \in \mathcal{X}\_{0}.$$

This implies *AscSeq*(*E*) = *Le f t*(*E*) ∩ *Val*(*E*) ⊆ *X*0.

*Le f t*<sup>1</sup> If <sup>π</sup> <sup>=</sup> <sup>ϵ</sup>, then <sup>π</sup> <sup>∈</sup> *<sup>X</sup>*<sup>0</sup> by asc-seq*empty*.

*Le f t*2 Let π = π ′ · *e* with π ′ ∈ *Le f t*(*E*) and *e* ∈ *Eintra* ∪ *Eret*. Assume that π ∈ *Val*(*E*). Then π ′ ∈ *Val*(*E*) by Theorem 5.18. With π ′ ∈ *Le f t*(*E*), we can apply the induction hypothesis and get π ′ <sup>∈</sup> *<sup>X</sup>*0. With asc-seq*asc*, we get π = π ′ ·*e* ∈ *X*0.

□

*Le f t*3 Let π = π ′ ·*ecall* · π ′′ ·*eret* with π ′ ∈ *Le f t*(*E*), π ′′ ∈ *Bal*(*E*), *ecall* ∈ *Ecall*, *eret* ∈ *Eret* and assume that π ∈ *Val*(*E*). Define *i*, *j* such that π <sup>&</sup>lt;*<sup>i</sup>* = π ′ , π *<sup>i</sup>* = *ecall*, π ]*i*,*j*[ = π ′′ and π *<sup>j</sup>* <sup>=</sup> *<sup>e</sup>ret*. Then we observe that (*i*, *<sup>j</sup>*) <sup>∈</sup> <sup>ν</sup><sup>π</sup> by definition. Since π ∈ *Val*(*E*), it follows that (*ecall*,*eret*) ∈ Φ. Furthermore, by Theorem 5.18, both π ′ and π ′′ are valid. Application of the induction hypothesis to π ′ yields π ′ ∈ *X*0. Furthermore, π ′′ is both balanced and valid, which means that π ′′ <sup>∈</sup> *SLSeq*(*E*). Now we can apply asc-seq*sl* and yield that π = π ′ ·*ecall* · π ′′ ·*eret* ∈ *X*0.

□

**Theorem 5.29.** *The set of descending sequences is the least subset X of E* <sup>⋆</sup> *with the following properties:*

$$\begin{array}{ll} \left(\mathsf{p\_{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\beta}}}}}}}}}}}}}}}\right){}}}\right){}}\right)}\ \left(\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\beta}}}}}}}}}}}}}\right){}}\frac{\pi{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\beta}}}}}}}}}}}}}}{\pi{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\beta}}}}}}}}}}}}}}}}}\right}}\}{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\beta}}}}}}}}}}}}}}}}}}}\}{\texttt{\texttt{\texttt{\texttt{\tilde{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\beta}}}}}}}}}}}}}}}}}}}\pi}}}\pi}}\pi}}} $$

*Proof.* The proof is very similar to the proof of Theorem 5.28. □

**Theorem 5.30.** *The valid sequences are exactly the concatenations of the ascending and the descending sequences. In particular:*

*1. If* π<sup>1</sup> *is ascending and* π<sup>2</sup> *is descending, then* π<sup>1</sup> · π<sup>2</sup> *is valid.*

*2. Every valid symbol sequence* π ∈ *E* <sup>⋆</sup> *can be split up into* π = π<sup>1</sup> · π<sup>2</sup> *such that* π<sup>1</sup> *is ascending and* π2 *is descending.*

*Proof.* I show both statements separately.

1. Let π = π<sup>1</sup> · π<sup>2</sup> and (*i*, *j*) ∈ νπ. First we observe that it must be either *i*, *j* ∈ *range*π(π<sup>1</sup> ) or *i*, *j* ∈ *range*π(π2): Assume, for the purpose of contradiction, that this is not the case. Then, since *i* < *j*, it must be *i* ∈ *range*π(π<sup>1</sup> ) and *j* ∈ *range*π(π2). But note that π<sup>1</sup> is ascending. Hence, there is *j* ′ ∈ *range*π(π<sup>1</sup> ) with (*i*, *j* ′ ) ∈ νπ<sup>1</sup> ⊆ νπ. But then we have (*i*, *j*) ∈ ν<sup>π</sup> and (*i*, *j* ′ ) ∈ ν<sup>π</sup> with *j* ′ < *j*, in contradiction to Theorem 5.8. Thus, the assumption must be false and we have either *i*, *j* ∈ *range*π(π<sup>1</sup> ) or *i*, *j* ∈ *range*π(π2). In either case, it follows that (π *i* , π *j* ) ∈ Φ, since both π<sup>1</sup> and π<sup>2</sup> are valid.

2. Let π be a valid symbol sequence. By Corollary 5.23, we can split up π into π = π<sup>1</sup> · π<sup>2</sup> such that νπ<sup>1</sup> is left-total and νπ<sup>2</sup> is right-total. Since π is valid, by Theorem 5.18 both π<sup>1</sup> and π<sup>2</sup> are valid. It follows that π<sup>1</sup> is ascending and π<sup>2</sup> is descending.

□

# **5.2 Interprocedural Graphs**

In this section, I introduce the graph model over which I will later define data-flow analyses. This graph model is intended to be general enough to cover both the classical interprocedural control-flow graphs (cf. Definition 3.2) and also other graphs like program dependence graphs.

**Definition 5.31.** *Given a finite set P of procedure labels, an* interprocedural graph

$$G = (N\_\prime E\_{\rm intra} E\_{\rm call} \, E\_{\rm ret} \, P\_\prime \Phi\_\prime N\_{\rm entry} N\_{\rm exit}),$$

*consists of*

• *a set of nodes N* = ⋃︁ *<sup>p</sup>*∈*<sup>P</sup> N<sup>p</sup> such that*

$$\forall p, p' \in \mathcal{P}. \, p \neq p' \implies N\_p \cap N\_{p'} = \emptyset$$

• *a set of* intraprocedural edges *Eintra* = ⋃︁ *<sup>p</sup>*∈*<sup>P</sup> E<sup>p</sup> such that*

$$\forall p \in P. \ E\_p \subseteq \mathcal{N}\_p \times \mathcal{N}\_p \text{ and } \forall p, p' \in P. \ E\_p \cap E\_p' = \emptyset$$

• *sets Ecall, Eret* ⊆ ⋃︁ *p*,*p* ′∈*<sup>P</sup> N<sup>p</sup>* × *N*′ *p of* call edges *and* return edges *with the property*

$$E\_{\text{intra}} \cap E\_{\text{call}} = E\_{\text{intra}} \cap E\_{\text{ret}} = E\_{\text{call}} \cap E\_{\text{ret}} = \emptyset\_{\prime \prime}$$

• *a* correspondence relation Φ ⊆ *Ecall* × *Eret, and*

• *sets Nentry*, *Nexit* ⊆ *N of* entry *and* exit nodes*, respectively, such that* **–** *every node with an incoming call edge is an entry node,*

$$\forall n \in \mathbb{N}. \ (\exists n' \in \mathbb{N}. \ \exists e \in E\_{\text{call}}. \ n' \xrightarrow{e} n \implies n \in N\_{\text{entry}})\_{\text{set}}$$

**–** *every node with an outgoing return edge is an exit node, and*

$$\forall n \in \mathbb{N}. \ (\exists n' \in \mathbb{N}. \ \exists e \in E\_{\text{ret}}. \ n \xrightarrow{e} n' \implies n \in N\_{\text{exit}})\_{\text{ret}}$$

**–** *no node is an entry node and an exit node at the same time.*

$$N\_{entry} \cap N\_{exit} = \emptyset.$$

I define *Einter de f* = *Ecall* ∪ *Eret* and call its elements *interprocedural edges*. I refer to each (*Np*, *Ep*) as *procedure graph*. According to the definition of interprocedural graphs, for each *n* ∈ *N* there is exactly one *p* ∈ *P* such that *n* ∈ *Np*. I call *p* the *procedure of n* and write it as *proc*(*n*). Occasionally, I will consider an interprocedural graph as a directed graph (*N*, *E*). Then I ignore the additional structure and use

$$E \stackrel{def}{=} E\_{intra} \cup E\_{call} \cup E\_{ret} \dots$$

In the rest of this thesis, I will assume that *Nentry* and *Nexit* are given but will not mention them explicitly when specifying an interprocedural graph.

**Definition 5.32.** *Let G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ) *be an interprocedural graph. Then I define the following special nodes:*

*1. A* call node *is a node that has outgoing call edges. I write Ncall for the set of call nodes.*

*2. A* return node *is a node that has incoming return edges. I write Nret for the set of return nodes.*

The following example shows that both interprocedural control-flow graphs and interprocedural program dependence graphs are subsumed by Definition 5.31.

**Example 5.33.** *1. Interprocedural control-flow graphs according to Definition 3.2 can be considered as interprocedural graphs with the following additional properties:*


*2. Interprocedural program dependence graphs as described in subsubsection 3.3.2.2 on page 65 can be considered as interprocedural graphs. We consider parameter-in edges as elements of Ecall and parameter-out edges as elements of Eret. Then the entry nodes are either ordinary procedure entries or formal-in nodes. Exit nodes are either ordinary procedure exits or formal-out nodes. Call nodes are either ordinary call nodes or actual-in nodes. Return nodes are either ordinary return nodes or actual-out nodes.*

**Definition 5.34.** *Let G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ) *be an interprocedural graph and let s*, *t* ∈ *N.*

	- same-level path *if* π ∈ *SLSeq*(*E*)*,*
	- ascending path *if* π ∈ *AscSeq*(*E*)*,*
	- descending path *if* π ∈ *DescSeq*(*E*)*, and*
	- valid path *if* π ∈ *Val*(*E*)*,*

*where SLSeq*(*E*)*, AscSeq*(*E*)*, DescSeq*(*E*) *are defined with respect to the correspondence relation* Φ*.*

*2. I define*

$$SL\_\prime A \text{SC}\_\prime \text{DECC}\_\prime V P: N \times N \to 2^{Pathus(G)}$$

*by*

$$\begin{aligned} SL(s,t) & \stackrel{def}{=} SLSeq(E) \cap Paths\_G(s,t) \\ ASC(s,t) & \stackrel{def}{=} AscSeq(E) \cap Paths\_G(s,t) \end{aligned}$$

198

$$\begin{aligned} \mathit{DESC}(\mathsf{s},t) & \stackrel{def}{=} \mathit{DescSeq}(E) \cap \mathit{Paths}\_{G}(\mathsf{s},t) \\ \mathit{VP}(\mathsf{s},t) & \stackrel{def}{=} \mathit{ValSeq}(E) \cap \mathit{Paths}\_{G}(\mathsf{s},t) \end{aligned}$$

*In the following, I will use these functions also as path sets. Particularly, I will write SL for the set of paths* π *such that there is s*,*t* ∈ *N with* π ∈ *SL*(*s*,*t*) *– analogously for ASC, DESC and VP.*

By combining the inductive definitions of the valid sequences and their partially-balanced variants with the inductive definition of *Paths*(*G*), I can derive inductive definitions for *SL*, *ASC*, *DESC* and *VP*, respectively.

**Theorem 5.35.** *SL is the least subset of N* × *N* × 2 *E* ⋆ *with the following closure properties:*

$$\left(\text{s1-EMPTY}\right)\frac{\pi \in \text{X(s,s)}}{\varepsilon \in \text{X(s,s)}} \qquad\qquad\qquad\left(\text{s1-NrtRA}\right)\frac{\pi \in \text{X(s,t')} \quad t' \xrightarrow{\varepsilon} t \quad e \in \text{E}\_{\text{intra}}}{\pi \cdot e \in \text{X(s,t)}}$$

$$\text{(sz.-sz)}\begin{array}{cccc} \pi \in \text{X}(s,n) & n \stackrel{\mathcal{e}\_{\text{call}}}{\xrightarrow{\mathcal{e}\_{\text{call}}}} n\_0 & \pi' \in \text{X}(n\_0, n\_1) & n\_1 \stackrel{\mathcal{e}\_{\text{ret}}}{\xrightarrow{\mathcal{e}\_{\text{ret}}}} t & (\mathcal{e}\_{\text{call}}, e\_{\text{ret}}) \in \Phi \end{array}$$

*Proof.* According to Theorem 2.19, we need to show

1. *SL* has the closure properties sl-empty, sl-intra and sl-sl, and

2. *SL* ⊆ *X*<sup>0</sup> where *X*<sup>0</sup> is the least subset of *N* × *N* × 2 *E* ⋆ with the closure properties sl-empty, sl-intra and sl-sl.

We show both claims separately.

1. We show that *SL* has the properties sl-empty, sl-intra and sl-sl.

sl-empty Let *<sup>s</sup>* <sup>∈</sup> *<sup>N</sup>*. Then <sup>ϵ</sup> <sup>∈</sup> *PathsG*(*s*,*s*) by path-empty. Moreover, <sup>ϵ</sup> <sup>∈</sup> *SLSeq*(*E*) by sl-seq*empty*. Together, it follows that <sup>ϵ</sup> <sup>∈</sup> *PathsG*(*s*,*s*) <sup>∩</sup> *SLSeq*(*E*) = *SL*(*s*,*s*).

sl-intra Let *s*,*t* ′ ,*t* ∈ *N*, π ∈ *SL*(*s*,*t* ′ ) and *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>* with *<sup>e</sup>* <sup>∈</sup> *<sup>E</sup>intra*. From π ∈ *SL*(*s*,*t* ′ ) we have π ∈ *PathsG*(*s*,*t* ′ ) and π ∈ *SLSeq*(*s*,*t* ′ ). From π ∈ *PathsG*(*s*,*t* ′ ) and *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>* we get <sup>π</sup> · *<sup>e</sup>* <sup>∈</sup> *PathsG*(*s*,*t*) and from <sup>π</sup> <sup>∈</sup> *SLSeq*(*E*) and *<sup>e</sup>* <sup>∈</sup> *<sup>E</sup>intra* we have <sup>π</sup> · *<sup>e</sup>* <sup>∈</sup> *SLSeq*(*E*) by sl-seq*intra*. Together we have π ·*e* ∈ *PathsG*(*s*, *t*) ∩ *SLSeq*(*E*) = *SL*(*s*, *t*).

sl-sl Let *s*, *n*, *n*0, *n*<sup>1</sup> ,*t* ∈ *N*, *ecall* ∈ *Ecall*, *eret* ∈ *Eret*, π ∈ *SL*(*s*, *n*) and π ′ ∈ *SL*(*n*0, *n*<sup>1</sup> ) with *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* and (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup>. Then we have <sup>π</sup> ·*ecall* · π ′ · *<sup>e</sup>ret* <sup>∈</sup> *SLSeq*(*E*) by sl-seq*inter*. Moreover, <sup>π</sup> · *<sup>e</sup>call* · <sup>π</sup> ′ · *eret* ∈ *PathsG*(*s*,*t*) follows from *SL*(*s*, *n*) ⊆ *PathsG*(*s*, *n*) and *SL*(*n*0, *n*<sup>1</sup> ) ⊆ *PathsG*(*n*0, *n*<sup>1</sup> ) by path-extend and Lemma 2.23.

2. Let *X*<sup>0</sup> be the least subset of *N* × *N* × 2 *E* ⋆ with the closure properties sl-empty, sl-intra and sl-sl. Using the induction principle induced by Theorem 5.27, we show

$$\forall \pi \in SLSeq. \,\forall s, t \in \mathcal{N}. \,\pi \in Paths\_G(s, t) \implies \pi \in \mathcal{X}\_0(s, t).$$

Let π ∈ *SLSeq* and *s*,*t* ∈ *N* with π ∈ *PathsG*(*s*,*t*). Then we need to show π ∈ *X*0(*s*, *t*).

sl-seq*empty* : If <sup>π</sup> <sup>=</sup> <sup>ϵ</sup>, then from <sup>π</sup> <sup>∈</sup> *PathsG*(*s*,*t*) we get *<sup>s</sup>* <sup>=</sup> *<sup>t</sup>*, hence <sup>π</sup> <sup>∈</sup> *<sup>X</sup>*0(*s*, *<sup>t</sup>*) by sl-empty.

sl-seq*intra* : Assume π = π ′ · *e* with π ′ ∈ *SLSeq* and *e* ∈ *Eintra*. By Lemma 2.24, from π ∈ *PathsG*(*s*,*t*) we yield *t* ′ ∈ *N* with π ′ ∈ *PathsG*(*s*,*t* ′ ) and *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>*. By induction hypothesis, we get <sup>π</sup> ′ ∈ *X*0(*s*,*t* ′ ). This implies <sup>π</sup> <sup>∈</sup> *<sup>X</sup>*0(*s*, *<sup>t</sup>*) by sl-intra.

sl-seq*inter* : Assume π = π ′ · *ecall* · π ′′ · *eret* with π ′ , π ′′ ∈ *SLSeq* and (*ecall*,*eret*) ∈ Φ. By splitting up π and applying Lemma 2.24 to π ∈ *PathsG*(*s*,*t*), we obtain *n*, *n*0, *n*<sup>1</sup> with π ′ ∈ *PathsG*(*s*, *n*), *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, π ′′ ∈ *PathsG*(*n*0, *n*<sup>1</sup> ) and *n*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>*. By induction hypothesis, applied to <sup>π</sup> ′ and π ′′, we get π ′ ∈ *X*0(*s*, *n*) and π ′′ ∈ *X*0(*n*0, *n*<sup>1</sup> ). This implies π ∈ *X*0(*s*, *t*) by sl-sl.

**Theorem 5.36.** *ASC is the least element X* ∈ *N* × *N* → 2 *E* ⋆ *with the following closure properties*

□

$$\left(\text{asc-EMrry}\right)\xrightarrow[\varepsilon\in\text{X(s,s)}]{}\left(\text{asc-asc}\right)\xrightarrow{\pi\in\text{X(s,t')}\quad t'\xrightarrow{\varepsilon}\ t\quad e\in\text{E}\_{\text{intra}}\cup\text{E}\_{\text{ret}}}$$

$$(\text{asc-sr})\xrightarrow{\pi\in\text{X}(\text{s},n)}\frac{\pi\in\text{X}(\text{s},n)\quad n\overset{\text{e}\_{\text{call}}}{\xrightarrow{\pi}}n\_{0}\quad\pi'\in\text{SL}(n\_{0},n\_{1})\quad n\_{1}\overset{\text{e}\_{\text{ret}}}{\xrightarrow{\pi}}t\quad(e\_{\text{call}},e\_{\text{ret}})\in\Phi}{\pi\cdot e\_{\text{call}}\cdot\pi'\cdot e\_{\text{ret}}\in\text{X}(\text{s},t)}$$

200

*Proof.* This can be shown analogously to Theorem 5.35. □

**Theorem 5.37.** *DESC is the least element X* ∈ *N* × *N* → 2 *E* ⋆ *with the following closure properties*

$$\left(\mathsf{pESC{-}EMPTY}\right)\frac{\mathsf{}}{\varepsilon \in X(s,s)}$$

$$\pi\_{\left(\mathsf{p\_{\mathsf{E}}\mathsf{c}\mathsf{c}\mathsf{c}\mathsf{c}\mathsf{c}\mathsf{c}\mathsf{c}\right)}}\frac{\pi\in X(s,t')\:\mathsf{t}'\xrightarrow{\mathcal{C}}\mathsf{t}\mathsf{ }e\in E\_{\mathsf{intra}}\cup E\_{\mathsf{call}}}{\pi\cdot e\in X(s,t)}$$

$$(\mathsf{p\_{\mathsf{reset-SL}}}) \frac{\pi \in \mathrm{X}(s, n) \cdot n \stackrel{\mathcal{e\_{call}}}{\xrightarrow{\mathcal{E}\_{\mathsf{call}}}} n\_0 \cdot \pi' \in \mathrm{SL}(n\_0, n\_1) \cdot n\_1 \stackrel{\mathcal{e\_{ret}}}{\xrightarrow{\mathcal{E}\_{\mathsf{ret}}}} t \ (\mathsf{e\_{call}}, \mathsf{e\_{ret}}) \in \Phi}{\pi \cdot e\_{call} \cdot \pi' \cdot e\_{ret} \in \mathrm{X}(s, t)}$$

*Proof.* This can be shown analogously to Theorem 5.35. □

**Theorem 5.38.** *For all s*, *t* ∈ *N, VP*(*s*, *t*) *can be characterized as follows:*

(valid-asc-desc)

*VP*(*s*, *t*) = {π<sup>1</sup> · π<sup>2</sup> | ∃*n* ∈ *N*.π<sup>1</sup> ∈ *ASC*(*s*, *n*) ∧ π<sup>2</sup> ∈ *DESC*(*n*, *t*)}

*Proof.* This follows from Theorem 5.30, Lemma 2.23 and Lemma 2.24. □

**Theorem 5.39.** *The following statemens are true.*

*1. Valid paths are closed under taking sub-paths, in particular under taking su*ffi*xes and prefixes.*

*2. Ascending paths are closed under taking su*ffi*xes.*

*3. Descending paths are closed under taking prefixes.*

*4. Taking a prefix of an ascending path or a su*ffi*x of a descending path yields a valid path.*

*Proof.* This can be derived by combining Remark 2.25 and Theorem 5.18.

□

**Theorem 5.40.** *For all s*, *t*, *t* ′ ∈ *N, we have*

∀π ∈ *ASC*(*s*, *t*). ∀π ′ ∈ *ASC*(*t*, *t* ′ ). π · π ′ ∈ *ASC*(*s*, *t* ′ (5.4) )

$$\text{(5.5)}\qquad \forall \pi \in \text{DECC}(\mathbf{s}, t). \; \forall \pi' \in \text{DECC}(t, t'). \; \pi \cdot \pi' \in \text{DECC}(\mathbf{s}, t').$$

∀π ∈ *VP*(*s*, *t*). ∀π ′ ∈ *SL*(*t*, *t* ′ ). π · π ′ ∈ *SL*(*s*, *t* ′ (5.6) )

201

*Proof.* The first two statements can be shown using Lemma 2.23, Theorem 5.20, Theorem 5.21 and the definitions of ascending and descending paths.

For the third statements, by Theorem 5.38 it suffices to show the respective property for descending paths. But this follows from Theorem 5.37.

□

With an additional regularity restriction of Φ, I can show that same-level paths end in the same procedure that they started in.

**Remark 5.41.** *Assume that the correspondence relation* Φ *has the following property*

$$(\text{5.7})\qquad (e\_{\text{call}}, e\_{\text{ret}}) \in \Phi \implies \text{proc}(\text{src}(e\_{\text{call}})) = \text{proc}(\text{tgt}(e\_{\text{ret}})) $$

*Then the following statement holds: If* π *is a same-level path from s to t, then proc*(*s*) = *proc*(*t*)*.*

*Proof.* By induction on <sup>π</sup> <sup>∈</sup> *SL*(*s*, *<sup>t</sup>*). □

Table 5.1 shows an overview of some notions of validness in the literature. The publications there can roughly be split into two groups: Those that characterize valid paths as suffixes of descending paths and those that characterize them as concatenations of ascending and descending paths. While the former notion is sensible for contexts in which valid paths are supposed to correspond to actual program executions28, the latter notion is more general. According to Theorem 5.39, suffixes of descending paths are always valid. Conversely, however, interprocedural graphs in general may contain valid paths that are not suffixes of any descending path. The two notions coincide if additional assumptions are made, as Theorem 5.42 states. Note that such assumptions are *not* made for the rest of

**Theorem 5.42.** *Let G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ) *be an interprocedural graph. Assume that every procedure graph Gp has a distinguished entry sp and that the following conditions hold:*

this thesis.

<sup>28</sup>They are also called *realizable* paths for this reason.


**Table 5.1:** Notions of validness in the literature

*There is a procedure main* ∈ *P such that the entry smain of Gmain reaches every s<sup>p</sup> using a descending path.*

∃*main* ∈ *P*. ∀*p* ′ (main-reach) <sup>∈</sup> *Proc*. *SL*(*smain*,*sp*) <sup>≠</sup> <sup>∅</sup>

*In every procedure graph Gp, every node n* ∈ *N<sup>p</sup> is same-level reachable from sp.*

(sl-reach) <sup>∀</sup>*<sup>p</sup>* <sup>∈</sup> . <sup>∀</sup>*<sup>n</sup>* <sup>∈</sup> *<sup>N</sup>p*. *SL*(*sp*, *<sup>n</sup>*) <sup>≠</sup> <sup>∅</sup>

*For every return edge e, there is a corresponding call edge that enters the procedure that e starts in:*

$$(\text{ret-call}) \qquad \forall e \in E\_{\text{ret}}. \; \exists e' \in E\_{\text{call}}. (e', e) \in \Phi \land t \text{gt}(e') = s\_{\text{proc}(\text{src}(e))}$$

*Then every valid path is the su*ffi*x of a descending path starting in smain:*

$$\begin{aligned} \text{(5.8)} \qquad & \forall \pi\_2 \in VP. \forall m, n \in N. \\ & \pi\_2 \in Paths\_G(m, n) \\ & \implies \exists \pi\_1 \in Paths\_G(s\_{\text{main}}, m). \ \pi\_1 \cdot \pi\_2 \in DEC(s\_{\text{main}}, n). \end{aligned}$$

*Proof.* It suffices to show that (5.8) holds for the ascending paths:

$$\begin{aligned} \text{(5.9)} \qquad & \forall \pi\_2 \in A \text{SC. } \forall m, n \in \text{N}. \\ & \pi\_2 \in \text{Paths}\_G(m, n) \\ & \implies \exists \pi\_1 \in \text{Paths}\_G(s\_{\text{main}}, m). \ \pi\_1 \cdot \pi\_2 \in \text{DECC}(s\_{\text{main}}, n). \end{aligned}$$

Suppose that (5.9) is true and let π ∈ *VP*(*m*, *n*) be a valid path. Then by Theorem 5.38, we obtain *m*<sup>0</sup> ∈ *N*, π<sup>1</sup> ∈ *ASC*(*m*, *m*0), π<sup>2</sup> ∈ *DESC*(*m*0, *n*) such that π = π<sup>1</sup> · π2. Now we apply (5.9) to π<sup>1</sup> and obtain σ ∈ *PathsG*(*smain*, *m*) such that σ · π<sup>1</sup> ∈ *DESC*(*smain*, *m*0). With π<sup>2</sup> ∈ *DESC*(*m*0, *n*), we have σ · π<sup>1</sup> · π<sup>2</sup> ∈ *DESC*(*smain*, *n*), as desired.

It remains to show (5.9). We proceed by induction on the number *k* of unmatched return positions in π<sup>2</sup> ∈ *ASC*.

**base case (***k* = 0**):** If π<sup>2</sup> has no unmatched return positions, then π<sup>2</sup> ∈ *SL*, because π<sup>2</sup> ∈ *ASC*, which means that π<sup>2</sup> has also no unmatched call positions. Now let *m*, *n* ∈ *N* such that π<sup>2</sup> ∈ *SL*(*m*, *n*) and let *p* = *proc*(*m*). We apply (main-reach) to *sp* and obtain π ′ 1 ∈ *DESC*(*smain*,*sp*). Moreover, we apply (sl-reach) to *m* ∈ *N<sup>p</sup>* and obtain π ′′ 1 ∈ *SL*(*sp*, *m*). Now consider π1 *de f* = π ′ 1 · π ′′ 1 . With the help of Theorem 5.40, from π ′ 1 ∈ *DESC*(*smain*,*sp*), π ′′ 1 ∈ *SL*(*sp*, *m*) and π<sup>2</sup> ∈ *SL*(*m*, *n*), we conclude π ′ 1 · π ′′ 1 · π<sup>2</sup> = π<sup>1</sup> · π<sup>2</sup> ∈

*DESC*(*smain*, *n*), as desired.

**induction step (***k* → *k* + 1**):** Let π<sup>2</sup> be an ascending path with *k* + 1 unmatched return positions. The induction hypothesis states that the claim is true for all ascending paths with *k* unmatched return positions. Let *m*, *n* ∈ *N* such that π<sup>2</sup> ∈ *ASC*(*m*, *n*) and let *j*<sup>0</sup> be the least unmatched return position in π2. Then we write π<sup>2</sup> as

$$
\pi\_2 = \pi\_2' \cdot e\_{ret} \cdot \pi\_2''
$$

where

$$
\begin{aligned}
\pi\_2' &\stackrel{d\varepsilon f}{=} \pi\_2^{j\_0} \in \operatorname{Paths}\_G(n\_{1'}n).
\end{aligned}
$$

for some *n*0, *n*<sup>1</sup> ∈ *N*, *eret* ∈ *Eret*.

Due to the maximality property of *j*0, π ′ 2 contains no unmatched return positions. Moreover, it cannot contain any unmatched call position. Assume, for the purpose of contradiction, that π ′ 2 contains an unmatched call position. Then the greatest such position is also a call position in π2, which would be matched by the return position *j*0, in contradiction to the choice of *j*<sup>0</sup> as unmatched return position. Hence, the assumption is false, which means that all call positions in π ′ <sup>2</sup> must be matched. Hence, π ′ 2 ∈ *SL*(*m*, *n*0). Moreover, π ′′ 2 is a suffix of the ascending path π<sup>2</sup> and therefore ascending because of Theorem 5.39.

Now let *p* = *proc*(*m*). We apply (ret-call) to *eret* and obtain *m*<sup>0</sup> *<sup>e</sup>call* <sup>→</sup> *<sup>s</sup><sup>p</sup>* such that (*ecall*,*eret*) ∈ Φ. Moreover, we apply (sl-reach) to *m* and obtain π<sup>0</sup> ∈ *SL*(*sp*, *m*). Now consider the path

$$\boldsymbol{\Theta} \stackrel{def}{=} \boldsymbol{e}\_{\text{call}} \cdot \boldsymbol{\pi}\_0 \cdot \boldsymbol{e}\_{\text{ret}} \cdot \boldsymbol{\pi}\_2 = \boldsymbol{e}\_{\text{call}} \cdot \boldsymbol{\pi}\_0 \cdot \boldsymbol{e}\_{\text{ret}} \cdot \boldsymbol{\pi}\_2' \cdot \boldsymbol{e}\_{\text{ret}} \cdot \boldsymbol{\pi}\_2''$$

from *m*<sup>0</sup> to *n*0: It is the concatenation of the same-level path *ecall* · π<sup>0</sup> ·*eret* and the ascending path π<sup>2</sup> and therefore ascending itself. Moreover, it contains *k* unmatched return positions because by choice of *ecall*, we have (0, 1 + *j*0) ∈ νθ. Hence, we can apply the induction hypothesis to θ and obtain σ ∈ *PathsG*(*smain*, *m*0) such that

$$
\sigma \cdot \theta \in DES(s\_{main}, n).
$$

With

$$
\pi\_1 \stackrel{def}{=} \sigma \cdot e\_{call} \cdot \pi\_{0\prime}
$$

we have π<sup>1</sup> · π<sup>2</sup> = σ · θ ∈ *DESC*(*smain*, *n*), as desired.

□

# **5.3 Data-Flow Analysis on Interprocedural Graphs**

Next, I define data-flow analysis instances for interprocedural graphs. This is a general and formal version<sup>29</sup> of the notions I have already described in subsubsection 3.2.2.1 on page 48.

**Definition 5.43.** *A data-flow analysis instance* F = (*G*, *L*, *F*, ρ) *consists of*

• *an interprocedural graph G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ)*,*

<sup>29</sup>Note that Definition 5.43 omits the initial information *init*. I will discuss this slight modification in subsection 9.2.2.

• *a complete lattice* (*L*,≤)*,*


Instead of ρ(*e*) I will write *fe*. I extend this notation to arbitrary paths by defining

$$\text{(5.10)}\qquad\qquad\qquad\qquad\qquadf\_{\varepsilon}\stackrel{\text{def}}{=}\text{id}$$

$$\text{(5.11)}\qquad\qquad\qquad\qquad f\_{\pi \cdot \varepsilon} \stackrel{def}{=} f\_{\varepsilon} \circ f\_{\pi \cdot \varepsilon}$$

Next, I want to introduce my generalized variant of the *merge-over-allvalid-paths* solution *MOVP*. This version of *MOVP* is more general in two aspects:

1. it not only considers paths that start in a fixed entry node, but takes the starting node as additional argument,

2. it not only considers descending paths but also paths with an ascending prefix.

Moreover, I explicitly do not make any assumptions about reachability. Hence, the value *MOVP*(*s*,*t*) not only reflects the resulting value if we merge the path functions for all valid paths from *s* to *t*, but it also communicates whether *VP*(*s*, *t*) is empty or not. In traditional data-flow analyses, this is never the case because they only consider *s*,*t* where *VP*(*s*,*t*) is not empty. However, ⊥*<sup>F</sup>* can still be a valid analysis result value, even if *VP*(*s*, *<sup>t</sup>*) <sup>≠</sup> <sup>∅</sup>.

Hence, in order to be able to distinguish between analysis results for non-empty and empty path sets, I adjoin *F* with an additional element ⊠ that represents *undefinedness*. Before I discuss the properties of ⊠, I give the definition of *MOVP* in Definition 5.44. Since I will also consider *Merge-Over-*P*-solutions* for other sets of paths than *VP*, Definition 5.44 is more general than needed right now.

**Figure 5.4:** Example in which it is important that ⊠ kills everything – *e* is an intraprocedural edge

**Definition 5.44.** *Let* P ⊆ *Paths*(*G*) *be a set of paths. The* Merge-Over-Psolution

$$\mathsf{MOP} : N \times N \to F\_{\mathsf{E}}$$

*is defined by*

$$\text{MO}\mathcal{P}(\mathbf{s},t) = \bigsqcup\_{\pi \in \mathcal{P} \cap \text{Paths}\_G(\mathbf{s},t)} f\_{\pi}.$$

Now I discuss the assumptions about and properties of ⊠ more closely. Firstly, I assume that ⊠ is smaller than any element of *F*.


This means that it never coincides with any *f*π:


These properties ensure that

*MOVP*(*s*, *<sup>t</sup>*) = <sup>⊠</sup> if and only if *VP*(*s*, *<sup>t</sup>*) = <sup>∅</sup>.

Moreover, I extend the function composition on *F* to *F*⊠ in a way such that ⊠ kills every value already computed:

$$(\mathsf{5.16})\qquad\forall f\in F\_{\mathsf{E}}.\,f\circ\mathsf{E}=\mathsf{E}\circ f=\mathsf{E}.$$

I make this assumption because I want to compose different values of *MOVP* consistently without explicitly thinking about ⊠ or whether the corresponding *VP* sets are empty or not. For illustration, consider the graph in Figure 5.4. Since *VP*(*s*,*t* ′ ) = ∅, we have *MOVP*(*s*,*t* ′ ) = ⊠. (5.16) ensures that I get the same result by computing *f<sup>e</sup>* ◦ *MOP*(*s*, *t*).

Instead of adjoining *F* with ⊠, I could have made two alternative choices that one could make to achieve the same goals. For one, I also could use partial functions *N* × *N* → *F* for my solution space. Then I could say that (*s*,*t*) does not belong to the domain of *MOVP* if *VP* is empty. In my approach, I communicate this by letting *MOVP*(*s*,*t*) be ⊠ in such cases. The other approach would be to restrict *F* to only allow *strict* functions. A function *f* : *L* → *L* is called *strict*, if *f*(*x*) = ⊥*<sup>L</sup>* is equivalent to *x* = ⊥*L*. If all *f* ∈ *F* are strict, then I can use λ*x*.⊥ as bottom element of *F* to represent the merge over the empty path set.

I decided to use ⊠ explicitly because I did not want to restrict the transfer functions but also did not want to introduce additional notational overhead for dealing with partial functions. I will make use of one additional convention: At several places, I will use elements ψ : *N* × *N* → *F*<sup>⊠</sup> as functions. Whenever I do this and do not explicitly discuss whether ψ = ⊠ or not, I will silently assume ψ ≠ ⊠. For instance, if I state equations such as ψ(*x*) = *y*, then I will silently assume that ψ is indeed a function.

I conclude this section by considering *distributivity*, an important property of transfer functions and frameworks. *Distributivity* ensures that the constraint systems that I show in chapter 6 coincide with their respective MOP solutions.

**Definition 5.45.** • *Let L be a complete lattice and F* ⊆ *L* →*mon L be a complete lattice of monotone functions that is closed under function composition. Then*

*1. f* ∈ *F is called strict, if*

$$\text{(5.17)}\qquad\qquad\qquad\qquad f\circ\bot = \top$$

*2. f* ∈ *F is called* distributive*, if*

$$(\text{5.18)}\qquad\qquad\forall \text{g}, h \in \text{F}. f \circ (\text{g} \sqcup h) = f \circ \text{g} \sqcup f \circ h$$

*3. f* ∈ *F is called* positive-distributive*, if*

$$(5.19) \qquad \forall A \subseteq FA \neq \emptyset \implies f \circ \bigsqcup A = \bigsqcup \{ f \circ g \mid g \in A \}$$

*4. f* ∈ *F is called* universally distributive*, if it is strict and positivedistributive, i.e. if*

$$(\text{5.20}) \qquad \forall A \subseteq F. f \circ \bigsqcup A = \bigsqcup f \circ \emptyset \text{ } \emptyset \text{ } \emptyset \text{ } A\text{-}A\text{-}A$$

*5. F is called strict, distributive, positive-distributive, universally distributive if all f* ∈ *F have the respective property.*

• *A data-flow framework instance* F = (*G*, *L*, *F*, ρ) *is called strict, distributive, positive-distributive, universally distributive if F has the respective property.*

*In the following, I will use "u.d." to abbreviate the term "universally distributive".*

The data-flow framework instances that I consider in this thesis are automatically strict because of (5.13), (5.12) and (5.16). Under these assumptions, a data-flow analysis framework instance is universally distributive if *F*⊠ is positive-distributive. However, it is worth mentioning that this applies to *F*⊠ and *not* to *F*: In order for *F*⊠ to be positivedistributive, *F* needs to be universally distributive. In this sense, adjoining ⊠ does not magically make *F* strict, but at least makes it possible to distinguish reachable nodes from unreachable parts of the given graph. In chapter 7, I will only consider data-flow analysis framework instances F = (*G*, *L*, *F*⊠, ρ) in which *F*<sup>⊠</sup> additionally satisfies (ACC). For such instances, distributivity is equivalent to universal distributivity.

# **5.4 Example Instances**

In this section, I want to discuss various data-flow analyses that can be expressed and solved using the abstract data-flow framework presented earlier.

# **5.4.1 Traditional Data-Flow Analyses on Interprocedural Control-Flow Graphs**

According to Example 5.33, interprocedural control-flow graphs can be regarded as interprocedural graphs. Hence, all traditional data-flow analyses in the sense of subsubsection 3.2.2.2 can be expressed as data-flow analysis instances in the sense of Definition 5.43. The solution *MOVP* defined in Definition 5.44 is more general, since it does not use the entry *smain* of the *main* procedure as starting point but rather considers all paths between arbitrary nodes *s* and *t*. Moreover, *MOVP* merges over all valid paths and not over all descending paths and also takes reachability into account. I already discussed this in subsection 5.4.2 and section 5.3.

# **5.4.2 Slicing**

As I already pointed out in subsection 3.3.4, slicing on program dependence graphs can be expressed as *reachability*, a very simple data-flow analysis instance. We can generalize this further by considering slicing on interprocedural graphs, which are a generalization of interprocedural program dependence graphs according to Example 5.33.

Let *G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ) be an interprocedural graph.

As for IPDGs, the *backwards slice* of a node *n* ∈ *N* can be characterized as the set of nodes which may reach *n* by a valid path. Analogously, the *forward slice* can be characterized as the set of nodes which may be reached by a valid path.

$$\text{(5.21)}\qquad\qquad BS(n) = \{m \in N \mid VP(m, n) \neq \emptyset\}$$

$$\text{(5.22)}\qquad\qquad FS(n) = \{m \in N \mid VP(n, m) \neq \emptyset\}$$

As in subsection 3.3.4, the reachability data-flow analysis instance (*G*, *L*, *F*, ρ) is defined as follows:

$$\begin{aligned} \text{(5.23)}\\ \{\text{(5.23)}\quad \text{(5.24)}\\ \rho \stackrel{def}{=} \lambda e.i d \end{aligned} \qquad \begin{aligned} \text{(\bot \text{ }\uparrow \text{ }\downarrow \text{ }\uparrow \text{)}\\ \{\text{(\lambda x. \bot \text{)}} \land e.\lambda x.\top\} \\ \{\text{(\lambda x. \bot \text{)}} \land e.\lambda x.\top\} \end{aligned} \qquad \begin{aligned} \text{(\bot \text{ }\downarrow \text{ }\uparrow \text{)} \\ \{\text{(\lambda x. \bot \text{)}} \land e.\lambda x.\top\} \end{aligned} \qquad \begin{aligned} \text{(\bot \text{ }\downarrow \text{ }\downarrow \text{)} \\ \{\text{(\lambda x. \bot \text{)}} \land e.\lambda x.\top\} \end{aligned}$$

Now, observe that

$$\forall \pi \in VP(s, t). f\_{\pi} = id$$

Hence, we have

$$\textit{MOVP}^{\mathcal{F}}(s,t) = \bigsqcup\_{\pi \in VP(s,t)} f\_{\pi} = \begin{cases} id & \text{if } VP(s,t) \neq \emptyset \\ \boxtimes & \text{otherwise} \end{cases}$$

This allows us to rewrite *BS* and *FS* as

$$\begin{aligned} BS(n) &= \{ m \in N \mid MOVP(m, n) \neq \mathbb{B} \} \\ FS(n) &= \{ m \in N \mid MOVP(n, m) \neq \mathbb{B} \} \end{aligned}$$

So, by computing *MOVP*, we can extract *BS* and *FS*.

# **5.4.3 Chopping**

Chopping [48, 138] was proposed as a means to make slicing on PDGs more focussed. Roughly, the idea is not to go back or forward from a single node, but to consider all nodes that may lie on paths *between* two nodes. Like slicing, chopping can also be considered on a general interprocedural graph *G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ). Given *s*,*t* ∈ *N*, the chop between *s* and *t* is defined as

$$\mathcal{CH}(\mathbf{s}, t) \stackrel{def}{=} \{ n \in \mathcal{N} \mid \exists \pi \in VP(\mathbf{s}, t) . n \in nodes(\pi) \}.$$

where for a sequence of edges π ∈ *E* <sup>⋆</sup>, *nodes*(π) is the set of nodes that occur in the symbol sequence:

$$(\text{5.24})\quad nodes(\pi)\stackrel{def}{=}\langle \mathfrak{n}\in \mathcal{N} \mid \exists i \in \text{range}(\pi). \mathfrak{n} = \text{src}(\pi^i) \vee \mathfrak{n} = \text{tgt}(\pi^i) \rangle.$$

Note that chops and slices actually have a strong connection. For *s* ≠ *t*, we have *<sup>s</sup>* <sup>∈</sup> *BS*(*t*) if and only if *CH*(*s*,*t*) <sup>≠</sup> <sup>∅</sup>. Hence, applications such as slicing-based information flow control can also be expressed using chopping.

Chopping can be expressed as a data-flow analysis instance as follows.

$$L \stackrel{def}{=} \langle \mathfrak{L}^N \rangle \Xi$$
 (5.25) 
$$\mathcal{F} \stackrel{def}{=} \langle \lambda X. (X \cap A) \cup B \mid A, B \in \mathfrak{L}^N \rangle$$
 
$$f\_\mathfrak{e}(X) \stackrel{def}{=} X \cup \{src(e), tgt(e)\}$$

It can easily be seen that (*G*, *L*, *F*, ρ) is indeed a data-flow framework instance with respect to Definition 5.43.

Moreover, using induction on the length of paths, we can show that

$$(\text{5.26}) \qquad \forall \pi \in \text{Paths}\_G(s, t). f\_{\pi}(X) = X \cup \text{nodes}(\pi).$$

Hence, we have

$$\begin{aligned} \mathsf{CH}(s,t) &= \bigcup\_{\pi \in VP(s,t)} nodes(\pi) & \{ \text{ definition } \} \\ &= \bigcup\_{\pi \in VP(s,t)} f\_{\pi}(\emptyset) & \{ \text{by (5.26) } \} \\ &= \left( \bigcup\_{\pi \in VP(s,t)} f\_{\pi} \right)(\emptyset) & \{ \text{ rewriting } \} \\ &= MOVP(s,t)(\emptyset) & \{ \text{ definition } \} \end{aligned}$$

Analogously, we can consider the *edge chop*

$$\text{(5.27)}\qquad \begin{array}{c} \text{ECH(s,t)} \stackrel{def}{=} \{ n \in \text{N} \mid \exists \pi \in VP(s,t) . n \in \text{edges}(\pi) \}. \end{array}$$

where

$$(5.28)\qquad\qquad\text{edges}(\pi)\stackrel{def}{=}\{e\in\mathcal{E}\mid\exists i\in\text{range}(\pi).\,e=\pi^{i}.\}$$

Edge chops can be computed using the following data-flow analysis instance.

$$L \stackrel{def}{=} \begin{pmatrix} \mathbf{2}^E, \mathbf{\underline{C}} \end{pmatrix}$$
 
$$\mathbf{F} \stackrel{def}{=} \{ \lambda \mathbf{X}. (\mathbf{X} \cap A) \cup \mathcal{B} \mid A, \mathcal{B} \in \mathbf{2}^E \}$$
 
$$f\_\mathbf{\mathcal{e}}(\mathbf{X}) \stackrel{def}{=} X \cup \{ \mathbf{e} \}$$

Like for node chops, it can be easily verified that this indeed satisfies Definition 5.43.

# **5.4.4 Strong Bridges and Strong Articulation Points**

*Strong bridges* [95] are a graph theoretical concept that describes the connectivity properties of a given directed graph. Intuitively, a strong bridge is an edge that disconnects a graph if it is removed. Strong bridges are interesting for applications such as slicing-based information flow control. Consider a PDG *G* = (*N*, *E*) and two nodes *s*,*t* ∈ *N*. Remember that slicing-based IFC works by computing the backwards slice *BS*(*t*) of *<sup>t</sup>* and checking whether *<sup>s</sup>* <sup>∈</sup> *BS*(*t*) or not. If *<sup>s</sup>* <sup>∉</sup> *BS*(*t*), then it is definitely not the case that *s* influences *t* in any way and if *s* ∈ *BS*(*t*), this may be the case. Equivalently, we can also compute the chop *CH*(*s*,*t*) of *s* and *t* and consider the *chop graph Cs*,*t* = (*CH*(*s*, *t*), *ECH*(*s*, *t*)). Now, suppose that *Cs*,*t* contains a bridge and we can show that this bridge is actually not justified, i.e. *G* would not contain it if it was obtained using a more precise analysis. Then this means that *s* actually does not influence *t*. Hence, strong bridges can be a tool for eliminating false alarms. This kind of reasoning has been applied to PDGs by Beckert, Bischof et al. [26].

Strong bridges as considered by, e.g., Italiano et al. [95], can be generalized to interprocedural graphs.

Given an interprocedural graph *G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ) and *s*, *t* ∈ *N*, a *strong bridge* with respect to *s* and *t* is an edge *e* ∈ *E* such that *e* ∈ *edges*(π) for all π ∈ *VP*(*s*, *t*).

The set of strong bridges is then defined as

$$(\text{5.30})\qquad \quad SB(\text{s},t) = \{ e \in E \mid \forall \pi \in VP(\text{s},t). \, e \in edges(\pi) \}.$$

Note that this is dual to the edge chops defined in (5.27). Hence, a dataflow analysis instance for expressing strong bridges can be obtained by reversing the partial order used for the data-flow analysis defined by (5.29):

$$\begin{aligned} L &= (\mathbb{Z}^E, \mathbb{Z})\\ F &= \{\lambda X. (X \cap A) \cup B \mid A\_\prime B \subseteq E\} \\ f\_\mathfrak{e}(A) &= A \cup \{\mathfrak{e}\} \end{aligned}$$

It is easy to see that *F* is indeed closed under composition and contains the identity function. We also note that if *f* = λ*X*.(*X* ∩ *A*<sup>1</sup> ) ∪ *B*<sup>1</sup> and *g* = λ*X*.(*X* ∩ *A*2) ∪ *B*2, then

$$(f \sqcup g)(X) = f(X) \cap g(X) = (X \cap A\_3) \cup B\_3$$

with *A*<sup>3</sup> = (*A*<sup>1</sup> ∩ *A*2) ∪ (*A*<sup>1</sup> ∩ *B*2) ∪ (*A*<sup>2</sup> ∩ *B*<sup>1</sup> ) and *B*<sup>3</sup> = *B*<sup>1</sup> ∩ *B*2. Due to the finiteness of *E*, this makes *F* a complete lattice with respect to the functional join induced by the join ∩ of (2 *<sup>E</sup>*,⊇).

The node analogon to strong bridges are *strong articulation point*. A strong articulation point is a node that disconnects a given graph if removed. A data-flow analysis instance that is able to compute the strong articulation points of a given graph can be specified as follows:

$$\begin{aligned} L &= (\mathfrak{L}^N, \Xi) \\ \text{(5.32)} & \quad F = \{ \lambda X. (X \cap A) \cup B \mid A, B \subseteq N \}, \\ f\_\ell(A) &= A \cup \{src(e), tg\mathfrak{t}(e)\}. \end{aligned}$$

# **5.4.5 Restricting to Paths With Regular Properties**

The reachability analysis shown in subsection 5.4.2 can be generalized to language-restricted reachability. In this subsection, I am going to demonstrate this for forward reachability and regular languages. I also will show two special cases of this.

Remember that a *finite automaton* is a quintuple

$$(\text{5.33})\qquad\qquad\qquad\qquad\mathcal{R}=(Q\_{\prime}A\_{\prime}q\_{0\prime}\Delta\_{0\prime}Q\_{\mathbb{F}})\dots$$

The components of are


Now, I recall the definition of the *language recognized by a finite automaton* A = (*Q*, *A*, *q*0, ∆0, *QF*). For this, I define

$$
\Delta: E^\star \to \mathcal{2}^Q \to \mathcal{2}^Q
$$

by

$$(\text{5.34})\qquad\qquad\Delta(\mathfrak{e}, \mathcal{S}) = \mathcal{S}$$

$$(\text{5.35})\qquad\Delta(\pi \cdot e\_\prime S) = \{q' \in Q \mid \exists q \in \Delta(\pi, S) . q' \in \Delta\_0(q\_\prime e)\}$$

Then the *language recognized by* A can be defined as

$$(\mathfrak{5}.36)\qquad\qquad\mathcal{L}(\mathcal{H})=\langle\pi\in E^{\star}\mid\Delta(\pi,\langle q\_{0}\rangle)\cap Q\_{F}\neq\emptyset\rangle$$

It is well-known that the regular languages are exactly the languages that are recognized by a finite automaton.

Now let *R* ⊆ *E* <sup>⋆</sup> be a regular language of edge sequences. I define the *R-restricted forward slice of s* by

$$\text{(5.37)}\qquad\qquad\qquad\quad FS(s,\mathbb{R})\stackrel{def}{=}\{t\in\mathbb{N}\mid VP(s,t)\cap\mathbb{R}\neq\emptyset\}.\tag{5.38}$$

In the following, I show how to compute *FS*(*s*, *R*) using a data-flow analysis instance. For this, let A = (*Q*, *E*, *q*0, ∆0, *QF*) be a finite automaton with

$$\text{(5.38)}\tag{7.38}$$

$$\mathcal{L}(\mathcal{A}) = \mathcal{R}.$$

The idea is that the information computed at each node consists of the set of states in *Q* that are reachable during a run of A. Consequently, the transfer functions are defined by ∆. Before I define this data-flow analysis instance, I need some helping notions and observations. Firstly, I observe that for each *e* ∈ *E*, the function λ*A*. ∆(*e*, *A*) is monotone: If *A* ⊆ *B*, then ∆(*e*, *A*) ⊆ ∆(*e*, *B*). Secondly, for a complete lattice *L* and a set *X* ⊆ *L* →*mon L* of monotone functions on *X*, I define *cl*(*X*) ⊆ *L* →*mon L* as the least (with respect to set inclusion) subset *Y* of *L* →*mon L* that (a) contains *X*, (b) is a complete lattice and (c) is closed under function composition. It is easy to see that *cl*(*X*) is unique and always exists.

Now we are ready to define the data-flow analysis instance.

*L* = 2 *<sup>Q</sup>* (5.39)

$$\text{(5.40)}\qquad F = cl(\{\lambda A.\,\,\Delta(e\_\prime A) : \mathbf{2}^Q \to \mathbf{2}^Q \mid e \in E\})$$

$$(5.41)\qquad\qquad f\_{\mathfrak{E}} = \lambda A.\Delta(e\_\prime A): \mathbb{2}^{\mathcal{Q}} \to \mathbb{2}^{\mathcal{Q}}:$$

**Theorem 5.46.** *For every s* ∈ *N, we have*

$$(\text{5.42})\qquad \quad FS(s,R) = \{ t \in N \mid MOVP(s,t)(\{q\_0\}) \cap Q\_F \neq \emptyset \}$$

*Proof.* Let *s* ∈ *N*. Firstly, by induction on π ∈ *E* <sup>⋆</sup>, it can easily be shown that

$$(\text{5.43}) \qquad \qquad \forall \pi \in E^{\star}. \forall A \subseteq Q. \, f\_{\pi}(A) = \Delta(\pi\_{\prime}A).$$

Secondly, we observe that

$$(\text{5.44}) \qquad \qquad \text{MOVP}(\text{s}, t)(\langle q\_0 \rangle) = \bigcup\_{\pi \in VP(\text{s}, t)} f\_{\pi}(q\_0) \dots$$

This follows from the definition of the given data-flow framework instance and the definition of *MOVP*.

From (5.43) and (5.44), we can derive the claimed set equality.

$$\begin{aligned} t \in FS(s, \mathbb{R}) &\iff VP(s, t) \cap \mathbb{R} \neq \emptyset &\qquad \text{(definition }\{\}\text{)}\\ &\iff \exists \pi \in VP(s, t), \pi \in \mathbb{R} &\qquad \text{(rewriting }\} \\ &\iff \exists \pi \in VP(s, t). \,\Delta(\pi, \langle q\_{0} \rangle) \cap Q\_{F} \neq \emptyset &\qquad \text{(5.38)}\\ &\iff \exists \pi \in VP(s, t). \, f\_{\pi}(\langle q\_{0} \rangle) \cap Q\_{F} \neq \emptyset &\qquad \text{(5.43)}\\ &\iff MOVP(s, t)(\langle q\_{0} \rangle) \cap Q\_{F} \neq \emptyset &\qquad \text{(5.44)}\\\end{aligned}$$

□

### **5.4.5.1 Barrier Slicing**

A simple yet important special case of language-restricted slices is *barrier slicing*. Barrier slicing was introduced for PDGs by Krinke [111] as a means to make slicing more focussed. The idea is to introduce a *barrier*, i.e. a set *B* of nodes that is not to be passed. In the following, I demonstrate that barrier slicing can be expressed as a regular language-restricted reachability analysis instance on interprocedural graphs.

Given an interprocedural graph *G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ) (e.g., a program dependence graph), the *(backwards) barrier slice* of *t* ∈ *N* with respect to *B* is defined to be the set

$$(\text{5.45})\qquad\qquad\qquad\qquad\quad\quad\quad\quad\text{BBS}(t,\mathcal{B})\stackrel{def}{=}\{s\in\mathcal{N}\mid\exists\pi\in VP(s,t).\,\text{nodes}(\pi)\cap\mathcal{B}=\emptyset\}.\,\Box$$

Analogously, the *forward barrier slice* can be defined as

$$\text{(5.46)}\qquad\text{FBS}(s,\mathcal{B}) \stackrel{def}{=} \{ t \in \mathcal{N} \mid \exists \pi \in \text{VP}(s,t). \text{ } nodes(\pi) \cap \mathcal{B} = \emptyset \}.$$

The property of being a sequence from *E* <sup>⋆</sup> that avoids nodes from *B* can be expressed as the language

$$(5.47)\qquad A(B) \stackrel{def}{=} \{ \pi \in \mathbb{E}^\star \mid \forall i \in \operatorname{range}(\pi). \operatorname{src}(\pi^i) \notin B \land \operatorname{tgt}(\pi^i) \notin B \}.$$

It can easily be seen that *A*(*B*) is regular. Hence *FBS*(*s*, *B*) and *BBS*(*t*, *B*) can both be determined using a regular language-restricted reachability analysis.

### **5.4.5.2 Explicit Information Flow**

As a second example for regular language restricted reachability analysis, I want to consider a heuristic property of slices that I call *explicit information flow*. This property is based on the observation that static helper analyses may cause the insertion of spurious control dependencies. Such control dependencies can easily lead to program dependence graphs in which everything that happens after some critical statement is control-dependent on this statement, solely because of the fact that this statement may fail due to an exception that could not be ruled out.

As an example, consider Figure 5.5b. The array access does not fail and hence the program should be secure. But with a static analysis that is too imprecise to prove this, the resulting PDG contains a control dependency between the array access and the print statement. Moreover, whether the


**Figure 5.5:** Analysis precision and control dependencies – assume that *arr* ≠ *null* and 0 ≤ *i* < *arr*.*length*

array access happens depends on the secret value. Hence, in this PDG, line 1 is connected to 7.

In contrast, the PDG of Figure 5.5a contains a path from the secret source to the public sink, even if a static analysis could prove that the array access never fails. The path has the property that it *ends in a data dependency*, where as the Figure 5.5b does not.

Explicit information flow only considers PDG paths that end in a data dependency. This way, PDG paths such as the one in Figure 5.5b are ignored, whereas the path in Figure 5.5a is covered.

We also can ignore the path in Figure 5.5b by simply ignoring *all* control dependencies (like we did in one variant of the SHRIFT approach in section 4.7), but then we would also ignore the path in Figure 5.5a that is in a sense more direct.

I consider explicit information flow as an example for a helper analysis for the further analysis of a PDG's valid paths. The setting that I have in mind is that a PDG-based information flow control tool like Joana fails to verify a given information flow requirement and the analyst tries to find out why. If Joana succeeds to verify the requirement with restriction to explicit information flow, then this may indicate that Joana's exception analysis is too imprecise.

In the following, I formally describe explicit information flow as a data-flow analysis framework instance.

Given a program dependence graph *G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ), I decompose *E* into *E* = *DD* ∪*CD*, where *DD* is the set of all data dependencies and *CD* is the set of all control dependencies. Then, *the explicit information*

**Figure 5.6:** A finite automaton for explicit information flow analysis – *P*(*e*) is short for {*e* ∈ *E* | *P*(*e*)}.

*flow (forward) slice of s* ∈ *N* consists of all nodes *t* such that at least one valid path from *s* to *t* ends in a data dependency:

$$EIF(s) \stackrel{def}{=} \{ t \in N \mid VP(s, t) \cap E^\*DD^+ \neq \emptyset \}.$$

As *E* <sup>∗</sup>*DD*<sup>+</sup> is a regular language, *EIF* is an instance of (5.37):

$$EIF(s) = FS(s, E^\*DD^+).$$

Figure 5.6 shows a finite automaton that recognizes *E* <sup>∗</sup>*DD*+.

# **5.4.6 Hammer's Approach to IFC**

In this section, I want to take a closer look at Hammer's PDG-based approach to IFC [86].

Hammer uses a finite lattice (*L*, ≤), where the levels *l* ∈ *L* are confidentiality levels; *x* ≤ *y* means that *y* is as confidential as or more confidential than *x*. Partial functions *P* and *R* are used to annotate sources and sinks. Sources have a *provided level P*(*n*) whereas sinks have *required level R*(*n*). For now, we assume that *dom*(*P*) ∩ *dom*(*R*) = ∅.

*P* and *R* are expanded to all nodes by defining

$$(\text{5.48}) \qquad \qquad P'(n) = \begin{cases} P(n) & \text{if } n \in dom(P) \\ \bot & \text{otherwise} \end{cases}$$

$$(\text{5.49}) \qquad \qquad R'(n) = \begin{cases} R(n) & \text{if } n \in dom(R) \\ \top & \text{otherwise} \end{cases} \dots$$

Intuitively, information leaving *n* has confidentiality level at least *P*(*n*) and information entering *n* has at most confidentiality level *R*(*n*). The goal is to propagate the confidentiality levels along the paths of *G*, or, respectively, whether such a propagation is possible without contradiction.

The following monotone constraint system (compare [86, (4.7) on p. 102]) describes a function *S* : *N* → *L* that *propagates* confidentiality levels from *dom*(*P*). Solutions of Constraint System 5.1 can be considered to conservatively describe the information flows of the given program with respect to the confidentiality specification given by *P*.

# **Constraint System 5.1.**

$$\begin{array}{ccc} \texttt{x} \in dom(P) & \quad & y \rightarrow\_G \texttt{x} \\ \hline S(\texttt{x}) \geq S(y) \sqcup P(\texttt{x}) & \quad & \quad \underline{\texttt{x} \notin dom(P)} \quad & \quad \underline{y} \to\_G \texttt{x} \\ \hline \end{array}$$

To be able to assess whether the given program is secure with respect to the complete security specification (*P*, *R*), Hammer introduces the notion of *maintaining confidentiality*. For this, he states a further set of constraints ([86, (4.8) on p. 102]):

$$\text{(5.50)}\qquad\qquad\forall\mathfrak{x}\in dom(\mathbb{R}).\,\,S(\mathfrak{x})\le\mathbb{R}(\mathfrak{x})...$$

For *G* to maintain confidentiality, Hammer requires that Constraint System 5.1 and (5.50) are simultaneously satisfied ( [86, Definition 4.1]). Note that the joint constraint system consisting of Constraint System 5.1 and Equation 5.50 is *not* monotone: Constraint System 5.1 and (5.50) use ≤ in different directions.

However, whether *G* maintains confidentiality can be checked by looking at the least solution *S* of the monotone constraint system Constraint System 5.1.

### **Lemma 5.47.** *The following two statements are equivalent:*


*Proof.* Since *S* is a solution of Constraint System 5.1, it is clear that *G* maintains confidentiality if *S* also satisfies (5.50).

For the converse direction, assume that *G* maintains confidentiality. Then there is a function *S* : *N* → *L* that satisfies both (5.1) and (5.50). Now, consider the least solution S of (5.1). We have to show that *S* satisfies (5.50). So let *x* ∈ *dom*(*R*). Then *S*(*x*) ≤ *R*(*x*). Moreover, since *S* is a solution of (5.1) and *S* is the least solution of (5.1), we have *S*(*x*) ≤ *S*(*x*). Together, it follows that *<sup>S</sup>*(*x*) <sup>≤</sup> *<sup>R</sup>*(*x*), as desired. □

Hammer does not state this directly, but suggests that he aims for the least solution of Constraint System 5.1 by stating that "Equation (4.11) is satisfied in the most precise way, and hence the risk that equation (4.8) is violated is minimized, if the inequality for *S* turns into equality"<sup>30</sup> [86, p. 103] and later referring to the solution as least fixed-point (e.g. [86, Theorem 4.2]).

The simple approach showed so far is conservative but not precise: In fact, Constraint System 5.1 propagates provided levels along arbitrary paths and not only along valid paths.

Hammer describes a slicing-based check that aims to solve this precision problem. A PDG *G* is said to *ensure non-interference* with respect to *R* and *P*, if

$$\forall n \in \mathcal{N}. \quad \bigsqcup\_{m \in BS(n)} P'(m) \le R'(n)$$

This amounts to computing

$$\text{(5.51)}\qquad\qquad S(n) = \bigsqcup\_{m \in BS(n)} P'(m)$$

and checking that ∀*n*. *S*(*n*) ≤ *R* ′ (*n*).

As Hammer also notices, (5.51) can be computed using data-flow analysis. In my notation, an appropriate data-flow analysis instance is given by the following ingredients.

<sup>30</sup>Equation (4.11) is a simplified version of Equation (4.7) in [86] or Constraint System 5.1, respectively.


3. ρ : *e* ↦→ *f<sup>e</sup>* is defined by

$$\begin{array}{c} \text{(5.52)} \end{array} \quad \begin{array}{c} f\_{\ell}(\mathbf{x}) \stackrel{def}{=} \mathbf{x} \sqcup P'(src(e)) \sqcup P'(tgt(e)) \end{array}$$

Then

$$\text{(5.53)}\qquad\qquad f\_{\pi}(\mathbf{x}) = \mathbf{x} \sqcup \bigsqcup\_{n \in \text{nodes}(\pi)} P'(n)$$

and

$$(\text{5.54})\qquad\qquad\qquad\qquad\text{MOVP}(s,t)(\mathbf{x})=\mathbf{x}\sqcup\sum\_{\pi\inVP(s,t)}\bigsqcup\_{n\in n\text{nodes}(\pi)}P'(n).\qquad$$

Hammer provides an algorithm [86, Algorithm 7] to compute (5.51). Specifically, this algorithm generates an appropriate subset of Constraint System 5.1. (5.51) then turns out to be a solution of that system and can be checked against an appropriate set of *R* constraints that is also generated by the algorithm [86, Theorem 4.2].

Now I show how (5.51) can be extracted from (5.54). For this, I note that

$$\text{(5.55)}\qquad BS(m) = \{m\} \cup \bigcup\_{n \in \mathcal{N}} \bigcup\_{\pi \in VP(n,m)} nodes(\pi).$$

This enables me to re-write (5.51) as follows.

$$\begin{split} S(n) &= \bigsqcup\_{m \in BS(n)} P'(m) \\ &= P(n) \sqcup \bigsqcup\_{l \in N} \bigsqcup\_{\pi \in VP(l,n)} \bigsqcup\_{m \in nodes(\pi)} P'(m) \qquad \text{(5.55)} \end{split} \tag{5.55}$$

222

$$\begin{aligned} &= \bigsqcup\_{l \in N} \bigsqcup\_{\pi \in VP(l,n)} \left( P(n) \sqcup \bigsqcup\_{m \in \text{nodes}(\pi)} P'(m) \right) & \quad \{\text{re-writing} \} \\ &= \bigsqcup\_{l \in N} \bigsqcup\_{\pi \in VP(l,n)} f\_{\pi}(P'(n)) & \quad \{\text{(5.53)} \} \\ &= \left( \bigsqcup\_{l \in N} \bigsqcup\_{\pi \in VP(l,n)} f\_{\pi} \right) (P'(n)) & \quad \{\text{re-writing} \} \\ &= \left( \bigsqcup\_{l \in N} MOVP(l,n) \right) (P'(n)). \end{aligned}$$

Note that (5.51) does *not* define a solution to Constraint System 5.1, as Constraint System 5.1 is too simplistic, hence demands too much. Hammer proposes an algorithm based on two-phase slicing that generates a subsystem of Constraint System 5.1 for which (5.51) indeed is a solution and which still adequately describes the property of maintaining confidentiality.

# **5.4.6.1 IFC With Declassification**

Hammer also supports a form of *where-declassification*, i.e. the specification of points in the program where confidential information is transformed into benign information which is allowed to be made available to lower observers. For this, he introduces a set *D* ⊆ *N* of *declassification nodes*. If information enters a declassification node *d* ∈ *D* with a level of at most *r*, then *d* downgrades it (e.g. by sanitizing or removing classified information) to level *p* ≤ *r*.

A declassification node has both a required and a provided level (cf. [86, (4.18)]):

(5.56) *x* ∈ *D* =⇒ (*x* ∈ *dom*(*P*) ∩ *dom*(*R*)) ∧ *R*(*x*) ≥ *P*(*x*)

Hammer then adapts Constraint System 5.1 and (5.50) to also support declassification nodes (cf. [86, (4.19)]). This leads to the following constraint system.

### **Constraint System 5.2.**

$$\begin{array}{ccc} \underline{x} \in D & \quad y \to\_G x\\ \underline{S(x) \ge P(x)} & \quad \qquad \qquad \frac{\underline{x} \notin D \qquad \quad y \to\_G x}{\underline{S(x) \ge S(y) \sqcup P'(x)}} \end{array}$$

The check (5.50) is adapted accordingly (cf. [86, (4.20)]):

$$\text{(5.57)}\qquad \forall \mathbf{x} \in dom(\mathcal{R}) \; \bigvee D. \; S(\mathbf{x}) \leq \mathcal{R}(\mathbf{x}) \land \forall \mathbf{x} \in D. \; \bigsqcup\_{\mathbf{y} \to \mathbf{y}} S(\mathbf{y}) \leq \mathcal{R}(\mathbf{x}).$$

The approach is now just like the in the case without declassification: The given program is considered secure if and only if the least solution *S* of Constraint System 5.2 satisfies Equation 5.57. Hammer's Algorithm 7 is also able to handle declassification nodes and generates appropriate subsets of Constraint System 5.2 and (5.57) that can be used to compute and check a solution (cf. [86, Theorem 4.6]).

In the following, I show how (5.57) can be expressed using a data-flow analysis instance.

The complete lattices *L* and *F* stay the same as in the non-declassification case, only the transfer functions need to be adapted. Note that Hammer does not give a solution representation for the declassification case like (5.51), so that I have to extract the transfer function from Constraint System 5.2. The definition of *fe* particularly accounts for the case that both *src*(*e*) and *tgt*(*e*) may be declassification nodes.

$$(\text{5.58})\qquad f\_{\varepsilon} \stackrel{def}{=} \begin{cases} \lambda \ge \iota \sqcup P'(src(e)) \sqcup P'(tgt(e)) & \text{if } src(e) \notin D \wedge tgt(e) \notin D \\ \lambda \ge P(tgt(e)) & \text{if } tgt(e) \in D \\ \lambda \ge P(src(e)) & \text{if } src(e) \in D \wedge tgt(e) \notin D \end{cases}$$

Next, I want to consider *f*π more concretely: In the case that π contains no declassification nodes, it is easy to see that

$$\text{(5.59)}\qquad\qquad f\_{\pi} = \lambda \mathfrak{x}. \mathfrak{x} \sqcup \bigsqcup\_{n \in \text{nodes}(\pi)} P'(n).$$

224

Hence, for *D* = ∅, (5.59) is compatible with the declassification-less instance defined in (5.52).

To describe *f*π if π contains declassification nodes, I need some additional notations. First of all, I call an edge *e* such that *src*(*e*) ∈ *D* or *tgt*(*e*) ∈ *D* <sup>a</sup> *declassification edge*. For <sup>π</sup> <sup>∈</sup> *PathsG*(*s*,*t*) such that *nodes*(π) <sup>∩</sup> *<sup>D</sup>* <sup>≠</sup> <sup>∅</sup>, I define *lastD*(π) ∈ *range*(π) as the greatest *i* such that π *i* is a declassification edge and *q*(π) *de f* = π ≥*lastD*(π) , the suffix of π that starts with the last declassification edge occurring in π, and *p*(π) *de f* = π <sup>&</sup>lt;*lastD*(π) as the corresponding prefix.

With the help of *q*(π), I can show that *f*<sup>π</sup> discards all provided levels up to the last declassification node in *q*(π 0 ).

**Theorem 5.48.** *If nodes*(π) <sup>∩</sup> *<sup>D</sup>* <sup>≠</sup> <sup>∅</sup>*, f*<sup>π</sup> *can be characterized as follows:*

$$(\text{5.60}) \qquad \begin{array}{c} \text{src}(q(\pi)^0) \in D \\ \land \text{tgt}(q(\pi)^0) \notin D \end{array} \implies f\_{\pi} = \lambda \text{x.} \quad \bigsqcup\_{n \in \text{nodes}(\pi)} P'(n)$$

$$(\text{5.61})\qquad \operatorname{tgt}(q(\pi)^0) \in D \implies f\_{\pi} = \lambda \text{x.} \bigsqcup\_{e \in \text{edges}(\pi)} P'(\text{tgt}(e))$$

*Proof.* First, we observe that it is enough to show (5.60) and (5.61) for *fq*(π) :

$$(\text{5.62}) \qquad \begin{array}{c} \text{src}(\text{q}(\pi)^0 < ) \in D \\ \land \text{tgt}(\text{q}(\pi)^0) \notin D \end{array} \implies f\_{\text{q}(\pi)} = \lambda \text{x.} \quad \bigsqcup\_{\pi \in \text{nodes}(\text{q}(\pi))} P'(n)$$

$$(\text{5.63})\qquad \operatorname{tgt}(q(\pi)^0) \in D \implies f\_{q(\pi)} = \lambda \ge \bigsqcup\_{e \in \text{edges}(q(\pi))} P'(\text{tgt}(e))$$

The reason is that the right-hand sides of (5.62) and (5.63) are constant functions, i.e. if (5.62) and (5.63) hold, then we have

$$\text{(5.64)}\qquad\qquad\forall \mathfrak{x}, \mathcal{Y} \in \mathcal{L}. \ f\_{q(\pi)}(\mathfrak{x}) = f\_{q(\pi)}(\mathfrak{y})$$

Now assume that (5.62) and (5.63) are true and let *x* ∈ *L*. Then we have

$$\begin{aligned} f\_{\pi}(\mathbf{x}) &= f\_{p(\pi) \cdot q(\pi)}(\mathbf{x}) & \{ \text{ definition of } p(\pi), q(\pi) \} \\ &= f\_{q(\pi)}(f\_{p(\pi)}(\mathbf{x})) & \{ \text{ properties of } f\_{\mathbf{\star}} \} \end{aligned}$$

$$=f\_{q(\pi)}(\mathbf{x}).\qquad\qquad\qquad\{\text{(5.64)}\}$$

It remains to show (5.62) and (5.63). For this, we consider *q*(π) more closely: It is not empty and consists of exactly one declassification edge at the beginning, i.e. *q*(π) 0 is a declassification edge and *q*(π) >0 contains no declassification edges and hence no declassification nodes. Hence, for every *x* ∈ *L*, we have

$$\begin{aligned} f\_{q(\pi)}(\mathbf{x}) &= f\_{q(\pi)^0 \cdot q(\pi)^{>0}}(\mathbf{x}) & \{ \text{ definition } \} \\ &= f\_{q(\pi)^{>0}}(f\_{q(\pi)^0}(\mathbf{x})) & \{ \text{ properties of } f\_{\mathbf{x}} \} \\ &= f\_{q(\pi)^0}(\mathbf{x}) \sqcup \bigsqcup\_{n \in \text{nodes}(q(\pi)^{>0})} P'(n) & \{ \text{(5.59)} \} \end{aligned}$$

Now, (5.62) and (5.63) follow from the different cases in the definition (5.58) of *f q*(π) <sup>0</sup> . Because *q*(π) 0 is a declassification edge, we do not need to consider first case in (5.58). For the remaining two cases, we argue as follows:

$$\begin{aligned} \bullet \text{ If } src(q(\pi)^0) &\in D \land \text{tgt}(q(\pi)^0) \notin D, \text{ then we have} \\ f\_{q(\pi)^0}(x) &\sqcup \bigsqcup\_{n \in \text{nodes}(q(\pi)^{>0})} P'(n) \\ &= P'(src(q(\pi)^0)) \sqcup \bigsqcup\_{n \in \text{nodes}(q(\pi)^{>0})} P'(n) \quad \{\text{(5.58)}\} \\ &= \bigsqcup\_{n \in \text{nodes}(q(\pi))} P'(n) . \quad \{\text{definition of nodes}\} \end{aligned}$$

• If *tgt*(*q*(π)) ∈ *D*, then we have

$$f\_{q(\pi)^0}(\mathbf{x}) \sqcup \bigsqcup\_{n \in \text{nodes}(q(\pi)^{>0})} P'(n)$$

$$= P'(t \mathfrak{g} t(q(\pi)^0)) \sqcup \bigsqcup\_{n \in \text{nodes}(q(\pi)^{>0})} P'(n) \qquad \text{ (definition )}$$

$$= \bigsqcup\_{e \in \text{edges}(q(\pi))} P'(t \mathfrak{g} t(e)) . \tag{1 \text{ } (\star) \text{ } }$$

To conclude the proof, we need to justify the last step (⋆). Its validity can easily be seen in the case that *q*(π) <sup>&</sup>gt;<sup>0</sup> = ϵ. For *q*(π) <sup>&</sup>gt;<sup>0</sup> ≠ ϵ, we note

$$\text{nodes}(q(\pi)^{>0}) = \{ tgt(e) \mid e \in edges(q(\pi)) \}.$$

Moreover, since *q*(π) is a path, we have

$$\operatorname{tgt}(q(\pi)^0) = \operatorname{src}(\left(q(\pi)^{>0}\right)^0) \in \operatorname{nodes}(q(\pi)^{>0}).$$

Together, this implies

$$\text{nodes}(q(\pi)^{>0}) = \text{tgt}(q(\pi)^0) \cup \{\text{tgt}(e) \mid e \in \text{edges}(q(\pi))\}.$$


# **5.4.7 Least Distances**

The problem of computing least distances can also be expressed as the instance of a data-flow framework. Computing least distances can be considered a flexible tool to help in PDG-based IFC analysis. By computing least distances, useful information about the valid paths of a given interprocedural graph can be generated. Such information could be used to classify the set of valid paths between two nodes, e.g. with respect to the kinds of dependencies. For example, a least distances analysis could be used to compute the minimum number of control dependencies between two nodes *s* and *t*. If this number is 0, then *s* and *t* are connected via a chain of data dependencies and the higher the number, the more control dependencies are required to transmit information between *s* and *t*.

In the following, I describe the data-flow framework for least distances in the simplest case that every edge is given a weight of 1.

Consider the set **N** ∪ {∞}, partially ordered by ⊑∞, which extends the natural ordering ≥ on **N** such that ∞ is the least element with respect to ⊑∞. Note that, in comparison to ≤, ⊑<sup>∞</sup> is "upside down".

With respect to ⊑∞, **N** ∪ {∞} is a complete lattice: Since every non-empty subset *A* ⊆ **N** has a least element with respect to ≤, the least upper bound has the following characterization:

$$\bigsqcup A = \begin{cases} \infty & \text{if } A = \emptyset \\ \min A & \text{if } A \neq \emptyset \land \infty \notin A \\ \min \left( A - \{\infty\} \right) & \text{if } \infty \in A \end{cases}$$

The space *F* of transfer functions can be chosen as

$$F := \{ \lambda \mathfrak{x}, \mathfrak{x} + d \mid d \in \mathbb{N}\_{\infty} \}$$

As can easily be seen, all *f* ∈ *F* are monotone, and *F* is closed under arbitrary joins and function composition.

In the easiest setting, we give each edge the distance 1:

$$
\rho(e)(\mathfrak{x}) = \mathfrak{x} + \mathbf{1}.
$$

Now let *G* = (*N*, *Eintra*, *Ecall*, *Eret*, *P*, Φ) be an interprocedural graph. For a path π ∈ *Paths*(*G*), *f*<sup>π</sup> is then the function that adds the length of π to its argument. Hence, if *VP*(*s*,*t*) <sup>≠</sup> <sup>∅</sup>, *MOVP*(*s*,*t*) adds the minimal length of a path between *s* and *t* to its argument. Generally, *MOVP*(*s*, *t*) ∈ *F*<sup>⊠</sup> can be characterized as follows31:

$$MOVP(s,t)(\mathbf{x}) = \begin{cases} \mathbf{x} + \min\{|\pi| \mid \pi \in VP(s,t)\} & \text{if } VP(s,t) \neq \emptyset \\ \boxtimes & \text{otherwise} \end{cases}$$

Note that although there are large similarities, the least-distances-alongvalid-paths problem cannot be cast as an instance of the traditional shortest-paths problem on directed graphs for which there are established approaches [56]. Knuth [108] considered a version of the leastdistances-problem that can be considered similar to our setting. Traditional approaches compute the length of a shortest *arbitrary* path in the given graph, while we are interested in the length of a shortest *valid* path. A

<sup>31</sup>As I pointed out in section 5.3, I always adjoin a the set *F* of transfer functions with an additional element ⊠. For this particular instance, this is technically not necessary, because <sup>λ</sup>*x*.<sup>∞</sup> has all the properties that I assume about <sup>⊠</sup>.

**Figure 5.7:** A small example with Φ = {(*e*1,*e*2),(*e*3,*e*4)} which shows that the shortest valid path may differ from the shortest arbitrary path. (b) shows the shortest arbitrary path, which has length 3 and is invalid, (c) shows the shortest valid path which has length 7.

simple example of this restriction is shown in Figure 5.7. In this example, the shortest arbitrary path between *s* and *t* does not respect the correspondence relation ϕ and therefore is invalid. The shortest valid path is significantly longer because both of its sections that enter *p* ′ have to leave it through the corresponding return edge.

It is also worth noting that distance calculations on PDGs have been considered before, e.g. by Krinke [110]. Consider a PDG with summary edges. If every edge, including summary edges, is annotated with a weight, a two-phase approach can be used to compute least weights along paths *including summary edges*. In his description, Krinke [110] does not specify the weight that he assigns to summary edges, but I suspect that he uses a weight of 1. Hence, he consequently under-estimates the length of samelevel paths and, in the terminology of the data-flow framework instance discussed in this section, computes an *over-approximation*<sup>32</sup> of *MOVP*. Hence, Krinke's approach is correct and more precise than computing least distances w.r.t. arbitrary paths, but it is imprecise w.r.t. *MOVP*.

The generalized algorithms that I present in chapter 7 can be used to (a) annotate each summary edge between to nodes *n*<sup>0</sup> and *n*<sup>1</sup> with the *length of*

<sup>32</sup>Remember that the partial order is upside down.

*a shortest same-level path between n*<sup>0</sup> *and n*<sup>1</sup> and then to (b) *precisely* compute the least distances along valid paths.

*People asking questions, lost in confusion. Well, I tell them there's no problem, only solutions.* <sup>J</sup>ohn <sup>L</sup>ennon **6**

# **Two Approaches to Abstract Data-Flow Analysis on Interprocedural Graphs**

In this chapter, I describe versions of the *functional approach* and the *callstring approach* to interprocedural data-flow analysis for my generalized data-flow frameworks. For classical data-flow frameworks, I already described these two approaches in chapter 3. The two approaches use different ideas to tackle a fundamental problem that arises in interprocedural data-flow analysis, namely that arbitrary paths in interprocedural graphs do not necessarily reflect valid calling and returning behavior of actual program executions. In chapter 3, I already described this problem: An arbitrary path may contain returns that do not return to the call site from which the call started.

The idea of the functional approach is to first solve a helper problem whose solution describes the data-flows along same-level paths. In a second step, this helper solution is then used to describe the data-flows along realizable paths.

In contrast, the call-string approach simulates the call stack usage of paths in order to rule out paths with invalid calling and returning behavior.

Sharir and Pnueli [154] showed that both approaches lead to correct approximations of MO*DESC* in interprocedural control-flow graphs and, for distributive frameworks, can compute MO*DESC* exactly. However, the unrestricted call-string approach uses stacks of unlimited height and therefore does not lead to effectively solvable constraint systems. This is why one usually uses *k*-bounded stacks, whose height is not greater than *k* items, and which lead to a correct approximation.

In this chapter, I generalize these results. I describe versions of the functional approach and the call string approach that are based on *interprocedural graphs*, the general graph model that I introduced in chapter 5. Interprocedural graphs cover both interprocedural control-flow graphs and program dependence graphs. Furthermore, I show that both the functional approach and the unrestricted call-string approach enjoy the same properties as in the classical case. Moreover, for the call-string approach, I introduce *stack abstractions*, a general technique that makes it possible to obtain correct approximations to the call-string approach. I demonstrate the applicability of stack abstractions by using them to show that my version of the call-string approach is correct for *k*-bounded stacks. The following sections are structured as follows. In section 6.1, I make some preparations and fixtures. After that, section 6.2 considers the generalized functional approach. Lastly, I describe my version of the call-string approach in section 6.3.

# **6.1 Preliminaries**

In section 6.2 and section 6.3, I will specify several constraint systems and examine the properties of their solutions. This section makes several necessary preparations. Subsection 6.1.1 specifies the expressions that appear in the constraint systems of this chapter and makes clear their semantics. Subsequently, subsection 6.1.1 introduces *correctness* and *precision* as quality criteria of solutions to constraint systems with respect to a given MOP function.

For this chapter, I fix a data-flow analysis framework instance F = (*G*, *L*, *F*⊠, ρ) in the sense of section 5.3.

# **6.1.1 Syntax and Semantics**

First, I have to make clear my assumptions about the expressions and their interpretations from which the constraint systems in this and the next chapter are formed. The set of variables *X* will always become clear from context and is left unspecified for the moment. Moreover, I assume that the set F of function symbols contains

• a unary symbol *f* for every *f* ∈ *F*<sup>⊠</sup> and

• a binary symbol ◦.

In order to provide the semantics functional ⟦·⟧ : *Expr*(<sup>F</sup> , *<sup>X</sup>*) <sup>→</sup> *<sup>F</sup>*⊠, I need to specify the interpretation function α that assigns every function symbol a function on with the appropriate number of inputs from *F*⊠. My obvious choice for α is

$$\alpha(\underline{f}) = f \text{ for all } f \in F\_{\mathbb{B}}$$

$$\alpha(\underline{\mathbf{e}})(f, \underline{g}) = \begin{cases} f \circ \underline{g} & \text{if } f, \underline{g} \in F \\ \underline{\mathbf{a}} & \text{if } f = \underline{\mathbf{a}} \vee \underline{g} = \underline{\mathbf{a}} \end{cases}$$

# **6.1.2 Correctness and Precision of Solutions**

The constraint systems that I will present in the following sections are supposed to approximate MOP for respective path sets P. Definition 6.1 provides the main criteria for assessing the quality of a given function relative to MOP.

**Definition 6.1.** *Let* P ⊆ *Paths*(*G*) *be a set of paths, E* ⊆ *N* × *N a set of node pairs and A* : *N* × *N* → *F*<sup>⊠</sup> *be a function. Then A is called*

*1.* (P, *E*)-domain-correct *if it yields a defined value for every* (*s*,*t*) ∈ *E that is connected by a path in* P*:*

$$\forall \mathbf{s}, t \in \mathbb{N}. \,(\mathbf{s}, t) \in E \land \text{Paths}\_{\mathbf{G}}(\mathbf{s}, t) \cap \mathcal{P} \neq \emptyset \implies A(\mathbf{s}, t) \neq \mathbf{a}.$$

*2.* (P, *E*)-domain-precise *if it does not yield a defined value for* (*s*, *t*) ∈ *E that is not connected by a path in* P*:*

$$\forall \mathbf{s}, t \in \mathcal{N}. \,(\mathbf{s}, t) \in \mathcal{E} \land A(\mathbf{s}, t) \neq \mathbf{z} \implies \text{Paths}\_G(\mathbf{s}, t) \cap \mathcal{P} \neq \emptyset.$$

*3.* (P, *E*)-correct *if it over-approximates* MOP *on E:*

$$\forall s, t \in \mathcal{N}. \left(s, t\right) \in E \implies A(s, t) \ge \bigsqcup\_{\pi \in \text{Paths}\_G(s, t) \cap \mathcal{P}} f\_{\pi}.$$

*4.* (P, *E*)-precise *if it does not exceed* MOP *on E:*

$$\forall s, t \in \mathcal{N}. \left(s, t\right) \in E \implies A(s, t) \le \bigsqcup\_{\pi \in \text{Paths}\_G(s, t) \cap \mathcal{P}} f\_{\pi}.$$

*I call A just* P-correct/precise*, if it has the corresponding property with respect to* (P, *N* × *N*)*.*

Definition 6.1 is more general than needed in chapter 6: Specifically, Definition 6.1 also takes solutions into account that are ≠ ⊠ only for a part of *N* × *N*. I will need this in chapter 7, which is concerned with computing partial solutions. As my solutions not only give analysis values but also communicate whether there is a value or not, an additional item of comparison is the domain. Hence, Definition 6.1 also incorporates notions that enable to compare the domains of solutions to the domains of objective functions.

It is easy to see that P-correctness of a given function *A* can be verified by simply showing that every *f*π is incorporated in *A*. This will be my main tool to prove correctness, so I note it here.

**Remark 6.2.** *A function A* : *N* × *N* → *F*<sup>⊠</sup> *is* P*-correct if and only if*

∀*s*, *t* ∈ *N*. ∀π ∈ *PathsG*(*s*, *t*). (*s*, *t*) ∈ *E* ∧ π ∈ P =⇒ *f*<sup>π</sup> ≤ *A*(*s*, *t*).

In order to show precision, I will use the argument that is formalized by Remark 6.3.

**Remark 6.3.** *Let*C*be a set of constraints over variables N* ×*N and* P ⊆ *Paths*(*G*)*. Then l f p*(*F*C) *is* P*-precise if* MOP *is a solution of* C*.*

# **6.2 The Functional Approach**

This section describes the functional approach to solve a given interprocedural data-flow framework instance.

The pattern of the functional approach is analogous to the successive construction of the same-level, the ascending, the descending and finally the valid paths that we saw in chapter 5.

Each step considers a set P of paths (where P = *SL*, *ASC*, *DESC*, *VP* in this order) and uses the results from earlier steps to specify a monotone constraint system CP, whose solutions

$$\chi\_{\mathcal{P}} : N \times N \to F\_{\mathbf{B}}$$

are P-correct, i.e. are over-approximations of MOP. For simplicity, I say that both C<sup>P</sup> and its solutions *correctly describe the data transfer along paths from* P.

# **6.2.1 Constraint Systems**

The constraint systems themselves are constructed in a fashion that is very similar to the inductive definitions of the corresponding path sets. First, I introduce a constraint system to describe the data transfer along same-level paths.

### **Constraint System 6.1.**

$$\chi\_{\rm SL}: N \times N \to F\_{\rm E}$$

*is a* same-level-solution *if it satisfies all constraints from the following system:*

$$\left(\text{sL-son-(i)}\right)\overline{\left(\text{X}\_{SL}(s,s)\right)} \ge id$$

$$\left(\text{sz-sor-(u)}\right)\frac{t' \xrightarrow{\varepsilon} t \qquad e \in E\_{\text{intra}}}{\text{X}\_{SL}(\text{s}, t) \ge f\_{\varepsilon} \circ \text{X}\_{SL}(\text{s}, t')}$$

$$(\text{sr-sor-(nn)})\ \frac{n\stackrel{\varepsilon\_{\text{call}}}{\rightarrow}n\_0}{X\_{\text{SL}}(\text{s}\_\prime t) \ge f\_{\text{\"}e\_{\text{rt}}} \circ X\_{\text{SL}}(n\_0, n\_1) \circ f\_{\text{\"}e\_{\text{call}}} \circ X\_{\text{SL}}(\text{s}\_\prime n)}$$

For the sake of presentation, my notation of Constraint System 6.1 and later constraint systems is somewhat sloppy. For one, I use the actual functions from *F* and function composition where I actually mean their corresponding function symbols. Also note that the *XSL*(\_, \_) actually denote corresponding variables. Thirdly, I want to point out that the clauses of Constraint System 6.1 – and most other constraint systems shown in this thesis – are to be understood as rules that specify when to include a constraint to the system. This is why in the following, I will refer to these clauses as *rules*.

All in all, sl-sol-(iii) reads:

$$\begin{aligned} &\text{For all } n \stackrel{\varepsilon\_{\text{call}}}{\rightarrow} n\_0 \text{ and } n\_1 \stackrel{\varepsilon\_{\text{ret}}}{\rightarrow} t \text{ with } (e\_{\text{call}}, e\_{\text{ret}}) \in \Phi, \text{ the system contains} \\ &\text{the constraint } \underline{X\_{SL}}(s, t) \ge \underline{f\_{\text{ret}}} \stackrel{\circ}{\rightarrow} \underline{X\_{SL}}(s, n) \underline{\rhd}\_{\underline{\text{call}}} \stackrel{\circ}{\rightarrow} \underline{X\_{SL}}(s, n). \end{aligned}$$

In the following, I want explain the intuition behind Constraint System 6.1. It is supposed to specify a function *XSL* that correctly describes the data transfer along same-level paths. More concretely, *XSL*(*s*,*t*) shall correctly approximate MO*SL*(*s*, *t*), i.e. incorporate all path functions *f*<sup>π</sup> along samelevel paths π from *s* to *t*. Hence, it is natural to construct Constraint System 6.1 in correspondence with the inductive definition of *SL*.

To illustrate this, I consider sl-sol-(iii) more closely. Let (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup> with *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*<sup>0</sup> and *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>*. In order to describe the data transfer along same-level paths from *s* to *t*, we have to ensure that

$$\text{(6.1)}\qquad\qquad X\_{SL}(s,t) \ge f\_{\varepsilon\_{\text{ret}}} \circ f\_{\pi\prime\prime} \circ f\_{\varepsilon\_{\text{call}}} \circ f\_{\pi\prime\prime}$$

for all π ′ ∈ *SL*(*s*, *n*) and all π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ).

Suppose that *XSL*(*n*0, *n*<sup>1</sup> ) describes the data transfer along same-level paths from *n*<sup>0</sup> to *n*<sup>1</sup> and that *XSL*(*s*, *n*) describes the data transfer along same-level paths from *s* to *n*. Then (6.1) can be satisfied for all π ′ ∈ *SL*(*s*, *n*) and all π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ) if

$$\text{(6.2)}\qquad\qquad\text{X}\_{SL}(\text{s}\_{\prime}t)\geq f\_{\text{eff}}\circ\text{X}\_{SL}(n\_{0\prime}n\_{1})\odot f\_{\text{eff}}\circ\text{X}\_{SL}(\text{s}\_{\prime}n).$$

Rule sl-sol-(iii) specifies that Constraint System 6.1 contains a constraint of the form (6.2) for all (*ecall*,*eret*) ∈ Φ with *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*<sup>0</sup> and *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>call* <sup>→</sup> *<sup>t</sup>*. The three rules together cover all possibilities to construct a same-level path. Next I use solutions *XSL* of Constraint System 6.1 to describe the dataflow along ascending paths. The construction principle behind these two

constraint systems is the same as for Constraint System 6.1 and their

**Constraint System 6.2.** *Let XSL* : *N* × *N* → *F*<sup>⊠</sup> *be a function. Then*

*XASC* : *N* × *N* → *F*<sup>⊠</sup>

*is an ascending-path-solution (relative to XSL) if it satisfies all constraints from the following system:*

$$\left(\stackrel{\cdot}{\text{asc-son-(i)}}\stackrel{\cdot}{\text{X}\_{\text{ASC}}(s,s)\ge id}\right)$$

intuition can be explained similarly.

$$\left(\text{asc-son-(u)}\right)\frac{t' \xrightarrow{\varepsilon} t \qquad e \in E\_{\text{intra}} \cup E\_{\text{ret}}}{X\_{\text{ASC}}(s\_\prime t) \ge f\_\varepsilon \circ X\_{\text{ASC}}(s\_\prime t')}$$

$$(\text{asc-sor-(un)})\frac{n\stackrel{\mathcal{e}\_{\text{call}}}{\rightarrow}n\_0}{X\_{\text{ASC}}(s,t)\geq f\_{\text{ret}}\,\circ X\_{\text{SL}}(n\_0,n\_1)\,\circ\,\boxtimes}$$

At this point, I want to highlight yet another subtlety in Constraint System 6.2 that applies to all constraint systems in this thesis that make use of some helper functions. Unlike in Constraint System 6.1, *XSL*(*n*0, *n*<sup>1</sup> ) in the lower part of asc-sol-(iii) is *not* a variable but a constant. In particular, if *XSL*(*n*0, *n*<sup>1</sup> ) = *f* ∈ *F*, I actually mean *f* when I write *XSL*(*n*0, *n*<sup>1</sup> ) in a constraint. Furthermore, it is also worth mentioning that since the upper part of asc-sol-(iii) is *not* part of the actual constraint but rather specifies when to include the constraint, *XSL*(*n*0, *n*<sup>1</sup> ) in the upper part is used as value.

In particular, no constraint is contained in the same situation but *XSL*(*n*0, *n*<sup>1</sup> ) = ⊠. For the solutions of Constraint System 6.2, it is not important whether *XSL*(*n*0, *n*<sup>1</sup> ) ≠ ⊠ is demanded or not for a constraint to be present. However, in chapter 7 I will make statements about variables on the left-hand sides of constraints and I need this assumption in order for these statements to make sense.

The data transfer along descending paths is described by Constraint System 6.3, which is completely analogous to Constraint System 6.2.

**Constraint System 6.3.** *Let XSL* : *N* × *N* → *F*<sup>⊠</sup> *be a function. Then*

$$\chi\_{DESC}: N \times N \to F\_{\mathbf{E}}$$

*is a descending-path-solution (relative to XSL) if it satisfies all constraints from the following system:*

$$\left(\text{pues-sor-(i)}\right)\frac{\left(\text{ ${}\_{\text{}}}\right)\_{\text{}}\overline{\left(\text{$ {}\_{\text{}}}\right)\_{\text{}}}\text{ }\_{\text{}}\text{-}\left(\text{ ${}\_{\text{}}}\right)\_{\text{}}\text{-}\left(\text{$ {}\_{\text{}}}\right)\_{\text{}}$$

$$\left(\text{p.ssc.-os.-(n)}\right)\frac{t' \xrightarrow{\varepsilon} t \qquad \varepsilon \in E\_{\text{intra}} \cup E\_{\text{call}}}{X\_{\text{DES}}(s, t) \ge f\_{\varepsilon} \circ X\_{\text{DES}}(s, t')}$$

$$\left(\mathsf{p\_{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\texttt{\mathsf{\mathcal{\beta}}}}}}}}}}}}}}}}\mathsf{\texttt{\texttt{\texttt{\cdot}}}}\mathsf{\texttt{\cdot}}}\right)}\right)}\right)}\mathsf{\cdot}r\_{0}\ \mathsf{n}\_{0}\ \mathsf{n}\_{1}\ \stackrel{\mathsf{e\_{\texttt{\texttt{\texttt{\cdot}}}}}}{}{}\ \mathsf{t}}\ \mathsf{t}\ \ (\mathsf{e\_{\texttt{\texttt{\cdot}}}}\ \mathsf{e\_{\texttt{re}}})\in\mathsf{\Phi}\ \quad\mathsf{X\_{\texttt{\mathcal{E}}}}\mathsf{\cdot}\ (\mathsf{n\_{0}}\ \mathsf{n}\_{1})\ \neq\mathsf{\underline{\mathbf{\cdot}}}\ \mathsf{e}\_{\texttt{\cdot}}$$

Finally, like valid paths are constructed from the ascending and descending paths, the data transfer along valid paths are described using solutions of Constraint System 6.2 and Constraint System 6.3. This directly leads to Constraint System 6.4.

### **Constraint System 6.4.** *Let*

$$X\_{\rm{ASC}} : N \times N \to F\_{\bf{E}}$$

*and*

$$\chi\_{DESC} : N \times N \to F\_{\mathbb{E}}$$

*be two functions. Then*

$$\mathcal{X}\_{VP}: \mathcal{N} \times \mathcal{N} \to F\_{\mathbf{E}}$$

*is a valid-path-solution (relative to XASC and XDESC) if it satisfies all constraints from the following system:*

(valid-sol) *XASC*(*s*, *n*) ≠ ⊠ *XDESC*(*n*, *t*) ≠ ⊠ *XVP*(*s*, *t*) ≥ *XDESC*(*n*, *t*) ◦ *XASC*(*s*, *n*)

# **6.2.2 Correctness and Precision**

In the following, I show important properties of the solutions of the constraint systems I just introduced. First, I show that the constraint systems indeed meet their purpose, i.e. that their solutions correctly describe the data transfer along the corresponding paths. Given the construction principle behind the constraint system that I described at the beginning of this section, this should be no surprise.

Before I start my actual proofs, I state two elementary properties about *F* that I will need later at various places.

**Remark 6.4.** *Function composition is monotone on F, both in the left and in the right argument:*

$$\text{(6.3)}\qquad \forall f\_1, f\_2, f\_2, \mathbf{g\_2} \in F. f\_1 \le f\_2 \land \mathbf{g\_1} \le \mathbf{g\_2} \implies f\_1 \circ \mathbf{g\_1} \le f\_2 \circ \mathbf{g\_2}$$

*Proof.* This is an easy calculation. □

**Remark 6.5.** *The function*

$$\left| \left( \text{6.4} \right) \right| \qquad \qquad \qquad \qquad \left| \quad \big| : \mathfrak{2}^{F} \to F :$$

*is monotone in the following sense:*

$$\forall \text{6.5} \qquad \forall A, B \in \mathbb{2}^F. A \subseteq B \implies \bigsqcup A \le \bigsqcup B.$$

238

I proceed with showing that solutions of Constraint System 6.1 are *SL*correct.

**Theorem 6.6.** *Every same-level solution XSL is SL-correct.*

*Proof.* We show

$$f\_{\pi} \le X\_{SL}(s, t)$$

by induction on the definition of π ∈ *SL*(*s*, *t*).

1. For π = ϵ, we have *f*<sup>π</sup> = *id* ≤ *XSL*(*s*,*s*) by definition and constraint sl-sol-(i).

2. Let π = π ′ · *e* with π ′ ∈ *SL*(*s*,*t* ′ ) and *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>*. By induction hypothesis we know

*f*π′ ≤ *XSL*(*s*, *t* ′ (IH ) *intra*)

Hence

$$\begin{aligned} f\_{\pi} &= f\_{\varepsilon} \circ f\_{\pi'} & \qquad \text{(by definition )}\\ &\le f\_{\varepsilon} \circ X\_{SL}(s, t') & \qquad \text{(by (IH}\_{intra}), \text{(6.3) )}\\ &\le X\_{SL}(s, t) & \qquad \text{(by constraint sur-son-(n) )} \end{aligned}$$

3. Let π = π ′ · *ecall* · π ′′ · *eret* with π ′ ∈ *SL*(*s*, *n*), *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, <sup>π</sup> ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ), *n*1 *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* and (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup>. By induction hypothesis we know

$$\text{(IH}\_{\text{sl}}) \qquad \qquad f\_{\pi'} \le \text{X}\_{\text{SL}}(\text{s}, t') \land f\_{\pi''} \le \text{X}\_{\text{SL}}(n\_0, n\_1).$$

Hence

$$\begin{aligned} f\_{\pi} &= f\_{\mathcal{E}\_{\text{ret}}} \circ f\_{\pi^{\prime\prime}} \circ f\_{\mathcal{E}\_{\text{call}}} \circ f\_{\pi^{\prime}} & \quad \text{(by definition }\text{)}\\ &\leq f\_{\mathcal{E}\_{\text{ret}}} \circ X\_{SL}(n\_{0}, n\_{1}) \circ f\_{\mathcal{E}\_{\text{call}}} \circ X\_{SL}(s, n) & \quad \text{(IH}\_{\text{sl}}), \text{(6.3) }\text{)}\\ &\leq X\_{SL}(s, t) & \quad \text{(by sur-son-(un) }\text{)}. \end{aligned}$$

□

Next, I consider the ascending paths. Theorem 6.7 states that solutions of Constraint System 6.2 are *ASC*-correct, provided that Constraint System 6.2 is defined with respect to an *SL*-correct function *XSL*. According to Theorem 6.6, *XSL may* be obtained as a solution of Constraint System 6.1, but could also be given in any other way – as long as it is *SL*-correct.

**Theorem 6.7.** *If XSL is SL-correct and XASC is an ascending-path solution relative to XSL, then XASC is ASC-correct:*

$$\mathcal{X}\_{A\mathcal{SC}} \ge \text{MO}\mathcal{ASC}$$

*Proof.* We show

$$\forall \pi \in A \text{SC}(\mathbf{s}, t) . f\_{\pi} \le \mathbf{X}\_{A \text{SC}}(\mathbf{s}, t)$$

by induction on <sup>π</sup> <sup>∈</sup> *ASC*. The cases asc-seq*empty* and asc-seq*asc* are very similar to the corresponding cases in the proof of Theorem 6.6, so we only consider asc-seq*sl*.

Let π = π ′ ·*ecall* · π ′′ ·*eret* where π ′ ∈ *ASC*(*s*, *n*), *n*0, π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ), *n*<sup>1</sup> *eret* → *t* and (*ecall*,*eret*) ∈ Φ. By assumption, we know

*f*π′′ ≤ *XSL*(*n*0, *n*<sup>1</sup> ),(Ass*sl*)

and, by induction hypothesis,

$$\text{(IH}\_{\text{sl}}) \qquad \qquad \qquad \qquad f\_{\pi'} \le X\_{\text{ASC}}(s, n).$$

Furthermore, we know that *<sup>f</sup>*π′′ <sup>≠</sup> <sup>⊠</sup> by (5.14) and (5.15). By (Ass*sl*), this means that also *XSL*(*n*0, *n*<sup>1</sup> ) ≠ ⊠. Hence, using (6.3),

$$\begin{aligned} f\_{\pi} &= f\_{\varepsilon\_{\text{left}}} \circ f\_{\pi'} \circ f\_{\varepsilon\_{\text{call}}} \circ f\_{\pi'} & \quad \text{(definition )}\\ &\leq f\_{\varepsilon\_{\text{left}}} \circ X\_{SL}(n\_0, n\_1) \circ f\_{\varepsilon\_{\text{call}}} \circ X\_{ASC}(s, n) & \quad \text{(Ass.s)}, \text{(IH}\_{\text{sl}})\\ &\leq X\_{ASC}(s, t) . & \quad \text{(by \; asc-son-(nn))} \end{aligned}$$

□

Similar to Theorem 6.7, Theorem 6.8 states that solutions of Constraint System 6.3 are *DESC*-correct, provided that Constraint System 6.3 is defined with respect to an *SL*-correct function *XSL*. The proof is omitted as it is completely analogous to the proof of Theorem 6.7.

**Theorem 6.8.** *If XSL is SL-correct and XDESC is a descending-path solution relative to XSL, then XDESC is DESC-correct.*

Finally, Theorem 6.9 states that any solution of Constraint System 6.4 with respect to an *ASC*-correct function *XASC* and a *DESC*-correct function *XDESC* is *VP*-correct.

**Theorem 6.9.** *Let XASC be a ASC-correct, XDESC be DESC-correct and XVP be a valid-path solution relative to XASC and XDESC. Then XVP is VP-correct.*

*Proof.* We show

$$\forall \pi \in VP(s, t) \text{.} \, f\_{\pi} \le \mathcal{X}\_{VP}.$$

by induction on π ∈ *VP*(*s*, *t*). So let *n* ∈ *N*, π<sup>1</sup> ∈ *ASC*(*s*, *n*) and π<sup>2</sup> ∈ *DESC*(*n*, *t*). By assumption, we have

$$f\_{\pi\_1} \le X\_{A \& \mathbb{C}}(s, n) \land f\_{\pi\_2} \le X\_{\text{DECC}}(n, t)$$

Furthermore, we know that *f*π<sup>1</sup> ∈ *F* and *f*π<sup>2</sup> ∈ *F* using (5.14) and (5.15). Hence we can conclude

$$\begin{aligned} f\_{\pi} &= f\_{\pi\_2} \circ f\_{\pi\_1} & \{ \text{ definition } \} \\ &\le X\_{\text{DES}}(n, t) \circ X\_{\text{ASC}}(s, n) & \{ \text{by assumption, monotonicity of } \circ \} \\ &\le X\_{VP} & \{ \text{ by constraint } \text{val.up-son.} \} \end{aligned}$$

□

Next, I want to consider the question under which circumstances the constraint systems not only correctly describe the data transfer along their corresponding path set P but are also P-precise. For classical interprocedural data-flow analysis, this is the case if they are universally distributive [154]. Such a result can also be given for my generalized setup.

Using the argument from Remark 6.3, it suffices to show that MOP is a solution of the corresponding constraint system C<sup>P</sup> in order to guarantee precision of *l f p*(*F*C<sup>P</sup> ). This is stated and proven in Theorem 6.10.

**Theorem 6.10.** *Assume that* F<sup>⊠</sup> *is universally distributive. Then the following statements are true:*

*1.* MO*SL is a same-level solution.*

*2. If XSL is SL-precise, then* MO*ASC is an ascending-path solution with respect to XSL.*

*3. If XSL is SL-precise, then* MO*DESC is a descending-path solution with respect to XSL.*

*4. If XASC is ASC-precise and XDESC is DESC-precise, then* MO*VP is a validpath solution with respect to XASC and XDESC.*

*Proof.* 1. We show thatMO*SL* satisfies sl-sol-(i), sl-sol-(ii) and sl-sol-(iii). sl-sol-(i) : For every *<sup>s</sup>* <sup>∈</sup> *<sup>N</sup>*, MO*SL*(*s*,*s*) <sup>≥</sup> *id* is satisfied because <sup>ϵ</sup> <sup>∈</sup> *SL*(*s*,*s*).

sl-sol-(ii) : Let *<sup>s</sup>* <sup>∈</sup> *<sup>N</sup>*, *<sup>e</sup>* <sup>∈</sup> *<sup>E</sup>intra*, *<sup>t</sup>* ′ ∈ *N* such that *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>*. Then we have

$$SL(s, t) \supseteq \{\pi' \cdot e \mid \pi' \in SL(s, t')\}$$

This means that

$$\begin{aligned} \text{MOSL}(s, t) &\geq \bigsqcup \{ f\_{\pi' \cdot \varepsilon} \mid \pi' \in SL(s, t') \} & \{ \text{(6.5)} \} \\ &= \bigsqcup \{ f\_{\varepsilon} \circ f\_{\pi'} \mid \pi' \in SL(s, t') \} & \{ \text{def. of } f\_{\pi} \} \\ &= f\_{\varepsilon} \circ \bigsqcup \{ f\_{\pi'} \mid \pi' \in SL(s, t') \} & \{ f\_{\varepsilon} \text{ is u.d.} \} \\ &= f\_{\varepsilon} \circ \text{MOSL}(s, t') & \{ \text{def. of } \text{MOSL} \} \end{aligned}$$

sl-sol-(iii) : Let *<sup>s</sup>* <sup>∈</sup> *<sup>N</sup>*, *<sup>e</sup><sup>c</sup>* <sup>∈</sup> *<sup>E</sup>call*, *<sup>e</sup><sup>r</sup>* <sup>∈</sup> *<sup>E</sup>ret*, and *<sup>n</sup>*, *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> <sup>∈</sup> *<sup>N</sup>* such that (*ec*,*er*) ∈ Φ, *n <sup>e</sup><sup>c</sup>* <sup>→</sup> *<sup>n</sup>*<sup>0</sup> and *<sup>n</sup>*<sup>1</sup> *<sup>e</sup><sup>r</sup>* <sup>→</sup> *<sup>t</sup>*. Then

$$SL(\mathbf{s}, t) \supseteq \{\pi\_1 \cdot e\_\mathbf{c} \cdot \pi\_2 \cdot e\_\mathbf{r} \mid \pi\_1 \in SL(\mathbf{s}, n), \pi\_2 \in SL(n\_0, n\_1)\}.$$

This means that

$$\begin{aligned} &\text{MOSL}(s,t) \\ &\geq \bigsqcup \{f\_{\mathcal{E}\_{\mathcal{E}}} \circ f\_{\pi\_{2}} \circ f\_{\mathcal{E}\_{\mathcal{E}}} \circ f\_{\pi\_{1}} \mid \pi\_{1} \in SL(s,n), \pi\_{2} \in SL(n\_{0},n\_{1})\} \\ &\text{(6.5), definition }\+ \\ &\equiv f\_{\mathcal{E}\_{\mathcal{E}}} \circ \bigsqcup \{f\_{\pi\_{2}} \mid \pi\_{2} \in SL(n\_{0},n\_{1})\} \circ f\_{\mathcal{E}\_{\mathcal{E}}} \circ \bigsqcup \{f\_{\pi\_{1}} \mid \pi\_{1} \in SL(s,n)\} \\ &\quad \{\mathcal{F}\} \text{ is u.d.}\ \} \\ &= f\_{\mathcal{E}\_{\mathcal{E}}} \circ \text{MOSL}(n\_{0},n\_{1}) \circ f\_{\mathcal{E}\_{\mathcal{E}}} \circ \text{MOSL}(s,n) \\ &\quad \{\text{ definition of MOSL}\} \end{aligned}$$

2. We show that MO*ASC* satisfies the constraints from Constraint System 6.2 with respect to *XSL*.

asc**-**sol**-(**i**)** For every *<sup>s</sup>* <sup>∈</sup> *<sup>N</sup>*, MO*ASC*(*s*,*s*) <sup>≥</sup> *id* is satisfied because <sup>ϵ</sup> <sup>∈</sup> *ASC*(*s*,*s*).

asc**-**sol**-(**ii**)** Let *s*,*t*,*t* ′ ∈ *N*, *e* ∈ *Eintra* ∪ *Eret* with *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>*. Then we have π ′ ·*e* ∈ *ASC*(*s*, *t*) for every π ′ ∈ *ASC*(*s*, *t* ′ ). Hence,

$$\{\pi' \cdot e \mid \lvert \pi' \in A \text{SC}(s, t') \rangle\} \subseteq A \text{SC}(s, t)\_{\prime \prime}$$

which translates to

$$\bigsqcup \{ f\_{\pi' \cdot \varepsilon} \mid \pi' \in A \& \mathbf{C}(s, t') \} \leq \text{MO} A \& \mathbf{C}(s, t) . \bigvee$$

Thus we can conclude

$$\begin{aligned} & \quad \text{MOASC}(s, t) \\ & \geq \bigsqcup \{ f\_{\pi' \cdot \varepsilon} \mid \pi' \in \text{ASC}(s, t') \} & \qquad \{ \text{see above} \} \\ &= \bigsqcup \{ f\_{\varepsilon} \circ f\_{\pi'} \mid \pi' \in \text{ASC}(s, t') \} & \qquad \{ \text{ definition} \} \\ &= f\_{\varepsilon} \circ \bigsqcup \{ f\_{\pi'} \mid \pi' \in \text{ASC}(s, t') \} & \qquad \{ F\_{\mathbb{E}} \text{ is u.d.} \} \\ &= f\_{\varepsilon} \circ \text{MOASC}(s, t') . \qquad \{ \text{ definition} \} \end{aligned}$$

asc**-**sol**-(**iii**)** Let *<sup>s</sup>*, *<sup>t</sup>* <sup>∈</sup> *<sup>N</sup>*, *<sup>e</sup><sup>c</sup>* <sup>∈</sup> *<sup>E</sup>call*, *<sup>e</sup><sup>r</sup>* <sup>∈</sup> *<sup>E</sup>ret* with *<sup>n</sup> <sup>e</sup><sup>c</sup>* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup><sup>r</sup>* <sup>→</sup> *<sup>t</sup>*, (*ec*,*er*) <sup>∈</sup> <sup>Φ</sup> and *XSL*(*n*0, *n*<sup>1</sup> ) ≠ ⊠. Then, according to the closure properties of *ASC*, we have π ′ · *e<sup>c</sup>* · π ′′ · *e<sup>r</sup>* ∈ *ASC*(*s*,*t*) for all π ′ ∈ *ASC*(*s*, *n*), π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ). Hence we have

$$\{\pi' \cdot e\_{\mathcal{C}} \cdot \pi'' \, ^\prime \cdot e\_{\mathcal{T}} \mid \pi' \in A \text{SC}(\mathbf{s}, \mathfrak{n}), \pi'' \in SL(\mathfrak{n}\_0, \mathfrak{n}\_1) \} \subseteq A \text{SC}(\mathbf{s}, \mathfrak{t})\_{\prime \prime}$$

which, due to (6.3), implies that

$$\text{(6.6)}\qquad\bigsqcup\_{\pi\in\text{MO}}\{f\_{\pi'\cdot\varepsilon\cdot\pi'\cdot\varepsilon\_{\ell}\prime}\mid\pi'\in\text{ASC}(s,n),\pi\prime\prime\in\text{SL}(n\_0,n\_1)\},$$

$$\leq \text{MO}\,\text{ASC}(s,t).$$

We conclude as follows:

$$\textsf{MOASC}(s, t)$$

$$\begin{split} & \geq \bigsqcup \{ f\_{\pi\_1 \circ \mathcal{E}\_{\ell'} \circ \pi\_2 \circ \pi\_1} \mid \pi\_1 \in A \text{SC}(s, t'), \pi\_2 \in SL(n\_0, n\_1) \} \\ & \qquad \{ \text{see above} \} \\ &= \bigsqcup \{ f\_{\ell\_{\mathcal{E}\_{\ell}}} \circ f\_{\pi\_2} \circ f\_{\ell\_{\mathcal{E}}} \circ f\_{\pi\_1} \mid \pi\_1 \in A \text{SC}(s, t'), \pi\_2 \in SL(n\_0, n\_1) \} \\ & \qquad \{ \text{ definition} \} \\ &= f\_{\ell\_{\mathcal{E}}} \circ \bigsqcup \{ f\_{\pi\_2} \mid \pi\_2 \in SL(n\_0, n\_1) \} \circ f\_{\ell\_{\mathcal{E}}} \circ \bigsqcup \{ f\_{\pi\_1} \mid \pi\_1 \in A \text{SC}(s, t') \} \\ & \qquad \{ \text{\$f\_{\mathcal{E}}\$ is u.d.\$ } \} \\ &= f\_{\ell\_{\mathcal{E}}} \circ \text{MOSL}(n\_0, n\_1) \circ f\_{\ell\_{\mathcal{E}}} \circ \text{MOASC}(s, n) \\ & \qquad \{ \text{ definition} \} \\ & \geq f\_{\ell\_{\mathcal{E}}} \circ X\_{\mathcal{S} L}(n\_0, n\_1) \circ f\_{\mathcal{E}} \circ \text{MOASC}(s, n) . \\ & \qquad \{ \text{\$X\_{\mathcal{S} L}\$ is S\$L\$-preise}, \text{(6.5)} \} \end{split}$$


$$\begin{aligned} &\text{MOVP}(s, t) \\ &= \bigsqcup \{ f\_{\pi\_2} \circ f\_{\pi\_1} \mid \pi\_1 \in A\text{SC}(s, n), \pi\_2 \in D\text{ESC}(n, t) \} \\ &\quad \{ \text{ definition} \} \\ &= \bigsqcup \{ f\_{\pi\_1} \mid \pi\_1 \in D\text{ESC}(n, t) \} \circ \bigsqcup \{ f\_{\pi\_1} \mid \pi\_1 \in A\text{SC}(s, n) \} \\ &\quad \{ F\_{\mathfrak{A}} \text{ is u.v. } \} \\ &\ge X\_{DESC}(n, t) \circ X\_{ASC}(s, n) . \\ &\quad \{ \text{assumptions about } X\_{ASC} \text{ and } X\_{DESC} \text{ (6.3) } \} \end{aligned}$$

□

# **6.3 The Call-String Approach**

The basic idea of the call-string approach, which I already explained in chapter 3, is to additionally simulate a *call stack*: Each time a procedure is called, the call site is pushed to the stack, and each time the procedure returns, the call site it is supposed to return to is popped off the stack. Sharir and Pnueli showed for classical interprocedural data-flow analysis that the call string approach with unbounded stacks yields the same solution as the functional approach [154]. However, unbounded stacks also lead to infinite constraint systems and lattices that do not satisfy (ACC). This is why in practice, one usually uses approximate approaches such as bounded stack heights.

In this section, I will give a general and formal presentation of the callstring approach under my more general assumptions. The general results of this section will be that (a) I can achieve the same result as Sharir and Pnueli under my more general assumptions and (b) that I provide a general framework from which one can derive correctness results for approximative call-string approaches that include but are potentially not limited to bounded stack heights.

This section is divided into four subsections. In subsection 6.3.1, I introduce the abstract concept of *stack spaces*, which provide enough structure to simulate the calling behavior of interprocedural programs but are general enough to at least cover both unbounded and bounded stack heights. Given a stack space S, I then introduce the S*-acceptable* paths, i.e. the set of paths of *G* that exhibit valid stack usage with respect to S. Moreover, I define a monotone constraint system and prove a correctness and a precision result for solutions of this system. These results are similar to the corresponding results in section 6.2, but do not compare solutions to *MOVP* but to the merge over all S-acceptable paths. In order to get results with respect to *MOVP*, I need to relate the S-acceptable paths to *VP* for specific stack spaces and this is the subject of the other subsections. As a first step, I consider the space of unbounded stacks in subsection 6.3.2. For this stack space, I can indeed show that the acceptable paths coincide with *VP*. After that, in subsection 6.3.3, I describe a way that allows to transfer correctness results between stack spaces. For this, I introduce the concept of *stack abstractions*, which allows to relate two stack spaces using the more general and well-known concept of *galois connection*. The main result of subsection 6.3.3 is that if there is a stack abstraction between two stack spaces S and S # , then all S-acceptable paths are also S # -acceptable. Finally, subsection 6.3.4 gives an example of an application of this main result and proves that the call-string approach using the space of *k*-bounded stacks yields a correct approximation of *MOVP*.

# **6.3.1 Stack Spaces**

Definition 6.11 introduces *stack spaces*. A stack space is an abstract structure that provides the interface needed by an interprocedural program to implement procedure calls.

**Definition 6.11.** *A* stack space *over the alphabet A is a partially ordered set* (*S*,≤) *(whose elements are called* stacks*) with a distinguished element* ϵ *(the empty stack) and functions*

$$\begin{aligned}push: A \times S &\to S\\pop: (S \mid \{\epsilon\}) &\to S\\top: (S \mid \{\epsilon\}) &\to A\end{aligned}$$

*such that the following conditions hold:*


The axioms (TopPush), (NEPushPop) and (PushNE) should not be surprising as they essentially describe how a stack works.

Additionally, stack spaces provide a partial order. The intuition behind ≤ is that a stack space not necessarily provides full and precise information about actual call stacks but may only give partial and approximate information. We can use ≤ to compare stacks with respect to the information they provide: Given two stacks σ, σ ′ , σ ≤ σ ′ is supposed to represent that σ ′ does not provide more information than σ. The axioms (PushMon), (PopMon) and (TopVsLe) specify that the main operations *push*, *pop* and *top* are compatible with ≤.

A special case is the empty stack ϵ that provides no information. This is formalized by (EpsMax). Finally, I want to give some explanation for (PopPushLe). It considers a stack σ and an element *a* and compares σ to the result of first pushing *a* to σ and then popping one element off the top. Normally, one would expect that the result of these two subsequent operations should be just σ. This is indeed the case for unbounded stacks but in general, I cannot demand this since stack spaces are supposed to also cover *bounded stacks*. For bounded stacks, which I will formally show in Example 6.13, there is a *k* > 0 such that no stack is higher than *k*. Consequently, such a stack can be *full*. If we push an element to a full stack and want to preserve the top, we simply remove the element at the bottom. Popping off the top element then does not yield the full stack but only a prefix of it. This is why (PopPushLe) only demands that σ ≤ *pop*(*push*(*a*, σ)).

The following two examples show to important stack spaces, namely the spaces of unbounded and *k*-bounded stacks, respectively.

**Example 6.12.** *Consider* S<sup>∞</sup> = (*E* ⋆ *call*,≤∞, <sup>ϵ</sup>∞, *push*∞, *pop*∞, *top*∞) *with*

$$\sigma \leq\_{\infty} \sigma' \overset{def}{\Longleftrightarrow} \ \sigma' \in \text{Prefixes}(\sigma)$$

$$\epsilon\_{\infty} \overset{def}{=} \epsilon \ \text{(empty sequence)}$$

$$\text{push}\_{\infty}(e, \sigma) \overset{def}{=} e \cdot \sigma$$

$$\operatorname{pop}\_{\infty}(e \cdot \sigma) \overset{def}{=} \sigma = (e \cdot \sigma)^{\geq 1}$$

$$\operatorname{top}\_{\infty}(e \cdot \sigma) \overset{def}{=} e = (e \cdot \sigma)^{0}$$

≤<sup>∞</sup> *is a partial order, as can easily be verified, and* S<sup>∞</sup> *is a stack space over Ecall:* (EpsMax) ϵ *is a prefix of every symbol sequence.*

(PopMon) *If e*· σ ′ *is a prefix e*′ · σ*, then e* = *e* ′ *and* σ ′ *is a prefix of* σ*.*

(PushMon) *If* σ ′ *is a prefix of* σ*, then e*· σ *is a prefix of e*· σ ′ *for every e* ∈ *Ecall.* (PushNE) *It is clear that* <sup>σ</sup> <sup>≠</sup> <sup>ϵ</sup> *implies a* · <sup>σ</sup> <sup>≠</sup> <sup>ϵ</sup>*.*

(TopPush) *It is also easy to see that top*∞(*push*∞(*a*, σ)) = *top*∞(*a* · σ) = *a.*

(NEPushPop) *Every non-empty sequence* σ *can be expressed as e*· σ ≥1 *for some e* ∈ *Ecall.*

(TopVsLe) *If* σ, σ ′ *are both not empty and* σ *is a prefix of* σ ′ *, then obviously* σ <sup>0</sup> = σ ′0 *.*

(PopPushLe) *For every* σ ∈ *E* ∞ *call, we have pop*∞(*push*(*a*, <sup>σ</sup>)) = <sup>σ</sup> <sup>≥</sup><sup>∞</sup> <sup>σ</sup>*.*

**Example 6.13.** *For k* ∈ **N***, consider* S*<sup>k</sup>* = (*E* ≤*k call*,≤*<sup>k</sup>* , ϵ*<sup>k</sup>* , *push<sup>k</sup>* , *pop<sup>k</sup>* , *top<sup>k</sup>* ) *with*

$$\begin{aligned} \sigma \leq\_k \sigma' &\overset{def}{\Longleftrightarrow} \sigma' \in \text{Prefixes}(\sigma) \\ \epsilon\_k &\overset{def}{=} \epsilon \quad (empty \, sequence) \\ \text{push}\_k(e, \sigma) &\overset{def}{=} (e \cdot \sigma)^{\le k} \\ \text{pop}\_k(e \cdot \sigma) &\overset{def}{=} \sigma \\ \text{top}\_k(e \cdot \sigma) &\overset{def}{=} e \end{aligned}$$

*Then* S*<sup>k</sup> is a stack space over Ecall: This is trivial for k* = 0*, so that we only consider the case that k* > 0*.* ≤*<sup>k</sup> is a partial order with greatest element* ϵ*<sup>k</sup> , so that EpsMax is satisfied.*

(PopMon) *See Example 6.12.*

(PushMon) *push<sup>k</sup>* (*e*, ·) *is monotone with respect to* ≤*<sup>k</sup> because it is the composition of* λσ. *e* · σ *and* λσ. σ ≤*k , which are both monotone with respect to* ≤*k .*

(PushNE) *Let* σ ∈ *E* ≤*k call with* <sup>σ</sup> <sup>≠</sup> <sup>ϵ</sup>*. Then push<sup>k</sup>* (*e*, σ) = (*e* · σ) <sup>≤</sup>*<sup>k</sup>* <sup>=</sup> *<sup>e</sup>* · (σ ≤*k*−1 ) ≠ ϵ*.*

(TopPush) *The argument from above shows in particular that*

$$\operatorname{top}(push(e, \sigma)) = e$$

*for every* σ ∈ *E* ≤*k call, e* ∈ *Ecall.* (NEPushPop) *Let* σ ∈ *E* ≤*k call be non-empty. Then* <sup>σ</sup> = *<sup>e</sup>* · <sup>σ</sup> ≥1 *for some e* ∈ *Ecall. Since* σ ∈ *E* ≤*k call, we have* <sup>σ</sup> = <sup>σ</sup> <sup>≤</sup>*<sup>k</sup>* = (*<sup>e</sup>* · <sup>σ</sup> ≥1 ) <sup>≤</sup>*<sup>k</sup>* = (*<sup>e</sup>* · *pop<sup>k</sup>* (σ))≤*<sup>k</sup>* = *push<sup>k</sup>* (*e*, *pop<sup>k</sup>* (σ))*.*

(TopVsLe) *See Example 6.12.*

(PopPushLe) *Let* σ ∈ *E* ≤*k call. We make a case distinction of whether* <sup>|</sup>σ<sup>|</sup> <sup>&</sup>lt; *<sup>k</sup> or* |σ| = *k:*


In the following, I consider a fixed stack space

$$\mathcal{S} = (E\_{call}, \mathcal{S}\_{\prime} \le \epsilon, push,pop,top)$$

over *Ecall*.

Next, I want to introduce the paths in *G* that exhibit an acceptable stack behavior with respect to S and the correspondence relation Φ. For this, I use a function *cs* that takes as input an edge sequence π ∈ *E* <sup>⋆</sup>. This function traverses π and simulates π's usage of calls and returns with respect to S and Φ. Each time a call edge is encountered, *cs* applies the *push* function of S to the current stack. Each time a return edge is encountered, *cs* checks whether the top of the current stack corresponds to the return edge with respect to Φ. If so, the top is popped off using the *pop* function of S – if not, *cs* concludes that π is unacceptable or *invalid*.

In summary, *cs* returns either a call stack that is left behind by π, e.g. π just consists of a series of call edges, then the result of *cs*is the stack that consists of these unfinished calls) or a special symbol ▼ that signals unacceptable stack behavior. Occasionally, I will also call ▼ the *invalid stack*.

I extend *<sup>S</sup>* and <sup>≤</sup> by ▼ such that ▼ is the least element with respect to <sup>≤</sup> and

(6.7) *push* : *Ecall* × *S*▼ → *S*▼

$$(6.8)\qquadpop:(S\backslash\{\epsilon\})\cup\{\mathbf{v}\}\to S\mathbf{v}$$

such that

$$(6.9) \qquad \forall \sigma \in \mathbb{S}. \forall e \in \mathbb{E}\_{\text{call}}.push(e, \sigma) = \mathbf{\v} \iff \sigma = \mathbf{\v}$$

$$(6.10)\qquad \forall \sigma \in \mathcal{S}.pop(\sigma) = \mathfrak{v} \iff \sigma = \mathfrak{v}$$

That is, if *push* and *pop* are applied to valid stacks, the result is guaranteed to be valid, and if they are applied to the invalid stack, the result stays invalid.

With the help of ▼, I give the definition of *cs* in Definition 6.14.

# **Definition 6.14.**

$$\mathsf{cs} : \mathsf{Paths}(\mathsf{G}) \to \mathsf{S}\_{\mathsf{T}}$$

*is defined by*

$$\begin{array}{ll} \mathsf{cs}(\boldsymbol{\epsilon}) = \boldsymbol{\epsilon} \\\\ \mathsf{cs}(\boldsymbol{\pi} \cdot \boldsymbol{e}) = \begin{cases} \mathsf{cs}(\boldsymbol{\pi}) & \text{if } \boldsymbol{e} \in \boldsymbol{E}\_{\text{intra}} \\ \mathsf{push}(\boldsymbol{e}, \mathsf{cs}(\boldsymbol{\pi})) & \text{if } \boldsymbol{e} \in \boldsymbol{E}\_{\text{call}} \\ \mathsf{cs}(\boldsymbol{\pi}) = \boldsymbol{\epsilon} & \text{if } \boldsymbol{e} \in \boldsymbol{E}\_{\text{ret}} \text{ and } \boldsymbol{c}\boldsymbol{\varsigma}(\boldsymbol{\pi}) = \boldsymbol{\epsilon} \\ \mathsf{pop}(\boldsymbol{c}\boldsymbol{s}(\boldsymbol{\pi})) & \text{if } \boldsymbol{e} \in \boldsymbol{E}\_{\text{ret}} \wedge \boldsymbol{c}\boldsymbol{s}(\boldsymbol{\pi}) \notin \{\mathsf{v}, \boldsymbol{\epsilon}\} \\ & \wedge \, (\mathsf{top}(\boldsymbol{c}\boldsymbol{s}(\boldsymbol{\pi})), \boldsymbol{e}) \in \boldsymbol{\Phi} \\ \mathsf{v} & \text{otherwise} \end{array} \end{array}$$

If a path π can be fully traversed without resulting in the invalid stack, I call <sup>π</sup> <sup>S</sup>*-acceptable*<sup>33</sup> .

**Definition 6.15.** *The* S*-acceptable paths are given by*

$$AP\_{\mathcal{S}}(s, t) \stackrel{def}{=} \{ \pi \in \text{Paths}\_{\mathcal{G}}(s, t) \mid cs(\pi) \neq \mathbf{v} \}.$$

Next, I introduce Constraint System 6.5, which can be used to describe the data-flows along paths from *AP*. Like the previous constraint systems, its idea is to simulate what happens when we extend a path by one edge. In addition to the application of the corresponding edge function, Constraint System 6.5 also considers the effect of the edge on the stack – this is why the variables in Constraint System 6.5 have an additional stack component.

**Constraint System 6.5.** *Let* S = (*Ecall*, *S*, ≤, ϵ, *push*, *pop*, *top*) *be a stack space over Ecall. Then X*<sup>S</sup> : *N* × *N* × *S* → *F*<sup>⊠</sup> *is a* S*-solution if it satisfies the following*

<sup>33</sup>A similar definition can be found in the work of Sharir and Pnueli[154, p. 227].

*constraints:*

$$\begin{array}{ll} \left(\texttt{mMTY}\_{\mathcal{S}}\right) \frac{1}{X\_{\mathcal{S}}(s,s,\epsilon) \geq id} & \left(\texttt{mrnRA}\_{\mathcal{S}}\right) \frac{e \in E\_{\textit{int}\,\mathsf{m}} \qquad t' \stackrel{\epsilon}{\rightarrow} t}{X\_{\mathcal{S}}(s,t,\sigma) \geq f\_{\mathcal{C}} \circ X\_{\mathcal{S}}(s,t',\sigma)} \\\\ \left(\texttt{call}\_{\epsilon}\right) \frac{e\_{\textit{call}} \in E\_{\textit{call}} \qquad t' \stackrel{\epsilon\_{\mathit{call}}}{\rightarrow} t \quad \sigma = \mathit{push}(e\_{\mathit{call}}, \sigma') \\\\ \left(\texttt{nerr}\_{\mathcal{S}}^{(1)}\right) \frac{e\_{\textit{ret}} \in E\_{\textit{ret}} \qquad t' \stackrel{\epsilon\_{\mathit{rel}}}{\rightarrow} t}{X\_{\mathcal{S}}(s,t,\epsilon) \geq f\_{\mathcal{C}\textit{rel}} \circ X\_{\mathcal{S}}(s,t',\epsilon)} \\\\ \left(\texttt{nerr}\_{\mathcal{S}}^{(2)}\right) \frac{t' \stackrel{\epsilon\_{\mathit{rel}}}{\rightarrow} t \; (e\_{\textit{call}}, e\_{\textit{ret}}) \in \mathsf{\underline{\sf{op}}} \; \mathsf{p}(\mathit{push}(e\_{\mathit{call}}, \sigma)) = \sigma \; \mathsf{p}\, \mathsf{push}(e\_{\textit{call}}, \sigma) \neq \epsilon \; \epsilon\_{\mathit{i}} \\\\ \left(\texttt{nerr}\_{\mathcal{S}}^{(2)}\right) \frac{\mathsf{t'} \stackrel{\epsilon\_{\mathit{rel}}}{\rightarrow} \mathsf{X}\_{\mathcal{S}}(s,t,\sigma) \geq f\_{\mathsf{ret}} \; \mathsf{x}\_{\mathcal{S}}(s,t',\texttt{push}(e\_{\textit{call}}, \sigma)) \neq \epsilon \; \epsilon\_{\mathit{i}} \; \$$

Like for previous constraint systems, I want to make some remarks on how to read and interpret Constraint System 6.5. First of all, the set of variables of Constraint System 6.5 is *N* × *N* × *S*. Hence, solutions to Constraint System 6.5 live in the complete lattice

$$N \times N \times S \to F\_{\boxtimes}$$

Moreover, I extend the function symbols and their interpretation function to also cover the stack operations. Analogously to previous constraint systems, for each occurrence of such a stack operation in a constraint in the lower part of one of the rules, I actually mean the corresponding function symbol. Regarding the upper part, I again want to remind the reader that the upper part of the rules specifies conditions under which the constraint in the lower part of the rule is contained in the set of constraints that makes up Constraint System 6.5. As an example, consider (6.5): It contains a constraint of the form

$$X\_{\mathcal{S}}(s, t, \sigma) \ge f\_{\mathcal{E}\_{\text{call}}} \circ X\_{\mathcal{S}}(s, t', \sigma')$$

for all *s*,*t*,*t* ′ ∈ *N*, *ecall* ∈ *E* and σ, σ ′ ∈ *S* such that *e* ∈ *Ecall* , *t* ′ *<sup>e</sup>call* <sup>→</sup> *<sup>t</sup>* and σ = *push*(*ecall*, σ ′ ).

Theorem 6.16 gives a correctness result for Constraint System 6.5.

**Theorem 6.16.** *Let X* : *N* × *N* × *S* → *F*<sup>⊠</sup> *be a* S*-solution. Then for arbitrary s*, *t* ∈ *N we have*

$$(6.11) \qquad \forall \pi \in \text{Paths}\_{\mathbb{G}}(s, t). \; \pi \in AP\_{\mathbb{S}}(s, t) \implies f\_{\pi} \le X(s, t, \text{cs}(\pi))$$

*In particular, X*̃(*s*, *t*) = ⨆︁ <sup>σ</sup>∈*<sup>S</sup> <sup>X</sup>*(*s*, *<sup>t</sup>*, <sup>σ</sup>) *defines a* MO*AP*S*-correct solution.*

*Proof.* First assume that (6.11) is proven. Then

$$\forall \pi \in AP\_{\mathcal{S}}(s, t). \, f\_{\pi} \le X(s, t, \text{cs}(\pi)) \le \bigsqcup\_{\sigma \in S} X(s, t, \sigma) = \tilde{X}(s, t).$$

so that

$$\text{MOAP}\_{\mathcal{S}}(s, t) = \bigsqcup\_{\pi \in AP\_{\mathcal{S}}(s, t)} f\_{\pi} \le \tilde{X}(s, t)$$

Now we prove (6.11) by induction on the length of paths. Let *n* ∈ **N**. The induction hypothesis is

(IH) <sup>∀</sup><sup>π</sup> <sup>∈</sup> *PathsG*(*s*, *<sup>t</sup>*). <sup>|</sup>π<sup>|</sup> <sup>&</sup>lt; *<sup>n</sup>* <sup>∧</sup> <sup>π</sup> <sup>∈</sup> *AP*S(*s*, *<sup>t</sup>*) =<sup>⇒</sup> *<sup>f</sup>*<sup>π</sup> <sup>≤</sup> *<sup>X</sup>*(*s*, *<sup>t</sup>*, *cs*(π))

We show

$$(6.12)\quad \forall \pi \in \text{Paths}\_{\mathbb{G}}(\text{s}, \text{t}).\\|\pi| = n \land \pi \in AP\_{\mathcal{S}}(\text{s}, \text{t}) \implies f\_{\pi} \leq \text{X}(\text{s}, \text{t}, \text{cs}(\pi))$$

by case distinction on *n*.

*<sup>n</sup>* <sup>=</sup> 0: clear by constraint emptyS.

*<sup>n</sup>* <sup>&</sup>gt; 0: Let <sup>π</sup> be a path from *<sup>s</sup>*to *<sup>t</sup>* with <sup>|</sup>π<sup>|</sup> = *<sup>n</sup>* <sup>&</sup>gt; <sup>0</sup> and <sup>π</sup> <sup>∈</sup> *AP*S(*s*, *<sup>t</sup>*). Hence, we can write π = π ′ ·*e* with π ′ ∈ *PathsG*(*s*, *t* ′ ) and *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>*. Moreover, since <sup>π</sup> <sup>∈</sup> *AP*S(*s*,*t*), it must be <sup>π</sup> ′ <sup>∈</sup> *AP*S(*s*,*t*) as well (this follows from the definition of *cs*). By definition, (IH), and (6.3), we see that

$$f\_{\pi} \le f\_{\varepsilon} \circ X(s, t', cs(\pi')).$$

Then we show

$$f\_{\varepsilon} \circ X(s, t', cs(\pi')) \le X(s, t, cs(\pi))$$

by case distinction on *e*:

• *e* ∈ *Eintra*: Then *cs*(π) = *cs*(π ′ ) and hence

$$\begin{aligned} \{ f\_{\boldsymbol{\sigma}} \circ \mathcal{X}(\mathbf{s}, t', \boldsymbol{\varsigma}\boldsymbol{\varsigma}(\pi')) &\leq \mathcal{X}(\mathbf{s}, t, \boldsymbol{\varsigma}\boldsymbol{\varsigma}(\pi')) &\{ \text{constraint } \mathsf{n}\mathsf{r}\mathsf{r}\mathsf{a}\_{\mathcal{S}} \} \\ &= \mathcal{X}(\mathsf{s}, t, \boldsymbol{\varsigma}\boldsymbol{\varsigma}(\pi)) &\{ \text{since } \mathsf{c}\mathsf{s}(\pi') = \mathsf{c}\mathsf{s}(\pi) \} \end{aligned}$$

• *e* = *ecall* ∈ *Ecall*: Then *cs*(π) = *push*(*ecall*, *cs*(π ′ )) and hence we can conclude

$$f\_{\mathcal{E}\_{\text{call}}} \circ X(s, t', c\varsigma(\pi')) \le X(s, t, c\varsigma(\pi)),$$

by callS.

	- 1. If *cs*(π ′ ) = *cs*(π) = ϵ, we can make the following reasoning:

$$\begin{aligned} f\_{\mathfrak{e}\_{\mathsf{ret}}} \circ \mathcal{X}(\mathsf{s}, \mathsf{t}', \mathsf{cs}(\pi')) &= f\_{\mathfrak{e}\_{\mathsf{ret}}} \circ \mathcal{X}(\mathsf{s}, \mathsf{t}', \mathsf{e}) & \{\mathsf{cs}\_{\infty}(\pi') = \mathsf{e}\} \\ &\leq \mathcal{X}(\mathsf{s}, \mathsf{t}, \mathsf{e}) & \{\mathsf{by \, \mathsf{next}}\_{\mathcal{S}}^{(1)}\} \\ &= \mathcal{X}(\mathsf{s}, \mathsf{t}, \mathsf{c}\mathbf{s}(\pi)) & \{\mathsf{cs}(\pi) = \mathsf{e}\} \end{aligned}$$

2. If *cs*(π ′ ) <sup>∉</sup> {▼, <sup>ϵ</sup>} and (*top*(*cs*(<sup>π</sup> ′ ),*eret*)) ∈ Φ, then for *ecall de f* = *top*(*cs*(π ′ )) we have (*ecall*,*eret*) ∈ Φ and *cs*(π) = *pop*(*cs*(π ′ )) (by definition of *cs*), hence

$$\text{(6.13)}\qquad \text{cs}(\pi') = push(e\_{\text{call}} \,\text{cs}(\pi)) = push(e\_{\text{call}} \,\text{pop}(\text{cs}(\pi')))$$

by (NEPushPop) and (TopPush). Hence, we can apply ret (2) S and conclude:

$$\begin{aligned} f\_{\mathfrak{c}\_{\text{ret}}} & \circ X(s, t', \text{cs}(\pi')) \\ = f\_{\mathfrak{c}\_{\text{ret}}} & \circ X(s, t', \text{push}(\mathfrak{e}\_{\text{call}}, \text{cs}(\pi))) \\ \leq & X(s, t, \text{cs}(\pi)) \\ \leq & \text{s}(s, t, \text{cs}(\pi)) \end{aligned} \tag{6.13}$$

Theorem 6.16 states that MO*AP*<sup>S</sup> can be correctly approximated by computing a solution of Constraint System 6.5 and subsequently joining over all stacks.

In order to give a precision result for Constraint System 6.5, I need a variant of MO*AP*<sup>S</sup> that has an additional stack component. This variant is defined in Definition 6.17. MO(Stack) *AP*<sup>S</sup> (*s*, *t*, σ) only merges over those paths that leave behind stack σ.

**Definition 6.17.** *The stack-based Merge-Over-All-*S*-Acceptable-Paths solution*

$$\mathsf{MO}^{(\text{Stack})}\_{\mathsf{AP}\_{\mathsf{S}}} : N \times N \times S \to\_{\mathsf{mon}} F\_{\mathsf{ES}}$$

*is defined by*

$$\text{MO}\_{\text{AP}\_{\text{S}}}^{\text{(Stack)}}(\text{s}, t, \sigma) = \bigsqcup\_{\pi \in VP(\text{s}, t), \text{cs}\_{\text{\infty}}(\pi) = \sigma} f\_{\pi}$$

Now I show that Constraint System 6.5 is also able to precisely describe MO*AP*S, provided that F is universally distributive.

**Theorem 6.18.** *Let* <sup>F</sup> *be a universally distributive framework. Then* MO*(Stack) AP is an* <sup>S</sup>*-solution. In particular, for the least* <sup>S</sup>*-solution X, we have <sup>X</sup>* <sup>≤</sup> MO*(Stack) AP .*

*Proof.* We show that MO(Stack) *AP* satisfies all the constraints of Constraint System 6.5.

empty<sup>S</sup> Let *<sup>s</sup>* <sup>∈</sup> *<sup>N</sup>*. Then MO(Stack) *AP* (*s*,*s*, <sup>ϵ</sup>) = *id*. So, empty<sup>S</sup> is satisfied.

intra<sup>S</sup> Let *<sup>s</sup>*,*t*,*<sup>t</sup>* ′ ∈ *N*, *e* ∈ *Eintra*, *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>* and <sup>σ</sup> <sup>∈</sup> *<sup>S</sup>*. Then for every π ′ <sup>∈</sup> *AP*S(*s*, *<sup>t</sup>* ′ ) with *cs*∞(π ′ ) = σ we have π ′ ·*<sup>e</sup>* <sup>∈</sup> *AP*S(*s*, *<sup>t</sup>*) and *cs*(<sup>π</sup> ′ ·*e*) = *cs*(π ′ ) = σ. This implies

$$\begin{aligned} & \mathbf{MO}\_{AP}^{\{\text{Stack}\}}(s, t, \sigma) \\ &= \bigsqcup \{ f\_{\pi} \mid \pi \in AP\_{\mathcal{S}}(s, t), \text{cs}(\pi) = \sigma \} & \qquad \{ \text{ definition} \} \\ &\geq \bigsqcup \{ f\_{\pi' \cdot \varepsilon} \mid \pi' \in AP\_{\mathcal{S}}(s, t'), \text{cs}(\pi') = \sigma \} & \qquad \{ \text{see above} \} \end{aligned}$$

□

$$\begin{aligned} &= \bigsqcup f\_{\ell} \circ f\_{\pi'} \mid \pi' \in AP\_{\mathcal{S}}(s, t'), \operatorname{cs}(\pi') = \sigma \big\rangle \qquad & \{\operatorname{ definition}\} \\ &= f\_{\ell} \circ \bigsqcup \{f\_{\pi'} \mid \pi' \in AP\_{\mathcal{S}}(s, t'), \operatorname{cs}(\pi') = \sigma\} \qquad & \{f\_{\ell} \text{ is u.d.}\} \\ &= f\_{\ell} \circ \operatorname{MO}\_{AP}^{\text{(Stack)}}(s, t', \sigma) \qquad & \{\operatorname{definiton}\} \end{aligned}$$

call<sup>S</sup> Let *<sup>s</sup>*,*t*,*<sup>t</sup>* ′ ∈ *N*, *ecall* ∈ *Ecall*, *t* ′ *<sup>e</sup>call* <sup>→</sup> *<sup>t</sup>* and <sup>σ</sup> <sup>∈</sup> *<sup>S</sup>*. Then for every π ′ ∈ *AP*(*s*,*t* ′ ) with *cs*(π ′ ) = σ we have π ′ · *<sup>e</sup>call* <sup>∈</sup> *AP*S(*s*,*t*) with *cs*(<sup>π</sup> ′ · *ecall*) = *push*(*ecall*, *cs*(π ′ )) = *push*(*ecall*, σ). Hence

$$\begin{aligned} &\quad \mathsf{MO}\_{AP}^{(\text{Stack})}(s,t,push(e\_{\text{call}},\sigma)) \\ &= \bigsqcup \{ f\_{\pi} \mid \pi \in AP\_{\mathcal{S}}(s,t), cs\_{\infty}(\pi) = push(e\_{\text{call}},\sigma) \} \qquad \{\text{definition} \} \\ &\geq \bigsqcup \{ f\_{\pi' \cdot e\_{\text{call}}} \mid \pi' \in AP\_{\mathcal{S}}(s,t'), cs(\pi') = \sigma \} \qquad \{\text{see above} \} \\ &= \bigsqcup \{ f\_{\mathcal{C}\_{\text{call}}} \circ f\_{\pi'} \mid \pi' \in AP\_{\mathcal{S}}(s,t'), cs(\pi') = \sigma \} \qquad \{\text{definition} \} \\ &= f\_{\mathcal{C}\_{\text{call}}} \bigsqcup \{ f\_{\pi'} \mid \pi' \in AP\_{\mathcal{S}}(s,t'), cs(\pi') = \sigma \} \qquad \{f\_{\mathcal{C}\_{\text{call}}} \text{ is u.d.} \} \\ &= f\_{\mathcal{C}\_{\text{call}}} \circ \mathsf{MO}\_{AP}^{(\text{Stack})}(s,t',\sigma) \qquad \{\text{definition} \} \end{aligned}$$

ret (1) S Let *s*,*t*,*t* ′ ∈ *N*, *eret* ∈ *Eret*, *t* ′ *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>*, <sup>σ</sup> <sup>∈</sup> *<sup>S</sup>* and <sup>π</sup> ′ <sup>∈</sup> *AP*S(*s*,*<sup>t</sup>* ′ ) with *cs*(π ′ ) = ϵ. Then π ′ · *<sup>e</sup>ret* <sup>∈</sup> *AP*S(*s*,*t*) and *cs*(<sup>π</sup> ′ · *eret*) = ϵ. Hence, we may conclude

$$\begin{aligned} &\mathbf{MO}\_{AP}^{(\text{Stack})}(s,t,\mathfrak{c})\\ &=\bigsqcup\{f\_{\pi}\mid\pi\in AP\_{\mathcal{S}}(s,t),\mathrm{cs}(\pi)=\mathfrak{c}\} \qquad\qquad\qquad\{\text{ definition}\}\\ &\geq\bigsqcup\{f\_{\pi'\cdot\varepsilon\_{\text{ret}}}\mid\pi'\in AP\_{\mathcal{S}}(s,t'),\mathrm{cs}(\pi')=\mathfrak{c}\} \qquad\qquad\{\text{see above}\}\\ &=\bigsqcup\{f\_{\varepsilon\_{\text{ret}}}\circ f\_{\pi'}\mid\pi'\in AP\_{\mathcal{S}}(s,t'),\mathrm{cs}(\pi')=\mathfrak{c}\} \qquad\qquad\{\text{ definition}\}\\ &=f\_{\varepsilon\_{\text{ret}}}\circ\bigsqcup\{f\_{\pi'}\mid\pi'\in AP\_{\mathcal{S}}(s,t'),\mathrm{cs}(\pi')=\mathfrak{c}\} \qquad\qquad\{f\_{\varepsilon\_{\text{ret}}}\text{ is u.d.}\}\\ &=f\_{\varepsilon\_{\text{ret}}}\circ\mathbf{MO}\_{AP}^{(\text{Stack})}(s,t',\mathfrak{c}) \qquad\qquad\{\text{ definition}\}\end{aligned}$$

ret (2) S Let <sup>σ</sup> <sup>∈</sup> *<sup>S</sup>* with *push*(*ecall*, <sup>σ</sup>) <sup>≠</sup> <sup>ϵ</sup> and *pop*(*push*(*ecall*, <sup>σ</sup>)) = <sup>σ</sup>. Furthermore, let *s*,*t*,*t* ′ ∈ *N*, *ecall* ∈ *Ecall*, *eret* ∈ *Eret*, *t* ′ *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>*, (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup>, and π ′ <sup>∈</sup> *AP*S(*s*,*<sup>t</sup>* ′ ) with *cs*(π ′ ) = *push*(*ecall*, σ). Then π ′ · *<sup>e</sup>ret* <sup>∈</sup> *AP*S(*s*,*t*) and *cs*(π ′ · *eret*) = *pop*(*cs*(π ′ )) = *pop*(*push*(*ecall*, σ)) = σ. This justifies that we conclude

$$\begin{split} & \quad \mathsf{MO}\_{AP}^{\text{(Stack)}}(s, t, \sigma) \\ &= \bigsqcup \{ \pi\_{\mathsf{f}} \mid \pi \in AP(s, t), \mathsf{cs}(\pi) = \sigma \} \qquad \qquad \qquad \{ \text{ definition} \} \\ &\geq \bigsqcup \{ f\_{\pi' \cdot \varepsilon\_{\mathsf{rf}\mathsf{e}}} \mid \pi' \in AP(s, t'), \mathsf{cs}(\pi') = \texttt{push}(e\_{\text{call}}, \sigma) \} \qquad \{ \text{see above} \} \\ &= \bigsqcup \{ \ell\_{\mathsf{f}\mathsf{e}\mathsf{e}} \circ f\_{\pi'} \mid \pi' \in AP(s, t'), \mathsf{cs}(\pi') = \texttt{push}(e\_{\text{call}}, \sigma) \} \quad \{ \text{ definition} \} \\ &= f\_{\mathsf{f}\mathsf{e}\mathsf{e}} \circ \bigcup \{ f\_{\pi'} \mid \pi' \in AP(s, t'), \mathsf{cs}(\pi') = \texttt{push}(e\_{\text{call}}, \sigma) \} \quad \{ f\_{\mathsf{f}\mathsf{e}\mathsf{e}} \text{ is u.d.} \} \\ &= f\_{\mathsf{f}\mathsf{e}} \circ \mathsf{MO}\_{AP}^{\text{(Stack)}}(s, t', \mathsf{push}(e\_{\text{ call}}, \sigma)) \qquad \{ \text{ definition} \} \end{split}$$

□

# **6.3.2 The Unbounded Stack Space**

Up until now, we only have compared solutions to the abstract set *AP*S. The subject of this and the following subsection is to examine stack spaces for which *AP*<sup>S</sup> relates to *VP* in a meaningful way. From the results, I will derive *VP*-correctness and *VP*-precision results in subsection 6.3.4

In this subsection, I consider the very important special case of the space S<sup>∞</sup> of unbounded stacks. My main result for this subsection is that the S∞-acceptable paths coincide with the valid paths.

The proof is split up into two parts. In the first part, I show that every valid path is S∞-acceptable. After that, I show every S∞-acceptable path is valid.

# **6.3.2.1 Valid Paths Are** S∞**-Acceptable**

Theorem 6.21 states that every valid path is S∞-acceptable. It makes use of two elementary properties of *cs*<sup>∞</sup> concerning same-level and ascending paths.

The first property states that appending a same-level path does not change the stack. This is not surprising as same-level paths are balanced and valid, which means that for every call there is a return and that every return corresponds to the innermost call.

**Lemma 6.19.** *If* π ′ *is a same-level path, then*

$$\forall \pi \in \text{Paths}(\mathcal{G}).\text{cs}\_{\infty}(\pi \cdot \pi') = \text{cs}\_{\infty}(\pi)$$

*Proof.* Induction on π ′ <sup>∈</sup> *SL* with arbitrary <sup>π</sup>. □

The second property, which is stated in Lemma 6.20, considers ascending paths. Basically, an ascending path is like a same-level path with an excess of return edges. Hence, if we start with an empty stack and have traversed the same-level prefix, the stack is empty. Then traversing the rest of the path leaves the stack empty. Note that this property also highlights a specific characteristic of my treatment of stacks. Because I not only consider descending paths, I have to handle the case of returning from an empty stack, which is not a valid situation in a normal program. In my definition of *cs*∞, I allow returning to any call site if the stack is empty, as long as stack usage is acceptable in the case that the stack is not empty.

**Lemma 6.20.** *If* π *is an ascending path, then cs*∞(π) = ϵ*.*

*Proof.* By induction on π ∈ *ASC*.

	- **–** If *e* ∈ *Eintra*, it follows that

$$\begin{aligned} \csc(\pi' \cdot \varepsilon) &= \csc(\pi') \\ &= \varepsilon \end{aligned} \qquad \begin{aligned} \text{(by definition)} \\ &\qquad \text{(by induction hypothesis)} \end{aligned}$$


$$\begin{aligned} c\varsigma\_{\infty}(\pi' \cdot e\_{\text{call}} \cdot \pi'' \cdot e\_{\text{ret}}) &= c\varsigma\_{\infty}(\pi') \quad \text{(by Lemma 6.19 }\text{)}\\ &= \epsilon \quad \text{(by induction hypothesis }\text{)} \end{aligned}$$

With help of Lemma 6.19 and Lemma 6.20, I can prove acceptability of valid paths.

**Theorem 6.21.** *If* <sup>π</sup> <sup>∈</sup> *VP*(*s*, *<sup>t</sup>*)*, then cs*∞(π) <sup>≠</sup> ▼∞*.*

*Proof.* We use Theorem 5.30. Let π<sup>1</sup> ∈ *ASC*(*s*,*t* ′ ) be an ascending path starting with *s* and ending in some arbitrary *t* ′ ∈ *N*, then it suffices to prove

$$\forall \pi\_2 \in \text{DES. } \forall t \in \text{N. } \pi\_2 \in \text{DES}(t', t) \implies cs\_{\infty}(\pi\_1 \cdot \pi\_2) \neq \mathfrak{v}\_{\infty}.$$

We prove this statement by induction on π<sup>2</sup> ∈ *DESC*.


$$\text{(IH)}\tag{1\text{H}}\tag{1\text{H}}\tag{1\text{H}}\tag{1\text{H}}\tag{1\text{H}}\tag{1\text{H}}\tag{1\text{H}}\tag{1\text{H}}\tag{1\text{H}}$$

We consider these two cases separately.

**–** Let π ′′ 2 = *ecall* ∈ *Ecall*, with *t* ′′ *<sup>e</sup>call* <sup>→</sup> *<sup>t</sup>* ′ . Then , we can conclude

$$\begin{aligned} & \quad cs\_{\infty}(\pi\_1 \cdot \pi\_2 \cdot e\_{\text{call}}) \\ &=push\_{\infty}(e\_{\text{call}} \cdot cs\_{\infty}(\pi\_1 \cdot \pi\_2)) \\ &\neq \mathbf{v}\_{\infty} \end{aligned} \qquad \qquad \begin{aligned} & \quad \{\text{definition}\} \\ & \quad \{\text{definition}\} \\ & \quad \{\text{(IH)}\} \end{aligned}$$

**–** Let π<sup>2</sup> = π ′ 2 · π ′′ 2 with π ′ 2 ∈ *DESC*(*t* ′ ,*t* ′′) and π ′′ 2 ∈ *SL*(*t* ′′ ,*t*) for some *t* ′′ ∈ *N*. Then, we can conclude

$$\begin{aligned} \text{cs}\_{\infty}(\pi\_1 \cdot \pi\_2' \cdot \pi\_2'') &= \text{cs}\_{\infty}(\pi\_1 \cdot \pi\_2') & \{\pi\_2'' \in SL, \text{Lemma 6.19 }\} \\ &\neq \mathbf{v}\_{\infty} & \{\text{(IH)}\} \end{aligned}$$

□

□

# **6.3.2.2** S∞**-Acceptable Paths Are Valid**

For the property that *cs*<sup>∞</sup> only accepts valid paths, which I will show and prove in Lemma 6.24, I need two additional properties of *cs*<sup>∞</sup> that are also of interest on their own.

The first property formally describes the possible shapes of a path π for which *cs*∞(π) is not empty: Suppose that *cs*∞(π) = *ecall* · σ. Then intuitively, it can only be the case that (a) some prefix of π left behind stack σ, (b) then *ecall* was encountered and (c) the part after *ecall* is a same-level path that leaves the stack unchanged. Lemma 6.22 states that this is indeed the case.

**Lemma 6.22.** *If* π ∈ *PathsG*(*s*, *t*) *and cs*∞(π) = *ecall* · σ*, then* π *can be split up into* π = π ′ ·*ecall* · π ′′ *where cs*∞(π ′ ) = σ *and* π ′′ *is a same-level path.*

*Proof.* We fix *s* ∈ *N* and prove the result for all π ∈ *PathsG*(*s*,*t*) with arbitrary *t* ∈ *N*, *e* ∈ *Ecall* and σ ∈ *E* ⋆ *call* by induction on the length of paths. Let *n* ∈ **N** and assume that the claim is true for all π ′ with |π ′ | < *n*. Let π<sup>0</sup> ∈ *PathsG*(*s*,*t*), *ecall* ∈ *Ecall*, σ ∈ *E* ⋆ *call* such that <sup>|</sup>π0<sup>|</sup> = *<sup>n</sup>* and *cs*∞(π0) = *ecall* · σ.

First we observe that *n* > 0, since *cs*∞(ϵ) = ϵ. So π<sup>0</sup> = π ·*e* for some *e* ∈ *E*. We make a case distinction on *e*:

*e* ∈ *Eintra*: Then *cs*∞(π · *e*) = *cs*∞(π). Hence, we can apply the induction hypothesis on π and get a decomposition π = π ′ · *ecall* · π ′′ where *cs*∞(π ′ ) = σ and π ′′ is a same-level path. Hence π ·*e* = π ′ ·*ecall* · (π ′′ ·*e*) is a decomposition of π · *e* with the desired properties: π ′′ · *e* is a same-level path, since *e* ∈ *Eintra*.

*e* ∈ *Ecall*: Then *cs*∞(π · *e*) = *e* · *cs*∞(π) = *ecall* · *cs*∞(π), so *e* = *ecall* and <sup>σ</sup> <sup>=</sup> *cs*∞(π) <sup>≠</sup> ▼∞. Now we make a case distinction on whether *cs*∞(π) is empty or not.

• If *cs*∞(π) is not empty, then *cs*∞(π) = *e* ′ · σ ′ for some *e* ′ ∈ *Ecall* and σ ′ ∈ *E* ⋆ *call*. We apply the induction hypothesis on <sup>π</sup> and obtain <sup>π</sup> ′ with *cs*∞(π ′ ) = σ ′ and a same-level path π ′′ such that

$$
\pi = \pi' \cdot e' \cdot \pi''
$$

Then

$$(6.14)\qquad\pi \cdot e = \pi' \cdot e' \cdot \pi'' \\ \cdot e = (\pi' \cdot e' \cdot \pi'') \cdot e \cdot \epsilon$$

ϵ is a same-level path, so in order to show that 6.14 is a decomposition of π ·*e* with the desired properties, we only need to show *cs*∞(π ′ ·*e* ′ · π ′′) = *cs*∞(π) = *e* ′ · σ ′ . But this can be seen as follows:

$$\begin{aligned} c\varsigma\_{\infty}(\pi' \cdot e' \cdot \pi'') &= c\varsigma\_{\infty}(\pi' \cdot e') & \{\pi'' \in SL, \text{Lemma 6.19 }\} \\ &= e' \cdot c\varsigma\_{\infty}(\pi') & \{\text{by definition of } c\varsigma\_{\infty} \, e' \in E\_{\text{call}}\} \\ &= e' \cdot \sigma' & \{\text{c}\varsigma\_{\infty}(\pi') = \sigma'\} \end{aligned}$$

• If *cs*∞(π) = ϵ, then σ = ϵ = *cs*∞(π) so that π · *e* = π · *e* · ϵ is a decomposition with the desired properties.

*e* ∈ *Eret*: We have *ecall* · σ = *cs*∞(π·*e*) = *pop*∞(*cs*∞(π)), so that by definition *cs*∞(π) = *e* ′ · (*ecall* · σ) for some *e* ′ ∈ *Ecall* with (*e* ′ ,*e*) ∈ Φ. We apply the induction hypothesis on π (|π| < *n*) and get π ′ , π ′′ such that *cs*∞(π ′ ) = *ecall* · σ, π ′′ is a same-level path and

$$
\pi = \pi' \cdot e' \cdot \pi''
$$

Next we apply the induction hypothesis on π ′ (|π ′ | < *n*) and get π ′ 1 , π ′ 2 such that *cs*∞(π ′ 1 ) = σ, π ′ 2 is a same-level path and

$$
\pi' = \pi\_1' \cdot e\_{call} \cdot \pi\_2'
$$

That means π ·*e* can be decomposed into

$$\text{(6.15)}\qquad \pi \cdot e = \pi\_1' \cdot e\_{\text{call}} \cdot \pi\_2' \cdot e' \cdot \pi' \cdot e = \pi\_1' \cdot e\_{\text{call}} \cdot (\pi\_2' \cdot e' \cdot \pi'' \cdot e)$$

Since (*e* ′ ,*e*) ∈ Φ and π ′′ is a same-level path, *e* ′ · π ′′ · *e* is a same-level path. Since π ′ 2 is also a same-level path, π ′ 2 · *e* ′ · π ′′ · *e* is a same-level path. Furthermore, *cs*∞(π ′ 1 ) = σ. In summary, 6.15 is a decomposition of π · *e* with the desired properties.

□

The second property that will be needed in my proof of Lemma 6.24 is the converse of Lemma 6.20: Empty stacks can only be left behind by ascending paths. In particular, even though *cs*<sup>∞</sup> allows returning to arbitrary call sites in the face of the empty stack, apart from this exception, it still requires that paths exhibit valid stack usage.

**Lemma 6.23.** *If* π ∈ *PathsG*(*s*, *t*) *with cs*∞(π) = ϵ∞*, then* π ∈ *ASC*(*s*, *t*)*.*

*Proof.* I prove the result by induction on the length of paths. The induction hypothesis is

$$\begin{aligned} \text{(IH)} \qquad \forall \pi \in \text{Paths}\_{\text{G}}. \ \forall s, t \in \text{N}. \ |\pi| < n \land \pi \in \text{Paths}\_{\text{G}}(s, t) \land cs\_{\infty}(\pi) = \epsilon \\ \implies \pi \in \text{ASC}(s, t) \end{aligned}$$

Let π ∈ *Paths<sup>G</sup>* with |π| = *n* and *s*,*t* ∈ *N* such that π ∈ *PathsG*(*s*,*t*). Furthermore, assume *cs*∞(π) = ϵ. We need to show π ∈ *ASC*(*s*,*t*). For this, we distinguish two cases:

*n* = 0: Then π must be empty and is hence ascending, so that the claim holds in this case.

*n* > 0: Then π = π ′ ·*e* and π ′ ∈ *PathsG*(*s*,*t* ′ ) for some *t* ′ ∈ *N*, *e* ∈ *E*, *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>*. Since *cs*∞(π) = ϵ, it cannot be the case that *e* ∈ *Ecall*, so we only need to distinguish the cases *e* ∈ *Eintra* and *e* ∈ *Eret*:

*e* ∈ *Eintra*: Then *cs*∞(π) = *cs*∞(π ′ ), so that *cs*∞(π ′ ) = ϵ. Hence we may apply (IH) to π ′ and conclude π ′ ∈ *ASC*(*s*,*t* ′ ). Thus π = π ′ ·*e* ∈ *ASC*(*s*,*t*) by asc-asc.

*e* ∈ *Eret*: By definition, *cs*∞(π) = ϵ implies that either *cs*∞(π ′ ) = ϵ or that *cs*∞(π ′ ) = *ecall* for some *ecall* ∈ *Ecall* with (*ecall*,*e*) ∈ Φ.

In the former case, we may reason just as for the case "*e* ∈ *Eintra*": Applying (IH), we obtainπ ′ ∈ *ASC*(*s*, *t* ′ ) and appending a return edge to an ascending path results in an ascending path.

In the latter case, we apply Lemma 6.22 and decompose π ′ into π ′ = π ′′ · *ecall* · π ′′′ such that *cs*∞(π ′′) = ϵ and π ′′′ is a same-level path. Then we apply (IH) to π ′′ and conclude that π ′′ is an ascending path. Furthermore, since (*ecall*,*e*) ∈ Φ and π ′′′ is same-level, π = π ′′ ·*ecall* · π ′′′ ·*e* is ascending by asc-seq*sl*. With <sup>π</sup> <sup>∈</sup> *PathsG*(*s*, *<sup>t</sup>*), we get <sup>π</sup> <sup>∈</sup> *ASC*(*s*, *<sup>t</sup>*) by definition.

**Lemma 6.24.** *If* <sup>π</sup> <sup>∈</sup> *PathsG*(*s*, *<sup>t</sup>*) *with cs*∞(π) <sup>≠</sup> ▼∞*, then* <sup>π</sup> *is a valid path.*

□

*Proof.* If *cs*∞(π) <sup>≠</sup> ▼∞, then *cs*∞(π) <sup>∈</sup> *<sup>E</sup>* ⋆ *call*, so we prove the claim by induction on the length of *cs*∞(π). So let *n* ∈ **N**. The induction hypothesis is

$$\text{(IH)}\qquad \forall \pi \in \text{Paths}\_{\text{G}}. |\text{cs}\_{\infty}(\pi)| < n \land \text{cs}\_{\infty}(\pi) \neq \mathbf{v}\_{\infty} \implies \pi \in V\mathbf{P}\_{\pi}$$

Let <sup>π</sup> <sup>∈</sup> *PathsG*(*s*,*t*) with *cs*∞(π) <sup>≠</sup> ▼∞, <sup>|</sup>*cs*∞(π)<sup>|</sup> <sup>=</sup> *<sup>n</sup>*. We distinguish two cases:

*n* = 0: Then *cs*∞(π) = ϵ. By Lemma 6.23, π ∈ *ASC* ⊆ *VP*.

*n* > 0: Then *cs*∞(π) = *e* · σ for some *e* ∈ *Ecall*. By Lemma 6.22, π can be decomposed into π = π ′ ·*e*· π ′′ where *cs*∞(π ′ ) = σ and π ′′ is a same-level path. In particular, *cs*∞(π ′ ) <sup>≠</sup> ▼<sup>∞</sup> and <sup>|</sup>*cs*∞(<sup>π</sup> ′ )| < *n*, so we may apply (IH) and get that π ′ ∈ *VP*. Furthermore, since *e* ∈ *Ecall* and π ′′ and is same-level, *e*· π ′′ is descending by Theorem 5.29. Furthermore, *e*· π ′′ is a path because it is a sub-sequence of π and π is a path. Hence, *e*· π ′′ ∈ *DESC* by Theorem 5.37.

Thus, π is the concatenation of a valid sequence and a descending sequence and therefore valid by Theorem 5.30. With π ∈ *PathsG*, we get π ∈ *VP* by definition.

□

# **6.3.2.3 Summary**

After I have proven the two subset relations, I formally state the main result of this subsection, namely that *AP*S<sup>∞</sup> coincides with *VP*, for later reference.

**Theorem 6.25.** *The valid paths are exactly the* S*-acceptable paths for* S = S∞*:*

$$AP\_{\mathcal{S}\_{\infty}} = VP$$

*Proof.* "⊆" is shown by Lemma 6.24 and "⊇" by Theorem 6.21. □

As a corollary, I conclude that for distributive frameworks, the least solution Constraint System 6.5 w.r.t. S<sup>∞</sup> coincides with *MOVP* and hence provides the same result as the functional approach. Thus, I can confirm the corresponding result, that Sharir and Pnueli [154, Theorem 7-4.6] showed for classical data-flow analyses on interprocedural graphs in my more general setting.

# **6.3.3 Stack Abstractions**

In the last subsections, we have seen that Constraint System 6.5 correctly and precisely describes the data-flows along the valid paths in the case that Constraint System 6.5 is based on the stack space S<sup>∞</sup> of unbounded stacks. In this sense, the unbounded call-string approach *coincides* with the functional approach.

In this and the next subsection, I examine general stack spaces. Specifically, I present an approach for providing correctness results for general stack spaces. Therefore, this subsection introduces the concept of *stack abstractions*. A stack abstraction is a function between stack spaces that preserves enough structure to maintain acceptability of paths. My main result is that if a stack space S is related to a stack space S # via a stack abstraction, then all S-acceptable paths are S # -acceptable. This enables to conclude *VP*-correctness of MO*AP*S# from *VP*-correctness of MO*AP*S.

In subsection 6.3.4, I will apply this result to obtain a *VP*-correctness result for MO*AP*S*<sup>k</sup>* .

Stack abstractions make use of *galois connections*, a concept that is wellknown in mathematics and also static program analysis.

A galois connection can be seen as a generalization of inverse functions for partial orders. Definition 6.26 can be found e.g. in [130, Section 4.3] or [50, 7.23, p. 155].

**Definition 6.26.** *Let* (*L*,≤*L*) *and* (*M*,≤*M*) *be two partially ordered sets and*

$$\begin{aligned} \alpha: L &\to M \\ \gamma: M &\to L \end{aligned}$$

*be two monotone functions. Then* (*L*, α, γ, *M*) *is called a* galois connection *if* α ◦ γ ≤*<sup>M</sup> id<sup>M</sup> and id<sup>L</sup>* ≤*<sup>L</sup>* γ ◦ α*.*

Lemma 6.27 gives a simple characterization of galois connections, that I will make use of occasionally in the following.

**Lemma 6.27.** (*L*, *M*, α, γ) *is a galois connection between the partially ordered sets* (*L*, ≤*L*) *and* (*M*, ≤*M*) *if and only if* α : *L* → *M and* γ : *M* → *M are monotone functions such that*

$$\forall l \in L. \; \forall m \in M. \; \alpha(l) \leq\_M m \iff l \leq\_L \; \gamma(m)$$

*Proof.* See [130, Proposition 4.20]. Note that [130] actually assumes that *L* and *M* are complete lattices, which is not necessary for the proof to be valid. □

Next, Definition 6.28 introduces stack abstractions, the main concept of this subsection.

**Definition 6.28.** *A stack abstraction between two stack spaces*

(*S*,≤, ϵ, *top*, *push*, *pop*)

*and*

$$(S^\#, \leq^\#, \epsilon^\#, top^\#, push^\#, pop^\#)$$

*is a pair* (α, γ) *of functions*

$$\begin{aligned} \alpha: & \mathbf{S}\_{\mathbf{V}} \to \mathbf{S}\_{\mathbf{V}^{\#}}^{\#} \\ \gamma: & \mathbf{S}\_{\mathbf{V}^{\#}}^{\#} \to \mathbf{S} \mathbf{v} \end{aligned}$$

*such that*

$$\text{(Gal)}\qquad\qquad\qquad\qquad\text{(S}\_{\mathbf{v}\prime}\alpha\,\gamma\,\text{.}S\_{\mathbf{v}\#}^{\#}\text{)}\text{ is a goalis connection.}$$

$$\text{(Bot)}\qquad\qquad\qquad\qquad\qquad\qquad\qquad\text{(}\mathbf{v}\text{)}\text{ = }\mathbf{v}\text{)}$$

$$\text{(Eps)}\qquad\qquad\forall\sigma^\#\in S^\#.\,\gamma(\sigma^\#)=\epsilon\implies\sigma^\#=\epsilon^\#$$

$$\text{(PushHom)}\qquad\forall\sigma^\#\in S^\#.\,\alpha(push(e,\gamma(\sigma^\#)))\le\,^\#push^\#(e,\sigma^\#)$$

$$\text{(PopHom)}\qquad\forall\sigma^\#\in\mathcal{S}^\#-\langle\epsilon^\#\rangle.\newline\alpha(pop(\gamma(\sigma^\#)))\leq^\#pop^\#(\sigma^\#).$$

$$\text{(TopHom)}\qquad\forall\sigma^\#\in\mathcal{S}^\#-\{\epsilon^\#\}.top(\sigma^\#)=\text{top}(\gamma(\sigma^\#))$$

Before I show an example of Definition 6.28, I want to discuss a bit the intuition behind stack abstraction and their properties.

For this, consider a stack abstraction (*S*, α, γ, *S* # ) between two stack spaces S = (*S*,≤, ϵ, *top*, *push*, *pop*) and S # = (*S*,≤, <sup>ϵ</sup>, *top*, *push*, *pop*).

A stack abstraction is supposed to "abstract away" from unnecessary details in a stack space while still preserving enough structure to make meaningful statements. For example, it shall enable to conclude from the validness of *cs*(π), that *cs*# (π) is also valid, i.e. it shall be possible to prove *cs*(π) <sup>≠</sup> ▼ <sup>=</sup><sup>⇒</sup> *cs*# (π) ≠ ▼.

First of all, (Gal) provides a formalization of the intuitive notion of *abstraction*. The function α, which is in the program analysis context also called *abstraction function*, can be thought of "abstracting away the details" from a concrete stack from S, whereas γ, also called *concretization function*, maps an abstract stack to a corresponding concrete stack.

Intuitively, we expect two properties of abstractions and their concretizations. For one, if we abstract a stack and then concretize again, then we get a concrete stack that provides at most as much as the original stack. In particular, this stack should be comparable to the original stack. Secondly, if we concretize an abstract stack and then abstract it, then we get a stack that provides at least as much information as the original abstract stack.

This is exactly what is achieved by the galois connection provided by (Gal). For every σ ∈ *S*, there is a corresponding stack α(σ) ∈ *S* # , which can be thought of as "at most as detailed" as σ. This is to be understood in the following sense: If we apply γ to α(σ), we arrive at a stack γ(α(σ)) with σ ≤ γ(α(σ)).

Conversely, for every stack σ # <sup>∈</sup> *<sup>S</sup>* # , there is a corresponding stack γ(σ # ) ∈ S in *S*, which can be though of as "at least as detailed". This is to be understood in the following sense: If we apply α to γ(σ # ), we arrive at a stack α(γ(σ # )) ≤ σ # .

Next, I consider the other properties. (Gal) in connection with (Bot) and (Eps) is strong enough to fully preserve ϵ and ▼, as Remark 6.29 shows.

**Remark 6.29.** *For every stack abstraction, we have*

$$(6.16)\tag{6.16}$$

$$\text{(6.17)}\tag{6.17}$$

$$(6.18)\qquad\qquad\qquad\qquad\qquad\alpha(\mathfrak{x})=\mathfrak{v}^{\#}\iff\mathfrak{x}=\mathfrak{v}$$

*Proof.* First, we consider (6.16) and (6.17). By (Gal), we have ϵ ≤ γ(α(ϵ)). Since ϵ is the greatest element of *S* with respect to ≤, this means that γ(α(ϵ)) = ϵ. Hence, α(ϵ) = ϵ # by (Eps). Again by application of (Gal), we have ϵ ≤ γ(α(ϵ)) = γ(ϵ # ), so γ(ϵ # ) = ϵ by ≤-maximality of ϵ.

Finally, we prove (6.18). For all σ # <sup>∈</sup> *<sup>S</sup>* # we have ▼ <sup>≤</sup> <sup>γ</sup>(<sup>σ</sup> # ) since ▼ is the least element of *<sup>S</sup>*▼. By (Gal) and Lemma 6.27, this means that <sup>α</sup>(▼) <sup>≤</sup> <sup>σ</sup> # for all σ # <sup>∈</sup> *<sup>S</sup>* # , which implies α(▼) = ▼ # . Conversely, for σ ∈ *S*▼ with α(σ) = ▼ # by (Gal) and (Bot) we have <sup>σ</sup> <sup>≤</sup> <sup>γ</sup>(α(σ)) = <sup>γ</sup>(▼ # ) = ▼, which implies σ = ▼. □

The other properties in Definition 6.28 ensure that α and γ are well-behaved with respect to the stack operations. Property (TopHom) ensures that the top elements of a stack and its abstract versions are the same. The other two properties consider two different ways to perform a push or pop operation on abstract stacks: The first way applies the respective abstract operation, whereas the second way first concretizes the stack, applies the concrete operation and then abstracts the result. The two properties (PushHom) and (PopHom) state that the first way must provide as most as much information as the second way.

In the following, I give an important example of a family of stack abstractions. To support the uniform presentation of the following example, I define *A* ≤∞ *de f* = *A* <sup>⋆</sup> for an alphabet *A*, σ ≤∞ *de f* = σ for every σ ∈ *A* <sup>⋆</sup> and ∞ − 1 *de f* = ∞. Furthermore, I assume that the natural order ≤ on **N** is extended to **N**<sup>∞</sup> *de f* = **N** ∪ {∞} by defining *x* ≤ ∞ for every *x* ∈ **N**∞.

**Example 6.30.** *For k*, *l* ∈ **N**∞*, l* ≤ *k, consider the two stack spaces* S*<sup>k</sup>* = (*E* ≤*k call*,≤*<sup>k</sup>* , ϵ*<sup>k</sup>* , *push<sup>k</sup>* , *pop<sup>k</sup>* ,*top<sup>k</sup>* ) *and* S*<sup>l</sup>* = (*E* ≤*l call*,≤*<sup>l</sup>* , ϵ*l* , *push<sup>l</sup>* , *pop<sup>l</sup>* ,*top<sup>l</sup>* )*. Define the functions* α*k*,*<sup>l</sup> and* γ*k*,*<sup>l</sup> by*

$$\begin{aligned} \alpha\_{k,l}(\sigma) &= \sigma^{\le l} \\ \alpha\_{k,l}(\mathbf{v}\_k) &= \mathbf{v}\_l \\ \mathsf{y}\_{k,l}(\sigma) &= \sigma \\ \mathsf{y}\_{k,l}(\mathbf{v}\_l) &= \mathbf{v}\_k \end{aligned}$$

*Then* (S*<sup>k</sup>* , α*k*,*<sup>l</sup>* , γ*k*,*<sup>l</sup>* , S*<sup>l</sup>* ) *is a stack abstraction.*

*Proof.* (Gal) (S*<sup>k</sup>* , α*k*,*<sup>l</sup>* , γ*k*,*<sup>l</sup>* , S*<sup>l</sup>* ) is a galois connection.

• For σ ∈ *E* ≤*k call* we have

$$
\sigma \leq\_k \sigma^{\leq l} \qquad\qquad\qquad \{\ \sigma^{\leq l} \text{ is a prefix of } \sigma\}
$$

}

}

}


(Eps) This is clear since γ*k*,*<sup>l</sup>* is the identity function.

(PushHom) Let σ ∈ *E* ≤*l call*. Then we have

$$\begin{aligned} \alpha\_{k,l}(push\_k(e, \gamma\_{k,l}(\sigma))) &= \alpha\_{k,l}(push\_k(e, \sigma)) & \{ \text{ definition of } \gamma\_{k,l} \} \\ &= \alpha\_{k,l}((e \cdot \sigma)^{\le k}) & \{ \text{ definition of } push\_k \} \\ &= ((e \cdot \sigma)^{\le k})^{\le l} & \{ \text{ definition of } \alpha\_{k,l} \} \\ &= (e \cdot \sigma)^{\le l} & \{ l \le k \} \\ &=push\_l(e, \sigma) & \{ \text{ definition of } push\_l \} \end{aligned}$$

(PopHom) The claim is trivial for *l* = 0, so we may assume *l* > 0. For *e*· σ ∈ *E* ≤*l call* \ {ϵ}, we have

$$\begin{aligned} \alpha\_{k,l}(pop\_k(\gamma\_{k,l}(e\cdot\sigma))) &= \alpha\_{k,l}(pop\_k(e\cdot\sigma)) & \{ \text{ definition of } \gamma\_{k,l} \} \\ &= \alpha\_{k,l}(\sigma) & \{ \text{ definition of } pop\_k \} \\ &= \sigma^{\leq l} & \{ \text{ definition of } \alpha\_{k,l} \} \\ &= \sigma & \{ \text{ }\sigma \in \mathop{\mathbb{E}\_{call}^{\le l-1}} \subseteq \mathop{\mathbb{E}\_{call}^{\le l}} \} \\ &= pop\_l(e\cdot\sigma) & \{ \text{ definition of } pop\_l\} \end{aligned}$$

(TopHom) Again, the claim is trivial for *l* = 0, so we only consider *l* > 0. For *e* · σ ∈ *E* ≤*l call* \ {ϵ}, we have <sup>γ</sup>*k*,*<sup>l</sup>* (σ) = σ and therefore *top<sup>k</sup>* (γ*k*,*<sup>l</sup>* (σ)) = *top<sup>l</sup>* (σ).

□

After I have introduced stack abstractions and gave an example, I provide the main result of this subsection in Theorem 6.31. If S and S # are two stack spaces and α and γ form a stack abstraction between S and S # , then every S-acceptable path is also S # -acceptable.

The proof of Theorem 6.31 is an easy consequence of the following key property, which I will prove later.

**Lemma 6.32.** *Let* (*S*, α, γ, *S* # ) *be a stack abstraction between the stack spaces* (*S*,≤, ϵ,*top*, *push*, *pop*) *and* (*S* # ,≤ # , ϵ # ,*top*# , *push*# , *pop*# ) *over Ecall. Then the following statement is true for any* π ∈ *Paths*(*G*)*:*

$$(6.21)\tag{6.21}$$

$$\alpha(\operatorname{cs}(\pi)) \le c\operatorname{s}^\#(\pi).$$

Lemma 6.32 can be used to show the main result of this section.

**Theorem 6.31.** *Let*

$$\mathcal{S} = (E\_{call}, \mathcal{S}\_\prime \le \epsilon, top, push,pop)$$

*and*

$$\mathbf{S}^{\#} = (E\_{call}, \mathbf{S}^{\#}, \leq^{\#}, \epsilon^{\#}, top^{\#}, push^{\#}, pop^{\#})$$

*be two stack spaces over Ecall. Furthermore, let* (*S*, α, γ, *S* # ) *be a stack abstraction between* S *and* S # *. Then*

$$\text{(6.19)}\qquad\qquad\forall\text{s}\;t\in\text{N}.\;AP\_{\mathcal{S}}(\text{s},t)\subseteq AP\_{\mathcal{S}^\#}(\text{s},t)$$

$$\text{(6.20)}\tag{6.20}$$

$$\text{(6.20)}\tag{6.26}$$

*Proof.* First, we show (6.19). Let <sup>π</sup> <sup>∈</sup> *AP*S(*s*,*t*). Then *cs*(π) <sup>≠</sup> ▼ which by Remark 6.29 implies α(*cs*(π)) ≠ ▼ # . By Lemma 6.32 it follows that <sup>α</sup>(*cs*(π)) <sup>≤</sup> *cs*# (π), which entails *cs*# (π) ≠ ▼ # . Thus <sup>π</sup> <sup>∈</sup> *AP*S# (*s*, *<sup>t</sup>*).

With (6.19), we see that MO*AP*S# joins over more paths than MO*AP*S. This shows (6.20). □

All that is left to do for this subsection is to prove Lemma 6.32.

**Lemma 6.32.** *Let* (*S*, α, γ, *S* # ) *be a stack abstraction between the stack spaces* (*S*,≤, ϵ,*top*, *push*, *pop*) *and* (*S* # ,≤ # , ϵ # ,*top*# , *push*# , *pop*# ) *over Ecall. Then the following statement is true for any* π ∈ *Paths*(*G*)*:*

$$(6.21)\tag{6.21}$$

$$\alpha(\operatorname{cs}(\pi)) \le c\operatorname{s}^\#(\pi)$$

*Proof.* First, we observe that

$$(6.22)\qquad a(\csc(\pi)) \le c s^\#(\pi) \iff c s(\pi) \le \gamma(\alpha^\#(\pi))$$

This follows from (Gal) and Lemma 6.27. Next we show

<sup>∀</sup><sup>π</sup> <sup>∈</sup> *Paths*(*G*). <sup>α</sup>(*cs*(π)) <sup>≤</sup> *cs*# (π)

by induction on the length of π. So let *n* ∈ **N**, π ∈ *Paths*(*G*) with |π| = *n*. The induction hypothesis is

$$(\mathbf{IH}) \qquad \forall \pi' \in \text{Paths}(\mathbf{G}). |\pi'| < \pi \implies \alpha(\text{cs}(\pi)) \le \text{cs}^\#(\pi).$$

By (6.22), this is equivalent to

$$(\mathbf{H} \mathbf{f}') \qquad \forall \pi' \in \text{Paths}(\mathbf{G}). |\pi'| < \pi \implies cs(\pi') \le \gamma(cs^\#(\pi'))$$

Now we make a case distinction on *n*.

*n* = 0: Then π = ϵ. Evaluation of both sides yields that they are equal: α(*cs*(π)) = α(ϵ) = ϵ # and *cs*# (π) = ϵ # .

*n* > 0: Then π = π ′ ·*e* with |π ′ | = *n* − 1 and *e* ∈ *Eintra* ∪ *Ecall* ∪ *Eret*.

First we consider the case that *cs*(π ′ ) = ▼. Then *cs*(π) = ▼ and hence, by (6.18), α(*cs*(π)) = ▼ # <sup>≤</sup> *cs*# (π).

Next assume *cs*# (π ′ ) = ▼ # . Then (**IH**) implies α(*cs*(π ′ )) <sup>≤</sup> ▼ # . Hence, α(*cs*(π ′ )) = ▼ # , and this implies *cs*(π ′ ) = ▼ by (6.18). As in the previous case, we can conclude that <sup>α</sup>(*cs*(π)) <sup>≤</sup> *cs*# (π) also holds in this case.

Now we can assume *cs*(π ′ ) ≠ ▼ and *cs*# (π ′ ) ≠ ▼ # and make a case distinction on *e*:

*e* ∈ *Eintra*:


*e* ∈ *Ecall*:

$$\begin{aligned} &\alpha(\text{cs}(\pi)) \\ &=\alpha(push(e,\text{cs}(\pi'))) & \text{(by definition, }e \in E\_{call}) \\ &\le\alpha(push(e,\text{y}(\text{cs}^\#(\pi')))) & \text{( (\textbf{IH'}),mon. of }push(e,\text{ })\text{ and }a \text{ )) \\ &\lepush^\#(e,\text{cs}^\#(\pi')) & \text{( (\textbf{Push}\textbf{Hom}) )} \\ &=\text{cs}^\#(\pi) & \text{(by definition, }e \in E\_{call}) \end{aligned}$$

case *e* ∈ *Eret*: By definition of *cs*, we distinguish two cases:

*cs*(π ′ ) = <sup>ϵ</sup>: By (**IH'**), this implies <sup>ϵ</sup> <sup>≤</sup> <sup>γ</sup>(*cs*# (π ′ )). It follows that γ(*cs*# (π ′ )) = ϵ. Hence *cs*# (π ′ ) = ϵ # and, by definition of *cs*# , *cs*# (π) = ϵ # . case *cs*(π ′ ) ≠ ϵ By assumption, we have *cs*(π ′ ) <sup>∉</sup> {▼, <sup>ϵ</sup>}.

First consider the case that (*top*(*cs*(π ′ )),*e*) ∉ Φ. Then *cs*(π) = ▼ by definition of *cs*. Hence, we can justify (6.21) as follows:

$$\begin{aligned} \alpha(\operatorname{cs}(\pi)) &= \alpha(\mathbf{v}) & \{ \text{see above } \} \\ &= \mathsf{v}^{\#} & \{ \text{(6.18) } \} \\ &\leq cs^{\#}(\pi). & \{ cs^{\#}(\pi) \in \mathcal{S}^{\#}, \mathsf{v}^{\#} \text{ is the least element of } \mathcal{S}^{\#} \} \end{aligned}$$

Now we may assume that *cs*(π ′ ) <sup>∉</sup> {▼, <sup>ϵ</sup>} ∧ (*top*(*cs*(<sup>π</sup> ′ )),*e*) ∈ Φ. Consider the case that *cs*# (π ′ ) = ϵ # . Then *cs*# (π) = ϵ # , so that <sup>α</sup>(*cs*(π)) <sup>≤</sup> *cs*# (π) holds.

Now assume *cs*# (π ′ ) ≠ ϵ # . Together with *cs*# (π ′ ) ≠ ▼ # , we have *cs*# (π ′ ) ∉ {▼ # , ϵ # }. By (**IH'**), we have *cs*(π ′ ) <sup>≤</sup> <sup>γ</sup>(*cs*# (π ′ )). Since *cs*# (π ′ ) ≠ ϵ # , it must be γ(*cs*# (π ′ )) ≠ ϵ (by (Eps)).

Because *cs*# (π ′ ) ≠ ϵ # , *cs*(π ′ ) ≠ ϵ and γ(*cs*# (π ′ )) ≠ ϵ, we can conclude from *cs*(π ′ ) <sup>≤</sup> <sup>γ</sup>(*cs*# (π ′ )) that

$$top(cs(\pi')) = top(\gamma(cs^\#(\pi'))) = top^\#(cs^\#(\pi'))$$

by (TopVsLe) and (TopHom)). Together with (*top*(*cs*(π)),*e*) ∈ Φ, it follows that (*top*(*cs*# (π)),*e*) <sup>∈</sup> <sup>Φ</sup>, so that *cs*# (π) = *pop*# (*cs*# (π ′ )) by definition of *cs*# . Now we can conclude

α(*cs*(π)) = α(*pop*(*cs*(π ′ <sup>≤</sup> <sup>α</sup>(*pop*(γ(*cs*# (π ′ <sup>≤</sup> *pop*# (*cs*# (π ′ = *cs*#

))) { definition of *cs*, assumptions } )))) { **(IH')**, mon. of *pop* and α } )) { **(PopHom)** } (π) { definition of *cs*# , see above }

□

# **6.3.4 Effective and Correct Approximations of the Unbounded Call-String Approach**

In subsection 6.3.2, we saw that solutions of Constraint System 6.5 w.r.t. S<sup>∞</sup> enjoy the same correctness and precision results as the functional approaches. Unfortunately, Constraint System 6.5 is not *e*ff*ective*. For one, Constraint System 6.5 w.r.t. S<sup>∞</sup> is infinite. Secondly, the complete lattice that forms the solution space of Constraint System 6.5 is

$$N \times N \times S^{\infty} \to F\_{\mathbb{E}\_{\Delta'}}$$

which does not satisfy the ascending chain condition. Particularly this second fact makes it impossible to use algorithms such as Algorithm 8, which rely on the ascending chain condition for termination.

However, in Example 6.13 I showed the family of (S*<sup>k</sup>* )*k*∈**<sup>N</sup>** of stack spaces that lead to finite versions of Constraint System 6.5 and to solution spaces that satisfy the ascending chain condition. Hence algorithms like Algorithm 8 are applicable to Constraint System 6.5 w.r.t. S*<sup>k</sup>* . The idea of S*<sup>k</sup>* , which is at least since the work of Sharir and Pnueli [154] well-known in the literature, is to only consider the *k* topmost stack elements, so that all stacks are *k*-bounded. As Example 6.30 showed, S<sup>∞</sup> and S*<sup>k</sup>* are connected via a stack abstraction, where the abstraction function projects a stack to the *k* topmost items.

Consequently, the main result Theorem 6.31 from the last subsection enables me to transfer the correctness result for S<sup>∞</sup> to S*<sup>k</sup>* .

**Corollary 6.33.** *1. Let* S *be a stack space such that there is a stack abstraction between* S<sup>∞</sup> *and* S*. Then the following statements hold:*

$$\text{(6.23)}\qquad\qquad\forall \text{s}, t \in \text{N}. \text{ } VP(\text{s}, t) \subseteq AP\_{\mathcal{S}}(\text{s}, t)$$

$$(6.24)\qquad\forall s, t\_\prime \in \mathcal{N}.\ \text{MO} \\ VP(s, t) \le \text{MO} \\ AP(s, t)$$

*2. Let k*, *l* ∈ **N** ∪ {∞} *with l* ≤ *k. Then the following statements hold:*

$$\text{(6.25)}\tag{6.25} \qquad \qquad \qquad \text{MOVP} \leq \text{MOAP}\_{\mathcal{S}\_{kl}}$$

$$\text{(6.26)}\tag{6.26} \\ \text{W} \text{O} \\ \text{OP}\_{\text{S}\_k} \leq \text{MO} \\ \text{AP}\_{\text{S}\_l}$$

*Proof.* 1. (6.23) follows from Theorem 6.31 and Theorem 6.25. The second statement (6.24) is an easy consequence of (6.23).

2. In Example 6.30 we have seen that (S*<sup>k</sup>* , α*k*,*<sup>l</sup>* , γ*k*,*<sup>l</sup>* , S*<sup>l</sup>* ) is a stack abstraction if *k*, *l* ∈ **N** ∪ {∞} with *l* ≤ *k*. This means that we can apply (6.23) and (6.24) to obtain the desired statements. □

Together with Theorem 7.34, this entails that solutions of Constraint System 6.5 w.r.t. S*<sup>k</sup>* are *VP*-correct. Hence, using *k*-bounded stacks in the call-strings approach is indeed correct. This is a generalization of the corresponding result that was obtained by Sharir and Pnueli for classical data-flow analyses on interprocedural control-flow graph [154, Theorem 7- 6.5]. For one, my assumptions about the given graph and its valid paths are weaker (as I already explained in chapter 5). Moreover, there may be other stack spaces than S*<sup>k</sup>* that can be obtained from S<sup>∞</sup> via a stack abstraction and lead to an effective version of Constraint System 6.5. The theory that I developed in this section provides a way to prove *VP*-correctness for all these stack spaces.

*I have su*ff*ered for this book; now it's your turn.* <sup>G</sup>eorge <sup>H</sup>arrison **7**

# **A Common Generalization of Interprocedural Data-Flow Analysis and Slicing**

After chapter 6 introduced constraint systems for the functional and the callstring approach to interprocedural data-flow analysis for interprocedural graphs, this chapter is concerned with the effective solution of meaningful portions of these constraint systems in order to compute correct and possibly precise approximations to MO*VP*.

In chapter 3, I compared slicers such as Algorithm 5 and Algorithm 6 with algorithms to solve data-flow problems, such as Algorithm 3. My conclusion was that although both algorithm groups are worklist-based, they differ in what I coined as *worklist policy*. While the slicers usually assume that worklist items just have been updated, update the values of their successors and put them on the worklist if their value has changed, Algorithm 3 acts on the assumption that worklist items have to be updated, updates them using all their predecessors and then puts all successors on the worklist. This difference becomes obvious if one considers how the two approaches handle reachability. While slicers are reachability analyses and naturally explore graphs iteratively using a reachability frontier, Algorithm 3 is geared towards situations where the goal is to compute values for every variable and the usual assumption is that every variable is reachable. Hence, Algorithm 3 proceeds rather clumsily if the analysis information to be computed is reachability itself: It uses the rule "this variable is reachable if one of its predecessors is reachable" and not – like it is natural – "if this variable is reachable, then all its successors are reachable". It *is possible* to use Algorithm 3 for data-flow problems where reachability is part of the analysis result, even without modifications. However, this necessitates modifications of the data-flow problems. Moreover, the initial loop, which iterates through *all* variables cannot be eliminated – its task is to assess that all unreachable variables are indeed unreachable because all their predecessors are. I take a different path to integrate reachability and data-flow analysis and modify Algorithm 3 in such a way that it is geared towards reachability and computes the actual analysis result for reachable variables as a side-product. Particularly, Algorithm 8, my modified version of Algorithm 3, does not need to visit every variable at least once but only the reachable variables.

The presentation and analysis of Algorithm 8 will be the subject of section 7.1.

Moreover, section 7.1 considers reachability and partial solution on the level of constraint systems. This is particularly necessary to be able to make statements about the properties of Algorithm 8. Moreover, I investigate under which circumstances the least solution of the restricted constraint system coincides with the least solution of the full constraint system on the reachable variables. This can also be seen as *slicing of constraint systems*. It turns out that the modified version of Algorithm 3 is indeed capable of computing such partial solutions.

Subsequently, I apply the theory developed in section 7.1 to the constraint systems given in chapter 6 in order to obtain algorithms that compute correct data-flow solutions on forward slices. Section 7.2 lays out the setup and gives an overview of the general scheme. Then, section 7.3 and section 7.4 consider the functional approach and the call-string approach, respectively.

# **7.1 Integrating Reachability Into the Solution Process**

As was already mentioned at the very beginning of this chapter, the goal of this section is to present Algorithm 8, a variant of Algorithm 3 that additionally takes a set *X*<sup>0</sup> of initial variables and solves only the part of the given constraint system that is *reachable* from this initial set of variables. This set cannot be chosen arbitrarily: The properties that *X*<sup>0</sup> has to satisfy in order for Algorithm 8 are one of the topics of this section. How *X*<sup>0</sup> is chosen concretely is dependent on the context and will be discussed in later sections of this chapter.

In the following, I consider a complete lattice *L* that satisfies the ascending chain condition. The idea behind Algorithm 8 is that it starts with applying the defining constraints of *X*<sup>0</sup> and then proceeds by applying all constraints that use variables from *X*0. Then it considers the variables whose values have changed in the same way and continues this procedure until no more value is changed. In the end, Algorithm 8 has computed a partial solution of the given constraint system and explored the variables and constraints that are reachable from *X*<sup>0</sup> using a chain of def-use relations.

To make this idea work, I need a way to distinguish variables that have been explored and whose solution value may be ⊥*<sup>L</sup>* from those which have not yet been explored and never will be34. For this purpose, I introduce a fresh value ⊠ that represents undefinedness and extend *L* to *L*⊠. Moreover, I also extend the interpretation so that it can also take ⊠ as input and assume that the extended interpretation has the properties (7.1) and (7.2). Remember from chapter 2 that *FV*(*t*) denotes the set of variables occurring in *t*.

(7.1) <sup>∀</sup>*<sup>x</sup>* <sup>∈</sup> *<sup>L</sup>*. <sup>⊠</sup> <sup>≤</sup> *<sup>x</sup>*

$$\begin{aligned} \text{(7.2)}\\ \qquad \qquad \forall \mathfrak{x} \geq t \in \mathcal{C}. \,\forall \psi: X \to L\_{\mathfrak{B}}. \qquad \qquad \qquad \|t\|(\psi) = \mathfrak{B} \\ \qquad \qquad \qquad \Longleftrightarrow \exists \mathfrak{x}' \in FV(t). \,\psi(\mathfrak{x}') = \mathfrak{B}. \end{aligned}$$

These two properties ensure that reachable variables which are set to the ordinary ⊥ element of *L* can be distinguished from those variables which have not been reached yet or will never be reached. Particularly, the second property ensures that reachability is maintained.

Also, the extension of *L* to *L*⊠ is always possible without destroying important properties of *L*. *L*⊠ is still complete lattice that satisfies the ascending chain condition.

Note that in general (7.2) restricts the possible interpretations, since (7.2) essentially enforces that a constraint can be omitted if not all variables on its right-hand side have defining constraints.

<sup>34</sup>Note that the unreachable variables do not actually exist in the memory of a machine executing Algorithm 8. In practice, the reachability of a variable can be assessed by evaluating whether Algorithm 8 already has assigned a value to this variable. Nevertheless, I need this additional element ⊠ to reason theoretically about unreachable variables.

However, the semantics functional ⟦·⟧ : *Expr*(<sup>F</sup> <sup>⊠</sup> , *X*) → *F*<sup>⊠</sup> that corresponds to a data-flow framework instance F = (*G*, *L*, *F*, ρ) and that I specified in section 6.1 satisfies both (7.1) and (7.2): (7.1) follows from (5.12) and (5.13), while (7.2) can be shown by induction with the help of (5.16) and (5.15).

Next, I want to give a formal definition of reachable constraints and variables. For motivation, I give an example. Consider the constraint system C that consists of the constraints

$$\begin{aligned} c\_1 &: v\_1 \ge f(v\_2, v\_3) \\ c\_2 &: v\_2 \ge g(v\_3) \\ c\_3 &: v\_3 \ge a \\ c\_4 &: v\_4 \ge h(v\_3, v\_5) \\ c\_5 &: v\_4 \ge b \end{aligned}$$

where *f*, *g*, *h*, *a*, *b* are function symbols with appropriate interpretations and the *v<sup>i</sup>* are variables.

If we start with *v*<sup>3</sup> and follow the def-use chains in C, we see that *c*<sup>1</sup> , *c*2, *c*3 and *c*<sup>4</sup> are the constraints from C that are reachable from *v*3. Moreover, the reachable variables are *v*<sup>1</sup> , *v*<sup>2</sup> and *v*3.

Also note that *c*<sup>4</sup> is not really useful as it uses the variable *v*5, which does not have any defining constraints in C. Hence, no solution of C needs to assign *v*<sup>5</sup> any defined value. Particularly, the least solution *l f p*(*F*C) must map *v*<sup>4</sup> to <sup>⊠</sup>, which implies that *l f p*(*F*C)(*v*<sup>4</sup> ) = *l f p*(*F*C\{*c*<sup>4</sup> } )(*v*<sup>4</sup> ). Similarly, we can conclude that it is not really useful to start with variables that have defining constraints with non-constant right-hand side, if one wants to preserve the least solution for the reachable variables. For example, if we start with *v*2, then we get the reachable constraints *c*<sup>1</sup> and *c*2, but both use *v*3, which is defined by neither *c*<sup>1</sup> nor *c*2, so that *l f p*(*F*C)(*v*<sup>1</sup> ) and *l f p*(*F*C)(*v*2) both become <sup>⊠</sup> if we restrict <sup>C</sup> to {*c*<sup>1</sup> , *c*2}.

Given a variable set *X*<sup>0</sup> ⊆ *X*, the following definition introduces the sets Core(C, *X*0) and CoreVars(C, *X*0). The former captures the set of constraints that are obtained by starting at *X*<sup>0</sup> and following the defuse chains in C, while the latter characterizes the variables defined by constraints from Core(C, *X*0).

Remember from chapter 2 that *Vars*(C) is the set of variables that occur on the left-hand sides of constraints in C and that *De f*(*x*) is the set of constraints for which *x* occurs on the left-hand side, respectively.

**Definition 7.1** (core and core variables of a constraint system)**.** *Let* C *be a constraint system and X*<sup>0</sup> ⊆ *X be a set of variables.*

*1.* Core(C, *X*0)*, the* core *of* C *with respect to X*0*, is defined as the least subset* C<sup>0</sup> ⊆ C *with the following closure properties:*

(C-Base) <sup>∀</sup>*x*≥*<sup>t</sup>* ∈ C. *FV*(*t*) = ∅ ∧ *<sup>x</sup>* <sup>∈</sup> *<sup>X</sup>*<sup>0</sup> <sup>=</sup><sup>⇒</sup> *<sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* ∈ C<sup>0</sup> (C-Step) <sup>∀</sup>*x*≥*<sup>t</sup>* ∈ C. *FV*(*t*) <sup>≠</sup> <sup>∅</sup> ∧∀*x* ′ ∈ *FV*(*t*). *De f*(*x* ′ ) ∩ C<sup>0</sup> <sup>≠</sup> <sup>∅</sup> <sup>=</sup><sup>⇒</sup> *<sup>x</sup>*≥*<sup>t</sup>* ∈ C<sup>0</sup>

*2.* CoreVars(C, *X*0)*, the* core variables *of* C*, is defined as the least subset X<sup>c</sup>* ⊆ *Vars*(C) *that has the following closure properties:*


The following two lemmas shed some light on the properties of Core and CoreVars and will be of great use later.

### **Lemma 7.2.**

*Vars*(Core(C, *X*0)) = CoreVars(C, *X*0)

*Proof.* ⊇: Show that *Vars*(Core(C, *X*0)) has the closure properties of CoreVars(C, *X*0):

1. Let *x*≥*t* ∈ C with *FV*(*t*) = ∅ and *x* ∈ *X*0. Then *x*≥*t* ∈ Core(C, *X*0), i.e. *x* ∈ *Vars*(Core(C, *X*0)).

2. Let *<sup>x</sup>*≥*<sup>t</sup>* ∈ C with *FV*(*t*) <sup>≠</sup> <sup>∅</sup> and *FV*(*t*) <sup>⊆</sup> *Vars*(Core(C, *<sup>X</sup>*0)). Then for every *x* ′ ∈ *FV*(*t*), *De f*(*x* ′ ) ∩ Core(C, *<sup>X</sup>*0) <sup>≠</sup> <sup>∅</sup>. Hence, *<sup>x</sup>*≥*<sup>t</sup>* ∈ Core(C, *<sup>X</sup>*0), which means that *x* ∈ *Vars*(Core(C, *X*0)).

⊆: Define

$$\mathcal{C}\_0 \stackrel{def}{=} \{ \mathfrak{x} \succeq t \in \mathcal{C} \mid \mathfrak{x} \in \mathsf{CoreVars}(\mathcal{C}\_\prime X\_0) \}$$

Then

$$(\star) \qquad \qquad Var(\mathcal{C}\_0) = \text{CoreVars}(\mathcal{C}\_\prime X\_0) . \tag{4}$$

This can be seen as follows:


Next we show that Core(C, *X*0) ⊆ C0. From this, it follows that

$$\operatorname{Vars}(\operatorname{Core}(\mathcal{C}\_{\prime}X\_{0})) \subseteq \operatorname{Vars}(\mathcal{C}\_{0}) = \operatorname{CoreVars}(\mathcal{C}\_{\prime}X\_{0}).$$

To prove Core(C, *X*0) ⊆ C0, we show that C<sup>0</sup> has the closure properties (C-Base) and (C-Step).

1. If *x*≥*t* ∈ C, *FV*(*t*) = ∅ and *x* ∈ *X*0, we have *x* ∈ CoreVars(C, *X*0) by (V-Base), hence *<sup>x</sup>*≥*<sup>t</sup>* ∈ C<sup>0</sup> by definition.

2. Let *<sup>x</sup>*≥*<sup>t</sup>* ∈ C with *FV*(*t*) <sup>≠</sup> <sup>∅</sup> and

$$\forall \mathfrak{x'} \in FV(t). \, Def(\mathfrak{x'}) \cap \mathcal{C}\_0 \neq \emptyset.$$

Then

$$FV(t) \subseteq Vars(\mathcal{C}\_0) \stackrel{(\star)}{=} \mathcal{C} \text{coreVars}(\mathcal{C}\_\prime X\_0) . .$$

Hence, *<sup>x</sup>* ∈ CoreVars(C, *<sup>X</sup>*0) by property (V-Step), which means that *<sup>x</sup>*≥*<sup>t</sup>* <sup>∈</sup> C<sup>0</sup> by definition of C0.

□

**Lemma 7.3.** *Let* C *be a constraint system and X*<sup>0</sup> ⊆ *X be a set of variables. Then the following statements are equivalent:*

(1) *x*≥*t* ∈ Core(C, *X*0)

$$(\mathcal{Q}) \quad (FV(t) = \emptyset \land \mathbf{x} \in \mathcal{X}\_0) \lor (FV(t) \neq \emptyset \land FV(t) \subseteq \text{CoreVars}(\mathcal{C}, \mathcal{X}\_0)))$$

*Proof.* (1) =⇒ (2) Assume *x*≥*t* ∈ Core(C, *X*0). Then either *FV*(*t*) = ∅ and *<sup>x</sup>* <sup>∈</sup> *<sup>X</sup>*<sup>0</sup> or *FV*(*t*) <sup>≠</sup> <sup>∅</sup> and <sup>∀</sup>*<sup>x</sup>* ′ ∈ *FV*(*t*). *De f*(*x* ′ ) ∩ Core(C, *<sup>X</sup>*0) <sup>≠</sup> <sup>∅</sup>. In the former case, (2) holds trivially. In the latter case, we conclude

$$\forall \mathbf{x'} \in FV(t). \; \mathbf{x'} \in Vars(\mathbf{Core}(\mathbf{C}, X\_0))\_{\mathbf{Y}}$$

which is, due to Lemma 7.2, equivalent to

$$\forall \mathbf{x'} \in FV(t). \,\,\mathfrak{x} \in \text{CoreVars}(\mathcal{C}\_{\prime}\mathbf{X}\_{0}).$$

(2) =⇒ (1) We prove the claim by case distinction.

1. Assume that

$$\alpha \in \mathcal{X}\_0 \land FV(t) = \emptyset.$$

Then *<sup>x</sup>*≥*<sup>t</sup>* ∈ Core(C, *<sup>X</sup>*0) by (C-Base).

2. Assume that

$$FV(t) \neq \emptyset \land FV(t) \subseteq \text{CoreVars}(\mathcal{C}\_{\prime}X\_{0})\dots$$

With the definition of *Vars* and Lemma 7.2, this implies

$$\forall \mathbf{x'} \in FV(t). \, Def(\mathbf{x'}) \cap \text{Core}(\mathcal{C}, \mathbf{X}\_0) \neq \emptyset.$$

Thus *<sup>x</sup>*≥*<sup>t</sup>* ∈ Core(C, *<sup>X</sup>*0) by (C-Step).

Core(C, *X*) only eliminates useless constraints. Therefore, the least solution of Core(C, *X*) must coincide with the least solution of C on the whole variable set.

**Lemma 7.4.** Core(C, *X*) *has the same least solution as* C*.*

*Proof.* First, we show that the two least solutions must coincide on *Vars*(Core(C, *X*)). It is clear that *l f p*(*F*Core(C,*X*) ) ≤ *l f p*(*F*C), since Core(C, *X*) ⊆ C. Next, we observe

$$(\text{7.3})\qquad \forall \mathbf{x} \in X \mid Var(\text{Core}(\mathbf{C}, X)). \newline 1. \newline f(F\_{\text{Core}(\mathbf{C}, X)})(\mathbf{x}) = \mathbf{z}. \newline \dots$$

□

The reason is that by definition, Core(C, *X*) has no defining constraints for variables outside of *Vars*(Core(C, *X*)). Finally, we show

$$(7.4)\tag{7.4}$$

$$\text{(7.4)}\tag{7.5}\text{(}\text{(F}\_{\text{C}}\text{)}\leq \text{lfp}(\text{F}\_{\text{C}\text{ore}(\text{C},X)})\text{)}$$

by fixed-point induction (cf. Corollary 2.7). Let

$$\mathcal{P} \stackrel{def}{=} \{ \psi : X \to L \mid \psi \le lfp(F\_{\text{Core}(\mathcal{C},X)}) \}.$$

It is easy to see that *const*(⊠) ∈ P and that <sup>P</sup> is closed under arbitrary joins. It remains to show that P is also closed under *F*C. For this, it suffices to show

$$\forall \psi \in \mathcal{P}. \,\forall \mathfrak{x} \ge t \in \mathcal{C}. \,\|\mathfrak{f}\|(\psi) \le lfp(F\_{\text{Core}(\mathcal{C},\mathcal{X})})(\mathfrak{x})$$

So let ψ ∈ P and *x*≥*t* ∈ C. We have to show that

$$(\text{7.5})\qquad \|t\| (\psi) \le \|t\| (lfp(F\_{\text{Core}(\mathcal{C},X)})) \le lfp(F\_{\text{Core}(\mathcal{C},X)}) (\text{x})\,.$$

This follows from the two properties

$$(7.6)\tag{7.6} \qquad \qquad \|t\| (\psi) \le \|t\| (lfp(F\_{\text{Core}(\mathcal{C}, \mathcal{X})})) $$

and

$$(\text{7.7})\qquad\qquad\|\mathbb{I}\|\|(\text{lfp}(\text{F}\_{\text{Core}(\text{C},\text{X})}))\leq\text{lfp}(\text{F}\_{\text{Core}(\text{C},\text{X})})(\text{x})\_{\text{M}}$$

which we are going to show in the following.

(7.6) By our assumption that ψ ∈ P, we have in particular

$$\forall \mathbf{x'} \in FV(t). \; \psi(\mathbf{x'}) \le lfp(F\_{\text{Core}(\mathcal{C}\_l X)})(\mathbf{x'}).\;$$

From this, (7.6) can be shown using Lemma 2.10.

(7.7) We distinguish two cases:

1. *<sup>x</sup>*≥*<sup>t</sup>* ∈ Core(C, *<sup>X</sup>*): Then (7.7) holds because *l f p*(*F*Core(C,*X*) ) is a solution of Core(C, *X*).

2. *<sup>x</sup>*≥*<sup>t</sup>* <sup>∉</sup> <sup>C</sup>ore(C, *<sup>X</sup>*). Then it must be the case that

$$FV(t) \neq \emptyset \land \exists \mathbf{x'} \in FV(t) . \mathbf{x'} \in X \backslash Vars(\mathbf{Core}(\mathbf{C}, X)) . \prime$$

since otherwise the closure properties of Core(C, *X*) would immediately imply *x*≥*t* ∈ Core(C, *X*).

But then, due to (7.3), there must be one *x* ′ ∈ *FV*(*t*) with

$$\operatorname{lfp}(F\_{\text{Core}(\mathcal{C},\mathcal{X})})(\mathfrak{x}') = \mathfrak{a}.$$

Now, (7.7) follows with (7.2) and (7.1).


Now I want to consider how <sup>C</sup>ore behaves for *<sup>X</sup>*<sup>0</sup> <sup>≠</sup> *<sup>X</sup>*.

The following lemma shows that Core(C, *X*0) can be obtained by first computing Core(C, *X*) and then restricting the variables to *X*0. The first step eliminates all constraints that do not contribute anything, while the second step eliminates all constraints that may contribute to the solution but depend on variables not in *X*0.

**Lemma 7.5.** *Applying* Core(*\_*, *X*0)*to* C *is the same as applying it to* Core(C, *X*)*:*

$$\text{(7.8)}\qquad\text{Core}(\mathcal{C}, X\_0) = \text{Core}(\text{Core}(\mathcal{C}, X), X\_0).$$

*Proof.* First, we observe that Core(C, *X*) is a subset of C and Core is monotone in its first argument. This implies

$$\text{Core}(\mathcal{C}, X\_0) \supseteq \text{Core}(\text{Core}(\mathcal{C}, X), X\_0) \dots$$

It remains to show "⊆". For this, we show that Core(Core(C, *X*), *X*0) has the properties (C-Base) and (C-Step) with respect to <sup>C</sup> and *<sup>X</sup>*0.

1. Let *x*≥*t* ∈ C with *FV*(*t*) = ∅ and *x* ∈ *X*0. Then in particular *x* ∈ *X*. Hence, by applying (C-Base) to *<sup>x</sup>*≥*<sup>t</sup>* ∈ C and *<sup>x</sup>* <sup>∈</sup> *<sup>X</sup>*, we get

$$
\alpha \ge t \in \text{Core}(\mathcal{C}, X).
$$

Then we apply (C-Base) to *<sup>x</sup>* <sup>∈</sup> *<sup>X</sup>*<sup>0</sup> and *<sup>x</sup>*≥*<sup>t</sup>* ∈ Core(C, *<sup>X</sup>*) and get our desired result

$$
\alpha \ge t \in \text{Core}(\text{Core}(\mathsf{C}, X), X\_0).
$$

2. Let *<sup>x</sup>*≥*<sup>t</sup>* ∈ C with *FV*(*t*) <sup>≠</sup> <sup>∅</sup> and

∀*x* ′ ∈ *FV*(*t*). *De f*(*x* ′ ) ∩ Core(Core(C, *<sup>X</sup>*), *<sup>X</sup>*0) <sup>≠</sup> <sup>∅</sup>.

Since Core(C, *X*) ⊆ C and *X*<sup>0</sup> ⊆ *X*, and due to the monotonicity of Core, we have

Core(Core(C, *X*), *X*0) ⊆ Core(C, *X*),

so that we can weaken the second condition to

∀*x* ′ ∈ *FV*(*t*). *De f*(*x* ′ ) ∩ Core(C, *<sup>X</sup>*) <sup>≠</sup> <sup>∅</sup>.

With (C-Step) for <sup>C</sup> and *<sup>X</sup>*, we conclude

$$
\alpha \ge t \in \text{Core}(\mathsf{C}, \mathsf{X}).
$$

Another application of (C-Step), this time for <sup>C</sup>ore(C, *<sup>X</sup>*) and *<sup>X</sup>*0, finally yields

$$\forall x \ge t \in \text{Core}(\text{Core}(\mathcal{C}, X), X\_0)$$

□

The intuition of Core is that the least solution of Core(C, *X*0) coincides with C on a relevant part of *X*, namely CoreVars(C, *X*0). But this is not true for every choice of *X*0.

**Example 7.6.** *Assume that Expr*(F , X) *contains appropriate function symbols for expressing subsets of* {*a*, *b*} *and set constraints over these sets. Moreover, assume an appropriate interpretation.*

*1. Consider the following constraint system* C*:*

$$\begin{aligned} \mathfrak{x} &\supseteq y \\ \mathfrak{x} &\supseteq \{a\} \\ y &\supseteq \{b\} \end{aligned}$$

*Now consider X*<sup>0</sup> = {*x*}*. Then we get*

$$\begin{aligned} \text{Core}(\mathcal{C}, \mathcal{X}\_0) &= \{\mathfrak{x} \supseteq \{a\}\} \\ \text{CoreVars}(\mathcal{C}, \mathcal{X}\_0) &= \{\mathfrak{x}\} \\ lfp(F\_{\text{Core}(\mathcal{C}, \mathcal{X}\_0)})(\mathfrak{x}) &= \{a\} \\ lfp(F\_{\mathcal{C}})(\mathfrak{x}) &= \{a, b\} \end{aligned}$$

282

*2. Consider the following constraint system* C ′ *.*

$$y \supseteq \ge \mathbf{x}$$

$$\mathfrak{x} \supseteq \{a\}$$

$$y \supseteq \{b\}$$

*Now consider X*′ 0 = {*x*}*. Then we get*

$$\begin{aligned} \text{Core}(\mathcal{C}', X\_0') &= \{ \mathfrak{x} \supseteq \{a\}, y \supseteq \mathfrak{x} \} \\ \text{CoreVars}(\mathcal{C}', X\_0') &= \{ \mathfrak{x}, y \} \\ lfp(F\_{\text{Core}(\mathcal{C}', X\_0')})(y) &= \{ a \} \\ lfp(F\_{\mathcal{C}'})(y) &= \{ a, b \} \end{aligned}$$

The two examples each highlight a characteristic problem that prevents Core(C, *X*0) from exhibiting the same least solution as C, if *X*<sup>0</sup> is not chosen appropriately.

In the first example, the problem is that the constraint *x* ⊇ *y* is not contained in Core(C, *X*0), as it does not depend on a constant constraint that defines *x* (or, more generally, a variable from *X*0).

In the second example, *y* ⊇ {*b*} is not included in Core(C ′ , *X* ′ 0 ) because *y* does not belong to *X* ′ 0 .

An appropriate choice of *X*<sup>0</sup> hence ensures two things:

1. Every constraint in Core(C, *X*0) transitively depends on a constant constraint defining some variable from *X*0.

2. If a variable *v* ∈ CoreVars(C, *X*0), then all constant constraints defining *v* are contained in Core(C, *X*0).

In the following, I introduce *definition-completeness* as a property of constraint systems that ensures the coincidence of least solutions. After that, I consider conditions on variable sets that ensure the definition-completeness of the corresponding Core set.

**Definition 7.7.** C<sup>0</sup> ⊆ C *is called* definition-complete *if it has the property*

$$\forall \mathbf{x} \ge t \in \mathcal{C}. \; Def(\mathbf{x}) \cap \mathcal{C}\_0 \ne \emptyset \implies Def(\mathbf{x}) \subseteq \mathcal{C}\_0.$$

Definition-completeness is indeed sufficient for the coincidence of the least solutions on the core-variables. This is formally stated by Theorem 7.9. Before I can prove that, I need a technical lemma that enables me to perform the fundamental proof steps.

**Lemma 7.8.** *If* C<sup>0</sup> *de f* = Core(C, *X*0) *is definition-complete, then* ψ ≤*Vars*(C<sup>0</sup> ) *l f p*(*F*C<sup>0</sup> ) =<sup>⇒</sup> *<sup>F</sup>*C(ψ) <sup>≤</sup>*Vars*(C<sup>0</sup> ) *F*C(*l f p*(*F*C<sup>0</sup> (7.9) )) *l f p*(*F*C) <sup>≤</sup>*Vars*(C<sup>0</sup> ) *l f p*(*F*C<sup>0</sup> (7.10) )

*Proof.* First, assume that (7.9). Then we can show (7.10) by fixed-point induction (Corollary 2.7). Let

$$\mathcal{P} \stackrel{d\varepsilon f}{=} \{ \psi : X \to L \mid \psi \leq\_{\operatorname{Vars}(\mathcal{C}\_0)} \operatorname{lfp}(\mathcal{F}\_{\mathcal{C}\_0}) \}.$$

We need to show that *const*(⊠) ∈ P, that <sup>P</sup> is closed under arbitrary joins and that P is closed under *F*C. The first two statements can easily be seen and the third claim is proven by (7.9).

Now we prove (7.9). Assume <sup>ψ</sup> <sup>≤</sup>*Vars*(C<sup>0</sup> ) *l f p*(*F*C<sup>0</sup> ). We need to show that *<sup>F</sup>*C(ψ) <sup>≤</sup>*Vars*(C<sup>0</sup> ) *F*C(*l f p*(*F*C<sup>0</sup> )) and for this it is enough to show

$$\forall \mathbf{x} \ge t \in \mathcal{C}. \,\mathbf{x} \in Vars(\mathcal{C}\_0) \implies \|t\|(\psi) \le lfp(F\_{\mathcal{C}\_0})(\mathbf{x}).$$

So let *x*≥*t* ∈ C with *x* ∈ *Vars*(C0). SinceC<sup>0</sup> is definition-complete, it must be *x*≥*t* ∈ C0. By Lemma 7.2 and Lemma 7.3, it follows that *FV*(*t*) ⊆ *Vars*(C0). But according to our assumption this means that

$$\forall \mathbf{x'} \in FV(t). \; \psi(\mathbf{x'}) \le lfp(F\_{C\_0})(\mathbf{x'}).$$

Using Lemma 2.10, this entails

$$(\star) \qquad \qquad \qquad \|t\| (\psi) \le \|t\| (lfp(F\_{\mathcal{C}\_0}))\_{\star}$$

and because *l f p*(*F*C<sup>0</sup> ) satisfies *x* ≥ *t* we have

$$(\star \star) \qquad \qquad \|t\| (lf p(F\_{\mathcal{C}\_0})) \le lfp(F\_{\mathcal{C}\_0})(\mathbf{x}).$$

From (⋆) and (⋆⋆) we get

$$\|t\| (\psi) \le lfp(F\_{C\_0})(\mathbf{x}),$$

as desired. □

**Theorem 7.9.** *If* Core(C, *X*0) *is definition-complete, then*

$$\text{(7.11)}\qquad lfp(F\_{\mathcal{C}}) =\_{\text{CoreValues}(\mathcal{C}, \mathcal{X}\_0)} lfp(F\_{\text{Core}(\mathcal{C}, \mathcal{X}\_0)})$$

*Proof.* With C<sup>0</sup> *de f* = Core(C, *X*0), we need show

*l f p*(*F*C) <sup>≤</sup>*Vars*(C<sup>0</sup> ) *l f p*(*F*C<sup>0</sup> (7.12) )

$$\text{(7.13)}\qquad\qquad lfp(F\_{\mathcal{C}\_{0}}) \leq\_{Vars(\mathcal{C}\_{0})} lfp(F\_{\mathcal{C}})$$

With the given assumptions, (7.12) follows directly from Lemma 7.8. Hence, it remains to show (7.13), which can be justified as follows.

Core(C, *X*0) is a subset of C. Therefore, every solution of C is a solution of Core(C, *X*0). Hence, *l f p*(*F*C) is a solution of Core(C, *X*0). Since *l f p*(*F*Core(C,*X*<sup>0</sup> ) ) is the least solution of Core(C, *X*0), this implies *l f p*(*F*Core(C,*X*<sup>0</sup> ) ) ≤ *l f p*(*F*C). Clearly, this also holds when restricting to <sup>C</sup>oreVars(C, *<sup>X</sup>*0). □

Theorem 7.10 states conditions on *X*<sup>0</sup> that characterize definitioncompleteness. This is basically a formalization of the intuition given in Example 7.6.

**Theorem 7.10.** Core(C, *X*0) *is definition-complete if and only if it satisfies the following two conditions:*

$$\begin{aligned} \text{(i)} \qquad & \forall x \ge t \in \mathcal{C}. \ x \in \text{CoreValues}(\mathcal{C}, X\_0) \land FV(t) = \emptyset \\ & \implies x \in X\_0 \end{aligned}$$

∀*x*≥*t* ∈ C. ∀*x* ′ (ii) ∈ *FV*(*t*). *x* ∈ CoreVars(C, *X*0) =⇒ *x* ′ ∈ CoreVars(C, *X*0)

*Proof.* We show the two directions separately.

1. Assume that Core(C, *X*0) is definition-complete and let *x*≥*t* ∈ C and *x* ∈ CoreVars(C, *X*0). Then *x* ∈ *Vars*(Core(C, *X*0)) by Lemma 7.2, which means that *De f*(*x*) ∩ Core(C, *<sup>X</sup>*0) <sup>≠</sup> <sup>∅</sup>. Since <sup>C</sup>ore(C, *<sup>X</sup>*0) is definitioncomplete, this entails that *x*≥*t* ∈ Core(C, *X*0).

Now we can show (i) and (ii) by considering the two cases *FV*(*t*) = ∅ and *FV*(*t*) <sup>≠</sup> <sup>∅</sup>:


$$Def(\mathfrak{x}) \cap \text{Core}(\mathcal{C}, X\_0) \neq \emptyset \land \mathfrak{x} \ge t \in Def(\mathfrak{x}).$$

Together with Lemma 7.2, this implies *x* ∈ CoreVars(C, *X*0).

Now we show *x*≥*t* ∈ Core(C, *X*0) by case distinction on whether *FV*(*t*) = ∅ or not.


$$\forall \mathbf{x'} \in FV(t). \; \mathbf{x'} \in \mathbf{CoreVars}(\mathbf{C}\_{\prime}X\_{0}).$$

by (ii). But by definition and Lemma 7.2 this is the same as

$$\forall \mathbf{x'} \in FV(t). \; Def(\mathbf{x}) \cap \text{Core}(\mathbf{C}\_{\prime}X\_{0}) \neq \emptyset.$$

This implies *<sup>x</sup>*≥*<sup>t</sup>* ∈ Core(C, *<sup>X</sup>*0) by (C-Step).


Now I am ready to present Algorithm 8, a variant of Algorithm 3 that also uses a worklist approach to compute the solution of a given constraint system, but at the same time also performs a reachability analysis. More specifically, Algorithm 8 takes a set *X*<sup>0</sup> of variables and, starting with *X*0, traverses the constraint dependency graph from usage to definition.

In the following, I state and prove a correctness result for Algorithm 8. This proof is a a variation of the correctness proof for the ordinary worklist algorithm [130] that I already mentioned in chapter 2. This proof is combined with correctness arguments for the reachability parts taken from Takai [160]<sup>35</sup> .

<sup>35</sup>Tamai [160] also considers integrating reachability into graph problems that can be encoded as constraint systems. They propose an algorithm whose worklist policy is the same as the one for Algorithm 8 and their correctness proof works similarly as the proof of Theorem 7.11. However, Tamai does not encode unreachability explicitly into the lattice.

**Algorithm 8:** A variant of the worklist algorithm where the items taken off the worklist have just been updated but changes have not been propagated yet

```
Input: a monotone constraint system C with interpretation over
        the variables X, a set X0 ⊆ X of initial variables
  Result: a function X → L with properties as stated in The-
         orem 7.11
1 A ← const(⊠)
2 foreach x≥t ∈ C s.t. x ∈ X0 ∧ FV(t) = ∅ do
3 A(x) ← A(x) ⊔ ⟦t⟧(A)
4 W ← W ∪ {x}
5 while W ≠ ∅ do
6 x ← remove(W)
7 foreach x
             ′ ≥ t ∈ C such that x ∈ FV(t) do
8 if ∀y ∈ FV(t). y ≠ x =⇒ A(y) ≠ ⊠ then
9 old ← A(x
                     ′
                     )
10 A(x
               ′
                ) ← A(x
                        ′
                        ) ⊔ ⟦t⟧(A)
11 if A(x
                 ′
                  ) ≠ old then
12 W ← W ∪ {x
                         ′
                          }
13 return A
```
For abbreviation, I define

$$\begin{aligned} \mathcal{C}\_0 & \stackrel{def}{=} \text{Core}(\mathcal{C}\_\prime X\_0), \\ \mathcal{V}\_0 & \stackrel{def}{=} Var(\mathcal{C}\_0) \end{aligned}$$

**Theorem 7.11.** *Algorithm 8 always terminates and upon termination, we have*

$$\text{(7.14)}\qquad\qquad\mathcal{A}(\mathbf{x}) = \begin{cases} \operatorname{lfp}(\mathcal{F}\_{\mathcal{C}\_{0}})(\mathbf{x}) & \text{if } \mathbf{x} \in \mathcal{V}\_{0} \\ \mathbb{E} & \text{otherwise} \end{cases}$$

*Moreover, if* C<sup>0</sup> *is definition-complete with respect to* Core(C, *X*)*, then*

$$\text{(7.15)}\qquad\qquad\mathcal{H}=\_{\mathcal{V}\_0} \operatorname{lfp}(F\_{\text{Core}(\mathcal{C},\text{X})}) = \operatorname{lfp}(F\_{\mathcal{C}})$$

*Proof.* First, assume that (7.14) has been shown and that C<sup>0</sup> is definitioncomplete with respect to Core(C, *X*). Then

$$\begin{aligned} \mathcal{A} &= \mathsf{V}\_0 \, \, lfp(\mathsf{F}\_{\mathsf{C}\_0}) & \{\,\, \mathsf{(7.14)}\} \\ &= \mathsf{V}\_0 \, \, lfp(\mathsf{F}\_{\mathsf{C} \mathsf{ore}(\mathsf{C}, \mathsf{X})}) & \{\,\, \mathsf{Theorem} \, \, \mathsf{7.9}\} \\ &= lfp(\mathsf{F}\_{\mathsf{C}}) . \end{aligned}$$

It remains to show (7.14). This follows from the following three statements:


$$\{\mathfrak{x} \in X \mid \mathcal{A}(\mathfrak{x}) \neq \mathfrak{x}\} \subseteq \mathcal{V}\_0$$

and upon termination, we have

$$\langle \mathfrak{x} \in X \mid \mathcal{R}(\mathfrak{x}) \neq \mathfrak{B} \rangle = \mathcal{V}\_0.$$

(C) Algorithm 8 maintains the invariant

$$\mathcal{H} \le lfp(F\_{C\_0})$$

and upon termination, we have

$$\mathcal{A} = \nu\_0 \, \_{lf} p(F\_{C\_0}) \, \_{\ast}$$

For a proof of (A), see Lemma 7.12. (B) is going to be shown in Lemma 7.13 and, finally, I will prove (C) in Lemma 7.14. □

The remainder of this section consists of the three statements (A), (B), and (C) that were left to show in the proof of Theorem 7.11.

**Lemma 7.12.** *Algorithm 8 terminates.*

*Proof.* It is sufficient to show that the main loop in the lines 5–13 is only traversed finitely often. For this purpose, we define *T de f* = (*<sup>X</sup>* <sup>→</sup> *<sup>L</sup>*) <sup>×</sup> <sup>2</sup> *X*. Elements of *T* can be used to represent the state which is maintained by the algorithm: The first component is the currently computed solution and the second component is the current worklist. Moreover, define on *T* the relation

$$(A\_1, W\_1) \leq\_T (A\_2, W\_2) \overset{def}{\Longleftrightarrow} A\_1 >\_{\left(X \to L\right)} A\_2 \lor \left(A\_1 = A\_2 \land W\_1 \subseteq W\_2\right))$$

Then ≤*<sup>T</sup>* is a partial order and satisfies the descending chain condition, since *X* → *L* satisfies the ascending chain condition and 2 *<sup>X</sup>* is finite. Let (A, *W*) ∈ *T* be the state of the algorithm at any time.

Now consider an iteration of the main loop. Let (A*old*, *Wold*) and (A*new*, *Wnew*) be the state at the beginning and end of this iteration, respectively. Now two cases are possible and we show that (A*new*, *Wnew*) <*<sup>T</sup>* (A*old*, *Wold*) must hold in either case.

First, assume that A*old* has not been changed in the iteration. Then we have A*new* = A*old* and the iteration has removed exactly one element of *<sup>W</sup>old* and did not add any elements to it, i.e. *<sup>W</sup>new* <sup>⊊</sup> *<sup>W</sup>old*, which means that

$$(\mathcal{H}\_{\text{new}}, \mathcal{W}\_{\text{new}}) \prec\_T (\mathcal{H}\_{\text{old}}, \mathcal{W}\_{\text{old}})$$

in this case.

Now assume that A*old* has indeed been touched. Then this can only have happened by executing line 10, which changes A*old* upwards. Hence, we have A*old*(*x*) < A*new*(*x*) for some *x* ∈ *X*. This entails

$$(\mathcal{H}\_{new}, \mathcal{W}\_{new}) \prec\_T (\mathcal{H}\_{old}, \mathcal{W}\_{old})\_{\prime\prime}$$

as desired.

Now we have seen that (A, *W*) becomes strictly smaller in each iteration of the main loop. Hence, since the partial order (*T*,≤*T*) satisfies the descending chain condition, the main loop can only be traversed finitely often, as desired. □

**Lemma 7.13.** *1. Algorithm 8 maintains the invariant*

$$\{\mathfrak{x}\in X \mid \mathcal{R}(\mathfrak{x}) \neq \mathfrak{B}\} \subseteq \mathcal{V}\_{0}.$$

### *2. Upon termination, we have*

$$\{\mathfrak{x}\in X \mid \mathcal{H}(\mathfrak{x}) \neq \mathfrak{B}\} = \text{CoreVars}(\mathcal{C}\_{\prime}X\_{0})\dots$$

*Proof.* 1. For ψ : *X* → *L* define

$$dom(\psi) \stackrel{def}{=} \{ \mathfrak{x} \in \mathcal{X} \mid \psi(\mathfrak{x}) \neq \mathfrak{B} \}.$$

Then we have to show that

(Inv) *dom*(A) ⊆ V<sup>0</sup>

holds after the execution of each statement in Algorithm 8. We only need to consider executions of line 3 or 10, since all the other statements leave A unchanged and therefore maintain (Inv) trivially.

So consider any execution of line 3 or 10 for a given constraint *x* ≥ *t*. Let A*old* be the value of A before this execution, A*new* be the value of A after this execution.

Assume that (Inv) holds for A*old*.

If the considered execution does not change A, (Inv) is maintained trivially. So assume that the considered execution indeed changes A. In this case we have

$$\operatorname{dom}(\mathcal{R}\_{new}) = \operatorname{dom}(\mathcal{R}\_{old}) \cup \{\mathfrak{x}\}\_{\mathsf{x}}$$

i.e. we must show that *x* ∈ V0.

For an execution of line 3, this is indeed the case: This follows by (V-Base), because line 3 is executed only if *x* ∈ *X*<sup>0</sup> and *FV*(*t*) = ∅.

Next consider any execution of line 10. Then it must be the case that *FV*(*t*) is not empty. Moreover, since the considered execution of line 10 changes <sup>A</sup>, it cannot be the case that <sup>A</sup>*old*(*x*) = <sup>⊠</sup>. Otherwise, ⟦*t*⟧(A*old*) would be <sup>⊠</sup> in the case that *<sup>x</sup>* <sup>∈</sup> *FV*(*t*), because of (7.2). Together with line 8, this ensures that *FV*(*t*) ⊆ *dom*(A*old*). By (Inv), this means that

$$\forall y \in FV(t). \, y \in \mathcal{V}\_0.$$

By Lemma 7.3, this entails *x* ≥ *t* ∈ C0, or, equivalently, *x* ∈ V0.

2. By induction on *x* ∈ V0, we show that for every *x* ∈ V0, A(*x*) is written to at some point in Algorithm 8.

(V-Base) The initial loop applies every constraint *<sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* with *<sup>x</sup>* <sup>∈</sup> *<sup>X</sup>*<sup>0</sup> and no variables on the right-hand side. Since A is only changed upwards, and because of (7.2), this guarantees that upon termination, we have <sup>A</sup>(*x*) <sup>≠</sup> <sup>⊠</sup> for every *x* ∈ *X*<sup>0</sup> for which there is a constraint *x* ≥ *t* with *FV*(*t*) = ∅.

(V-Step) Consider a constraint *<sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* such that

$$
\emptyset \neq FV(t) \subseteq \mathcal{V}\_0.
$$

By induction hypothesis, we may assume that for all *y* ∈ *FV*(*t*), A(*y*) is written to for the first time at some point in Algorithm 8. Since *FV*(*t*) <sup>≠</sup> <sup>∅</sup>, we may further assume that there is an iteration in which this happens for the last *y*<sup>0</sup> ∈ *FV*(*t*). Particularly, at the beginning of this iteration, <sup>A</sup>(*y*) <sup>≠</sup> <sup>⊠</sup> for all *<sup>y</sup>* <sup>∈</sup> *FV*(*t*) \ {*y*0} and <sup>A</sup>(*y*0) = <sup>⊠</sup> and at the end of this iteration <sup>A</sup>(*y*) <sup>≠</sup> <sup>⊠</sup> for all *<sup>y</sup>* <sup>∈</sup> *FV*(*t*). This means that <sup>A</sup>(*y*0) is changed in this iteration and since *y*<sup>0</sup> ∈ *FV*(*t*), *x* is added to *W*. Hence, at the end of the iteration, *x* ∈ *W*. Since Algorithm 8 terminates (Lemma 7.12), *x* is eventually considered in some later iteration. We may assume that <sup>A</sup>(*x*) = <sup>⊠</sup> at the beginning of this iteration, since otherwise our claim follows trivially. Furthermore, we notice that

$$\forall y \in FV(t) . \mathcal{A}(y) \neq \mathbb{B}$$

is maintained until Algorithm 8 terminates. So, this property holds particularly in the iteration in which *x* is removed from the worklist. During this iteration, *x* ≥ *t* is eventually considered. Again, we assume that <sup>A</sup>(*x*) = <sup>⊠</sup> until *<sup>x</sup>* <sup>≥</sup> *<sup>t</sup>* is considered, since otherwise there is nothing to show. Now, when *x* ≥ *t* is eventually processed, the check in line 8 passes and A(*x*) is written to for the first time.

□

**Lemma 7.14.** *1. Algorithm 8 maintains the invariant*

$$\mathcal{H} \le lfp(F\_{\mathcal{C}\_0})\,.$$

*2. Upon termination, we have*

$$\mathcal{A} =\_{\mathcal{V}\_0} \operatorname{lfp}(F\_{\mathcal{C}\_0}) \dots$$

*Proof.* We show the two statements separately.

1. We have to show that

$$\text{(Inv)}\tag{1\text{nv}} \qquad\qquad\qquad\qquad\qquad\mathcal{R}\leq \operatorname{lfp}(\mathcal{F}\_{\mathcal{C}\_{0}})\_{\mathcal{F}}$$

is maintained by every execution of each statement in Algorithm 8. We only need to consider executions of line 3 or 10, since all the other statements leave A unchanged and therefore maintain (Inv) trivially.

So consider any execution of 3 or 10. Let *x* ≥ *t* be the constraint that is applied. First, we notice that line 3 is only executed if *x* ∈ *X*<sup>0</sup> and *t* contains no variables. Hence, *x*≥*t* ∈ C0. Secondly, we see that line 10 is only executed if

$$\forall y \in FV(t) . \mathcal{R}(y) \neq \mathbb{B}\_{\prime}$$

and this implies, according to the invariant in Lemma 7.13, that *FV*(*t*) ⊆ V0. Hence, in both lines 3 and 10 we have *x*≥*t* ∈ C<sup>0</sup> and therefore

$$l(\star) \qquad \qquad lfp(F\_{\mathcal{C}\_0})(x) \ge \|t\| (lfp(F\_{\mathcal{C}\_0})) .$$

Let A*old* be the value of A before the execution and A*new* be the value of A after the execution of line 3 or 10. Then we have

$$(\star \star) \qquad \qquad \mathcal{A}\_{\text{new}}(\mathfrak{x}) = \mathcal{A}\_{\text{old}}(\mathfrak{x}) \sqcup \llbracket t \rrbracket(\mathcal{A}\_{\text{old}}) .$$

Now assume that (Inv) holds for A*old*. To show (Inv) for A*new*, we only need to consider A*new*(*x*) since the rest is left unchanged.

Then, we can argue as follows:

$$\begin{aligned} &\mathcal{A}\_{new}(\mathbf{x})\\ &=\mathcal{A}\_{old}(\mathbf{x})\sqcup\llbracket\!\!\!\!f(\mathcal{A}\_{old})\\ &\leq\inf\_{\mathbf{f}}(\mathbf{F}\_{\mathbf{C}\_{0}})(\mathbf{x})\sqcup\llbracket\!\!\!f(\mathbf{f}\!p(\mathbf{F}\_{\mathbf{C}\_{0}})) & &\{\}(\text{Inv}),\text{Lemma 2.10 }\}\\ &=\ulcorner{\mathcal{J}}{\scriptstyle\!f}(\mathbf{F}\_{\mathbf{C}\_{0}})(\mathbf{x}) & &\{\}(\star)\nmid\end{aligned}$$

2. We show that the main loop of Algorithm 8 maintains the invariant

$$\begin{aligned} (\text{Inv}) \quad & \forall \mathfrak{x} \succeq \mathfrak{t} \in \mathcal{C}. \quad \mathfrak{x} \in \text{dom}(\mathcal{R})\\ & \qquad \land \, FV(t) \subseteq \text{dom}(\mathcal{R})\\ & \qquad \land \, FV(t) \cap \mathcal{W} = \emptyset\\ & \Longrightarrow \mathcal{A}(\mathfrak{x}) \ge \|\mathfrak{t}\|(\mathcal{A}). \end{aligned}$$

292

This is sufficient: Upon termination, *W* is empty. Hence, the following property holds:

$$\forall \mathbf{x} \ge t \in \mathcal{C}. \,\mathbf{x} \in dom(\mathcal{H}) \land FV(t) \subseteq dom(\mathcal{H}) \implies \mathcal{R}(\mathbf{x}) \ge \|t\|(\mathcal{R}).$$

Using Lemma 7.13, this is equivalent to

$$\forall \text{7.16} \qquad \forall \mathbf{x} \in \mathcal{V}\_{0}. \; \forall \mathbf{x} \ge t \in \mathcal{C}. \; FV(t) \subseteq \mathcal{V}\_{0} \implies \mathcal{R}(\mathbf{x}) \ge \|t\|(\mathcal{R}).$$

Now consider any constraint *x*≥*t* ∈ Core(C, *X*0). If *x* ∈ *X*<sup>0</sup> and *FV*(*t*) = ∅, then (7.16) trivially implies that A satisfies *x* ≥ *t*. Next, consider the case that *FV*(*t*) <sup>≠</sup> <sup>∅</sup> and

$$\forall y \in FV(t). \; Def(y) \cap \mathcal{C}\_0 \neq \emptyset.$$

ByLemma 7.3 and Lemma 7.2, it follows that *FV*(*t*) ⊆ V0. Again, by application of (7.16) we see that *x* ≥ *t* is satisfied by A.

It remains to show that (Inv) holds at the beginning of the main loop and is maintained by each of its iterations.

(Inv) **holds just before the first iteration of the main loop:** In the initialization loop, A was only updated for *x* ∈ *X*<sup>0</sup> and only for those constraints *x* ≥ *t* where *FV*(*t*) = ∅. Hence, if *x*≥*t* ∈ C with *FV*(*t*) ∩ *W* = ∅ and <sup>A</sup>(*x*) <sup>≠</sup> <sup>⊠</sup> and <sup>∀</sup>*<sup>y</sup>* <sup>∈</sup> *FV*(*t*). <sup>A</sup>(*y*) <sup>≠</sup> <sup>⊠</sup>, it must be the case that *<sup>x</sup>* <sup>∈</sup> *<sup>X</sup>*<sup>0</sup> and *FV*(*t*) = <sup>∅</sup>. Due to the initialization, <sup>A</sup>(*x*) <sup>≥</sup> ⟦*t*⟧(A) holds since this was ensured by the assignment in line 3 and could not be invalidated by any other execution of this line.

(Inv) **is maintained by any iteration:** Let *i* be some iteration of the main loop. Let A*old* and *Wold* be the values of A and *W* at the beginning of this iteration and A*new* and *Wnew* the respective values at the end. Assume that (Inv) holds at the beginning of *i*:

$$\begin{aligned} (\text{Inv}\_{pre}) \quad & \quad \forall \mathfrak{x} \ge t \in \mathcal{C}. \quad \mathfrak{x} \in \text{dom}(\mathcal{A}\_{old})\\ & \quad \land FV(t) \subseteq \text{dom}(\mathcal{A}\_{old})\\ & \quad \land FV(t) \cap \mathcal{W}\_{old} = \emptyset\\ & \implies \mathcal{R}\_{old}(\mathfrak{x}) \ge \|t\|(\mathcal{A}\_{old}). \end{aligned}$$

Now consider a constraint *<sup>x</sup>*≥*<sup>t</sup>* ∈ C with <sup>A</sup>*new*(*x*) <sup>≠</sup> <sup>⊠</sup> and *FV*(*t*) <sup>∩</sup> *<sup>W</sup>new* <sup>=</sup> <sup>∅</sup> and *FV*(*t*) <sup>⊆</sup> *dom*(A*new*). We have to show <sup>A</sup>*new*(*x*) <sup>≥</sup> ⟦*t*⟧(A*new*).

First, we make two important observations: Firstly, it must be the case that

$$\text{(7.17)}\tag{7.17}$$

$$\mathcal{H}\_{\text{new}} \ge \mathcal{H}\_{\text{old}}.$$

Secondly, from *FV*(*t*) ∩ *Wnew* = ∅, we can conclude

$$(7.18) \qquad \forall y \in FV(t). \; \mathcal{R}\_{new}(y) = \mathcal{R}\_{old}(y).$$

Now we make a case distinction:

a) <sup>A</sup>*old*(*x*) <sup>≥</sup> ⟦*t*⟧(A*old*). Then we can show our claim as follows:


b) <sup>A</sup>*old*(*x*) ̸≥ ⟦*t*⟧(A*old*). Then because of (Inv*pre*), one of the following statements must be true:


We consider each of these cases separately.

• For (7.19), we argue as follows:

$$\begin{aligned} & \quad \|t\| (\mathcal{H}\_{\text{new}})\\ &= \|t\| (\mathcal{H}\_{old})\\ &= \mathbb{E} & \qquad \qquad \qquad \qquad \{\text{(7.18)}\}\\ &\leq \mathcal{H}\_{\text{new}}(\mathbf{x}) & \qquad \qquad \{\text{(7.1)}\} \end{aligned}$$

• If (7.20) holds, then line 10 must have been executed for *x* ≥ *t*. Afterwards, A(*x*) could only have changed upwards, so we can conclude:

A*new*(*x*)


• If (7.21) is true, then a variable *x* ′′ was removed from *W* and every *x* ′ ≥ *t* ′ with *x* ′′ ∈ *FV*(*t* ′ ) was considered and A(*x* ′ ) was updated if needed. Since *x* ′′ <sup>∈</sup> *<sup>W</sup>old* and *FV*(*t*) <sup>∩</sup> *<sup>W</sup>old* <sup>≠</sup> <sup>∅</sup> and *FV*(*t*) <sup>∩</sup> *<sup>W</sup>new* <sup>=</sup> <sup>∅</sup> and *x* ′′ was the only element which was removed from *W*, it must be *x* ′′ ∈ *FV*(*t*). Hence, line 10 was particularly executed for *x* ≥ *t*. Now we can argue just as in case (7.20).

□

# **7.2 Integration of Interprocedural Slicing and Interprocedural Data-Flow Analysis**

This section gives an outline of sections 7.3 and 7.4, in which I am going to apply the results from section 7.1 to the constraint systems that I showed and discussed in chapter 6.

Section 7.3 is dedicated to the functional approach, while section 7.4 considers the call-string approach.

For my purposes, I fix a data-flow framework F = (*G*, *L*, *F*, ρ). Moreover, I fix a set *Src* ⊆ *N* of *source nodes*.

The goal of the following sections is to derive algorithms that compute a *VP*-correct solution on the forward slice of *Src*. I do this by instantiating Algorithm 8 for solving relevant parts of the constraint systems that we saw in chapter 6.

To be able to handle unreachability, I adjoin <sup>F</sup> with an element <sup>⊠</sup> and extend *F* and ⟦.⟧ like outlined in section 5.3 and section 6.1, respectively. In particular, this is compatible with (7.1) and (7.2), so that I can actually apply the results from section 7.1 to the constraint systems chapter 6.

Next, I want to describe a recurring theme of the following sections. For simplicity, I exclude the call-string approach for the moment. I will handle its specifics in section 7.4.

Consider a constraint system C<sup>P</sup> corresponding to a set of paths P ⊆ *PathsG*. Then I want to use an instantiation of Algorithm 8 to compute a solution A<sup>P</sup> along the paths from P such that

$$\forall (\mathbf{s}, t) \in \mathcal{V}\_0^{(\mathcal{P})} . \mathcal{R}\rho(\mathbf{s}, t) = lfp(\mathbf{C}p)(\mathbf{s}, t) .$$

where

$$\mathcal{V}\_0^{(\mathcal{P})} \stackrel{def}{=} \{ (\mathbf{s}, t) \in \mathcal{N} \times \mathcal{N} \mid \text{Paths}\_{\mathcal{G}}(\mathbf{s}, t) \cap \mathcal{P} \neq \emptyset \}.$$

According to Theorem 7.11, Algorithm 8 computes such an A<sup>P</sup> for V (P) 0 = CoreVars(CP, *X*0), where *X*<sup>0</sup> ⊆ *X* is chosen appropriately andCore(CP, *X*0) is definition-complete in Core(CP, *X*). Hence, the general steps of the following sections will be


$$\text{CoreVars}(\mathsf{C}\_{\mathcal{P}}, X\_0) = \{ (\mathsf{s}, t) \in \mathsf{N} \times \mathsf{N} \mid \mathsf{Paths}\_{\mathcal{G}}(\mathsf{s}, t) \cap \mathcal{P} \neq \emptyset \}$$

and, finally,

4. show that Core(CP, *X*0) is definition-complete in Core(CP, *X*).

# **7.3 Functional Approach**

I want to remind the reader of Constraint System 6.4, which was defined as follows:

$$\begin{pmatrix} \text{vALup-son} \\ \text{vALup-son} \end{pmatrix} \begin{array}{l} \text{X}\_{\text{ASC}}(s,n) \neq \texttt{\texttt{\texttt{\\_}}} \qquad \begin{array}{l} \text{X}\_{\text{DECSC}}(n,t) \neq \texttt{\texttt{\\_}} \end{array} \\\begin{array}{l} \text{X}\_{\text{VP}}(s,t) \geq \texttt{\\_}\_{\text{DECSC}}(n,t) \odot \text{X}\_{\text{ASC}}(s,n) \\\text{\\_} \quad \begin{array}{l} \text{\\_} \ \text{\\_} \end{array} \qquad \begin{array}{l} \text{\\_} \end{array} \} \end{array}$$

Given *XASC* and *XDESC*, a valid-paths solution *XVP* could be obtained by evaluating the equation

$$\text{(7.22)}\qquad\qquad\text{X}\_{VP}(\mathbf{s},t)=\bigsqcup\_{n\in\mathcal{N}}\text{X}\_{\text{DESC}}(n,t)\circ\text{X}\_{\text{ASC}}(\mathbf{s},n).$$

One direct and naive strategy would be to first compute both *XASC* and *XDESC* on the whole set *N* × *N* and then use these functions to evaluate (7.22). However, such an approach would perform a lot of unnecessary work: In fact, one only needs to compute *XDESC*(*n*, *t*) ◦ *XASC*(*s*, *n*) for those *<sup>s</sup>*, *<sup>n</sup>*, *<sup>t</sup>* <sup>∈</sup> *<sup>N</sup>* for which *<sup>s</sup>* <sup>∈</sup> *Src*, *<sup>X</sup>ASC*(*s*, *<sup>n</sup>*) <sup>≠</sup> <sup>⊠</sup> and *<sup>X</sup>DESC*(*n*, *<sup>t</sup>*) <sup>≠</sup> <sup>⊠</sup>.

We can reduce the amount of unnecessary work by first computing *XASC* on *dom*(*ASC*) and then use *XASC* to compute an integral part of *XVP*. Roughly, the idea is to compute a solution *XNASC* such that *XVP* can be written as *XASC* ⊔ *XNASC*. For this, I need to introduce another set of paths, the *non-ascending* paths.

**Definition 7.15.** π ∈ *PathsG*(*s*,*t*) *is called* non-ascending *if* π *can be written as* π<sup>1</sup> · π<sup>2</sup> *such that*

$$\text{(i)}\tag{1}$$

$$\pi\_1 \in \text{ASC}(\text{s}, n)$$

(ii) π<sup>2</sup> ∈ *DESC*(*n*, *t*)

$$\text{(iii)}\tag{iii}$$

π1 (iv) · <sup>π</sup><sup>2</sup> <sup>∉</sup> *ASC*(*s*, *<sup>t</sup>*)

*I denote the set of non-ascending paths from s to t with NASC*(*s*, *t*)*.*

Definition 7.15 is motivated by the fact that any valid path is either ascending or non-ascending. This is formalized by Lemma 7.16 and Remark 7.17.

**Lemma 7.16.** *If* π ∈ *VP*(*s*, *t*)*, then either of the following is true:*

*1.* π ∈ *ASC*(*s*, *t*)

*2. There are c* ∈ *Ncall,* π<sup>1</sup> ∈ *ASC*(*s*, *c*) *and* π<sup>2</sup> ∈ *DESC*(*c*,*t*) *such that* π = π<sup>1</sup> · π2

*Proof.* Let π ∈ *VP*(*s*,*t*) \ *ASC*(*s*,*t*). By Theorem 5.22 there is *i* ∈ *range*(π) such that π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Le f t*(*E*), <sup>π</sup> <sup>≥</sup>*<sup>i</sup>* <sup>∈</sup> *Right*(*E*) and <sup>π</sup> *<sup>i</sup>* <sup>∈</sup> *<sup>E</sup>call*. Define

$$c \stackrel{def}{=} src(\pi^i)\_{\prime}$$

$$\pi\_1 \stackrel{def}{=} \pi^{
$$\pi\_2 \stackrel{def}{=} \pi^{\ge i}.$$
$$

Then *c* ∈ *Ncall*, since π *i* is an outgoing call edge of π *i* . Moreover, because π ∈ *PathsG*(*s*, *t*), we have π<sup>1</sup> ∈ *PathsG*(*s*, *c*) and π<sup>2</sup> ∈ *PathsG*(*c*, *t*). Lastly, by Theorem 5.39, we have π<sup>1</sup> ∈ *Val*(*E*) and π<sup>2</sup> ∈ *Val*(*E*). In summary, π<sup>1</sup> , π<sup>2</sup> and *c* have the desired properties. □ **Remark 7.17.** π ∈ *NASC*(*s*, *t*) *if and only if* π ∈ *VP*(*s*, *t*) \ *ASC*(*s*, *t*)*.*

*Proof.* " ⇐= " is covered by Lemma 7.16. So let π ∈ *NASC*(*s*,*t*). Then π ∈ *VP*(*s*,*t*) by Theorem 5.30 and since <sup>π</sup> <sup>∈</sup> *PathsG*(*s*, *<sup>t</sup>*). Furthermore, <sup>π</sup> <sup>∉</sup> *ASC*(*s*, *<sup>t</sup>*) by Definition 7.15. □

Lemma 7.16 can be applied as follows: Since any valid path is either ascending or non-ascending, we can obtain a valid-paths solution by joining an ascending-paths solution with a solution *XNASC* that merges over all *non-*ascending paths.

The rest of this section is dedicated to computing a valid-paths-solution using the approach that I just sketched. First, subsection 7.3.1 considers the computation of the least same-level solution <sup>A</sup>(*SL*) . Then, subsection 7.3.2 shows how to use a same-level solution such as <sup>A</sup>(*SL*) to compute the least ascending-paths solution <sup>A</sup>(*ASC*) . After that, subsection 7.3.3 is dedicated to the extension of a given ascending-paths solution like <sup>A</sup>(*ASC*) along the descending paths to obtain the least non-ascending-paths-solution: First, a constraint system is given that characterizes non-ascending-paths solutions and then the usual scheme is used to compute the least nonascending-paths solution <sup>A</sup>(*NASC*) .

The last three subsections consider the combination of <sup>A</sup>(*ASC*) and <sup>A</sup>(*NASC*) . Subsection 7.3.4 shows that joining <sup>A</sup>(*ASC*) and <sup>A</sup>(*NASC*) actually yields a valid-paths-correct solution, provided that <sup>A</sup>(*ASC*) and <sup>A</sup>(*NASC*) are correct relative to their respective path sets. Moreover, it compares the solution obtained by this approach with the original least valid-paths solution. After that, subsection 7.3.5 integrates all the sections before in a simple algorithm and shows its correctness. This algorithm uses the same ideas as the two-phase slicer by Horwitz et al. that I already discussed in chapter 3, however there are still some differences. Subsection 7.3.6 explores these differences, modifies the algorithm from subsection 7.3.5 in such a way that it essentially becomes the two-phase slicer and sketches a correctness proof.

# **7.3.1 Computing the Same-Level Solution**

In the following, I consider the constraint system from Constraint System 6.1, which I denote with C (*SL*) .

I define

$$\{\text{7.23}\} \qquad \qquad \begin{array}{c} \text{X}\_0^{(\text{SL})} \stackrel{def}{=} \{\text{(s,s)} \mid s \in \text{N}\_{\text{entry}}\}. \end{array}$$

$$\text{(7.24)}\qquad\qquad\mathcal{C}\_0^{(SL)} \stackrel{def}{=} \text{Core}(\mathcal{C}^{(SL)}, \mathcal{X}\_0^{(SL)})$$

$$(\text{7.25}) \qquad \qquad \mathcal{V}\_0^{(SL)} \stackrel{def}{=} \text{CoreVars}(\mathcal{C}^{(SL)}, X\_0^{(SL)})$$

Algorithm 9 is an instantiation of Algorithm 8 for the constraint system C (*SL*) , using *X* (*SL*) as set of initial variables.

0 The loop in lines 2–4 corresponds to the initialization loop in Algorithm 8. It processes all constant constraints in Constraint System 6.1 whose left-hand side is in *X* (*SL*) .

0 The loop in lines 5–18 corresponds to the main loop Algorithm 8. All constraints *x* ≥ *u* with (*s*,*t*) ∈ *FV*(*u*) are enumerated and the respective <sup>A</sup>(*SL*) (*x*) is updated, just like in Algorithm 8. However, the constraint enumeration loop in Algorithm 9 is split into three parts.

The first of these parts, in lines 7–9, enumerates all constraints of the form sl-sol-(ii).

The other two parts are dedicated to constraint sl-sol-(iii). This is because Algorithm 9 propagates from the right-hand side of a constraint to its left-hand side. In contrast to sl-sol-(ii), there are *two* free variables on the right-hand side of sl-sol-(iii)-constraints, so that (*s*,*t*) can occur at two positions. To illustrate this, let us take a look at such a constraint. It has the form

$$X\_{SL}(s, t) \ge f\_{\mathfrak{e}\_{\text{ret}}} \circ X\_{SL}(\mathfrak{m}\_0 \cdot \mathfrak{n}\_1) \circ f\_{\mathfrak{e}\_{\text{call}}} \circ X\_{SL}(s, \mathfrak{n})$$

with *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* and (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup>.

Hence, each of the two variables on the right-hand side of a rule have to be considered separately.

The loop in lines 10–13 takes care of the case that the variable currently processed is (*s*, *n*) in the above situation, whereas the loop in lines 15–18 takes care of the other case: Here, the variable currently processed is (*n*0, *n*<sup>1</sup> ) in the above situation.

In summary, the two loops enumerate all constraints *c* of the form sl-sol-(iii) with (*s*, *<sup>t</sup>*) <sup>∈</sup> *FV*(*rhs*(*c*)).

**Algorithm 9:** Algorithm for computing the least same-level solution

```
Input: a data-flow framework instance F = (G, L, F⊠, ρ) as de-
         scribed on page 295
  Result: least same-level solution for F , as stated in Theorem 7.18
1 A(SL) ← const(⊠)
2 foreach s ∈ Nentry do
3 A(SL)
            (s,s) ← id
4 W = W ∪ {(s,s)}
5 while W ≠ ∅ do
6 (s, t) ← remove(W)
7 foreach t
               ′ ∈ N s.t. t e→ t
                             ′ ∧ e ∈ Eintra do
8 A(SL)
               (s, t
                   ′
                   ) ← A(SL)
                              (s, t
                                 ′
                                  ) ⊔ fe ◦ A(SL)
                                               (s, t)
9 W ← W ∪ {(s, t
                        ′
                         )} if A(SL)
                                   (s, t
                                       ′
                                       ) has changed
10 foreach (ec,er)∈Φ s.t. t ec → n0 ∧ n1
                                        er → t
                                            ′ ∧ A(SL)
                                                     (n0, n1
                                                            ) ≠ ⊠ do
11 slSol ← fer
                    ◦ A(SL)
                            (n0, n1
                                  ) ◦ fec
12 A(SL)
               (s, t
                   ′
                   ) ← A(SL)
                              (s, t
                                 ′
                                  ) ⊔ s ◦ A(SL)
                                              (s, t)
13 W ← W ∪ {(s, t
                        ′
                         )} if A(SL)
                                   (s, t
                                       ′
                                       ) has changed
14 foreach (ec,er)∈Φ s.t. a ec → s ∧ t
                                      er → b do
15 foreach u ∈ Nentry s.t. A(SL)
                                     (u, a)≠⊠ do
16 slSol ← fer
                        ◦ A(SL)
                               (s, t) ◦ fec
17 A(SL)
                   (u, b) ← A(SL)
                                 (u, b) ⊔ slSol ◦ A(SL)
                                                     (u, a)
18 W ← W ∪ {(u, b)} if A(SL)
                                      (u, b) has changed
19 return A(SL)
```
Now that the reader is convinced that Algorithm 9 is an instantiation of Algorithm 8, I want to instantiate the correctness result of Algorithm 8 for Algorithm 9.

For the moment, I assume that C (*SL*) 0 is definition-complete with respect to C (*SL*) . Then I can use Theorem 7.11 to prove the following correctness result.

**Theorem 7.18.** *The following statements hold:*

*1. Algorithm 9 always terminates and upon termination, we have*

$$(\text{7.26})\qquad\qquad\mathcal{H}^{(SL)}(\text{s},t)=\begin{cases}lfp(\text{F}\_{\text{C}^{(SL)}})(\text{s},t) & \text{if } (\text{s},t) \in \mathcal{V}\_{0}^{(SL)}\\ \boxtimes & \text{otherwise} \end{cases}$$


Two things are left to do. Firstly, we have to characterize V (*SL*) 0 appropriately and secondly, we have to show that C (*SL*) 0 is indeed definitioncomplete with respect to C (*SL*) .

We start with the characterization of V (*SL*) 0 . Ideally, considering the fact that C (*SL*) is defined along the same-level paths of *G*, same-level reachability should be the right property to characterize C (*SL*) 0 . Moreover, taking *X* (*SL*) 0 into account, we can only expect (*s*, *<sup>t</sup>*) ∈ V(*SL*) 0 if *s* ∈ *Nentry*.

Lemma 7.19 formally confirms that V (*SL*) 0 indeed has the desired characterization.

**Lemma 7.19.** V (*SL*) 0 *and* CoreVars(C, *N* × *N*) *can be characterized as follows:*

$$(\text{7.27})\qquad \mathcal{V}\_0^{(SL)} = \{(\text{s}, t) \in N \times N \mid SL(\text{s}, t) \neq \emptyset\} \cap N\_{\text{entry}} \times N \tag{7.27}$$

$$(7.28)\qquad \text{CoreVars}(\mathcal{C}, N \times N) = \{(s, t) \in N \times N \mid SL(s, t) \neq \emptyset\}$$

*In particular,* V (*SL*) 0 *can be obtained from* CoreVars(C, *N* × *N*) *by restricting the first components to Nentry:*

$$(7.29) \qquad \qquad \mathcal{V}\_0^{(SL)} = \text{CoreVars}(\mathcal{C}, N \times N) \cap N\_{\text{entry}} \times N \tag{5.7.2}$$

*Proof.* The claim (7.29) follows directly from (7.27).

It remains to show (7.27) and (7.28). We only show (7.27), since the proof for (7.28) is very similar.

We prove (7.27) by showing the two subset relations separately.

# 1. In order to prove "⊇", we show

$$\begin{aligned} \forall \pi \in SL. \mathsf{V}(s, t) \in \mathsf{N}\_{entry} \times \mathsf{N}. \,\,\pi \in SL(s, t) \\ \implies (s, t) \in \mathsf{V}^{(SL)}\_{0} \end{aligned}$$

by induction on π ∈ *SL*. So let π ∈ *SL* and (*s*,*t*) ∈ *Nentry* × *N* such that π ∈ *SL*(*s*, *t*).

a) Suppose that π = ϵ. Then we have *s* = *t* and *s* ∈ *Nentry*, so that (*s*, *t*) = (*s*,*s*) ∈ *X* (*SL*) 0 . By sl-sol-(i), <sup>C</sup> (*SL*) contains the constraint

$$X(s,s) \ge id\_{\prime\prime}$$

which does not have variables on the right-hand side. Hence, (*s*,*s*) ∈ CoreVars(C (*SL*) , *X* (*SL*) 0 ) by (V-Base).

b) Assume that π = π ′ ·*e*, where *e* ∈ *Eintra*, *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>* and <sup>π</sup> ′ ∈ *SL*(*s*, *t* ′ ). Since π ′ ∈ *SL*(*s*,*t* ′ ), we can apply the induction hypothesis to π ′ : *s* ∈ *Nentry* implies that

$$(s, t') \in \mathcal{V}\_0^{(SL)}.$$

By sl-sol-(ii), <sup>C</sup> (*SL*) contains the constraint

$$X(s, t) \ge f\_{\varepsilon} \circ X(s, t').$$

Since (*s*, *t* ′ ) ∈ V(*SL*) 0 we may apply (V-Step) and conclude

$$(\mathbf{s}, t) \in \mathcal{V}\_0^{(SL)}.$$

c) Suppose that

$$
\pi = \pi' \cdot e\_{call} \cdot \pi'' \cdot e\_{ret}
$$

with π ′ ∈ *SL*(*s*, *n*), π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ), *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* and (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup>. We apply the induction hypothesis to π ′ ∈ *SL*(*s*, *n*) and conclude from *s* ∈ *Nentry* that

$$(\text{7.30})\tag{7.30}$$

302

Moreover, note that *n*<sup>0</sup> has an incoming call edge. Hence, we have *n*<sup>0</sup> ∈ *Nentry*. With π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ) we can apply the induction hypothesis to π ′′ and obtain

$$(\text{7.31})\qquad\qquad(\boldsymbol{\eta}\_{0},\boldsymbol{\eta}\_{1})\in\mathsf{CoreVars}(\mathcal{C}^{(SL)},\boldsymbol{X}\_{0}^{(SL)})\,.$$

Moreover, by sl-sol-(iii), <sup>C</sup> (*SL*) contains the constraint

$$X(s, t) \ge f\_{\varepsilon\_{ret}} \circ X(n\_0, n\_1) \circ f\_{\varepsilon\_{call}} \circ X(s, n).$$

With (V-Step), we conclude (*s*, *<sup>t</sup>*) ∈ V(*SL*) 0 from (7.30) and (7.31).

2. For "⊆", we show

$$(s,t)\in \mathcal{V}\_0^{(SL)} \implies SL(s,t) \neq \emptyset \land s \in \mathcal{N}\_{\textit{entry}}$$

by induction over V (*SL*) 0 = CoreVars(C (*SL*) , *X* (*SL*) 0 ).

Let (*s*,*t*) ∈ V(*SL*) 0 . Let *<sup>c</sup>* ∈ C(*SL*) 0 with *lhs*(*c*) = (*s*,*t*). Our induction hypothesis states

$$\forall (\mathbf{x}, \mathbf{y}) \in FV(\text{rhs}(\mathbf{c})). \ SL(\mathbf{x}, \mathbf{y}) \neq \emptyset \land \mathbf{s} \in \mathcal{N}\_{entry}$$

a) Suppose that *FV*(*rhs*(*c*)) = ∅. Then *c* must be of the form

$$\mathbf{x}\_{SL}(s,s) \ge id$$

Clearly, *SL*(*s*,*s*) <sup>≠</sup> <sup>∅</sup>, since <sup>ϵ</sup> <sup>∈</sup> *SL*(*s*,*s*).

b) Suppose that *FV*(*rhs*(*c*)) <sup>≠</sup> <sup>∅</sup>. Then *<sup>c</sup>* is either of the form sl-sol-(ii) or sl-sol-(iii). We only consider sl-sol-(iii), since sl-sol-(ii) is similar. So assume that *c* has the form

$$X\_{SL}(\mathbf{s}, t) \ge f\_{\mathbf{c}\_{\text{ret}}} \circ X\_{SL}(n\_0, n\_1) \circ f\_{\mathbf{c}\_{\text{call}}} \circ X\_{SL}(\mathbf{s}, n)$$

such that *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* and (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup>. From *<sup>c</sup>* ∈ C(*SL*) 0 , we conclude (*s*, *<sup>n</sup>*) ∈ V(*SL*) 0 and (*n*0, *n*<sup>1</sup> ) ∈ V(*SL*) 0 . Hence, we can apply the induction hypothesis to (*s*, *n*) and (*n*0, *n*<sup>1</sup> ) and yield *s* ∈ *Nentry*, π ′ ∈ *SL*(*s*, *n*) and π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ). These two paths can be used to obtain

$$
\pi \stackrel{def}{=} \pi' \cdot e\_{call} \cdot \pi'' \cdot e\_{ret} \in SL(s, t)\_{\prime \prime}
$$

as desired.

□

In order to complete the proof of Theorem 7.18, we show the definitioncompleteness of C (*SL*) 0 with respect to Core(C (*SL*) , *N* × *N*).

**Lemma 7.20.** C (*SL*) 0 *is definition-complete with respect to its superset*

$$\text{Core}(\mathcal{C}^{(SL)}, \mathcal{N} \times \mathcal{N}).$$

*Proof.* We use Theorem 7.10. Let *c* ∈ Core(C (*SL*) , *N* × *N*) with *lhs*(*c*) ∈ V (*SL*) 0 . Then we have to show

$$(7.32) \qquad \qquad FV(rhs(c)) = \emptyset \implies lhs(c) \in X\_0^{(SL)}$$

*FV*(*rhs*(*c*)) <sup>≠</sup> ∅ ∧ *<sup>x</sup>* <sup>∈</sup> *FV*(*rhs*(*c*)) =<sup>⇒</sup> *<sup>x</sup>* ∈ CoreVars(<sup>C</sup> (*SL*) , *X* (*SL*) 0 (7.33) )

• For (7.32), assume that *FV*(*rhs*(*c*)) = ∅. Then *c* is of the form

*XSL*(*s*,*s*) ≥ *id*

From *lhs*(*c*) = (*s*,*s*) ∈ V(*SL*) 0 we get *s* ∈ *Nentry* by (7.27). Hence, (*s*,*s*) ∈ *X* (*SL*) 0 .

	- 1. Let *c* be of the form sl-sol-(iii), i.e.

$$X\_{SL}(s, t) \ge f\_{\varepsilon\_{\rm ret}} \circ X\_{SL}(n\_0, n\_1) \circ f\_{\varepsilon\_{\rm call}} \circ X\_{SL}(s, n)$$

with *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* and (*ecall*,*eret*) <sup>∈</sup> <sup>Φ</sup>. From *lhs*(*c*) = (*s*,*t*) ∈ V(*SL*) 0 , we conclude *s* ∈ *Nentry* by (7.27). Since *<sup>c</sup>* ∈ C(*SL*) 0 , we have

$$(\star) \qquad \qquad X\_{SL}(\mathbf{s}, n) \in \mathsf{CoreVars}(\mathcal{C}^{(SL)}, N \times N) \text{, and } n$$

$$(\star \star) \qquad \quad X\_{SL}(n\_0, n\_1) \in \text{CoreVars}(\mathcal{C}^{(SL)}, N \times N).$$

We apply (7.29) and conclude from *s* ∈ *Nentry*, (⋆) and (⋆⋆) that *XSL*(*s*, *n*) ∈ CoreVars(C (*SL*) , *X* (*SL*) 0 ). Moreover, note that *n*<sup>0</sup> ∈ *Nentry*, since it has an incoming call edge. Hence, *XSL*(*n*0, *n*<sup>1</sup> ) ∈ V(*SL*) 0 by (7.29).

With (C-Step), it follows from (⋆) and (⋆⋆) that *<sup>c</sup>* ∈ C(*SL*) 0 , and, with (V-Step), (*s*, *<sup>t</sup>*) ∈ V(*SL*) 0 . This concludes the proof of (7.33) for *c*.

2. The argument for the form sl-sol-(ii) is very similar.

□

# **7.3.2 Computing the Ascending Solution**

In the following, I consider the constraint system from Constraint System 6.2, which I denote with C (*ASC*) . I define

$$(\text{7.34})\qquad\qquad X\_0^{(\text{ASC})\ d} \stackrel{def}{=} \{(s,s) \mid s \in \text{Src}\}$$

$$\text{(7.35)}\qquad\qquad\mathcal{C}\_{0}^{(\text{ASC})} \stackrel{def}{=} \text{Core}(\mathcal{C}^{(\text{ASC})}, X\_{0}^{(\text{ASC})})$$

$$(\text{7.36}) \qquad \qquad \mathcal{V}\_0^{(\text{ASC})} \stackrel{def}{=} \text{CoreVars}(\mathcal{C}^{(\text{ASC})}, X\_0^{(\text{ASC})})$$

Algorithm 10 is a straight-forward instantiation of Algorithm 8 to solve Constraint System 6.2 with respect to a function

$$
\mathbb{X}\_{\mathrm{SL}} : \mathrm{N} \times \mathrm{N} \to F\_{\mathbb{B}}.
$$

This function could have been computed by Algorithm 9, but can also have been obtained in any other way.

Theorem 7.21 gives a correctness result for Algorithm 10. Similar to Theorem 7.18, the first item in Theorem 7.21 directly follows from the generic result Theorem 7.11, once I have shown that C (*ASC*) 0 is definitioncomplete with respect to C (*ASC*) . However, this first item only is concerned with the relation between the result of Theorem 7.18 and the least solution of C (*ASC*) . In order to get full correctness and precision results, I need additional requirements for *XSL*, relative to a set Ψ ⊆ *N* × *N*.

In principle, there are multiple choices for Ψ. I just need to ensure that (a) Ψ is large enough so that C (*ASC*) and Algorithm 10 never access *XSL* outside of Ψ and that (b) the output of Algorithm 9 is indeed (*SL*, Ψ)-correct. The following choice of Ψ has both properties:

$$\begin{aligned} (\mathsf{T}.\mathsf{37}) \quad (n\_0, n\_1) \in \mathsf{Y} \stackrel{def}{\Leftrightarrow} \quad (n\_0, n\_1) \in \mathsf{N}\_{\mathsf{entity}} \times \mathsf{N}\_{\mathsf{exit}}\\ \wedge \exists n, t \in \mathsf{N}. \; \exists (e\_{\mathsf{call}}, e\_{\mathsf{ret}}) \in \mathsf{Φ}. \; n \stackrel{e\_{\mathsf{call}}}{\rightarrow} n\_0 \land n\_1 \stackrel{e\_{\mathsf{ret}}}{\rightarrow} t. \end{aligned}$$

Property (a) can be seen by inspection of C (*ASC*) and Algorithm 10. Moreover, <sup>Ψ</sup> also has property (b). (*SL*, <sup>Ψ</sup>)-correctness of <sup>A</sup>(*SL*) follows from the (*SL*, V (*SL*) 0 )-correctness of <sup>A</sup>(*SL*) and the fact that <sup>Ψ</sup> <sup>⊆</sup> *<sup>N</sup>entry* <sup>×</sup> *<sup>N</sup>*, which entails

$$\left\{\Psi \cap \left\{ (\mathbf{s}, t) \mid SL(\mathbf{s}, t) \neq \emptyset \right\} \subseteq \mathrm{N}\_{entry} \times \mathrm{N} \cap \left\{ (\mathbf{s}, t) \mid SL(\mathbf{s}, t) \neq \emptyset \right\} \stackrel{\left(\mathbf{7.27}\right)}{=} \mathcal{V}\_{0}^{(\mathrm{SL})}.\right\}$$

Thus, if (*s*,*t*) <sup>∈</sup> <sup>Ψ</sup> with <sup>π</sup> <sup>∈</sup> *SL*(*s*,*t*), then (*s*,*t*) ∈ V(*SL*) 0 and hence, by Theorem 7.18, *<sup>f</sup>*<sup>π</sup> ≤ A(*SL*) (*s*, *t*).

A similar argument shows that (*SL*, V (*SL*) 0 )-precision of <sup>A</sup>(*SL*) implies (*SL*, <sup>Ψ</sup>)-precision of <sup>A</sup>(*SL*) for universally distributive frameworks.

Using an appropriate choice of Ψ and assuming that C (*ASC*) 0 is definitioncomplete with respect to C (*ASC*) , I can state the correctness property of Theorem 7.21.

**Theorem 7.21.** *Let XSL* : *N* × *N* → *F*<sup>⊠</sup> *be a function and let* C (*ASC*) *the set of constraints in Constraint System 6.2 with respect to XSL. Consider XSL as input for Algorithm 10.*

*1. Algorithm 10 terminates and upon termination, we have*

$$\text{(7.38)}\qquad \mathcal{H}(\mathbf{s},t) = \begin{cases} \text{lfp}(\mathcal{F}\_{\mathcal{C}^{(\text{ASC})}})(\mathbf{s},t) & \text{if } (\mathbf{s},t) \in \mathcal{V}\_0^{(\text{ASC})}\\ \mathbb{B} & \text{otherwise} \end{cases}$$

*2. If XSL is* (*SL*, Ψ)*-correct, then* A *is* (*ASC*, V (*ASC*) 0 )*-correct.*

*3. If XSL is* (*SL*, Ψ)*-precise and* F *is universally distributive, then* A *is* (*ASC*, V (*ASC*) 0 )*-precise.*

In the following, I am going to prove Theorem 7.21. Firstly, I assume that item 1 is proven and consider items 2 and 3. In order to prove item 2, I need to generalize Theorem 6.7: Remember that Theorem 6.7 states that *l f p*(*F* <sup>C</sup>(*ASC*)) is *ASC*-correct if *<sup>X</sup>SL* is *SL*-correct. But an inspection of its proof shows that actually (*SL*, Ψ)-correctness of *XSL* is sufficient for the *ASC*-correctness of *l f p*(*F* C(*ASC*)). Hence, the proof of Theorem 6.7 actually shows the following statement:

*If XSL is* (*SL*, Ψ)*-correct, then l f p*(*F* C(*ASC*)) *is* (*ASC*, <sup>V</sup> (*ASC*) 0 )*-correct.*

With (7.38), this shows item 2. With a similar argument, item 3 can be shown.

Two things are left to be done. In order to complete the proof of Theorem 7.21, I need to show that C (*ASC*) 0 is definition-complete with respect to C (*ASC*) . Moreover, I need to characterize V (*ASC*) 0 appropriately, to be sure that Theorem 7.21 indeed proves Algorithm 10 correct.

Lemma 7.22 gives a characterization of CoreVars(C (*ASC*) , *N* × *N*) and V (*ASC*) 0 . Analogously to Lemma 7.19, it provides a strong connection to *ASC*-reachability. Since C (*ASC*) is defined with respect to *XSL*, Lemma 7.22 has to make additional assumptions about *XSL*. However, for Lemma 7.22 to be valid, full *SL*-correctness or *SL*-precision of *XSL* is not neccessary. It suffices to require that *XSL* ≠ ⊠ for the right points.

**Lemma 7.22.** *Define dom*(*ASC*) *de f* <sup>=</sup> {(*s*, *<sup>t</sup>*) <sup>∈</sup> *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>|</sup> *ASC*(*s*, *<sup>t</sup>*) <sup>≠</sup> ∅}*. Then the following statements are true:*

*1. If XSL is* (*SL*, Ψ)*-domain-correct, then we have*

$$(\mathsf{T}.\mathsf{39})\qquad\qquad\text{dom}(\mathsf{A}\mathsf{SC})\cap\mathsf{S}\mathsf{rc}\times\mathsf{N}\subseteq\mathsf{V}\_{0}^{(\mathsf{A}\mathsf{SC})}$$

**Algorithm 10:** Given a function *XSL* : *N* × *N* → *F*⊠, computes the least ascending solution with respect to *XSL*

**Input:** a data-flow framework instance F = (*G*, *L*, *F*⊠, ρ) as described on page 295, a function *XSL* : *N* × *N* → *F*<sup>⊠</sup> **Result:** the least ascending solution with respect to *XSL*, as stated in Theorem 7.21 **<sup>1</sup>** A ← *const*(⊠) **<sup>2</sup> foreach** *s* ∈ *Src* **do <sup>3</sup>** A(*s*,*s*) ← *id* **<sup>4</sup>** *W* ← *W* ∪ {(*s*,*s*)} **<sup>5</sup> while** *<sup>W</sup>* <sup>≠</sup> <sup>∅</sup> **do <sup>6</sup>** (*s*, *t* ′ ) ← *remove*(*W*) **<sup>7</sup> foreach** *e* ∈ *Eintra* ∪ *Eret such that t*′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>* **do <sup>8</sup>** A(*s*, *t*) ← A(*s*, *t*) ⊔ *f<sup>e</sup>* ◦ A(*s*, *t* ′ ) **<sup>9</sup>** *W* ← *W* ∪ {(*s*, *t*)} if A(*s*, *t*) has changed **<sup>10</sup> foreach** (*ecall*,*eret*) ∈ Φ *such that t* ′ *<sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*<sup>0</sup> <sup>∧</sup> *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* <sup>∧</sup> *XSL*(*n*0, *n*<sup>1</sup> ) ≠ ⊠ **do <sup>11</sup>** *sameLevelIn f o* ← *feret* ◦ *XSL*(*n*0, *n*<sup>1</sup> ) ◦ *fecall* **<sup>12</sup>** A(*s*, *t*) ← A(*s*, *t*) ⊔ *sameLevelIn f o* ◦ A(*s*, *t* ′ ) **<sup>13</sup>** *W* ← *W* ∪ {(*s*, *t* ′ )} if A(*s*, *t*) has changed

$$\text{и } \texttt{return } \mathcal{R}$$

$$(\text{7.40})\qquad\qquad\text{dom}(\text{ASC})\subseteq\text{CoreValues}(\mathcal{C}^{(\text{ASC})}, \mathcal{N}\times\mathcal{N})$$

*2. If XSL is* (*SL*, Ψ)*-domain-precise, then we have*

$$(7.41)\tag{7.41} \qquad \qquad \mathcal{V}\_0^{(\text{ASC})} \subseteq \text{dom}(\text{ASC}) \cap \text{Src} \times N$$

$$(7.42)\qquad \text{CoreVars}(\mathcal{C}^{(ASC)}, N \times N) \subseteq \text{dom}(\text{ASC})$$

$$\text{3. } \mathcal{V}\_0^{(\text{ASC})} \text{ and } \mathsf{CoreValues}(\mathcal{C}^{(\text{ASC})}, \mathsf{N} \times \mathsf{N}) \text{ have the following connection:} $$

$$(7.43)\qquad\mathcal{V}\_0^{(ASC)} = \text{CoreVars}(\mathcal{C}^{(ASC)}, N \times N) \cap \text{Src} \times N.$$

*Proof.* Regarding the first two items, we only show (7.39) and (7.41), since (7.40) and (7.42) can be proven using very similar arguments.

1. Assume that *XSL* is (*SL*, Ψ)-domain-correct. Then (7.39) is implied by the statement

$$(7.44) \quad \forall \pi \in A \text{SC. } \forall (s, t) \in \text{Src} \times \text{N. } \pi \in A \text{SC}(s, t) \implies (s, t) \in \mathcal{V}\_0^{(A \text{SC})}.$$

We prove this statement by induction on π ∈ *ASC*. The proof is largely similar to the corresponding part of the proof of Lemma 7.19. We only highlight the parts where the assumptions about *XSL* are used. Hence, we only consider the case that π = π ′ · *ecall* · π ′′ · *eret* with *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *n*1 *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>*, <sup>π</sup> ′ ∈ *ASC*(*s*, *n*), π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ) and (*ecall*,*eret*) ∈ Φ. Note that (*n*0, *n*<sup>1</sup> ) ∈ Ψ. With the (*SL*, Ψ)-domain-correctness of *XSL* this means that *XSL*(*n*0, *n*<sup>1</sup> ) <sup>≠</sup> <sup>⊠</sup>. Hence, by asc-sol-(iii), <sup>C</sup> (*ASC*) contains a constraint

$$X(s, t) \ge f\_{\varepsilon\_{ret}} \circ X(n\_0, n\_1) \circ f\_{\varepsilon\_{call}} \circ X(s, n).$$

By induction hypothesis, we get (*s*, *<sup>n</sup>*) ∈ V(*ASC*) 0 . We conclude

$$X(s,t) \in \mathcal{V}\_0^{(A\mathcal{SC})}$$

by (V-Base).

2. Assume that *XSL* is (*SL*, Ψ)-domain-precise. In order to show (7.42), we prove

$$(7.45) \qquad \qquad \forall (s, t) \in \mathcal{V}\_0^{(ASC)}.\newline ASC(s, t) \neq \emptyset$$

by induction on (*s*, *<sup>t</sup>*) ∈ V(*ASC*) 0 = CoreVars(C (*ASC*) , *X* (*ASC*) 0 ). So let (*s*, *t*) ∈ V (*ASC*) 0 . Then there is *<sup>c</sup>* ∈ C(*ASC*) 0 with *lhs*(*c*) = (*s*,*t*). Because the other cases are similar to the proof of Lemma 7.19, we only consider the case that *c* is of the form

$$X\_{\rm{ASC}}(s, t) \ge f\_{\varepsilon\_{\rm{rel}}} \circ X\_{\rm{SL}}(n\_0, n\_1) \circ f\_{\varepsilon\_{\rm{call}}} \circ X\_{\rm{ASC}}(s, n)$$

with (*ecall*,*eret*) ∈ Φ, *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* and *<sup>X</sup>SL*(*n*0, *<sup>n</sup>*<sup>1</sup> ) ≠ ⊠. Then we have (*n*0, *n*<sup>1</sup> ) ∈ Ψ. Since *XSL* is (*SL*, Ψ)-domain-precise, *XSL*(*n*0, *n*<sup>1</sup> ) ≠ ⊠ implies that there is π ′′ ∈ *SL*(*n*0, *n*<sup>1</sup> ). Furthermore, we may apply the induction hypothesis to (*s*, *n*) and get π ′ ∈ *ASC*(*s*, *n*). Together with the assumptions about *ecall* and *eret*, we get π ′ ·*ecall* · π ′′ ·*eret* ∈ *ASC*(*s*, *t*), as desired.

	- For ⊆, it is clear that

$$\mathcal{V}\_0^{(A\text{SC})} \subseteq \text{CoreVars}(\mathcal{C}^{(A\text{SC})}, N \times N)\_{\prime \prime}$$

since *Src* × *N* ⊆ *N* × *N* and CoreVars(C (*ASC*) , \_) is monotone.

It remains to show

$$\forall (\mathbf{s}, t) \in \mathcal{V}\_0^{(A \mathbf{S} \mathbf{C})}. (\mathbf{s}, t) \in \mathbf{S} \mathbf{c} \times \mathbf{N}.$$

The proof is a straight-forward induction along V (*ASC*) 0 , where the induction steps work by noticing that the first components of the variables on the left-hand and right-hand sides of constraints in V (*ASC*) 0 are the same.

• For ⊇, induction on (*s*, *t*) ∈ CoreVars(C (*ASC*) , *N* × *N*) shows that

$$\begin{aligned} \mathsf{V}(s,t) \in \mathsf{CoreVars}(\mathsf{C}^{(\text{ASC})}, \mathsf{N} \times \mathsf{N}). \ (s,t) \in \mathsf{Src} \times \mathsf{N} \\ \implies (s,t) \in \mathsf{V}\_0^{(\text{ASC})}. \end{aligned}$$

□

With Lemma 7.22, I can provide the last puzzle piece to the proof of Theorem 7.21: The definition-completeness of C (*ASC*) 0 with respect to Core(C (*ASC*) , *N* × *N*).

**Lemma 7.23.** C (*ASC*) 0 *is definition-complete with respect to its superset*

$$\text{Core}(\mathcal{C}^{(A\mathcal{C})}, \mathcal{N} \times \mathcal{N})\text{.}$$

*Proof.* Like in the proof of Lemma 7.20, we use Theorem 7.10. Consider *c* ∈ Core(C (*ASC*) , *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>*) with (*s*,*t*) = *lhs*(*c*) ∈ V(*ASC*) 0 . Then we have to show

$$(7.46)\newline\qquad FV(rhs(c)) = \emptyset \implies (s,t) \in X\_{0}^{(ASC)}$$

$$(7.47) \quad (s', t') \in FV(rhs(c)) \implies (s', t') \in \text{CoreVars}(\mathcal{C}^{(\text{ASC})}, X\_0^{(\text{ASC})})$$

As the other cases are very similar, we only consider the case that *c* ∈ Core(C (*ASC*) , *N* × *N*) is of the form

$$X\_{\rm ASC}(s, t) \ge f\_{\varepsilon\_{\rm ret}} \circ X\_{\rm SL}(n\_0, n\_1) \circ f\_{\varepsilon\_{\rm call}} \circ X\_{\rm ASC}(s, n)$$

with (*ecall*,*eret*) ∈ Φ, *n <sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*0, *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>*, *<sup>X</sup>SL*(*n*0, *<sup>n</sup>*<sup>1</sup> ) <sup>≠</sup> <sup>⊠</sup>, and (*s*,*t*) <sup>∈</sup> CoreVars(C (*ASC*) , *X* (*ASC*) 0 ). We need to show

$$(s, n) \in \text{CoreVars}(\mathcal{C}^{(A\text{SC})}, X\_0^{(A\text{SC})}) .$$

But note that

(*s*, *n*) ∈ CoreVars(C (*ASC*) , *N* × *N*), and(1)

*s* ∈ *Src*.(2)

Statement (1) is implied by *c* ∈ Core(C (*ASC*) , *N* × *N*), while statement (2) follows, by application of Lemma 7.22, from (*s*,*t*) ∈ V(*ASC*) 0 . Both statements together imply (*s*, *<sup>n</sup>*) ∈ V(*ASC*) 0 by Lemma 7.22. □

# **7.3.3 Extending the Solution Along the Non-Ascending Paths**

Constraint System 7.1 is a modified version of Constraint System 6.3 that describes the data flows along the non-ascending paths.

The constant constraints of Constraint System 7.1 use the values *XASC*(*s*, *c*) with *XASC*(*s*, *c*) ≠ ⊠ to initialize the solution. Note that *c* can be restricted to *Ncall* according to Lemma 7.16. The non-constant constraints then extend the solution along the descending paths.

**Constraint System 7.1.** *Given functions XASC*, *XSL* : *N* × *N* → *F*⊠*, the function*

$$\chi\_{\text{NASC}} : N \times N \to F\_{\mathbf{E}}$$

*is a* non-ascending solution *with respect to XASC and XSL if it satisfies all constraints from the following system:*

$$\begin{aligned} \text{(\text{NON-ASC-(t)})} & \frac{t' \xrightarrow{\varepsilon} t \qquad e \in E\_{\text{call}} & X\_{\text{ASC}}(s, t') \neq \mathtt{E}}{X\_{\text{NASC}}(s, t) \ge f\_{\varepsilon} \circ X\_{\text{ASC}}(s, t')} \\\\ & \text{(\text{NON-asc-(n)})} \frac{t' \xrightarrow{\varepsilon} t \qquad e \in E\_{\text{intrta}} \cup E\_{\text{call}}}{X\_{\text{NASC}}(s, t) \ge f\_{\varepsilon} \circ X\_{\text{NASC}}(s, t')} \end{aligned}$$

$$\varepsilon\_{\text{call}} \qquad \varepsilon\_{\text{ret}, \varepsilon\_{\text{act}}}(\varepsilon\_{\text{act}}, \varepsilon\_{\text{act}}, \varepsilon\_{\text{act}}, \varepsilon\_{\text{act}})$$

$$\left(\text{(non-asc-(un))}\right) \frac{n \stackrel{\mathcal{e\_{call}}}{\rightarrow} n\_0}{\text{X}\_{\text{NASC}}(s, t) \ge f\_{\mathcal{E}\_{\text{rt}}} \circ \text{X}\_{\text{SL}}(n\_0, n\_1) \circ \mathbb{1}} \left(\text{>} \frac{1}{n}\right) \to \mathbb{1}$$

Using a similar argument as in Theorem 6.7, I can prove that Constraint System 7.1 indeed describes *NASC*-correct solutions, provided that *XSL* and *XASC* enjoy their respective correctness properties.

**Theorem 7.24.** *Let XSL* : *N* × *N* → *F*<sup>⊠</sup> *be SL-correct, XASC* : *N* × *N* → *F*<sup>⊠</sup> *be ASC-correct and let XNASC be a NASC-solution with respect to XSL and XASC. Then XNASC is NASC-correct.*

*Proof.* Let *s* ∈ *N*, *c* ∈ *Ncall* and π<sup>1</sup> ∈ *ASC*(*s*, *c*). Using a straight-forward induction along π<sup>2</sup> ∈ *DESC* with arguments similar to the ones in the proof of, e.g., Theorem 6.6, we show

$$\begin{aligned} \forall \pi\_2 \in \text{DECC. } \forall t \in \mathsf{N}. \\ \pi\_2 \in \text{DECC}(\mathsf{c}, t) \land \pi\_1 \cdot \pi\_2 \notin \text{ASC}(\mathsf{s}, t) \\ \implies f\_{\pi\_1 \cdot \pi\_2} \le \mathsf{X}\_{\text{NASC}}(\mathsf{s}, t). \end{aligned}$$

Using Definition 7.15, this shows that

$$\forall \pi \in \text{NASC}. \,\forall s, t \in \text{N}. \pi \in \text{NASC}(s, t) \implies f\_{\pi} \le \text{X}\_{\text{NASC}}(s, t)\_{\prime t}$$

as desired. □

Moreover, for universally distributive frameworks and with the respective precision requirements satisfied for *XASC* and *XSL*, the least solution of Constraint System 7.1 coincides with *MONASC*. The proof is very similar to the proof of Theorem 6.10 and is not repeated here.

**Algorithm 11:** computes the least non-ascending solution on

```
V
  (NASC)
  0
         with respect to XASC and XSL
```

```
Input: a data-flow framework instance F = (G, L, F⊠, ρ) as de-
         scribed on page 295, functions XSL, XASC : N × N → F⊠
  Result: the least non-ascending solution with respect to XSL, as
          stated in Theorem 7.26
1 A ← const(⊠)
2 foreach (s, c) ∈ Src × N with XASC(s, c) ≠ ⊠ do
3 foreach e ∈ Ecall, t ∈ N such that c e→ t do
4 A(s, t) ← A(s, t) ⊔ fe ◦ XASC(s, c)
5 W ← W ∪ {(s, t)}
6 while W ≠ ∅ do
7 (s, t) ← remove(W)
8 foreach e ∈ Eintra ∪ Ecall such that t e→ t
                                            ′ do
9 A(s, t
               ′
                ) ← A(s, t
                          ′
                           ) ⊔ fe ◦ A(s, t)
10 W ← W ∪ {(s, t
                        ′
                         )} if A(s, t
                                   ′
                                    ) has changed
11 foreach (ecall,eret) ∈ Φ such that t
                                          ecall → n0 ∧ n1
                                                       eret → t
                                                            ′ ∧
       XSL(n0, n1
                 ) ≠ ⊠ do
12 sumIn f o ← feret ◦ XSL(n0, n1
                                     ) ◦ fecall
13 A(s, t
               ′
                ) ← A(s, t
                          ′
                           ) ⊔ sumIn f o ◦ A(s, t)
14 W ← W ∪ {(s, t
                        ′
                         )} if A(s, t
                                   ′
                                    ) has changed
15 return A
```
**Theorem 7.25.** *Let* F *be universally distributive. Furthermore, let XSL be SL-precise and XASC be ASC-precise. Then* MO*NASC is a NASC-solution with respect to XSL and XASC. In particular, l f p*(*F*C*NASC*) *is NASC-precise, if* C*NASC is defined with respect to XASC and XSL.*

All that remains to do is to instantiate Algorithm 8 appropriately and prove the usual correctness result. The instantiation can be seen in Algorithm 11. I fix two functions

$$X\_{A \to \prime} X\_{SL} : N \times N \to F\_{\mathbb{E} \mathcal{A}}$$

and let C (*NASC*) be the set of constraints defined by Constraint System 7.1, with respect to *XSL* and *XASC*. I define

$$(7.48)\qquad X\_0^{(NASC)} \stackrel{def}{=} \{ (\text{s.c}) \mid \text{s} \in \text{Src} \land \text{c} \in \text{N}\_{\text{call}} \land X\_{A \text{SC}}(\text{s}, \text{c}) \neq \text{B} \}$$

$$(7.49)\qquad \mathcal{C}\_0^{(NASC)} \stackrel{def}{=} \text{Core}(\mathcal{C}^{(NASC)}, X\_0^{(ASC)})^\dagger$$

V (*NASC*) 0 *de f* = CoreVars(C (*NASC*) , *X* (*ASC*) 0 (7.50) )

Like before, an inspection of Constraint System 7.1 shows that Algorithm 11 is indeed an instance of Algorithm 8. Thus, assuming definitioncompleteness of C (*NASC*) 0 in Core(C (*NASC*) , *N* × *N*), I can give the following correctness result for Algorithm 11.

**Theorem 7.26.** *Let XSL*, *XASC* : *N* × *N* → *F*<sup>⊠</sup> *be two functions and let* C*NASC be the instance of Constraint System 7.1 with respect to XSL and XASC. Consider XSL and XASC as input for Algorithm 11.*

*1. Algorithm 11 terminates and upon termination, we have*

$$\text{(7.51)}\qquad \mathcal{A}(\mathbf{s},t) = \begin{cases} lfp(\mathcal{F}\_{\mathcal{C}\_{\text{NASC}}})(\mathbf{s},t) & \text{if } \mathbf{s} \in \mathcal{V}\_0^{(\text{NASC})}\\ \boxtimes & \text{otherwise.} \end{cases}$$

*2. If XSL is* (*SL*, Ψ)*-correct and XASC is* (*ASC*, *Src* × *N*)*-correct, then* A *is* (*NASC*, V (*NASC*) 0 )*-correct.*

*3. If* F<sup>⊠</sup> *is u.d., XSL is* (*SL*, Ψ)*-precise, and XASC is* (*ASC*, *Src* × *N*)*-precise, then* A *is* (*NASC*, V (*NASC*) 0 )*-precise.*

In the following, I sketch a proof for Theorem 7.26. First, assume that the first item is shown. Then the second item follows if I can show that *l f p*(*F* C(*NASC*)) is (*NASC*, <sup>V</sup> (*NASC*) 0 )-correct. But this can be concluded from the (*SL*, Ψ)-correctness of *XSL* and the (*ASC*, *Src* × *N*)-correctness of *XASC* with a similar argument as in the proof of Constraint System 7.1.

The third item in Theorem 7.26 can be shown similarly by adapting Theorem 7.25 appropriately.

It remains to characterize the variableV (*NASC*) 0 and to prove the definition-completeness of C (*NASC*) 0 in Core(C (*NASC*) , *N* × *N*). Lemma 7.27 gives a characterization of V (*NASC*) 0 that is analogous to Lemma 7.22.

**Lemma 7.27.** *Define dom*(*NASC*) *de f* <sup>=</sup> {(*s*, *<sup>t</sup>*) <sup>∈</sup> *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>|</sup> *NASC*(*s*, *<sup>t</sup>*) <sup>≠</sup> ∅}*. Then the following statements are true:*

*1. If XSL is*(*SL*, Ψ)*-domain-correct and XASC is*(*ASC*, *Src*×*N*)*-domain-correct, then we have*

$$(7.52) \qquad dom(\text{NASC}) \cap \text{Src} \times \text{N} \subseteq \mathcal{V}\_0^{(\text{NASC})}$$

*dom*(*NASC*) ⊆ CoreVars(C (*NASC*) (7.53) , *N* × *N*)

*2. If XSL is* (*SL*, Ψ)*-domain-precise and XASC is* (*ASC*, *Src*×*N*)*-domain-precise, then we have*

$$(7.54) \qquad \qquad \mathcal{V}\_0^{(NASC)} \subseteq dom(NASC) \cap src \times N$$

$$(7.55)\qquad \text{CoreVars}(\mathcal{C}^{(NASC)}, N \times N) \subseteq dom(\text{NASC})$$

$$\text{3. } \mathcal{V}\_0^{(\text{NASC})} \text{ and } \text{CoreVars}(\mathcal{C}^{(\text{NASC})}, \text{N} \times \text{N}) \text{ have the following connection:} $$

$$(7.56)\qquad \mathcal{V}\_0^{(NASC)} = \text{CoreVars}(\mathcal{C}^{(NASC)}, N \times N) \cap \text{Src} \times N.$$

*Proof.* 1. We only consider (7.52), since the proof of (7.53) is very similar. Assume that *XSL* is (*SL*, Ψ)-domain-correct and that *XASC* is (*ASC*, *Src*×*N*) domain-correct. Let *s*, *c* ∈ *N* and π<sup>1</sup> ∈ *ASC*(*s*, *c*). Then, we can show

$$\begin{aligned} \forall \pi\_2 \in \text{DECC. } \forall t \in \text{N. } \pi\_2 \in \text{DECC}(c, t) \land \pi\_1 \cdot \pi\_2 \notin \text{ASC}(s, t) \\ \implies (s, t) \in \mathcal{V}\_0^{(\text{NASC})} \end{aligned}$$

by induction on π<sup>2</sup> ∈ *DESC*. The details are very similar to the corresponding part of the proof of Lemma 7.22 and are omitted here. By Definition 7.15, this implies (7.52).

2. Assume that *XSL* is (*SL*, Ψ)-domain-precise and, moreover, that *XASC* is (*ASC*, *Src* × *N*)-domain-precise. Then we can show

$$\forall (\mathbf{s}, t) \in \mathcal{V}\_0^{(\text{NASC})} . (\mathbf{s}, t) \in dom(\text{NASC}) \cap \mathbf{Src} \times \mathbf{N}$$

by induction on (*s*,*t*) ∈ V(*NASC*) 0 . For each case, the argument uses a combination of the respective induction hypothesis and the assumptions about *XSL* and *XASC* to obtain an appropriate non-ascending path. The details are very similar to Lemma 7.22 and I do not repeat them here. The proof of (7.54) is similar.

3. This is completely analogous to the proof of (7.43).

Using Lemma 7.27, the definition-completeness of C (*NASC*) 0 with respect to its superset Core(C (*NASC*) , *N* × *N*) can be shown. The proof is completely analogous to the proofs of Lemma 7.23 and Lemma 7.20 and is omitted here.

□

**Lemma 7.28.** Core(C (*ASC*) , *X*0) *is definition-complete with respect to its superset* Core(C*NASC*, *N* × *N*)*.*

# **7.3.4 Combining the Ascending Solution and the Non-Ascending Solution**

Constraint System 7.2 combines two given functions *XASC* and *XNASC* by simply joining them.

**Constraint System 7.2.** *Given functions XASC*, *XNASC* : *N* × *N* → *F*⊠*, the function*

$$X\_{VP'}: N \times N \to F\_{\mathbb{E}}$$

*is an* alternative valid-paths-solution *with respect to XASC and XNASC if it satisfies all constraints from the following system:*

$$\left(\text{vp}\,'\text{-(t)}\right)\,\frac{\text{X}\_{\text{ASC}}(\text{s},t)\neq\mathbb{B}}{\text{X}\_{\text{VP}'}(\text{s},t)\geq\text{X}\_{\text{ASC}}(\text{s},t)}\qquad\left(\text{vp}\,'\text{-(tu)}\right)\,\frac{\text{X}\_{\text{NASC}}(\text{s},t)\neq\mathbb{B}}{\text{X}\_{\text{VP}'}(\text{s},t)\geq\text{X}\_{\text{NASC}}(\text{s},t)}$$

Constraint System 7.2 is so simple that I can give a clear and direct characterization of *l f p*(*F* C(*VP*′)), which is easy to see:

$$\text{(7.57)}\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\text{!}\\fp(F\_{C^{(VP')}}) = X\_{A\text{SC}} \sqcup X\_{NA\text{SC}}.$$

Alternative valid-paths solutions are indeed *VP*-correct, if *XASC* and *XDESC* satisfy their respective correctness conditions. This is an easy consequence of Remark 7.17, which is why I omit the proof.

**Theorem 7.29.** *Let XASC*, *XNASC* : *N* × *N* → *F*<sup>⊠</sup> *be two functions. Then the following statements hold:*

*1. Let XVP*′ *be an alternative valid-paths solution with respect to XASC and XNASC. If XASC is ASC-correct and XNASC is NASC-correct, then XVP*′ *is VP-correct.*

*2. If XASC is ASC-precise and XNASC is NASC-precise, then l f p*(*F*C*VP*′ ) *is VP-precise.*

In chapter 6, we considered Constraint System 6.4 as a characterization for valid-paths solutions. For the following considerations, let C (*VP*) be the set of constraints from Constraint System 6.4.

Note that Theorem 7.29 does not say anything about the relation of *l f p*(*F* C(*VP*)) and *l f p*(*<sup>F</sup>* C(*VP*′)) in general, even if they are considered with respect to the same helper solutions. As C (*VP*) and <sup>C</sup> (*VP*′ ) make different choices about when they join and when they compose, we cannot expect in general that they coincide36. In general, <sup>C</sup> (*VP*) and <sup>C</sup> (*VP*′ ) can simply be seen as alternative approaches to characterize an over-approximation to *MOVP*. This is especially relevant if F is not distributive and the helper solutions are correct and as precise as possible (e.g. if they were obtained using the algorithms from the previous sections), but also if the helper solutions themselves are only over-approximations, even for the distributive case.

<sup>36</sup>My conjecture is (a) that *l f p*(*F*C(*VP*) ) <sup>≤</sup> *l f p*(*F*C(*VP*′) ) under relatively general assumptions and (b) that, under the same assumptions, there are examples for which *l f p*(*F*C(*VP*′) ) ̸≤ *l f p*(*F*C(*VP*) ), since <sup>C</sup> (*VP*) "joins later" than <sup>C</sup> (*VP*′ ) . I will however not attempt to prove this within the scope of this thesis.

For distributive frameworks and the most precise helper solutions, *l f p*(*F* C(*VP*′)) can be shown to coincide with *l f p*(*<sup>F</sup>* C(*VP*)).

**Corollary 7.30.** *Assume that* F *is universally-distributive. Consider the following constraint systems:*

$$\begin{aligned} &\mathcal{C}^{(ASC)} \text{ with respect to } lfp(\mathcal{F}\_{\mathcal{C}^{(SL)}})\\ &\mathcal{C}^{(DESC)} \text{ with respect to } lfp(\mathcal{F}\_{\mathcal{C}^{(SL)}})\\ &\mathcal{C}^{(NASC)} \text{ with respect to } lfp(\mathcal{F}\_{\mathcal{C}^{(SL)}}) \text{ and } lfp(\mathcal{F}\_{\mathcal{C}^{(ASC)}})\\ &\mathcal{C}^{(VP)} \text{ with respect to } lfp(\mathcal{F}\_{\mathcal{C}^{(ASC)}}) \text{ and } lfp(\mathcal{F}\_{\mathcal{C}^{(DESC)}}), \text{and}\\ &\mathcal{C}^{(VP')} \text{ with respect to } lfp(\mathcal{F}\_{\mathcal{C}^{(ASC)}}) \text{ and } lfp(\mathcal{F}\_{\mathcal{C}^{(NASC)}}). \end{aligned}$$

*Then l f p*(*F* C(*VP*)) = *l f p*(*<sup>F</sup>* C(*VP*′))*.*

*Proof.* By Theorem 6.7 and Theorem 6.10, *l f p*(*F* C(*ASC*)) is both *ASC*-correct and *ASC*-precise. Moreover, Theorem 7.24 and Theorem 7.25 imply that *l f p*(*F* C(*NASC*)) is both *NASC*-correct and *NASC*-precise. Hence, Theorem 7.29 implies that *l f p*(*F* C(*VP*′)) is both *VP*-correct and *VP*-precise, i.e.

$$lfp(F\_{\mathcal{C}^{(VP')}}) = MOVP.$$

Analogously, using Theorem 6.7, Theorem 6.8, Theorem 6.10 and Theorem 6.9, one can conclude

$$lfp(F\_{\mathcal{C}^{(VP)}}) = MOVP.$$

□

Lastly, I want to consider the connection between the domain of *l f p*(*F*C*VP*′ ) and forward slices. Remember that the forward slice *FS*(*s*) of a given node *<sup>s</sup>* <sup>∈</sup> *<sup>N</sup>* is the set of nodes *<sup>t</sup>* <sup>∈</sup> *<sup>N</sup>* such that *VP*(*s*, *<sup>t</sup>*) <sup>≠</sup> <sup>∅</sup>.

**Theorem 7.31.** *Let XASC*, *XNASC* : *N* × *N* → *F*<sup>⊠</sup> *be two functions. Then the following statements hold:*

*1. Let XVP*′ *be an alternative valid-paths solution with respect to XASC and XNASC. If XASC is ASC-domain-correct and XNASC is NASC-domain-correct, then XVP*′ *is VP-domain-correct. In particular, we have*

(7.58) ∀*s* ∈ *N*. *FS*(*s*) ⊆ {*t* ∈ *N* | *l f p*(*F*C*VP*′ )(*s*, *<sup>t</sup>*) <sup>≠</sup> <sup>⊠</sup>}.

*2. If XASC is ASC-domain-precise and XNASC is NASC-domain-precise, then l f p*(*F*C*VP*′ ) *is VP-domain-precise. In particular, we have*

$$(7.59) \qquad \forall s \in N. \ (t \in N \mid lfp(F\_{C\_{VP'}})(s, t) \neq \mathbb{B}) \subseteq FS(s).$$

*Proof.* For given *s* ∈ *N*, we define

$$\begin{aligned} \text{FS}\_{\text{ASC}}(\mathbf{s}) & \stackrel{def}{=} \{ t \in \mathbb{N} \mid \text{ASC}(\mathbf{s}, t) \neq \emptyset \}, \text{ and} \\ \text{FS}\_{\text{NASC}}(\mathbf{s}) & \stackrel{def}{=} \{ t \in \mathbb{N} \mid \text{NASC}(\mathbf{s}, t) \neq \emptyset \}. \end{aligned}$$

By Lemma 7.16, we have *FS*(*s*) = *FSASC*(*s*) ∪ *FSNASC*(*s*) for all *s* ∈ *N*. Moreover, by definition we have


From this, all claimed statements can be shown using (7.57). □

# **7.3.5 Putting It All Together**

In this section, I combine the algorithms from the previous sections to yield algorithms that compute the least alternative valid-paths solution with the most precise helper solutions.

First, I show a simple algorithm that is always correct and then I consider a variant that works for distributive frameworks.

The simple approach, which can be seen in Algorithm 12, takes the set *Src* <sup>⊆</sup> *<sup>N</sup>* of source nodes as input and uses the pre-computed result <sup>A</sup>(*SL*) of running Algorithm 9 on *X* (*SL*) 0 .

As its first step, Algorithm 12 invokes Algorithm 10 to compute the least ascending-paths solution <sup>A</sup>(*ASC*) with respect to <sup>A</sup>(*SL*) . After that, it runs Algorithm 11 to obtain the least non-ascending solution <sup>A</sup>(*NASC*) with respect to <sup>A</sup>(*SL*) and <sup>A</sup>(*ASC*) . Finally, it joins <sup>A</sup>(*ASC*) and <sup>A</sup>(*NASC*) to obtain its result <sup>A</sup>(*VP*′ ) .

For Algorithm 12 I can give the following simple correctness result.

**Theorem 7.32.** *Consider*

$$\begin{aligned} & \mathcal{C}^{(VP')} \text{ with respect to } lfp(\mathcal{C}^{(ASC)}) \text{ and } lfp(\mathcal{C}^{(NASC)})\\ & \mathcal{C}^{(NASC)} \text{ with respect to } lfp(\mathcal{C}^{(ASC)}) \text{ and } lfp(\mathcal{C}^{(SL)}), and\\ & lfp(\mathcal{C}^{(ASC)}) \text{ with respect to } lfp(\mathcal{C}^{(SL)}). \end{aligned}$$

*Then Algorithm 12 has the following properties:*

*1. Algorithm 12 always terminates and upon termination, we have*

$$(\mathsf{T}.60)\qquad\mathscr{A}^{(VP')}(\mathsf{s},t) = \begin{cases} \mathit{lfp}(\mathsf{F}\_{\mathscr{C}^{(VP')}})(\mathsf{s},t) & \mbox{if } t \in \mathsf{V}\_{0}^{(A\mathcal{C})} \cup \mathsf{V}\_{0}^{(NASC)}\\ \mathsf{B} & \mbox{otherwise} \end{cases}$$

*2. Define F de f* = ⋃︁ *<sup>s</sup>*∈*Src FS*(*s*)*. If* <sup>F</sup> *is universally distributive, then*

$$\text{(7.61)}\tag{7.61} = \text{ } \space \text{//}\space \text{V}^{\prime P^{\prime}}\text{)}=\_{\text{F}}\text{ MOVP.}$$

**Algorithm 12:** Computes the least alternative valid-paths solution on *Src* × *N* with respect to the most precise helper functions

**Input:** a data-flow framework instance F = (*G*, *L*, *F*⊠, ρ) as described on page 295, least same-level solution <sup>A</sup>(*SL*) , node set *Src* ⊆ *N*

**Result:** least alternative valid-paths solution, as stated in Theorem 7.32

**<sup>1</sup>** <sup>A</sup>(*ASC*) <sup>←</sup> *ComputeAscendingSolution*(*Src*, <sup>A</sup>(*SL*) )

$$\text{2. } \mathcal{R}^{(\text{NASC})}\_{\text{........}} \leftarrow \text{ComputeNonAscending Solution} (\mathcal{R}^{(\text{SL})}, \mathcal{R}^{(\text{ASC})})$$

$$\text{a. } \mathcal{H}^{(VP')} \leftarrow \mathcal{H}^{(AS\dot{C})}\_{\cdot} \sqcup \mathcal{H}^{(DESC)}$$

$$\mathfrak{a} \text{ течип } \mathcal{R}^{(VP')}$$

*Proof.* 1. (7.60) is an easy consequence of (7.57), considering the final value of <sup>A</sup>(*VP*′ ) in Algorithm 12.

2. Assume that F is universally distributive. Then (7.61) is an easy consequence of

*l f p*(*F* C(*VP*′)) = *MOVP*, and(7.62)

$$(7.63) \qquad \qquad \mathcal{V}\_0^{(ASC)} \cup \mathcal{V}\_0^{(NASC)} = \bigcup\_{s \in Scc} FS(s).$$

These two facts follow from the universal distributivity of F , Theorem 7.29 and Theorem 7.31.

□

# **7.3.6 Towards the Two-Phase Slicer**

Remember that Algorithm 6 works in two phases: The first visits all nodes that are *ASC*-reachable from the given start nodes. It uses summary edges to skip call edges and instead collects the call nodes on a second worklist *W*2. The second phase starts with *W*<sup>2</sup> and extends the set of visited nodes along the descending paths of the given graph, using summary edges to skip return edges.

I can employ this pattern in my more general setting. The result is Algorithm 13, a variant of Algorithm 12 that integrates the two main steps of Algorithm 12 and offers large similarities to Algorithm 6. This variant also computes the alternative valid-paths solution on the forward slice of *Src*, provided that the given framework F is distributive.

Like Algorithm 12, Algorithm 13 proceeds in two steps.

The first step, which is implemented by Algorithm 14, is a variant of Algorithm 10 that computes the least ascending solution <sup>A</sup>(*ASC*) with respect to the pre-computed least same-level solution and additionally collects all (*s*, *<sup>c</sup>*) <sup>∈</sup> *Src* <sup>×</sup> *<sup>N</sup>call* such that *ASC*(*s*, *<sup>c</sup>*) <sup>≠</sup> <sup>∅</sup> in a set *<sup>W</sup>*2, which it also returns. It can easily be verified that Algorithm 14 has the same properties as Algorithm 10 but additionally has the property that, upon termination, *W*<sup>2</sup> contains the set of *all* (*s*, *c*) ∈ *Src* × *N* such that *c* ∈ *Ncall* and *ASC*(*s*, *<sup>c</sup>*) <sup>≠</sup> <sup>∅</sup>.

The second step, which is implemented by Algorithm 15, then takes <sup>A</sup>(*ASC*) and *W*<sup>2</sup> and executes a variant of Algorithm 11. Algorithm 15 differs from **Algorithm 13:** Computes the least alternative valid-paths solution on *Src* × *N* with respect to the most precise helper functions


Algorithm 11 in two key aspects: Firstly, it does not initialize the solution with *const*(⊠), but rather initializes it with <sup>A</sup>(*ASC*) and secondly, it does not have an initial loop. However, it also starts with a non-empty worklist that, by the additional correctness property of Algorithm 15, contains all (*s*, *<sup>c</sup>*) <sup>∈</sup> *Src* <sup>×</sup> *<sup>N</sup>* such that *<sup>n</sup>* <sup>∈</sup> *<sup>N</sup>call* and *ASC*(*s*, *<sup>c</sup>*) <sup>≠</sup> <sup>⊠</sup>. It can be verified that the initial loop is actually integrated in the main loop.

Assuming the distributivity of F , we can show that Algorithm 15 indeed computes *l f p*(C*VP*′). The proof is analogous to the proof of Theorem 7.11 and proceeds in three steps, which I state here but omit the proofs.


$$\{(s,t)\in \mathcal{S}rc \times N \mid \mathcal{A}^{(VP')}(s,t) \neq \mathbb{B}\} \subseteq \mathcal{V}\_0^{(A\mathcal{SC})} \cup \mathcal{V}\_0^{(NA\mathcal{SC})}$$

and upon termination, we have

$$\{(\mathbf{s},t)\in \mathbf{S}\mathbf{r}\mathbf{c}\times\mathbf{N} \mid \mathcal{A}^{(VP')}(\mathbf{s},t)\neq\mathbf{s}\} = \mathcal{V}\_0^{(A\mathbf{S}\mathbf{C})} \cup \mathcal{V}\_0^{(NA\mathbf{C})}$$

3. Algorithm 15 maintains the invariant

$$\mathcal{H}^{(VP')} \le lfp(F\_{\mathcal{C}^{(ASC)}}) \sqcup lfp(F\_{\mathcal{C}^{(NASC)}}),$$

and upon termination, we have

$$\mathcal{R}^{(VP')} = \operatorname{lfp}(\mathcal{F}\_{\mathcal{C}^{(A\text{SC})}}) \sqcup \operatorname{lfp}(\mathcal{F}\_{\mathcal{C}^{(NA\text{SC})}}) = \operatorname{lfp}(\mathcal{F}\_{\mathcal{C}^{(VP')}}) . $$

322

The invariant in the last step actually uses the distributivity of F . My conjecture, which I will not prove within the scope of this thesis is that (a) Algorithm 13 always computes an over-approximation *l f p*(C*VP*′) and that (b) there are non-distributive frameworks for which <sup>A</sup>(*VP*′ ) <sup>&</sup>gt; *l f p*(C*VP*′).

# **7.4 Call-String Approach**

This section is dedicated to computing a *VP*-correct solution using a callstring-based constraint system. The general pattern is the same as in the

**Algorithm 14:** Implementation of ComputeAscendingSolution' **Input:** a data-flow framework instance F = (*G*, *L*, *F*⊠, ρ) as described on page 295, least same-level solution <sup>A</sup>(*SL*) , node set *Src* ⊆ *N* **Result:** Ascending solution A set *W*<sup>2</sup> ⊆ *Src* × *N* such that (*s*, *n*) ∈ *W*<sup>2</sup> implies that *n* ∈ *Ncall* and *n* is *ASC*-reachable from *Src* **<sup>1</sup>** A ← *const*(⊠) **<sup>2</sup> foreach** *s* ∈ *Src* **do <sup>3</sup>** A(*s*,*s*) ← *id* **<sup>4</sup>** *W* ← *W* ∪ {(*s*,*s*)} **<sup>5</sup> while** *<sup>W</sup>* <sup>≠</sup> <sup>∅</sup> **do <sup>6</sup>** (*s*, *t* ′ ) ← *remove*(*W*) **<sup>7</sup> foreach** *e* ∈ *Eintra* ∪ *Eret such that t*′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>* **do <sup>8</sup>** A(*s*, *t*) ← A(*s*, *t*) ⊔ *f<sup>e</sup>* ◦ A(*s*, *t* ′ ) **<sup>9</sup>** *W* ← *W* ∪ {(*s*, *t*)} if A(*s*, *t*) has changed **<sup>10</sup> foreach** (*ecall*,*eret*) ∈ Φ *such that t* ′ *<sup>e</sup>call* <sup>→</sup> *<sup>n</sup>*<sup>0</sup> <sup>∧</sup> *<sup>n</sup>*<sup>1</sup> *<sup>e</sup>ret* <sup>→</sup> *<sup>t</sup>* <sup>∧</sup> *XSL*(*n*0, *n*<sup>1</sup> ) ≠ ⊠ **do <sup>11</sup>** *sumIn f o* ← *feret* ◦ *XSL*(*n*0, *n*<sup>1</sup> ) ◦ *fecall* **<sup>12</sup>** A(*s*, *t*) ← A(*s*, *t*) ⊔ *sumIn f o* ◦ A(*s*, *t* ′ ) **<sup>13</sup>** *W*<sup>2</sup> ← *W*<sup>2</sup> ∪ {(*s*, *t* ′ )} **<sup>14</sup>** *W* ← *W* ∪ {(*s*, *t* ′ )} if A(*s*, *t*) has changed **<sup>15</sup> return** A

**Algorithm 15:** Implementation of ExtendAlongNonAscSolution'

```
Input: a data-flow framework instance F = (G, L, F⊠, ρ) as
          described on page 295, least same-level solution A(SL)
                                                                    ,
          node set Src ⊆ N, ascending solution A(ASC)
                                                         , set W of call
          nodes that are ASC-reachable from Src
  Result: alternative valid-paths solution A(VP′
                                                   )
1 A(VP′
         ) ← A(ASC)
2 while W ≠ ∅ do
3 (s, t) ← remove(W)
4 foreach e ∈ Eintra ∪ Ecall such that t e→ t
                                              ′ do
 5 A(VP′
                )
                 (s, t
                     ′
                      ) ← A(VP′
                                 )
                                  (s, t
                                     ′
                                      ) ⊔ fe ◦ A(VP′
                                                    )
                                                     (s, t)
 6 W ← W ∪ {(s, t
                         ′
                          )} if A(VP′
                                     )
                                      (s, t
                                          ′
                                          ) has changed
7 foreach (ecall,eret) ∈ Φ such that t
                                           ecall → n0 ∧ n1
                                                         eret → t
                                                              ′ ∧
       XSL(n0, n1
                  ) ≠ ⊠ do
 8 sumIn f o ← feret ◦ XSL(n0, n1
                                       ) ◦ fecall
 9 A(VP′
                )
                 (s, t
                     ′
                      ) ← A(VP′
                                 )
                                  (s, t
                                     ′
                                      ) ⊔ sumIn f o ◦ A(VP′
                                                           )
                                                            (s, t)
10 W ← W ∪ {(s, t
                         ′
                          )} if A(VP′
                                     )
                                      (s, t
                                          ′
                                          ) has changed
11 return A(VP′
                )
```
last sections. First, I am going to instantiate Algorithm 8 so that it solves a sufficient portion of Constraint System 6.5. After that, I will state an appropriate instance of Theorem 7.11. Lastly, I give a characterize the core variables of the subsystem solved by the algorithm and convince myself that the algorithm indeed solves a definition-complete subsystem.

I fix a stack space S = (*Ecall*, *S*, ≤, ϵ, *push*, *pop*, *top*) such that *S* is finite. The finiteness of *S* ensures that the complete lattice

$$N \times N \times S \to\_{mon} F\_{\boxtimes}$$

satisfies the ascending chain condition.

Before I am able to instantiate Algorithm 8 to compute the least solution of Constraint System 6.5, I need to modify Constraint System 6.5 a bit: In Algorithm 8, constraints are applied by updating the left-hand sides for given updated right-hand sides. However, Constraint System 6.5 does not present all constraints in such a form. Particularly, ret (2) S needs to be transformed.

The ret (2) S -constraints have the form

$$(\mathsf{nerr}\_{\mathcal{S}}^{(2)}) \xrightarrow{\mathsf{t}' \xrightarrow{\mathcal{C}\_{\text{ret}}} \mathsf{t} \ (\mathsf{e}\_{\text{call}}, \mathsf{e}\_{\text{ret}}) \in \mathsf{\Phi} \ \mathsf{pop}(\mathsf{push}(\mathsf{e}\_{\text{call}}, \sigma)) = \sigma \ \mathsf{push}(\mathsf{e}\_{\text{call}}, \sigma) \neq \mathsf{e}\_{\text{set}} }$$
 
$$\underline{\mathcal{X}\_{\mathcal{S}}(\mathsf{s}, \mathsf{t}, \sigma) \ge f\_{\mathsf{cret}} \circ \underline{\mathcal{X}\_{\mathcal{S}}(\mathsf{s}, \mathsf{t}', \texttt{push}(\mathsf{e}\_{\text{call}}, \sigma))}$$

I fix *ecall* ∈ *Ecall* and consider the following two sets:

$$\begin{aligned} A &= \{ (\sigma, push(e\_{call}, \sigma)) \mid \sigma \in \mathcal{S} \\ &\qquad \land pop(push(e\_{call}, \sigma)) = \sigma \\ &\qquad \land push(e\_{call}, \sigma) \neq \epsilon \} \\ B &= \{ (pop(\sigma), \sigma) \mid \sigma \in \mathcal{S} \land \sigma \neq \epsilon \land top(\sigma) = e\_{ call} \} \end{aligned}$$

The elements of *A* describe the relation between the two stacks appearing on the left-hand side and the right-hand side of a ret (2) S -rule involving *ecall*, whereas the elements of *B* describe this relation for a respective transformed rule.

In the following, I am going to show that *A* = *B* by proving that they are contained in each other.

So let (σ, *push*(*ecall*, σ)) ∈ *A*. Then we have

$$pop(push(e\_{call}, \sigma)) = \sigma \qquad \qquad \text{and} \qquad push(e\_{call}, \sigma) \neq \epsilon.$$

Define σ̃ *de f* = *push*(*ecall*, <sup>σ</sup>). Then <sup>σ</sup>̃ <sup>≠</sup> <sup>ϵ</sup>, *top*(σ̃) = *<sup>e</sup>call* and

$$pop(\tilde{\sigma}) = pop(push(e\_{call}, \sigma)) = \sigma.$$

This means that

$$(\sigma \, \_\prime push(e\_{\text{call} \, \_\prime} \sigma)) = (pop(\vec{\sigma}) \, \_\prime \vec{\sigma}) \in B.$$

Conversely, let (*pop*(σ), <sup>σ</sup>) <sup>∈</sup> *<sup>B</sup>*. Then <sup>σ</sup> <sup>≠</sup> <sup>ϵ</sup> and *top*(σ) = *<sup>e</sup>call*. Define σ̃ *de f* = *pop*(σ). Then

$$push(e\_{call\prime}\sigma) = push(e\_{call\prime}pop(\sigma)) = \sigma \neq \epsilon$$

325

and

$$pop(push(e\_{call}\,\tilde{\sigma})) = pop(\sigma) = \tilde{\sigma}.$$

This means that

$$(pop(\sigma), \sigma) = (\mathfrak{d} \, \_\primepush(e\_{call} \, \mathfrak{d} \,)) \in A.$$

The equality of *A* and *B* implies that the ret (2) S -constraints from Constraint System 6.5 can be replaced by the following equivalent ret' (2) S -constraints:

$$(\mathsf{n} \mathsf{n} \mathsf{r} \mathsf{r}'^{(2)}\_{\mathcal{S}}) \frac{e\_{\mathsf{ret}} \in \mathsf{E}\_{\mathsf{ret}} \quad t' \stackrel{\mathcal{E}\_{\mathsf{ret}}}{\to} t \quad (e\_{\mathsf{call}}, e\_{\mathsf{ret}}) \in \Phi \quad \sigma \neq \varepsilon \quad top(\sigma) = e\_{\mathsf{call}}}{\mathsf{X}\_{\mathcal{S}}(s, t, top(\sigma)) \ge f\_{\mathsf{ret}} \circ \mathsf{X}\_{\mathcal{S}}(s, t', \sigma)}$$

The replacement of ret (2) S by ret' (2) S in Constraint System 6.5 leads to Constraint System 7.3.

**Constraint System 7.3.** *Let* S = (*Ecall*, *S*, ≤, ϵ, *push*, *pop*, *top*) *be a stack space over <sup>E</sup>call. Then <sup>X</sup>*<sup>S</sup> : *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>×</sup> *<sup>S</sup>* <sup>→</sup>*mon <sup>F</sup> is an* <sup>S</sup>*-solution if it satisfies the following constraints:*

$$\begin{pmatrix} \text{(\text{EMTT}\_{S})} \frac{s \in N}{X\_{\mathcal{S}}(s, s, \epsilon) \ge id} \text{(\text{NTRA}\_{\mathcal{S}})} \frac{e \in E\_{\text{intrra}} \quad t' \stackrel{\varepsilon}{\to} t}{X\_{\mathcal{S}}(s, t, \sigma) \ge f\_{\mathbf{c}} \circ X\_{\mathcal{S}}(s, t', \sigma)} \end{pmatrix}$$

$$\begin{pmatrix} \text{cAL}\_{\mathcal{S}} \end{pmatrix} \frac{e\_{\text{call}} \in E\_{\text{call}} \quad t' \stackrel{\varepsilon\_{\text{cell}}}{\to} t}{X\_{\mathcal{S}}(s, t, \text{push}(e\_{\text{call}}, \sigma')) \ge f\_{\mathbf{c}} \circ X\_{\mathcal{S}}(s, t', \sigma')}$$

$$\begin{pmatrix} \text{Rev}^{(1)}\_{\mathcal{S}} \end{pmatrix} \frac{e\_{\text{ret}} \in E\_{\text{ret}} \quad t' \stackrel{\varepsilon\_{\text{ret}}}{\to} t}{X\_{\mathcal{S}}(s, t, \epsilon) \ge f\_{\mathbf{c}rt} \circ X\_{\mathcal{S}}(s, t', \epsilon)}$$

$$\begin{pmatrix} \text{Rev}^{(2)}\_{\mathcal{S}} \end{pmatrix} \frac{e\_{\text{ret}} \in E\_{\text{ret}} \quad t' \stackrel{\varepsilon\_{\text{ret}}}{\to} t \quad (e\_{\text{call}}, e\_{\text{ret}}) \in \Phi \quad \sigma \neq \epsilon \quad \text{top}(\sigma) = e\_{\text{call}}$$

$$X\_{\mathcal{S}}(s, t, \text{pop}(\sigma)) \ge f\_{\mathbf{c}rt} \circ X\_{\mathcal{S}}(s, t', \sigma)$$

**Lemma 7.33.** *Constraint System 6.5 and Constraint System 7.3 are equivalent: X is a solution of Constraint System 6.5 if and only if it is a solution of Constraint System 7.3.*

*Proof.* See the previous considerations. □

Now let C (S) be the set of constraints from Constraint System 7.3 with respect to the abstract stack space S. Furthermore, I define

*X* (S) 0 *de f* (7.64) <sup>=</sup> {(*s*,*s*, <sup>ϵ</sup>) <sup>|</sup> *<sup>s</sup>* <sup>∈</sup> *Src*}

$$\text{(7.65)}\qquad\qquad\mathcal{C}\_0^{(\mathcal{S})} \stackrel{def}{=} \text{Core}(\mathcal{C}^{(\mathcal{S})}, X\_0^{(\mathcal{S})})$$

$$(\text{7.66}) \qquad \qquad \mathcal{V}\_0^{(\mathcal{S})} \stackrel{def}{=} \text{CoreVars}(\mathcal{C}^{(\mathcal{S})}, X\_0^{(\mathcal{S})})$$

Algorithm 16 is an instantiation of Algorithm 8 that is supposed to solve C (S) 0 . The initialization loop can be seen in lines 2–4 and the main loop in lines 5–11. The body of the main loop enumerates all constraints *c* with (*s*, *t*, σ) ∈ *FV*(*rhs*(*c*)) by inspecting the stack σ and the outgoing edge *e* and determining which constraint to apply.

Assuming that C (S) 0 is definition-complete in Core(C (S) , *N* × *N* × *S*), we get the following correctness result for Algorithm 16.

**Theorem 7.34.** *Algorithm 16 always terminates and upon termination, we have*

$$(\mathsf{T}.6\mathsf{T})\qquad\mathcal{H}\_{\mathsf{S}}(\mathsf{s},t,\sigma)=\begin{cases}\operatorname{lfp}(\mathsf{F}\_{\mathsf{C}\_{0}^{(\mathsf{S})}})(\mathsf{s},t,\sigma) & \text{if } (\mathsf{s},t,\sigma)\in\mathsf{V}\_{0}^{(\mathsf{S})}\\\mathsf{B} & \text{otherwise}\end{cases}$$

*Moreover, we have*

$$\text{(7.68)}\qquad \mathcal{A}\_{\mathcal{S}} = \prescript{}{\mathcal{V}\_0^{(\mathcal{S})}}{\mathcal{V}\_0^{(\mathcal{S})}} \, \text{lfp}(\mathcal{F}\_{\text{Core}(\mathcal{C}^{(\mathcal{S})}, \mathcal{N} \times \mathcal{N} \times \mathcal{S})}) = \, \text{lfp}(\mathcal{F}\_{\mathcal{C}^{(\mathcal{S})}}) \, \text{l}$$

To fully establish the correctness and meaningfulness of Theorem 7.34, it remains to show that C (S) 0 is definition-complete in Core(C (S) , *N* × *N* × *S*) and give an appropriate characterization of V (S) 0 . I start with the latter. The following lemma does not fully solve this task, but at least shows that V (S) 0 is large enough<sup>37</sup> .

<sup>37</sup>My conjecture is that an appropriate converse can also be shown, once a suitable formalization of reachability on *N* × *N* × *S* is available. I will however not consider this within the scope of this thesis.

**Algorithm 16:** Algorithm for computing the least S-solution

**Input:** a data-flow framework instance F = (*G*, *L*, *F*⊠, ρ) as described on page 295, set *Src* ⊆ *N* of sources **Result:** the least S-solution, as stated in Theorem 7.34 **<sup>1</sup>** <sup>A</sup><sup>S</sup> <sup>←</sup> *const*(⊠) **<sup>2</sup> foreach** *s* ∈ *Src* **do <sup>3</sup>** <sup>A</sup>S(*s*,*s*, <sup>ϵ</sup>) <sup>←</sup> *id* **<sup>4</sup>** *W* ← {(*s*,*s*, ϵ)} **<sup>5</sup> while** *<sup>W</sup>* <sup>≠</sup> <sup>∅</sup> **do <sup>6</sup>** (*s*, *t*, σ) ← *remove*(*W*) **<sup>7</sup> foreach** *<sup>e</sup>* <sup>∈</sup> *E such that t <sup>e</sup>*<sup>→</sup> *<sup>t</sup>* ′ **do <sup>8</sup>** σ ′ ← ⎧ ⎪⎪⎪⎪⎪⎪⎨ ⎪⎪⎪⎪⎪⎪⎩ σ if *e* ∈ *Eintra* ∨ *e* ∈ *Eret* ∧ σ = ϵ *push*(*e*, σ) if *e* ∈ *Ecall pop*(σ) if <sup>σ</sup> <sup>≠</sup> <sup>ϵ</sup> <sup>∧</sup> (*top*(σ),*e*) <sup>∈</sup> <sup>Φ</sup> ▼ otherwise **<sup>9</sup> if** σ ′ ≠ ▼ **then <sup>10</sup>** <sup>A</sup>S(*s*, *<sup>t</sup>* ′ , σ ′ ) ← AS(*s*, *<sup>t</sup>* ′ , σ ′ ) <sup>⊔</sup> *<sup>f</sup><sup>e</sup>* ◦ AS(*s*, *<sup>t</sup>*, <sup>σ</sup>) **<sup>11</sup>** *W* ← *W* ∪ {(*s*, *t* ′ , σ ′ )} if <sup>A</sup>S(*s*, *<sup>t</sup>* ′ , σ ′ ) has changed **<sup>12</sup> return** A<sup>S</sup>

**Lemma 7.35.** *For all s*, *t* ∈ *N, we have*

$$AP\_{\mathcal{S}}(\mathbf{s}, t) \neq \emptyset \land \mathbf{s} \in \text{Src} \implies \exists \sigma \in \text{S. } (\mathbf{s}, t, \sigma) \in \text{CoreVars}(\mathcal{C}\_{\mathcal{S}}, X\_0)$$

*Proof.* We show the statement

$$\begin{aligned} \forall \pi \in \text{Paths}\_G. \; \forall s, t \in \text{N}. \; \pi \in \text{Paths}\_G(s\_\prime t) \land s \in \text{Src} \land cs(\pi) \neq \bullet \\ \implies (s\_\prime t, cs(\pi)) \in \text{CoreVars}(\mathcal{C}\_\mathcal{S}, X\_0) \end{aligned}$$

by induction on π ∈ *PathsG*. So let <sup>π</sup> <sup>∈</sup> *Paths<sup>G</sup>* be a path with <sup>π</sup> <sup>∈</sup> *PathsG*(*s*, *<sup>t</sup>*), *<sup>s</sup>* <sup>∈</sup> *Src* and *cs*(π) <sup>≠</sup> ▼.

1. Assume that π = ϵ. Then *s* = *t* and (*s*,*t*, ϵ) = (*s*,*t*, *cs*(π)) ∈ *X*<sup>0</sup> since *s* ∈ *Src*. Furthermore, C<sup>S</sup> contains a constraint

$$X\_{\mathcal{S}}(s, s, \epsilon) \ge id$$

by emptyS. Thus, (*s*, *<sup>t</sup>*, <sup>ϵ</sup>) ∈ CoreVars(CS, *<sup>X</sup>*0).

2. Assume that π = π ′ ·*e* with π ′ ∈ *PathsG*(*s*, *t* ′ ) and *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>*. By definition of *cs*, *cs*(π) ≠ ⊠ implies that *cs*(π ′ ) ≠ ⊠. Hence by induction hypothesis we may assume

$$(s, t', cs(\pi')) \in \text{CoreVars}(\mathcal{C}\_{\mathcal{S}'}X\_0).$$

We observe that C<sup>S</sup> contains the constraint

$$(\star) \qquad \qquad X\_{\mathcal{S}}(s, t, \operatorname{cs}(\pi)) \ge f\_{\mathcal{E}} \circ X\_{\mathcal{S}}(s, t', \operatorname{cs}(\pi')) . \mathbb{R}$$

To see this, we make a case distinction on *e*.

*e* ∈ *Eintra*: Then *cs*(π) = *cs*(π ′ ) and we obtain the constraint by intraS. *e* ∈ *Ecall*: Then *cs*(π) = *push*(*ecall*, *cs*(π ′ )) and we obtain the constraint by

callS.

*e* ∈ *Eret*: Then one of the following statements holds:

$$\text{(1)}\qquad\text{cs}(\pi) = \text{cs}(\pi') = \epsilon$$

$$\text{(2)}\qquad \text{cs}(\pi') \neq \epsilon \land (\text{top}(\text{cs}(\pi')) , \epsilon) \in \Phi \land \text{cs}(\pi) = \text{pop}(\text{cs}(\pi')) .$$

In case (1), we obtain the constraint by ret (1) S and in case (2) by ret' (2) S .

Since (*s*,*t* ′ , *cs*(π ′ )) ∈ CoreVars(CS, *<sup>X</sup>*0), constraint (⋆) is contained in <sup>C</sup>ore(CS, *<sup>X</sup>*0) and we obtain

$$(\mathbf{s}, t, cs(\pi')) \in \mathbf{CoreVars}(\mathbf{C}\_{\mathbf{S'}} \mathbf{X}\_0)\_{\mathbf{s'}}$$

as desired.

□

To prove the definition-completeness, I need a connection between V (S) 0 and <sup>C</sup>oreVars(CS, *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>×</sup> *<sup>S</sup>*). Its proof is analogous to the proof of, e.g., (7.43) in Lemma 7.22.

**Lemma 7.36.** *For all s*, *t* ∈ *N and* σ ∈ *S, we have*

$$\mathcal{V}\_0^{(\mathcal{S})} = \text{CoreVars}(\mathcal{C}\_{\mathcal{S}'}N \times N \times \mathcal{S}) \cap \mathcal{S}rc \times N \times \mathcal{S}$$

Now I can give the proof of the definition-completeness of V (S) 0 in <sup>C</sup>oreVars(CS, *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>×</sup> *<sup>S</sup>*).

**Lemma 7.37.** <sup>C</sup>ore(CS, *<sup>X</sup>*0) *is definition-complete with respect to its superset* <sup>C</sup>ore(CS, *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>×</sup> *<sup>S</sup>*)*.*

*Proof.* We use Theorem 7.10. Let *<sup>c</sup>* ∈ Core(CS, *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>×</sup> *<sup>S</sup>*) with *lhs*(*c*) <sup>∈</sup> <sup>C</sup>oreVars(CS, *<sup>X</sup>*0). Then we have to show

$$(7.69) \qquad \qquad FV(rhs(c)) = \emptyset \implies lhs(c) \in X\_0$$

(7.70) *FV*(*rhs*(*c*)) <sup>≠</sup> ∅ ∧ *<sup>x</sup>* <sup>∈</sup> *FV*(*rhs*(*c*)) =<sup>⇒</sup> *<sup>x</sup>* ∈ CoreVars(CS, *<sup>X</sup>*0)

We show the two claims separately.

(7.69)**:** If *FV*(*rhs*(*c*)) = ∅, then *c* is of the form

$$X\_{\mathcal{S}}(s, s, \epsilon) \ge id$$

Since (*s*,*s*, <sup>ϵ</sup>) = *lhs*(*c*) ∈ CoreVars(CS, *<sup>X</sup>*0), it must be the case that (*s*,*s*, <sup>ϵ</sup>) <sup>∈</sup> *X*<sup>0</sup> by Lemma 7.36.

(7.70)**:** Let *FV*(*rhs*(*c*)) <sup>≠</sup> <sup>∅</sup>. Inspection of Constraint System 7.3 shows that *c* is of the form

$$X\_{\mathcal{S}}(s, t, \sigma) \ge f\_{\mathfrak{e}} \circ X\_{\mathcal{S}}(s, t', \sigma')$$

where *t* ′ *<sup>e</sup>*<sup>→</sup> *<sup>t</sup>* and (*s*,*t*, <sup>σ</sup>) = *lhs*(*c*). Note that the *<sup>s</sup>* on the right-hand side is the same as on the left-hand side. Since *<sup>c</sup>* ∈ Core(CS, *<sup>N</sup>* <sup>×</sup> *N* × *S*), we have (*s*,*t* ′ , σ ′ ) ∈ CoreVars(CS, *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>×</sup> *<sup>S</sup>*). Moreover, since (*s*,*t*, <sup>σ</sup>) ∈ CoreVars(CS, *<sup>X</sup>*0), we get *<sup>s</sup>* <sup>∈</sup> *Src* by Lemma 7.36. Thus (*s*, *t* ′ , σ ′ ) ∈ CoreVars(CS, *<sup>X</sup>*0) by Lemma 7.36.

□

Theorem 7.34 states that Algorithm 16 solves C (S) for an appropriate subset of *<sup>N</sup>* <sup>×</sup> *<sup>N</sup>* <sup>×</sup> *<sup>S</sup>*. In particular, for every (*s*, *<sup>t</sup>*) <sup>∈</sup> *Src* <sup>×</sup> *<sup>N</sup>* such that *AP*(*s*, *<sup>t</sup>*) <sup>≠</sup> <sup>∅</sup>, there is some <sup>σ</sup> <sup>∈</sup> *<sup>S</sup>* such that the result <sup>A</sup>S(*s*,*t*, <sup>σ</sup>) = *l f p*(*<sup>F</sup>* C(S))(*s*,*t*, <sup>σ</sup>). It remains to establish a connection between *l f p*(*F* C(S)) and *MOVP* to be fully convinced that Algorithm 16 indeed yields a (*VP*, *Src* × *N*)-correct solution.

Now define

$$\mathcal{A}(s,t) \stackrel{def}{=} \bigsqcup\_{\sigma \in S} \mathcal{A}\_{\mathcal{S}}(s,t,\sigma).$$

Then the above considerations and Theorem 6.16 entail that

$$\text{(7.71)}\qquad\qquad\text{AP(s,t)}\neq\emptyset\implies\mathcal{R}(\mathbf{s},t)\geq\text{MOAP}(\mathbf{s},t).$$

Moreover, if F is distributive, then by Theorem 6.18, I can refine this to

$$AP(s, t) \neq \emptyset \implies \mathcal{R}(s, t) = MOAP(s, t)$$

Finally, if S is chosen appropriately, it follows from Corollary 6.33 that Algorithm 16 can be used to compute a (*VP*, *Src* × *N*)-correct solution.

**Theorem 7.38.** *Assume that there is a stack abstraction between* S<sup>∞</sup> *and* S*. Let* A<sup>S</sup> *be the result of Algorithm 16 and define*

$$\mathcal{A}(s,t) \stackrel{def}{=} \bigsqcup\_{\sigma \in \mathcal{S}} \mathcal{A}\_{\mathcal{S}}(s,t,\sigma).$$

*Then* A *is* (*VP*, *Src* × *N*)*-correct.*

*Proof.* Assume (*s*,*t*) ∈ *Src* × *N*. Then we have to show *MOVP*(*s*,*t*) ≤ A(*s*,*t*). For *VP*(*s*,*t*) = ∅, there is nothing to show, so we may assume *VP*(*s*,*t*) <sup>≠</sup> <sup>∅</sup>. Because of our assumption about <sup>S</sup>, we may apply Corollary 6.33 and yield *VP*(*s*, *t*) ⊆ *AP*(*s*, *t*) and

$$\text{(7.72)}\qquad\qquad\qquad\text{MOVP}(\mathbf{s},t)\leq\text{MOAP}(\mathbf{s},t).$$

Because *VP*(*s*, *<sup>t</sup>*) <sup>≠</sup> <sup>∅</sup>, we get *AP*(*s*, *<sup>t</sup>*) <sup>≠</sup> <sup>∅</sup>. Hence, we may apply (7.71) and get

$$\text{(7.73)}\qquad\qquad\qquad\qquad\qquad\text{MOAP}(s,t)\leq\mathcal{H}(s,t)\text{)}$$

(7.72) and (7.73) imply *MOVP*(*s*, *<sup>t</sup>*) ≤ A(*s*, *<sup>t</sup>*), as desired. □

*And I think it's gonna be a long long time.*

# <sup>E</sup>lton <sup>J</sup>ohn **8**

# **Implementation and Evaluation**

In the previous chapters, I theoretically developed and examined various algorithms to conduct generalized interprocedural data-flow analysis on interprocedural graphs. The purpose of this chapter is to demonstrate that these theoretically stated algorithms also work in practice. To this end, I implemented the algorithms in the Joana framework and conducted an evaluation of both performance and precision.

This chapter is organized as follows. First, I am going to give an overview of my implementation in section 8.1. The other sections are dedicated to the evaluation.

Section 8.2 describes several aspects on the setup of my evaluation, particularly which samples and instances I chose. Then, I describe and discuss the results of the performance evaluation in section 8.3. Lastly, section 8.4 is dedicated to the precision evaluation.

# **8.1 Notes on the Implementation**

My implementation allows for arbitrary data-flow framework instances. In order to enable evaluations and comparison with Joana's summary edge computation and slicers, my implementation works on Joana's graph data structure.

In the next subsections, I am going to consider several aspects of my implementation in more detail. Subsection 8.1.1 is dedicated to my implementation of the functional approach and subsection 8.1.2 considers my implementation of the call string approach. Finally, in subsection 8.1.3, I will conduct a worst-case complexity analysis on all implemented algorithms.

# **8.1.1 Functional Approach**

As we still remember from previous chapters, the functional approach consists of a preprocessing phase that computes same-level information, and a two-phase algorithm that computes the actual data-flow analysis solution using the same-level information. In the following, I will give a brief overview of my implementations of both steps, in this order.

I implemented two variants of the same-level info computations, which I will elaborate on in subsubsection 8.1.1.2 and subsubsection 8.1.1.3, respectively. Before that, in subsubsection 8.1.1.1 I give some general hints on how my implementations represent same-level information.

After having described my summary info computation implementations, I consider my implementation of the actual two-phase data-flow analysis in subsubsection 8.1.1.4.

# **8.1.1.1 Representing Same-Level Information**

Recall that both the summary edge algorithm and the two-phase slicer employ *summary edges* between actual-in and actual-out nodes in order to avoid descending into callees.

My implementations of the generic same-level problem use a generalized version of summary edges: In addition to the mere information that there is a same-level path, my generalized summary edges in addition are annotated with the result of the same-level solution for the given pair of nodes.

More specifically, a common difference to Algorithm 9 is that expressions of the form *<sup>f</sup>eret* ◦ A(*SL*) (*n*0, *n*<sup>1</sup> ) ◦ *fecall* are stored separately in a map *sumIn f o*. Moreover, my implementation of the generalized two-phase approach assumes that *sumIn f o* is available. Practically, I persist *sumIn f o* in a separate file using the JSON format [94].

I implemented two variants of Algorithm 9, *consequent* and *optimized*. I discuss them briefly in the following.

# **8.1.1.2 Consequent Summary Info Computation**

The *consequent* variant is very close to Algorithm 9 and only specifies an order in which the worklist items are traversed. In the following, I give some hints on how this ordering is chosen.

Remember that Algorithm 9 maintains pairs (*s*, *t*) of nodes on its worklist, where *s* and *t* belong to the same procedure. The ordering aims to ensure that callees are processed before callers. The idea is that, if we process a given procedure *p*, we want to use the most recent summary information to avoid re-computation. Hence we aim to ensure that the procedures called by *p* are processed before *p* is processed. This works perfectly as long as there are no recursive cycles in the call graph.

The ordering consists of two parts: The first part orders procedures on the call graph, while the second part orders the nodes within a procedure. The ordering on the call graph is obtained as follows.

1. We are given a call graph *C* = (*P*, *E*) of the given PDG *G* where *P* consists of the procedures of the given PDG *G* and two nodes *p* and *p* ′ are connected by a directed edge *p* → *p* ′ if *p* contains a call of *p* ′ .

2. Reverse the edges of *C* and obtain graph *C* ′ = (*P*, *E* ′ ).

3. Compute the *condensation* of *C* ′ , that is, a graph *C* ′′ = (*S*, *E* ′′) such that the nodes *S* ⊆ 2 *<sup>P</sup>* of *C* ′′ are the strongly connected components of *C* ′ and *A* →*C*′′ *B* if and only if ∃*p* ∈ *A*. ∃*p* ′ ∈ *B*. *p* →*C*′ *p* ′ . Note that *C* ′′ is acyclic [59, p. 98].

4. Now sort *C* ′′ topologically. This yields a total order <*top* of *C* ′′ .

5. By carefully enumerating the nodes in each SCC *A* in a well-defined fashion, use <*top* to obtain a function *i* : *P* → **N** of *G* ′ such that *i*(*p*) ≠ *i*(*p* ′ ) iff *p* ≠ *p* ′ and *i*(*p*) < *i*(*p* ′ ) if *p* and *p* ′ belong to two different SCCs *A* and *B* with *A* <*top B*.

Orderings within the procedures were obtained by enumerating the nodes in depth-first order. Note that this relies on Joana's node iteration order for its internal PDG structure and therefore can be subject to non-determinism on some level.

# **8.1.1.3 Optimized Summary Info Computation**

Variant *optimized*, which is depicted in Algorithm 17, is based on the observation that Algorithm 9 actually computes a global same-level solution, although it only needs a relatively small part of it, namely the summary information for pairs of call nodes and their return counterparts. Instead, the *optimized* variant only maintains the actual summary information, re-computes intraprocedural parts of the solution as needed and discards non-essential parts of them as soon as possible. Also, instead of node pairs that need to be updated, it maintains procedures that need to be re-processed on its worklist.

Processing a procedure *p* means to completely re-compute the intraprocedural result for *p* and to propagate this result to *p*'s callers. If this propagation leads to a change in some summary information between two nodes *m* and *n*, the procedure that contains *m* and *n* is scheduled for re-computation by putting it on the worklist. For worklist ordering, a callee-caller-ordering similar to the procedure ordering of the *consequent* variant is used.

Initially, all procedures are put on the worklist to ensure that each intraprocedural result is computed at least once.

# **8.1.1.4 Generalized Two-Phase Approach**

For the two-phase approach, I implemented Algorithm 13, which can give precision guarantees for distributive problems. My implementation is relatively straight-forward. The most notable deviation from Algorithm 13 is that the implementation uses summary information instead of same-level information, as described in subsubsection 8.1.1.1.

# **8.1.2 Call-string Approach**

In my implementation of the call-string approach, I use a variant of Algorithm 16 that exploits fundamental properties of program dependence graphs with respect to their correspondence relation Φ.

Recall that Algorithm 16 uses stacks, i.e. sequences of call edges, to remember to which caller the analysis shall return after exiting a given procedure. If such a stack *ecall* · σ is given and the algorithm is about to leave procedure *p* through the return edge *eret*, it needs to check that (*ecall*,*eret*) ∈ Φ, i.e. that the top of the stack corresponds to *eret*, in order to proceed with *tgt*(*eret*) and stack σ.

In a program dependence graph, we generally have (*ecall*,*eret*) ∈ Φ if and only if *src*(*ecall*) and *tgt*(*eret*) belong to the same call site and *tgt*(*ecall*) and *src*(*eret*) belong to the same procedure. Hence, for PDGs, the stack does not need to contain call edges, but only the call sites. A call site can be represented by the node that describes the actual call instruction.

Hence, my implementation of Algorithm 16 uses sequences of call *nodes* as stacks. Using sequences of call nodes instead of parameter edges as stacks mainly saves a lot of space. This is especially beneficial for calls of procedures with many parameters. With the ordinary stack representation, all these parameters induce different, equivalent call stacks.

**Algorithm 17:** A variant of Algorithm 9 that trades recomputation of intra-procedural results for a more compact solution and worklist representation – for *updateIntraprocResult*, see Algorithm 18

**Input:** a data-flow framework instance F = (*G*, *L*, *F*⊠, ρ) as described on page 295 **Result:** least summary information for F **procedure** *computeSumInfoOptimized sumIn f o* <sup>←</sup> *const*(⊠) **<sup>3</sup>** *W* ← *Proc* **while** *<sup>W</sup>* <sup>≠</sup> <sup>∅</sup> **do** *p* ← *remove*(*W*) **foreach** *s* ∈ *Entries<sup>p</sup>* **do** *newResults* ← *updateIntraprocResult*(*s*,*sumIn f o*) **foreach** *t* ∈ *Exits<sup>p</sup>* **do foreach** (*ecall*,*eret*) ∈ Φ *and m*, *n s.t. m <sup>e</sup>call* <sup>→</sup> *<sup>s</sup>* <sup>∧</sup> *<sup>t</sup> <sup>e</sup>ret* <sup>→</sup> *<sup>n</sup>* **do** *old* ← *sumIn f o*(*m*, *n*) *sumIn f o*(*m*, *n*) ← *sumIn f o*(*m*, *n*) ⊔ *feret* ◦ *newResults*(*s*, *t*) ◦ *fecall* **if** *sumIn f o*(*m*, *n*) ≠ *old* **then** *W* ← *W* ∪ {*proc*(*m*)} **return** *sumInfo*

**Algorithm 18:** Intraprocedural part of Algorithm 17

```
Input: a data-flow framework instance F = (G, L, F⊠, ρ) as de-
         scribed on page 295, s ∈ N, function sumInfo: N × N → F⊠
  Result: updated version of sumIn f o on {s} × Exitsp
1 procedure updateIntraprocResult(s: N, sumInfo: N × N → F⊠)
2 Analysis ← const(⊠)
3 W = W ∪ {s}
4 Analysis(s,s) ← id
5 while W ≠ ∅ do
6 t ← remove(W)
7 foreach t
                 ′ ∈ N such that t e→ t
                                   ′ ∧ e ∈ Eintra do
8 old ← Analysis(s, t
                            ′
                             )
9 Analysis(s, t
                      ′
                       ) ← Analysis(s, t
                                      ′
                                       ) ⊔ fe ◦ Analysis(s, t)
10 if Analysis(s, t
                         ′
                         ) ≠ old then
11 W ← W ∪ {t
                          ′
                           }
12 foreach t
                 ′ ∈ N such that sumIn f o(t, t
                                         ′
                                          ) ≠ ⊠ do
13 old ← Analysis(s, t
                            ′
                             )
14 Analysis(s,t
                      ′
                       ) ← Analysis(s,t
                                        ′
                                        ) ⊔ sumIn f o(t,t
                                                       ′
                                                       ) ◦
             Analysis(s, t)
15 if Analysis(s, t
                         ′
                         ) ≠ old then
16 W ← W ∪ {t
                          ′
                           }
17 return Analysis ↾ {s} × Exitsp
```
# **8.1.3 Complexity Considerations**

For data-flow frameworks (*L*, *F*) where *F* has finite height *n*, an asymptotic upper bound on the time taken to execute Algorithm 8 can be given in terms of the number of applications of constraints, as this is the most elementary and most often executed operation in this algorithm. Any constraint on a given system can be applied at most *n* times. Hence, the total number of constraint applications is no more than O(|C| · *n*) = O(|C| · |F |). For frameworks where *F* does not have finite height but satisfies (ACC), such a bound cannot be given in the general case, so that the concrete instance has to be taken into account.

Next, I am going to give worst-case time bounds for the different algorithms I presented in chapter 7. For simplicity, I assume that *F* has finite height. Subsection 8.1.3.1 considers the algorithms of the functional approach, while subsubsection 8.1.3.2 consider the call-string approach.

### **8.1.3.1 Functional Approach**

**8.1.3.1.1 Consequent Same-Level Problem Solver** In order to give a worst-case time bound, I give an upper bound for the size of the subset C ′ *SL* of Constraint System 6.1 that is solved by Algorithm 9:

$$\begin{aligned} &|\mathcal{C}'\_{SL}| \\ &\leq |\mathcal{N}\_{entry}| \\ &+\sum\_{p\in\mathcal{P}\text{rec}}|\mathcal{N}\_{p}^{entry}|\cdot|\mathcal{E}\_{p}^{intra}| \\ &+\sum\_{p\in\mathcal{P}\text{rec}}|\mathcal{N}\_{p}^{entry}|\cdot|\Phi\cap\{(n\stackrel{\mathcal{E}\_{\text{call}}}{\stackrel{\mathcal{E}\_{\text{null}}}{\longrightarrow}}n\_{0},n\_{1}\stackrel{\mathcal{E}\_{\text{ref}}}{\stackrel{\mathcal{E}\_{\text{ref}}}{\stackrel{\mathcal{E}\_{\text{self}}}{\longrightarrow}}t})\mid\operatorname{proc}(n)=\operatorname{proc}(t)=p\}|\mathcal{E}\_{p}^{inter}| \end{aligned}$$

where *N entry p de f* = *N<sup>p</sup>* ∩ *Nentry* and *E intra p de f* = *E<sup>p</sup>* ∩ *Eintra*.

The first term counts the number of constraints of the form sl-sol-(i): C ′ *SL* contains such a constraint for every *s* ∈ *Nentry*. The second term approximates the number of constraints of the form sl-sol-(ii): <sup>C</sup> ′ *SL* contains such a constraint at most for every *s* ∈ *Nentry* and every *e* ∈ *Eintra* whose source and target both lie in the same procedure as *s*. Finally, the third term approximates the number of constraints of the form sl-sol-(iii): C ′ *SL* contains such a constraint at most for every *s* ∈ *Nentry* and every (*ecall*,*eret*) ∈ Φ such that *src*(*ecall*) and *tgt*(*eret*) lie in the same procedure as *s*. The second and third terms are only approximations since they do not take into account the reachability analysis performed by Algorithm 9.


$$|\Phi \cap \{ (n \stackrel{\varepsilon\_{\text{call}}}{\rightarrow} n\_0. n\_1 \stackrel{\varepsilon\_{\text{ret}}}{\rightarrow} t) \mid \operatorname{proc}(n) = \operatorname{proc}(t) = p \} |\Phi\rangle$$

can be bounded by |*E*| 2 . In summary, an asymptotic upper bound to the overall time needed by Algorithm 9 in terms of the size of the given PDG *G* can be given by

$$\mathcal{O}(|\mathcal{F}| \cdot (|\mathcal{N}| + |\mathcal{N}| \cdot |\mathcal{E}| + |\mathcal{N}| \cdot |\mathcal{E}|^2)) = \mathcal{O}(|\mathcal{F}| \cdot |\mathcal{N}| \cdot |\mathcal{E}|^2).$$

**8.1.3.1.2 Optimized Same-Level Problem Solver** For the optimized same-level problem solver, I give a coarse yet simple upper bound that only uses |*N*|, |*E*|, |*F*| and the number |*P*| of procedures in the given graph. First, consider updateIntraprocResult. It consists of an instantiation of Algorithm 8 that solves a part of Constraint System 6.1. Using the argument from above, an invocation of updateIntraprocResult takes no more than O(|*F*| · |*E*| 2 ) constraint applications. Next, consider one iteration of the main loop in Algorithm 17: It consists of no more than |*N*| invocations of updateIntraprocResult and no more than |*N*| · |*N*| constraint applications. Hence, one iteration of the loop does not take more than O(|*N*| · |*N*| · |*F*| · |*E*| <sup>2</sup> <sup>+</sup> <sup>|</sup>*N*| · |*N*|) = <sup>O</sup>(|*N*| · |*N*| · |*F*| · |*E*<sup>|</sup> 2 ) rule applications. A given procedure *p* can be put at most |*N*| · |*N*| · |*F*| times onto the worklist (*sumIn f o*'s domain consists of pairs of nodes and every value can change at most |*F*| times), hence the loop can be executed no more than |*P*| · |*N*| 2 · |*F*| times. All in all, Algorithm 17 executes no more than O(|*P*| · |*N*| 4 · |*F*| 2 · |*E*| 2 ) constraint applications.

Again, I would like to point out that this bound is indeed very coarse and formally appears to be worse than the bound for the consequent same-level problem solver. I suspect that it is possible to conduct a more elaborate analysis and yield a tighter upper bound that is closer to the bound for the consequent variant. The evaluation will show that the optimized variant can perform better than the consequent variant in practice.

**8.1.3.1.3 Generalized Two-Phase Approach** For Algorithm 13, similar considerations can be made as in paragraph 8.1.3.1.1. Hence, we arrive at the same rough upper bound O(|*F*| · |*N*| · |*E*| 2 ). However, there is one important aspect that needs to be highlighted. The summary information computation only needs to be conducted once and can then be incorporated into the given graph. A usual approach is to insert summary edges into the given graph and annotate them with the respective value of the same-level analysis result. Then, Algorithm 13 does not operate on the same graph as Algorithm 17, but on an extended graph with more edges. This lowers the upper bound of


to |*E*| and the overall bound to O(|*F*| · |*N*| · |*E*|).

To put it more shortly, using the functional approach makes the actual problem solver linear in the graph size, but may cause the graph's number of edges to increase quadratically. Note that this also applies to simple slicing.

# **8.1.3.2 Call-String Approach**

For the analysis of the call-string approach, I assume a fixed source node *s* ∈ *N* and the stack space S*<sup>k</sup>* for *k* ∈ **N** and consider the part of Constraint System 6.5 where the variables have *s* as first component. A closer look then shows that there is one constraint for every edge *e* ∈ *E* and every stack σ ∈ *S k* . This means that the size of the constraint system is bounded to O(|*E*| · |*Ecall*| *k* ) = O(|*E*| *k*+1 ), which results in an upper bound of O(|*F*| · |*E*| *k*+1 ) for the costs of Algorithm 16.

As I mentioned in subsection 8.1.2, my implementation uses node sequences instead of edge sequences to represent stacks which changes the bound to O(|*F*| · |*N*| *k*+1 ).

# **8.2 Description of the Setup**

In this section, I describe the setup of all my evaluations. Subsection 8.2.1 describes the environment of the evaluation. After that, subsection 8.2.2 gives an overview of the programs that I ran my analyses on. Lastly, subsection 8.2.3 describes the data-flow framework instances that I selected for this evaluation.

# **8.2.1 Evaluation Environment**

The environment of my evaluation consists of the hardware and the software of the setup that I used but not provided myself. An overview of this is given in Table 8.1.


**Table 8.1:** Characteristics of the machine used for performance evaluations

The machine that I ran my experiments on consisted of 80 cores. I ran multiple experiments in parallel, in order to save time. This may have slightly disturbed the results, however, I believe that this effect is negligible.

# **8.2.2 Samples**

The programs I used for the evaluation are described in Table 8.2.

As a preliminary step to all evaluations, I used Joana to construct PDGs without summary edges for all sample programs. I assumed a sequential setting, i.e. I configured Joana to not analyze threads. Moreover, I used context-insensitive points-to analysis, parameter modelling based on object graphs [78] and interprocedural exception analysis.

Including all libraries added to the respective analysis scope, the example programs have between 654k and 15.7M bytecode instructions. After call graph construction, on average 95% were classified as unreachable. Additionally, Joana pruned away 53% of the remaining instructions, so that the parts of the programs that were covered by the resulting program dependence graphs had between 1.6k and 119.3k bytecode instructions.

I divided the example programs in two groups: One group consists of four very large programs, namely javap, dacapo-eclipse, hsqldb and freecs, the other group consists of the rest of the programs. In the following, I refer to these programs as *large*, and to the others as *non-large*.

The reason for this grouping is that the large programs performed substantially worse than all other programs (across all algorithms and instances), which is why I had to choose different evaluation parameters.

The sizes of the corresponding PDGs are shown in Table 8.3.


**Table 8.2:** Description of the sample programs used for this evaluation


**Table 8.3:** Sizes of the PDGs in the sample considered for my evaluation

# **8.2.3 Instances**

I considered the three data-flow framework instances reach (cf. subsection 5.4.2), explicit-info-flow (cf. subsubsection 5.4.5.2) and dist (cf. subsection 5.4.7). Considering the reach instance enables me to compare the performance of my generic algorithms with Joana's hand-optimized algorithms for summary edge computation and two-phase slicing. Instance dist has a lattice with unbounded height. Lastly, I included explicit-info-flow (occasionally abbreviated by eif) as a simple example for language-restricted reachability (cf. subsection 5.4.5) that does not need additional parameters like a barrier.

# **8.3 Performance Evaluation**

In this section, I describe how I conducted my performance evaluation and discuss its result.

My performance evaluation falls into two groups: Same-level problem solvers and data-flow analyses. I consider the former in subsection 8.3.1, while subsection 8.3.2 is dedicated to the latter.

# **8.3.1 Same-Level Problem Solvers**

I evaluated both the consequent (cons) and the optimized (opt) variants of the same-level problem solver for all three considered instances. For comparison, I also evaluated the summary edge computation which Joana performs currently as part of SDG construction. As I explained earlier, this corresponds to the reach instance. This algorithm will be abbreviated as classic. A graphical overview of the runtimes for the non-large programs can be seen in Figure 8.1. For full results, see Table 8.5 and Table 8.6. The remainder of this subsection is structured as follows: In subsubsection 8.3.1.1, I describe the method that I applied to conduct my measurement. After that, I discuss various aspects of the results: Subsection 8.3.1.2 gives a general overview, subsubsection 8.3.1.3 describes how the graph size influences the runtime of the algorithms, subsubsection 8.3.1.4 compares the runtime performance of the different algorithms and subsubsection 8.3.1.5 discusses how the instance affects the runtime. Lastly, I give a short summary in subsubsection 8.3.1.6.

# **8.3.1.1 Method**

I ran the different algorithms *m* times, distributed over *n* JVM invocations. For each of the *m* · *n* runs, I performed *w* warm-up iterations. An overview of the choices of these parameters for the different samples and algorithms is given in Table 8.4.

The times that I report for every sample, algorithm and instance are the average times of the *m* · *n* runs.

**Figure 8.1:** Runtime distributions for the various same-level problem solvers and instances – a-c: only non-large, d-f: including large

# **8.3.1.2 General Impression of Runtimes and Error Discussion**

Generally, for all instances and most programs, all algorithms finished within reasonable time. For large programs, the runtimes ranged between several minutes (classic/reach) and up to several hours (opt). Algorithm cons was not able to finish its computation on the large programs within the given time and space constraints.

In order to assess the quality of these averages, I also computed confidence intervals for confidence levels 1 − α = 0.95, assuming that the runtimes are normally distributed [124, §4.2]. The radii of these intervals can be found in the *err* columns of Table 8.5 and Table 8.6. With respect to this metric, it


**Table 8.4:** Overview of the chosen parameters for the evaluation of the different same-level problem solvers – *m* is the number of JVM invocations, *n* is the number of iterations per JVM invocation, *w* is the number of warmup iterations before each iteration

can be said that the accuracy of the reported runtimes corresponds to the number of runs performed to obtain the runtimes: For large programs and non-classic algorithms, the error can be up to 27% relative to the reported time. Conversely, for the rest of the configurations, errors tend to be small (up to 8%, 1-2% on average). However, the benefit of performing more runs on large programs is negligible with respect to the effort, since the runtimes are an order of magnitude larger than for the non-large programs. Also note that the runtimes are subject to distortions caused by continuous optimizations and garbage collection performed by the JVM. Apart from trying to distribute the runs over multiple JVM invocations, I do not consider these issues in the scope of this thesis. For further information, see Georges et al. [64].

# **8.3.1.3 Relationship Between Graph Size and Runtime**

Figure 8.2 visualizes how the runtimes of the various same-level problem solvers relate to the graph size. Graph size is measured in the number of edges. We see that generally the runtime increases with the number of edges. The regression appears to be somewhere between linear and quadratic, although it is rather difficult to make a reliable statement here since the number of programs is too small and has some notable outliers. For example, all solvers consistently take longer on maven than on dacapo-antlr, although maven's PDG has fewer edges than dacapo-antlr's PDG (compare Table 8.3, Table 8.5 and Table 8.6). Conversely, hsqldb performs better relative to freecs than the ratio of their respective graph sizes suggests. Still, the number of edges in a program dependence graph appears to be a sufficiently reliable criterion for estimating the runtime of the measured algorithms and does not contradict the theoretical complexity analysis conducted in subsection 8.1.3, which suggests that the runtimes of all same-level problem solvers grows no more than quadratically with the number of edges.

# **8.3.1.4 Comparison of the Algorithms**

Figure 8.3 shows how the different algorithms perform relative to each other on the chosen set of programs and instances. For this, I computed for each pair of algorithms (*a*<sup>1</sup> , *a*2) and each sample *s* the ratio of the runtimes that *a*<sup>1</sup> and *a*<sup>2</sup> took on *s*. Figure 8.3 shows boxplots of the resulting distributions.

Two key observations can be made here.

**8.3.1.4.1 reach Instance** For one, Joana's hand-optimized summary edge computation algorithm is clearly superior to the two generic algorithms. It can be up to 10 times as fast as the opt variant. This observation is not very surprising, since the summary edge computation is tailored to the reach instance and was optimized especially to work on large PDGs.

**8.3.1.4.2 Comparison of Opt and Cons** The other observation is that the opt variant performs moderately faster than the cons variant. Apart from the fact that for the majority of configurations, opt performed 2.5 to 3 times as fast as cons, opt was able to finish the computation on the large samples for all instances within the given time and space constraints, whereas cons was not. Recall from subsection 8.1.1 that the improvement in opt mainly consists of a more compact worklist item representation: The worklist contains procedures to be processed as opposed to pairs of entry/exit nodes. This results in a much smaller worklist, hence less memory consumption. The processing of a procedure consists of a complete intraprocedural traversal and re-computation for all intra-procedural node pairs. The price of this modification is that procedures are re-processed even if only a small part of them changed – resulting in potentially lots of spurious re-computations. This is also reflected in the runtime results.

**Figure 8.2:** Relationship between graph size and runtime for the various same-level problem solvers and instances

**Figure 8.3:** Comparison between the evaluated algorithms for same-level computation – a and b compare classic with opt and cons, respectively, c–e compare opt with cons

The opt variant is consistently faster than the cons variant, but only by a moderately small constant factor. Moreover, opt is able to process programs with rather large PDGs, whereas cons is not.

# **8.3.1.5 Comparison Between Different Instances**

Figure 8.4 compares the different instances with respect to the additional effort relative to the reach instance. For this purpose, I computed for each program the ratio between the required runtime for the respective instance and the reach instance. Figure 8.4 shows a boxplot of the distributions of these ratios.

**Figure 8.4:** Additional effort of same-level computation for non-reach instances (non-large programs)

For both generic algorithms, instance explicit-info-flow takes less additional effort than dist. Moreover, it can be seen that algorithm opt performs slightly better on dist: For cons, the majority of samples take at least 2.5 times as much time as for reach. The corresponding factor for opt is less than 2.

# **8.3.1.6 Summary**

The key takeaways of the runtime evaluation of the same-level problem solvers are:



8.3 Performance Evaluation


**Table 8.6:** Performance results for the summary information computation, part 2

# **8.3.2 Data-Flow Solvers**

I evaluated three data-flow solvers for all three instances:


Note that all evaluated instances are distributive, so that Algorithm 13 indeed produces a precise result.

For comparison, I also evaluated Joana's standard two-phase slicer for the reach instance (v2p-classic). The full results can be found in Table 8.8 and Table 8.9. A graphical overview is shown in Figure 8.5.

**Figure 8.5:** Overview of the runtime distributions of the evaluated data-flow solvers; a–c: only non-large programs, d–f: including large programs

Next, in subsubsection 8.3.2.1, I am going to describe the method that I used to obtain the results. After that, I will discuss several aspects of the results. Subsection 8.3.2.2 gives a general impression, subsubsection 8.3.2.3 considers the influence of the graph size on the runtimes, subsubsection 8.3.2.4 compares the different data-flow analysis algorithms and subsubsection 8.3.2.5 discusses the influence of the framework instances on the runtimes. Finally, I sum up my observations in subsubsection 8.3.2.6.

# **8.3.2.1 Method**

In order to obtain a runtime measurement for the various data-flow solvers, I took for each program *p* a sample *Sp* of *n* nodes from *p*'s PDG and then followed the following recipe:


Table 8.7 gives an overview of the parameters I chose for each configuration. For all configurations, I chose *q* = 0.5 (the median). To determine the parameters *l* and *u*, I used a statistical method for conservatively determining estimates of confidence intervals of quantiles without assumptions about the underlying distribution[124, §5.2.2].

# **8.3.2.2 General Impression of the Runtimes and Error Discussion**

Generally, the measured runtimes show that the v2p solver delivers reasonably fast times for all programs under evaluation, although it is clearly inferior to Joana's classic slicer on the reach instance. The cs0 algorithm was able to finish in all configurations, while the cs1 solver did not finish on large program within a reasonable amount of time.

The errors show the same pattern as in the evaluation for the same-level problem solvers: For most configurations, they are relatively small. For the large programs, where the number of nodes and runs was smaller, they can be very large. Hence, the times for the large programs must be read with caution. Keeping this in mind, I will not further discuss errors in the following.

<sup>38</sup>For v2p and v2p-classic, where most times tended to be very short, I let each iteration run in a loop for at least 1 second and reported the average times.


**Table 8.7:** Overview of the chosen parameters for the evaluation of the different data-flow solvers

# **8.3.2.3 Relationship Between Graph Size and Runtime**

Figure 8.6 visualizes how the runtime of the various data-flow analysis algorithms relate to graph size. Graph size is measured in the number of edges.

Generally, it can be seen that the runtime increases with graph size. Both for the classic slicer and the v2p algorithm, the runtime appears to be roughly linear in the graph size, whereas the call-string algorithms suggest a superlinear regression. We also see that there are outliers. For example, the times for hsqldb is consistently much lower than the times for dacapo-eclipse, although hsqldb is the program whose PDG has the highest number of edges. A reason for this may be that the evaluated algorithms not only solve a constraint system but also perform a reachability analysis and their runtime is also affected by the size of their forward slices: If hsqldb is less connected than dacapo-eclipse, its forward slices are smaller and data-flow analyses on hsqldb take less time than on dacapo-eclipse.

# **8.3.2.4 Comparison of the Algorithms**

Both callstring-based algorithms perform worse than the two-phase approach. This can be seen in Figure 8.7, which visualizes the distribution of the respective runtime ratios. While cs0 shows roughly the same runtime

**Figure 8.6:** Relationship between graph size and runtime for the various DFA algorithms and instances

**Figure 8.7:** Performance comparison between the call-string-based algorithms and v2p

behavior as v2p for the reach and the explicit info flow instances, it can however take up to twice as much time. The dist instance shows a clear deviation – here cs0 generally takes twice as much time and can take up to 3 or 4 times as much time.

The other evaluated callstring-variant cs1 shows a much worse behavior: It runs 5-10 times as long as v2p and may take up to 60 times.

# **8.3.2.5 Comparison Between Different Instances**

Another observation is that, like for the same-level problem solvers, it appears that more complex instances require more effort. This can be seen in Figure 8.8. It shows that the explicit-info-flow instance take roughly twice as much time as reach. This is consistent among all evaluated algorithm. The dist instance shows a more heterogeneous picture. Figure 8.8 shows that at least twice as much effort is required for dist relative to reach. However, this additional effort is much higher for the callstring-based approaches and appears to grow with the depth of the call-strings.

# **8.3.2.6 Summary**

The key takeaways of the runtime evaluation of the data-flow problem solvers are:

**Figure 8.8:** Additional effort of DFA for complex instances relative to reach; a/b: only non-large programs, c/d: including large programs



**Table 8.8:** Runtime performance of the various data-flow solvers for reach instance


**Table 8.9:**

Runtime

performance

 of the evaluated

 data-flow

 solvers for complex

 instances

# **8.4 Precision Evaluation**

In my evaluation, I not only measured performance, but also the precision of the considered data-flow analyses. In particular, I practically compared the precision of cs0 and cs1.

It is not obvious how to measure precision of a data-flow analysis. Hence, in subsection 8.4.1, I am going to consider this aspect more closely and describe a general method that allows to practically assess the precision of a given data-flow analysis. In subsection 8.4.2, I will present the results of my precision evaluation and describe how I obtained them. After that, I will discuss the result in subsection 8.4.3.

# **8.4.1 How to Measure the Precision of Data-Flow Analyses**

In program analysis theory, precision is usually a *binary* concept – a program analysis is either precise with respect to a given ideal baseline, or it is not.

However, for practical purposes, it is desirable to perceive precision as *comparative*. With a comparative notion of precision, one can make statements like "analysis A is more precise than analysis B".

In chapter 6, we already encountered results that may be read as pointers to comparative precision statements.

For example, in Corollary 6.33 we saw that the MO*AP*S*<sup>k</sup>* is correct with respect to MO*AP*S*<sup>l</sup>* if *<sup>k</sup>* <sup>≤</sup> *<sup>l</sup>*. In other words, MO*AP*S*<sup>l</sup>* is always at least as precise as MO*AP*S*<sup>l</sup>* if *k* ≤ *l*. Moreover, it is easy to give examples for which MO*AP*S*<sup>l</sup>* yields a result that is strictly more precise than the result for MO*AP*S*<sup>k</sup>* .

However, it is very challenging if not impossible to theoretically and generally assess the precision of program analyses comparatively<sup>39</sup> .

It is therefore more promising to concentrate on practical evaluation that usually evaluates the different analyses on a concrete sample of example programs.

<sup>39</sup>According to Jansen [97], different approaches to interprocedural data-flow analyses are comparable, but only in simple cases.

One possibility to establish a comparative notion of precision is to *quantify* it using some metric that assigns an analysis a number that assesses *how precise* this analysis is. Then, the precision of multiple approaches can be compared using this metric.

In chapter 4, we saw two examples for this. Firstly, in the scope of the SHRIFT approach we compare different points-to analyses of Joana by relating the number of reported information flows with the number that a black box approach would report that just assumes that every sink depends on every source. Secondly, in our work on ifspec, we compare the precision of different information flow tools by relating the number of insecure programs with the number of programs that were classified as insecure by the respective tool.

In the following, I describe a method for practically evaluating the precision of a given data-flow analysis approach. The method is independent of instances and algorithms and only makes a few general assumptions. The basic idea is to compute a metric that indicates how close a given solution of a given data-flow analysis approach is to the respective section of the *MOVP* solution. By computing this number for a multitude of solutions, we get a distribution for the given analysis. Multiple analyses can then be compared with respect to this distribution.

Let F = (*G*, *L*, *F*⊠, ρ) be a data-flow analysis framework and let D be a data-flow analysis. Moreover, I fix a node *s* ∈ *N*. Analysis D takes *s* as input and outputs a function <sup>A</sup>(*s*) : *<sup>N</sup>* <sup>→</sup> *<sup>F</sup>*⊠. I want to compare <sup>A</sup>(*s*) with the portion of *MOVP* where the first argument is *s*. Therefore, I introduce the function MO*VP*(*s*) : *N* → *F*<sup>⊠</sup> that is defined by

$$\mathbf{MOVP}^{(s)}(t) \stackrel{def}{=} \mathbf{MOVP}(s, t).$$

I assume that, regardless of *s*, D only produces (*MOVP*,{*s*} × *N*)-correct solutions, i.e. ∀*t* ∈ *N*. A(*t*) ≥ *MOVP*(*s*,*t*). Moreover, I assume that F allows for practically evaluating *MOVP*, e.g. that it is distributive and allows for an effective execution of Algorithm 9 and Algorithm 13.

A straight-forward way to assess the precision of <sup>A</sup>(*s*) is to evaluate the fraction of *<sup>t</sup>* for which <sup>A</sup>(*s*) (*t*) coincides with *MOVP*(*s*) (*t*). I call this metric the *value precision* and define it as follows:

$$vp(s) \stackrel{def}{=} \frac{|\{t \in dom(\mathcal{A}^{(s)}) \mid \mathcal{A}^{(s)}(t) = MOVP^{(s)}(t)\}|}{|dom(\mathcal{A}^{(s)})|}.$$

The function *vp* assumes values between 0 and 1 and measures the coincidence between <sup>A</sup>(*s*) and *MOVP*(*s*) . A value of <sup>0</sup> means that <sup>A</sup>(*s*) does not coincide at all with *MOVP*(*s*) , where as a value of 1 means that *vp*(*s*) perfectly coincides with *MOVP*(*s*) . However, one issue of *vp* is that it does not differentiate between the two main reasons for <sup>A</sup>(*s*) (*t*) and *MOVP*(*s*) (*t*) to differ. For one, it may be the case that *<sup>t</sup>* <sup>∈</sup> *dom*(*MOVP*(*s*) ) and *Analysis*(*s*) (*t*) ≠ *MOVP*(*s*) (*t*), i.e. that *t* is a node for which every (*MOVP*,{*s*} × *N*)-correct analysis must actually compute a result. The second case is that *<sup>t</sup>* <sup>∈</sup> *dom*(A(*s*) ) \ *dom*(*MOVP*(*s*) ), i.e. that A computes a value for *t*, although this is not absolutely necessary for a context-sensitive analysis (and therefore imprecise). To distinguish between these two cases, I introduce two additional metrics, namely the *relative slice size*

$$\operatorname{ss}(s) \stackrel{def}{=} \frac{|dom(MOVP^{(s)})|}{|dom(\mathcal{H}^{(s)})|}, \text{and}$$

the *value precision on the common core*

$$vp\_{cc}(s) \stackrel{def}{=} \frac{|\{t \in dom(MOVP^{(s)}) \, | \, \mathcal{F}^{(s)}(t) = MOVP^{(s)}(t)\}|}{|dom(MOVP^{(s)})|}.$$

Both *ss*(*s*) and *vpcc*(*s*) also assume values between 0 and 1 and larger value reflect more precision – while *ss* focuses on the slice, *vpcc* focuses on values. This is reflected by the equation

$$(8.1)\tag{8.1} \qquad \qquad vp(s) = ss(s) \cdot vp\_{\mathcal{IC}}(s).$$

The validity of (8.1) can be seen as follows: From *<sup>t</sup>* <sup>∈</sup> *dom*(*MOVP*(*s*) ) and <sup>A</sup>(*s*) (*t*) = *MOVP*(*s*) (*t*), it follows that *<sup>t</sup>* <sup>∈</sup> *dom*(A(*s*) ). Hence, we can also write *vpcc* as

$$vp\_{cc}(s) = \frac{|\{t \in dom(\mathcal{A}^{(s)}) \, | \, \mathcal{A}^{(s)}(t) = MOVP^{(s)}(t)\}|}{|dom(MOVP^{(s)})|}.$$

366

and from this, Equation 8.1 follows by an easy calculation.

With help of (8.1), I can identify two important special cases. For one, if *vpcc*(*s*) = 1, then <sup>A</sup>(*s*) assumes perfectly context-sensitive values on *dom*(*MOVP*(*s*) ) but may be too large (if *ss*(*s*) < 1). Secondly, if *ss*(*s*) = 1, then *dom*(A(*s*) ) coincides with *dom*(*MOVP*(*s*) ) but may contain different results (if *vpcc* < 1).

# **8.4.2 Results**

In my precision evaluation I applied the methodology described in subsection 8.4.1 to obtain a practical precision comparison between the two call-string approaches cs0 and cs1. I considered the programs from Table 8.2 and the three instances reach, explicit-info-flow and dist that I already considered for my performance evaluation. Note that all instances are distributive and can be solved precisely using the functional approach, so that I can use the v2p algorithm to provide *MOVP*-solutions. For each of the programs and the instances, I evaluated all three metrics *vp*, *ss* and *vpcc* for a sample of randomly selected nodes in the respective program's PDG. For the non-large programs, I took 100 nodes, whereas for the large programs, I took 10 nodes.

The distributions of the evaluated metrics are shown in Figure 8.9.

# **8.4.3 Discussion**

In the following, I want to briefly discuss the results that are visualized in Figure 8.9. Generally, the *ss* distributions for cs0 and cs1 are very similar. In particular, with respect to the relative slice size, cs1 offers only a little precision gain in comparison with cs0. Notably, for the reach instance, cs0 and cs1 differ only in their *ss* distributions. The values on the common core coincide completely, so that *vpcc* is 1 for all measurements. This is indeed no wonder because data-flow solutions for the reach instance can assume exactly one value.

On the complex instances explicit info flow and dist, cs1 delivers more precise results than cs0 on the common core. I suspect that the reason for this is that the dist instance offers a larger space of possible values with more possibility for the different data-flow analyses to differ. On the other

**Figure 8.9:** Comparison of cs0 and cs1 with respect to precision; a – c: slice sizes, d – f: value precision on common core, g – i: overall value precision

hand, we also see that *vpcc* for the dist instance offers more variability for cs1 than for cs0.

All in all, we see that, based on this sample, cs0 and cs1 offer similar result with respect to the amount of spurious values. Regarding the computed values on the common core, cs1 tends to deliver a more precise value on the common core, especially for complex instances. It can be imagined that call-string approaches with a higher bound deliver even more precise results. However, with regards to the performance results, the question is whether the precision gain is worth the additional effort.

*Looking through the bent-backed tulips, to see how the other half live.*

# <sup>T</sup>he <sup>B</sup>eatles **9**

# **Discussion and Related Work**

In this chapter, I critically discuss the approach that I developed in the last chapters and compare it to the existing literature.

In section 9.2, I discuss restrictions of my approach and simplifications that I deliberately applied for the sake of presentation. After that, I look at some of the benefits of my approach and discuss possible improvements in section 9.3. Lastly, in section 9.4, I consider other approaches that could also be used to generalize slicing.

Before I start with the actual discussion, I want to give some general remarks that may help to place the work in this thesis in the right context.

# **9.1 The Role of Data-Flow Analysis and Slicing in Program Analysis**

Schmidt and Steffen [146] propose the view that a multitude of program analyses mainly consists of three steps: Given a program and an operational semantics, the program is first transformed into a model that adequately captures its semantics. This program model is usually some kind of labeled transition system – i.e. a (possibly infinite) graph whose nodes represent the program state and whose edges represent the possible state transitions. The second step consists of an abstraction that abstracts from irrelevant details or makes the model more tractable and still respects the semantics. Lastly, this abstraction of the program model is analyzed with respect to graph-theoretic properties.

If all steps indeed respect the given program semantics, graph theoretic properties of the program model abstraction can indeed be mapped to actual properties of the program.

Following this view, both data-flow analysis and context-sensitive slicing are techniques that operate as third step. That is, control-flow graphs and program dependence graphs are abstractions of underlying program models. One main result of chapters 5–7 is that program dependence graphs and control-flow graphs can be seen as instances of the same general graph model. However, this generalization ignores program semantics. Whether or not the results that generalized data-flow analyses yield can be transferred to actual program properties is out of the scope of this thesis. For data-flow analyses on control-flow graphs, we can state that if they are constructed properly, they can give semantic guarantees. However, such a statement cannot easily be generalized to data-flow analyses on interprocedural graphs, let alone transferred to program dependence graphs. I will consider semantics more closely in subsection 9.2.4. Hence, within the scope of this work, generalized data-flow analysis is to be understood as an abstract technique for analyzing graphs with respect

to their path sets.

# **9.2 Simplification and Restrictions**

In the following, I discuss some aspects that I chose to simplify for the sake of presentation and uniformity. Moreover, I look at some of the restrictions of generalized data-flow analysis on interprocedural graphs and discuss possible ways to lift them.

The first two subsections are dedicated to the simplifications and the last two subsections discuss the restrictions.

# **9.2.1 Forward Analysis vs. Backward Analysis**

Forward analyses consider the propagation of data-flow information along the paths of the given directed graph, in the direction given by the graph's edges. In contrast, backward analyses consider the propagation of data-flow information against the direction given by the graph's edges. There are numerous program analyses that are naturally expressed as backward data-flow analyses (cf. [130, Figure 2.6]). Another important example of a backward analysis is slicing in its original formulation. As I explained in subsection 3.3.1, program slicing was originally introduced as a technique for focusing on the parts of a program that contribute to the value of a given variable at a given program location. This naturally leads to the notion of *backward slices*. Hence, subsequent work on slicing mainly focused on computing backward slices. Even the summary edge computation was originally presented as a backward analysis [93, 137], although this is not strictly necessary, since the property of same-level path reachability is symmetric.

Although I mainly concentrated on forward analyses, all my considerations apply to backward analyses as well. Adapting my framework to backward analyses would result in algorithms that are even closer to the original slicing and summary edge computation approaches.

# **9.2.2 Functional Level vs. Ordinary Level and Initial Values**

My representation of data-flow analyses uses an ordinary lattice *L* of possible values and a lattice *F* that consists of monotone functions on *L*, contains the identity function and is closed under function composition. The goal function *MOVP* has values in *F* – i.e. it assigns a pair (*s*,*t*) a function *MOVP*(*s*, *t*) that represents the transformation of data-flow facts along all valid paths from *s* to *t*. After *MOVP* has been computed or approximated, one can apply *MOVP*(*s*,*t*) to some value *l* ∈ *L* to yield the transformed value *MOVP*(*s*, *t*)(*l*).

Classically, one is not interested in the function *MOVP*(*s*,*t*) but rather the value *MOVP*(*s*,*t*)(*init*) for a specific element *init* ∈ *L*, called *initial information*. This element is traditionally associated with the entry or exit node of the procedure or program to be analyzed. Hence, it is customary to include *init* into a data-flow instance and focus on the computation of data-flow analysis solutions that aim to approximate *MOVP*(*s*, *t*)(*init*) for *t* ∈ *N*. This is also how I introduced classical intra-procedural data-flow analysis in subsubsection 3.2.2.1. If *s* is not a fixed node, then we can also consider *init* as a function *N* → *L*.

Both the functional and the call-string approach to interprocedural dataflow analysis can in principle be formulated in this way: It is not hard to come up with variations of Constraint System 6.5, Constraint System 7.1, Constraint System 6.2 and the corresponding algorithms and correctness results that employ the initial information. The only component that requires the functional level is the same-level solution. The reason is that we want to use MO*SL* (or the least same-level solution, respectively) to "fill the gap" between an entry node *n*<sup>0</sup> and an exit node *n*<sup>1</sup> (or between a call node and a corresponding return node, respectively), without knowing the value that arrives at *n*0. That is, we explicitly need a function that describes the transformation of data-flow facts along same-level paths from *n*<sup>0</sup> to *n*<sup>1</sup> . I chose to completely stay on the functional level because it is required for the same-level problem and I wanted my presentation to be uniform.

# **9.2.3 Concurrency**

My analysis framework only considers nesting structures that occur in sequential programs. For both control-flow graphs and program dependence graphs, extensions have been proposed to support concurrency. In the following, I want to discuss these extensions briefly.

For control-flow graphs, it has been shown that it is possible to support simple parallelism constructs [151] and even dynamic thread creation [117] and enable a limited yet important class of data-flow analyses.

Program Dependence Graphs have also been extended to support multithreading [110, 86, 65, 66]. As I briefly explained in subsection 4.2.2, additional edges called *interference edges* model data dependencies across thread borders. Multi-threaded PDGs can also be sliced using an *iterated two-phase slicing approach* that was first described by Nanda et al. [129] and later also considered by Hammer [86] and extended by Giffhorn [65]. The basic idea is to employ an additional loop around the two-phase slicer that invokes a two-phase slice each time an interference edge is encountered. A possible generalization of my framework to concurrent PDGs would be to formally characterize the paths that are traversed by the iterated two-phase slicer and then establish a monotone constraint system that characterizes the data-flow along these paths. A worklist algorithm that solves this system could then turn out to be a generalization of Nanda's iterated two-phase slicer.

Note that for the iterated two-phase slicer, we cannot expect that the iterated two-phase backward slice can be used to verify a non-interferencelike property (or a result such as the slicing theorem, respectively). As I explained in subsection 4.2.1, additional dependencies have to be taken into account to obtain such a result. However, I think it is possible to re-formalize e.g. the RLSOD check, which I described in subsection 4.2.1, in such a way that it performs a form of slicing instead of checking, possibly on an extension of the multi-threaded PDG by an additional type of dependency. As for the iterated two-phase slicer, one could establish a monotone constraint system that characterizes the data-flow along the paths that are traversed by the RLSOD slicer.

In summary, I think that it is possible to extend my framework for multithreaded PDGs. However, such an extension would probably be specific to PDGs and I suspect that the resulting data-flow analysis cannot be unified with data-flow analysis for multi-threaded control-flow graphs as described by Lammich and Müller-Olm et al. [117].

# **9.2.4 Semantics**

Classic data-flow analyses have a strong connection to program semantics. Control-flow graphs can be considered as static approximations of the possible program executions. This connection can be used to formally characterize the program properties that a given data-flow analysis verifies [45].

Program Dependence Graphs can also be connected to program semantics, albeit not that directly: As we saw in subsection 3.4.1, PDGs have been semantically justified in the sense that equivalent programs have isomorphic PDGs and the reachability instance, i.e. PDG-based slicing, has been shown to verify non-interference. However, it is unclear how such results can be extended to other data-flow analyses on PDGs.

In particular, it is not clear what a given data-flow analysis result along the paths of a program's dependence graph tells about the executions of that program. For example, the least distances analysis from subsection 5.4.7 provides purely graph-theoretic information about the structure of the given graph.

Hence, generalized data-flow analyses on interprocedural graphs per se only compute properties of the given graph and additional arguments are necessary to provide the connection to a reference semantics such as program semantics. Nonetheless, I still think that such generalized analyses can be useful. For example, the least distances analysis from subsection 5.4.7 could be extended in such a way that it also constructs a shortest path<sup>40</sup> that can serve as a "simplest witness" for the connectedness of two nodes. Moreover, strong bridges or strong articulation points (as described in subsection 5.4.4) can be computed in order to automatically infer parts of a program dependence graph where it may be promising to increase analysis precision.

# **9.3 Benefits and Possible Improvements of My Approach**

In this section, I discuss possible improvements and benefits of my approach.

# **9.3.1 Applicability of Existing Extensions and Improvements**

Generally, the approaches that I describe in this work are extensions of the two approaches presented by Sharir and Pnueli [154]. As such, they inherit all benefits and drawbacks.

The functional approach yields the most precise result but only works *e*ff*ectively* for a given data-flow framework instance F if the lattice of functions of F satisfies the ascending chain condition and functions can be encoded effectively.

In the literature, we can find several contributions that improve certain aspects of the functional approach and that can also be applied in the context that I consider here.

For example, Reps et al. [136] consider an important class of data-flow problems that can be encoded particularly well. For this class, which consists of data-flow analyses with a finite subset lattice and distributive transformers, it is possible to encode the summary functions in such a way that the whole data-flow analysis can be reduced to graph reachability. Sagiv et al. [145] extend this work to data-flow problems in which the

<sup>40</sup>I suspect that such an algorithm would generally consist of two parts: (a) a two-phase approach that treats the edge functions and same-level information as weights and works analogously to a classic algorithm and (b) a second step that exploits same-level information to iteratively replace summary edges by shortest *same-level paths*.

data-flow facts are mappings from variables to lattice values but the transformers still enjoy distributivity properties.

Moreover, one shortcoming of the functional approach of Sharir and Pnueli is that it does not properly support local variables. Knoop and Steffen [106] present a solution for this: They extend a given data-flow framework instance by a stack component and additional transformers for modelling parameter-passing. This technique works solely on the level of the dataflow framework instance and therefore only needs little adaption of the analysis approach itself.

# **9.3.2 Benefits of My Framework Compared to Adhoc Approaches**

My theory gives a formal characterization of the results of data-flow analyses in terms of information transformers along certain path sets and provides generic algorithms to generate these results. This is beneficial in situations where algorithms on PDGs are considered that can be expressed as data-flow analyses. Having available a general result that only needs to be instantiated to a concrete framework instances eliminates the necessity of a separate correctness argument.

One notable example is Hammer's approach to information flow control, which I considered in subsection 5.4.6. While Hammer notes that his approach to IFC can be expressed as a data-flow analysis, he only applies this fact in the intraprocedural case [86, p. 103]. For the interprocedural case with declassification, he presents adapted versions of the well-known summary edge algorithm [86, Algorithm 8/9] and the two-phase slicer ([86, Algorithm 7]) and gives dedicated correctness arguments (see [86, Theorem 4.10] and [86, Theorem 4.4], respectively).

With my framework, such separate correctness arguments are not necessary: It is only necessary to show that Hammer's approach to IFC with declassification can be performed by computing the *MOVP* solution of an appropriate data-flow analysis (as I did in subsection 5.4.6). Then, e.g., Algorithm 9 and Algorithm 12 can safely be used to compute the least solution, in connection with appropriate checks.

Another example, which I want to discuss at this point, is barrier slicing. I already considered barrier slicing in subsection 5.4.5.

Krinke [111] proposes a two-phase approach with a barrier-specific preprocessing phase to compute context-sensitive barrier slices. The preprocessing phase relies on the existence of summary edges. It starts with the assumption that all summary edges are *blocked*, i.e. that all same-level paths contain nodes from the given barrier. Then, it iteratively unblocks all summary edges for which it can construct a barrier-free same-level path. The following two-phase approach then uses only unblocked summary edges and itself skips the barrier. While the correctness of Krinke's approach is intuitively plausible, he does not prove formally that his approach indeed computes context-sensitive barrier slices. Moreover, Krinke's approach is tailored to the problem of computing barrier slices. It is unclear how his approach needs to be adapted in order to compute slices with other properties. As I pointed out in subsection 5.4.5, barrier slicing can be viewed as an instance of a more general problem, namely the problem of computing all nodes that are reachable using paths of a given regular language. This means that barrier slices can be computed with the help of Algorithm 9 and Algorithm 12, which both come with formally proven correctness properties. Moreover, if we want to consider slices with other regular properties, all that we need to do is change the dataflow framework instance and use the algorithms for the other instance. However, it is worth pointing out that this flexibility comes at a price, at least when using the functional approach: Since summary information is specific to the framework instance, it can only be re-used for problems of the same instance. Moreover, the data-flow framework instance for barrier slicing is dependent on the barrier. Hence, the summary information has to be re-computed if the barrier changes. This problem was also noted by Krinke [111, p. 4].

There are two potential remedies to this drawback of using a generic approach. Firstly, one could use a call-string approach. Call-string approaches are also generic but do not need barrier-specific pre-processing phases. However, as we saw in chapter 8, they are significantly less precise and considerably more costly. Secondly, one could try to use a more elaborate data-flow framework instance. Such an instance would propagate the information "this path skips the following nodes" – i.e. node sets – instead of the binary information "this path skips the given barrier". I suspect that this would result in a more expensive summary information computation phase, since its lattice is more complex, but potentially increases the re-usability for a variety of barriers.

# **9.4 Alternative Approaches to Generalize Slicing**

In this thesis, I consider context-sensitive slicing on program dependence graphs as a form of data-flow analysis on a generalized interprocedural graph model. However, data-flow analysis is not the only technique that can be unified with slicing. In the following subsections, I take a look at three formalisms from the literature for which I suspect that they are also suitable to represent program dependence graphs and context-sensitive slicing.

# **9.4.1 Pushdown Systems**

One alternative approach is to employ *pushdown systems*[36, 90]. Pushdown systems are extensions of finite state machines that are able to represent the control-flow in sequential programs with recursive procedure calls. A *configuration* of a pushdown system consists of a *control state* and a *stack*. The control state may assume finitely many values and the stack consists of a (finite but arbitrarily long) list of *stack symbols* from a finite alphabet. Possible transitions of a pushdown system may alter the control state and manipulate the stack, depending on the stack's top symbol.

An interprocedural (program dependence) graph *G* can be represented as a pushdown system as follows: The possible control states are the nodes of *G*, while the stack alphabet consists of the *G*'s call edges. The transitions can then be defined in a similar fashion as the constraints from Constraint System 6.5.

Pushdown systems can be analyzed with respect to their configuration space: For any regular set *C* of configurations, the set *pre*⋆(*C*) of configurations that reach *C* by a sequence of allowed transitions is also regular [36]. In particular, if *C* is given as a finite automaton, one can use a saturation procedure to construct a finite automaton that accepts *pre*⋆(*C*). This result can be applied to obtain a context-sensitive slicer that is more flexible than classical two-phase slicing. The slicing criterion does not need to be a set of plain nodes but can also encode regular properties about the possible call stacks. A simple version of such a context-restricted slicer was already considered by Krinke [112].

The analysis of pushdown systems is not restricted to reachability. A pushdown system P can also be equipped with *weights* and it is possible to compute a form of merge-over-all-paths solution on the configuration transition graph of P [147]. The structure of these weights is largely similar to the transfer functions considered in data-flow analysis. This idea, which was already noted by Schwoon et al. [147], was further developed by Reps et al. [139], who showed that weighted pushdown systems are general enough to express important special cases of interprocedural data-flow analysis.

# **9.4.2 Recursive State Machines**

Another formalism to represent PDGs, which I want to briefly mention, are *recursive state machines* [13]. Recursive state machines are a model of sequential, imperative, recursive programs and consist of multiple components, which may have multiple entries and exits. Slicing then can be expressed as reachability analysis on recursive state machines. Such an analysis can be performed using a functional approach [13].

# **9.4.3 Visibly Push-Down Languages**

The valid paths considered in this thesis appear to be an example of *visibly push-down languages* [15, 14]. Their key feature is that they are recognized by a class of push-down automata that are restricted in their stack manipulation operations. This restriction is still general enough to be useful in program analysis – yet, visibly push-down languages enjoy nice closure properties. For example, they are closed under intersection (unlike general context-free languages) and union (unlike deterministic contextfree languages). Hence, by employing visibly push-down languages, one could define and compute language-restricted slices with respect to properties that are expressible by visibly push-down languages – a generalization of the regular language-restricted slices considered in subsection 5.4.5.

*All things must pass.*

# <sup>G</sup>eorge <sup>H</sup>arrison **10 Conclusion**

# **10.1 Summary and Main Theses**

In the following, I give a summary of this dissertation and revisit the main theses stated in section 1.3.

# **10.1.1 Applications to Software Security**

**Summary** Chapter 3 gave a general overview of static analysis techniques and data structures, including data-flow analysis on control-flow graphs and slicing on program dependence graphs. It also mentioned the connection between slicing and information flow control. The last section of chapter 3 described the PDG-based information flow control tool Joana and several analysis techniques for object-oriented languages such as Java. Subsequently, in chapter 4 I reported on the contributions of the programming paradigms group at KIT to the priority program RS<sup>3</sup> .

I presented research results of the sub-project "Information Flow Control for Mobile Components" concerning information flow control for concurrent languages. In particular, I described a static PDG-based check to guarantee probabilistic non-interference that was developed in our group within the scope of RS<sup>3</sup> .

I also reported on the contributions of our group to two of the three reference scenarios of RS<sup>3</sup> , namely "Security In E-Voting" and "Software Security For Mobile Devices". In the former, Joana is combined with a theorem prover to verify cryptographic properties of prototypical electronic voting systems, and in the latter, Joana provides static checks of user-defined security policies in the server component of a secure app store.

Lastly, I described several collaborations within RS<sup>3</sup> . In these collaborations, we demonstrate that Joana can be used to increase the precision and performance of dynamic usage control and to simplify the security verification obligations in component-based systems. A third cooperation is concerned with the development of RIFL, a machine-readable language dedicated to the specification of security properties. RIFL specifications can not only be read by machines, but also be checked by information flow analysis tools. I contributed a Joana-back-end for RIFL. An application of RIFL is ifspec, a benchmark for information flow analysis tools.

**Main Thesis 1: PDG-based information flow control is useful, practically applicable and relevant.** As shown in chapter 4, applications of Joana range from highly relevant scenarios such as mobile security and electronic voting systems to the support of both theorem provers and dynamic usage control systems.

# **10.1.2 Systematic Approaches to Advanced Information Flow Analysis**

**Summary** In chapter 5, I developed a general notion of valid paths and described a graph-based model and a data-flow framework that incorporates both interprocedural data-flow analysis on control-flow graphs and PDG-based slicing as special cases. I discussed several examples from the literature that can be expressed systematically within this framework. Chapter 6 demonstrated that instances of the general framework developed

in chapter 5 can indeed be solved with the two classical approaches of Sharir and Pnueli [154] – the functional approach and the call-string approach, respectively. I specified monotone constraint systems whose least solutions can be used to compute an over-approximation of the *merge-over-all-validpaths (*MO*VP)* solution given by the framework instance. Similar to classic results, I showed that the functional approach and the unrestricted call-string approach both fully characterize the MO*VP* solution. For the call-string approach, I gave a sufficient criterion under which it yields a correct over-approximation of MO*VP* using a possibly finite constraint system. I showed that this criterion is in particular satisfied for call-strings whose length is at most *k*.

In chapter 7, I showed how the constraint systems developed in chapter 6 can be solved algorithmically. Here, I combined a classical worklist-based solving algorithm with a reachability analysis that explores the *relevant core* of the given constraint system. Given a set of variables to start with, only the constraints that the initial variables may influence are solved, provided that the initial variables satisfy a regularity condition. Roughly speaking, this can be imagined as computing a *(forward) slice of the given constraint system*. I instantiated the resulting algorithm multiple times to obtain solving algorithms for the constraint systems of the functional and the call-string approach. Using this method, I showed that the functional approach can be performed analogously to the methods proposed by Horwitz et al. [93, 137] for context-sensitive slicing: First, the same-level problem needs to be solved. This can be understood as a generalization of the summary edge computation. After that, a two-phase algorithm can be used to solve the actual problem. I also showed that the call-string approach can be performed using an appropriate instance of the general algorithm.

Within the scope of this thesis, I not only developed a general framework and its solution approaches theoretically, but also implemented it in Joana and evaluated it to demonstrate that it is practically feasible. In chapter 8, I presented my implementation and discussed some of the practical choices I made. In addition, I discussed the methods and results of my evaluation. Last but not least, in chapter 9 I discussed my approach and put it into the context of related and similar work. I pointed out the restrictions and possible improvements and extensions. Moreover, I discussed other formalisms that also appear to be a natural generalization of contextsensitive slicing.

**Main Thesis 2: Data-flow analysis can be systematically applied to program dependence graphs.** The theory that I developed in chapters 5–7 proposes to view context-sensitive slicing as a special case of a generalized version of the classical technique of interprocedural data-flow analysis. Particularly, this applies to earlier PDG-based approaches such as Hammer's IFC [86, 87] and Krinke's barrier slicing [111, 110].

Hence, PDG-based approaches can profit from the advantages of a rich, generic toolkit for systematically deriving sophisticated analyses: If a given problem on a PDG can be expressed as a data-flow analysis instance, the framework provides generic and re-usable solution algorithms with general correctness guarantees that give formal descriptions of the solutions. This also includes correctness arguments such as the ones given by Hammer and Krinke for their respective problems.

Moreover, I demonstrated that generalized data-flow problems can be described with both a functional approach and a call-string approach. The resulting constraint systems can then be solved with a general solution algorithm that integrates a classical worklist algorithm with a reachability analysis. The instantiations of this algorithm for the functional approach can be modified in a way such that they resemble a generalization of the summary edge computation and the two-phase approach known for context-sensitive slicing [93, 137], respectively.

**Main Thesis 3: Data-flow analysis on PDGs can be practically conducted.** My evaluation demonstrates that it is also practically feasible to solve complex data-flow analysis problems on program dependence graphs. Moreover, it shows that the functional approach outperforms the call-string approach with respect to both performance and precision. A side product of the precision evaluation is the formal description of an instance- and approach-independent method for the precision evaluation of data-flow analyses. Future work can use this method to ensure comparability.

# **10.2 Future Work**

I want to close this thesis by giving an outlook on future work. I restrict this outlook to the area of generalized data-flow analysis on interprocedural graphs like program dependence graphs, to which I consider this thesis as a starting point.

# **10.2.1 Approximation of Same-Level Information**

One characteristic of the functional approach is that the two-phase algorithm that solves the actual constraint system relies on a same-level solution. The most precise same-level solution can be computed by an algorithm like Algorithm 9 or Algorithm 17. The evaluation in chapter 8 shows that this can be quite expensive. However, my theory provides a remedy for the case that precision can be sacrificed. In fact, the correctness properties in section 7.3 state that the algorithms for computing ascending and non-ascending-path solutions still produce correct results if they are fed with a same-level solution that is not as precise as possible. This opens up a possibility for less precise yet faster ways to produce correct same-level solutions.

An extreme example would be to simply use a same-level solution whose value is always the greatest element ⊤. This is surely *SL*-correct because of the maximality property of ⊤. More generally, we could exploit domainspecific knowledge about the data-flow framework instance and use a value for which we know that it is a universal upper bound of MO*SL*. For example, consider the dist instance that was described in subsection 5.4.7 and evaluated in chapter 8: According to its definition we have ⊤ = 0. Using such a simplified same-level information would amount to a distance calculation that would assume that entries and exits of the same procedure are connected by a path with length 0. Such a distance calculation would result in statements like "nodes *s* and *t* are connected by a valid path that has a length of at least *n*", which are still correct if the length of same-level paths is coarsely underestimated<sup>41</sup> .

Other approximations of same-level information are imaginable. One variant, which I considered more closely within the scope of this thesis but did not evaluate practically, uses a **N**-indexed family of constraint systems C*n*, so that for all *n* ∈ **N** we have


Property (a) ensures that we can correctly use any *l f p*(C*n*) as same-level information, while property (b) entails that we can get more precision by just increasing *n*. A special case of this scheme uses constraint systems C*<sup>n</sup>* that are defined in a similar fashion as Constraint System 6.1, but have a different sl-sol-(iii)-clause: Instead of recursively relying on the solution characterized by the constraint system itself, it consults *l f p*(C*<sup>k</sup>* )

<sup>41</sup>As I already briefly mentioned in subsection 5.4.7, Krinke [110] also considered distances in system dependence graphs and since he does not provide details about how he computed distances of same-level paths, I firmly suspect that he assigned summary edges a value of 1 (like any other edge), which also is a very coarse underestimation.

for some *k* < *n* as a helper solution for same-level information42. Property (a) ensures that this indeed characterizes a solution that is correct with respect to MO*SL*. Every *l f p*(C*n*) can then be computed by subsequently computing *l f p*(C0), . . . , *l f p*(C*n*−<sup>1</sup> ) and then, finally, computing *l f p*(C*n*).

# **10.2.2 Further Exploration of Stack Spaces and** *MOVP***-Correct Abstractions**

In chapter 6, I introduced stack spaces as an abstract structure for representing call stacks. The constraint system for the call-string approach is parameterized with a given stack space. This enables me to not only consider one call-string approach but multiple concrete instances that differ only in the stack space parameter. I also provide *stack abstractions* as a tool to relate two stack spaces. With the help of stack abstractions, I state a sufficient criterion for when the call-string approach with respect to a given stack space leads to a *MOVP*-correct solution. This criterion requires that there has to be a stack abstraction from the stack space S<sup>∞</sup> of unbounded stacks to the given stack space. Examples for the satisfiability of this criterion, and hence for stack spaces that lead to *MOVP*-correct solutions, are the *k*-bounded stack spaces S*<sup>k</sup>* . There are two open questions in this context:

1. Is the criterion necessary for obtaining a *MOVP*-correct solution?

2. Are there, apart from the S*<sup>k</sup>* , other stack spaces that lead to *MOVP*correct solutions?

If these two questions can be answered, we could either be assured that there are no other stack spaces that lead to *MOVP*-correct solutions, or other appropriate stack spaces could be found that offer a better compromise between precision and performance.

Regarding the first question, stack abstractions as introduced in this work have the restriction that they do not change the alphabet. It may be sensible to explore whether and how this restriction can be lifted. If it is

<sup>42</sup>This idea can indeed be used to construct such a family (C*n*)*n*∈**<sup>N</sup>** of constraint systems from Constraint System 6.1 as follows: C<sup>0</sup> demands *X*(*s*,*t*) ≥ ⊤ for all *s*,*t* ∈ *N*, and each C*<sup>n</sup>* is a copy of Constraint System 6.1 where the sl-sol-(iii)-clause is modified such that the right-hand side relies on *l f p*(C*n*−1) as a helper solution for same-level information.

possible to obtain stack spaces with *MOVP*-correct solutions from S<sup>∞</sup> via such generalized stack abstractions, then it may also be possible to obtain *MOVP*-correct stack spaces for which there is no ordinary stack abstraction, so that the first question would have to be answered negatively.

I suspect that the answer to the second question is that there are indeed stack spaces other than S*<sup>k</sup>* that satisfy the criterion. It seems possible that there are stack abstractions that do not apply the same bound to all stacks but crop each stack depending on their content. More formally, given a function *d* : *E* ⋆ *call* <sup>→</sup> **<sup>N</sup>**, one can define

$$\begin{aligned} \alpha(\sigma) &= \sigma^{\leq d(\sigma)}, \\ \gamma(\sigma) &= \sigma, \end{aligned}$$

which should be a stack abstraction. Note that *d* has to be bounded so that the resulting constraint system is finite.

# **10.2.3 Relation Between the Results of Data-Flow Analysis on PDGs and Program Semantics**

An important special case of my general data-flow framework is data-flow analysis on PDGs. All the examples that are described in this thesis are inherently graph-theoretic, i.e. they do not allow for conclusions about semantic properties of the program represented by the PDG. From my point of view, this gap needs to be examined further. Some interesting questions in this area include:


# **10.2.4 Generalization of Chopping**

Chopping [96] was introduced as a generalization of slicing to enable the extraction of more focused program parts. A chop has two nodes *s* and *t* as parameter and is defined as the set of nodes that lie on valid paths between *s* and *t*. Like for slicing, approaches for the computation of context-sensitive chops have been proposed [138].

I think that it is possible to generalize chopping in a way that is similar to how slicing was generalized in this thesis. The objective function of such a generalization would take not two arguments, like *MOVP*, but three arguments *s*, *t* and *n*. Its value *MOVPch*(*s*,*t*, *n*) could be defined as the merge-over-all-valid paths from *s* to *t* that also contain *n*. Analogously to *MOVP*, one could try to approximate *MOVPch* by means of monotone constraint systems. I suspect that this can be done both with a functional and a call-string approach. A benefit of such an analysis would be significantly more detailed results, and hence more information than for the slicing variant. For example, for the dist instance, one could yield statements like "all paths from *s* to *t* that pass *n* have a length of at least *k*" – that is, with the additional argument, one would get a whole spectrum of results instead of just one.

# **10.2.5 Extensions to Concurrent Programs**

As already discussed in chapter 9, the framework that I develop in this thesis is restricted to sequential constructs. Hence, future work should explore how this restriction can be lifted. Possible ideas for PDGs, which I considered more closely in subsection 9.2.3, include (a) formalizing and generalizing slicers for concurrent PDGs, like for example the iterated two-phase slicer, and (b) examining and generalizing the constraint system that are solved by IFC checkers on multi-threaded PDGs such as the RLSOD approach [31].

# **10.2.6 Exploration of Other Generalizations of Context-Sensitive Slicing**

As already pointed out in subsection 9.4.1, other formalisms than dataflow analysis could be considered as possible generalizations of contextsensitive slicing. For example, Push-Down Systems enable slicers that allow for significantly more flexible queries. Such an approach could also be generalized to an analysis that not only computes a slice, but also computes data-flow analysis results (or weights, respectively).

# **Bibliography**


www.reliably- secure- software- systems.de/About (visited on 09/22/2021).


# **A Proofs**

# **A.1 Proof of Theorem 5.9**

**Lemma A.1.** *Let* π ∈ *Bal*(*E*) *be a balanced symbol sequence. Assume that* (*i*, *j*) ∈ ν<sup>π</sup> *and that either of the following holds:*


*Then both* π <*i and* π >*j are balanced.*

*Proof.* We first consider case (i). Since π is balanced, we have *c*(π <*i* ′ ) ≥ 0 for any *i* ′ ≤ *i*. Furthermore, *c*(π <*i* ) must be 0 because π <*i* contains no call symbols. This together proves that π <*i* is balanced. Now we show that π >*j* is balanced. It suffices to show that (1) *c*(π >*j* ) = 0 and (2) *c*(π ]*j*,*k*[ ) ≥ 0 for every *k* ∈]*j*, *n* − 1], where *n de f* = |π|.

We have *c*(π) = 0 since π is balanced. Moreover, we already have argued that *c*(π <*i* ) = 0. Furthermore, because (*i*, *j*) ∈ νπ, π ]*i*,*j*[ is balanced, so we have *c*(π ]*i*,*j*[ ) = 0. Together we can conclude:

$$\begin{aligned} 0 &= c(\pi) = c(\pi^{\le j}) + c(\pi^{>j}) \\ &= c(\pi^{j}) \\ &= 0 + 1 + 0 + (-1) + c(\pi^{>j}) \\ &= 0 + c(\pi^{>j}) \\ &= c(\pi^{>j}) \end{aligned}$$

It remains to show that *c*(π ]*j*,*k*[ ) ≥ 0 for every *k* ∈]*j*, *n* − 1]. For this, note that

$$c(\pi^{$$

and *c*(π *j* ) = −1. Now pick any *k* ∈]*j*, *n* − 1]. Then we have

$$\begin{aligned} 0 &\le c(\pi^{$$

Now consider case (ii). We have *c*(θ) ≥ 0 for every prefix θ of π <*i* since π is balanced and *c*(θ ′ ) ≥ 0 for every prefix θ ′ of π >*j* , since π <sup>&</sup>gt;*<sup>j</sup>* does not contain any return symbols. Furthermore, due to the balancedness of π ]*i*,*j*[ we can derive

$$\begin{aligned} 0 = c(\pi) &= c(\pi^{j}) \\ &= c(\pi^{j}) \\ &= c(\pi^{j}) \end{aligned}$$

Since both *c*(π <*i* ) and *c*(π >*j* ) are not negative, they must both be 0. In summary, we have shown that both *c*(π <*i* ) and *c*(π >*j* ) must be balanced. □

**Theorem 5.9.** *For any symbol sequence* π ∈ *E* <sup>⋆</sup>*, the following conditions are equivalent:*


*Proof.* (a) =⇒ (b)

We show that the claim

∀π ∈ *E* <sup>⋆</sup>. <sup>π</sup> balanced <sup>=</sup><sup>⇒</sup> <sup>ν</sup><sup>π</sup> bijective by strong induction on the number *K* ∈ **N** of call symbols in π.

**induction hypothesis:** The claim is proven for all π ′ which contain *L* < *K* call symbols.

Let π ∈ *E* <sup>⋆</sup> be a symbol sequence with *K* call symbols and assume that π is balanced. Then, by Lemma 5.4, π also contains *K* return symbols. Let *n* = |π| be the length of π.

If *K* = 0, i.e. if π does not contain any call or return symbols, then ν<sup>π</sup> is the empty relation and therefore trivially fulfills the conditions of a bijective function between empty sets.

Now assume that π contains *K* > 0 call and *K* return symbols. We show left- and right-totality separately.

**left-totality** Let *i* ∈ *range*(π) be the least index such that π*<sup>i</sup>* = *ecall* ∈ *Ecall*. Hence, we can write

$$\text{(A.1)}\qquad\qquad\qquad\qquad\qquad\pi=\pi^{<\text{i}\,^{\text{c}}}\cdot e\_{\text{call}}\cdot\pi^{>\text{i}}$$

First, we show that there must be a *k* ∈]*i*, *n*[ such that *c*(π ]*i*,*k*] ) < 0. This can be seen as follows. First, due to the choice of *i*, π <sup>&</sup>lt;*<sup>i</sup>* does not contain any call symbols. Moreover, because π is balanced, we have *c*(π <*i* ) ≥ 0. But this means that π <*i* cannot contain return symbols either. Hence, *c*(π <*i* ) = 0 and from this, we derive that *c*(π >*i* ) < 0 by the following computation:

$$\begin{aligned} 0 &= c(\pi) & \{\pi \text{ balanced }\} \\ &= c(\pi^{i}) & \{\text{(A.1), additivity of } c\} \\ &= c(\pi\_i) + c(\pi^{>i}) & \{c(\pi^{ c(\pi^{>i}). & \{\pi^i \in E\_{call}\} \end{aligned}$$

But by definition of *c*, *c*(π >*i* ) < 0 implies that there must be *k* ∈]*i*, *n*[ such that *c*(π ]*i*,*k*] ) < 0.

Now let *j* be the smallest *k* ∈]*i*, *n*[ such that *c*(π ]*i*,*k*] ) < 0. Obviously, we have *i* < *j*. Moreover:

• π*<sup>j</sup>* ∈ *Eret*: If this were not the case, it would follow that

$$c(\pi^{[i,j-1]}) < 0,$$

a contradiction to the choice of *j*.


From the former two statements we conclude that π ]*i*,*j*[ is balanced. Now we have

$$
\pi = \pi^{j}
$$

and have shown that π ]*i*,*j*[ is balanced. Together with *i* < *j* and π*<sup>j</sup>* = *eret* ∈ *Eret* this entails (*i*, *j*) ∈ νπ.

Now, because π is balanced (by assumption) and due to the choice of *i* we can apply Lemma A.1 and additionally conclude that π >*j* is balanced. Since both π ]*i*,*j*[ and π >*j* contain at most *K* − 1 call symbols, we can apply the induction hypothesis to them and gain that ν π ]*i*,*j*[ and ν π <sup>&</sup>gt;*<sup>j</sup>* are bijective, so in particular left-total.

Let *i* ′ ≠ *i* be another call position of π. Due to the choice of *i*, it must be *i* ′ > *i* and since π*<sup>j</sup>* ∈ *Eret*, we conclude that either *i* ′ < *j* or *i* ′ > *j*. If *i* ′ < *j* then by using that π *i* ′ = (π ]*i*,*j*[ ) *i* ′−(*i*+1) and the left-totality of ν π ]*i*,*j*[ , we obtain a *j* ′ such that (*i* ′ − (*i* + 1), *j* ′ ) ∈ ν π ]*i*,*j*[ which means by Lemma 5.17 that (*i*, *j* ′ + (*i* + 1)) ∈ νπ. Similarly, we find a *j* ′ with (*i* ′ , *j* ′ ) ∈ ν<sup>π</sup> in the case that *i* ′ > *j*. This concludes the proof of the left-totality of νπ.

**right-totality** Let *j* ∈ *range*(π) be the greatest index such that π *<sup>j</sup>* <sup>∈</sup> *<sup>E</sup>ret*. Then we have

$$0 \ge c(\pi^{\le j}) = c(\pi^{$$

which means that *c*(π ≤*j* ) > 0. Hence, by the properties of *c*, there must be a *k* ∈ [0, *j*[ such that *c*(π [*k*,*j*[ ) > 0. We choose *i* to be the greatest such *k*. Then we have *i* < *j*. Moreover, due to the maximality of *i*, we have *c*(π ]*i*,*j*[ ) ≤ 0. Lastly, we have

$$0 < c(\pi^{[i,j]}) = c(\pi^i) + c(\pi^{[i,j]}).$$

Because *c*(π ]*i*,*j*[ ) ≤ 0, this necessarily entails *c*(π *i* ) > 0, i.e. *c*(π *i* ) = 1, and *c*(π ]*i*,*j*[ ) = 0.

Now let *k* ∈]*i*, *j*[. Then we have

$$0 = c(\pi^{]i,j[}) = c(\pi^{]i,k[}) + c(\pi^{]k,j[})\_{\prime\prime}$$

but since *k* > *i* and because of the maximality property of *i*, it must be *c*(π ]*k*,*j*[ ) ≤ 0, which means that *c*(π ]*i*,*k*] ) ≥ 0.

Together this shows (*i*, *j*) ∈ νπ. By Lemma A.1 and the choice of *j*, π <*i* is balanced. For the other return positions *j* ′ < *j* we proceed similarly to the left-totality part: We apply the induction hypothesis (noting that π ]*i*,*j*[ and π <*i* contain less call symbols) and Lemma 5.17 to obtain *i* ′ such that (*i* ′ , *j* ′ ) ∈ νπ.

(b) =⇒ (a) Let π ∈ *E* <sup>⋆</sup> be a symbol sequence such that

$$\nu\_{\pi} : \mathsf{Call} pos(\pi) \to \mathsf{Retpos}(\pi)$$

is a bijective function. We must show that π is balanced. By Lemma 5.6 it suffices to show that

$$\text{(1)}\tag{1}$$

$$\forall i \in range(\pi). c(\pi^{\le i}) \ge 0$$

Claim (1) is clear because of the bijectivity of νπ and the first statement in Lemma 5.4.

It remains to show (2). We proceed by strong induction on *i*. Let *i* ∈ *range*(π). The induction hypothesis is

∀*i* ′ < *i*.*c*(π ≤*i* ′ ) ≥ 0(IH)

We have to show *c*(π ≤*i* ) ≥ 0. For this, we make a case distinction on whether *i* = 0 or not.


Now consider the case that π*<sup>i</sup>* ∈ *Eret*. Since ν<sup>π</sup> is bijective, there is *j* < *i* such that π*<sup>j</sup>* ∈ *Ecall* and π ]*j*,*i*[ is balanced. We conclude

$$c(\pi^{ 0.$$

This proves that *c*(π ≤*i* ) ≥ 0.

**A.2 Proof of Theorem 5.10**

**Theorem 5.10.** *Given* π ∈ *E* <sup>⋆</sup>*, assume that* (*i*, *j*),(*i* ′ , *j* ′ ) ∈ νπ*. Then one of the following statements is true:*


*Proof.* For (*i*, *j*) = (*i* ′ , *j* ′ ), the theorem is trivially true. If (*i*, *j*) ≠ (*i* ′ , *j* ′ ), then left- and right-uniqueness of ν<sup>π</sup> (Theorem 5.8) gives us *i* ≠ *i* ′ <sup>∧</sup> *<sup>j</sup>* <sup>≠</sup> *<sup>j</sup>* ′ . Also note that *i* ≠ *j* ′ and *j* ≠ *i* ′ , because π *i* , π *i* ′ ∈ *Ecall* and π *j* , π *j* ′ ∈ *Eret*. Since (*i*, *j*) ∈ νπ, π can be split up into

$$
\pi = \pi^{j},
$$

where π*<sup>i</sup>* ∈ *Ecall*, π*<sup>j</sup>* ∈ *Eret* and π ]*i*,*j*[ is balanced. Now, we make a case distinction of where *i* ′ lies relative to *i* and *j* and show for every case that one of the three conditions from the claim must be true.

*i* ′ < *i* : Then *j* ′ cannot be in ]*i*, *j*[. Assume, for the purpose of contradiction, that it were. Since π ]*i*,*j*[ is balanced, we may apply Theorem 5.9 and find *i* ′′ such that (*i* ′′ , *j* ′ − (*i* + 1)) ∈ ν π ]*i*,*j*[ . By shifting, we see that (*i* ′′ + *i* + 1, *j* ′ ) ∈ νπ. Since ν<sup>π</sup> is left-unique, this means that *i* ′ = *i* ′′ + *i* + 1. But, since *i* + 1 > 0, it must be *i* ′ > *i*, which contradicts the case we consider currently. So, the assumption that *j* ′ is in ]*i*, *j*[ is false. It follows that either *j* ′ < *i* or *j* ′ > *j*. In the former case, since *j* ′ > *i* ′ , we have [*i*, *j*] ∩ [*i* ′ , *j* ′ ] = ∅ and in the latter case we have [*i*, *j*] ⊆ [*i* ′ , *j* ′ ].

□

*i* ′ > *i* ∧ *i* ′ < *j* : First, we note that *j* ′ > *i*. This follows from (*i* ′ , *j* ′ ) ∈ ν<sup>π</sup> and *i* ′ > *i*. Moreover, we observe that *j* ′ cannot be greater than *j*. This can be shown analogously to the previous case, by shifting around, by using that νπ is right-unique and by exploiting the balancedness of π ]*i*,*j*[ . In summary, we have shown [*i* ′ , *j* ′ ] ⊆ [*i*, *j*].

*i* ′ > *j* : Since (*i* ′ , *j* ′ ) ∈ ν<sup>π</sup> implies *i* ′ < *j* ′ , it must also be *j* ′ > *j*. So we have [*i*, *j*] ∩ [*i* ′ , *j* ′ ] = ∅.

□

□

# **A.3 Proof of Theorem 5.19**

**Lemma A.2.** *We have Eintra* ⊆ *Bal*(*E*)*, Ecall* ⊆ *Right*(*E*) *and Eret* ⊆ *Le f t*(*E*)*.*

	- For *e* ∈ *Ecall*, ν*<sup>e</sup>* is right-total, since *Retpos*(*e*) = ∅.
	- For *e* ∈ *Eret*, ν*<sup>e</sup>* is left-total, since *Callpos*(*e*) = ∅.

**Lemma A.3.** *Le f t*(*E*)*, Right*(*E*) *and Bal*(*E*) *are all closed under concatenation.*


*Proof.* 1. Let *i* ∈ *range*(π · π ′ ). Then either *i* ∈ *range*(π) or *i* = |π| + *i* ′ with *i* ′ ∈ *range*(π ′ ). We consider each case separately:

a) If *i* ∈ *range*(π), then because π ∈ *Le f t*(*E*), there is *j* ∈ *range*(π) ⊆ *range*(π · π ′ ) with (*i*, *j*) ∈ νπ.

b) If *i* = |π| + *i* ′ with *i* ′ ∈ *range*(π ′ ), then because π ′ ∈ *Le f t*(*E*), we find *j* ′ ∈ *range*(π ′ ) with (*i* ′ , *j* ′ ) ∈ νπ′. Then (*i* ′ + |π|, *j* ′ + |π|) = (*i*, *j* ′ + |π|) ∈ νπ·π′ by Lemma 5.17.

2. This follows by an analogous argument as the first statement.

3. This is a consequence of Theorem 5.9 and the first two statements.

**Lemma A.4.** *If* π ∈ *Bal*(*E*)*, ecall* ∈ *Ecall and eret* ∈ *Eret, then ecall* · π · *eret* ∈ *Bal*(*E*)*.*

*Proof.* Let π ∈ *Bal*(*E*), *ecall* ∈ *Ecall* and *eret* ∈ *Eret* and define π ′ *de f* = *ecall* · π · *eret*. We show that

$$\text{(A)}\qquad\qquad\qquad\qquad\qquad\qquad\text{c}(\pi')=0$$

$$\text{(B)}\tag{1}$$

$$\text{(B)}\qquad\qquad\qquad\qquad\forall\theta\in\text{Prefix}(\pi').\ c(\theta)\geq0.$$

This implies that π ′ ∈ *Bal*(*E*) by Lemma 5.6.

(A) Since π ∈ *Bal*(*E*) we have *c*(π) = 0. This implies

$$c(\pi') = c(e\_{call}) + c(\pi) + c(e\_{rt}) = 1 + c(\pi) + (-1) = 1 + 0 + (-1) = 0.1$$

(B) Let θ ∈ *Pre f ix*(π ′ ). The case θ = π ′ is already covered by (A). Also, *c*(ϵ) = 0 holds by definition. It remains the case that θ = *ecall* · θ ′ for some prefix θ ′ of π. But π ∈ *Bal*(*E*), which implies *c*(θ ′ ) ≥ 0 by Lemma 5.6. It follows

$$\mathcal{c}(\theta) = \mathcal{c}(e\_{\text{call}} \cdot \theta') = \mathcal{c}(e\_{\text{call}}) + \mathcal{c}(\theta') = 1 + \mathcal{c}(\theta') > 0.1$$

□

□

**Theorem 5.19.** *Bal*(*E*) *is the least subset X of E* <sup>⋆</sup> *with the following properties:*

$$(Bal1)\ \frac{\pi \in X}{\varepsilon \in X} \quad (Bal2)\ \frac{\pi \in X \qquad e \in E\_{intra}}{\pi \cdot e \in X}$$

$$(Bal3)\ \frac{\pi \in X \qquad \pi' \in X \qquad e\_{call} \in E\_{call} \qquad e\_{ret} \in E\_{ret}}{\pi \cdot e\_{call} \cdot \pi' \cdot e\_{ret} \in X}$$

*Proof.* First, we show that *Bal*(*E*) satisfies the properties *Bal*1, *Bal*2, *Bal*3. It is clear that *Bal*(*E*) satisfies *Bal*1. Furthermore, Lemma A.2, Lemma A.3 and Lemma A.4 imply that it also has the properties *Bal*2 and *Bal*3.

It remains to show that *Bal*(*E*) is the least subset of *E* <sup>⋆</sup> with properties *Bal*1, *Bal*2, *Bal*3. For this, let *X* ⊆ *E* <sup>⋆</sup> be a set of symbol sequences that satisfies the closure properties *Bal*1, *Bal*2 and *Bal*3. We show *Bal*(*E*) ⊆ *X* by strong induction on the lengths of symbol sequences. The induction hypothesis for *n* ∈ **N** is

$$(\text{A.2})\qquad\qquad\forall\pi\in\text{Bal}(E).|\pi|$$

Now let π ∈ *Bal*(*E*), |π| = *n*. We show π ∈ *X* by a case distinction on whether *Retpos*(π) = ∅ or not.

1. If *Retpos*(π) = ∅, then we also have *Callpos*(π) = ∅, because π is balanced. Hence, π is either empty or solely consists of intraprocedural symbols. Thus, π ∈ *X* can be derived by repeated application of *Bal*1 and *Bal*2.

2. If *Retpos*(π) <sup>≠</sup> <sup>∅</sup>, then let *<sup>j</sup>* be the maximum of *Retpos*(π). Since <sup>π</sup> is balanced, there must be *i* ∈ *range*(π) with (*i*, *j*) ∈ νπ. With *ecall de f* = π *<sup>i</sup>* and *eret de f* = π *j* , π can be decomposed into

$$
\pi^{j}
$$

From (*i*, *j*) ∈ ν<sup>π</sup> it follows that π ]*i*,*j*[ is balanced. From this, the balancedness of π and the choice of *j*, we can conclude by application of Lemma A.1 that both π <sup>&</sup>lt;*<sup>i</sup>* and π <sup>&</sup>gt;*<sup>j</sup>* are balanced.

Since both π <sup>&</sup>lt;*<sup>i</sup>* and π ]*i*,*j*[ are balanced and shorter than π, we obtain π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *<sup>X</sup>* and π ]*i*,*j*[ <sup>∈</sup> *<sup>X</sup>* by induction hypothesis. With *Bal*3 we get

$$\text{(A.3)}\qquad\qquad\qquad\pi^{$$

Due to the choice of *j*, we have *Retpos*(π >*j* ) = ∅. Since π >*j* is balanced, this implies that *Callpos*(π >*j* ) = ∅, too. Hence, from (A.3) we get

$$
\pi^{j} \in X
$$

by repeated application of *Bal*2.

□

# **A.4 Proof of Theorem 5.20**

**Definition A.5.** *For a given symbol sequence* π ∈ *E* <sup>⋆</sup>*, we define the set MRet*(π) *of* matched return positions *as follows:*

$$\mathit{MRet}(\pi) \stackrel{def}{=} \{ j \in \mathit{Retpos}(\pi) \mid \exists i \in \mathit{range}(\pi). \ (i, j) \in \nu\_{\pi} \}$$

**Lemma A.6.** *Let* <sup>π</sup> <sup>∈</sup> *Le f t*(*E*) *such that MRet*(π) <sup>≠</sup> <sup>∅</sup>*. Assume that <sup>j</sup> is the greatest element of MRet and let i* = ν −1 <sup>π</sup> (*j*)*. Then the following statements hold:*

*1. Both* π <*i and* π >*j are left-total.*

*2.* π >*j consists only of symbols from Eintra* ∪ *Eret*

*Proof.* We show the two statements separately.

1. Let *i* ′ ∈ *Callpos*(π <*i* ). Because *Callpos*(π <*i* ) ⊆ *Callpos*(π) and π ∈ *Le f t*(*E*), we find *j* ′ ∈ *range*(π) with (*i* ′ , *j* ′ ) ∈ νπ. This *j* ′ is a member of *MRet*(π): *j* ′ ∈ *MRet*(π). Due to the choice of *j*, we have *j* ′ < *j*. With Theorem 5.10 and *i* ′ < *i* we get *j* ′ < *i*. Therefore, (*i* ′ , *j* ′ ) ∈ *v*π<*<sup>i</sup>* . This shows π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Le f t*(*E*).

2. This is an easy consequence of the maximality of *j*.

□

**Theorem 5.20.** *Le f t*(*E*) *is the least subset X of E* <sup>⋆</sup> *which has the following properties:*

$$(Left1)\ \frac{\pi \in X}{\pi \in X} \quad (Left2)\ \frac{\pi \in X \qquad e \in E\_{intra} \cup E\_{ret}}{\pi \cdot e \in X}$$

$$(Left3)\ \frac{\pi \in X \qquad \pi' \in Bal(E) \qquad e\_{call} \in E\_{call} \qquad e\_{ret} \in E\_{ret}}{\pi \cdot e\_{call} \cdot \pi' \cdot e\_{ret} \in X}$$

*Proof.* First, we observe that *Le f t*(*E*) has the closure properties *Le f t*1, *Le f t*2, *Le f t*3. This is clear for *Le f t*1, the other two properties follow from Lemma A.2, Lemma A.3, Lemma A.4 and Remark 5.12. Next, let *X* ⊆ *E* <sup>⋆</sup> be a set with the closure properties *Le f t*1, *Le f t*2 and *Le f t*3. Now we show *Le f t*(*E*) ⊆ *X* by strong induction on the length of sequences from *E* ⋆.

For *n* ∈ **N**, the induction hypothesis is

$$\forall \text{(A.4)} \qquad \forall \pi \in E^{\star}. |\pi| < n \land \pi \in Left(E) \implies \pi \in X$$

Now let π ∈ *E* <sup>⋆</sup> be a symbol sequence with <sup>|</sup>π<sup>|</sup> = *<sup>n</sup>* and <sup>π</sup> <sup>∈</sup> *Le f t*(*E*). We show π ∈ *X* by case distinction on whether *MRet*(π) = ∅ or not.

1. If *MRet*(π) = ∅, then *Callpos*(π) must be empty, too: Assume, for the purpose of contradiction, that *Callpos*(π) contains some call position *i*. Then *i* cannot be matched, because *MRet*(π) = ∅. But this is a contradiction to π ∈ *Le f t*(*E*). Hence, the assumption is false and *Callpos*(π) = ∅.

Because *Callpos*(π) = ∅, π solely consists of symbols from *Eintra* ∪ *Eret*. Hence, π ∈ *X* can be derived by repeated application of *Le f t*1 and *Le f t*2.

2. If  $M \text{Re} t(\pi) \neq \emptyset$ , then let  $j$  be the greatest element of  $M \text{Re} t(\pi)$  and  $j = \nu\_{\pi}^{-1}(j)$ . With  $e\_{\text{call}} \stackrel{def}{=} \pi^{i} \text{ and } e\_{\text{ret}} \stackrel{def}{=} \pi^{j}$ ,  $\pi$  can be written as

$$
\pi = \pi^{j}
$$

By definition, π ]*i*,*j*[ is balanced. Plus, sinceπ ∈ *Le f t*(*E*) and due to the choice of *j*, by application of Lemma A.6 we obtain π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Le f t*(*E*), <sup>π</sup> <sup>&</sup>gt;*<sup>j</sup>* <sup>∈</sup> *Le f t*(*E*) and that π >*j* consists only of symbols from *Eintra* ∪ *Eret*.

Equipped with these observations, we can finish the proof as follows:

a) Because |π <*i* | < |π| and π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Le f t*(*E*), we may apply (A.4) to <sup>π</sup> <sup>&</sup>lt;*<sup>i</sup>* and obtain that π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *<sup>X</sup>*.

b) Now we apply *Le f t*3 to π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *<sup>X</sup>*, <sup>π</sup> ]*i*,*j*[ <sup>∈</sup> *Bal*(*E*), *<sup>e</sup>call* <sup>∈</sup> *<sup>E</sup>call* and *<sup>e</sup>ret* <sup>∈</sup> *<sup>E</sup>ret* and get π <*i* ·*ecall* · π ]*i*,*j*[ ·*eret* ∈ *X*.

c) Finally, because π <sup>&</sup>gt;*<sup>j</sup>* <sup>∈</sup> (*Eintra* <sup>∪</sup> *<sup>E</sup>ret*) <sup>⋆</sup>, we obtain

$$
\pi^{j} \in X
$$

by repeated application of *Le f t*2 to π <*i* ·*ecall* · π ]*i*,*j*[ ·*eret* ∈ *X*.

□

# **A.5 Proof of Theorem 5.21**

**Lemma A.7.** *1. If* π ∈ *Le f t*(*E*) *and* π ≥*k is a su*ffi*x of* π*, then* π <sup>≥</sup>*<sup>k</sup>* <sup>∈</sup> *Le f t*(*E*)*.*

*2. If* π ∈ *Right*(*E*) *and* π ≤*j is a prefix of* π*, then* π <sup>≤</sup>*<sup>j</sup>* <sup>∈</sup> *Right*(*E*)*.*

*Proof.* 1. Consider an arbitrary π ∈ *Le f t*(*E*) and let π <sup>≥</sup>*<sup>k</sup>* be a suffix of π. We need to show that νπ≥*<sup>k</sup>* is left-total. Let *i* ∈ *Callpos*(π ≥*k* ). Then we show that there is *j* ∈ *Retpos*(π ≥*k* ) such that (*i*, *j*) ∈ νπ≥*<sup>k</sup>* .

First, we observe (π ≥*k* ) *<sup>i</sup>* = π *k*+*i* . Hence, we have *i* + *k* ∈ *Callpos*(π). Because ν<sup>π</sup> is left-total, there is *j* ′ ∈ *Retpos*(π) such that (*i* + *k*, *j* ′ ) ∈ νπ. Moreover, because *j* ′ > *k* + *i* ≥ *k*, we can write *j* ′ as *j* ′ = (*j* ′ − *k*) + *k*. In particular, we have *j* ′ − *k* ∈ *range*(π ≥*k* ). With Lemma 5.17, it follows from (*i* + *k*,(*j* ′ − *k*) + *k*) ∈ ν<sup>π</sup> that (*i*, *j* ′ − *k*) ∈ νπ≥*<sup>k</sup>* , which concludes the proof.

2. Let π ∈ *Right*(*E*) and π <sup>≤</sup>*<sup>j</sup>* be a prefix of π. Let

$$l 
∈ 
Rectpos
{(
π^{
≤ j}
)} 
⊆ 
kerpos(
π)$$

be a return position in π ≤*j* . Due to right-totality of νπ, we find a *k* ∈ *Callpos*(π) such that (*k*, *l*) ∈ νπ. This means in particular that *k* < *l*. With *l* ≤ *j* we get *k* ≤ *j* so that we can conclude (*k*, *l*) ∈ ν π ≤*j* . This proves that ν π <sup>≤</sup>*<sup>j</sup>* is right-total.

□

**Lemma A.8.** *Let* <sup>π</sup> <sup>∈</sup> *Right*(*E*) *such that Retpos*(π) <sup>≠</sup> <sup>∅</sup>*. Assume that <sup>j</sup> is the greatest element of Retpos*(π) *and let i* = ν −1 <sup>π</sup> (*j*)*. Then both* π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Right*(*E*) *and* π <sup>&</sup>gt;*<sup>j</sup>* <sup>∈</sup> *Right*(*E*)*.*

*Proof.* Since *j* is the greatest return position, we have *Retpos*(π >*j* ) = ∅. Hence, π >*j* can only consist of symbols from *Eintra* ∪ *Ecall*, so that π <sup>&</sup>gt;*<sup>j</sup>* <sup>∈</sup> *Right*(*E*) holds trivially. Furthermore, π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Right*(*E*) holds because *Right*(*E*) is closed under prefixes (by Lemma A.7). □ **Theorem 5.21.** *Right*(*E*) *is the least subset X of E* <sup>⋆</sup> *which has the following properties:*

$$(\operatorname{Right1})\begin{array}{cc} \frac{\pi \in X}{\epsilon \in X} & \text{(Right2)} \end{array} \frac{\pi \in X \qquad e \in \operatorname{E}\_{\operatorname{intra}} \cup \operatorname{E}\_{\operatorname{call}}}{\pi \cdot e \in X}$$

$$(\operatorname{Right3})\begin{array}{cc} \pi \in X \qquad \pi' \in \operatorname{Bal}(E) & e\_{\operatorname{call}} \in \operatorname{E}\_{\operatorname{call}} \qquad e\_{\operatorname{ret}} \in \operatorname{E}\_{\operatorname{ret}} \\ \pi \cdot e\_{\operatorname{call}} \cdot \pi' \cdot e\_{\operatorname{ret}} \in X \end{array}$$

*Proof.* First, we observe that *Right*(*E*) has the closure properties *Right*1, *Right*2, *Right*3. This is clear for *Right*1, the other two properties follow from Lemma A.2, Lemma A.3, Lemma A.4 and Remark 5.12.

Next, let *X* ⊆ *E* <sup>⋆</sup> be a set with the closure properties *Right*1, *Right*2 and *Right*3. We show *Right*(*E*) ⊆ *X* by strong induction on the length of sequences from *E* ⋆.

For *n* ∈ **N**, the induction hypothesis is

$$\text{(A.5)}\qquad\forall\pi\in\boldsymbol{E}^{\star}.|\pi|<\boldsymbol{n}\land\pi\in\text{Right}(E)\implies\pi\in X.$$

Now let π ∈ *E* <sup>⋆</sup> be a symbol sequence with <sup>|</sup>π<sup>|</sup> = *<sup>n</sup>* and <sup>π</sup> <sup>∈</sup> *Right*(*E*). We show π ∈ *X* by case distinction on whether *Retpos*(π) = ∅ or not.

1. If *Retpos*(π) = ∅, then π does not contain any return symbols. Hence, π is either empty or solely consists of symbols from *Eintra* ∪ *Ecall*. In both cases, π ∈ *X* can be derived by repeated application of *Right*1 and *Right*2.

2. If *Retpos*(π) <sup>≠</sup> <sup>∅</sup>, then let *<sup>j</sup>* be the greatest element of *Retpos*(π) and *i* = ν −1 <sup>π</sup> (*j*). This is well-defined because π ∈ *Right*(*E*). With *ecall de f* = π *<sup>i</sup>* and *eret de f* = π *j* , π can be written as

$$
\pi = \pi^{j}
$$

By definition, π ]*i*,*j*[ is balanced. Plus, since π ∈ *Right*(*E*) and due to the choice of *j*, application of Lemma A.8 yields that π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Right*(*E*).

Now we can finish the proof as follows:

a) Because |π <*i* | < |π| and π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *Right*(*E*), we may apply (A.5) to <sup>π</sup> <sup>&</sup>lt;*<sup>i</sup>* and obtain that π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *<sup>X</sup>*.

b) Now we apply *Right*3 to π <sup>&</sup>lt;*<sup>i</sup>* <sup>∈</sup> *<sup>X</sup>*, <sup>π</sup> ]*i*,*j*[ <sup>∈</sup> *Bal*(*E*), *<sup>e</sup>call* <sup>∈</sup> *<sup>E</sup>call* and *eret* ∈ *Eret* and get π <*i* ·*ecall* · π ]*i*,*j*[ ·*eret* ∈ *X*.

c) Finally, we note that, due to the choice of *j*, π <sup>&</sup>gt;*<sup>j</sup>* only contains symbols from *Eintra* ∪ *Ecall*. Hence, we obtain

$$
\pi^{j} \in X
$$

by repeated application of *Right*2 to π <*i* ·*ecall* · π ]*i*,*j*[ ·*eret* ∈ *X*.

□

# **A.6 Proof of Lemma 5.24**

**Lemma A.9.** *For* π ∈ *E* <sup>⋆</sup> *and <sup>e</sup>* <sup>∈</sup> *E, we have* <sup>ν</sup>π·*<sup>e</sup>* <sup>=</sup> <sup>ν</sup><sup>π</sup> *if one of the following conditions is satisfied:*

$$\text{(i)}\tag{i}$$

$$e \in E\_{\text{intra}} \cup E\_{\text{call}}$$

$$\text{(ii)}\tag{1\text{i}} \qquad \qquad e \in E\_{\text{ret}} \land \pi \in \text{Left}(E)$$

*Proof.* By definition, it is clear that ν<sup>π</sup> ⊆ νπ·*e*. For the other inclusion we assume either condition and show that νπ·*<sup>e</sup>* ⊆ ν<sup>π</sup> holds in both cases.

(i) Assume that *e* ∈ *Eintra* ∪ *Ecall*. Let (*i*, *j*) ∈ νπ·*e*. Then *i* < *j* and (π · *e*) *j* ∈ *Eret*. Since *Eret* ∩ (*Eintra* ∪ *Ecall*) = ∅, (π · *e*) *<sup>j</sup>* <sup>∈</sup> *<sup>E</sup>ret* implies that *<sup>j</sup>* <sup>≠</sup> <sup>|</sup>π|. Hence *j* ∈ *range*(π). But with *i* < *j* this means that *i* ∈ *range*(π). Hence (*i*, *j*) ∈ νπ.

(ii) Assume *e* ∈ *Eret* and π ∈ *Le f t*(*E*). Let (*i*, *j*) ∈ νπ·*e*. From (*i*, *j*) ∈ νπ·*<sup>e</sup>* we get *i* < *j* so that *i* ∈ *range*(π). Moreover, π ∈ *Le f t*(*E*), hence there is *j* ′ ∈ *range*(π) with (*i*, *j* ′ ) ∈ ν<sup>π</sup> ⊆ νπ·*e*. From Theorem 5.8, we get *j* = *j* ′ and thus (*i*, *j*) ∈ νπ.

□

**Lemma 5.24.** *1. If* π ∈ *AscSeq*(*E*) *and e* ∈ *Eintra* ∪*Eret, then* π·*e* ∈ *AscSeq*(*E*)*.*

*2. If* π ∈ *DescSeq*(*E*) *and e* ∈ *Eintra* ∪ *Ecall, then* π ·*e* ∈ *DescSeq*(*E*)*.*

*3. If* π ∈ *SLSeq*(*E*) *and e* ∈ *Eintra, then* π ·*e* ∈ *SLSeq*(*E*)*.*

*Proof.* 1. Let π ∈ *AscSeq*(*E*) and *e* ∈ *Eintra* ∪ *Eret*. Then π ∈ *Le f t*(*E*) ∩ *Val*(*E*). It follows that π·*e* ∈ *Le f t*(*E*) by Theorem 5.20. Moreover, νπ·*<sup>e</sup>* = ν<sup>π</sup> by Lemma A.9. Thus, π ·*e* ∈ *Val*(*E*) follows from π ∈ *Val*(*E*).

2. Let π ∈ *DescSeq*(*E*) and *e* ∈ *Eintra* ∪ *Ecall*. Then π ∈ *Right*(*E*) ∩ *Val*(*E*). It follows that π · *e* ∈ *Right*(*E*) by Theorem 5.21. Moreover, νπ·*<sup>e</sup>* = ν<sup>π</sup> by Lemma A.9. Thus, validness of π ·*e* follows from validness of π ∈ *Val*(*E*).

3. This follows from a combination of the first two statements.

□

# **A.7 Proof of Lemma 5.25**

**Lemma 5.25.** *Let*π ∈ *Val*(*E*) *and*π ′ ∈ *SLSeq*(*E*)*. Then the following statements hold:*


# *Proof.*

The first statement can be seen as follows: Let (*i*, *j*) ∈ νπ·π′. Then either *j* ∈ *range*(π) or there is *j* ′ ∈ *range*(π ′ ) with *j* = |π| + *j* ′ . We show that (*i*, *j*) ∈ Φ in either case.

1. Assume *j* ∈ *range*(π). Then *i* ∈ *range*(π) since *i* < *j*. Furthermore we have (*i*, *j*) ∈ ν<sup>π</sup> by definition of νπ. This entails (*i*, *j*) ∈ Φ because π is valid.

2. Assume that *j* = |π| + *j* ′ for *j* ′ ∈ *range*(π ′ ). Since π ′ is balanced and hence right-total, there is *i* ′ ∈ *range*(π ′ ) with (*i* ′ , *j* ′ ) ∈ νπ′. By Lemma 5.17, this means that (*i* ′ + |π|, *j*) ∈ νπ·π′ and because νπ·π′ is right-unique, it follows that *i* = *i* ′ + |π ′ |. Since π ′ is valid, we get

$$((
\pi \cdot \pi')^i, (\pi \cdot \pi')^j) = (\pi'^{i'}, \pi'^{j'}) \in \Phi.$$

The other three statements are implied by the first statement and Lemma A.3 after noticing that *SLSeq*(*E*) ⊆ *Bal*(*E*), *SLSeq*(*E*) ⊆ *Le f t*(*E*) and *SLSeq*(*E*) ⊆ *Right*(*E*), respectively. □

# **A.8 Proof of Lemma 5.26**

**Lemma 5.26.** *If* π ∈ *SLSeq*(*E*)*, ecall* ∈ *Ecall, eret* ∈ *Eret and* (*ecall*,*eret*) ∈ Φ*, then ecall* · π ·*eret* ∈ *SLSeq*(*E*)*.*

*Proof.* Define π ′ *de f* = *ecall* · π · *eret*. From π ∈ *Bal*(*E*), *ecall* ∈ *Ecall*, *eret* ∈ *Eret*, we conclude that π ′ ∈ *Bal*(*E*) by Lemma A.4. It remains to show that π ′ ∈ *Val*(*E*). Let (*i*, *j*) ∈ νπ′. Then we show by case distinction on whether *i* = 0 or not that (π *i* , π *j* ) ∈ Φ.

1. If *i* = 0, then we must have *j* = |π ′ | − 1: Since νπ′ is left-unique by Theorem 5.8, there can be only one *j* with (0, *j*) ∈ νπ′. Furthermore, by definition of νπ′, we have (0, |π ′ | − 1) ∈ νπ′:


Thus (π ′*i* , π ′*j* ) = (π ′0 , π ′|π ′ |−1 ) = (*ecall*,*eret*) ∈ Φ by assumption.

2. Assume *<sup>i</sup>* <sup>≠</sup> 0. Then *<sup>i</sup>* <sup>∈</sup> [1, <sup>|</sup><sup>π</sup> ′ | − 1[ because π ′*i* ∈ *Ecall* and π ′|π ′ |−1 ∈ *Eret* and *Ecall* ∩ *Eret* = ∅. Furthermore, *j* ∈]1, |π ′ | − 1[: *j* > 1 follows from 0 < *i* and *i* < *j*. Moreover, since νπ′ is right-unique and (0, |π ′ | − 1) ∈ νπ′ and *<sup>i</sup>* <sup>≠</sup> 0, we have *<sup>j</sup>* <sup>≠</sup> <sup>|</sup><sup>π</sup> ′ | − 1. Hence, by Lemma 5.17 and because π is valid, we get (π ′*i* , π ′*j* ) ∈ Φ.

□

# **B**

# **List of Figures**





# **C**

# **List of Tables**



# **D**

# **List of Listings**



# **E**

# **List of Algorithms**



# **Index**

(P, *E*)-correct, 233 (P, *E*)-domain-correct, 233 (P, *E*)-domain-precise, 233 (P, *E*)-precise, 233 *n <sup>e</sup>*<sup>→</sup> *<sup>n</sup>* ′ , 35

alphabet, 33 arity, 22 ascending chain, 16 ascending chain condition, 16

bound greates lower, 14 least upper, 13 lower, 14 upper, 13

call node, 197 chain, 16 closed under F , 30 complete lattice, 15 constant, 22 constraint, 22 left-hand side, 22 constraint system, 22 core, 277

core variables, 277 corresponding functional of a, 24 definition-complete, 283 least solution, 24 solution, 24 control dependency, 64 control-flow graph intraprocedural, 45 correspondence relation, 186, 196

data dependency, 63 data-flow analysis instance, 205 descending chain, 16 descending chain condition, 16 directed graph, 35 set of paths, 36 distributive, 208

edge, 35 call, 196 interprocedural, 196 intraprocedural, 196 return, 196 expression, 22 free variables, 22 extensive, 15

finite height, 17 fixed-point, 15 function symbol, 22

galois connection, 263

height of a (finite) chain, 17 of a partial order, 17

inductively defined by F , 30 interpretation, 23 interprocedural graph, 196

letter, 33

monotone constraint, 22 monotone constraint system, 22 monotone function, 15

node, 35 entry, 197 exit, 197

operator, 30

partial order, 13 chain-complete, 17 path S-acceptable, 250 ascending, 198 descending, 198 non-ascending, 297 same-level, 198 valid, 198 positive-distributive, 208 procedure graph, 197 procedure of a node, 197

range of a sequence, 34 of a sub-sequence, 34 reductive, 15 relation left-otal, 12 left-unique, 11 right-total, 12 right-unique, 12 return node, 197 right-hand side, 22 satisfaction of a constraint, 24 of a constraint system, 24 sequence set of prefixes, 34 solution S-solution (modified), 326 S-solution, 250 alternative valid-paths, 316 ascending, 236 descending, 237 merge-over-P, 207 non-ascending, 311 same-level, 235 stack-based Merge-Over-All-S-Acceptable-Paths, 254 valid-paths, 238 stack abstraction, 264 stack space, 246 *cs* function, 250 empty stack, 246 invalid stack, 249

pop function, 246 push function, 246 top function, 246 strict, 208 symbol, 33 symbol sequence *i*-th item, 34 ascending, 186 balanced, 182 concatenation, 34 content, 180 deficiency, 180 descending, 186 left-total, 185 length, 34 matching relation, 183 partially balanced, 185 right-total, 185 same-level, 186 sub-sequence, 34 valid, 186

universally distributive, 208

variable assignment, 23

I report on applications of slicing and program dependence graphs (PDGs) to software security. Moreover, I propose a framework that generalizes both data-flow analysis on control-flow graphs and slicing on program dependence graphs. Such a framework enables to systematically derive dataflow-like analyses on program dependence graphs that go beyond slicing.

The main theses of my work are:


Systematic Approaches to Advanced Information Flow Analysis