mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-12 18:18:08 +00:00
add regression testing tasks (#664)
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -358,6 +358,10 @@ jobs:
|
|||||||
(cd libfuzzer ; make )
|
(cd libfuzzer ; make )
|
||||||
cp -r libfuzzer/fuzz.exe libfuzzer/seeds artifacts/linux-libfuzzer
|
cp -r libfuzzer/fuzz.exe libfuzzer/seeds artifacts/linux-libfuzzer
|
||||||
|
|
||||||
|
mkdir -p artifacts/linux-libfuzzer-regression
|
||||||
|
(cd libfuzzer-regression ; make )
|
||||||
|
cp -r libfuzzer-regression/broken.exe libfuzzer-regression/fixed.exe libfuzzer-regression/seeds artifacts/linux-libfuzzer-regression
|
||||||
|
|
||||||
mkdir -p artifacts/linux-trivial-crash
|
mkdir -p artifacts/linux-trivial-crash
|
||||||
(cd trivial-crash ; make )
|
(cd trivial-crash ; make )
|
||||||
cp -r trivial-crash/fuzz.exe trivial-crash/seeds artifacts/linux-trivial-crash
|
cp -r trivial-crash/fuzz.exe trivial-crash/seeds artifacts/linux-trivial-crash
|
||||||
|
@ -37,6 +37,7 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
* [proxy_created](#proxy_created)
|
* [proxy_created](#proxy_created)
|
||||||
* [proxy_deleted](#proxy_deleted)
|
* [proxy_deleted](#proxy_deleted)
|
||||||
* [proxy_failed](#proxy_failed)
|
* [proxy_failed](#proxy_failed)
|
||||||
|
* [regression_reported](#regression_reported)
|
||||||
* [scaleset_created](#scaleset_created)
|
* [scaleset_created](#scaleset_created)
|
||||||
* [scaleset_deleted](#scaleset_deleted)
|
* [scaleset_deleted](#scaleset_deleted)
|
||||||
* [scaleset_failed](#scaleset_failed)
|
* [scaleset_failed](#scaleset_failed)
|
||||||
@ -168,7 +169,6 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"input_blob",
|
|
||||||
"executable",
|
"executable",
|
||||||
"crash_type",
|
"crash_type",
|
||||||
"crash_site",
|
"crash_site",
|
||||||
@ -475,11 +475,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"libfuzzer_coverage",
|
"libfuzzer_coverage",
|
||||||
"libfuzzer_crash_report",
|
"libfuzzer_crash_report",
|
||||||
"libfuzzer_merge",
|
"libfuzzer_merge",
|
||||||
|
"libfuzzer_regression",
|
||||||
"generic_analysis",
|
"generic_analysis",
|
||||||
"generic_supervisor",
|
"generic_supervisor",
|
||||||
"generic_merge",
|
"generic_merge",
|
||||||
"generic_generator",
|
"generic_generator",
|
||||||
"generic_crash_report"
|
"generic_crash_report",
|
||||||
|
"generic_regression"
|
||||||
],
|
],
|
||||||
"title": "TaskType"
|
"title": "TaskType"
|
||||||
},
|
},
|
||||||
@ -1030,6 +1032,261 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### regression_reported
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"container": "container-name",
|
||||||
|
"filename": "example.json",
|
||||||
|
"regression_report": {
|
||||||
|
"crash_test_result": {
|
||||||
|
"crash_report": {
|
||||||
|
"asan_log": "example asan log",
|
||||||
|
"call_stack": [
|
||||||
|
"#0 line",
|
||||||
|
"#1 line",
|
||||||
|
"#2 line"
|
||||||
|
],
|
||||||
|
"call_stack_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"crash_site": "example crash site",
|
||||||
|
"crash_type": "example crash report type",
|
||||||
|
"executable": "fuzz.exe",
|
||||||
|
"input_blob": {
|
||||||
|
"account": "contoso-storage-account",
|
||||||
|
"container": "crashes",
|
||||||
|
"name": "input.txt"
|
||||||
|
},
|
||||||
|
"input_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"scariness_description": "example-scariness",
|
||||||
|
"scariness_score": 10,
|
||||||
|
"task_id": "00000000-0000-0000-0000-000000000000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"original_crash_test_result": {
|
||||||
|
"crash_report": {
|
||||||
|
"asan_log": "example asan log",
|
||||||
|
"call_stack": [
|
||||||
|
"#0 line",
|
||||||
|
"#1 line",
|
||||||
|
"#2 line"
|
||||||
|
],
|
||||||
|
"call_stack_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"crash_site": "example crash site",
|
||||||
|
"crash_type": "example crash report type",
|
||||||
|
"executable": "fuzz.exe",
|
||||||
|
"input_blob": {
|
||||||
|
"account": "contoso-storage-account",
|
||||||
|
"container": "crashes",
|
||||||
|
"name": "input.txt"
|
||||||
|
},
|
||||||
|
"input_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"scariness_description": "example-scariness",
|
||||||
|
"scariness_score": 10,
|
||||||
|
"task_id": "00000000-0000-0000-0000-000000000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"definitions": {
|
||||||
|
"BlobRef": {
|
||||||
|
"properties": {
|
||||||
|
"account": {
|
||||||
|
"title": "Account",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"container": {
|
||||||
|
"title": "Container",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"account",
|
||||||
|
"container",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"title": "BlobRef",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"CrashTestResult": {
|
||||||
|
"properties": {
|
||||||
|
"crash_report": {
|
||||||
|
"$ref": "#/definitions/Report"
|
||||||
|
},
|
||||||
|
"no_repro": {
|
||||||
|
"$ref": "#/definitions/NoReproReport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "CrashTestResult",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NoReproReport": {
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"title": "Error",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"executable": {
|
||||||
|
"title": "Executable",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"input_blob": {
|
||||||
|
"$ref": "#/definitions/BlobRef"
|
||||||
|
},
|
||||||
|
"input_sha256": {
|
||||||
|
"title": "Input Sha256",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"job_id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Job Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"task_id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Task Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tries": {
|
||||||
|
"title": "Tries",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"input_sha256",
|
||||||
|
"executable",
|
||||||
|
"task_id",
|
||||||
|
"job_id",
|
||||||
|
"tries"
|
||||||
|
],
|
||||||
|
"title": "NoReproReport",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RegressionReport": {
|
||||||
|
"properties": {
|
||||||
|
"crash_test_result": {
|
||||||
|
"$ref": "#/definitions/CrashTestResult"
|
||||||
|
},
|
||||||
|
"original_crash_test_result": {
|
||||||
|
"$ref": "#/definitions/CrashTestResult"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"crash_test_result"
|
||||||
|
],
|
||||||
|
"title": "RegressionReport",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"Report": {
|
||||||
|
"properties": {
|
||||||
|
"asan_log": {
|
||||||
|
"title": "Asan Log",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"call_stack": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Call Stack",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"call_stack_sha256": {
|
||||||
|
"title": "Call Stack Sha256",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"crash_site": {
|
||||||
|
"title": "Crash Site",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"crash_type": {
|
||||||
|
"title": "Crash Type",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"executable": {
|
||||||
|
"title": "Executable",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"input_blob": {
|
||||||
|
"$ref": "#/definitions/BlobRef"
|
||||||
|
},
|
||||||
|
"input_sha256": {
|
||||||
|
"title": "Input Sha256",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"input_url": {
|
||||||
|
"title": "Input Url",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"job_id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Job Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"scariness_description": {
|
||||||
|
"title": "Scariness Description",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"scariness_score": {
|
||||||
|
"title": "Scariness Score",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"task_id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Task Id",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"executable",
|
||||||
|
"crash_type",
|
||||||
|
"crash_site",
|
||||||
|
"call_stack",
|
||||||
|
"call_stack_sha256",
|
||||||
|
"input_sha256",
|
||||||
|
"task_id",
|
||||||
|
"job_id"
|
||||||
|
],
|
||||||
|
"title": "Report",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"container": {
|
||||||
|
"title": "Container",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"title": "Filename",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"regression_report": {
|
||||||
|
"$ref": "#/definitions/RegressionReport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"regression_report",
|
||||||
|
"container",
|
||||||
|
"filename"
|
||||||
|
],
|
||||||
|
"title": "EventRegressionReported",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### scaleset_created
|
### scaleset_created
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
@ -1283,7 +1540,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"setup",
|
"setup",
|
||||||
"tools",
|
"tools",
|
||||||
"unique_inputs",
|
"unique_inputs",
|
||||||
"unique_reports"
|
"unique_reports",
|
||||||
|
"regression_reports"
|
||||||
],
|
],
|
||||||
"title": "ContainerType"
|
"title": "ContainerType"
|
||||||
},
|
},
|
||||||
@ -1456,6 +1714,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "Rename Output",
|
"title": "Rename Output",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"report_list": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Report List",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"stats_file": {
|
"stats_file": {
|
||||||
"title": "Stats File",
|
"title": "Stats File",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -1554,11 +1819,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"libfuzzer_coverage",
|
"libfuzzer_coverage",
|
||||||
"libfuzzer_crash_report",
|
"libfuzzer_crash_report",
|
||||||
"libfuzzer_merge",
|
"libfuzzer_merge",
|
||||||
|
"libfuzzer_regression",
|
||||||
"generic_analysis",
|
"generic_analysis",
|
||||||
"generic_supervisor",
|
"generic_supervisor",
|
||||||
"generic_merge",
|
"generic_merge",
|
||||||
"generic_generator",
|
"generic_generator",
|
||||||
"generic_crash_report"
|
"generic_crash_report",
|
||||||
|
"generic_regression"
|
||||||
],
|
],
|
||||||
"title": "TaskType"
|
"title": "TaskType"
|
||||||
},
|
},
|
||||||
@ -1715,7 +1982,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"setup",
|
"setup",
|
||||||
"tools",
|
"tools",
|
||||||
"unique_inputs",
|
"unique_inputs",
|
||||||
"unique_reports"
|
"unique_reports",
|
||||||
|
"regression_reports"
|
||||||
],
|
],
|
||||||
"title": "ContainerType"
|
"title": "ContainerType"
|
||||||
},
|
},
|
||||||
@ -1936,6 +2204,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "Rename Output",
|
"title": "Rename Output",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"report_list": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Report List",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"stats_file": {
|
"stats_file": {
|
||||||
"title": "Stats File",
|
"title": "Stats File",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -2034,11 +2309,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"libfuzzer_coverage",
|
"libfuzzer_coverage",
|
||||||
"libfuzzer_crash_report",
|
"libfuzzer_crash_report",
|
||||||
"libfuzzer_merge",
|
"libfuzzer_merge",
|
||||||
|
"libfuzzer_regression",
|
||||||
"generic_analysis",
|
"generic_analysis",
|
||||||
"generic_supervisor",
|
"generic_supervisor",
|
||||||
"generic_merge",
|
"generic_merge",
|
||||||
"generic_generator",
|
"generic_generator",
|
||||||
"generic_crash_report"
|
"generic_crash_report",
|
||||||
|
"generic_regression"
|
||||||
],
|
],
|
||||||
"title": "TaskType"
|
"title": "TaskType"
|
||||||
},
|
},
|
||||||
@ -2188,7 +2465,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"setup",
|
"setup",
|
||||||
"tools",
|
"tools",
|
||||||
"unique_inputs",
|
"unique_inputs",
|
||||||
"unique_reports"
|
"unique_reports",
|
||||||
|
"regression_reports"
|
||||||
],
|
],
|
||||||
"title": "ContainerType"
|
"title": "ContainerType"
|
||||||
},
|
},
|
||||||
@ -2361,6 +2639,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "Rename Output",
|
"title": "Rename Output",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"report_list": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Report List",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"stats_file": {
|
"stats_file": {
|
||||||
"title": "Stats File",
|
"title": "Stats File",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -2459,11 +2744,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"libfuzzer_coverage",
|
"libfuzzer_coverage",
|
||||||
"libfuzzer_crash_report",
|
"libfuzzer_crash_report",
|
||||||
"libfuzzer_merge",
|
"libfuzzer_merge",
|
||||||
|
"libfuzzer_regression",
|
||||||
"generic_analysis",
|
"generic_analysis",
|
||||||
"generic_supervisor",
|
"generic_supervisor",
|
||||||
"generic_merge",
|
"generic_merge",
|
||||||
"generic_generator",
|
"generic_generator",
|
||||||
"generic_crash_report"
|
"generic_crash_report",
|
||||||
|
"generic_regression"
|
||||||
],
|
],
|
||||||
"title": "TaskType"
|
"title": "TaskType"
|
||||||
},
|
},
|
||||||
@ -2587,7 +2874,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"setup",
|
"setup",
|
||||||
"tools",
|
"tools",
|
||||||
"unique_inputs",
|
"unique_inputs",
|
||||||
"unique_reports"
|
"unique_reports",
|
||||||
|
"regression_reports"
|
||||||
],
|
],
|
||||||
"title": "ContainerType"
|
"title": "ContainerType"
|
||||||
},
|
},
|
||||||
@ -2760,6 +3048,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "Rename Output",
|
"title": "Rename Output",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"report_list": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Report List",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"stats_file": {
|
"stats_file": {
|
||||||
"title": "Stats File",
|
"title": "Stats File",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -2872,11 +3167,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"libfuzzer_coverage",
|
"libfuzzer_coverage",
|
||||||
"libfuzzer_crash_report",
|
"libfuzzer_crash_report",
|
||||||
"libfuzzer_merge",
|
"libfuzzer_merge",
|
||||||
|
"libfuzzer_regression",
|
||||||
"generic_analysis",
|
"generic_analysis",
|
||||||
"generic_supervisor",
|
"generic_supervisor",
|
||||||
"generic_merge",
|
"generic_merge",
|
||||||
"generic_generator",
|
"generic_generator",
|
||||||
"generic_crash_report"
|
"generic_crash_report",
|
||||||
|
"generic_regression"
|
||||||
],
|
],
|
||||||
"title": "TaskType"
|
"title": "TaskType"
|
||||||
},
|
},
|
||||||
@ -3013,7 +3310,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"setup",
|
"setup",
|
||||||
"tools",
|
"tools",
|
||||||
"unique_inputs",
|
"unique_inputs",
|
||||||
"unique_reports"
|
"unique_reports",
|
||||||
|
"regression_reports"
|
||||||
],
|
],
|
||||||
"title": "ContainerType"
|
"title": "ContainerType"
|
||||||
},
|
},
|
||||||
@ -3186,6 +3484,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "Rename Output",
|
"title": "Rename Output",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"report_list": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Report List",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"stats_file": {
|
"stats_file": {
|
||||||
"title": "Stats File",
|
"title": "Stats File",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -3284,11 +3589,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"libfuzzer_coverage",
|
"libfuzzer_coverage",
|
||||||
"libfuzzer_crash_report",
|
"libfuzzer_crash_report",
|
||||||
"libfuzzer_merge",
|
"libfuzzer_merge",
|
||||||
|
"libfuzzer_regression",
|
||||||
"generic_analysis",
|
"generic_analysis",
|
||||||
"generic_supervisor",
|
"generic_supervisor",
|
||||||
"generic_merge",
|
"generic_merge",
|
||||||
"generic_generator",
|
"generic_generator",
|
||||||
"generic_crash_report"
|
"generic_crash_report",
|
||||||
|
"generic_regression"
|
||||||
],
|
],
|
||||||
"title": "TaskType"
|
"title": "TaskType"
|
||||||
},
|
},
|
||||||
@ -3468,10 +3775,23 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"setup",
|
"setup",
|
||||||
"tools",
|
"tools",
|
||||||
"unique_inputs",
|
"unique_inputs",
|
||||||
"unique_reports"
|
"unique_reports",
|
||||||
|
"regression_reports"
|
||||||
],
|
],
|
||||||
"title": "ContainerType"
|
"title": "ContainerType"
|
||||||
},
|
},
|
||||||
|
"CrashTestResult": {
|
||||||
|
"properties": {
|
||||||
|
"crash_report": {
|
||||||
|
"$ref": "#/definitions/Report"
|
||||||
|
},
|
||||||
|
"no_repro": {
|
||||||
|
"$ref": "#/definitions/NoReproReport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "CrashTestResult",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"Error": {
|
"Error": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"code": {
|
"code": {
|
||||||
@ -3821,6 +4141,29 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "EventProxyFailed",
|
"title": "EventProxyFailed",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"EventRegressionReported": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"container": {
|
||||||
|
"title": "Container",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"title": "Filename",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"regression_report": {
|
||||||
|
"$ref": "#/definitions/RegressionReport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"regression_report",
|
||||||
|
"container",
|
||||||
|
"filename"
|
||||||
|
],
|
||||||
|
"title": "EventRegressionReported",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"EventScalesetCreated": {
|
"EventScalesetCreated": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -4074,6 +4417,7 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"task_state_updated",
|
"task_state_updated",
|
||||||
"task_stopped",
|
"task_stopped",
|
||||||
"crash_reported",
|
"crash_reported",
|
||||||
|
"regression_reported",
|
||||||
"file_added",
|
"file_added",
|
||||||
"task_heartbeat",
|
"task_heartbeat",
|
||||||
"node_heartbeat"
|
"node_heartbeat"
|
||||||
@ -4129,6 +4473,48 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "JobTaskStopped",
|
"title": "JobTaskStopped",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"NoReproReport": {
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"title": "Error",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"executable": {
|
||||||
|
"title": "Executable",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"input_blob": {
|
||||||
|
"$ref": "#/definitions/BlobRef"
|
||||||
|
},
|
||||||
|
"input_sha256": {
|
||||||
|
"title": "Input Sha256",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"job_id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Job Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"task_id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Task Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tries": {
|
||||||
|
"title": "Tries",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"input_sha256",
|
||||||
|
"executable",
|
||||||
|
"task_id",
|
||||||
|
"job_id",
|
||||||
|
"tries"
|
||||||
|
],
|
||||||
|
"title": "NoReproReport",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"NodeState": {
|
"NodeState": {
|
||||||
"description": "An enumeration.",
|
"description": "An enumeration.",
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -4152,6 +4538,21 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
],
|
],
|
||||||
"title": "OS"
|
"title": "OS"
|
||||||
},
|
},
|
||||||
|
"RegressionReport": {
|
||||||
|
"properties": {
|
||||||
|
"crash_test_result": {
|
||||||
|
"$ref": "#/definitions/CrashTestResult"
|
||||||
|
},
|
||||||
|
"original_crash_test_result": {
|
||||||
|
"$ref": "#/definitions/CrashTestResult"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"crash_test_result"
|
||||||
|
],
|
||||||
|
"title": "RegressionReport",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"Report": {
|
"Report": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"asan_log": {
|
"asan_log": {
|
||||||
@ -4212,7 +4613,6 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"input_blob",
|
|
||||||
"executable",
|
"executable",
|
||||||
"crash_type",
|
"crash_type",
|
||||||
"crash_site",
|
"crash_site",
|
||||||
@ -4394,6 +4794,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"title": "Rename Output",
|
"title": "Rename Output",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"report_list": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Report List",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"stats_file": {
|
"stats_file": {
|
||||||
"title": "Stats File",
|
"title": "Stats File",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -4506,11 +4913,13 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
"libfuzzer_coverage",
|
"libfuzzer_coverage",
|
||||||
"libfuzzer_crash_report",
|
"libfuzzer_crash_report",
|
||||||
"libfuzzer_merge",
|
"libfuzzer_merge",
|
||||||
|
"libfuzzer_regression",
|
||||||
"generic_analysis",
|
"generic_analysis",
|
||||||
"generic_supervisor",
|
"generic_supervisor",
|
||||||
"generic_merge",
|
"generic_merge",
|
||||||
"generic_generator",
|
"generic_generator",
|
||||||
"generic_crash_report"
|
"generic_crash_report",
|
||||||
|
"generic_regression"
|
||||||
],
|
],
|
||||||
"title": "TaskType"
|
"title": "TaskType"
|
||||||
},
|
},
|
||||||
@ -4638,6 +5047,9 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
|||||||
{
|
{
|
||||||
"$ref": "#/definitions/EventCrashReported"
|
"$ref": "#/definitions/EventCrashReported"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/EventRegressionReported"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/EventFileAdded"
|
"$ref": "#/definitions/EventFileAdded"
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
// Licensed under the MIT License.
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
#![allow(clippy::large_enum_variant)]
|
#![allow(clippy::large_enum_variant)]
|
||||||
use crate::tasks::{analysis, coverage, fuzz, heartbeat::*, merge, report};
|
use crate::tasks::{
|
||||||
|
analysis, coverage, fuzz,
|
||||||
|
heartbeat::{init_task_heartbeat, TaskHeartbeatClient},
|
||||||
|
merge, regression, report,
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use onefuzz::machine_id::{get_machine_id, get_scaleset_name};
|
use onefuzz::machine_id::{get_machine_id, get_scaleset_name};
|
||||||
use onefuzz_telemetry::{
|
use onefuzz_telemetry::{
|
||||||
@ -70,6 +74,9 @@ pub enum Config {
|
|||||||
#[serde(alias = "libfuzzer_coverage")]
|
#[serde(alias = "libfuzzer_coverage")]
|
||||||
LibFuzzerCoverage(coverage::libfuzzer_coverage::Config),
|
LibFuzzerCoverage(coverage::libfuzzer_coverage::Config),
|
||||||
|
|
||||||
|
#[serde(alias = "libfuzzer_regression")]
|
||||||
|
LibFuzzerRegression(regression::libfuzzer::Config),
|
||||||
|
|
||||||
#[serde(alias = "generic_analysis")]
|
#[serde(alias = "generic_analysis")]
|
||||||
GenericAnalysis(analysis::generic::Config),
|
GenericAnalysis(analysis::generic::Config),
|
||||||
|
|
||||||
@ -84,6 +91,9 @@ pub enum Config {
|
|||||||
|
|
||||||
#[serde(alias = "generic_crash_report")]
|
#[serde(alias = "generic_crash_report")]
|
||||||
GenericReport(report::generic::Config),
|
GenericReport(report::generic::Config),
|
||||||
|
|
||||||
|
#[serde(alias = "generic_regression")]
|
||||||
|
GenericRegression(regression::generic::Config),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@ -104,11 +114,13 @@ impl Config {
|
|||||||
Config::LibFuzzerMerge(c) => &mut c.common,
|
Config::LibFuzzerMerge(c) => &mut c.common,
|
||||||
Config::LibFuzzerReport(c) => &mut c.common,
|
Config::LibFuzzerReport(c) => &mut c.common,
|
||||||
Config::LibFuzzerCoverage(c) => &mut c.common,
|
Config::LibFuzzerCoverage(c) => &mut c.common,
|
||||||
|
Config::LibFuzzerRegression(c) => &mut c.common,
|
||||||
Config::GenericAnalysis(c) => &mut c.common,
|
Config::GenericAnalysis(c) => &mut c.common,
|
||||||
Config::GenericMerge(c) => &mut c.common,
|
Config::GenericMerge(c) => &mut c.common,
|
||||||
Config::GenericReport(c) => &mut c.common,
|
Config::GenericReport(c) => &mut c.common,
|
||||||
Config::GenericSupervisor(c) => &mut c.common,
|
Config::GenericSupervisor(c) => &mut c.common,
|
||||||
Config::GenericGenerator(c) => &mut c.common,
|
Config::GenericGenerator(c) => &mut c.common,
|
||||||
|
Config::GenericRegression(c) => &mut c.common,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,11 +130,13 @@ impl Config {
|
|||||||
Config::LibFuzzerMerge(c) => &c.common,
|
Config::LibFuzzerMerge(c) => &c.common,
|
||||||
Config::LibFuzzerReport(c) => &c.common,
|
Config::LibFuzzerReport(c) => &c.common,
|
||||||
Config::LibFuzzerCoverage(c) => &c.common,
|
Config::LibFuzzerCoverage(c) => &c.common,
|
||||||
|
Config::LibFuzzerRegression(c) => &c.common,
|
||||||
Config::GenericAnalysis(c) => &c.common,
|
Config::GenericAnalysis(c) => &c.common,
|
||||||
Config::GenericMerge(c) => &c.common,
|
Config::GenericMerge(c) => &c.common,
|
||||||
Config::GenericReport(c) => &c.common,
|
Config::GenericReport(c) => &c.common,
|
||||||
Config::GenericSupervisor(c) => &c.common,
|
Config::GenericSupervisor(c) => &c.common,
|
||||||
Config::GenericGenerator(c) => &c.common,
|
Config::GenericGenerator(c) => &c.common,
|
||||||
|
Config::GenericRegression(c) => &c.common,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,11 +146,13 @@ impl Config {
|
|||||||
Config::LibFuzzerMerge(_) => "libfuzzer_merge",
|
Config::LibFuzzerMerge(_) => "libfuzzer_merge",
|
||||||
Config::LibFuzzerReport(_) => "libfuzzer_crash_report",
|
Config::LibFuzzerReport(_) => "libfuzzer_crash_report",
|
||||||
Config::LibFuzzerCoverage(_) => "libfuzzer_coverage",
|
Config::LibFuzzerCoverage(_) => "libfuzzer_coverage",
|
||||||
|
Config::LibFuzzerRegression(_) => "libfuzzer_regression",
|
||||||
Config::GenericAnalysis(_) => "generic_analysis",
|
Config::GenericAnalysis(_) => "generic_analysis",
|
||||||
Config::GenericMerge(_) => "generic_merge",
|
Config::GenericMerge(_) => "generic_merge",
|
||||||
Config::GenericReport(_) => "generic_crash_report",
|
Config::GenericReport(_) => "generic_crash_report",
|
||||||
Config::GenericSupervisor(_) => "generic_supervisor",
|
Config::GenericSupervisor(_) => "generic_supervisor",
|
||||||
Config::GenericGenerator(_) => "generic_generator",
|
Config::GenericGenerator(_) => "generic_generator",
|
||||||
|
Config::GenericRegression(_) => "generic_regression",
|
||||||
};
|
};
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
@ -193,6 +209,16 @@ impl Config {
|
|||||||
Config::GenericReport(config) => {
|
Config::GenericReport(config) => {
|
||||||
report::generic::ReportTask::new(config).managed_run().await
|
report::generic::ReportTask::new(config).managed_run().await
|
||||||
}
|
}
|
||||||
|
Config::GenericRegression(config) => {
|
||||||
|
regression::generic::GenericRegressionTask::new(config)
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Config::LibFuzzerRegression(config) => {
|
||||||
|
regression::libfuzzer::LibFuzzerRegressionTask::new(config)
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use crate::tasks::{
|
use crate::tasks::{
|
||||||
config::CommonConfig,
|
config::CommonConfig,
|
||||||
heartbeat::*,
|
heartbeat::{HeartbeatSender, TaskHeartbeatClient},
|
||||||
utils::{self, default_bool_true},
|
utils::{self, default_bool_true},
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
use crate::tasks::{
|
use crate::tasks::{
|
||||||
config::{CommonConfig, ContainerType},
|
config::{CommonConfig, ContainerType},
|
||||||
heartbeat::*,
|
heartbeat::{HeartbeatSender, TaskHeartbeatClient},
|
||||||
report::crash_report::monitor_reports,
|
report::crash_report::monitor_reports,
|
||||||
stats::common::{monitor_stats, StatsFormat},
|
stats::common::{monitor_stats, StatsFormat},
|
||||||
utils::CheckNotify,
|
utils::CheckNotify,
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use crate::tasks::{
|
use crate::tasks::{
|
||||||
config::CommonConfig,
|
config::CommonConfig,
|
||||||
heartbeat::*,
|
heartbeat::HeartbeatSender,
|
||||||
utils::{self, default_bool_true},
|
utils::{self, default_bool_true},
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
@ -8,6 +8,7 @@ pub mod fuzz;
|
|||||||
pub mod generic;
|
pub mod generic;
|
||||||
pub mod heartbeat;
|
pub mod heartbeat;
|
||||||
pub mod merge;
|
pub mod merge;
|
||||||
|
pub mod regression;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
162
src/agent/onefuzz-agent/src/tasks/regression/common.rs
Normal file
162
src/agent/onefuzz-agent/src/tasks/regression/common.rs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
use crate::tasks::{
|
||||||
|
heartbeat::{HeartbeatSender, TaskHeartbeatClient},
|
||||||
|
report::crash_report::{parse_report_file, CrashTestResult, RegressionReport},
|
||||||
|
};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use onefuzz::syncdir::SyncedDir;
|
||||||
|
use reqwest::Url;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Defines implementation-provided callbacks for all implementers of regression tasks.
|
||||||
|
///
|
||||||
|
/// Shared regression task behavior is implemented in this module.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RegressionHandler {
|
||||||
|
/// Test the provided input and generate a crash result
|
||||||
|
/// * `input` - path to the input to test
|
||||||
|
/// * `input_url` - input url
|
||||||
|
async fn get_crash_result(&self, input: PathBuf, input_url: Url) -> Result<CrashTestResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the regression task
|
||||||
|
pub async fn run(
|
||||||
|
heartbeat_client: Option<TaskHeartbeatClient>,
|
||||||
|
regression_reports: &SyncedDir,
|
||||||
|
crashes: &SyncedDir,
|
||||||
|
report_dirs: &[&SyncedDir],
|
||||||
|
report_list: &Option<Vec<String>>,
|
||||||
|
readonly_inputs: &Option<SyncedDir>,
|
||||||
|
handler: &impl RegressionHandler,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("Starting generic regression task");
|
||||||
|
handle_crash_reports(
|
||||||
|
handler,
|
||||||
|
crashes,
|
||||||
|
report_dirs,
|
||||||
|
report_list,
|
||||||
|
®ression_reports,
|
||||||
|
&heartbeat_client,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(readonly_inputs) = &readonly_inputs {
|
||||||
|
handle_inputs(
|
||||||
|
handler,
|
||||||
|
readonly_inputs,
|
||||||
|
®ression_reports,
|
||||||
|
&heartbeat_client,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the regression on the files in the 'inputs' location
|
||||||
|
/// * `handler` - regression handler
|
||||||
|
/// * `readonly_inputs` - location of the input files
|
||||||
|
/// * `regression_reports` - where reports should be saved
|
||||||
|
/// * `heartbeat_client` - heartbeat client
|
||||||
|
pub async fn handle_inputs(
|
||||||
|
handler: &impl RegressionHandler,
|
||||||
|
readonly_inputs: &SyncedDir,
|
||||||
|
regression_reports: &SyncedDir,
|
||||||
|
heartbeat_client: &Option<TaskHeartbeatClient>,
|
||||||
|
) -> Result<()> {
|
||||||
|
readonly_inputs.init_pull().await?;
|
||||||
|
let mut input_files = tokio::fs::read_dir(&readonly_inputs.path).await?;
|
||||||
|
while let Some(file) = input_files.next_entry().await? {
|
||||||
|
heartbeat_client.alive();
|
||||||
|
|
||||||
|
let file_path = file.path();
|
||||||
|
if !file_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = file_path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| format_err!("missing filename"))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let input_url = readonly_inputs.url.url().join(&file_name)?;
|
||||||
|
|
||||||
|
let crash_test_result = handler.get_crash_result(file_path, input_url).await?;
|
||||||
|
RegressionReport {
|
||||||
|
crash_test_result,
|
||||||
|
original_crash_test_result: None,
|
||||||
|
}
|
||||||
|
.save(None, regression_reports)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_crash_reports(
|
||||||
|
handler: &impl RegressionHandler,
|
||||||
|
crashes: &SyncedDir,
|
||||||
|
report_dirs: &[&SyncedDir],
|
||||||
|
report_list: &Option<Vec<String>>,
|
||||||
|
regression_reports: &SyncedDir,
|
||||||
|
heartbeat_client: &Option<TaskHeartbeatClient>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// without crash report containers, skip this method
|
||||||
|
if report_dirs.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
crashes.init_pull().await?;
|
||||||
|
|
||||||
|
for possible_dir in report_dirs {
|
||||||
|
possible_dir.init_pull().await?;
|
||||||
|
|
||||||
|
let mut report_files = tokio::fs::read_dir(&possible_dir.path).await?;
|
||||||
|
while let Some(file) = report_files.next_entry().await? {
|
||||||
|
heartbeat_client.alive();
|
||||||
|
let file_path = file.path();
|
||||||
|
if !file_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = file_path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| format_err!("missing filename"))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if let Some(report_list) = &report_list {
|
||||||
|
if !report_list.contains(&file_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let original_crash_test_result = parse_report_file(file.path())
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("unable to parse crash report: {}", file_name))?;
|
||||||
|
|
||||||
|
let input_blob = match &original_crash_test_result {
|
||||||
|
CrashTestResult::CrashReport(x) => x.input_blob.clone(),
|
||||||
|
CrashTestResult::NoRepro(x) => x.input_blob.clone(),
|
||||||
|
}
|
||||||
|
.ok_or_else(|| format_err!("crash report is missing input blob: {}", file_name))?;
|
||||||
|
|
||||||
|
let input_url = crashes.url.blob(&input_blob.name).url();
|
||||||
|
let input = crashes.path.join(&input_blob.name);
|
||||||
|
let crash_test_result = handler.get_crash_result(input, input_url).await?;
|
||||||
|
|
||||||
|
RegressionReport {
|
||||||
|
crash_test_result,
|
||||||
|
original_crash_test_result: Some(original_crash_test_result),
|
||||||
|
}
|
||||||
|
.save(Some(file_name), regression_reports)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
109
src/agent/onefuzz-agent/src/tasks/regression/generic.rs
Normal file
109
src/agent/onefuzz-agent/src/tasks/regression/generic.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
use crate::tasks::{
|
||||||
|
config::CommonConfig,
|
||||||
|
report::{crash_report::CrashTestResult, generic},
|
||||||
|
utils::default_bool_true,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use onefuzz::syncdir::SyncedDir;
|
||||||
|
use reqwest::Url;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
use super::common::{self, RegressionHandler};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub target_exe: PathBuf,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_options: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_env: HashMap<String, String>,
|
||||||
|
|
||||||
|
pub target_timeout: Option<u64>,
|
||||||
|
|
||||||
|
pub crashes: SyncedDir,
|
||||||
|
pub regression_reports: SyncedDir,
|
||||||
|
pub report_list: Option<Vec<String>>,
|
||||||
|
pub reports: Option<SyncedDir>,
|
||||||
|
pub unique_reports: Option<SyncedDir>,
|
||||||
|
pub no_repro: Option<SyncedDir>,
|
||||||
|
pub readonly_inputs: Option<SyncedDir>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub check_asan_log: bool,
|
||||||
|
#[serde(default = "default_bool_true")]
|
||||||
|
pub check_debugger: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub check_retry_count: u64,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub minimized_stack_depth: Option<usize>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: CommonConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GenericRegressionTask {
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RegressionHandler for GenericRegressionTask {
|
||||||
|
async fn get_crash_result(&self, input: PathBuf, input_url: Url) -> Result<CrashTestResult> {
|
||||||
|
let args = generic::TestInputArgs {
|
||||||
|
input_url: Some(input_url),
|
||||||
|
input: &input,
|
||||||
|
target_exe: &self.config.target_exe,
|
||||||
|
target_options: &self.config.target_options,
|
||||||
|
target_env: &self.config.target_env,
|
||||||
|
setup_dir: &self.config.common.setup_dir,
|
||||||
|
task_id: self.config.common.task_id,
|
||||||
|
job_id: self.config.common.job_id,
|
||||||
|
target_timeout: self.config.target_timeout,
|
||||||
|
check_retry_count: self.config.check_retry_count,
|
||||||
|
check_asan_log: self.config.check_asan_log,
|
||||||
|
check_debugger: self.config.check_debugger,
|
||||||
|
minimized_stack_depth: self.config.minimized_stack_depth,
|
||||||
|
};
|
||||||
|
generic::test_input(args).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericRegressionTask {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) -> Result<()> {
|
||||||
|
info!("Starting generic regression task");
|
||||||
|
let heartbeat_client = self.config.common.init_heartbeat().await?;
|
||||||
|
|
||||||
|
let mut report_dirs = vec![];
|
||||||
|
for dir in &[
|
||||||
|
&self.config.reports,
|
||||||
|
&self.config.unique_reports,
|
||||||
|
&self.config.no_repro,
|
||||||
|
] {
|
||||||
|
if let Some(dir) = dir {
|
||||||
|
report_dirs.push(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common::run(
|
||||||
|
heartbeat_client,
|
||||||
|
&self.config.regression_reports,
|
||||||
|
&self.config.crashes,
|
||||||
|
&report_dirs,
|
||||||
|
&self.config.report_list,
|
||||||
|
&self.config.readonly_inputs,
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
107
src/agent/onefuzz-agent/src/tasks/regression/libfuzzer.rs
Normal file
107
src/agent/onefuzz-agent/src/tasks/regression/libfuzzer.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
use crate::tasks::{
|
||||||
|
config::CommonConfig,
|
||||||
|
report::{crash_report::CrashTestResult, libfuzzer_report},
|
||||||
|
utils::default_bool_true,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use reqwest::Url;
|
||||||
|
|
||||||
|
use super::common::{self, RegressionHandler};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use onefuzz::syncdir::SyncedDir;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub target_exe: PathBuf,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_options: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub target_env: HashMap<String, String>,
|
||||||
|
|
||||||
|
pub target_timeout: Option<u64>,
|
||||||
|
|
||||||
|
pub crashes: SyncedDir,
|
||||||
|
pub regression_reports: SyncedDir,
|
||||||
|
pub report_list: Option<Vec<String>>,
|
||||||
|
pub unique_reports: Option<SyncedDir>,
|
||||||
|
pub reports: Option<SyncedDir>,
|
||||||
|
pub no_repro: Option<SyncedDir>,
|
||||||
|
pub readonly_inputs: Option<SyncedDir>,
|
||||||
|
|
||||||
|
#[serde(default = "default_bool_true")]
|
||||||
|
pub check_fuzzer_help: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub check_retry_count: u64,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub minimized_stack_depth: Option<usize>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: CommonConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LibFuzzerRegressionTask {
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RegressionHandler for LibFuzzerRegressionTask {
|
||||||
|
async fn get_crash_result(&self, input: PathBuf, input_url: Url) -> Result<CrashTestResult> {
|
||||||
|
let args = libfuzzer_report::TestInputArgs {
|
||||||
|
input_url: Some(input_url),
|
||||||
|
input: &input,
|
||||||
|
target_exe: &self.config.target_exe,
|
||||||
|
target_options: &self.config.target_options,
|
||||||
|
target_env: &self.config.target_env,
|
||||||
|
setup_dir: &self.config.common.setup_dir,
|
||||||
|
task_id: self.config.common.task_id,
|
||||||
|
job_id: self.config.common.job_id,
|
||||||
|
target_timeout: self.config.target_timeout,
|
||||||
|
check_retry_count: self.config.check_retry_count,
|
||||||
|
minimized_stack_depth: self.config.minimized_stack_depth,
|
||||||
|
};
|
||||||
|
libfuzzer_report::test_input(args).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LibFuzzerRegressionTask {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) -> Result<()> {
|
||||||
|
info!("Starting libfuzzer regression task");
|
||||||
|
|
||||||
|
let mut report_dirs = vec![];
|
||||||
|
for dir in &[
|
||||||
|
&self.config.reports,
|
||||||
|
&self.config.unique_reports,
|
||||||
|
&self.config.no_repro,
|
||||||
|
] {
|
||||||
|
if let Some(dir) = dir {
|
||||||
|
report_dirs.push(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let heartbeat_client = self.config.common.init_heartbeat().await?;
|
||||||
|
common::run(
|
||||||
|
heartbeat_client,
|
||||||
|
&self.config.regression_reports,
|
||||||
|
&self.config.crashes,
|
||||||
|
&report_dirs,
|
||||||
|
&self.config.report_list,
|
||||||
|
&self.config.readonly_inputs,
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
6
src/agent/onefuzz-agent/src/tasks/regression/mod.rs
Normal file
6
src/agent/onefuzz-agent/src/tasks/regression/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
pub mod common;
|
||||||
|
pub mod generic;
|
||||||
|
pub mod libfuzzer;
|
@ -5,7 +5,10 @@ use anyhow::{Context, Result};
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use onefuzz::{blob::BlobUrl, monitor::DirectoryMonitor, syncdir::SyncedDir};
|
use onefuzz::{blob::BlobUrl, monitor::DirectoryMonitor, syncdir::SyncedDir};
|
||||||
use onefuzz_telemetry::{
|
use onefuzz_telemetry::{
|
||||||
Event::{new_report, new_unable_to_reproduce, new_unique_report},
|
Event::{
|
||||||
|
new_report, new_unable_to_reproduce, new_unique_report, regression_report,
|
||||||
|
regression_unable_to_reproduce,
|
||||||
|
},
|
||||||
EventData,
|
EventData,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -46,6 +49,7 @@ pub struct CrashReport {
|
|||||||
pub job_id: Uuid,
|
pub job_id: Uuid,
|
||||||
|
|
||||||
pub scariness_score: Option<u32>,
|
pub scariness_score: Option<u32>,
|
||||||
|
|
||||||
pub scariness_description: Option<String>,
|
pub scariness_description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,11 +66,42 @@ pub struct NoCrash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum CrashTestResult {
|
pub enum CrashTestResult {
|
||||||
CrashReport(CrashReport),
|
CrashReport(CrashReport),
|
||||||
NoRepro(NoCrash),
|
NoRepro(NoCrash),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct RegressionReport {
|
||||||
|
pub crash_test_result: CrashTestResult,
|
||||||
|
pub original_crash_test_result: Option<CrashTestResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegressionReport {
|
||||||
|
pub async fn save(
|
||||||
|
self,
|
||||||
|
report_name: Option<String>,
|
||||||
|
regression_reports: &SyncedDir,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (event, name) = match &self.crash_test_result {
|
||||||
|
CrashTestResult::CrashReport(report) => {
|
||||||
|
let name = report_name.unwrap_or_else(|| report.unique_blob_name());
|
||||||
|
(regression_report, name)
|
||||||
|
}
|
||||||
|
CrashTestResult::NoRepro(report) => {
|
||||||
|
let name = report_name.unwrap_or_else(|| report.blob_name());
|
||||||
|
(regression_unable_to_reproduce, name)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if upload_or_save_local(&self, &name, regression_reports).await? {
|
||||||
|
event!(event; EventData::Path = name);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn upload_or_save_local<T: Serialize>(
|
async fn upload_or_save_local<T: Serialize>(
|
||||||
report: &T,
|
report: &T,
|
||||||
dest_name: &str,
|
dest_name: &str,
|
||||||
@ -76,6 +111,10 @@ async fn upload_or_save_local<T: Serialize>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CrashTestResult {
|
impl CrashTestResult {
|
||||||
|
/// Saves the crash result as a crash report
|
||||||
|
/// * `unique_reports` - location to save the deduplicated report if the bug was reproduced
|
||||||
|
/// * `reports` - location to save the report if the bug was reproduced
|
||||||
|
/// * `no_repro` - location to save the report if the bug was not reproduced
|
||||||
pub async fn save(
|
pub async fn save(
|
||||||
&self,
|
&self,
|
||||||
unique_reports: &Option<SyncedDir>,
|
unique_reports: &Option<SyncedDir>,
|
||||||
@ -113,7 +152,7 @@ impl CrashTestResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct InputBlob {
|
pub struct InputBlob {
|
||||||
pub account: Option<String>,
|
pub account: Option<String>,
|
||||||
pub container: Option<String>,
|
pub container: Option<String>,
|
||||||
@ -188,7 +227,7 @@ impl NoCrash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_report_file(path: PathBuf) -> Result<CrashTestResult> {
|
pub async fn parse_report_file(path: PathBuf) -> Result<CrashTestResult> {
|
||||||
let raw = std::fs::read_to_string(&path)
|
let raw = std::fs::read_to_string(&path)
|
||||||
.with_context(|| format_err!("unable to open crash report: {}", path.display()))?;
|
.with_context(|| format_err!("unable to open crash report: {}", path.display()))?;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ use super::crash_report::{CrashReport, CrashTestResult, InputBlob, NoCrash};
|
|||||||
use crate::tasks::{
|
use crate::tasks::{
|
||||||
config::CommonConfig,
|
config::CommonConfig,
|
||||||
generic::input_poller::{CallbackImpl, InputPoller, Processor},
|
generic::input_poller::{CallbackImpl, InputPoller, Processor},
|
||||||
heartbeat::*,
|
heartbeat::{HeartbeatSender, TaskHeartbeatClient},
|
||||||
utils::default_bool_true,
|
utils::default_bool_true,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -18,6 +18,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use storage_queue::{Message, QueueClient};
|
use storage_queue::{Message, QueueClient};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@ -86,27 +87,103 @@ impl ReportTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TestInputArgs<'a> {
|
||||||
|
pub input_url: Option<Url>,
|
||||||
|
pub input: &'a Path,
|
||||||
|
pub target_exe: &'a Path,
|
||||||
|
pub target_options: &'a [String],
|
||||||
|
pub target_env: &'a HashMap<String, String>,
|
||||||
|
pub setup_dir: &'a Path,
|
||||||
|
pub task_id: Uuid,
|
||||||
|
pub job_id: Uuid,
|
||||||
|
pub target_timeout: Option<u64>,
|
||||||
|
pub check_retry_count: u64,
|
||||||
|
pub check_asan_log: bool,
|
||||||
|
pub check_debugger: bool,
|
||||||
|
pub minimized_stack_depth: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_input(args: TestInputArgs<'_>) -> Result<CrashTestResult> {
|
||||||
|
let tester = Tester::new(
|
||||||
|
args.setup_dir,
|
||||||
|
args.target_exe,
|
||||||
|
args.target_options,
|
||||||
|
args.target_env,
|
||||||
|
)
|
||||||
|
.check_asan_log(args.check_asan_log)
|
||||||
|
.check_debugger(args.check_debugger)
|
||||||
|
.check_retry_count(args.check_retry_count)
|
||||||
|
.set_optional(args.target_timeout, |tester, timeout| {
|
||||||
|
tester.timeout(timeout)
|
||||||
|
});
|
||||||
|
|
||||||
|
let input_sha256 = sha256::digest_file(args.input).await?;
|
||||||
|
let task_id = args.task_id;
|
||||||
|
let job_id = args.job_id;
|
||||||
|
let input_blob = args
|
||||||
|
.input_url
|
||||||
|
.and_then(|u| BlobUrl::new(u).ok())
|
||||||
|
.map(InputBlob::from);
|
||||||
|
|
||||||
|
let test_report = tester.test_input(args.input).await?;
|
||||||
|
|
||||||
|
if let Some(crash_log) = test_report.asan_log {
|
||||||
|
let crash_report = CrashReport::new(
|
||||||
|
crash_log,
|
||||||
|
task_id,
|
||||||
|
job_id,
|
||||||
|
args.target_exe,
|
||||||
|
input_blob,
|
||||||
|
input_sha256,
|
||||||
|
args.minimized_stack_depth,
|
||||||
|
);
|
||||||
|
Ok(CrashTestResult::CrashReport(crash_report))
|
||||||
|
} else if let Some(crash) = test_report.crash {
|
||||||
|
let call_stack_sha256 = sha256::digest_iter(&crash.call_stack);
|
||||||
|
let crash_report = CrashReport {
|
||||||
|
input_blob,
|
||||||
|
input_sha256,
|
||||||
|
executable: PathBuf::from(args.target_exe),
|
||||||
|
call_stack: crash.call_stack,
|
||||||
|
crash_type: crash.crash_type,
|
||||||
|
crash_site: crash.crash_site,
|
||||||
|
call_stack_sha256,
|
||||||
|
asan_log: None,
|
||||||
|
scariness_score: None,
|
||||||
|
scariness_description: None,
|
||||||
|
task_id,
|
||||||
|
job_id,
|
||||||
|
minimized_stack: vec![],
|
||||||
|
minimized_stack_sha256: None,
|
||||||
|
minimized_stack_function_names: vec![],
|
||||||
|
minimized_stack_function_names_sha256: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CrashTestResult::CrashReport(crash_report))
|
||||||
|
} else {
|
||||||
|
let no_repro = NoCrash {
|
||||||
|
input_blob,
|
||||||
|
input_sha256,
|
||||||
|
executable: PathBuf::from(args.target_exe),
|
||||||
|
task_id,
|
||||||
|
job_id,
|
||||||
|
tries: 1 + args.check_retry_count,
|
||||||
|
error: test_report.error.map(|e| format!("{}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CrashTestResult::NoRepro(no_repro))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct GenericReportProcessor<'a> {
|
pub struct GenericReportProcessor<'a> {
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
tester: Tester<'a>,
|
|
||||||
heartbeat_client: Option<TaskHeartbeatClient>,
|
heartbeat_client: Option<TaskHeartbeatClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> GenericReportProcessor<'a> {
|
impl<'a> GenericReportProcessor<'a> {
|
||||||
pub fn new(config: &'a Config, heartbeat_client: Option<TaskHeartbeatClient>) -> Self {
|
pub fn new(config: &'a Config, heartbeat_client: Option<TaskHeartbeatClient>) -> Self {
|
||||||
let tester = Tester::new(
|
|
||||||
&config.common.setup_dir,
|
|
||||||
&config.target_exe,
|
|
||||||
&config.target_options,
|
|
||||||
&config.target_env,
|
|
||||||
)
|
|
||||||
.check_asan_log(config.check_asan_log)
|
|
||||||
.check_debugger(config.check_debugger)
|
|
||||||
.check_retry_count(config.check_retry_count);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
tester,
|
|
||||||
heartbeat_client,
|
heartbeat_client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,56 +194,25 @@ impl<'a> GenericReportProcessor<'a> {
|
|||||||
input: &Path,
|
input: &Path,
|
||||||
) -> Result<CrashTestResult> {
|
) -> Result<CrashTestResult> {
|
||||||
self.heartbeat_client.alive();
|
self.heartbeat_client.alive();
|
||||||
let input_sha256 = sha256::digest_file(input).await?;
|
|
||||||
let task_id = self.config.common.task_id;
|
let args = TestInputArgs {
|
||||||
let job_id = self.config.common.job_id;
|
input_url,
|
||||||
let input_blob = match input_url {
|
input,
|
||||||
Some(x) => Some(InputBlob::from(BlobUrl::new(x)?)),
|
target_exe: &self.config.target_exe,
|
||||||
None => None,
|
target_options: &self.config.target_options,
|
||||||
|
target_env: &self.config.target_env,
|
||||||
|
setup_dir: &self.config.common.setup_dir,
|
||||||
|
task_id: self.config.common.task_id,
|
||||||
|
job_id: self.config.common.job_id,
|
||||||
|
target_timeout: self.config.target_timeout,
|
||||||
|
check_retry_count: self.config.check_retry_count,
|
||||||
|
check_asan_log: self.config.check_asan_log,
|
||||||
|
check_debugger: self.config.check_debugger,
|
||||||
|
minimized_stack_depth: self.config.minimized_stack_depth,
|
||||||
};
|
};
|
||||||
|
let result = test_input(args).await?;
|
||||||
|
|
||||||
let test_report = self.tester.test_input(input).await?;
|
Ok(result)
|
||||||
|
|
||||||
if let Some(asan_log) = test_report.asan_log {
|
|
||||||
let crash_report = CrashReport::new(
|
|
||||||
asan_log,
|
|
||||||
task_id,
|
|
||||||
job_id,
|
|
||||||
&self.config.target_exe,
|
|
||||||
input_blob,
|
|
||||||
input_sha256,
|
|
||||||
self.config.minimized_stack_depth,
|
|
||||||
);
|
|
||||||
Ok(CrashTestResult::CrashReport(crash_report))
|
|
||||||
} else if let Some(crash) = test_report.crash {
|
|
||||||
let call_stack_sha256 = sha256::digest_iter(&crash.call_stack);
|
|
||||||
let crash_report = CrashReport {
|
|
||||||
input_blob,
|
|
||||||
input_sha256,
|
|
||||||
executable: PathBuf::from(&self.config.target_exe),
|
|
||||||
call_stack: crash.call_stack,
|
|
||||||
crash_type: crash.crash_type,
|
|
||||||
crash_site: crash.crash_site,
|
|
||||||
call_stack_sha256,
|
|
||||||
task_id,
|
|
||||||
job_id,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(CrashTestResult::CrashReport(crash_report))
|
|
||||||
} else {
|
|
||||||
let no_repro = NoCrash {
|
|
||||||
input_blob,
|
|
||||||
input_sha256,
|
|
||||||
executable: PathBuf::from(&self.config.target_exe),
|
|
||||||
task_id,
|
|
||||||
job_id,
|
|
||||||
tries: 1 + self.config.check_retry_count,
|
|
||||||
error: test_report.error.map(|e| format!("{}", e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(CrashTestResult::NoRepro(no_repro))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
use super::crash_report::*;
|
use super::crash_report::*;
|
||||||
use crate::tasks::{
|
use crate::tasks::{
|
||||||
config::CommonConfig, generic::input_poller::*, heartbeat::*, utils::default_bool_true,
|
config::CommonConfig,
|
||||||
|
generic::input_poller::*,
|
||||||
|
heartbeat::{HeartbeatSender, TaskHeartbeatClient},
|
||||||
|
utils::default_bool_true,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@ -75,7 +78,7 @@ impl ReportTask {
|
|||||||
let mut processor = AsanProcessor::new(self.config.clone()).await?;
|
let mut processor = AsanProcessor::new(self.config.clone()).await?;
|
||||||
|
|
||||||
if let Some(crashes) = &self.config.crashes {
|
if let Some(crashes) = &self.config.crashes {
|
||||||
self.poller.batch_process(&mut processor, crashes).await?;
|
self.poller.batch_process(&mut processor, &crashes).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config.check_queue {
|
if self.config.check_queue {
|
||||||
@ -88,6 +91,72 @@ impl ReportTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TestInputArgs<'a> {
|
||||||
|
pub input_url: Option<Url>,
|
||||||
|
pub input: &'a Path,
|
||||||
|
pub target_exe: &'a Path,
|
||||||
|
pub target_options: &'a [String],
|
||||||
|
pub target_env: &'a HashMap<String, String>,
|
||||||
|
pub setup_dir: &'a Path,
|
||||||
|
pub task_id: uuid::Uuid,
|
||||||
|
pub job_id: uuid::Uuid,
|
||||||
|
pub target_timeout: Option<u64>,
|
||||||
|
pub check_retry_count: u64,
|
||||||
|
pub minimized_stack_depth: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_input(args: TestInputArgs<'_>) -> Result<CrashTestResult> {
|
||||||
|
let fuzzer = LibFuzzer::new(
|
||||||
|
args.target_exe,
|
||||||
|
args.target_options,
|
||||||
|
args.target_env,
|
||||||
|
args.setup_dir,
|
||||||
|
);
|
||||||
|
|
||||||
|
let task_id = args.task_id;
|
||||||
|
let job_id = args.job_id;
|
||||||
|
let input_blob = args
|
||||||
|
.input_url
|
||||||
|
.and_then(|u| BlobUrl::new(u).ok())
|
||||||
|
.map(InputBlob::from);
|
||||||
|
let input = args.input;
|
||||||
|
let input_sha256 = sha256::digest_file(args.input)
|
||||||
|
.await
|
||||||
|
.with_context(|| format_err!("unable to sha256 digest input file: {}", input.display()))?;
|
||||||
|
|
||||||
|
let test_report = fuzzer
|
||||||
|
.repro(args.input, args.target_timeout, args.check_retry_count)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match test_report.asan_log {
|
||||||
|
Some(crash_log) => {
|
||||||
|
let crash_report = CrashReport::new(
|
||||||
|
crash_log,
|
||||||
|
task_id,
|
||||||
|
job_id,
|
||||||
|
args.target_exe,
|
||||||
|
input_blob,
|
||||||
|
input_sha256,
|
||||||
|
args.minimized_stack_depth,
|
||||||
|
);
|
||||||
|
Ok(CrashTestResult::CrashReport(crash_report))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let no_repro = NoCrash {
|
||||||
|
input_blob,
|
||||||
|
input_sha256,
|
||||||
|
executable: PathBuf::from(&args.target_exe),
|
||||||
|
task_id,
|
||||||
|
job_id,
|
||||||
|
tries: 1 + args.check_retry_count,
|
||||||
|
error: test_report.error.map(|e| format!("{}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CrashTestResult::NoRepro(no_repro))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AsanProcessor {
|
pub struct AsanProcessor {
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
heartbeat_client: Option<TaskHeartbeatClient>,
|
heartbeat_client: Option<TaskHeartbeatClient>,
|
||||||
@ -109,58 +178,22 @@ impl AsanProcessor {
|
|||||||
input: &Path,
|
input: &Path,
|
||||||
) -> Result<CrashTestResult> {
|
) -> Result<CrashTestResult> {
|
||||||
self.heartbeat_client.alive();
|
self.heartbeat_client.alive();
|
||||||
let fuzzer = LibFuzzer::new(
|
let args = TestInputArgs {
|
||||||
&self.config.target_exe,
|
input_url,
|
||||||
&self.config.target_options,
|
input,
|
||||||
&self.config.target_env,
|
target_exe: &self.config.target_exe,
|
||||||
&self.config.common.setup_dir,
|
target_options: &self.config.target_options,
|
||||||
);
|
target_env: &self.config.target_env,
|
||||||
|
setup_dir: &self.config.common.setup_dir,
|
||||||
let task_id = self.config.common.task_id;
|
task_id: self.config.common.task_id,
|
||||||
let job_id = self.config.common.job_id;
|
job_id: self.config.common.job_id,
|
||||||
let input_blob = match input_url {
|
target_timeout: self.config.target_timeout,
|
||||||
Some(x) => Some(InputBlob::from(BlobUrl::new(x)?)),
|
check_retry_count: self.config.check_retry_count,
|
||||||
None => None,
|
minimized_stack_depth: self.config.minimized_stack_depth,
|
||||||
};
|
};
|
||||||
let input_sha256 = sha256::digest_file(input).await.with_context(|| {
|
let result = test_input(args).await?;
|
||||||
format_err!("unable to sha256 digest input file: {}", input.display())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let test_report = fuzzer
|
Ok(result)
|
||||||
.repro(
|
|
||||||
input,
|
|
||||||
self.config.target_timeout,
|
|
||||||
self.config.check_retry_count,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match test_report.asan_log {
|
|
||||||
Some(asan_log) => {
|
|
||||||
let crash_report = CrashReport::new(
|
|
||||||
asan_log,
|
|
||||||
task_id,
|
|
||||||
job_id,
|
|
||||||
&self.config.target_exe,
|
|
||||||
input_blob,
|
|
||||||
input_sha256,
|
|
||||||
self.config.minimized_stack_depth,
|
|
||||||
);
|
|
||||||
Ok(CrashTestResult::CrashReport(crash_report))
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let no_repro = NoCrash {
|
|
||||||
input_blob,
|
|
||||||
input_sha256,
|
|
||||||
executable: PathBuf::from(&self.config.target_exe),
|
|
||||||
task_id,
|
|
||||||
job_id,
|
|
||||||
tries: 1 + self.config.check_retry_count,
|
|
||||||
error: test_report.error.map(|e| format!("{}", e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(CrashTestResult::NoRepro(no_repro))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ extern crate onefuzz_telemetry;
|
|||||||
extern crate onefuzz;
|
extern crate onefuzz;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::StaticConfig, coordinator::StateUpdateEvent, heartbeat::*, work::WorkSet,
|
config::StaticConfig, coordinator::StateUpdateEvent, heartbeat::init_agent_heartbeat,
|
||||||
worker::WorkerEvent,
|
work::WorkSet, worker::WorkerEvent,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -95,6 +95,8 @@ pub enum Event {
|
|||||||
new_report,
|
new_report,
|
||||||
new_unique_report,
|
new_unique_report,
|
||||||
new_unable_to_reproduce,
|
new_unable_to_reproduce,
|
||||||
|
regression_report,
|
||||||
|
regression_unable_to_reproduce,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
@ -109,6 +111,8 @@ impl Event {
|
|||||||
Self::new_report => "new_report",
|
Self::new_report => "new_report",
|
||||||
Self::new_unique_report => "new_unique_report",
|
Self::new_unique_report => "new_unique_report",
|
||||||
Self::new_unable_to_reproduce => "new_unable_to_reproduce",
|
Self::new_unable_to_reproduce => "new_unable_to_reproduce",
|
||||||
|
Self::regression_report => "regression_report",
|
||||||
|
Self::regression_unable_to_reproduce => "regression_unable_to_reproduce",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,15 @@ impl BlobUrl {
|
|||||||
bail!("Invalid blob URL: {}", url)
|
bail!("Invalid blob URL: {}", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_blob_info(account: &str, container: &str, name: &str) -> Result<Self> {
|
||||||
|
// format https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#resource-uri-syntax
|
||||||
|
let url = Url::parse(&format!(
|
||||||
|
"https://{}.blob.core.windows.net/{}/{}",
|
||||||
|
account, container, name
|
||||||
|
))?;
|
||||||
|
Self::new(url)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse(url: impl AsRef<str>) -> Result<Self> {
|
pub fn parse(url: impl AsRef<str>) -> Result<Self> {
|
||||||
let url = Url::parse(url.as_ref())?;
|
let url = Url::parse(url.as_ref())?;
|
||||||
|
|
||||||
|
@ -271,6 +271,9 @@ def repro_extensions(
|
|||||||
if report is None:
|
if report is None:
|
||||||
raise Exception("invalid report: %s" % repro_config)
|
raise Exception("invalid report: %s" % repro_config)
|
||||||
|
|
||||||
|
if report.input_blob is None:
|
||||||
|
raise Exception("unable to perform reproduction without an input blob")
|
||||||
|
|
||||||
commands = []
|
commands = []
|
||||||
if setup_container:
|
if setup_container:
|
||||||
commands += [
|
commands += [
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Iterator, List, Optional
|
from typing import Iterator, List, Optional, Union
|
||||||
|
|
||||||
from azure.devops.connection import Connection
|
from azure.devops.connection import Connection
|
||||||
from azure.devops.credentials import BasicAuthentication
|
from azure.devops.credentials import BasicAuthentication
|
||||||
@ -24,7 +24,7 @@ from azure.devops.v6_0.work_item_tracking.work_item_tracking_client import (
|
|||||||
WorkItemTrackingClient,
|
WorkItemTrackingClient,
|
||||||
)
|
)
|
||||||
from memoization import cached
|
from memoization import cached
|
||||||
from onefuzztypes.models import ADOTemplate, Report
|
from onefuzztypes.models import ADOTemplate, RegressionReport, Report
|
||||||
from onefuzztypes.primitives import Container
|
from onefuzztypes.primitives import Container
|
||||||
|
|
||||||
from ..secrets import get_secret_string_value
|
from ..secrets import get_secret_string_value
|
||||||
@ -51,7 +51,11 @@ def get_valid_fields(
|
|||||||
|
|
||||||
class ADO:
|
class ADO:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, container: Container, filename: str, config: ADOTemplate, report: Report
|
self,
|
||||||
|
container: Container,
|
||||||
|
filename: str,
|
||||||
|
config: ADOTemplate,
|
||||||
|
report: Report,
|
||||||
):
|
):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.renderer = Render(container, filename, report)
|
self.renderer = Render(container, filename, report)
|
||||||
@ -203,8 +207,20 @@ class ADO:
|
|||||||
|
|
||||||
|
|
||||||
def notify_ado(
|
def notify_ado(
|
||||||
config: ADOTemplate, container: Container, filename: str, report: Report
|
config: ADOTemplate,
|
||||||
|
container: Container,
|
||||||
|
filename: str,
|
||||||
|
report: Union[Report, RegressionReport],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if isinstance(report, RegressionReport):
|
||||||
|
logging.info(
|
||||||
|
"ado integration does not support regression reports. "
|
||||||
|
"container:%s filename:%s",
|
||||||
|
container,
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
"notify ado: job_id:%s task_id:%s container:%s filename:%s",
|
"notify ado: job_id:%s task_id:%s container:%s filename:%s",
|
||||||
report.job_id,
|
report.job_id,
|
||||||
|
@ -4,13 +4,18 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from github3 import login
|
from github3 import login
|
||||||
from github3.exceptions import GitHubException
|
from github3.exceptions import GitHubException
|
||||||
from github3.issues import Issue
|
from github3.issues import Issue
|
||||||
from onefuzztypes.enums import GithubIssueSearchMatch
|
from onefuzztypes.enums import GithubIssueSearchMatch
|
||||||
from onefuzztypes.models import GithubAuth, GithubIssueTemplate, Report
|
from onefuzztypes.models import (
|
||||||
|
GithubAuth,
|
||||||
|
GithubIssueTemplate,
|
||||||
|
RegressionReport,
|
||||||
|
Report,
|
||||||
|
)
|
||||||
from onefuzztypes.primitives import Container
|
from onefuzztypes.primitives import Container
|
||||||
|
|
||||||
from ..secrets import get_secret_obj
|
from ..secrets import get_secret_obj
|
||||||
@ -107,10 +112,18 @@ def github_issue(
|
|||||||
config: GithubIssueTemplate,
|
config: GithubIssueTemplate,
|
||||||
container: Container,
|
container: Container,
|
||||||
filename: str,
|
filename: str,
|
||||||
report: Optional[Report],
|
report: Optional[Union[Report, RegressionReport]],
|
||||||
) -> None:
|
) -> None:
|
||||||
if report is None:
|
if report is None:
|
||||||
return
|
return
|
||||||
|
if isinstance(report, RegressionReport):
|
||||||
|
logging.info(
|
||||||
|
"github issue integration does not support regression reports. "
|
||||||
|
"container:%s filename:%s",
|
||||||
|
container,
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handler = GithubIssue(config, container, filename, report)
|
handler = GithubIssue(config, container, filename, report)
|
||||||
|
@ -10,12 +10,18 @@ from uuid import UUID
|
|||||||
from memoization import cached
|
from memoization import cached
|
||||||
from onefuzztypes import models
|
from onefuzztypes import models
|
||||||
from onefuzztypes.enums import ErrorCode, TaskState
|
from onefuzztypes.enums import ErrorCode, TaskState
|
||||||
from onefuzztypes.events import EventCrashReported, EventFileAdded
|
from onefuzztypes.events import (
|
||||||
|
EventCrashReported,
|
||||||
|
EventFileAdded,
|
||||||
|
EventRegressionReported,
|
||||||
|
)
|
||||||
from onefuzztypes.models import (
|
from onefuzztypes.models import (
|
||||||
ADOTemplate,
|
ADOTemplate,
|
||||||
Error,
|
Error,
|
||||||
GithubIssueTemplate,
|
GithubIssueTemplate,
|
||||||
NotificationTemplate,
|
NotificationTemplate,
|
||||||
|
RegressionReport,
|
||||||
|
Report,
|
||||||
Result,
|
Result,
|
||||||
TeamsTemplate,
|
TeamsTemplate,
|
||||||
)
|
)
|
||||||
@ -26,7 +32,7 @@ from ..azure.queue import send_message
|
|||||||
from ..azure.storage import StorageType
|
from ..azure.storage import StorageType
|
||||||
from ..events import send_event
|
from ..events import send_event
|
||||||
from ..orm import ORMMixin
|
from ..orm import ORMMixin
|
||||||
from ..reports import get_report
|
from ..reports import get_report_or_regression
|
||||||
from ..tasks.config import get_input_container_queues
|
from ..tasks.config import get_input_container_queues
|
||||||
from ..tasks.main import Task
|
from ..tasks.main import Task
|
||||||
from .ado import notify_ado
|
from .ado import notify_ado
|
||||||
@ -102,7 +108,7 @@ def get_queue_tasks() -> Sequence[Tuple[Task, Sequence[str]]]:
|
|||||||
|
|
||||||
|
|
||||||
def new_files(container: Container, filename: str) -> None:
|
def new_files(container: Container, filename: str) -> None:
|
||||||
report = get_report(container, filename)
|
report = get_report_or_regression(container, filename)
|
||||||
|
|
||||||
notifications = get_notifications(container)
|
notifications = get_notifications(container)
|
||||||
if notifications:
|
if notifications:
|
||||||
@ -134,9 +140,15 @@ def new_files(container: Container, filename: str) -> None:
|
|||||||
)
|
)
|
||||||
send_message(task.task_id, bytes(url, "utf-8"), StorageType.corpus)
|
send_message(task.task_id, bytes(url, "utf-8"), StorageType.corpus)
|
||||||
|
|
||||||
if report:
|
if isinstance(report, Report):
|
||||||
send_event(
|
send_event(
|
||||||
EventCrashReported(report=report, container=container, filename=filename)
|
EventCrashReported(report=report, container=container, filename=filename)
|
||||||
)
|
)
|
||||||
|
elif isinstance(report, RegressionReport):
|
||||||
|
send_event(
|
||||||
|
EventRegressionReported(
|
||||||
|
regression_report=report, container=container, filename=filename
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
send_event(EventFileAdded(container=container, filename=filename))
|
send_event(EventFileAdded(container=container, filename=filename))
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from onefuzztypes.models import Report, TeamsTemplate
|
from onefuzztypes.models import RegressionReport, Report, TeamsTemplate
|
||||||
from onefuzztypes.primitives import Container
|
from onefuzztypes.primitives import Container
|
||||||
|
|
||||||
from ..azure.containers import auth_download_url
|
from ..azure.containers import auth_download_url
|
||||||
@ -54,12 +54,15 @@ def send_teams_webhook(
|
|||||||
|
|
||||||
|
|
||||||
def notify_teams(
|
def notify_teams(
|
||||||
config: TeamsTemplate, container: Container, filename: str, report: Optional[Report]
|
config: TeamsTemplate,
|
||||||
|
container: Container,
|
||||||
|
filename: str,
|
||||||
|
report: Optional[Union[Report, RegressionReport]],
|
||||||
) -> None:
|
) -> None:
|
||||||
text = None
|
text = None
|
||||||
facts: List[Dict[str, str]] = []
|
facts: List[Dict[str, str]] = []
|
||||||
|
|
||||||
if report:
|
if isinstance(report, Report):
|
||||||
task = Task.get(report.job_id, report.task_id)
|
task = Task.get(report.job_id, report.task_id)
|
||||||
if not task:
|
if not task:
|
||||||
logging.error(
|
logging.error(
|
||||||
|
@ -8,7 +8,7 @@ import logging
|
|||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from memoization import cached
|
from memoization import cached
|
||||||
from onefuzztypes.models import Report
|
from onefuzztypes.models import RegressionReport, Report
|
||||||
from onefuzztypes.primitives import Container
|
from onefuzztypes.primitives import Container
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
@ -16,17 +16,16 @@ from .azure.containers import get_blob
|
|||||||
from .azure.storage import StorageType
|
from .azure.storage import StorageType
|
||||||
|
|
||||||
|
|
||||||
def parse_report(
|
def parse_report_or_regression(
|
||||||
content: Union[str, bytes], file_path: Optional[str] = None
|
content: Union[str, bytes], file_path: Optional[str] = None
|
||||||
) -> Optional[Report]:
|
) -> Optional[Union[Report, RegressionReport]]:
|
||||||
if isinstance(content, bytes):
|
if isinstance(content, bytes):
|
||||||
try:
|
try:
|
||||||
content = content.decode()
|
content = content.decode()
|
||||||
except UnicodeDecodeError as err:
|
except UnicodeDecodeError as err:
|
||||||
logging.error(
|
logging.error(
|
||||||
"unable to parse report (%s): unicode decode of report failed - %s",
|
f"unable to parse report ({file_path}): "
|
||||||
file_path,
|
f"unicode decode of report failed - {err}"
|
||||||
err,
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -34,22 +33,31 @@ def parse_report(
|
|||||||
data = json.loads(content)
|
data = json.loads(content)
|
||||||
except json.decoder.JSONDecodeError as err:
|
except json.decoder.JSONDecodeError as err:
|
||||||
logging.error(
|
logging.error(
|
||||||
"unable to parse report (%s): json decoding failed - %s", file_path, err
|
f"unable to parse report ({file_path}): json decoding failed - {err}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
regression_err = None
|
||||||
try:
|
try:
|
||||||
entry = Report.parse_obj(data)
|
return RegressionReport.parse_obj(data)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
logging.error("unable to parse report (%s): %s", file_path, err)
|
regression_err = err
|
||||||
return None
|
|
||||||
|
|
||||||
return entry
|
try:
|
||||||
|
return Report.parse_obj(data)
|
||||||
|
except ValidationError as err:
|
||||||
|
logging.error(
|
||||||
|
f"unable to parse report ({file_path}) as a report or regression. "
|
||||||
|
f"regression error: {regression_err} report error: {err}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# cache the last 1000 reports
|
# cache the last 1000 reports
|
||||||
@cached(max_size=1000)
|
@cached(max_size=1000)
|
||||||
def get_report(container: Container, filename: str) -> Optional[Report]:
|
def get_report_or_regression(
|
||||||
|
container: Container, filename: str
|
||||||
|
) -> Optional[Union[Report, RegressionReport]]:
|
||||||
file_path = "/".join([container, filename])
|
file_path = "/".join([container, filename])
|
||||||
if not filename.endswith(".json"):
|
if not filename.endswith(".json"):
|
||||||
logging.error("get_report invalid extension: %s", file_path)
|
logging.error("get_report invalid extension: %s", file_path)
|
||||||
@ -60,4 +68,11 @@ def get_report(container: Container, filename: str) -> Optional[Report]:
|
|||||||
logging.error("get_report invalid blob: %s", file_path)
|
logging.error("get_report invalid blob: %s", file_path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return parse_report(blob, file_path=file_path)
|
return parse_report_or_regression(blob, file_path=file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_report(container: Container, filename: str) -> Optional[Report]:
|
||||||
|
result = get_report_or_regression(container, filename)
|
||||||
|
if isinstance(result, Report):
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
@ -164,6 +164,12 @@ class Repro(BASE_REPRO, ORMMixin):
|
|||||||
if report is None:
|
if report is None:
|
||||||
return Error(code=ErrorCode.VM_CREATE_FAILED, errors=["missing report"])
|
return Error(code=ErrorCode.VM_CREATE_FAILED, errors=["missing report"])
|
||||||
|
|
||||||
|
if report.input_blob is None:
|
||||||
|
return Error(
|
||||||
|
code=ErrorCode.VM_CREATE_FAILED,
|
||||||
|
errors=["unable to perform repro for crash reports without inputs"],
|
||||||
|
)
|
||||||
|
|
||||||
files = {}
|
files = {}
|
||||||
|
|
||||||
if task.os == OS.windows:
|
if task.os == OS.windows:
|
||||||
|
@ -348,6 +348,9 @@ def build_task_config(
|
|||||||
else True
|
else True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TaskFeature.report_list in definition.features:
|
||||||
|
config.report_list = task_config.task.report_list
|
||||||
|
|
||||||
if TaskFeature.expect_crash_on_failure in definition.features:
|
if TaskFeature.expect_crash_on_failure in definition.features:
|
||||||
config.expect_crash_on_failure = (
|
config.expect_crash_on_failure = (
|
||||||
task_config.task.expect_crash_on_failure
|
task_config.task.expect_crash_on_failure
|
||||||
|
@ -414,4 +414,131 @@ TASK_DEFINITIONS = {
|
|||||||
],
|
],
|
||||||
monitor_queue=ContainerType.crashes,
|
monitor_queue=ContainerType.crashes,
|
||||||
),
|
),
|
||||||
|
TaskType.generic_regression: TaskDefinition(
|
||||||
|
features=[
|
||||||
|
TaskFeature.target_exe,
|
||||||
|
TaskFeature.target_env,
|
||||||
|
TaskFeature.target_options,
|
||||||
|
TaskFeature.target_timeout,
|
||||||
|
TaskFeature.check_asan_log,
|
||||||
|
TaskFeature.check_debugger,
|
||||||
|
TaskFeature.check_retry_count,
|
||||||
|
TaskFeature.report_list,
|
||||||
|
],
|
||||||
|
vm=VmDefinition(compare=Compare.AtLeast, value=1),
|
||||||
|
containers=[
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.setup,
|
||||||
|
compare=Compare.Equal,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.regression_reports,
|
||||||
|
compare=Compare.Equal,
|
||||||
|
value=1,
|
||||||
|
permissions=[
|
||||||
|
ContainerPermission.Write,
|
||||||
|
ContainerPermission.Read,
|
||||||
|
ContainerPermission.List,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.crashes,
|
||||||
|
compare=Compare.Equal,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.reports,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.unique_reports,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.no_repro,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.readonly_inputs,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[
|
||||||
|
ContainerPermission.Read,
|
||||||
|
ContainerPermission.List,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TaskType.libfuzzer_regression: TaskDefinition(
|
||||||
|
features=[
|
||||||
|
TaskFeature.target_exe,
|
||||||
|
TaskFeature.target_env,
|
||||||
|
TaskFeature.target_options,
|
||||||
|
TaskFeature.target_timeout,
|
||||||
|
TaskFeature.check_fuzzer_help,
|
||||||
|
TaskFeature.check_retry_count,
|
||||||
|
TaskFeature.report_list,
|
||||||
|
],
|
||||||
|
vm=VmDefinition(compare=Compare.AtLeast, value=1),
|
||||||
|
containers=[
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.setup,
|
||||||
|
compare=Compare.Equal,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.regression_reports,
|
||||||
|
compare=Compare.Equal,
|
||||||
|
value=1,
|
||||||
|
permissions=[
|
||||||
|
ContainerPermission.Write,
|
||||||
|
ContainerPermission.Read,
|
||||||
|
ContainerPermission.List,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.crashes,
|
||||||
|
compare=Compare.Equal,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.unique_reports,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.reports,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.no_repro,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||||
|
),
|
||||||
|
ContainerDefinition(
|
||||||
|
type=ContainerType.readonly_inputs,
|
||||||
|
compare=Compare.AtMost,
|
||||||
|
value=1,
|
||||||
|
permissions=[
|
||||||
|
ContainerPermission.Read,
|
||||||
|
ContainerPermission.List,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from onefuzztypes.models import Report
|
from onefuzztypes.models import Report
|
||||||
|
|
||||||
from __app__.onefuzzlib.reports import parse_report
|
from __app__.onefuzzlib.reports import parse_report_or_regression
|
||||||
|
|
||||||
|
|
||||||
class TestReportParse(unittest.TestCase):
|
class TestReportParse(unittest.TestCase):
|
||||||
@ -20,16 +20,15 @@ class TestReportParse(unittest.TestCase):
|
|||||||
data = json.load(handle)
|
data = json.load(handle)
|
||||||
|
|
||||||
invalid = {"unused_field_1": 3}
|
invalid = {"unused_field_1": 3}
|
||||||
report = parse_report(json.dumps(data))
|
report = parse_report_or_regression(json.dumps(data))
|
||||||
self.assertIsInstance(report, Report)
|
self.assertIsInstance(report, Report)
|
||||||
|
|
||||||
with self.assertLogs(level="ERROR"):
|
with self.assertLogs(level="ERROR"):
|
||||||
self.assertIsNone(parse_report('"invalid"'))
|
self.assertIsNone(parse_report_or_regression('"invalid"'))
|
||||||
|
|
||||||
with self.assertLogs(level="WARNING") as logs:
|
with self.assertLogs(level="WARNING") as logs:
|
||||||
self.assertIsNone(parse_report(json.dumps(invalid)))
|
self.assertIsNone(parse_report_or_regression(json.dumps(invalid)))
|
||||||
|
self.assertTrue(any(["unable to parse report" in x for x in logs.output]))
|
||||||
self.assertTrue(any(["unable to parse report" in x for x in logs.output]))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -174,11 +174,13 @@ class Files(Endpoint):
|
|||||||
sas = self.onefuzz.containers.get(container).sas_url
|
sas = self.onefuzz.containers.get(container).sas_url
|
||||||
return ContainerWrapper(sas)
|
return ContainerWrapper(sas)
|
||||||
|
|
||||||
def list(self, container: primitives.Container) -> models.Files:
|
def list(
|
||||||
|
self, container: primitives.Container, prefix: Optional[str] = None
|
||||||
|
) -> models.Files:
|
||||||
""" Get a list of files in a container """
|
""" Get a list of files in a container """
|
||||||
self.logger.debug("listing files in container: %s", container)
|
self.logger.debug("listing files in container: %s", container)
|
||||||
client = self._get_client(container)
|
client = self._get_client(container)
|
||||||
return models.Files(files=client.list_blobs())
|
return models.Files(files=client.list_blobs(name_starts_with=prefix))
|
||||||
|
|
||||||
def delete(self, container: primitives.Container, filename: str) -> None:
|
def delete(self, container: primitives.Container, filename: str) -> None:
|
||||||
""" delete a file from a container """
|
""" delete a file from a container """
|
||||||
@ -845,6 +847,7 @@ class Tasks(Endpoint):
|
|||||||
vm_count: int = 1,
|
vm_count: int = 1,
|
||||||
preserve_existing_outputs: bool = False,
|
preserve_existing_outputs: bool = False,
|
||||||
colocate: bool = False,
|
colocate: bool = False,
|
||||||
|
report_list: Optional[List[str]] = None,
|
||||||
) -> models.Task:
|
) -> models.Task:
|
||||||
"""
|
"""
|
||||||
Create a task
|
Create a task
|
||||||
@ -907,6 +910,8 @@ class Tasks(Endpoint):
|
|||||||
target_workers=target_workers,
|
target_workers=target_workers,
|
||||||
type=task_type,
|
type=task_type,
|
||||||
wait_for_files=task_wait_for_files,
|
wait_for_files=task_wait_for_files,
|
||||||
|
report_list=report_list,
|
||||||
|
preserve_existing_outputs=preserve_existing_outputs,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -505,6 +505,14 @@ def output(result: Any, output_format: str, expression: Optional[Any]) -> None:
|
|||||||
print(result, flush=True)
|
print(result, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def log_exception(args: argparse.Namespace, err: Exception) -> None:
|
||||||
|
if args.verbose > 0:
|
||||||
|
entry = traceback.format_exc()
|
||||||
|
for x in entry.split("\n"):
|
||||||
|
LOGGER.error("traceback: %s", x)
|
||||||
|
LOGGER.error("command failed: %s", " ".join([str(x) for x in err.args]))
|
||||||
|
|
||||||
|
|
||||||
def execute_api(api: Any, api_types: List[Any], version: str) -> int:
|
def execute_api(api: Any, api_types: List[Any], version: str) -> int:
|
||||||
builder = Builder(api_types)
|
builder = Builder(api_types)
|
||||||
builder.add_version(version)
|
builder.add_version(version)
|
||||||
@ -545,11 +553,7 @@ def execute_api(api: Any, api_types: List[Any], version: str) -> int:
|
|||||||
try:
|
try:
|
||||||
result = call_func(args.func, args)
|
result = call_func(args.func, args)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if args.verbose > 0:
|
log_exception(args, err)
|
||||||
entry = traceback.format_exc()
|
|
||||||
for x in entry.split("\n"):
|
|
||||||
LOGGER.error("traceback: %s", x)
|
|
||||||
LOGGER.error("command failed: %s", " ".join([str(x) for x in err.args]))
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
output(result, args.format, expression)
|
output(result, args.format, expression)
|
||||||
|
@ -78,11 +78,21 @@ class JobMonitor:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_stopped(self) -> Tuple[bool, str, Any]:
|
||||||
|
tasks = self.onefuzz.tasks.list(job_id=self.job.job_id)
|
||||||
|
waiting = [
|
||||||
|
"%s:%s" % (x.config.task.type.name, x.state.name)
|
||||||
|
for x in tasks
|
||||||
|
if x.state != TaskState.stopped
|
||||||
|
]
|
||||||
|
return (not waiting, "waiting on: %s" % ", ".join(sorted(waiting)), None)
|
||||||
|
|
||||||
def wait(
|
def wait(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
wait_for_running: Optional[bool] = False,
|
wait_for_running: Optional[bool] = False,
|
||||||
wait_for_files: Optional[Dict[Container, int]] = None,
|
wait_for_files: Optional[Dict[Container, int]] = None,
|
||||||
|
wait_for_stopped: Optional[bool] = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if wait_for_running:
|
if wait_for_running:
|
||||||
wait(self.is_running)
|
wait(self.is_running)
|
||||||
@ -92,3 +102,7 @@ class JobMonitor:
|
|||||||
self.containers = wait_for_files
|
self.containers = wait_for_files
|
||||||
wait(self.has_files)
|
wait(self.has_files)
|
||||||
self.onefuzz.logger.info("new files found")
|
self.onefuzz.logger.info("new files found")
|
||||||
|
|
||||||
|
if wait_for_stopped:
|
||||||
|
wait(self.is_stopped)
|
||||||
|
self.onefuzz.logger.info("tasks stopped")
|
||||||
|
@ -13,6 +13,7 @@ from .templates.afl import AFL
|
|||||||
from .templates.libfuzzer import Libfuzzer
|
from .templates.libfuzzer import Libfuzzer
|
||||||
from .templates.ossfuzz import OssFuzz
|
from .templates.ossfuzz import OssFuzz
|
||||||
from .templates.radamsa import Radamsa
|
from .templates.radamsa import Radamsa
|
||||||
|
from .templates.regression import Regression
|
||||||
|
|
||||||
|
|
||||||
class Template(Command):
|
class Template(Command):
|
||||||
@ -24,6 +25,7 @@ class Template(Command):
|
|||||||
self.afl = AFL(onefuzz, logger)
|
self.afl = AFL(onefuzz, logger)
|
||||||
self.radamsa = Radamsa(onefuzz, logger)
|
self.radamsa = Radamsa(onefuzz, logger)
|
||||||
self.ossfuzz = OssFuzz(onefuzz, logger)
|
self.ossfuzz = OssFuzz(onefuzz, logger)
|
||||||
|
self.regression = Regression(onefuzz, logger)
|
||||||
|
|
||||||
def stop(
|
def stop(
|
||||||
self,
|
self,
|
||||||
|
@ -7,9 +7,10 @@ import json
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from onefuzztypes.enums import OS, ContainerType
|
from onefuzztypes.enums import OS, ContainerType, TaskState
|
||||||
from onefuzztypes.models import Job, NotificationConfig
|
from onefuzztypes.models import Job, NotificationConfig
|
||||||
from onefuzztypes.primitives import Container, Directory, File
|
from onefuzztypes.primitives import Container, Directory, File
|
||||||
|
|
||||||
@ -39,6 +40,12 @@ def _build_container_name(
|
|||||||
build=build,
|
build=build,
|
||||||
platform=platform.name,
|
platform=platform.name,
|
||||||
)
|
)
|
||||||
|
elif container_type == ContainerType.regression_reports:
|
||||||
|
guid = onefuzz.utils.namespaced_guid(
|
||||||
|
project,
|
||||||
|
name,
|
||||||
|
build=build,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
guid = onefuzz.utils.namespaced_guid(project, name)
|
guid = onefuzz.utils.namespaced_guid(project, name)
|
||||||
|
|
||||||
@ -87,6 +94,7 @@ class JobHelper:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.wait_for_running: bool = False
|
self.wait_for_running: bool = False
|
||||||
|
self.wait_for_stopped: bool = False
|
||||||
self.containers: Dict[ContainerType, Container] = {}
|
self.containers: Dict[ContainerType, Container] = {}
|
||||||
self.tags: Dict[str, str] = {"project": project, "name": name, "build": build}
|
self.tags: Dict[str, str] = {"project": project, "name": name, "build": build}
|
||||||
if job is None:
|
if job is None:
|
||||||
@ -116,6 +124,15 @@ class JobHelper:
|
|||||||
self.platform,
|
self.platform,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_unique_container_name(self, container_type: ContainerType) -> Container:
|
||||||
|
return Container(
|
||||||
|
"oft-%s-%s"
|
||||||
|
% (
|
||||||
|
container_type.name.replace("_", "-"),
|
||||||
|
uuid4().hex,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def create_containers(self) -> None:
|
def create_containers(self) -> None:
|
||||||
for (container_type, container_name) in self.containers.items():
|
for (container_type, container_name) in self.containers.items():
|
||||||
self.logger.info("using container: %s", container_name)
|
self.logger.info("using container: %s", container_name)
|
||||||
@ -123,6 +140,9 @@ class JobHelper:
|
|||||||
container_name, metadata={"container_type": container_type.name}
|
container_name, metadata={"container_type": container_type.name}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def delete_container(self, container_name: Container) -> None:
|
||||||
|
self.onefuzz.containers.delete(container_name)
|
||||||
|
|
||||||
def setup_notifications(self, config: Optional[NotificationConfig]) -> None:
|
def setup_notifications(self, config: Optional[NotificationConfig]) -> None:
|
||||||
if not config:
|
if not config:
|
||||||
return
|
return
|
||||||
@ -233,9 +253,58 @@ class JobHelper:
|
|||||||
}
|
}
|
||||||
self.wait_for_running = wait_for_running
|
self.wait_for_running = wait_for_running
|
||||||
|
|
||||||
|
def check_current_job(self) -> Job:
|
||||||
|
job = self.onefuzz.jobs.get(self.job.job_id)
|
||||||
|
if job.state in ["stopped", "stopping"]:
|
||||||
|
raise StoppedEarly("job unexpectedly stopped early")
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for task in self.onefuzz.tasks.list(job_id=self.job.job_id):
|
||||||
|
if task.state in ["stopped", "stopping"]:
|
||||||
|
if task.error:
|
||||||
|
errors.append("%s: %s" % (task.config.task.type, task.error))
|
||||||
|
else:
|
||||||
|
errors.append("%s" % task.config.task.type)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise StoppedEarly("tasks stopped unexpectedly.\n%s" % "\n".join(errors))
|
||||||
|
return job
|
||||||
|
|
||||||
|
def get_waiting(self) -> List[str]:
|
||||||
|
tasks = self.onefuzz.tasks.list(job_id=self.job.job_id)
|
||||||
|
waiting = [
|
||||||
|
"%s:%s" % (x.config.task.type.name, x.state.name)
|
||||||
|
for x in tasks
|
||||||
|
if x.state not in TaskState.has_started()
|
||||||
|
]
|
||||||
|
return waiting
|
||||||
|
|
||||||
|
def is_running(self) -> Tuple[bool, str, Any]:
|
||||||
|
waiting = self.get_waiting()
|
||||||
|
return (not waiting, "waiting on: %s" % ", ".join(sorted(waiting)), None)
|
||||||
|
|
||||||
|
def has_files(self) -> Tuple[bool, str, Any]:
|
||||||
|
self.check_current_job()
|
||||||
|
|
||||||
|
new = {
|
||||||
|
x: len(self.onefuzz.containers.files.list(x).files)
|
||||||
|
for x in self.to_monitor.keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
for container in new:
|
||||||
|
if new[container] > self.to_monitor[container]:
|
||||||
|
del self.to_monitor[container]
|
||||||
|
return (
|
||||||
|
not self.to_monitor,
|
||||||
|
"waiting for new files: %s" % ", ".join(self.to_monitor.keys()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
def wait(self) -> None:
|
def wait(self) -> None:
|
||||||
JobMonitor(self.onefuzz, self.job).wait(
|
JobMonitor(self.onefuzz, self.job).wait(
|
||||||
wait_for_running=self.wait_for_running, wait_for_files=self.to_monitor
|
wait_for_running=self.wait_for_running,
|
||||||
|
wait_for_files=self.to_monitor,
|
||||||
|
wait_for_stopped=self.wait_for_stopped,
|
||||||
)
|
)
|
||||||
|
|
||||||
def target_exe_blob_name(
|
def target_exe_blob_name(
|
||||||
|
@ -61,6 +61,36 @@ class Libfuzzer(Command):
|
|||||||
expect_crash_on_failure: bool = True,
|
expect_crash_on_failure: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
regression_containers = [
|
||||||
|
(ContainerType.setup, containers[ContainerType.setup]),
|
||||||
|
(ContainerType.crashes, containers[ContainerType.crashes]),
|
||||||
|
(ContainerType.unique_reports, containers[ContainerType.unique_reports]),
|
||||||
|
(
|
||||||
|
ContainerType.regression_reports,
|
||||||
|
containers[ContainerType.regression_reports],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.logger.info("creating libfuzzer_regression task")
|
||||||
|
regression_task = self.onefuzz.tasks.create(
|
||||||
|
job.job_id,
|
||||||
|
TaskType.libfuzzer_regression,
|
||||||
|
target_exe,
|
||||||
|
regression_containers,
|
||||||
|
pool_name=pool_name,
|
||||||
|
duration=duration,
|
||||||
|
vm_count=1,
|
||||||
|
reboot_after_setup=reboot_after_setup,
|
||||||
|
target_options=target_options,
|
||||||
|
target_env=target_env,
|
||||||
|
tags=tags,
|
||||||
|
target_timeout=crash_report_timeout,
|
||||||
|
check_retry_count=check_retry_count,
|
||||||
|
check_fuzzer_help=check_fuzzer_help,
|
||||||
|
debug=debug,
|
||||||
|
colocate=colocate_all_tasks or colocate_secondary_tasks,
|
||||||
|
)
|
||||||
|
|
||||||
fuzzer_containers = [
|
fuzzer_containers = [
|
||||||
(ContainerType.setup, containers[ContainerType.setup]),
|
(ContainerType.setup, containers[ContainerType.setup]),
|
||||||
(ContainerType.crashes, containers[ContainerType.crashes]),
|
(ContainerType.crashes, containers[ContainerType.crashes]),
|
||||||
@ -92,7 +122,7 @@ class Libfuzzer(Command):
|
|||||||
expect_crash_on_failure=expect_crash_on_failure,
|
expect_crash_on_failure=expect_crash_on_failure,
|
||||||
)
|
)
|
||||||
|
|
||||||
prereq_tasks = [fuzzer_task.task_id]
|
prereq_tasks = [fuzzer_task.task_id, regression_task.task_id]
|
||||||
|
|
||||||
coverage_containers = [
|
coverage_containers = [
|
||||||
(ContainerType.setup, containers[ContainerType.setup]),
|
(ContainerType.setup, containers[ContainerType.setup]),
|
||||||
@ -219,6 +249,7 @@ class Libfuzzer(Command):
|
|||||||
ContainerType.no_repro,
|
ContainerType.no_repro,
|
||||||
ContainerType.coverage,
|
ContainerType.coverage,
|
||||||
ContainerType.unique_inputs,
|
ContainerType.unique_inputs,
|
||||||
|
ContainerType.regression_reports,
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_inputs:
|
if existing_inputs:
|
||||||
|
284
src/cli/onefuzz/templates/regression.py
Normal file
284
src/cli/onefuzz/templates/regression.py
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Copyright (c) Microsoft Corporation.
|
||||||
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from onefuzztypes.enums import ContainerType, TaskDebugFlag, TaskType
|
||||||
|
from onefuzztypes.models import NotificationConfig, RegressionReport
|
||||||
|
from onefuzztypes.primitives import Container, Directory, File, PoolName
|
||||||
|
|
||||||
|
from onefuzz.api import Command
|
||||||
|
|
||||||
|
from . import JobHelper
|
||||||
|
|
||||||
|
|
||||||
|
class Regression(Command):
|
||||||
|
""" Regression job """
|
||||||
|
|
||||||
|
def _check_regression(self, container: Container, file: File) -> bool:
|
||||||
|
content = self.onefuzz.containers.files.get(Container(container), file)
|
||||||
|
as_str = content.decode()
|
||||||
|
as_obj = json.loads(as_str)
|
||||||
|
report = RegressionReport.parse_obj(as_obj)
|
||||||
|
|
||||||
|
if report.crash_test_result.crash_report is not None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if report.crash_test_result.no_repro is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
raise Exception("invalid crash report")
|
||||||
|
|
||||||
|
def generic(
|
||||||
|
self,
|
||||||
|
project: str,
|
||||||
|
name: str,
|
||||||
|
build: str,
|
||||||
|
pool_name: PoolName,
|
||||||
|
*,
|
||||||
|
reports: Optional[List[str]] = None,
|
||||||
|
crashes: Optional[List[File]] = None,
|
||||||
|
target_exe: File = File("fuzz.exe"),
|
||||||
|
tags: Optional[Dict[str, str]] = None,
|
||||||
|
notification_config: Optional[NotificationConfig] = None,
|
||||||
|
target_env: Optional[Dict[str, str]] = None,
|
||||||
|
setup_dir: Optional[Directory] = None,
|
||||||
|
reboot_after_setup: bool = False,
|
||||||
|
target_options: Optional[List[str]] = None,
|
||||||
|
dryrun: bool = False,
|
||||||
|
duration: int = 24,
|
||||||
|
crash_report_timeout: Optional[int] = None,
|
||||||
|
debug: Optional[List[TaskDebugFlag]] = None,
|
||||||
|
check_retry_count: Optional[int] = None,
|
||||||
|
check_fuzzer_help: bool = True,
|
||||||
|
delete_input_container: bool = True,
|
||||||
|
check_regressions: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
generic regression task
|
||||||
|
|
||||||
|
:param File crashes: Specify crashing input files to check in the regression task
|
||||||
|
:param str reports: Specify specific report names to verify in the regression task
|
||||||
|
:param bool check_regressions: Specify if exceptions should be thrown on finding crash regressions
|
||||||
|
:param bool delete_input_container: Specify wether or not to delete the input container
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._create_job(
|
||||||
|
TaskType.generic_regression,
|
||||||
|
project,
|
||||||
|
name,
|
||||||
|
build,
|
||||||
|
pool_name,
|
||||||
|
crashes=crashes,
|
||||||
|
reports=reports,
|
||||||
|
target_exe=target_exe,
|
||||||
|
tags=tags,
|
||||||
|
notification_config=notification_config,
|
||||||
|
target_env=target_env,
|
||||||
|
setup_dir=setup_dir,
|
||||||
|
reboot_after_setup=reboot_after_setup,
|
||||||
|
target_options=target_options,
|
||||||
|
dryrun=dryrun,
|
||||||
|
duration=duration,
|
||||||
|
crash_report_timeout=crash_report_timeout,
|
||||||
|
debug=debug,
|
||||||
|
check_retry_count=check_retry_count,
|
||||||
|
check_fuzzer_help=check_fuzzer_help,
|
||||||
|
delete_input_container=delete_input_container,
|
||||||
|
check_regressions=check_regressions,
|
||||||
|
)
|
||||||
|
|
||||||
|
def libfuzzer(
|
||||||
|
self,
|
||||||
|
project: str,
|
||||||
|
name: str,
|
||||||
|
build: str,
|
||||||
|
pool_name: PoolName,
|
||||||
|
*,
|
||||||
|
reports: Optional[List[str]] = None,
|
||||||
|
crashes: Optional[List[File]] = None,
|
||||||
|
target_exe: File = File("fuzz.exe"),
|
||||||
|
tags: Optional[Dict[str, str]] = None,
|
||||||
|
notification_config: Optional[NotificationConfig] = None,
|
||||||
|
target_env: Optional[Dict[str, str]] = None,
|
||||||
|
setup_dir: Optional[Directory] = None,
|
||||||
|
reboot_after_setup: bool = False,
|
||||||
|
target_options: Optional[List[str]] = None,
|
||||||
|
dryrun: bool = False,
|
||||||
|
duration: int = 24,
|
||||||
|
crash_report_timeout: Optional[int] = None,
|
||||||
|
debug: Optional[List[TaskDebugFlag]] = None,
|
||||||
|
check_retry_count: Optional[int] = None,
|
||||||
|
check_fuzzer_help: bool = True,
|
||||||
|
delete_input_container: bool = True,
|
||||||
|
check_regressions: bool = False,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
"""
|
||||||
|
libfuzzer regression task
|
||||||
|
|
||||||
|
:param File crashes: Specify crashing input files to check in the regression task
|
||||||
|
:param str reports: Specify specific report names to verify in the regression task
|
||||||
|
:param bool check_regressions: Specify if exceptions should be thrown on finding crash regressions
|
||||||
|
:param bool delete_input_container: Specify wether or not to delete the input container
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._create_job(
|
||||||
|
TaskType.libfuzzer_regression,
|
||||||
|
project,
|
||||||
|
name,
|
||||||
|
build,
|
||||||
|
pool_name,
|
||||||
|
crashes=crashes,
|
||||||
|
reports=reports,
|
||||||
|
target_exe=target_exe,
|
||||||
|
tags=tags,
|
||||||
|
notification_config=notification_config,
|
||||||
|
target_env=target_env,
|
||||||
|
setup_dir=setup_dir,
|
||||||
|
reboot_after_setup=reboot_after_setup,
|
||||||
|
target_options=target_options,
|
||||||
|
dryrun=dryrun,
|
||||||
|
duration=duration,
|
||||||
|
crash_report_timeout=crash_report_timeout,
|
||||||
|
debug=debug,
|
||||||
|
check_retry_count=check_retry_count,
|
||||||
|
check_fuzzer_help=check_fuzzer_help,
|
||||||
|
delete_input_container=delete_input_container,
|
||||||
|
check_regressions=check_regressions,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_job(
|
||||||
|
self,
|
||||||
|
task_type: TaskType,
|
||||||
|
project: str,
|
||||||
|
name: str,
|
||||||
|
build: str,
|
||||||
|
pool_name: PoolName,
|
||||||
|
*,
|
||||||
|
crashes: Optional[List[File]] = None,
|
||||||
|
reports: Optional[List[str]] = None,
|
||||||
|
target_exe: File = File("fuzz.exe"),
|
||||||
|
tags: Optional[Dict[str, str]] = None,
|
||||||
|
notification_config: Optional[NotificationConfig] = None,
|
||||||
|
target_env: Optional[Dict[str, str]] = None,
|
||||||
|
setup_dir: Optional[Directory] = None,
|
||||||
|
reboot_after_setup: bool = False,
|
||||||
|
target_options: Optional[List[str]] = None,
|
||||||
|
dryrun: bool = False,
|
||||||
|
duration: int = 24,
|
||||||
|
crash_report_timeout: Optional[int] = None,
|
||||||
|
debug: Optional[List[TaskDebugFlag]] = None,
|
||||||
|
check_retry_count: Optional[int] = None,
|
||||||
|
check_fuzzer_help: bool = True,
|
||||||
|
delete_input_container: bool = True,
|
||||||
|
check_regressions: bool = False,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
if dryrun:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.logger.info("creating regression task from template")
|
||||||
|
|
||||||
|
helper = JobHelper(
|
||||||
|
self.onefuzz,
|
||||||
|
self.logger,
|
||||||
|
project,
|
||||||
|
name,
|
||||||
|
build,
|
||||||
|
duration,
|
||||||
|
pool_name=pool_name,
|
||||||
|
target_exe=target_exe,
|
||||||
|
)
|
||||||
|
|
||||||
|
helper.define_containers(
|
||||||
|
ContainerType.setup,
|
||||||
|
ContainerType.crashes,
|
||||||
|
ContainerType.reports,
|
||||||
|
ContainerType.no_repro,
|
||||||
|
ContainerType.unique_reports,
|
||||||
|
ContainerType.regression_reports,
|
||||||
|
)
|
||||||
|
|
||||||
|
containers = [
|
||||||
|
(ContainerType.setup, helper.containers[ContainerType.setup]),
|
||||||
|
(ContainerType.crashes, helper.containers[ContainerType.crashes]),
|
||||||
|
(ContainerType.reports, helper.containers[ContainerType.reports]),
|
||||||
|
(ContainerType.no_repro, helper.containers[ContainerType.no_repro]),
|
||||||
|
(
|
||||||
|
ContainerType.unique_reports,
|
||||||
|
helper.containers[ContainerType.unique_reports],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ContainerType.regression_reports,
|
||||||
|
helper.containers[ContainerType.regression_reports],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if crashes:
|
||||||
|
helper.containers[
|
||||||
|
ContainerType.readonly_inputs
|
||||||
|
] = helper.get_unique_container_name(ContainerType.readonly_inputs)
|
||||||
|
containers.append(
|
||||||
|
(
|
||||||
|
ContainerType.readonly_inputs,
|
||||||
|
helper.containers[ContainerType.readonly_inputs],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
helper.create_containers()
|
||||||
|
if crashes:
|
||||||
|
for file in crashes:
|
||||||
|
self.onefuzz.containers.files.upload_file(
|
||||||
|
helper.containers[ContainerType.readonly_inputs], file
|
||||||
|
)
|
||||||
|
|
||||||
|
helper.setup_notifications(notification_config)
|
||||||
|
|
||||||
|
helper.upload_setup(setup_dir, target_exe)
|
||||||
|
target_exe_blob_name = helper.target_exe_blob_name(target_exe, setup_dir)
|
||||||
|
|
||||||
|
self.logger.info("creating regression task")
|
||||||
|
task = self.onefuzz.tasks.create(
|
||||||
|
helper.job.job_id,
|
||||||
|
task_type,
|
||||||
|
target_exe_blob_name,
|
||||||
|
containers,
|
||||||
|
pool_name=pool_name,
|
||||||
|
duration=duration,
|
||||||
|
vm_count=1,
|
||||||
|
reboot_after_setup=reboot_after_setup,
|
||||||
|
target_options=target_options,
|
||||||
|
target_env=target_env,
|
||||||
|
tags=tags,
|
||||||
|
target_timeout=crash_report_timeout,
|
||||||
|
check_retry_count=check_retry_count,
|
||||||
|
debug=debug,
|
||||||
|
check_fuzzer_help=check_fuzzer_help,
|
||||||
|
report_list=reports,
|
||||||
|
)
|
||||||
|
helper.wait_for_stopped = check_regressions
|
||||||
|
|
||||||
|
self.logger.info("done creating tasks")
|
||||||
|
helper.wait()
|
||||||
|
|
||||||
|
if check_regressions:
|
||||||
|
task = self.onefuzz.tasks.get(task.task_id)
|
||||||
|
if task.error:
|
||||||
|
raise Exception("task failed: %s", task.error)
|
||||||
|
|
||||||
|
container = helper.containers[ContainerType.regression_reports]
|
||||||
|
for filename in self.onefuzz.containers.files.list(container).files:
|
||||||
|
self.logger.info("checking file: %s", filename)
|
||||||
|
if self._check_regression(container, File(filename)):
|
||||||
|
raise Exception(f"regression identified: {filename}")
|
||||||
|
self.logger.info("no regressions")
|
||||||
|
|
||||||
|
if (
|
||||||
|
delete_input_container
|
||||||
|
and ContainerType.readonly_inputs in helper.containers
|
||||||
|
):
|
||||||
|
helper.delete_container(helper.containers[ContainerType.readonly_inputs])
|
156
src/integration-tests/check-regression.py
Executable file
156
src/integration-tests/check-regression.py
Executable file
@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Copyright (c) Microsoft Corporation.
|
||||||
|
# Licensed under the MIT License.
|
||||||
|
#
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from onefuzz.api import Command, Onefuzz
|
||||||
|
from onefuzz.cli import execute_api
|
||||||
|
from onefuzztypes.enums import OS, ContainerType, TaskState, TaskType
|
||||||
|
from onefuzztypes.models import Job, RegressionReport
|
||||||
|
from onefuzztypes.primitives import Container, Directory, File, PoolName
|
||||||
|
|
||||||
|
|
||||||
|
class Run(Command):
|
||||||
|
def cleanup(self, test_id: UUID):
|
||||||
|
for pool in self.onefuzz.pools.list():
|
||||||
|
if str(test_id) in pool.name:
|
||||||
|
self.onefuzz.pools.shutdown(pool.name, now=True)
|
||||||
|
|
||||||
|
self.onefuzz.template.stop(
|
||||||
|
str(test_id), "linux-libfuzzer", build=None, delete_containers=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def _wait_for_regression_task(self, job: Job) -> None:
|
||||||
|
while True:
|
||||||
|
self.logger.info("waiting for regression task to finish")
|
||||||
|
for task in self.onefuzz.jobs.tasks.list(job.job_id):
|
||||||
|
if task.config.task.type not in [
|
||||||
|
TaskType.libfuzzer_regression,
|
||||||
|
TaskType.generic_regression,
|
||||||
|
]:
|
||||||
|
continue
|
||||||
|
if task.state != TaskState.stopped:
|
||||||
|
continue
|
||||||
|
return
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
def _check_regression(self, job: Job) -> bool:
|
||||||
|
# get the regression reports containers for the job
|
||||||
|
results = self.onefuzz.jobs.containers.list(
|
||||||
|
job.job_id, ContainerType.regression_reports
|
||||||
|
)
|
||||||
|
|
||||||
|
# expect one and only one regression report container
|
||||||
|
if len(results) != 1:
|
||||||
|
raise Exception(f"unexpected regression containers: {results}")
|
||||||
|
container = list(results.keys())[0]
|
||||||
|
|
||||||
|
# expect one and only one file in the container
|
||||||
|
if len(results[container]) != 1:
|
||||||
|
raise Exception(f"unexpected regression container output: {results}")
|
||||||
|
file = results[container][0]
|
||||||
|
|
||||||
|
# get the regression report
|
||||||
|
content = self.onefuzz.containers.files.get(Container(container), file)
|
||||||
|
as_str = content.decode()
|
||||||
|
as_obj = json.loads(as_str)
|
||||||
|
report = RegressionReport.parse_obj(as_obj)
|
||||||
|
|
||||||
|
if report.crash_test_result.crash_report is not None:
|
||||||
|
self.logger.info("regression report has crash report")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if report.crash_test_result.no_repro is not None:
|
||||||
|
self.logger.info("regression report has no-repro")
|
||||||
|
return False
|
||||||
|
|
||||||
|
raise Exception(f"unexpected report: {report}")
|
||||||
|
|
||||||
|
def _run_job(
|
||||||
|
self, test_id: UUID, pool: PoolName, target: str, exe: File, build: int
|
||||||
|
) -> Job:
|
||||||
|
if build == 1:
|
||||||
|
wait_for_files = [ContainerType.unique_reports]
|
||||||
|
else:
|
||||||
|
wait_for_files = [ContainerType.regression_reports]
|
||||||
|
job = self.onefuzz.template.libfuzzer.basic(
|
||||||
|
str(test_id),
|
||||||
|
target,
|
||||||
|
str(build),
|
||||||
|
pool,
|
||||||
|
target_exe=exe,
|
||||||
|
duration=1,
|
||||||
|
vm_count=1,
|
||||||
|
wait_for_files=wait_for_files,
|
||||||
|
)
|
||||||
|
if job is None:
|
||||||
|
raise Exception(f"invalid job: {target} {build}")
|
||||||
|
|
||||||
|
if build > 1:
|
||||||
|
self._wait_for_regression_task(job)
|
||||||
|
self.onefuzz.template.stop(str(test_id), target, str(build))
|
||||||
|
return job
|
||||||
|
|
||||||
|
def _run(self, target_os: OS, test_id: UUID, base: Directory, target: str) -> None:
|
||||||
|
pool = PoolName(f"{target}-{target_os.name}-{test_id}")
|
||||||
|
self.onefuzz.pools.create(pool, target_os)
|
||||||
|
self.onefuzz.scalesets.create(pool, 5)
|
||||||
|
broken = File(os.path.join(base, target, "broken.exe"))
|
||||||
|
fixed = File(os.path.join(base, target, "fixed.exe"))
|
||||||
|
|
||||||
|
self.logger.info("starting first build")
|
||||||
|
self._run_job(test_id, pool, target, broken, 1)
|
||||||
|
|
||||||
|
self.logger.info("starting second build")
|
||||||
|
job = self._run_job(test_id, pool, target, fixed, 2)
|
||||||
|
if self._check_regression(job):
|
||||||
|
raise Exception("fixed binary should be a no repro")
|
||||||
|
|
||||||
|
self.logger.info("starting third build")
|
||||||
|
job = self._run_job(test_id, pool, target, broken, 3)
|
||||||
|
if not self._check_regression(job):
|
||||||
|
raise Exception("broken binary should be a crash report")
|
||||||
|
|
||||||
|
self.onefuzz.pools.shutdown(pool, now=True)
|
||||||
|
|
||||||
|
def test(
|
||||||
|
self,
|
||||||
|
samples: Directory,
|
||||||
|
*,
|
||||||
|
endpoint: Optional[str] = None,
|
||||||
|
):
|
||||||
|
test_id = uuid4()
|
||||||
|
self.logger.info(f"launch test {test_id}")
|
||||||
|
self.onefuzz.__setup__(endpoint=endpoint)
|
||||||
|
error: Optional[Exception] = None
|
||||||
|
try:
|
||||||
|
self._run(OS.linux, test_id, samples, "linux-libfuzzer-regression")
|
||||||
|
except Exception as err:
|
||||||
|
error = err
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.logger.warning("interruptted")
|
||||||
|
finally:
|
||||||
|
self.logger.info("cleaning up tests")
|
||||||
|
self.cleanup(test_id)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise error
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
return execute_api(
|
||||||
|
Run(Onefuzz(), logging.getLogger("regression")), [Command], "0.0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
21
src/integration-tests/git-bisect/README.md
Normal file
21
src/integration-tests/git-bisect/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# git-bisect regression source
|
||||||
|
|
||||||
|
This assumes you have a working clang with libfuzzer, bash, and git.
|
||||||
|
|
||||||
|
This makes a git repo `test` with 9 commits. Each commit after the first adds a bug.
|
||||||
|
|
||||||
|
* `commit 0` has no bugs.
|
||||||
|
* `commit 1` will additionally cause an abort if the input is `1`.
|
||||||
|
* `commit 2` will additionally cause an abort if the input is `2`.
|
||||||
|
* `commit 3` will additionally cause an abort if the input is `3`.
|
||||||
|
* etc.
|
||||||
|
|
||||||
|
This directory provides exemplar scripts that demonstrate how to perform
|
||||||
|
`git bisect` with libfuzzer.
|
||||||
|
|
||||||
|
* [run-local.sh](run-local.sh) builds & runs the libfuzzer target locally. It uses [src/bisect-local.sh](src/bisect-local.sh) as the `git bisect run` command.
|
||||||
|
* [run-onefuzz.sh](run-onefuzz.sh) builds the libfuzzer target locally, but uses OneFuzz to run the regression tasks. It uses [src/bisect-onefuzz.sh](src/bisect-onefuzz.sh) as the `git bisect run` command.
|
||||||
|
|
||||||
|
With each project having their own unique paradigm for building, this model
|
||||||
|
allows plugging OneFuzz as a `bisect` command in whatever fashion your
|
||||||
|
project requires.
|
18
src/integration-tests/git-bisect/build.sh
Executable file
18
src/integration-tests/git-bisect/build.sh
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rm -rf test
|
||||||
|
|
||||||
|
git init test
|
||||||
|
(cd test; git config user.name "Example"; git config user.email example@contoso.com)
|
||||||
|
(cp src/Makefile test; cd test; git add Makefile)
|
||||||
|
for i in $(seq 0 8); do
|
||||||
|
cp src/fuzz.c test/fuzz.c
|
||||||
|
for j in $(seq $i 8); do
|
||||||
|
if [ $i != $j ]; then
|
||||||
|
sed -i /TEST$j/d test/fuzz.c
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
(cd test; git add fuzz.c; git commit -m "commit $i")
|
||||||
|
done
|
17
src/integration-tests/git-bisect/run-local.sh
Executable file
17
src/integration-tests/git-bisect/run-local.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# build our git repo with our samples in `test`
|
||||||
|
# (note, we don't care about the output of this script)
|
||||||
|
./build.sh 2>/dev/null > /dev/null
|
||||||
|
|
||||||
|
# create our crashing input
|
||||||
|
echo -n '3' > test/test.txt
|
||||||
|
|
||||||
|
cd test
|
||||||
|
|
||||||
|
# start the bisect, looking from HEAD backwards 8 commits
|
||||||
|
git bisect start HEAD HEAD~8 --
|
||||||
|
git bisect run ../src/bisect-local.sh test.txt
|
||||||
|
git bisect reset
|
17
src/integration-tests/git-bisect/run-onefuzz.sh
Executable file
17
src/integration-tests/git-bisect/run-onefuzz.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# build our git repo with our samples in `test`
|
||||||
|
# (note, we don't care about the output of this script)
|
||||||
|
./build.sh 2>/dev/null > /dev/null
|
||||||
|
|
||||||
|
# create our crashing input
|
||||||
|
echo -n '3' > test/test.txt
|
||||||
|
|
||||||
|
cd test
|
||||||
|
|
||||||
|
# start the bisect, looking from HEAD backwards 8 commits
|
||||||
|
git bisect start HEAD HEAD~8 --
|
||||||
|
git bisect run ../src/bisect-onefuzz.sh test.txt
|
||||||
|
git bisect reset
|
13
src/integration-tests/git-bisect/src/Makefile
Normal file
13
src/integration-tests/git-bisect/src/Makefile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CC=clang
|
||||||
|
|
||||||
|
CFLAGS=-g3 -fsanitize=fuzzer -fsanitize=address
|
||||||
|
|
||||||
|
all: fuzz.exe
|
||||||
|
|
||||||
|
fuzz.exe: fuzz.c
|
||||||
|
$(CC) $(CFLAGS) fuzz.c -o fuzz.exe
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -f fuzz.exe
|
7
src/integration-tests/git-bisect/src/bisect-local.sh
Executable file
7
src/integration-tests/git-bisect/src/bisect-local.sh
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
make clean
|
||||||
|
make
|
||||||
|
./fuzz.exe $*
|
12
src/integration-tests/git-bisect/src/bisect-onefuzz.sh
Executable file
12
src/integration-tests/git-bisect/src/bisect-onefuzz.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROJECT=${PROJECT:-regression-test}
|
||||||
|
TARGET=${TARGET:-$(uuidgen)}
|
||||||
|
BUILD=regression-$(git rev-parse HEAD)
|
||||||
|
POOL=${ONEFUZZ_POOL:-linux}
|
||||||
|
|
||||||
|
make clean
|
||||||
|
make
|
||||||
|
onefuzz template regression libfuzzer ${PROJECT} ${TARGET} ${BUILD} ${POOL} --check_regressions --delete_input_container --reports --crashes $*
|
13
src/integration-tests/git-bisect/src/fuzz.c
Normal file
13
src/integration-tests/git-bisect/src/fuzz.c
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
int LLVMFuzzerTestOneInput(char *data, size_t len) {
|
||||||
|
if (len != 1) { return 0; }
|
||||||
|
if (data[0] == '1') { abort(); } // TEST1
|
||||||
|
if (data[0] == '2') { abort(); } // TEST2
|
||||||
|
if (data[0] == '3') { abort(); } // TEST3
|
||||||
|
if (data[0] == '4') { abort(); } // TEST4
|
||||||
|
if (data[0] == '5') { abort(); } // TEST5
|
||||||
|
if (data[0] == '6') { abort(); } // TEST6
|
||||||
|
if (data[0] == '7') { abort(); } // TEST7
|
||||||
|
if (data[0] == '8') { abort(); } // TEST8
|
||||||
|
return 0;
|
||||||
|
}
|
16
src/integration-tests/libfuzzer-regression/Makefile
Normal file
16
src/integration-tests/libfuzzer-regression/Makefile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
CC=clang
|
||||||
|
|
||||||
|
CFLAGS=-g3 -fsanitize=fuzzer -fsanitize=address
|
||||||
|
|
||||||
|
all: broken.exe fixed.exe
|
||||||
|
|
||||||
|
broken.exe: simple.c
|
||||||
|
$(CC) $(CFLAGS) simple.c -o broken.exe
|
||||||
|
|
||||||
|
fixed.exe: simple.c
|
||||||
|
$(CC) $(CFLAGS) simple.c -o fixed.exe -DFIXED
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f broken.exe fixed.exe
|
@ -0,0 +1 @@
|
|||||||
|
good
|
26
src/integration-tests/libfuzzer-regression/simple.c
Normal file
26
src/integration-tests/libfuzzer-regression/simple.c
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
|
||||||
|
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t len) {
|
||||||
|
int cnt = 0;
|
||||||
|
|
||||||
|
if (len < 3) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[0] == 'x') { cnt++; }
|
||||||
|
if (data[1] == 'y') { cnt++; }
|
||||||
|
if (data[2] == 'z') { cnt++; }
|
||||||
|
|
||||||
|
#ifndef FIXED
|
||||||
|
if (cnt >= 3) {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
@ -31,6 +31,7 @@ from onefuzztypes.events import (
|
|||||||
EventProxyCreated,
|
EventProxyCreated,
|
||||||
EventProxyDeleted,
|
EventProxyDeleted,
|
||||||
EventProxyFailed,
|
EventProxyFailed,
|
||||||
|
EventRegressionReported,
|
||||||
EventScalesetCreated,
|
EventScalesetCreated,
|
||||||
EventScalesetDeleted,
|
EventScalesetDeleted,
|
||||||
EventScalesetFailed,
|
EventScalesetFailed,
|
||||||
@ -45,8 +46,10 @@ from onefuzztypes.events import (
|
|||||||
)
|
)
|
||||||
from onefuzztypes.models import (
|
from onefuzztypes.models import (
|
||||||
BlobRef,
|
BlobRef,
|
||||||
|
CrashTestResult,
|
||||||
Error,
|
Error,
|
||||||
JobConfig,
|
JobConfig,
|
||||||
|
RegressionReport,
|
||||||
Report,
|
Report,
|
||||||
TaskConfig,
|
TaskConfig,
|
||||||
TaskContainers,
|
TaskContainers,
|
||||||
@ -87,6 +90,24 @@ def main() -> None:
|
|||||||
],
|
],
|
||||||
tags={},
|
tags={},
|
||||||
)
|
)
|
||||||
|
report = Report(
|
||||||
|
input_blob=BlobRef(
|
||||||
|
account="contoso-storage-account",
|
||||||
|
container=Container("crashes"),
|
||||||
|
name="input.txt",
|
||||||
|
),
|
||||||
|
executable="fuzz.exe",
|
||||||
|
crash_type="example crash report type",
|
||||||
|
crash_site="example crash site",
|
||||||
|
call_stack=["#0 line", "#1 line", "#2 line"],
|
||||||
|
call_stack_sha256=ZERO_SHA256,
|
||||||
|
input_sha256=EMPTY_SHA256,
|
||||||
|
asan_log="example asan log",
|
||||||
|
task_id=UUID(int=0),
|
||||||
|
job_id=UUID(int=0),
|
||||||
|
scariness_score=10,
|
||||||
|
scariness_description="example-scariness",
|
||||||
|
)
|
||||||
examples: List[Event] = [
|
examples: List[Event] = [
|
||||||
EventPing(ping_id=UUID(int=0)),
|
EventPing(ping_id=UUID(int=0)),
|
||||||
EventTaskCreated(
|
EventTaskCreated(
|
||||||
@ -193,27 +214,18 @@ def main() -> None:
|
|||||||
pool_name=PoolName("example"),
|
pool_name=PoolName("example"),
|
||||||
state=NodeState.setting_up,
|
state=NodeState.setting_up,
|
||||||
),
|
),
|
||||||
|
EventRegressionReported(
|
||||||
|
regression_report=RegressionReport(
|
||||||
|
crash_test_result=CrashTestResult(crash_report=report),
|
||||||
|
original_crash_test_result=CrashTestResult(crash_report=report),
|
||||||
|
),
|
||||||
|
container=Container("container-name"),
|
||||||
|
filename="example.json",
|
||||||
|
),
|
||||||
EventCrashReported(
|
EventCrashReported(
|
||||||
container=Container("container-name"),
|
container=Container("container-name"),
|
||||||
filename="example.json",
|
filename="example.json",
|
||||||
report=Report(
|
report=report,
|
||||||
input_blob=BlobRef(
|
|
||||||
account="contoso-storage-account",
|
|
||||||
container=Container("crashes"),
|
|
||||||
name="input.txt",
|
|
||||||
),
|
|
||||||
executable="fuzz.exe",
|
|
||||||
crash_type="example crash report type",
|
|
||||||
crash_site="example crash site",
|
|
||||||
call_stack=["#0 line", "#1 line", "#2 line"],
|
|
||||||
call_stack_sha256=ZERO_SHA256,
|
|
||||||
input_sha256=EMPTY_SHA256,
|
|
||||||
asan_log="example asan log",
|
|
||||||
task_id=UUID(int=0),
|
|
||||||
job_id=UUID(int=0),
|
|
||||||
scariness_score=10,
|
|
||||||
scariness_description="example-scariness",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
EventFileAdded(container=Container("container-name"), filename="example.txt"),
|
EventFileAdded(container=Container("container-name"), filename="example.txt"),
|
||||||
EventNodeHeartbeat(machine_id=UUID(int=0), pool_name=PoolName("example")),
|
EventNodeHeartbeat(machine_id=UUID(int=0), pool_name=PoolName("example")),
|
||||||
|
@ -78,6 +78,7 @@ class TaskFeature(Enum):
|
|||||||
preserve_existing_outputs = "preserve_existing_outputs"
|
preserve_existing_outputs = "preserve_existing_outputs"
|
||||||
check_fuzzer_help = "check_fuzzer_help"
|
check_fuzzer_help = "check_fuzzer_help"
|
||||||
expect_crash_on_failure = "expect_crash_on_failure"
|
expect_crash_on_failure = "expect_crash_on_failure"
|
||||||
|
report_list = "report_list"
|
||||||
|
|
||||||
|
|
||||||
# Permissions for an Azure Blob Storage Container.
|
# Permissions for an Azure Blob Storage Container.
|
||||||
@ -149,11 +150,13 @@ class TaskType(Enum):
|
|||||||
libfuzzer_coverage = "libfuzzer_coverage"
|
libfuzzer_coverage = "libfuzzer_coverage"
|
||||||
libfuzzer_crash_report = "libfuzzer_crash_report"
|
libfuzzer_crash_report = "libfuzzer_crash_report"
|
||||||
libfuzzer_merge = "libfuzzer_merge"
|
libfuzzer_merge = "libfuzzer_merge"
|
||||||
|
libfuzzer_regression = "libfuzzer_regression"
|
||||||
generic_analysis = "generic_analysis"
|
generic_analysis = "generic_analysis"
|
||||||
generic_supervisor = "generic_supervisor"
|
generic_supervisor = "generic_supervisor"
|
||||||
generic_merge = "generic_merge"
|
generic_merge = "generic_merge"
|
||||||
generic_generator = "generic_generator"
|
generic_generator = "generic_generator"
|
||||||
generic_crash_report = "generic_crash_report"
|
generic_crash_report = "generic_crash_report"
|
||||||
|
generic_regression = "generic_regression"
|
||||||
|
|
||||||
|
|
||||||
class VmState(Enum):
|
class VmState(Enum):
|
||||||
@ -207,6 +210,7 @@ class ContainerType(Enum):
|
|||||||
tools = "tools"
|
tools = "tools"
|
||||||
unique_inputs = "unique_inputs"
|
unique_inputs = "unique_inputs"
|
||||||
unique_reports = "unique_reports"
|
unique_reports = "unique_reports"
|
||||||
|
regression_reports = "regression_reports"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset_defaults(cls) -> List["ContainerType"]:
|
def reset_defaults(cls) -> List["ContainerType"]:
|
||||||
@ -219,8 +223,9 @@ class ContainerType(Enum):
|
|||||||
cls.readonly_inputs,
|
cls.readonly_inputs,
|
||||||
cls.reports,
|
cls.reports,
|
||||||
cls.setup,
|
cls.setup,
|
||||||
cls.unique_reports,
|
|
||||||
cls.unique_inputs,
|
cls.unique_inputs,
|
||||||
|
cls.unique_reports,
|
||||||
|
cls.regression_reports,
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -11,7 +11,15 @@ from uuid import UUID, uuid4
|
|||||||
from pydantic import BaseModel, Extra, Field
|
from pydantic import BaseModel, Extra, Field
|
||||||
|
|
||||||
from .enums import OS, Architecture, NodeState, TaskState, TaskType
|
from .enums import OS, Architecture, NodeState, TaskState, TaskType
|
||||||
from .models import AutoScaleConfig, Error, JobConfig, Report, TaskConfig, UserInfo
|
from .models import (
|
||||||
|
AutoScaleConfig,
|
||||||
|
Error,
|
||||||
|
JobConfig,
|
||||||
|
RegressionReport,
|
||||||
|
Report,
|
||||||
|
TaskConfig,
|
||||||
|
UserInfo,
|
||||||
|
)
|
||||||
from .primitives import Container, PoolName, Region
|
from .primitives import Container, PoolName, Region
|
||||||
from .responses import BaseResponse
|
from .responses import BaseResponse
|
||||||
|
|
||||||
@ -156,6 +164,12 @@ class EventCrashReported(BaseEvent):
|
|||||||
filename: str
|
filename: str
|
||||||
|
|
||||||
|
|
||||||
|
class EventRegressionReported(BaseEvent):
|
||||||
|
regression_report: RegressionReport
|
||||||
|
container: Container
|
||||||
|
filename: str
|
||||||
|
|
||||||
|
|
||||||
class EventFileAdded(BaseEvent):
|
class EventFileAdded(BaseEvent):
|
||||||
container: Container
|
container: Container
|
||||||
filename: str
|
filename: str
|
||||||
@ -183,6 +197,7 @@ Event = Union[
|
|||||||
EventTaskStopped,
|
EventTaskStopped,
|
||||||
EventTaskHeartbeat,
|
EventTaskHeartbeat,
|
||||||
EventCrashReported,
|
EventCrashReported,
|
||||||
|
EventRegressionReported,
|
||||||
EventFileAdded,
|
EventFileAdded,
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -207,6 +222,7 @@ class EventType(Enum):
|
|||||||
task_state_updated = "task_state_updated"
|
task_state_updated = "task_state_updated"
|
||||||
task_stopped = "task_stopped"
|
task_stopped = "task_stopped"
|
||||||
crash_reported = "crash_reported"
|
crash_reported = "crash_reported"
|
||||||
|
regression_reported = "regression_reported"
|
||||||
file_added = "file_added"
|
file_added = "file_added"
|
||||||
task_heartbeat = "task_heartbeat"
|
task_heartbeat = "task_heartbeat"
|
||||||
node_heartbeat = "node_heartbeat"
|
node_heartbeat = "node_heartbeat"
|
||||||
@ -234,6 +250,7 @@ EventTypeMap = {
|
|||||||
EventType.task_heartbeat: EventTaskHeartbeat,
|
EventType.task_heartbeat: EventTaskHeartbeat,
|
||||||
EventType.task_stopped: EventTaskStopped,
|
EventType.task_stopped: EventTaskStopped,
|
||||||
EventType.crash_reported: EventCrashReported,
|
EventType.crash_reported: EventCrashReported,
|
||||||
|
EventType.regression_reported: EventRegressionReported,
|
||||||
EventType.file_added: EventFileAdded,
|
EventType.file_added: EventFileAdded,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +170,7 @@ class TaskDetails(BaseModel):
|
|||||||
target_timeout: Optional[int]
|
target_timeout: Optional[int]
|
||||||
ensemble_sync_delay: Optional[int]
|
ensemble_sync_delay: Optional[int]
|
||||||
preserve_existing_outputs: Optional[bool]
|
preserve_existing_outputs: Optional[bool]
|
||||||
|
report_list: Optional[List[str]]
|
||||||
|
|
||||||
@validator("check_retry_count", allow_reuse=True)
|
@validator("check_retry_count", allow_reuse=True)
|
||||||
def validate_check_retry_count(cls, value: int) -> int:
|
def validate_check_retry_count(cls, value: int) -> int:
|
||||||
@ -237,7 +238,7 @@ class BlobRef(BaseModel):
|
|||||||
|
|
||||||
class Report(BaseModel):
|
class Report(BaseModel):
|
||||||
input_url: Optional[str]
|
input_url: Optional[str]
|
||||||
input_blob: BlobRef
|
input_blob: Optional[BlobRef]
|
||||||
executable: str
|
executable: str
|
||||||
crash_type: str
|
crash_type: str
|
||||||
crash_site: str
|
crash_site: str
|
||||||
@ -251,6 +252,26 @@ class Report(BaseModel):
|
|||||||
scariness_description: Optional[str]
|
scariness_description: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class NoReproReport(BaseModel):
|
||||||
|
input_sha256: str
|
||||||
|
input_blob: Optional[BlobRef]
|
||||||
|
executable: str
|
||||||
|
task_id: UUID
|
||||||
|
job_id: UUID
|
||||||
|
tries: int
|
||||||
|
error: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class CrashTestResult(BaseModel):
|
||||||
|
crash_report: Optional[Report]
|
||||||
|
no_repro: Optional[NoReproReport]
|
||||||
|
|
||||||
|
|
||||||
|
class RegressionReport(BaseModel):
|
||||||
|
crash_test_result: CrashTestResult
|
||||||
|
original_crash_test_result: Optional[CrashTestResult]
|
||||||
|
|
||||||
|
|
||||||
class ADODuplicateTemplate(BaseModel):
|
class ADODuplicateTemplate(BaseModel):
|
||||||
increment: List[str]
|
increment: List[str]
|
||||||
comment: Optional[str]
|
comment: Optional[str]
|
||||||
@ -377,6 +398,7 @@ class TaskUnitConfig(BaseModel):
|
|||||||
stats_file: Optional[str]
|
stats_file: Optional[str]
|
||||||
stats_format: Optional[StatsFormat]
|
stats_format: Optional[StatsFormat]
|
||||||
ensemble_sync_delay: Optional[int]
|
ensemble_sync_delay: Optional[int]
|
||||||
|
report_list: Optional[List[str]]
|
||||||
|
|
||||||
# from here forwards are Container definitions. These need to be inline
|
# from here forwards are Container definitions. These need to be inline
|
||||||
# with TaskDefinitions and ContainerTypes
|
# with TaskDefinitions and ContainerTypes
|
||||||
@ -390,6 +412,7 @@ class TaskUnitConfig(BaseModel):
|
|||||||
tools: CONTAINER_DEF
|
tools: CONTAINER_DEF
|
||||||
unique_inputs: CONTAINER_DEF
|
unique_inputs: CONTAINER_DEF
|
||||||
unique_reports: CONTAINER_DEF
|
unique_reports: CONTAINER_DEF
|
||||||
|
regression_reports: CONTAINER_DEF
|
||||||
|
|
||||||
|
|
||||||
class Forward(BaseModel):
|
class Forward(BaseModel):
|
||||||
|
Reference in New Issue
Block a user