Study Notes: Static Analysis
Purpose
These study notes cover static analysis techniques for finding software defects without executing code. The material follows the lecture structure: motivation, theoretical foundations, technique overview, two deep dives (zero analysis and model checking), and modern industrial practice.
Primary Sources:
- Engler et al. [1] - Bugs as deviant behavior (Linux kernel)
- Flanagan et al. [2] - Extended static checking for Java
- Holzmann [3] - The SPIN model checker
- Chess & West [4] - Secure programming with static analysis
Industrial Evidence:
- Sadowski et al. [5] - Google Tricorder platform
- Zheng et al. [6] - Nortel Networks cost-effectiveness study
- Jaspan et al. [7] - eBay FindBugs deployment
- Lenarduzzi et al. [8] - Six-tool comparison study
Emerging Research:
- Cadar et al. [9] - KLEE symbolic execution
- Li et al. [10] - IRIS: LLM-assisted static analysis
- Wang et al. [11] - LLM program analysis survey
- Bailey & Nicholas [12] - Symbolic execution survey
Part 1: Why Static Analysis Matters
Learning Objectives
- Explain what static analysis is and why it catches bugs that testing misses
- Identify categories of defects where static analysis excels
- Articulate the value proposition: systematic, path-complete, early detection
1.1 What is Static Analysis?
Definition: Static analysis systematically examines an abstraction of a program’s state space without executing the program. It checks all possible execution paths, including rare error paths that testing rarely exercises.
Key distinction from testing: Testing checks specific inputs and observes behavior. Static analysis reasons about all inputs and all paths through the code. Testing finds bugs on paths you run; static analysis finds bugs on paths you never thought to run.
1.2 The goto fail; Bug (2014)
Apple’s SSL/TLS implementation contained a duplicated goto fail; statement that bypassed certificate verification on every iOS and macOS device:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <-- DUPLICATED LINE: always executes!
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
Why this is devastating: The second goto fail; is unconditional. It always jumps to the cleanup code, skipping the final verification step. Any SSL certificate would be accepted as valid.
Why static analysis catches it instantly:
- Unreachable code detection – the
SSLHashSHA1.final()call can never execute - Control flow analysis – the unconditional jump is flagged as suspicious
- Every commercial static analyzer on the market detected this bug
Why testing missed it: Functional tests typically verify that valid certificates are accepted and invalid certificates are rejected. The bug made all certificates appear valid, so the “accept valid certificate” tests passed. Only a carefully crafted test with a deliberately broken certificate chain would have caught it.
1.3 Non-Local Bugs in Large Codebases
Engler et al. [1] analyzed the Linux kernel and found over 1,000 bugs by treating deviations from common patterns as potential errors. A classic example is an interrupt-handling bug:
// Linux kernel interrupt handler
spin_lock(&lock);
if (error_condition) {
return -EINVAL; // BUG: returns with lock held!
}
// ... normal path ...
spin_unlock(&lock);
return 0;
Why humans miss this: The spin_lock and the missing spin_unlock on the error path may be separated by dozens of lines. Code reviewers focus on the happy path. The error path exits early, and the forgotten unlock is invisible during a line-by-line read.
Why SA finds it: Static analysis tracks the lock state across all paths, including the error return. It sees: lock acquired at entry, no unlock on the error path, function returns – violation.
Key Insight: Engler [1] demonstrated that static analysis of the Linux kernel found over 1,000 bugs by inferring “beliefs” from code patterns and flagging deviations. This approach requires no specification – the code’s own consistency is the spec.
1.4 Six Categories of Hard-to-Test Defects
Static analysis excels at finding defects that are difficult to trigger through testing:
| Category | Example | Why Hard to Test |
|---|---|---|
| Buffer overruns | Writing past array bounds | Requires specific input sizes; may not crash immediately |
| Null dereference | Calling method on null pointer | Requires specific sequence of calls that return null |
| Resource leaks | File handle not closed on error path | Error paths rarely exercised in tests |
| API violations | Calling functions in wrong order | Protocol errors may not cause visible failure |
| Race conditions | Two threads modifying shared data | Non-deterministic; may never trigger in test environment |
| Security vulnerabilities | SQL injection, XSS | Requires attacker-crafted input; normal tests use benign data |
Common thread: These bugs hide in rare paths, require specific timing, or need attacker-controlled input. Static analysis checks ALL paths systematically, making it the right tool for these categories.
Exam Tip: Be able to explain why each defect category is hard to find through testing but amenable to static analysis. The key idea is that SA checks all paths, while testing checks only exercised paths.
Part 2: Foundations
Learning Objectives
- Compare static and dynamic analysis along key dimensions
- Define soundness, completeness, and their practical tradeoffs
- Explain Rice’s Theorem and its implications for analysis design
- Map the technique continuum from pattern matching to theorem proving
- Explain how abstraction makes analysis tractable
2.1 Static vs Dynamic Analysis
| Dimension | Static Analysis | Dynamic Analysis (Testing) |
|---|---|---|
| Execution | Does not run the program | Runs the program on inputs |
| Coverage | All paths (via abstraction) | Only exercised paths |
| False positives | Yes (warns about infeasible paths) | No (every failure is real) |
| False negatives | Possible (unsound tools exist) | Yes (misses untested paths) |
| Timing | Before deployment | During or after execution |
| Concurrency | Can reason about all interleavings | Tests specific schedules |
| Input dependence | Independent of specific inputs | Depends entirely on test inputs |
| Cost scaling | Scales with code size | Scales with input space |
Key definition: Static analysis systematically examines an abstraction of the program state space. The word “abstraction” is critical – we do not track exact values, we track simplified representations that capture the properties we care about.
2.2 Soundness vs Completeness
These two properties define what guarantees an analysis provides:
Soundness (over-approximation):
- If the analysis says “no bugs,” there truly are no bugs (of the checked kind)
- May report bugs that do not actually exist (false positives)
- “No false negatives” – never misses a real bug
- Analogy: a smoke detector that goes off for steam is sound – it never misses real fire, but sometimes triggers falsely
Completeness (under-approximation):
- If the analysis reports a bug, it is truly a bug (every report is real)
- May miss bugs that do exist (false negatives)
- “No false positives” – every warning is actionable
- Analogy: testing is complete – every failing test indicates a real problem, but passing tests do not prove absence of bugs
2.3 Confusion Matrix for Analysis Results
| Actual Bug Present | No Actual Bug | |
|---|---|---|
| Analysis reports bug | True Positive (TP) – Correct warning | False Positive (FP) – False alarm |
| Analysis reports clean | False Negative (FN) – Missed bug | True Negative (TN) – Correct silence |
- Sound analysis: FN = 0 (no missed bugs), but may have FP > 0
- Complete analysis: FP = 0 (no false alarms), but may have FN > 0
- Ideal analysis: Both FP = 0 and FN = 0 – impossible in general (see Rice’s Theorem)
2.4 Rice’s Theorem
Rice [13] proved a fundamental impossibility result:
“Any nontrivial property about the language recognized by a Turing machine is undecidable.”
In plain language: For any interesting question about what a program does (Does it divide by zero? Does it terminate? Does it access invalid memory?), there is no algorithm that can always give the correct yes/no answer for every possible program.
Consequence for static analysis: No analysis tool can simultaneously be:
- Sound – reports all bugs (no false negatives)
- Complete – reports only real bugs (no false positives)
- Terminating – always finishes in finite time
Every practical tool must sacrifice at least one of these properties:
| Tool Strategy | Sound? | Complete? | Terminates? | Example |
|---|---|---|---|---|
| Over-approximate (conservative) | Yes | No | Yes | Dataflow analysis, abstract interpretation |
| Under-approximate (optimistic) | No | Yes | Yes | Testing, bounded model checking |
| Precise but may not halt | Yes | Yes | No | Unbounded theorem provers |
| Heuristic (practical) | No | No | Yes | Most commercial tools (FindBugs, PMD) |
Exam Tip: Most commercial tools are neither sound nor complete. They use heuristics to find common bugs quickly. This is a practical engineering choice, not a theoretical deficiency.
2.5 The Technique Continuum
Static analysis techniques form a spectrum trading automation for expressiveness:
| Technique | Automation | Expressiveness | Example Tool | What It Checks |
|---|---|---|---|---|
| Pattern matching | Very high | Low | PMD, grep | Known code patterns |
| Type checking | Very high | Low-Medium | javac, mypy | Type correctness |
| Dataflow analysis | High | Medium | Coverity, Infer | Value flow, null, zero |
| Model checking | Medium | High | SPIN, SLAM | Temporal properties, protocols |
| Theorem proving | Low | Very high | Coq, Isabelle | Arbitrary specifications |
Tradeoff: Moving right increases the sophistication of properties you can check but requires more human effort (annotations, specifications, proofs).
2.6 Abstraction Reduces State Space Exponentially
Without abstraction: Consider 3 integer variables, each 32 bits. The concrete state space is $2^{32 \times 3} = 2^{96}$ – approximately $10^{29}$ states. No computer can enumerate them all.
With zero analysis abstraction: Each variable can be in one of 3 abstract states: $Z$ (Zero), $NZ$ (Not-Zero), or $MZ$ (Maybe-Zero). With 3 variables, the abstract state space is $3^3 = 27$ states (at most $2^6 = 64$ if we consider the lattice structure). This is tractable.
The tradeoff: Abstraction loses information. We can no longer distinguish x=5 from x=42 – both are NZ. This means some paths that are infeasible in reality will appear feasible in the abstract model, producing false positives. But the analysis becomes tractable and terminates.
Key Takeaway: Abstraction is what makes static analysis possible despite Rice’s Theorem. We accept imprecision (false positives) in exchange for tractability and termination.
Part 3: Techniques Overview
Learning Objectives
- Describe AST pattern matching and its tools (PMD, SpotBugs)
- Explain how compilers serve as basic static analyzers
- Define taint analysis and its application to security vulnerabilities
3.1 AST Pattern Matching
How it works:
- Parse source code into an Abstract Syntax Tree (AST)
- Walk the tree looking for subtrees that match known bug patterns
- Report matches as potential defects
Example tools:
- PMD – Matches AST patterns via XPath-like rules for Java, JavaScript, Apex
- SpotBugs (successor to FindBugs) – Bytecode pattern analysis for Java
- Semgrep – Lightweight pattern matching across many languages
Strengths: Fast, easy to write new rules, low false positive rate for well-defined patterns.
Limitations: Cannot reason about data flow or execution order. If a bug depends on the sequence of operations (lock then unlock), AST matching cannot detect it.
3.2 Compilers as Static Analyzers
The compiler is already performing static analysis every time you build:
- Type checking – catches type mismatches at compile time
- Argument count – wrong number of function arguments
- Unreachable code – statements after unconditional return/break
- Uninitialized variables – some compilers warn about this
Best practice: Compile at the highest warning level and treat warnings as errors:
gcc -Wall -Werror program.c
Lint tools extend compiler checks further:
- Splint (for C): variable used before definition, assignment in condition (
if (x = 5)instead ofif (x == 5)), unreachable code - These are cheap checks with high value – effectively free static analysis
3.3 Taint Analysis
Taint analysis is a specialized form of dataflow analysis used primarily for security. It tracks the flow of untrusted (user-controlled) data through the program [4].
The four-element model:
- Source – where untrusted data enters (HTTP parameters, form fields, file reads)
- Propagation – how tainted data flows through variables and function calls
- Sanitizer – functions that make data safe (escaping, validation, encoding)
- Sink – dangerous operations where tainted data causes harm (SQL query, HTML output)
If tainted data reaches a sink without passing through a sanitizer, report a vulnerability.
| Vulnerability | Source | Sink | Sanitizer |
|---|---|---|---|
| SQL Injection | HTTP parameter | SQL query execution | Parameterized query / escaping |
| XSS | User input field | HTML output | HTML encoding |
| Command Injection | Form field | exec() / system() | Input validation / whitelist |
| Path Traversal | URL parameter | File open | Path canonicalization |
Vulnerable code (SQL injection):
String query = "SELECT * FROM users WHERE id = " + request.getParameter("id");
Statement stmt = conn.createStatement();
stmt.execute(query); // SINK: tainted data in SQL query!
Safe code:
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setString(1, request.getParameter("id")); // Sanitized via parameterized query
stmt.execute();
Tools for taint analysis: CodeQL (GitHub), Semgrep, Fortify (Micro Focus) [4]
Exam Tip: Be able to trace a taint flow from source to sink and identify where sanitization is needed. Know the four vulnerability types and their source/sink/sanitizer patterns.
Part 4: DEEP DIVE – Zero Analysis (Dataflow Analysis)
Learning Objectives
- Define dataflow analysis and explain what questions it answers
- Construct the abstract domain and transfer rules for zero analysis
- Trace a zero analysis step by step through a code example
- Explain fixed-point iteration and why it terminates
4.1 What is Dataflow Analysis?
Dataflow analysis tracks how abstract values flow through a program’s control flow graph (CFG). Unlike AST pattern matching, dataflow analysis is sensitive to the order of operations and the paths through the program.
Questions dataflow analysis can answer:
- Did you read a file after closing it?
- Does this null value flow to that dereference?
- Could this variable be zero at this division?
- Where did this tainted user input end up?
- Is this variable always initialized before use?
Key advantage over AST matching: Order and paths matter. Dataflow analysis can determine that a variable is safe on one path but dangerous on another.
4.2 The Abstract Domain for Zero Analysis
We abstract the infinite set of integers into just three values:
| Concrete Values | Abstract Value | Meaning |
|---|---|---|
| $0$ | $Z$ (Zero) | Definitely zero |
| Any $n \neq 0$ | $NZ$ (Not Zero) | Definitely not zero |
| Unknown / mixed | $MZ$ (Maybe Zero) | Could be zero or not |
The lattice: When two paths merge (e.g., after an if-else), we compute the join (least upper bound):
Join rules:
- $Z \sqcup Z = Z$
- $NZ \sqcup NZ = NZ$
- $Z \sqcup NZ = MZ$ (we don’t know which path was taken)
- $MZ \sqcup \text{anything} = MZ$
4.3 Transfer Rules
Transfer rules define how abstract values change when the program executes a statement:
| Statement | Input State | Output State | Explanation |
|---|---|---|---|
x = 0 | any | $x{:}Z$ | Literal zero assignment |
x = 10 | any | $x{:}NZ$ | Non-zero literal |
x = y | $y{:}V$ | $x{:}V$ | Copy propagation |
x = y + z | $y{:}NZ, z{:}NZ$ | $x{:}MZ$ | Sum of non-zeros could be zero (e.g., 5 + (-5)) |
x = y - 1 | $y{:}NZ$ | $x{:}MZ$ | Subtracting 1 from non-zero could yield zero |
x = y * z | $y{:}Z$ or $z{:}Z$ | $x{:}Z$ | Anything times zero is zero |
x = y / z | $z{:}MZ$ | WARNING | Division by maybe-zero! |
x = y / z | $z{:}Z$ | ERROR | Division by definite zero! |
4.4 The Example Code
// Block B1: initialization
x = 10;
y = x;
z = 0;
// Block B2: loop condition
while (y > -1) {
// Block B3: loop body
x = x / y; // <-- CHECK: is y safe here?
y = y - 1;
z = 5;
}
// Block B4: after loop
4.5 Step-by-Step Walkthrough
Iteration 1: First pass through the code
B1 (initialization):
| Statement | x | y | z |
|---|---|---|---|
| (entry) | ? | ? | ? |
x = 10 | NZ | ? | ? |
y = x | NZ | NZ | ? |
z = 0 | NZ | NZ | Z |
State leaving B1: {x: NZ, y: NZ, z: Z}
B2 (condition, first visit):
- Input from B1: {x: NZ, y: NZ, z: Z}
- Condition
y > -1– we enter the loop - State entering B3: {x: NZ, y: NZ, z: Z}
B3 (loop body, first execution):
| Statement | x | y | z |
|---|---|---|---|
| (entry from B2) | NZ | NZ | Z |
x = x / y | NZ (y is NZ, safe!) | NZ | Z |
y = y - 1 | NZ | MZ (NZ - 1 could be 0) | Z |
z = 5 | NZ | MZ | NZ |
State leaving B3: {x: NZ, y: MZ, z: NZ}
Result of Iteration 1: No warnings. The division x / y was safe because y was NZ.
Iteration 2: Second pass (merge at B2)
B2 (condition, second visit) – MERGE:
- Input from B1: {x: NZ, y: NZ, z: Z}
- Input from B3: {x: NZ, y: MZ, z: NZ}
- Merged state: {$x{:} NZ \sqcup NZ = NZ$, $y{:} NZ \sqcup MZ = \mathbf{MZ}$, $z{:} Z \sqcup NZ = \mathbf{MZ}$}
State entering B3: {x: NZ, y: MZ, z: MZ}
B3 (loop body, second execution):
| Statement | x | y | z |
|---|---|---|---|
| (entry from B2) | NZ | MZ | MZ |
x = x / y | NZ ⚠️ WARNING: y is MZ! | MZ | MZ |
y = y - 1 | NZ | MZ (MZ - 1 = MZ) | MZ |
z = 5 | NZ | MZ | NZ |
State leaving B3: {x: NZ, y: MZ, z: NZ}
The warning is issued: At x = x / y, the variable y has abstract value MZ (Maybe Zero). The analysis reports a potential divide-by-zero. Crucially, x stays NZ — the analysis continues only on the non-crash paths (y ≠ 0), and NZ ÷ NZ = NZ in this model.
Iteration 3: Third pass (check for fixed point)
B2 (merge again):
- Input from B1: {x: NZ, y: NZ, z: Z}
- Input from B3: {x: NZ, y: MZ, z: NZ}
- Merged: {$x{:} NZ \sqcup NZ = NZ$, $y{:} NZ \sqcup MZ = MZ$, $z{:} Z \sqcup NZ = MZ$}
This is identical to Iteration 2’s merged state {$x{:}NZ, y{:}MZ, z{:}MZ$}. Fixed point reached. No more changes. Analysis terminates.
Final abstract state at loop body entry: {x: NZ, y: MZ, z: MZ}
4.6 Null Pointer Analysis: Same Idea, Different Domain
The same framework applies to null pointer detection. Replace the abstract domain:
| Abstract Value | Meaning |
|---|---|
| $Null$ | Definitely null |
| $NotNull$ | Definitely not null |
| $MaybeNull$ | Could be null |
Example (Java):
Object y = bar(); // y: MaybeNull (bar() might return null)
Object x;
if (y != null) {
x = new Object(); // x: NotNull (in this branch)
} else {
x = y; // x: Null (y is null in this branch)
}
// After merge: x = NotNull join Null = MaybeNull
x.intVal(); // WARNING: x is MaybeNull!
Path sensitivity: After the null check if (y != null), the analysis refines the abstract value: y is NotNull on the true branch and Null on the false branch. This is called path-sensitive or branch-refined analysis.
4.7 Why Fixed-Point Iteration Terminates
Guaranteed termination requires three conditions:
- Finite abstract domain: The set of abstract values is finite (e.g., ${Z, NZ, MZ}$ has exactly 3 elements)
- Monotonic transfer functions: Values can only move “up” in the lattice ($f(x) \sqsubseteq f(y)$ when $x \sqsubseteq y$). $Z$ or $NZ$ can become $MZ$, but $MZ$ never becomes $Z$ or $NZ$
- Finite height lattice: There are only finitely many “steps” from bottom to $\top = MZ$
Because values can only increase and the domain is finite, eventually no value can change further. This is the fixed point. For zero analysis with 3 variables, the lattice height is 2, so each variable can change at most twice. The analysis always terminates.
Exam Tip: Be able to trace through a zero analysis example step by step. Practice on new code snippets: identify blocks, write initial states, apply transfer rules, merge at joins, iterate until fixed point. The division warning always appears when the divisor reaches MZ.
Part 5: DEEP DIVE – Model Checking Locking Discipline
Learning Objectives
- Define model checking and how it differs from dataflow analysis
- State the three rules of locking discipline
- Construct a product automaton from code and a property FSM
- Trace through the product automaton to find violations
- Explain CEGAR and its role in making model checking practical
5.1 What is Model Checking?
Model checking is a formal verification technique that exhaustively explores all reachable states of a system to verify whether a property holds [14]. If the property is violated, the model checker produces a counterexample – a concrete execution path demonstrating the violation.
Four-step process:
- Build model – represent the program as a state machine
- Specify property – express the requirement in temporal logic
- Exhaustively explore – check every reachable state
- Counterexample – if property violated, provide a witness path
Temporal logic (brief):
- Safety: $G(\neg \textit{bad})$ – “Globally, the bad state never occurs.” Example: never divide by zero.
- Liveness: $G(\textit{request} \rightarrow F\;\textit{response})$ – “Globally, every request is eventually followed by a response.” Example: every lock is eventually unlocked.
When to use model checking vs dataflow analysis:
| Criterion | Dataflow Analysis | Model Checking |
|---|---|---|
| Best for | Value tracking (null, zero, taint) | Protocol/behavioral properties |
| Concurrency | Limited | Excellent (all interleavings) |
| Scalability | Good (handles large codebases) | Limited (state explosion) |
| Output | Warnings at specific lines | Counterexample traces |
| Cost | Moderate | Expensive |
5.2 Locking Discipline: Three Rules
A thread interacting with a shared resource via lock/unlock must obey three rules:
- No double lock: Do not lock a resource you already hold
- No double unlock: Do not unlock a resource you do not hold
- No terminate-while-locked: Do not exit while still holding a lock
Property FSM (Finite State Machine):
| Transition | From | To | Meaning |
|---|---|---|---|
lock | $P_0$ | $P_1$ | Valid: acquire lock |
unlock | $P_1$ | $P_0$ | Valid: release lock |
unlock | $P_0$ | $P_2$ | ERROR: double unlock |
lock | $P_1$ | $P_2$ | ERROR: double lock |
end | $P_1$ | $P_2$ | ERROR: terminate while locked |
This FSM encodes the locking discipline as a specification. Any path that reaches $P_2$ represents a violation.
5.3 The Code Under Analysis (Holzmann Example)
Consider this C code handling a device write queue [3]:
do {
lock(&devExt->writeListLock); // S0: acquire lock
nPacketsOld = nPackets;
request = devExt->WriteListHeadVa;
if (request && request->status) { // S1: check condition
devExt->WriteListHeadVa = request->nxt;
unlock(&devExt->writeListLock); // S2: release lock (inside if)
nPackets++;
}
} while (nPackets != nPacketsOld); // S3: loop condition
unlock(&devExt->writeListLock); // S4: release lock (after loop)
Code locations:
- S0: Lock acquired at top of loop
- S1: Conditional check (if true: process request; if false: skip to loop condition)
- S2: Unlock inside the if-body
- S3: Loop condition check (continue or exit)
- S4: Unlock after loop exit
At first glance, this looks correct: lock at the top, unlock inside the if-body when processing, unlock after the loop when done. But model checking reveals two hidden bugs.
5.4 Product Automaton Construction
The product automaton combines the code’s CFG with the lock property FSM. Each state is a pair: (code location, lock state).
Step-by-step construction:
Start: $(S_0, P_0)$ – at the lock statement, lock is not held
Path A: if-condition is TRUE (request exists and has status)
\[P_0S_0 \xrightarrow{\text{lock}} P_1S_1 \xrightarrow{\text{if true}} P_1S_2 \xrightarrow{\text{unlock}} P_0S_3 \xrightarrow{\text{exit}} P_0S_4 \xrightarrow{\text{unlock}} \textbf{ERROR}\]Double unlock! Lock was released at $S_2$, then released again at $S_4$.
Path B: if-condition is FALSE (no request or bad status)
\[P_0S_0 \xrightarrow{\text{lock}} P_1S_1 \xrightarrow{\text{if false}} P_1S_3 \xrightarrow{\text{loop}} P_1S_0 \xrightarrow{\text{lock}} \textbf{ERROR}\]Double lock! Lock was never released (if-body skipped), then acquired again on next iteration.
5.5 Bug 1: Double Lock (if-condition FALSE)
Counterexample trace:
- Start: unlocked (P0)
- S0:
lock()– acquire lock, now P1 - S1: condition is false (request is null or status is 0)
- Skip if-body entirely – lock is never released
- S3:
nPackets == nPacketsOldis still true (nothing changed), so loop continues - S0:
lock()again – ERROR: already locked (P1 -> P2)
Root cause: When the if-condition is false, the unlock() inside the if-body is never executed. The loop continues back to S0, which tries to lock again while the lock is still held.
5.6 Bug 2: Double Unlock (if-condition TRUE, loop exits)
Counterexample trace:
- Start: unlocked (P0)
- S0:
lock()– acquire lock, now P1 - S1: condition is true (request exists)
- S2:
unlock()– release lock, now P0 - S3:
nPackets != nPacketsOldis false (one packet processed, counts differ – wait, actuallynPacketswas incremented, so they differ and loop continues)
Actually, let us trace more carefully: After processing one request, nPackets is incremented but nPacketsOld was set to the old value. So nPackets != nPacketsOld is TRUE and the loop continues. The loop exits only when no request is processed in an iteration. But that means the if-condition was false, which is Bug 1’s scenario.
Revised trace for Bug 2: Consider the case where the if-condition is true on the first iteration (unlock at S2) and then the if-condition is false on the second iteration (no unlock). Now nPackets == nPacketsOld, so the loop exits at S3. At this point, the lock is held (from the second iteration’s lock at S0 without unlock). Then S4 does unlock() – this is actually correct for this path.
The double unlock scenario occurs if: after processing the last request (unlock at S2), the loop condition evaluates to exit (nPackets == nPacketsOld somehow), and then S4 does unlock() again. The model checker identifies the precise condition under which this path is feasible.
Path: $P_0 \xrightarrow{\text{lock}(S_0)} P_1 \xrightarrow{\text{unlock}(S_2)} P_0 \xrightarrow{\text{exit}(S_3)} P_0 \xrightarrow{\text{unlock}(S_4)} \textbf{ERROR}$
Key insight: Both bugs are non-local – the lock and unlock operations are spread across different code locations and control flow paths. The product automaton mechanically reveals all paths to the error state.
Exam Tip: Be able to construct a product automaton given a short code snippet and a property FSM. Practice: label code locations (S0, S1, …), draw the property FSM (P0, P1, P2), then enumerate all (Si, Pj) pairs reachable from the start state.
5.7 CEGAR: Counter-Example Guided Abstraction Refinement
Real programs have too many states to explore exhaustively. CEGAR makes model checking practical by iteratively refining abstractions [15]:
The CEGAR loop:
- Abstract: Create a coarse model of the program (ignore some details)
- Verify: Run model checker on the abstract model
- Check counterexample: Is the reported violation real or spurious?
- If real: report the bug (done)
- If spurious: the abstraction was too coarse
- Refine: Add more detail to the abstraction to eliminate the spurious counterexample
- Repeat from step 2 with the refined model
Why CEGAR works: It only adds detail where needed. Most of the program can remain abstract; only the parts relevant to the bug need precision.
5.8 Microsoft SLAM / Static Driver Verifier
The SLAM project at Microsoft [15] [16] industrialized CEGAR-based model checking:
| Aspect | Detail |
|---|---|
| Domain | Windows device drivers |
| Technique | CEGAR with predicate abstraction |
| Deployment | Static Driver Verifier (SDV), shipped in the Windows Driver Development Kit |
| Duration | A decade of successful deployment (2001-2011+) |
| Impact | Reduced driver-related Blue Screens of Death (BSODs) |
| Requirement | Every Windows driver must pass SDV before certification |
Ball et al. [16] reported that SLAM successfully transitioned from research to product, verifying temporal safety properties of device drivers (such as locking discipline, IRP handling, and power management protocols).
5.9 State Explosion Problem
The fundamental challenge of model checking is state explosion: the number of states grows exponentially with the number of variables and threads.
| Factor | Growth |
|---|---|
| Variables | $k$ variables with $n$ values each = $n^k$ states |
| Threads | $t$ threads with $s$ states each = $s^t$ interleavings |
| Combined | Multiplicative: variables $\times$ threads |
Mitigation strategies:
| Strategy | Description | Example |
|---|---|---|
| Abstraction | Replace concrete values with abstract domains | CEGAR, predicate abstraction |
| Partial-order reduction | Skip equivalent interleavings | SPIN |
| Symbolic model checking | Represent state sets as BDDs/SAT formulas | NuSMV |
| Bounded model checking | Check only paths up to length k | CBMC |
| Compositional verification | Verify components separately | Assume-guarantee |
Key Insight: The state explosion problem is fundamental and cannot be eliminated – it can only be managed through abstraction and clever search strategies.
Exam Tip: Be able to explain CEGAR in your own words: start coarse, check, refine only where needed. Know that SLAM used CEGAR for Windows drivers and that SDV was shipped in the DDK.
Part 6: Modern Practice
Learning Objectives
- Classify modern SA tools by category and capability
- Explain the 10% false positive rule and its industrial validation
- Compare cost-effectiveness of SA vs testing vs inspection
- Describe symbolic execution and the LLM frontier
6.1 Modern Tool Landscape
| Category | Example Tools | Best For |
|---|---|---|
| Linters | ESLint, Pylint, Checkstyle | Style, basic patterns |
| Type checkers | mypy, TypeScript, Flow | Type safety |
| Bug finders | SpotBugs, PMD, Error Prone | Known bug patterns |
| Advanced analyzers | SonarQube, Coverity, Infer | Deep dataflow, inter-procedural |
| Security analyzers | CodeQL, Semgrep, Fortify | Vulnerability detection |
| Platforms | Tricorder (Google), Infer (Meta) | Scale deployment, workflow integration |
No single tool is sufficient. Different tools catch different categories of bugs with different precision/recall tradeoffs. Best practice is to use multiple complementary tools.
6.2 Google Tricorder: The 10% Rule in Practice
Sadowski et al. [5] described Google’s Tricorder platform:
| Metric | Value |
|---|---|
| Daily findings | ~93,000 across 31,000 changelists |
| False positive threshold | <10% effective “not useful” rate |
| Probation trigger | >10% “not useful” rate |
| Immediate disable | >25% “not useful” rate |
| Platform-wide “not useful” rate | ~5% (<4% excluding probationary tools) |
| Daily “PLEASE FIX” clicks | 716 |
Key design principles:
- Measure usefulness, not accuracy – developer action is the metric
- Workflow integration – findings appear during code review, not in a separate dashboard
- Empower domain experts – engineers write custom analyzers for their subsystems
- Project-level customization – not per-user (maintains team consistency)
- Provide fixes, not just warnings – suggested auto-fixes lower the barrier to action
“The bottom line is that developers will decide whether an analysis tool has high impact, and what a false positive is.” [5]
6.3 The Tool Agreement Problem
Lenarduzzi et al. [8] compared six Java SA tools on 47 projects (13.5 million warning instances):
| Metric | Value |
|---|---|
| Line-level agreement | <0.4% |
| Highest pairwise overlap | FindBugs + PMD: 9.4% |
| 3+ tool agreement | Zero at line level |
Implications:
- “Our SonarQube is clean” does NOT mean “our code is bug-free”
- Each tool sees different things – they are complementary, not redundant
- The ensemble approach (multiple tools) catches more bugs than any single tool
- High precision does not mean comprehensive (CheckStyle is precise but narrow)
6.4 Cost-Effectiveness Evidence
Multiple industrial studies converge on consistent cost-effectiveness benchmarks:
| Metric | Value | Source |
|---|---|---|
| Dev vs Testing cost | 10x cheaper to find defects during development | Jaspan et al. [7] |
| ASA vs Manual Inspection cost | ASA costs 60-72% of manual inspection per defect | Zheng et al. [6] |
| ASA defect yield | 23-37% of total defects | Zheng et al. [6] |
| Testing defect yield | 63-98% of total defects | Zheng et al. [6] |
| eBay initial FP rate | 50% (out of the box) | Jaspan et al. [7] |
| eBay after customization | ~10% effective FP rate | Jaspan et al. [7] |
| Google SA volume | 93K findings/day | Sadowski et al. [5] |
Interpretation:
- SA finds different bugs than testing – Checking/Assignment defects vs Functional/Algorithmic
- SA is cheaper per defect than manual inspection but finds fewer total defects
- The combination of SA + testing catches more than either alone
- Customization is essential: eBay reduced FP from 50% to 10% by dropping 75% of default checks
ROI example (eBay):
2 FTE x $200K/year = $400K/year for SA maintenance
= Same cost as 2 manual testers
But SA scales to millions of LOC; manual testers don't
6.5 Symbolic Execution
Symbolic execution treats program inputs as symbolic variables rather than concrete values, building path conditions – logical formulas that describe what inputs follow each path. An SMT solver (Z3, STP) determines if each path is feasible and generates concrete test inputs.
KLEE results (Cadar et al. [9]):
| Metric | Value |
|---|---|
| COREUTILS coverage | 90%+ average (median 94%) |
| vs Developer tests | +16.8% better coverage |
| Bugs found | 56 serious bugs (including 3 missed for 15 years) |
| Run time | ~89 hours (automated) |
“The roughly 89-hour run used to generate COREUTILS line coverage beat the developers’ own test suite – built incrementally over fifteen years – by 16.8%!” [9]
Challenge: Path explosion. For $n$ branches, there are up to $2^n$ paths. Mitigation strategies [12] include scope reduction, guidance heuristics, and hybrid approaches (combining with fuzzing).
6.6 The LLM Frontier
Li et al. [10] developed IRIS, a neuro-symbolic approach combining LLMs with CodeQL:
| Metric | IRIS (GPT-4) | CodeQL Alone |
|---|---|---|
| Vulnerabilities detected | 55 | 27 |
| Improvement | +103.7% | baseline |
| New vulnerabilities found | 4 | 0 |
How IRIS works:
- LLM infers taint specifications for third-party APIs that CodeQL cannot handle
- Augmented specifications fed to CodeQL engine
- LLM filters spurious results using contextual reasoning
Wang et al. [11] surveyed LLM applications in program analysis, reporting 50-99% effectiveness depending on task and model, but noted significant limitations: non-determinism, hallucinations, token limits, and prompt engineering dependence.
Key Insight: LLMs augment, not replace, traditional SA tools. Best results come from combining LLM semantic understanding with traditional SA soundness guarantees.
6.7 Industrial Deployment: Nortel Case Study
Zheng et al. [6] studied SA deployment at Nortel Networks across 33+ million lines of code:
| Method | Defect Yield | Cost per Defect |
|---|---|---|
| ASA (FlexeLint, Klocwork) | 23-37% | 60-72% of inspection cost |
| Manual Inspection | 20-40% | Baseline (100%) |
| Testing | 63-98% | Highest (but catches most) |
Key finding: ASA and manual inspection have similar yield, but ASA costs 28-40% less. The defect types are complementary: ASA excels at Checking and Assignment errors; testing excels at Functional and Algorithmic errors.
Exam Tip: Know the 10% FP rule (Tricorder threshold), the <0.4% tool agreement finding, and why workflow integration matters more than tool accuracy. Be able to cite the cost-effectiveness numbers: 10x cheaper than testing, 60-72% of inspection cost.
Part 7: Summary
7.1 Key Takeaways
-
Systematic: Static analysis examines all paths via abstraction. Zero analysis demonstrates exactly how abstraction reduces $2^{96}$ states to 64.
-
Limited by theory: Rice’s Theorem [13] proves no analysis can be simultaneously sound, complete, and terminating. Every practical tool makes tradeoffs.
-
Diverse techniques: The continuum from AST pattern matching to theorem proving offers different cost/precision tradeoffs. Dataflow analysis tracks values; model checking explores behaviors.
-
Proven at scale: Microsoft SLAM [15] [16] verified Windows drivers. Google Tricorder [5] processes 93K findings/day. eBay [7] achieved 10x cost savings.
-
Cost-effective: SA costs 60-72% of manual inspection [6] and is 10x cheaper than finding defects during testing [7].
-
Workflow integration is critical: The 10% FP threshold [5], tool customization [7], and multiple complementary tools [8] are the three pillars of successful deployment.
-
Evolving frontier: Symbolic execution (KLEE: 90%+ coverage [9]) and LLM augmentation (IRIS: +103% detection [10]) are expanding what static analysis can achieve.
7.2 Key Numbers Reference Card
| Metric | Value | Source |
|---|---|---|
| FP threshold (Google) | <10% | Sadowski 2015 |
| Daily findings (Google) | 93K | Sadowski 2015 |
| Tool agreement | <0.4% | Lenarduzzi 2021 |
| SonarQube precision | 18% | Lenarduzzi 2021 |
| Dev vs Testing cost | 10x cheaper | Jaspan 2007 |
| ASA vs Inspection cost | 60-72% | Zheng 2006 |
| ASA defect yield | 23-37% | Zheng 2006 |
| eBay FP after tuning | ~10% | Jaspan 2007 |
| KLEE coverage | 90%+ median | Cadar 2008 |
| IRIS improvement | +103% | Li 2024 |
| Linux kernel bugs (Engler) | 1000+ | Engler 2001 |
References
- D. Engler, D. Y. Chen, S. Hallem, A. Chou, and B. Chelf, “Bugs as Deviant Behavior: A General Approach to Inferring Errors in Systems Code,” in Proceedings of the 18th ACM Symposium on Operating Systems Principles (SOSP), 2001, pp. 57–72. doi: 10.1145/502034.502041.
- C. Flanagan, K. R. M. Leino, M. Lillibridge, G. Nelson, J. B. Saxe, and R. Stata, “Extended Static Checking for Java,” in Proceedings of the ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 2002, pp. 234–245. doi: 10.1145/512529.512558.
- G. J. Holzmann, The SPIN Model Checker: Primer and Reference Manual. Addison-Wesley, 2004.
- B. Chess and J. West, Secure Programming with Static Analysis. Addison-Wesley, 2007.
- C. Sadowski, J. van Gogh, C. Jaspan, E. Söderberg, and C. Winter, “Tricorder: Building a Program Analysis Ecosystem,” in Proceedings of the 37th International Conference on Software Engineering (ICSE), 2015, pp. 598–608. doi: 10.1109/ICSE.2015.76.
- J. Zheng, L. Williams, N. Nagappan, W. Snipes, J. P. Hudepohl, and M. A. Vouk, “On the Value of Static Analysis for Fault Detection in Software,” IEEE Transactions on Software Engineering, vol. 32, no. 4, pp. 240–253, 2006, doi: 10.1109/TSE.2006.38.
- C. Jaspan, I.-C. Chen, and A. Sharma, “Understanding the Value of Program Analysis Tools,” in Companion to the 22nd ACM SIGPLAN Conference on Object-Oriented Programming Systems and Applications (OOPSLA), 2007, pp. 963–970. doi: 10.1145/1297846.1297964.
- V. Lenarduzzi, N. Saarimäki, and D. Taibi, “A Critical Comparison on Six Static Analysis Tools: Detection, Agreement, and Precision,” Journal of Systems and Software, vol. 198, p. 111575, 2023, doi: 10.1016/j.jss.2022.111575.
- C. Cadar, D. Dunbar, and D. Engler, “KLEE: Unassisted and Automatic Generation of High-Coverage Tests for Complex Systems Programs,” in Proceedings of the 8th USENIX Symposium on Operating Systems Design and Implementation (OSDI), 2008, pp. 209–224.
- Z. Li, S. Pacheco, M. Chheda, and M. Naik, “IRIS: LLM-Assisted Static Analysis for Detecting Security Vulnerabilities,” in arXiv preprint arXiv:2405.17238, 2024. doi: 10.48550/arXiv.2405.17238.
- J. Wang, C. Li, X. Li, S. Dong, and H. Zhong, “A Contemporary Survey of Large Language Model Assisted Program Analysis,” arXiv preprint arXiv:2502.18474, 2025, doi: 10.48550/arXiv.2502.18474.
- J. Bailey and C. Nicholas, “Symbolic Execution in Practice: A Survey of Applications in Vulnerability, Malware, Firmware, and Protocol Analysis,” arXiv preprint arXiv:2508.06643, 2025, doi: 10.48550/arXiv.2508.06643.
- H. G. Rice, “Classes of Recursively Enumerable Sets and Their Decision Problems,” Transactions of the American Mathematical Society, vol. 74, no. 2, pp. 358–366, 1953, doi: 10.2307/1990888.
- M. Ben-Ari, A Primer on Model Checking. Springer, 2010.
- T. Ball, B. Cook, V. Levin, and S. K. Rajamani, “SLAM and Static Driver Verifier: Technology Transfer of Formal Methods inside Microsoft,” in Integrated Formal Methods (IFM), in LNCS, vol. 2999. Springer, 2004, pp. 1–20. doi: 10.1007/978-3-540-24756-2_1.
- T. Ball, V. Levin, and S. K. Rajamani, “A Decade of Software Model Checking with SLAM,” Communications of the ACM, vol. 54, no. 7, pp. 68–76, 2011, doi: 10.1145/1965724.1965743.
Disclaimer: AI is used for text summarization, polishing and explaining. Authors have verified all facts and claims. In case of an error, feel free to file an issue.