Empirical Evaluation Against Real CVEs
Motivation
The design rationale argues that arithmetic safety is a dangerous and under discussed class of bugs, and the verification page establishes that the fundamental operations are correct for all inputs by transcription from Why3 proofs. Those two arguments answer "is the mechanism sound." This page answers a different and complementary question: "how often would the mechanism, if adopted, have engaged on real historical vulnerabilities."
To answer it we assembled a corpus of real, publicly disclosed CVEs whose root cause is an
integer arithmetic fault, grouped by CWE, and for each one we reproduced the specific
faulting computation that the disclosure and its patch identify. Each corpus entry is a
self contained test that proves two things under identical inputs: the native fixed width
computation silently produces the same incorrect value the vulnerability is attributed to,
and the identical computation written with boost::safe_numbers detects the fault at the
faulting operation, either by throwing at runtime or by failing to compile.
The corpus, its per file metadata, the analysis script, and the machine generated tables live in test/cve_corpus. The full methodology, inclusion and exclusion criteria, and threats to validity are in the corpus README.
The claim, stated carefully
We reproduce the arithmetic fault. We do not re run the original programs. The defensible claim is therefore a counterfactual about the arithmetic mechanism in isolation:
For each in scope CVE the native fixed width computation silently produces the incorrect value the disclosure attributes the vulnerability to, whereas the identical computation written with boost::safe_numbers halts at the faulting operation with a typed, catchable fault. Had that computation been written with the library under its default policy, the specific silent miscalculation identified as the root cause would have been converted into a controlled, observable failure rather than propagating as a valid looking value.
We do not claim that the entire original program, with its full state and control flow, would have been immune. The library acts at the arithmetic root, upstream of the downstream memory safety consequence.
Method in brief
CVEs are sourced from the NVD by CWE, so each entry sits in a category the National Vulnerability Database itself assigned, not one we reassigned. Every examined CVE is placed in exactly one disposition, and nothing is silently dropped:
-
PREVENTED_RUNTIME: the safe computation throws at the faulting operation. -
PREVENTED_COMPILETIME: porting to safe types makes the faulting expression ill formed (for example a mixed signedness comparison or a mixed width operation). -
PREVENTED_BOUNDED: the value’s legal domain is expressed with a bounded type, which rejects the out of domain input before any arithmetic runs. -
NOT_PREVENTED: a genuine integer arithmetic CVE whose faulting value is representable and in range, so the library cannot detect it. These count in the denominator. -
OUT_OF_SCOPE: on reduction, not a fixed width integer arithmetic fault. Recorded and reported separately, removed from the denominator.
To guard against a result that only looks good because the categories were chosen to flatter the library, the study includes CWE-682 (Incorrect Calculation) as a negative control, a category where many defects are logic errors the library cannot catch and where we therefore expect a distinctly lower prevention rate.
The current corpus is a curated (purposive) sample: from the by-CWE pools we selected CVEs whose arithmetic root cause is reducible to a self contained snippet. The per category prevention rate should therefore be read as conditional on that reducibility, and the credibility of the result rests on the negative control and the honest not prevented cases rather than on a claim of random sampling. The corpus README documents the sampling methodology and the threats to validity in full.
CVE corpus results
This section is generated from the corpus metadata by test/cve_corpus/analysis/aggregate.py.
Disposition funnel
| Category | Examined | Out of scope | In scope | Prevented | Not prevented |
|---|---|---|---|---|---|
CWE-190 |
30 |
0 |
30 |
30 |
0 |
CWE-191 |
30 |
0 |
30 |
30 |
0 |
CWE-681 |
30 |
0 |
30 |
30 |
0 |
CWE-369 |
30 |
0 |
30 |
30 |
0 |
CWE-682 |
20 |
10 |
10 |
6 |
4 |
All |
140 |
10 |
130 |
126 |
4 |
Prevention rate by category
The "by default" column counts detection with the default checked types alone; the "incl. bounded" column adds cases prevented once a value’s domain is expressed with a bounded type.
| CWE | Weakness | In scope | Prevented by default | Prevented incl. bounded | Prevention rate (95% Wilson CI) | McNemar p |
|---|---|---|---|---|---|---|
CWE-190 |
Integer Overflow or Wraparound |
30 |
29 |
30 |
100.0% (88.6% to 100.0%) |
1.86e-09 |
CWE-191 |
Integer Underflow |
30 |
30 |
30 |
100.0% (88.6% to 100.0%) |
1.86e-09 |
CWE-681 |
Incorrect Conversion between Numeric Types |
30 |
30 |
30 |
100.0% (88.6% to 100.0%) |
1.86e-09 |
CWE-369 |
Divide By Zero |
30 |
30 |
30 |
100.0% (88.6% to 100.0%) |
1.86e-09 |
CWE-682 |
Incorrect Calculation (negative control) |
10 |
3 |
6 |
60.0% (31.3% to 83.2%) |
0.0312 |
POOLED |
All categories |
130 |
122 |
126 |
96.9% (92.4% to 98.8%) |
2.35e-38 |
Category averaged prevention rate (unweighted mean of the per category rates): 92.0%.
Reading these numbers
The native baseline detects zero of these faults by construction, since each CVE shipped in native integer code. Every prevented case is therefore a discordant pair favoring safe_numbers, so the McNemar p value is ceilinged by design: it certifies that the asymmetry is not chance, and it is not an effect size. The prevention rate and its Wilson interval are the primary result, bounded by the genuine failure mode visible in the not prevented cases.
In the CWE-682 negative control the gap between the two prevented columns is the contribution of bounded types: calculation errors whose wrong result is used as an index, offset, or length into a buffer of known size are caught when that domain is declared with a bounded type, even though the arithmetic itself does not overflow. The cases that remain not prevented are in range value errors (rounding, a wrong cryptographic result, a floating point mishandling) where no bound applies.
Bounded types: a precondition layer
Beyond the default checked types, every primary-category entry in the corpus carries a third arm that declares the value’s legal domain with a bounded type. This models the common case where a valid range is known independently of the computation: a length that must be at least a header size, a divisor that must be non zero, an index into a buffer of known size. Declaring that domain rejects a malicious input at the boundary, before the arithmetic runs at all, which is a second and earlier line of defense than the operation-level check.
For example, an index into a 150-element buffer has the domain 0 through 149:
using boost::safe_numbers::bounded_uint;
using boost::safe_numbers::u8;
using table_index = bounded_uint<0U, 149U>; // valid indices for a 150-element buffer
const u8 base {100U};
const u8 length {50U};
const u8 end_index {base + length}; // 150: correct arithmetic, no overflow
// The default checked type does not object: 100 + 50 is representable. It is the declared
// index domain that catches the off-by-one when the value is materialized as an index:
(void) table_index{end_index}; // throws std::domain_error
The addition is correct and does not overflow, so the default check stays silent. The declared index domain is what catches the off-by-one.
|
One primary-category case is prevented only through a bounded type, which is why the CWE-190
row reads 29 prevented by default and 30 including bounded. In CVE-2018-13785 (libpng) an
oversized image width flows into a This is a modeling choice that mirrors libpng’s own fix. The downstream |
Where bounded types change the outcome: the CWE-682 control
The negative control makes the effect concrete. Of its ten in-scope cases, three are prevented by the default checked types (they are divide-by-zero or shift faults that the NVD happened to tag as calculation errors). Declaring domains with bounded types prevents three more, raising the control from three prevented to six:
-
an off-by-one index used against a fixed-size table (the example above, CVE-2011-3062),
-
a length that omits a padding term and exceeds the real available space (CVE-2011-1573), and
-
a file offset that lands past a known line buffer (CVE-2018-11790).
In each, the wrong result is used as an index, offset, or length into a buffer of independently known size, so the out-of-domain value is caught when it is formed. The bound must come from an independent quantity, the real buffer size, never from the correct answer.
The remaining four control cases stay unprevented because their wrong result is a valid in-domain value: a rounded time value (CVE-2016-7433), a wrong elliptic-curve point (CVE-2017-8932), a mishandled floating point number (CVE-2018-14439), and a wrong cipher-reset output (CVE-2018-20999). No bound applies to these, which is exactly why the negative control still marks the library’s limit. The "by default" and "incl. bounded" columns in the table above quantify this split: three by default, six once domains are declared, four beyond reach of either.
Reproducing these numbers
The corpus is built and run like any other test in the suite, and the statistics are regenerated from the metadata by a dependency free Python script:
# build and run the corpus (and confirm the compile-fail entries fail to compile)
b2 libs/safe_numbers/test
# regenerate corpus.csv, results_by_cwe.csv, results.adoc, and the doc partial
python3 libs/safe_numbers/test/cve_corpus/analysis/aggregate.py
The script validates every metadata block, cross checks the recorded classification against the test outcome when a results file is supplied, and exits nonzero on any inconsistency, so the corpus can be gated in continuous integration.
How to read the significance
The native baseline detects zero of these faults by construction, since every CVE shipped in native integer code. Every prevented case is therefore a discordant pair favoring safe_numbers, so the paired McNemar test is ceilinged by design: it certifies that the asymmetry is not chance, and it is not an effect size. The scientifically meaningful quantity is the prevention rate and its Wilson confidence interval, which is bounded by the genuine failure mode visible in the not prevented cases and in the negative control. This empirical coverage result complements, and does not replace, the soundness result on the verification page.