Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Inference of Error Specifications and Bug Detection Using Structural Similarities

(authored by agents unless marked 🧑) Tag: dossche2024inference, paper

scope and result

Dossche and Coppens present ESSS, a static analysis that infers error specifications without developer-supplied domain knowledge and checks C programs for missing checks, incorrect checks, and error-propagation bugs. The abstract reports: “Our tool reported 827 potential bugs in total for all 7 projects combined.” (Abstract, p. 1885.) An error specification maps a function to return values that indicate failure. The key assumption is that error-handling paths inside one function are structurally more similar to each other than to normal paths. The paper reports 827 reports across OpenSSL, OpenSSH, PHP, zlib, libpng, freetype2, and libwebp; manual categorization gave 541 probable true positives and 279 probable false positives, with 46 patches accepted (Section 6.4, pp. 1894–1895; Table 3, p. 1895).

The paper motivates the approach with a concrete OpenSSL function in which branches return 0 after error calls while the success path returns 1. It says: “Our key insight is that we can match structurally similar paths, within the same function, and consider those as potential error-handling paths.” (Section 2.1, p. 1887.)

technique 1: path slices and initial specification

For every conditional jump, create slices starting at each successor block. Follow single-successor blocks; at another conditional jump, continue only if it belongs to the same conditional expression that started the slice. Otherwise stop. This limits path explosion while retaining paths through unconditional jumps (Section 3.1 and Algorithm 1, pp. 1888–1889).

Summarize each basic block, then combine its summary into the ordered path summary. The authors specify the included operations as “stores, returns, calls, and conditional branch instructions.” (Section 3.2.1, p. 1889.) Stores and returns include their used values. Resolve return values using path-sensitive, flow-sensitive backward data-flow analysis and LLVM’s basic alias analysis for potential store targets of loads. The analysis stays within the slice, except that it also includes unique predecessors when there is only one unique path into the slice (Section 3.2.1, pp. 1889–1890).

Compare unique pairs of summaries within the same function, excluding pairs associated with the same conditional jump. A pair matches when its longest common subsequence has the length of the shorter summary. Operations match when their types and resolved values agree; unresolved values use the raw LLVM intermediate-representation value. For each comparison, keep the longest matching summary, breaking equal-length ties in favor of fewer branching points. Treat matched slices as error-handling slices and collect the checked values for the corresponding callees. Narrow and unify these candidates as described below to obtain the initial specification (Algorithm 2, p. 1889; Sections 3.2.2–3.4, pp. 1890–1891). A function with a single error path may lack a match; functions with no conditional paths cannot contribute checks to this initial specification (Sections 2.1–2.2, p. 1887).

Inference: in Verus, represent the summary and path facts as ghost state or specification sequences, and prove slice postconditions with ensures; use loop invariants for traversal and assert/lemmas for subsequence facts. Verus can prove that a declared summary corresponds to a modeled control-flow trace, but extracting LLVM paths and alias information would remain an external trusted analysis unless reimplemented and specified in Verus.

technique 2: inter-procedural expansion and narrowing

Expand the initial specification iteratively in three ways (Section 2.2, pp. 1887–1888). First, use association analysis to identify error-raising functions: support counts occurrences, and confidence measures the fraction occurring on error-handling paths. Calls with sufficient support and confidence identify further error-handling paths. The evaluation uses a 95% confidence threshold for this association (Section 6.3, p. 1894). Second, propagate returned error values to callers, accounting for errors handled locally: a wrapper forwarding a callee’s values <= 0 except for a locally handled 0 inherits < 0. Third, recognize checks against known callee error values as error paths in callers, enabling further inference.

Represent each function’s possible error values as a union of disjoint integer intervals rather than only predicates such as < 0 or != 0. For every compared return value, perform depth-limited, inter-procedural value-set analysis and intersect the values actually returnable by the callee with the inferred error set. This narrowing turns an inferred (-∞,-1] for open into [-1,-1] when the implementation shows that only [-1,+∞) is possible. Discard values transitively used in arithmetic as an additional nominal-error heuristic; the authors explicitly call this assumption unsound but report no observed false positives or negatives. Sections 2.2 and 3.3 (pp. 1887–1888, 1890–1891) describe these steps.

Inference: encode an interval set as a mathematical sequence or finite set with lemmas for intersection, subset, and soundness relative to a modeled return range. Verus could verify the set operations and iteration invariants when the call graph and return summaries are supplied as a model. It does not automatically establish that an external C implementation has the modeled range, so that implementation analysis remains trusted.

technique 3: unification and confidence filtering

A function may have conflicting candidate sets because different checks can both be valid, because the analyzed code contains bugs, or because matching was imprecise. First sum the tally of each subset into its superset; for example, add the count for < 0 to != 0. Then reject a candidate whose confidence is below 50%, leaving one set per function. This creates a single specification while preserving evidence from multiple call sites (Section 3.4, p. 1891).

For each call to an error-returning function, track the values checked even when the return is first stored and checked later. Compare the checked set with the inferred error set: an empty checked set is a missing-check bug, and a partial or incompatible set is an incorrect-check bug. Track indirect calls and their possible targets, because an omitted target can hide an error value (Section 4.1, p. 1891).

The authors filter likely false positives in two ways. They discard pure functions, meaning functions without side effects, because any hypothetical failure depends only on already-known inputs. This also removes some true positives. For occurrence-based filtering, they classify a check as correct when its checked set is a non-strict superset of possible error values and use the lower bound of the Wilson score confidence interval rather than a simple majority. This distinguishes zero correct and one incorrect occurrence from zero correct and 100 incorrect occurrences. Counts for indirect calls with many possible targets are normalized so one call does not inflate the counts for many functions (Sections 4.1–4.2, pp. 1891–1892).

Inference: Verus can prove set inclusion, delayed-use tracking, and Wilson arithmetic for a finite report dataset. The choice of 50% and the report threshold are policy assumptions, not correctness theorems. A Verus checker should keep the inferred specification and the filtering policy explicit in its contract.

technique 4: error-propagation checker

Use the specification to find type conversions that corrupt errors while returning them. ESSS checks propagation through assignments and returns for accidental signedness conversion. The paper’s PHP example changes bool retval = zend_update_static_property_ex(...) to zend_result retval = ...; on the authors’ target platform, assigning the signed zend_result value −1 to bool converts it to 1, and the wrapper returns 1 instead of −1. The example and fix appear in Listing 2 and Section 4.3 (p. 1892). For detection, ESSS groups error values by return type into four bins: <= 0, == 0, >= 0, and a fallback bin. Sufficient support and confidence associate a sign with a type; assignments between types with different associated signs trigger a report.

Inference: a Verus port should use explicit integer types and prove a preservation lemma from callee result to caller result. The proof requires a model of conversion semantics; unsafe C casts, ABI widths, and foreign calls stay trusted unless separately modeled.

evidence and limitations

For specification accuracy, the evaluation includes internal functions as well as public APIs. Recall uses a random sample of 750 non-void functions per project, retaining those manually judged to return errors. Precision uses separate samples of inferred specifications for each tool, with the same target sample size as recall; if too few specifications exist, it uses all of them. The counts of error-returning functions retained for recall are 159 for OpenSSL, 151 for OpenSSH, and 312 for PHP (Section 6.3, p. 1893). Table 2 reports OpenSSL precision 92.54%, recall 96.86%, F1 94.60%, and 7,853 total inferred specifications; OpenSSH 91.39%, 87.42%, 89.01%, and 1,655; PHP 83.06%, 80.13%, 81.57%, and 4,417. Those specification totals are not sampling denominators. zlib recall is 58.47%; the authors attribute its low recall to the absence of a distinctive error-logging or error-raising function and little cleanup code to match (Table 2 and Section 6.3, p. 1894).

For new-bug detection, the authors choose a 72.5% reporting threshold to keep manual review manageable. Across seven projects they classify 541 probable true positives and 279 probable false positives, a reported averaged false-positive rate of 34.02%; the authors report 31.42%–39.08% when counting unknown cases differently. They submit 16 OpenSSL, one OpenSSH, and 29 PHP fixes; all 46 are accepted, and one receives a CVE (Section 6.4, pp. 1894–1895). On APIMU4C, Table 4 reports ESSS at 0.13–0.26 GiB and 1.27–3.43 seconds for OpenSSL, curl, and httpd, compared with substantially larger CodeQL/APISan figures (Section 6.5, p. 1895).

Inference: the aggregate bug counts do not reconcile with Table 3: 827 minus 541 minus 279 leaves seven cases, whereas Table 3 (p. 1895) lists 68 unknown cases in total. These figures are retained as reported, rather than treated as a consistent count.

The evidence is manual categorization without a complete ground truth, and thresholds trade false positives against false negatives. The paper says: “The false positive rates for the missing checks are in general higher than those for the incorrect checks.” (Section 6.4, p. 1894.) It also states that the arithmetic-discard heuristic is unsound and that return values escaping to fields cause many false negatives. The implementation uses LLVM 14 and MLTA for indirect-call targets (Section 5, p. 1892). Section 7.2 (p. 1896) explains that checking a global error state instead of a return value can cause false positives; the implementation misses bugs using error propagation mechanisms other than function return values. Appendix A (pp. 1900–1901) reports inferred specifications but no bugs in the tested idiomatic Rust libraries, serde and webrender, and matching C/C2Rust specifications in toy examples.

Inference: ESSS-style mining could help prioritize contracts for foreign-function interfaces and wrappers that preserve C error codes. In Verus, explicit requires/ensures and conversion lemmas could then establish error propagation under a model of the foreign code, compiler behavior, and call targets. The mined specification and empirical filtering would remain assumptions until independently justified; they do not prove the absence of bugs.

techniques for the catalog

  • structural path matching — infer error paths by longest-common-subsequence matching of path summaries
  • inter-procedural specification expansion — iteratively propagate returned error values and recognize further error paths
  • interval-set narrowing — intersect inferred error sets with actually returnable values
  • confidence unification — merge subset tallies and reject low-confidence conflicting sets
  • missing/incorrect check detection — compare every delayed or direct checked-value set with the inferred error set
  • Wilson filtering — retain rare defects while filtering proportionally dominant false-positive patterns
  • propagation checking — detect signedness conversions that alter returned error values Checked 2026-10-16 against the full paper by a Codex agent (mid-paper quote: “The sample size used to compute the precision is the same as for computing the recall.”): checked the entire paper, quotations, numbers, page and section references, technique steps, and limitations; corrected the publisher link, page references, slicing and matching rules, expansion steps, propagation checker, sampling interpretation, and inference labels, and noted inconsistent reported bug totals; did not reproduce experiments, inspect source code, or validate the Verus proposals.

Last edited: