Skip to content

Commit f056ee5

Browse files
LiedtkeV8-internal LUCI CQ
authored andcommitted
[wasm] Add support for try-catch blocks with result
Change-Id: Iddb7a57b15a25cd7c4eff9b1ac1293670ddf070e Reviewed-on: https://chrome-internal-review.googlesource.com/c/v8/fuzzilli/+/7970030 Commit-Queue: Matthias Liedtke <[email protected]> Reviewed-by: Carl Smith <[email protected]>
1 parent 2afdd29 commit f056ee5

File tree

10 files changed

+165
-20
lines changed

10 files changed

+165
-20
lines changed

Sources/Fuzzilli/Base/ProgramBuilder.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,11 +3165,13 @@ public class ProgramBuilder {
31653165
b.emit(WasmBranchIf(labelTypes: b.type(of: label).wasmLabelType!.parameters), withInputs: [label] + args + [condition])
31663166
}
31673167

3168-
public func wasmBuildIfElse(_ condition: Variable, ifBody: () -> Void, elseBody: () -> Void) {
3168+
public func wasmBuildIfElse(_ condition: Variable, ifBody: () -> Void, elseBody: (() -> Void)? = nil) {
31693169
b.emit(WasmBeginIf(), withInputs: [condition])
31703170
ifBody()
3171-
b.emit(WasmBeginElse())
3172-
elseBody()
3171+
if let elseBody = elseBody {
3172+
b.emit(WasmBeginElse())
3173+
elseBody()
3174+
}
31733175
b.emit(WasmEndIf())
31743176
}
31753177

@@ -3215,6 +3217,29 @@ public class ProgramBuilder {
32153217
b.emit(WasmEndTry())
32163218
}
32173219

3220+
// The catchClauses expect a list of (tag, block-generator lambda).
3221+
// The lambda's inputs are the block label, the exception label (for rethrowing) and the
3222+
// tag arguments.
3223+
@discardableResult
3224+
public func wasmBuildLegacyTryWithResult(with signature: Signature, args: [Variable],
3225+
body: (Variable, [Variable]) -> Variable,
3226+
catchClauses: [(tag: Variable, body: (Variable, Variable, [Variable]) -> Variable)],
3227+
catchAllBody: ((Variable) -> Variable)? = nil) -> Variable {
3228+
assert(signature.parameters.count == args.count)
3229+
assert(signature.outputType != .nothing)
3230+
let instr = b.emit(WasmBeginTry(with: signature), withInputs: args)
3231+
var result = body(instr.innerOutput(0), Array(instr.innerOutputs(1...)))
3232+
for (tag, generator) in catchClauses {
3233+
let instr = b.emit(WasmBeginCatch(with: b.type(of: tag).wasmTagType!.parameters => signature.outputType), withInputs: [tag, result])
3234+
result = generator(instr.innerOutput(0), instr.innerOutput(1), Array(instr.innerOutputs(2...)))
3235+
}
3236+
if let catchAllBody = catchAllBody {
3237+
let instr = b.emit(WasmBeginCatchAll(with: signature), withInputs: [result])
3238+
result = catchAllBody(instr.innerOutput(0))
3239+
}
3240+
return b.emit(WasmEndTry(outputType: signature.outputType), withInputs: [result]).output
3241+
}
3242+
32183243
public func WasmBuildLegacyCatch(tag: Variable, body: ((Variable, Variable, [Variable]) -> Void)) {
32193244
// TODO(mliedtke): A catch block can produce a result type, however that result type
32203245
// has to be in sync with the try result type (afaict).

Sources/Fuzzilli/CodeGen/CodeGeneratorWeights.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ public let codeGeneratorWeights = [
275275
"WasmLoopGenerator": 8,
276276
"WasmLoopWithSignatureGenerator": 8,
277277
"WasmLegacyTryCatchGenerator": 8,
278+
"WasmLegacyTryCatchWithResultGenerator": 8,
278279
"WasmLegacyTryDelegateGenerator": 8,
279280
"WasmThrowGenerator": 2,
280281
"WasmRethrowGenerator": 10,

Sources/Fuzzilli/CodeGen/WasmCodeGenerators.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,28 @@ public let WasmCodeGenerators: [CodeGenerator] = [
633633
}
634634
},
635635

636+
RecursiveCodeGenerator("WasmLegacyTryCatchWithResultGenerator", inContext: .wasmFunction) { b in
637+
let function = b.currentWasmModule.currentWasmFunction
638+
// Choose a few random wasm values as arguments if available.
639+
let args = (0..<Int.random(in: 0...5)).map {_ in b.findVariable {b.type(of: $0).Is(.wasmPrimitive)}}.filter {$0 != nil}.map {$0!}
640+
let parameters = args.map {arg in Parameter.plain(b.type(of: arg))}
641+
let tags = (0..<Int.random(in: 0...5)).map {_ in b.findVariable { b.type(of: $0).isWasmTagType }}.filter {$0 != nil}.map {$0!}
642+
// Disallowing void here to simplify the logic. The WasmLegacyTryCatchGenerator generates try-catch blocks without a result type.
643+
let outputType = b.randomWasmBlockOutputType(allowVoid: false)
644+
let signature = parameters => outputType
645+
let recursiveCallCount = 2 + tags.count
646+
function.wasmBuildLegacyTryWithResult(with: signature, args: args, body: { label, args in
647+
b.buildRecursive(block: 1, of: recursiveCallCount, n: 4)
648+
return b.randomVariable(ofType: outputType) ?? function.generateRandomWasmVar(ofType: outputType)
649+
}, catchClauses: tags.enumerated().map {i, tag in (tag, {_, _, _ in
650+
b.buildRecursive(block: 2 + i, of: recursiveCallCount, n: 4)
651+
return b.randomVariable(ofType: outputType) ?? function.generateRandomWasmVar(ofType: outputType)
652+
})}, catchAllBody: { label in
653+
b.buildRecursive(block: 2 + tags.count, of: recursiveCallCount, n: 4)
654+
return b.randomVariable(ofType: outputType) ?? function.generateRandomWasmVar(ofType: outputType)
655+
})
656+
},
657+
636658
RecursiveCodeGenerator("WasmLegacyTryDelegateGenerator", inContext: .wasmFunction, inputs: .required(.anyLabel)) { b, label in
637659
let function = b.currentWasmModule.currentWasmFunction
638660
// Choose a few random wasm values as arguments if available.

Sources/Fuzzilli/FuzzIL/Instruction.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,8 +1221,10 @@ extension Instruction: ProtobufConvertible {
12211221
$0.parameters = convertParametersToWasmTypeEnums(op.signature.parameters)
12221222
$0.returnType = ILTypeToWasmTypeEnum(op.signature.outputType)
12231223
}
1224-
case .wasmEndTry(_):
1225-
$0.wasmEndTry = Fuzzilli_Protobuf_WasmEndTry()
1224+
case .wasmEndTry(let op):
1225+
$0.wasmEndTry = Fuzzilli_Protobuf_WasmEndTry.with {
1226+
$0.returnType = ILTypeToWasmTypeEnum(op.outputType)
1227+
}
12261228
case .wasmBeginTryDelegate(let op):
12271229
$0.wasmBeginTryDelegate = Fuzzilli_Protobuf_WasmBeginTryDelegate.with {
12281230
$0.parameters = convertParametersToWasmTypeEnums(op.signature.parameters)
@@ -2020,8 +2022,8 @@ extension Instruction: ProtobufConvertible {
20202022
Parameter.plain(WasmTypeEnumToILType(param))
20212023
})
20222024
op = WasmBeginCatch(with: parameters => WasmTypeEnumToILType(p.returnType))
2023-
case .wasmEndTry(_):
2024-
op = WasmEndTry()
2025+
case .wasmEndTry(let p):
2026+
op = WasmEndTry(outputType: WasmTypeEnumToILType(p.returnType))
20252027
case .wasmBeginTryDelegate(let p):
20262028
let parameters: [Parameter] = p.parameters.map({ param in
20272029
Parameter.plain(WasmTypeEnumToILType(param))

Sources/Fuzzilli/FuzzIL/WasmOperations.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,8 +1054,11 @@ final class WasmBeginCatchAll : WasmOperation {
10541054
init(with signature: Signature) {
10551055
self.signature = signature
10561056

1057+
let inputTypes = signature.outputType != .nothing ? [signature.outputType] : []
10571058
super.init(
1058-
innerOutputTypes: [.label()],
1059+
inputTypes: inputTypes,
1060+
outputType: .nothing,
1061+
innerOutputTypes: [.label(inputTypes)],
10591062
attributes: [
10601063
.isBlockEnd,
10611064
.isBlockStart,
@@ -1074,15 +1077,17 @@ final class WasmBeginCatch : WasmOperation {
10741077

10751078
init(with signature: Signature) {
10761079
self.signature = signature
1080+
let labelTypes: [ILType] = signature.outputType != .nothing ? [signature.outputType] : []
1081+
let inputTypes: [ILType] = [.object(ofGroup: "WasmTag")] + labelTypes
10771082
// TODO: In an ideal world, the catch would only have one label that is used both for
10781083
// branching as well as for rethrowing the exception. However, rethrows may only use labels
10791084
// from catch blocks and branches may use any label but need to be very precise on the type
10801085
// of the label parameters, so typing the label would require different subtyping based on
10811086
// the usage. For now, we just emit a label for branching and the ".exceptionLabel" for
10821087
// rethrows.
10831088
super.init(
1084-
inputTypes: [.object(ofGroup: "WasmTag")],
1085-
innerOutputTypes: [.label([]), .exceptionLabel] + signature.parameters.convertPlainToILTypes(),
1089+
inputTypes: inputTypes,
1090+
innerOutputTypes: [.label(labelTypes), .exceptionLabel] + signature.parameters.convertPlainToILTypes(),
10861091
attributes: [
10871092
.isBlockEnd,
10881093
.isBlockStart,
@@ -1095,8 +1100,9 @@ final class WasmBeginCatch : WasmOperation {
10951100
final class WasmEndTry: WasmOperation {
10961101
override var opcode: Opcode { .wasmEndTry(self) }
10971102

1098-
init() {
1099-
super.init(attributes: [.isBlockEnd], requiredContext: [.wasmFunction])
1103+
init(outputType: ILType = .nothing) {
1104+
let inputTypes = outputType != .nothing ? [outputType] : []
1105+
super.init(inputTypes: inputTypes, outputType: outputType, attributes: [.isBlockEnd], requiredContext: [.wasmFunction])
11001106
}
11011107
}
11021108

Sources/Fuzzilli/Lifting/FuzzILLifter.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,18 +1003,20 @@ public class FuzzILLifter: Lifter {
10031003
case .wasmBeginCatchAll(_):
10041004
assert(instr.numOutputs == 0)
10051005
w.decreaseIndentionLevel()
1006-
w.emit("WasmBeginCatchAll -> L:\(instr.innerOutput(0))")
1006+
let inputs = instr.inputs.map(lift).joined(separator: ", ")
1007+
w.emit("WasmBeginCatchAll [\(inputs)] -> L:\(instr.innerOutput(0))")
10071008
w.increaseIndentionLevel()
10081009
case .wasmBeginCatch(_):
10091010
assert(instr.numOutputs == 0)
10101011
w.decreaseIndentionLevel()
1011-
w.emit("WasmBeginCatch \(input(0)) -> L:\(instr.innerOutput(0)) E:\(instr.innerOutput(1)) [\(liftCallArguments(instr.innerOutputs(2...)))]")
1012+
w.emit("WasmBeginCatch \(input(0)) [\(instr.numInputs > 1 ? input(1) : "")] -> L:\(instr.innerOutput(0)) E:\(instr.innerOutput(1)) [\(liftCallArguments(instr.innerOutputs(2...)))]")
10121013
w.increaseIndentionLevel()
10131014

1014-
case .wasmEndTry(_):
1015-
assert(instr.numOutputs == 0)
1015+
case .wasmEndTry(let op):
10161016
w.decreaseIndentionLevel()
1017-
w.emit("WasmEndTry")
1017+
let outputPrefix = op.numOutputs > 0 ? "\(output()) <- " : ""
1018+
let inputs = instr.inputs.map(lift).joined(separator: ", ")
1019+
w.emit("\(outputPrefix)WasmEndTry [\(inputs)]")
10181020

10191021
case .wasmThrow(_):
10201022
w.emit("WasmThrow \(instr.inputs.map(lift).joined(separator: ", "))")

Sources/Fuzzilli/Lifting/WasmLifter.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,9 +1092,11 @@ public class WasmLifter {
10921092
self.tags[instr.output] = op.parameters
10931093

10941094
case .wasmBeginCatch(let op):
1095+
assert(typer.type(of: instr.input(0)).wasmTagType!.parameters == op.signature.parameters)
10951096
importTagIfNeeded(tag: instr.input(0), parameters: op.signature.parameters)
10961097

10971098
case .wasmThrow(let op):
1099+
assert(typer.type(of: instr.input(0)).wasmTagType!.parameters == op.parameters)
10981100
importTagIfNeeded(tag: instr.input(0), parameters: op.parameters)
10991101

11001102
default:

Sources/Fuzzilli/Protobuf/operations.pb.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4034,6 +4034,8 @@ public struct Fuzzilli_Protobuf_WasmEndTry: Sendable {
40344034
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
40354035
// methods supported on all messages.
40364036

4037+
public var returnType: Fuzzilli_Protobuf_WasmILType = .consti32
4038+
40374039
public var unknownFields = SwiftProtobuf.UnknownStorage()
40384040

40394041
public init() {}
@@ -11891,18 +11893,31 @@ extension Fuzzilli_Protobuf_WasmBeginCatch: SwiftProtobuf.Message, SwiftProtobuf
1189111893

1189211894
extension Fuzzilli_Protobuf_WasmEndTry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
1189311895
public static let protoMessageName: String = _protobuf_package + ".WasmEndTry"
11894-
public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
11896+
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
11897+
1: .same(proto: "returnType"),
11898+
]
1189511899

1189611900
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
11897-
// Load everything into unknown fields
11898-
while try decoder.nextFieldNumber() != nil {}
11901+
while let fieldNumber = try decoder.nextFieldNumber() {
11902+
// The use of inline closures is to circumvent an issue where the compiler
11903+
// allocates stack space for every case branch when no optimizations are
11904+
// enabled. https://github.com/apple/swift-protobuf/issues/1034
11905+
switch fieldNumber {
11906+
case 1: try { try decoder.decodeSingularEnumField(value: &self.returnType) }()
11907+
default: break
11908+
}
11909+
}
1189911910
}
1190011911

1190111912
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
11913+
if self.returnType != .consti32 {
11914+
try visitor.visitSingularEnumField(value: self.returnType, fieldNumber: 1)
11915+
}
1190211916
try unknownFields.traverse(visitor: &visitor)
1190311917
}
1190411918

1190511919
public static func ==(lhs: Fuzzilli_Protobuf_WasmEndTry, rhs: Fuzzilli_Protobuf_WasmEndTry) -> Bool {
11920+
if lhs.returnType != rhs.returnType {return false}
1190611921
if lhs.unknownFields != rhs.unknownFields {return false}
1190711922
return true
1190811923
}

Sources/Fuzzilli/Protobuf/operations.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,7 @@ message WasmBeginCatch {
11621162
}
11631163

11641164
message WasmEndTry {
1165+
WasmILType returnType = 1;
11651166
}
11661167

11671168
message WasmBeginTryDelegate {

Tests/FuzzilliTests/WasmTests.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,75 @@ class WasmFoundationTests: XCTestCase {
12461246
testForOutput(program: jsProg, runner: runner, outputString: "444\n")
12471247
}
12481248

1249+
func testTryWithBlockParametersAndResult() throws {
1250+
let runner = try GetJavaScriptExecutorOrSkipTest()
1251+
let liveTestConfig = Configuration(logLevel: .error, enableInspection: true)
1252+
1253+
// We have to use the proper JavaScriptEnvironment here.
1254+
// This ensures that we use the available builtins.
1255+
let fuzzer = makeMockFuzzer(config: liveTestConfig, environment: JavaScriptEnvironment())
1256+
let b = fuzzer.makeBuilder()
1257+
1258+
let outputFunc = b.createNamedVariable(forBuiltin: "output")
1259+
let tagVoid = b.createWasmTag(parameterTypes: [])
1260+
let tagi32 = b.createWasmTag(parameterTypes: [.wasmi32])
1261+
let tagi32Other = b.createWasmTag(parameterTypes: [.wasmi32])
1262+
let module = b.buildWasmModule { wasmModule in
1263+
wasmModule.addWasmFunction(with: [.wasmi32] => .wasmi32) { function, args in
1264+
let result = function.wasmBuildLegacyTryWithResult(with: [.wasmi32] => .wasmi32, args: args, body: { label, args in
1265+
function.wasmBuildIfElse(function.wasmi32EqualZero(args[0])) {
1266+
function.WasmBuildThrow(tag: tagVoid, inputs: [])
1267+
}
1268+
function.wasmBuildIfElse(function.wasmi32CompareOp(args[0], function.consti32(1), using: .Eq)) {
1269+
function.WasmBuildThrow(tag: tagi32, inputs: [function.consti32(100)])
1270+
}
1271+
function.wasmBuildIfElse(function.wasmi32CompareOp(args[0], function.consti32(2), using: .Eq)) {
1272+
function.WasmBuildThrow(tag: tagi32Other, inputs: [function.consti32(200)])
1273+
}
1274+
return args[0]
1275+
}, catchClauses: [
1276+
(tagi32, {label, exception, args in
1277+
return args[0]
1278+
}),
1279+
(tagi32Other, {label, exception, args in
1280+
let value = function.wasmi32BinOp(args[0], function.consti32(2), binOpKind: .Add)
1281+
function.wasmBranch(to: label, args: [value])
1282+
return function.consti32(-1)
1283+
}),
1284+
], catchAllBody: { _ in
1285+
return function.consti32(900)
1286+
})
1287+
function.wasmReturn(result)
1288+
}
1289+
}
1290+
let exports = module.loadExports()
1291+
1292+
var expectedString = ""
1293+
// Note that in the comments below "returns" means that this will be passed on as a result
1294+
// to the EndTry block. At the end of the wasm function it performs a wasm return of the
1295+
// result of the EndTry.
1296+
for (input, expected) in [
1297+
// input 0 throws tagVoid which is not caught, so the catchAllBody returns 900.
1298+
(0, 900),
1299+
// input 1 throws tagi32(100) which is caught by a catch clause that returns the
1300+
// tag argument.
1301+
(1, 100),
1302+
// input 2 throws tagi32Other(200) which is caught by a catch clause that branches
1303+
// to the end of its block adding 2 to the tag argument.
1304+
(2, 202),
1305+
// input 3 doesn't throw anything, the try body returns the value directly, meaning
1306+
// the value "falls-through" from the try body to the endTry operation.
1307+
(3, 3)] {
1308+
let wasmOut = b.callMethod(module.getExportedMethod(at: 0), on: exports, withArgs: [b.loadInt(Int64(input))])
1309+
b.callFunction(outputFunc, withArgs: [b.callMethod("toString", on: wasmOut)])
1310+
expectedString += "\(expected)\n"
1311+
}
1312+
1313+
let prog = b.finalize()
1314+
let jsProg = fuzzer.lifter.lift(prog)
1315+
testForOutput(program: jsProg, runner: runner, outputString: expectedString)
1316+
}
1317+
12491318
func testTryDelegate() throws {
12501319
let runner = try GetJavaScriptExecutorOrSkipTest()
12511320
let liveTestConfig = Configuration(logLevel: .error, enableInspection: true)

0 commit comments

Comments
 (0)