differential-coverage 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- differential_coverage/__init__.py +123 -0
- differential_coverage-0.1.0.dist-info/METADATA +113 -0
- differential_coverage-0.1.0.dist-info/RECORD +6 -0
- differential_coverage-0.1.0.dist-info/WHEEL +5 -0
- differential_coverage-0.1.0.dist-info/entry_points.txt +2 -0
- differential_coverage-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Compute relscore (as defined in SBFT'25) from coverage in afl-showmap format (id:count per line).
|
|
4
|
+
Input: directory of subdirectories (one per fuzzer), each containing coverage files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_afl_showmap_file(file: Path) -> dict[int, int]:
|
|
12
|
+
"""Parse one afl-showmap file; return dict of edge ids to counts."""
|
|
13
|
+
edges = {}
|
|
14
|
+
for i, line in enumerate(file.read_text().strip().splitlines()):
|
|
15
|
+
line = line.strip()
|
|
16
|
+
if not line:
|
|
17
|
+
continue
|
|
18
|
+
split = line.split(":")
|
|
19
|
+
if len(split) != 2:
|
|
20
|
+
raise ValueError(f"Invalid line {file}:{i}: {line}")
|
|
21
|
+
id, count = split
|
|
22
|
+
edges[int(id)] = int(count)
|
|
23
|
+
return edges
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_fuzzer_dir(path: Path) -> dict[str, dict[int, int]]:
|
|
27
|
+
"""Read all afl-showmap files in a directory; return dict of trial id to dict of edge ids to counts."""
|
|
28
|
+
trials = {}
|
|
29
|
+
for file in path.iterdir():
|
|
30
|
+
if file.is_file():
|
|
31
|
+
trials[file.name] = read_afl_showmap_file(file)
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError(f"Invalid file: {file}")
|
|
34
|
+
return trials
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_campaign_dir(path: Path) -> dict[str, dict[str, dict[int, int]]]:
|
|
38
|
+
"""Read all fuzzer directories in a campaign directory; return dict of fuzzer name to dict of trial id to dict of edge ids to counts."""
|
|
39
|
+
campaigns = {}
|
|
40
|
+
for fuzzer_dir in path.iterdir():
|
|
41
|
+
if fuzzer_dir.is_dir():
|
|
42
|
+
campaigns[fuzzer_dir.name] = read_fuzzer_dir(fuzzer_dir)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Invalid file: {fuzzer_dir}")
|
|
45
|
+
return campaigns
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def calculate_fuzzer_score(
|
|
49
|
+
trials: dict[str, dict[int, int]],
|
|
50
|
+
all_edges: set[int],
|
|
51
|
+
fuzzers_that_never_hit_edge: dict[int, set[str]],
|
|
52
|
+
) -> float:
|
|
53
|
+
score = 0.0
|
|
54
|
+
for e in all_edges:
|
|
55
|
+
fuzzers_that_never_hit_e = len(fuzzers_that_never_hit_edge[e])
|
|
56
|
+
trials_that_hit_e = len([trial for trial in trials.values() if trial.get(e, 0)])
|
|
57
|
+
trials_with_non_empty_cov = len(
|
|
58
|
+
[trial for trial in trials.values() if any(trial.values())]
|
|
59
|
+
)
|
|
60
|
+
score += (
|
|
61
|
+
fuzzers_that_never_hit_e * trials_that_hit_e / trials_with_non_empty_cov
|
|
62
|
+
)
|
|
63
|
+
return score
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def calculate_differential_coverage_scores(
|
|
67
|
+
campaign: dict[str, dict[str, dict[int, int]]],
|
|
68
|
+
) -> dict[str, float]:
|
|
69
|
+
"""
|
|
70
|
+
differential_coverage(f,e) = (# fuzzers that never hit e) * (# trials of f that hit e) / (# trials of f with non-empty cov).
|
|
71
|
+
score(f) = sum of differential_coverage(f,e) over all edges e.
|
|
72
|
+
Returns scores for each fuzzer run.
|
|
73
|
+
"""
|
|
74
|
+
all_edges = set(
|
|
75
|
+
edge
|
|
76
|
+
for fuzzer in campaign.values()
|
|
77
|
+
for trial in fuzzer.values()
|
|
78
|
+
for edge in trial.keys()
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
fuzzers_that_never_hit_edge: dict[int, set[str]] = {
|
|
82
|
+
edge: set(
|
|
83
|
+
fuzzer
|
|
84
|
+
for fuzzer, trials in campaign.items()
|
|
85
|
+
if not any(trial.get(edge, 0) for trial in trials.values())
|
|
86
|
+
)
|
|
87
|
+
for edge in all_edges
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
scores = {
|
|
91
|
+
fuzzer: calculate_fuzzer_score(trials, all_edges, fuzzers_that_never_hit_edge)
|
|
92
|
+
for fuzzer, trials in campaign.items()
|
|
93
|
+
}
|
|
94
|
+
return scores
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def read_campaign_and_calculate_score(root: Path) -> dict[str, float]:
|
|
98
|
+
if not root.is_dir():
|
|
99
|
+
raise ValueError(f"Not a directory: {root}")
|
|
100
|
+
|
|
101
|
+
campaign_data = read_campaign_dir(root)
|
|
102
|
+
return calculate_differential_coverage_scores(campaign_data)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
p = argparse.ArgumentParser(
|
|
107
|
+
description="Compute differential coverage from afl-showmap coverage dirs."
|
|
108
|
+
)
|
|
109
|
+
p.add_argument(
|
|
110
|
+
"dir",
|
|
111
|
+
type=Path,
|
|
112
|
+
help="Directory containing one subdir per fuzzer with coverage files",
|
|
113
|
+
)
|
|
114
|
+
args = p.parse_args()
|
|
115
|
+
|
|
116
|
+
root = args.dir.resolve()
|
|
117
|
+
scores = read_campaign_and_calculate_score(root)
|
|
118
|
+
for fuzzer, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
|
|
119
|
+
print(f"{fuzzer}: {score:.2f}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: differential_coverage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compute differential coverage for fuzzer runs.
|
|
5
|
+
Author-email: Valentin Huber <contact@valentinhuber.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/valentinhuber/differential_coverage
|
|
8
|
+
Project-URL: Documentation, https://github.com/valentinhuber/differential_coverage#readme
|
|
9
|
+
Keywords: fuzzing,coverage,differential-coverage
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: mypy>=1.19.1; extra == "dev"
|
|
17
|
+
Requires-Dist: pre-commit>=4.3.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.15.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# Differential Coverage
|
|
22
|
+
|
|
23
|
+
Absolute coverage numbers aren't painting the whole picture: Different fuzzers may reach the same total coverage, but cover different parts of the program. Fuzzer (A) may reach more coverage than fuzzer (B), but (B) may reach coverage that (A) doesn't reach — so it's still valuable.
|
|
24
|
+
|
|
25
|
+
## Definition
|
|
26
|
+
|
|
27
|
+
Differential coverage is a measure for this. It was proposed by Leonelli et al. in their paper on TwinFuzz[^1]. This implementation relies on the formulas from the SBFT’25 Competition Report[^2]. There, Crump et al. define a fuzzers overall score compared to others as
|
|
28
|
+
|
|
29
|
+
$$
|
|
30
|
+
relscore(f,b,s,e) = \left|f_0\in F|e\notin \text{cov}(b,t,s)\forall t\in \text{trials}(f_0,b)\right|
|
|
31
|
+
\times\frac
|
|
32
|
+
{\left|\{t\in \text{trials}(f,b)\ |\ e \in \text{cov}(b,t,s)\}\right|}
|
|
33
|
+
{\left|\{t\in \text{trials}(f,b)\ |\ \text{cov}(b,t,s)\neq \emptyset\}\right|}
|
|
34
|
+
$$
|
|
35
|
+
|
|
36
|
+
The score of a fuzzer is then
|
|
37
|
+
|
|
38
|
+
$$
|
|
39
|
+
\text{score}(f,b,s)=\sum_{e\in E}\text{relscore}(f,b,s,e)
|
|
40
|
+
$$
|
|
41
|
+
|
|
42
|
+
## Explanation
|
|
43
|
+
This can be simplified to the following:
|
|
44
|
+
|
|
45
|
+
$$
|
|
46
|
+
\text{differential coverage}(f,e) = \text{number of fuzzers that never hit }e
|
|
47
|
+
\times\frac
|
|
48
|
+
{\text{number of trials of }f\text{ that hit }e}
|
|
49
|
+
{\text{number of trials of }f\text{ with non-empty cov}}
|
|
50
|
+
$$
|
|
51
|
+
$$
|
|
52
|
+
\text{score}(f) = \text{sum of differential coverage}(f,e)\text{ over all edges e}
|
|
53
|
+
$$
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
Assume `<input_dir>` is a directory of directories for each fuzzer, where each fuzzer subdirectory contains coverage files for each trial. Currently, the output format of `afl-showmap` is supported (lines with `edge_id:count`, where we only care if `count > 0`).
|
|
57
|
+
|
|
58
|
+
PRs for other data formats welcome :)
|
|
59
|
+
|
|
60
|
+
### Command Line Interface
|
|
61
|
+
```
|
|
62
|
+
differential_coverage <input_dir>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Example
|
|
66
|
+
[`tests/sample_coverage`](./tests/sample_coverage/) provides sample coverage data to explain differential coverage. It contains 3 fuzzers and 3 edges:
|
|
67
|
+
- Edge 1 is always hit by all fuzzers
|
|
68
|
+
- Edge 2 is always hit by `fuzzer_c`, sometimes hit by `fuzzer_a`, and never hit by `fuzzer_b`
|
|
69
|
+
- Edge 3 is always hit by `fuzzer_b` and `fuzzer_c`, but only sometimes by `fuzzer_a`
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
differential_coverage tests/sample_coverage
|
|
73
|
+
```
|
|
74
|
+
then produces
|
|
75
|
+
```
|
|
76
|
+
fuzzer_c: 1.00
|
|
77
|
+
fuzzer_a: 0.50
|
|
78
|
+
fuzzer_b: 0.00
|
|
79
|
+
```
|
|
80
|
+
### API
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
from differential_coverage import (
|
|
85
|
+
calculate_scores_for_campaign,
|
|
86
|
+
calculate_differential_coverage_scores,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# From a campaign directory (one subdir per fuzzer, each with coverage files):
|
|
90
|
+
scores = read_campaign_and_calculate_score(Path("path/to/campaign_dir"))
|
|
91
|
+
# -> dict[str, float]: fuzzer name -> score
|
|
92
|
+
|
|
93
|
+
# From in-memory campaign data (fuzzer -> trial_id -> edge_id -> count):
|
|
94
|
+
campaign = {...} # dict[str, dict[str, dict[int, int]]]
|
|
95
|
+
scores = calculate_differential_coverage_scores(campaign)
|
|
96
|
+
# -> dict[str, float]: fuzzer name -> score
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
```bash
|
|
101
|
+
pip install .
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
Install dev dependencies and set up the pre-commit hook (runs a couple of checks before committing):
|
|
106
|
+
```bash
|
|
107
|
+
pip install -e ".[dev]"
|
|
108
|
+
pre-commit install
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
[^1]: TWINFUZZ: Differential Testing of Video Hardware Acceleration Stacks, https://www.ndss-symposium.org/ndss-paper/twinfuzz-differential-testing-of-video-hardware-acceleration-stacks/
|
|
112
|
+
|
|
113
|
+
[^2]: SBFT’25 Competition Report — Fuzzing Track, https://ieeexplore.ieee.org/document/11086561
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
differential_coverage/__init__.py,sha256=ohz_zTNNLcUOYgjSZT42MXrFmrNKX26wbTAFZppuOmc,4005
|
|
2
|
+
differential_coverage-0.1.0.dist-info/METADATA,sha256=MtXIDaJjjnqF7XU56SRUSYnFJ-6Czh3hojhtRqeR1f0,4130
|
|
3
|
+
differential_coverage-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
4
|
+
differential_coverage-0.1.0.dist-info/entry_points.txt,sha256=RDuS6NY1W0lkv1cqyZtILrYcKzriadKBaqIPcf_fl4I,69
|
|
5
|
+
differential_coverage-0.1.0.dist-info/top_level.txt,sha256=LBXwH0S9KR4pQuypS4SrY7OBD7rvX9Ys0cM0fRQE0Hc,22
|
|
6
|
+
differential_coverage-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
differential_coverage
|