Skip to content

Commit

Permalink
Merge pull request #98 from swiftwasm/katei/fix-heap-overflow
Browse files Browse the repository at this point in the history
Fix value stack overflow
  • Loading branch information
kateinoigakukun committed Jul 1, 2024
2 parents 4b70f1b + 59c7da8 commit d30a1de
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 100 deletions.
23 changes: 1 addition & 22 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
NAME := WasmKit

MODULES = $(notdir $(wildcard Sources/*))

.PHONY: all
all: build

.PHONY: build
build:
@swift build

.PHONY: test
test:
@swift test

.PHONY: docs
docs:
swift package generate-documentation --target WasmKit
Expand All @@ -37,7 +24,7 @@ $(SPECTEST_ROOT)/%.json: $(TESTSUITE_DIR)/%.wast

.PHONY: spectest
spectest: spec
swift run Spectest $(SPECTEST_ROOT)
swift run --sanitize address Spectest $(SPECTEST_ROOT)


### WASI Test Suite
Expand All @@ -52,11 +39,3 @@ wasitest:
generate:
swift ./Utilities/generate_inst_visitor.swift
swift ./Utilities/generate_inst_dispatch.swift

GIT_STATUS = $(shell git status --porcelain)
ensure_clean:
@[ -z "$(GIT_STATUS)" ] \
&& echo Working directory is clean \
|| (printf "Uncommitted changes: \n $(GIT_STATUS)\n" && exit 1)

FORCE:
27 changes: 8 additions & 19 deletions Sources/WasmKit/Execution/Instructions/Expression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@ import WasmParser

struct InstructionSequence: Equatable {
let instructions: UnsafeBufferPointer<Instruction>

init(instructions: [Instruction]) {
assert(_isPOD(Instruction.self))
let buffer = UnsafeMutableBufferPointer<Instruction>.allocate(capacity: instructions.count + 1)
for (idx, instruction) in instructions.enumerated() {
buffer[idx] = instruction
}
buffer[instructions.count] = .endOfFunction
self.instructions = UnsafeBufferPointer(buffer)
}

func deallocate() {
instructions.deallocate()
/// The maximum height of the value stack during execution of this function.
/// This height does not count the locals.
let maxStackHeight: Int

init(instructions: UnsafeBufferPointer<Instruction>, maxStackHeight: Int) {
self.instructions = instructions
assert(self.instructions.last == .endOfFunction)
self.maxStackHeight = maxStackHeight
}

var baseAddress: UnsafePointer<Instruction> {
Expand All @@ -26,12 +21,6 @@ struct InstructionSequence: Equatable {
}
}

extension InstructionSequence: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Instruction...) {
self.init(instructions: elements)
}
}

struct ExpressionRef: Equatable {
let _relativeOffset: UInt32
var relativeOffset: Int {
Expand Down
1 change: 1 addition & 0 deletions Sources/WasmKit/Execution/Runtime/Function.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public struct Function: Equatable {
public func invoke(_ arguments: [Value] = [], runtime: Runtime) throws -> [Value] {
try withExecution { execution in
var stack = Stack()
defer { stack.deallocate() }
let numberOfResults = try invoke(execution: &execution, stack: &stack, with: arguments, runtime: runtime)
try execution.run(runtime: runtime, stack: &stack)
return Array(stack.popValues(count: numberOfResults))
Expand Down
39 changes: 26 additions & 13 deletions Sources/WasmKit/Execution/Runtime/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,21 @@ extension Runtime {
.numericConst(.i32(UInt32(element.initializer.count))),
.tableInit(tableIndex, elementIndex),
.tableElementDrop(elementIndex),
.endOfFunction,
])
let initIseq = InstructionSequence(instructions: instructions)
defer { initIseq.deallocate() }
try evaluateConstExpr(initIseq, instance: instance)
try instructions.withUnsafeBufferPointer {
let initIseq = InstructionSequence(instructions: $0, maxStackHeight: 2)
try evaluateConstExpr(initIseq, instance: instance)
}

case .declarative:
let initIseq: InstructionSequence = [.tableElementDrop(elementIndex)]
defer { initIseq.deallocate() }
try evaluateConstExpr(initIseq, instance: instance)
let instructions: [Instruction] = [.tableElementDrop(elementIndex), .endOfFunction]
try instructions.withUnsafeBufferPointer {
let initIseq = InstructionSequence(
instructions: $0, maxStackHeight: 0
)
try evaluateConstExpr(initIseq, instance: instance)
}

case .passive:
continue
Expand All @@ -122,14 +128,17 @@ extension Runtime {
default:
throw InstantiationError.unsupported("init expr in data section \(data.offset)")
}
let iseq = InstructionSequence(instructions: instructions + [
instructions.append(contentsOf: [
.numericConst(.i32(0)),
.numericConst(.i32(UInt32(data.initializer.count))),
.memoryInit(UInt32(dataIndex)),
.memoryDataDrop(UInt32(dataIndex)),
.endOfFunction,
])
defer { iseq.deallocate() }
try evaluateConstExpr(iseq, instance: instance)
try instructions.withUnsafeBufferPointer {
let iseq = InstructionSequence(instructions: $0, maxStackHeight: 2)
try evaluateConstExpr(iseq, instance: instance)
}
}
} catch Trap.outOfBoundsMemoryAccess {
throw InstantiationError.outOfBoundsMemoryAccess
Expand All @@ -141,6 +150,7 @@ extension Runtime {
if let startIndex = module.start {
try withExecution { initExecution in
var stack = Stack()
defer { stack.deallocate() }
try initExecution.invoke(functionAddress: instance.functionAddresses[Int(startIndex)], runtime: self, stack: &stack)
try initExecution.run(runtime: self, stack: &stack)
}
Expand Down Expand Up @@ -189,10 +199,12 @@ extension Runtime {
default:
throw InstantiationError.unsupported("init expr in global section \(global.initializer)")
}
let iseq = InstructionSequence(instructions: instructions)
defer { iseq.deallocate() }
return try evaluateConstExpr(iseq, instance: globalModuleInstance, arity: 1) { _, stack in
return stack.popValue()
instructions.append(.endOfFunction)
return try instructions.withUnsafeBufferPointer {
let iseq = InstructionSequence(instructions: $0, maxStackHeight: 1)
return try evaluateConstExpr(iseq, instance: globalModuleInstance, arity: 1) { _, stack in
return stack.popValue()
}
}
}

Expand All @@ -212,6 +224,7 @@ extension Runtime {
) throws -> T {
try withExecution { initExecution in
var stack = Stack()
defer { stack.deallocate() }
try stack.pushFrame(
iseq: iseq,
arity: arity,
Expand Down
12 changes: 10 additions & 2 deletions Sources/WasmKit/Execution/Runtime/Stack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ struct Stack {
returnPC: ProgramCounter,
address: FunctionAddress? = nil
) throws {
// TODO: Stack overflow check can be done at the entry of expression
guard (frames.count + numberOfValues) < limit else {
guard frames.count < limit, (numberOfValues + iseq.maxStackHeight + (defaultLocals?.count ?? 0)) < limit else {
throw Trap.callStackExhausted
}
let valueFrameIndex = self.numberOfValues - argc
Expand Down Expand Up @@ -63,6 +62,11 @@ struct Stack {
self.valueStack.copyValues(copyCount: copyCount, popCount: popCount)
}

func deallocate() {
self.valueStack.deallocate()
self.frames.deallocate()
}

var topValue: Value {
self.valueStack.topValue
}
Expand Down Expand Up @@ -204,6 +208,10 @@ struct FixedSizeStack<Element> {
subscript(_ index: Int) -> Element {
self.buffer[index]
}

func deallocate() {
self.buffer.deallocate()
}
}

extension FixedSizeStack: Sequence {
Expand Down
28 changes: 4 additions & 24 deletions Sources/WasmKit/ModuleParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,45 +125,25 @@ func parseModule<Stream: ByteStream>(stream: Stream, features: WasmFeatureSet =
memoryTypes: module.memories.map { $0.type },
tables: module.tables
)
let allocator = module.allocator
let functions = codes.enumerated().map { [hasDataCount = parser.hasDataCount, features] index, code in
let funcTypeIndex = typeIndices[index]
let funcType = module.types[Int(funcTypeIndex)]
return GuestFunction(
type: typeIndices[index], locals: code.locals,
type: typeIndices[index], locals: code.locals, allocator: allocator,
body: {
let enableAssert: Bool
#if ASSERT
enableAssert = true
#else
enableAssert = false
#endif

var translator = InstructionTranslator(
allocator: module.allocator,
allocator: allocator,
module: translatorContext,
type: funcType, locals: code.locals
)

if enableAssert && !_isFastAssertConfiguration() {
let globalFuncIndex = module.imports.count + index
print("🚀 Starting Translation for code[\(globalFuncIndex)] (\(funcType))")
var tracing = InstructionTracingVisitor(trace: {
print("🍵 code[\(globalFuncIndex)] Translating \($0)")
}, visitor: translator)
try WasmParser.parseExpression(
bytes: Array(code.expression),
features: features, hasDataCount: hasDataCount,
visitor: &tracing
)
let newISeq = InstructionSequence(instructions: tracing.visitor.finalize())
return newISeq
}
try WasmParser.parseExpression(
bytes: Array(code.expression),
features: features, hasDataCount: hasDataCount,
visitor: &translator
)
return InstructionSequence(instructions: translator.finalize())
return translator.finalize()
})
}
module.functions = functions
Expand Down
33 changes: 30 additions & 3 deletions Sources/WasmKit/Translator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ class ISeqAllocator {
return buffer
}

func allocateDefaultLocals(_ locals: [ValueType]) -> UnsafeBufferPointer<Value> {
let buffer = UnsafeMutableBufferPointer<Value>.allocate(capacity: locals.count)
for (index, localType) in locals.enumerated() {
buffer[index] = localType.defaultValue
}
self.buffers.append(UnsafeMutableRawBufferPointer(buffer))
return UnsafeBufferPointer(buffer)
}

func allocateInstructions(capacity: Int) -> UnsafeMutableBufferPointer<Instruction> {
assert(_isPOD(Instruction.self), "Instruction must be POD")
let buffer = UnsafeMutableBufferPointer<Instruction>.allocate(capacity: capacity)
self.buffers.append(UnsafeMutableRawBufferPointer(buffer))
return buffer
}

deinit {
for buffer in buffers {
buffer.deallocate()
Expand Down Expand Up @@ -145,12 +161,16 @@ struct InstructionTranslator: InstructionVisitor {
}
struct ValueStack {
private var values: [MetaValue] = []
/// The maximum height of the stack within the function
private(set) var maxHeight: Int = 0
var height: Int { values.count }

mutating func push(_ value: ValueType) {
self.values.append(.some(value))
push(.some(value))
}
mutating func push(_ value: MetaValue) {
// Record the maximum height of the stack we have seen
maxHeight = max(maxHeight, height)
self.values.append(value)
}

Expand Down Expand Up @@ -393,13 +413,20 @@ struct InstructionTranslator: InstructionVisitor {
valueStack.truncate(height: currentFrame.stackHeight)
}

public mutating func finalize() -> [Instruction] {
public mutating func finalize() -> InstructionSequence {
iseqBuilder.pinLabelHere(self.endOfFunctionLabel)
#if DEBUG
// Check dangling labels
iseqBuilder.assertDanglingLabels()
#endif
return iseqBuilder.finalize()
let instructions = iseqBuilder.finalize()
// TODO: Figure out a way to avoid the copy here while keeping the execution performance.
let buffer = allocator.allocateInstructions(capacity: instructions.count + 1)
for (idx, instruction) in instructions.enumerated() {
buffer[idx] = instruction
}
buffer[instructions.count] = .endOfFunction
return InstructionSequence(instructions: UnsafeBufferPointer(buffer), maxStackHeight: valueStack.maxHeight)
}

// MARK: - Visitor
Expand Down
14 changes: 7 additions & 7 deletions Sources/WasmKit/Types/Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ typealias LabelIndex = UInt32
/// > Note:
/// <https://webassembly.github.io/spec/core/syntax/modules.html#functions>
struct GuestFunction {
init(type: TypeIndex, locals: [WasmParser.ValueType], body: @escaping () throws -> InstructionSequence) {
init(
type: TypeIndex,
locals: [WasmParser.ValueType],
allocator: ISeqAllocator,
body: @escaping () throws -> InstructionSequence
) {
self.type = type
// TODO: Deallocate const default locals after the module is deallocated
let defaultLocals = UnsafeMutableBufferPointer<Value>.allocate(capacity: locals.count)
for (index, localType) in locals.enumerated() {
defaultLocals[index] = localType.defaultValue
}
self.defaultLocals = UnsafeBufferPointer(defaultLocals)
self.defaultLocals = allocator.allocateDefaultLocals(locals)
self.materializer = body
}

Expand Down
41 changes: 31 additions & 10 deletions Tests/WasmKitTests/Execution/HostModuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,49 @@ final class HostModuleTests: XCTestCase {
func testReentrancy() throws {
let runtime = Runtime()
let voidSignature = WasmParser.FunctionType(parameters: [], results: [])
let allocator = ISeqAllocator()
func compile(_ instructions: [WasmKit.Instruction], maxStackHeight: Int) -> InstructionSequence {
let buffer = allocator.allocateInstructions(capacity: instructions.count + 1)
for (i, instruction) in instructions.enumerated() {
buffer[i] = instruction
}
buffer[instructions.count] = .endOfFunction
return InstructionSequence(
instructions: UnsafeBufferPointer(buffer), maxStackHeight: maxStackHeight
)
}
let module = Module(
types: [voidSignature],
functions: [
// [0] (import "env" "bar" func)
// [1] (import "env" "qux" func)
// [2] "foo"
GuestFunction(
type: 0, locals: [],
type: 0,
locals: [],
allocator: allocator,
body: {
[
.call(functionIndex: 0),
.call(functionIndex: 0),
.call(functionIndex: 0),
]
compile(
[
.call(functionIndex: 0),
.call(functionIndex: 0),
.call(functionIndex: 0),
],
maxStackHeight: 0
)
}),
// [3] "bar"
GuestFunction(
type: 0, locals: [],
type: 0,
locals: [],
allocator: allocator,
body: {
[
.call(functionIndex: 1)
]
compile(
[
.call(functionIndex: 1)
],
maxStackHeight: 0
)
}),
],
imports: [
Expand Down

0 comments on commit d30a1de

Please sign in to comment.