Skip to content

Commit d30a1de

Browse files
Merge pull request #98 from swiftwasm/katei/fix-heap-overflow
Fix value stack overflow
2 parents 4b70f1b + 59c7da8 commit d30a1de

File tree

9 files changed

+118
-100
lines changed

9 files changed

+118
-100
lines changed

Makefile

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
11
NAME := WasmKit
22

3-
MODULES = $(notdir $(wildcard Sources/*))
4-
5-
.PHONY: all
6-
all: build
7-
8-
.PHONY: build
9-
build:
10-
@swift build
11-
12-
.PHONY: test
13-
test:
14-
@swift test
15-
163
.PHONY: docs
174
docs:
185
swift package generate-documentation --target WasmKit
@@ -37,7 +24,7 @@ $(SPECTEST_ROOT)/%.json: $(TESTSUITE_DIR)/%.wast
3724

3825
.PHONY: spectest
3926
spectest: spec
40-
swift run Spectest $(SPECTEST_ROOT)
27+
swift run --sanitize address Spectest $(SPECTEST_ROOT)
4128

4229

4330
### WASI Test Suite
@@ -52,11 +39,3 @@ wasitest:
5239
generate:
5340
swift ./Utilities/generate_inst_visitor.swift
5441
swift ./Utilities/generate_inst_dispatch.swift
55-
56-
GIT_STATUS = $(shell git status --porcelain)
57-
ensure_clean:
58-
@[ -z "$(GIT_STATUS)" ] \
59-
&& echo Working directory is clean \
60-
|| (printf "Uncommitted changes: \n $(GIT_STATUS)\n" && exit 1)
61-
62-
FORCE:

Sources/WasmKit/Execution/Instructions/Expression.swift

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@ import WasmParser
22

33
struct InstructionSequence: Equatable {
44
let instructions: UnsafeBufferPointer<Instruction>
5-
6-
init(instructions: [Instruction]) {
7-
assert(_isPOD(Instruction.self))
8-
let buffer = UnsafeMutableBufferPointer<Instruction>.allocate(capacity: instructions.count + 1)
9-
for (idx, instruction) in instructions.enumerated() {
10-
buffer[idx] = instruction
11-
}
12-
buffer[instructions.count] = .endOfFunction
13-
self.instructions = UnsafeBufferPointer(buffer)
14-
}
15-
16-
func deallocate() {
17-
instructions.deallocate()
5+
/// The maximum height of the value stack during execution of this function.
6+
/// This height does not count the locals.
7+
let maxStackHeight: Int
8+
9+
init(instructions: UnsafeBufferPointer<Instruction>, maxStackHeight: Int) {
10+
self.instructions = instructions
11+
assert(self.instructions.last == .endOfFunction)
12+
self.maxStackHeight = maxStackHeight
1813
}
1914

2015
var baseAddress: UnsafePointer<Instruction> {
@@ -26,12 +21,6 @@ struct InstructionSequence: Equatable {
2621
}
2722
}
2823

29-
extension InstructionSequence: ExpressibleByArrayLiteral {
30-
init(arrayLiteral elements: Instruction...) {
31-
self.init(instructions: elements)
32-
}
33-
}
34-
3524
struct ExpressionRef: Equatable {
3625
let _relativeOffset: UInt32
3726
var relativeOffset: Int {

Sources/WasmKit/Execution/Runtime/Function.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public struct Function: Equatable {
88
public func invoke(_ arguments: [Value] = [], runtime: Runtime) throws -> [Value] {
99
try withExecution { execution in
1010
var stack = Stack()
11+
defer { stack.deallocate() }
1112
let numberOfResults = try invoke(execution: &execution, stack: &stack, with: arguments, runtime: runtime)
1213
try execution.run(runtime: runtime, stack: &stack)
1314
return Array(stack.popValues(count: numberOfResults))

Sources/WasmKit/Execution/Runtime/Runtime.swift

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,21 @@ extension Runtime {
8787
.numericConst(.i32(UInt32(element.initializer.count))),
8888
.tableInit(tableIndex, elementIndex),
8989
.tableElementDrop(elementIndex),
90+
.endOfFunction,
9091
])
91-
let initIseq = InstructionSequence(instructions: instructions)
92-
defer { initIseq.deallocate() }
93-
try evaluateConstExpr(initIseq, instance: instance)
92+
try instructions.withUnsafeBufferPointer {
93+
let initIseq = InstructionSequence(instructions: $0, maxStackHeight: 2)
94+
try evaluateConstExpr(initIseq, instance: instance)
95+
}
9496

9597
case .declarative:
96-
let initIseq: InstructionSequence = [.tableElementDrop(elementIndex)]
97-
defer { initIseq.deallocate() }
98-
try evaluateConstExpr(initIseq, instance: instance)
98+
let instructions: [Instruction] = [.tableElementDrop(elementIndex), .endOfFunction]
99+
try instructions.withUnsafeBufferPointer {
100+
let initIseq = InstructionSequence(
101+
instructions: $0, maxStackHeight: 0
102+
)
103+
try evaluateConstExpr(initIseq, instance: instance)
104+
}
99105

100106
case .passive:
101107
continue
@@ -122,14 +128,17 @@ extension Runtime {
122128
default:
123129
throw InstantiationError.unsupported("init expr in data section \(data.offset)")
124130
}
125-
let iseq = InstructionSequence(instructions: instructions + [
131+
instructions.append(contentsOf: [
126132
.numericConst(.i32(0)),
127133
.numericConst(.i32(UInt32(data.initializer.count))),
128134
.memoryInit(UInt32(dataIndex)),
129135
.memoryDataDrop(UInt32(dataIndex)),
136+
.endOfFunction,
130137
])
131-
defer { iseq.deallocate() }
132-
try evaluateConstExpr(iseq, instance: instance)
138+
try instructions.withUnsafeBufferPointer {
139+
let iseq = InstructionSequence(instructions: $0, maxStackHeight: 2)
140+
try evaluateConstExpr(iseq, instance: instance)
141+
}
133142
}
134143
} catch Trap.outOfBoundsMemoryAccess {
135144
throw InstantiationError.outOfBoundsMemoryAccess
@@ -141,6 +150,7 @@ extension Runtime {
141150
if let startIndex = module.start {
142151
try withExecution { initExecution in
143152
var stack = Stack()
153+
defer { stack.deallocate() }
144154
try initExecution.invoke(functionAddress: instance.functionAddresses[Int(startIndex)], runtime: self, stack: &stack)
145155
try initExecution.run(runtime: self, stack: &stack)
146156
}
@@ -189,10 +199,12 @@ extension Runtime {
189199
default:
190200
throw InstantiationError.unsupported("init expr in global section \(global.initializer)")
191201
}
192-
let iseq = InstructionSequence(instructions: instructions)
193-
defer { iseq.deallocate() }
194-
return try evaluateConstExpr(iseq, instance: globalModuleInstance, arity: 1) { _, stack in
195-
return stack.popValue()
202+
instructions.append(.endOfFunction)
203+
return try instructions.withUnsafeBufferPointer {
204+
let iseq = InstructionSequence(instructions: $0, maxStackHeight: 1)
205+
return try evaluateConstExpr(iseq, instance: globalModuleInstance, arity: 1) { _, stack in
206+
return stack.popValue()
207+
}
196208
}
197209
}
198210

@@ -212,6 +224,7 @@ extension Runtime {
212224
) throws -> T {
213225
try withExecution { initExecution in
214226
var stack = Stack()
227+
defer { stack.deallocate() }
215228
try stack.pushFrame(
216229
iseq: iseq,
217230
arity: arity,

Sources/WasmKit/Execution/Runtime/Stack.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ struct Stack {
2323
returnPC: ProgramCounter,
2424
address: FunctionAddress? = nil
2525
) throws {
26-
// TODO: Stack overflow check can be done at the entry of expression
27-
guard (frames.count + numberOfValues) < limit else {
26+
guard frames.count < limit, (numberOfValues + iseq.maxStackHeight + (defaultLocals?.count ?? 0)) < limit else {
2827
throw Trap.callStackExhausted
2928
}
3029
let valueFrameIndex = self.numberOfValues - argc
@@ -63,6 +62,11 @@ struct Stack {
6362
self.valueStack.copyValues(copyCount: copyCount, popCount: popCount)
6463
}
6564

65+
func deallocate() {
66+
self.valueStack.deallocate()
67+
self.frames.deallocate()
68+
}
69+
6670
var topValue: Value {
6771
self.valueStack.topValue
6872
}
@@ -204,6 +208,10 @@ struct FixedSizeStack<Element> {
204208
subscript(_ index: Int) -> Element {
205209
self.buffer[index]
206210
}
211+
212+
func deallocate() {
213+
self.buffer.deallocate()
214+
}
207215
}
208216

209217
extension FixedSizeStack: Sequence {

Sources/WasmKit/ModuleParser.swift

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -125,45 +125,25 @@ func parseModule<Stream: ByteStream>(stream: Stream, features: WasmFeatureSet =
125125
memoryTypes: module.memories.map { $0.type },
126126
tables: module.tables
127127
)
128+
let allocator = module.allocator
128129
let functions = codes.enumerated().map { [hasDataCount = parser.hasDataCount, features] index, code in
129130
let funcTypeIndex = typeIndices[index]
130131
let funcType = module.types[Int(funcTypeIndex)]
131132
return GuestFunction(
132-
type: typeIndices[index], locals: code.locals,
133+
type: typeIndices[index], locals: code.locals, allocator: allocator,
133134
body: {
134-
let enableAssert: Bool
135-
#if ASSERT
136-
enableAssert = true
137-
#else
138-
enableAssert = false
139-
#endif
140-
141135
var translator = InstructionTranslator(
142-
allocator: module.allocator,
136+
allocator: allocator,
143137
module: translatorContext,
144138
type: funcType, locals: code.locals
145139
)
146140

147-
if enableAssert && !_isFastAssertConfiguration() {
148-
let globalFuncIndex = module.imports.count + index
149-
print("🚀 Starting Translation for code[\(globalFuncIndex)] (\(funcType))")
150-
var tracing = InstructionTracingVisitor(trace: {
151-
print("🍵 code[\(globalFuncIndex)] Translating \($0)")
152-
}, visitor: translator)
153-
try WasmParser.parseExpression(
154-
bytes: Array(code.expression),
155-
features: features, hasDataCount: hasDataCount,
156-
visitor: &tracing
157-
)
158-
let newISeq = InstructionSequence(instructions: tracing.visitor.finalize())
159-
return newISeq
160-
}
161141
try WasmParser.parseExpression(
162142
bytes: Array(code.expression),
163143
features: features, hasDataCount: hasDataCount,
164144
visitor: &translator
165145
)
166-
return InstructionSequence(instructions: translator.finalize())
146+
return translator.finalize()
167147
})
168148
}
169149
module.functions = functions

Sources/WasmKit/Translator.swift

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ class ISeqAllocator {
1111
return buffer
1212
}
1313

14+
func allocateDefaultLocals(_ locals: [ValueType]) -> UnsafeBufferPointer<Value> {
15+
let buffer = UnsafeMutableBufferPointer<Value>.allocate(capacity: locals.count)
16+
for (index, localType) in locals.enumerated() {
17+
buffer[index] = localType.defaultValue
18+
}
19+
self.buffers.append(UnsafeMutableRawBufferPointer(buffer))
20+
return UnsafeBufferPointer(buffer)
21+
}
22+
23+
func allocateInstructions(capacity: Int) -> UnsafeMutableBufferPointer<Instruction> {
24+
assert(_isPOD(Instruction.self), "Instruction must be POD")
25+
let buffer = UnsafeMutableBufferPointer<Instruction>.allocate(capacity: capacity)
26+
self.buffers.append(UnsafeMutableRawBufferPointer(buffer))
27+
return buffer
28+
}
29+
1430
deinit {
1531
for buffer in buffers {
1632
buffer.deallocate()
@@ -145,12 +161,16 @@ struct InstructionTranslator: InstructionVisitor {
145161
}
146162
struct ValueStack {
147163
private var values: [MetaValue] = []
164+
/// The maximum height of the stack within the function
165+
private(set) var maxHeight: Int = 0
148166
var height: Int { values.count }
149167

150168
mutating func push(_ value: ValueType) {
151-
self.values.append(.some(value))
169+
push(.some(value))
152170
}
153171
mutating func push(_ value: MetaValue) {
172+
// Record the maximum height of the stack we have seen
173+
maxHeight = max(maxHeight, height)
154174
self.values.append(value)
155175
}
156176

@@ -393,13 +413,20 @@ struct InstructionTranslator: InstructionVisitor {
393413
valueStack.truncate(height: currentFrame.stackHeight)
394414
}
395415

396-
public mutating func finalize() -> [Instruction] {
416+
public mutating func finalize() -> InstructionSequence {
397417
iseqBuilder.pinLabelHere(self.endOfFunctionLabel)
398418
#if DEBUG
399419
// Check dangling labels
400420
iseqBuilder.assertDanglingLabels()
401421
#endif
402-
return iseqBuilder.finalize()
422+
let instructions = iseqBuilder.finalize()
423+
// TODO: Figure out a way to avoid the copy here while keeping the execution performance.
424+
let buffer = allocator.allocateInstructions(capacity: instructions.count + 1)
425+
for (idx, instruction) in instructions.enumerated() {
426+
buffer[idx] = instruction
427+
}
428+
buffer[instructions.count] = .endOfFunction
429+
return InstructionSequence(instructions: UnsafeBufferPointer(buffer), maxStackHeight: valueStack.maxHeight)
403430
}
404431

405432
// MARK: - Visitor

Sources/WasmKit/Types/Module.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,14 @@ typealias LabelIndex = UInt32
8080
/// > Note:
8181
/// <https://webassembly.github.io/spec/core/syntax/modules.html#functions>
8282
struct GuestFunction {
83-
init(type: TypeIndex, locals: [WasmParser.ValueType], body: @escaping () throws -> InstructionSequence) {
83+
init(
84+
type: TypeIndex,
85+
locals: [WasmParser.ValueType],
86+
allocator: ISeqAllocator,
87+
body: @escaping () throws -> InstructionSequence
88+
) {
8489
self.type = type
85-
// TODO: Deallocate const default locals after the module is deallocated
86-
let defaultLocals = UnsafeMutableBufferPointer<Value>.allocate(capacity: locals.count)
87-
for (index, localType) in locals.enumerated() {
88-
defaultLocals[index] = localType.defaultValue
89-
}
90-
self.defaultLocals = UnsafeBufferPointer(defaultLocals)
90+
self.defaultLocals = allocator.allocateDefaultLocals(locals)
9191
self.materializer = body
9292
}
9393

Tests/WasmKitTests/Execution/HostModuleTests.swift

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,49 @@ final class HostModuleTests: XCTestCase {
2323
func testReentrancy() throws {
2424
let runtime = Runtime()
2525
let voidSignature = WasmParser.FunctionType(parameters: [], results: [])
26+
let allocator = ISeqAllocator()
27+
func compile(_ instructions: [WasmKit.Instruction], maxStackHeight: Int) -> InstructionSequence {
28+
let buffer = allocator.allocateInstructions(capacity: instructions.count + 1)
29+
for (i, instruction) in instructions.enumerated() {
30+
buffer[i] = instruction
31+
}
32+
buffer[instructions.count] = .endOfFunction
33+
return InstructionSequence(
34+
instructions: UnsafeBufferPointer(buffer), maxStackHeight: maxStackHeight
35+
)
36+
}
2637
let module = Module(
2738
types: [voidSignature],
2839
functions: [
2940
// [0] (import "env" "bar" func)
3041
// [1] (import "env" "qux" func)
3142
// [2] "foo"
3243
GuestFunction(
33-
type: 0, locals: [],
44+
type: 0,
45+
locals: [],
46+
allocator: allocator,
3447
body: {
35-
[
36-
.call(functionIndex: 0),
37-
.call(functionIndex: 0),
38-
.call(functionIndex: 0),
39-
]
48+
compile(
49+
[
50+
.call(functionIndex: 0),
51+
.call(functionIndex: 0),
52+
.call(functionIndex: 0),
53+
],
54+
maxStackHeight: 0
55+
)
4056
}),
4157
// [3] "bar"
4258
GuestFunction(
43-
type: 0, locals: [],
59+
type: 0,
60+
locals: [],
61+
allocator: allocator,
4462
body: {
45-
[
46-
.call(functionIndex: 1)
47-
]
63+
compile(
64+
[
65+
.call(functionIndex: 1)
66+
],
67+
maxStackHeight: 0
68+
)
4869
}),
4970
],
5071
imports: [

0 commit comments

Comments
 (0)