Skip to content

Commit e0f0b0b

Browse files
lebe-gV8-internal LUCI CQ
authored andcommitted
Try minimizing crashes of instrumented runs
When a program instrumented by RuntimeAssistedMutator crashes, we avoid calling processCrash() on it immediately. This is because the boilerplate code added during instrumentation makes such crashes difficult to minimize (see, e.g., https://g-issues.chromium.org/issues/488963988?pli=1&authuser=0). Instead, we follow this procedure: 1. Always log the crash of the instrumented program. 2. Check if the process()'d version of the instrumented program also crashes. 3. If yes, we call processCrash() on that program instead, as its more straightforward to minimize. Bug: 488963988 Change-Id: Iffefc9435f4ef31a3fbf798d374a04f9f1fc115a Reviewed-on: https://chrome-internal-review.googlesource.com/c/v8/fuzzilli/+/9129498 Reviewed-by: Matthias Liedtke <mliedtke@google.com> Commit-Queue: Leon Bettscheider <bettscheider@google.com>
1 parent 43fa174 commit e0f0b0b

4 files changed

Lines changed: 190 additions & 16 deletions

File tree

Sources/Fuzzilli/Fuzzer.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -789,25 +789,34 @@ public class Fuzzer {
789789
return true
790790
}
791791

792+
/// Collect information about a crash.
793+
func collectCrashInfo(for program: Program, withSignal termsig: Int, withStderr stderr: String, withStdout stdout: String, withExectime exectime: TimeInterval) -> [String] {
794+
var info = [String]()
795+
info.append("CRASH INFO")
796+
info.append("==========")
797+
if let tag = config.tag {
798+
info.append("INSTANCE TAG: \(tag)")
799+
}
800+
info.append("TERMSIG: \(termsig)")
801+
info.append("STDERR:")
802+
info.append(stderr.trimmingCharacters(in: .newlines))
803+
info.append("STDOUT:")
804+
info.append(stdout.trimmingCharacters(in: .newlines))
805+
info.append("FUZZER ARGS: \(config.arguments.joined(separator: " "))")
806+
info.append("TARGET ARGS: \(runner.processArguments.joined(separator: " "))")
807+
info.append("CONTRIBUTORS: \(program.contributors.map({ $0.name }).joined(separator: ", "))")
808+
info.append("EXECUTION TIME: \(Int(exectime * 1000))ms")
809+
return info
810+
}
811+
792812
/// Process a program that causes a crash.
793813
func processCrash(_ program: Program, withSignal termsig: Int, withStderr stderr: String, withStdout stdout: String, origin: ProgramOrigin, withExectime exectime: TimeInterval) {
794814
func processCommon(_ program: Program) {
795815
let hasCrashInfo = program.comments.at(.footer)?.contains("CRASH INFO") ?? false
796816
if !hasCrashInfo {
797-
program.comments.add("CRASH INFO", at: .footer)
798-
program.comments.add("==========", at: .footer)
799-
if let tag = config.tag {
800-
program.comments.add("INSTANCE TAG: \(tag)", at: .footer)
817+
for line in collectCrashInfo(for: program, withSignal: termsig, withStderr: stderr, withStdout: stdout, withExectime: exectime) {
818+
program.comments.add(line, at: .footer)
801819
}
802-
program.comments.add("TERMSIG: \(termsig)", at: .footer)
803-
program.comments.add("STDERR:", at: .footer)
804-
program.comments.add(stderr.trimmingCharacters(in: .newlines), at: .footer)
805-
program.comments.add("STDOUT:", at: .footer)
806-
program.comments.add(stdout.trimmingCharacters(in: .newlines), at: .footer)
807-
program.comments.add("FUZZER ARGS: \(config.arguments.joined(separator: " "))", at: .footer)
808-
program.comments.add("TARGET ARGS: \(runner.processArguments.joined(separator: " "))", at: .footer)
809-
program.comments.add("CONTRIBUTORS: \(program.contributors.map({ $0.name }).joined(separator: ", "))", at: .footer)
810-
program.comments.add("EXECUTION TIME: \(Int(exectime * 1000))ms", at: .footer)
811820
}
812821
assert(program.comments.at(.footer)?.contains("CRASH INFO") ?? false)
813822

Sources/Fuzzilli/Mutators/RuntimeAssistedMutator.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class RuntimeAssistedMutator: Mutator {
3232
enum Outcome: String, CaseIterable {
3333
case success = "Success"
3434
case cannotInstrument = "Cannot instrument input"
35+
case instrumentedProgramCrashed = "Instrumented program crashed"
3536
case instrumentedProgramFailed = "Instrumented program failed"
3637
case instrumentedProgramTimedOut = "Instrumented program timed out"
3738
case noResults = "No results received"
@@ -90,6 +91,10 @@ public class RuntimeAssistedMutator: Mutator {
9091

9192
// Execute the instrumented program (with a higher timeout) and collect the output.
9293
let execution = fuzzer.execute(instrumentedProgram, withTimeout: fuzzer.config.timeout * 4, purpose: .runtimeAssistedMutation)
94+
// We need to cache these because they're invalidated the next time we call execute().
95+
let oldStdout = execution.stdout
96+
let oldStderr = execution.stderr
97+
let oldFuzzout = execution.fuzzout
9398
switch execution.outcome {
9499
case .failed(_):
95100
// We generally do not expect the instrumentation code itself to cause runtime exceptions. Even if it performs new actions those should be guarded with try-catch.
@@ -109,8 +114,31 @@ public class RuntimeAssistedMutator: Mutator {
109114
// In this case we still try to process the instrumentation output (if any) and produce the final, uninstrumented program
110115
// so that we obtain reliable testcase for crashes due to (1). However, to not loose crashes due to (2), we also
111116
// report the instrumented program as crashing here. We may therefore end up with two crashes from one mutation.
112-
let stdout = execution.fuzzout + "\n" + execution.stdout
113-
fuzzer.processCrash(instrumentedProgram, withSignal: signal, withStderr: execution.stderr, withStdout: stdout, origin: .local, withExectime: execution.execTime)
117+
118+
let stdout = oldFuzzout + "\n" + oldStdout
119+
120+
// We log the crash here in case something goes wrong.
121+
let crashInfoText = fuzzer.collectCrashInfo(for: program, withSignal: signal, withStderr: oldStderr, withStdout: oldStdout, withExectime: execution.execTime)
122+
logger.error("Instrumented program crashed: \(fuzzer.lifter.lift(program))\n\(crashInfoText.joined(separator: "\n"))")
123+
124+
// Check if the process()'d program also crashes. If yes, we report that crash instead.
125+
// This allows to use Fuzzilli's input minimization, which wouldn't work for the instrumented program.
126+
let (mutatedProgram, outcome) = process(execution.fuzzout, ofInstrumentedProgram: instrumentedProgram, using: b)
127+
if let mutatedProgram {
128+
assert(outcome == .success)
129+
130+
let execution = fuzzer.execute(mutatedProgram, withTimeout: fuzzer.config.timeout , purpose: .runtimeAssistedMutation)
131+
if case .crashed(let signal) = execution.outcome {
132+
logger.info("Mutated program crashed as well, reporting mutated program instead")
133+
let stdout = execution.fuzzout + "\n" + execution.stdout
134+
fuzzer.processCrash(mutatedProgram, withSignal: signal, withStderr: execution.stderr, withStdout: stdout, origin: .local, withExectime: execution.execTime)
135+
return failure(.instrumentedProgramCrashed)
136+
}
137+
}
138+
139+
// If we reach here, the process()'d program did not crash, so we need to report the instrumented program.
140+
logger.info("Mutated program did not crash, reporting original crash of the instrumented program")
141+
fuzzer.processCrash(instrumentedProgram, withSignal: signal, withStderr: oldStderr, withStdout: stdout, origin: .local, withExectime: execution.execTime)
114142
case .succeeded:
115143
// The expected case.
116144
break
@@ -119,7 +147,7 @@ public class RuntimeAssistedMutator: Mutator {
119147
}
120148

121149
// Process the output to build the mutated program.
122-
let (maybeMutatedProgram, outcome) = process(execution.fuzzout, ofInstrumentedProgram: instrumentedProgram, using: b)
150+
let (maybeMutatedProgram, outcome) = process(oldFuzzout, ofInstrumentedProgram: instrumentedProgram, using: b)
123151
guard let mutatedProgram = maybeMutatedProgram else {
124152
assert(outcome != .success)
125153
return failure(outcome)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
@testable import Fuzzilli
17+
18+
// This mutator generates a crashing instrumented program.
19+
// It is used for testing: the process()'d program should be
20+
// reported instead of the instrument()'d program, if that program
21+
// also crashes, in order to enable better minimization.
22+
23+
class CrashingInstrumentationMutator: RuntimeAssistedMutator {
24+
private let shouldProcessedProgramCrash: Bool
25+
26+
init(shouldProcessedProgramCrash: Bool = true) {
27+
self.shouldProcessedProgramCrash = shouldProcessedProgramCrash
28+
super.init("CrashingInstrumentationMutator", verbose: true)
29+
}
30+
31+
override func instrument(_ program: Program, for fuzzer: Fuzzer) -> Program? {
32+
let b = fuzzer.makeBuilder()
33+
b.eval("fuzzilli('FUZZILLI_CRASH', 0)");
34+
35+
// Add a JsInternalOperation to satisfy the assertion in RuntimeAssistedMutator.swift:89
36+
let v = b.loadInt(42)
37+
b.doPrint(v)
38+
39+
b.append(program)
40+
return b.finalize()
41+
}
42+
43+
44+
override func process(_ output: String, ofInstrumentedProgram instrumentedProgram: Program, using b: ProgramBuilder) -> (Program?, Outcome) {
45+
// Purpose of this print: Distinguish crashes originating from instrument() and process().
46+
let printFct = b.createNamedVariable(forBuiltin: "print")
47+
b.callFunction(printFct, withArgs: [b.loadString("This is the processed program")])
48+
if self.shouldProcessedProgramCrash {
49+
b.append(instrumentedProgram)
50+
}
51+
return (b.finalize(), .success)
52+
}
53+
54+
override func logAdditionalStatistics() {}
55+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import XCTest
16+
@testable import Fuzzilli
17+
18+
class RuntimeAssistedMutatorTests: XCTestCase {
19+
class CrashMockScriptRunner: ScriptRunner {
20+
var processArguments: [String] = []
21+
var env: [(String, String)] = []
22+
func run(_ script: String, withTimeout timeout: UInt32) -> Execution {
23+
if script.contains("fuzzilli('FUZZILLI_CRASH', 0)") {
24+
return MockExecution(outcome: .crashed(9), stdout: "", stderr: "", fuzzout: "", execTime: 0.1)
25+
} else {
26+
return MockExecution(outcome: .succeeded, stdout: "", stderr: "", fuzzout: "", execTime: 0.1)
27+
}
28+
}
29+
func setEnvironmentVariable(_ key: String, to value: String) {}
30+
func initialize(with fuzzer: Fuzzer) {}
31+
var isInitialized: Bool { true }
32+
}
33+
34+
// This test checks that if *both* the instrumented and the processed programs crash,
35+
// we report the processed program.
36+
37+
func testInstrumentedAndProcessedProgramsCrash() {
38+
let runner = CrashMockScriptRunner()
39+
let config = Configuration(logLevel: .error)
40+
let fuzzer = makeMockFuzzer(config: config, runner: runner)
41+
42+
let mutator = CrashingInstrumentationMutator()
43+
let b = fuzzer.makeBuilder()
44+
b.loadInt(42)
45+
let program = b.finalize()
46+
47+
let crashEventTriggered = self.expectation(description: "Crash reported on processed program")
48+
fuzzer.registerEventListener(for: fuzzer.events.CrashFound) { ev in
49+
let lifted = fuzzer.lifter.lift(ev.program)
50+
if lifted.contains("This is the processed program") {
51+
crashEventTriggered.fulfill()
52+
}
53+
}
54+
55+
_ = mutator.mutate(program, using: b, for: fuzzer)
56+
waitForExpectations(timeout: 5, handler: nil)
57+
}
58+
59+
// This test checks that if *only* the instrumented program crashes, we report it.
60+
61+
func testOnlyInstrumentedProgramCrashes() {
62+
let runner = CrashMockScriptRunner()
63+
let config = Configuration(logLevel: .error)
64+
let fuzzer = makeMockFuzzer(config: config, runner: runner)
65+
66+
let mutator = CrashingInstrumentationMutator(shouldProcessedProgramCrash: false)
67+
let b = fuzzer.makeBuilder()
68+
b.loadInt(42)
69+
let program = b.finalize()
70+
71+
let crashEventTriggered = self.expectation(description: "Crash reported on instrumented program")
72+
fuzzer.registerEventListener(for: fuzzer.events.CrashFound) { ev in
73+
let lifted = fuzzer.lifter.lift(ev.program)
74+
if !lifted.contains("This is the processed program") {
75+
crashEventTriggered.fulfill()
76+
}
77+
}
78+
79+
_ = mutator.mutate(program, using: b, for: fuzzer)
80+
waitForExpectations(timeout: 5, handler: nil)
81+
}
82+
}

0 commit comments

Comments
 (0)