Skip to content

Commit 7b9f695

Browse files
Danylo MocherniukMathias Payerdatenschutzakademie-ag1tregua87Christian Wressnegger
authored andcommitted
[dumpling] Add RelateTool and DiffOracle
DiffOracle is a library that allows to see if there was a difference between optimized and unoptimized runs. RelateTool is designed as a CLI tool to compare optimized vs unoptimized runs. Usage: swift run -c release RelateTool --d8=... --poc=... Bug: 441467877 Change-Id: Ie8850e8534ae3a890f93be77ba2d0961f51a129e Co-authored-by: Mathias Payer <[email protected]> Co-authored-by: Liam Wachter <[email protected]> Co-authored-by: Flavio Toffalini <[email protected]> Co-authored-by: Christian Wressnegger <[email protected]> Co-authored-by: Julian Gremminger <[email protected]> Reviewed-on: https://chrome-internal-review.googlesource.com/c/v8/fuzzilli/+/8759816 Reviewed-by: Matthias Liedtke <[email protected]> Commit-Queue: Danylo Mocherniuk <[email protected]>
1 parent d37a8ed commit 7b9f695

File tree

4 files changed

+665
-0
lines changed

4 files changed

+665
-0
lines changed

Package.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ let package = Package(
7070
.executableTarget(name: "FuzzILTool",
7171
dependencies: ["Fuzzilli"]),
7272

73+
// Tool that runs d8 in Dumpling mode. First time it runs with Maglev
74+
// and Turbofan. Second time without. In both runs frames are dumped
75+
// in certain points to the files. The dumps are later compared for
76+
// equality. If they are not equal, it means that there's likely a bug
77+
// in V8.
78+
.executableTarget(name: "RelateTool",
79+
dependencies: ["Fuzzilli"]),
80+
7381
.testTarget(name: "FuzzilliTests",
7482
dependencies: ["Fuzzilli"],
7583
resources: [.copy("CompilerTests")]),
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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+
// The tool implemented in this file compares two Dumpling dumps for
16+
// equality. Each Dumpling dump consists of multiple frame dumps.
17+
// Example frame dump:
18+
// ---I
19+
// b:34
20+
// f:500
21+
// n:3
22+
// m:5
23+
// x:40
24+
// r0:30
25+
// r3:some_string
26+
// a0:1
27+
// a1:2
28+
//
29+
// A frame dump always starts with a header which is one of
30+
// ---I (for interpreter), ---S (for Sparkplug), ---M (for Maglev),
31+
// ---T (for Turbofan), ---D (deopt Turbofan: this is frame produced by actual
32+
// Turbofan deopt, not by Dumpling).
33+
// Next line with 'b' might follow (see when it and other lines might get omitted
34+
// in the NOTE below), it denotes bytecode offset of a dump (within a function).
35+
// Next line with 'f' might follow, it denotes JS function id of a dump.
36+
// Next line with 'x' might follow, it denotes the dump of the accumulator.
37+
// Next lines with 'n' and 'm' might follow, which denote parameters of a function
38+
// and register count of a frame respectfully.
39+
// Next multiple lines 'ai' might follow, where 'a' denotes that this is a dump
40+
// of a function parameter and 'i' denotes the number of the parameter.
41+
// Next multiple lines 'ri' might follow, where 'r' denotes that this is a dump
42+
// of a register and 'i' denotes the number of the register.
43+
// Lastly an empty line always ends the frame dump.
44+
//
45+
// NOTE: frame dumps are implemented incrementally to not write too much data
46+
// to the file.
47+
// IOW let's say we dumped 2 frames with full format as follows:
48+
// ---I
49+
// b:30
50+
// f:40
51+
// a0:1
52+
// a1:2
53+
//
54+
// ---I
55+
// b:35
56+
// f:40
57+
// a0:1
58+
// a1:1
59+
//
60+
// In order to save space a file will contain just:
61+
// ---I
62+
// b:30
63+
// f:40
64+
// a0:1
65+
// a1:2
66+
//
67+
// ---I
68+
// b:35
69+
// a1:1
70+
71+
72+
import Foundation
73+
74+
// This class is implementing one public function relate(optimizedOutput, unoptimizedOutput).
75+
// `relate` compares optimizedOutput and unoptimizedOutput for equality.
76+
public final class DiffOracle {
77+
private enum FrameType {
78+
case interpreter
79+
case sparkplug
80+
case maglev
81+
case turbofan
82+
case deoptTurbofan
83+
}
84+
85+
private struct Frame: Equatable {
86+
let bytecodeOffset: Int
87+
let accumulator: String
88+
let arguments: [String]
89+
let registers: [String]
90+
let functionId: Int
91+
let frameType: FrameType
92+
93+
// 'reference' is the value from Unoptimized frame.
94+
func matches(reference: Frame) -> Bool {
95+
96+
guard self.bytecodeOffset == reference.bytecodeOffset,
97+
self.functionId == reference.functionId,
98+
self.arguments.count == reference.arguments.count,
99+
self.registers.count == reference.registers.count else {
100+
return false
101+
}
102+
103+
// Logic: 'self' is the Optimized frame. It is allowed to have "<optimized_out>".
104+
func isMatch(_ optValue: String, unoptValue: String) -> Bool {
105+
return optValue == "<optimized_out>" || optValue == unoptValue
106+
}
107+
108+
if !isMatch(self.accumulator, unoptValue: reference.accumulator) {
109+
return false
110+
}
111+
112+
if !zip(self.arguments, reference.arguments).allSatisfy(isMatch) {
113+
return false
114+
}
115+
116+
if !zip(self.registers, reference.registers).allSatisfy(isMatch) {
117+
return false
118+
}
119+
120+
return true
121+
}
122+
}
123+
124+
private static func parseDiffFrame(_ frameArr: ArraySlice<Substring>, _ prevFrame: Frame?) -> Frame {
125+
func parseValue<T>(prefix: String, defaultValue: T, index: inout Int, conversion: (Substring) -> T) -> T {
126+
if index < frameArr.endIndex && frameArr[index].starts(with: prefix) {
127+
let value = conversion(frameArr[index].dropFirst(prefix.count))
128+
index += 1
129+
return value
130+
}
131+
return defaultValue
132+
}
133+
var i = frameArr.startIndex
134+
func parseFrameType(_ type: Substring) -> FrameType {
135+
switch type {
136+
case "---I": .interpreter
137+
case "---S": .sparkplug
138+
case "---M": .maglev
139+
case "---T": .turbofan
140+
case "---D": .deoptTurbofan
141+
default:
142+
fatalError("Unknown frame type")
143+
}
144+
}
145+
146+
let frameType: FrameType = parseFrameType(frameArr[i])
147+
148+
i += 1
149+
150+
let bytecodeOffset = parseValue(prefix: "b:", defaultValue: prevFrame?.bytecodeOffset ?? -1, index: &i){ Int($0)! }
151+
let functionId = parseValue(prefix: "f:", defaultValue: prevFrame?.functionId ?? -1, index: &i){ Int($0)! }
152+
let accumulator = parseValue(prefix: "x:", defaultValue: prevFrame?.accumulator ?? "", index: &i){ String($0) }
153+
let argCount = parseValue(prefix: "n:", defaultValue: prevFrame?.arguments.count ?? -1, index: &i){ Int($0)! }
154+
let regCount = parseValue(prefix: "m:", defaultValue: prevFrame?.registers.count ?? -1, index: &i){ Int($0)! }
155+
156+
func updateValues(prefix: String, totalCount: Int, oldValues: [String]) -> [String] {
157+
var newValues = oldValues
158+
159+
if newValues.count > totalCount {
160+
newValues.removeLast(newValues.count - totalCount)
161+
} else if newValues.count < totalCount {
162+
let missingCount = totalCount - newValues.count
163+
let defaults = Array(repeating: "<missing>", count: missingCount)
164+
newValues.append(contentsOf: defaults)
165+
}
166+
167+
while i < frameArr.endIndex && frameArr[i].starts(with: prefix) {
168+
let data = frameArr[i].dropFirst(1).split(separator: ":", maxSplits: 1)
169+
let number = Int(data[0])!
170+
let value = String(data[1])
171+
newValues[number] = value
172+
i += 1
173+
}
174+
return newValues
175+
176+
}
177+
178+
let arguments = updateValues(prefix: "a", totalCount: argCount, oldValues: prevFrame?.arguments ?? [])
179+
let registers = updateValues(prefix: "r", totalCount: regCount, oldValues: prevFrame?.registers ?? [])
180+
181+
let frame = Frame(bytecodeOffset: bytecodeOffset,
182+
accumulator: accumulator,
183+
arguments: arguments,
184+
registers: registers,
185+
functionId: functionId,
186+
frameType: frameType)
187+
return frame
188+
}
189+
190+
private static func parseFullFrames(_ stdout: String) -> [Frame] {
191+
var frameArray: [Frame] = []
192+
var prevFrame: Frame? = nil
193+
194+
let split = stdout.split(separator: "\n", omittingEmptySubsequences: false)
195+
let frames = split.split(separator: "")
196+
197+
for frame in frames {
198+
assert(frame.first?.starts(with: "---") == true, "Invalid frame header found: \(frame.first ?? "nil")")
199+
200+
prevFrame = parseDiffFrame(frame, prevFrame)
201+
frameArray.append(prevFrame!)
202+
}
203+
return frameArray
204+
}
205+
206+
public static func relate(_ optIn: String, with unoptIn: String) -> Bool {
207+
let optFrames = parseFullFrames(optIn)
208+
let unoptFrames = parseFullFrames(unoptIn)
209+
var unoptFramesLeft = ArraySlice(unoptFrames)
210+
211+
for optFrame in optFrames {
212+
guard let unoptIndex = unoptFramesLeft.firstIndex(where: optFrame.matches) else {
213+
print(optFrame as AnyObject)
214+
print("--------------------------")
215+
print("[")
216+
for unoptFrame in unoptFrames {
217+
if unoptFrame.bytecodeOffset == optFrame.bytecodeOffset {
218+
print(unoptFrame as AnyObject)
219+
}
220+
}
221+
print("]")
222+
return false
223+
}
224+
// Remove all skipped frames and the found frame.
225+
unoptFramesLeft = unoptFramesLeft[(unoptIndex + 1)...]
226+
}
227+
return true
228+
}
229+
}

Sources/RelateTool/main.swift

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
import Fuzzilli
17+
18+
public struct V8DifferentialConfig {
19+
public static let commonArgs: [String] = [
20+
"--expose-gc",
21+
"--omit-quit",
22+
"--allow-natives-for-differential-fuzzing",
23+
"--fuzzing",
24+
"--future",
25+
"--harmony",
26+
"--predictable",
27+
"--trace",
28+
"--print-bytecode",
29+
"--correctness-fuzzer-suppressions",
30+
"--no-lazy-feedback-allocation",
31+
]
32+
33+
public static let differentialArgs: [String] = [
34+
"--no-sparkplug",
35+
"--jit-fuzzing",
36+
"--maglev-dumping",
37+
"--turbofan-dumping",
38+
"--turbofan-dumping-print-deopt-frames"
39+
]
40+
41+
public static let referenceArgs: [String] = [
42+
"--no-turbofan",
43+
"--no-maglev",
44+
"--sparkplug-dumping",
45+
"--interpreter-dumping"
46+
]
47+
}
48+
49+
struct Relater {
50+
let d8Path: String
51+
let pocPath: String
52+
let dumpFilePath: String
53+
54+
private func runV8(args: [String]) throws {
55+
let process = Process()
56+
process.executableURL = URL(fileURLWithPath: d8Path)
57+
process.arguments = args + [pocPath]
58+
59+
let pipe = Pipe()
60+
process.standardOutput = pipe
61+
process.standardError = pipe
62+
63+
try process.run()
64+
process.waitUntilExit()
65+
}
66+
67+
private func readDumpFile() throws -> String {
68+
return try String(contentsOfFile: dumpFilePath, encoding: .utf8)
69+
}
70+
71+
private func cleanDumpFile() {
72+
try? FileManager.default.removeItem(atPath: dumpFilePath)
73+
}
74+
75+
/// Main execution flow.
76+
func run() {
77+
do {
78+
cleanDumpFile()
79+
let optArgs = V8DifferentialConfig.commonArgs + V8DifferentialConfig.differentialArgs
80+
try runV8(args: optArgs)
81+
let optDumps = try readDumpFile()
82+
83+
cleanDumpFile()
84+
let refArgs = V8DifferentialConfig.commonArgs + V8DifferentialConfig.referenceArgs
85+
try runV8(args: refArgs)
86+
let unOptDumps = try readDumpFile()
87+
88+
let result = DiffOracle.relate(optDumps, with: unOptDumps)
89+
print("Differential check result: \(result)")
90+
91+
if !result {
92+
exit(1)
93+
}
94+
95+
} catch {
96+
print("Error during relate: \(error)")
97+
exit(1)
98+
}
99+
}
100+
}
101+
102+
let args = Arguments.parse(from: CommandLine.arguments)
103+
104+
guard let jsShellPath = args["--d8"],
105+
let pocPath = args["--poc"] else {
106+
print("Usage: --d8 <path_to_d8> --poc <path_to_poc> [--dump <path_to_dump_file>]")
107+
exit(1)
108+
}
109+
110+
// Parse optional dump path, default to /tmp/output_dump.txt
111+
let dumpPath = args["--dump"] ?? "/tmp/output_dump.txt"
112+
113+
let relater = Relater(d8Path: jsShellPath, pocPath: pocPath, dumpFilePath: dumpPath)
114+
relater.run()

0 commit comments

Comments
 (0)