Skip to content

Commit 8b18149

Browse files
MaxDesiatovkateinoigakukun
authored andcommitted
Add shared linear memory as prerequisite for wasip1-threads
A `shared` memory is backed by `SharedMemoryStorage`, which reserves its full address range up front so the base pointer stays stable, then commits pages on `memory.grow` under a spinlock that the SIGSEGV/SIGBUS handler also takes, so a concurrent grow cannot race an in-flight access; `memory.atomic.wait32/64` and `memory.atomic.notify` block and wake threads through `AtomicParkingLot`. Adds `SharedMemoryStorageTests` and `AtomicParkingLotTests`.
1 parent 46640f2 commit 8b18149

22 files changed

Lines changed: 2887 additions & 299 deletions

Sources/WasmKit/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ add_wasmkit_library(WasmKit
4040
Execution/NameRegistry.swift
4141
Execution/Profiler.swift
4242
Execution/Runtime.swift
43+
Execution/SharedMemoryStorage.swift
4344
Execution/SignpostTracer.swift
4445
Execution/Store.swift
4546
Execution/StoreAllocator.swift

Sources/WasmKit/Execution/AtomicParkingLot.swift

Lines changed: 231 additions & 125 deletions
Large diffs are not rendered by default.

Sources/WasmKit/Execution/DispatchInstruction.swift

Lines changed: 207 additions & 0 deletions
Large diffs are not rendered by default.

Sources/WasmKit/Execution/Errors.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ public struct WasmKitException: Error, CustomStringConvertible {
8080
}
8181

8282
/// A reason for a trap that occurred during execution of a WebAssembly module.
83-
package enum TrapReason: Error, CustomStringConvertible {
84-
package struct Message {
83+
package enum TrapReason: Error, Equatable, CustomStringConvertible {
84+
package struct Message: Equatable {
8585
let text: String
8686

8787
init(_ text: String) {
@@ -158,6 +158,15 @@ extension TrapReason.Message {
158158
static var cannotAssignToImmutableGlobal: Self {
159159
Self("cannot assign to an immutable global")
160160
}
161+
static func mmapFailed(reserveBytes: Int) -> Self {
162+
Self("failed to reserve \(reserveBytes) bytes of virtual address space for shared memory")
163+
}
164+
static var sharedMemoryGuardRegistryFull: Self {
165+
Self("shared memory guard registry is full (too many concurrent shared memories)")
166+
}
167+
static var atomicWaitUnsupported: Self {
168+
Self("memory.atomic.wait is not supported on this platform (no thread-blocking primitive)")
169+
}
161170
static func noGlobalExportWithName(globalName: String, instance: Instance) -> Self {
162171
Self("no global export with name \(globalName) in a module instance \(instance)")
163172
}

Sources/WasmKit/Execution/Execution.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,13 @@ extension Execution {
513513
var pc = pc
514514
var md = md
515515
var ms = ms
516-
let shouldUseMprotectTrapGuards = store.value.engine.configuration.memoryBoundsChecking == .mprotect
516+
// Shared memory always uses software bounds checking. A guard-page fault
517+
// recovered by siglongjmp would unwind the interpreter past a lock held by a
518+
// concurrent grow or atomic operation, so the multi-threaded path must stay
519+
// free of non-local jumps. mprotect guards are reserved for non-shared memory.
520+
let isNonSharedMemory = sp.currentInstance?.memories.first?.withValue { $0.sharedStorage == nil } ?? true
521+
let shouldUseMprotectTrapGuards =
522+
store.value.engine.configuration.memoryBoundsChecking == .mprotect && isNonSharedMemory
517523
let storeValue = store.value
518524
while true {
519525
let handler = pc.read(wasmkit_tc_exec.self)

Sources/WasmKit/Execution/Instances.swift

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,30 @@ package struct EntityHandle<T: ~Copyable>: Equatable, Hashable, Copyable {
6666
}
6767
}
6868

69+
/// A `Sendable` reference to a `SharedMemoryStorage` allocation.
70+
///
71+
/// `EntityHandle<T>` wraps `UnsafeMutablePointer<T>`, which is not `Sendable` in
72+
/// Swift 6. Shared memory is the only entity that must cross thread boundaries
73+
/// (via `ThreadGroup`), so this purpose-built type stores the pointer as a `UInt`
74+
/// bit pattern that the compiler can verify as `Sendable`.
75+
///
76+
/// The underlying `SharedMemoryStorage` must outlive all threads holding a
77+
/// `SharedMemoryEntity`. This is guaranteed by the `StoreAllocator`'s
78+
/// `BumpAllocator<SharedMemoryStorage>`, which lives as long as the parent `Store`.
79+
package struct SharedMemoryEntity: Sendable, Equatable, Hashable {
80+
private let bits: UInt
81+
82+
init(unsafe pointer: UnsafeMutablePointer<SharedMemoryStorage>) {
83+
self.bits = UInt(bitPattern: pointer)
84+
}
85+
86+
@inline(__always)
87+
func withValue<R>(_ body: (inout SharedMemoryStorage) throws -> R) rethrows -> R {
88+
let pointer = UnsafeMutablePointer<SharedMemoryStorage>(bitPattern: bits)!
89+
return try body(&pointer.pointee)
90+
}
91+
}
92+
6993
extension EntityHandle: ValidatableEntity where T: ValidatableEntity, T: ~Copyable {
7094
static func createOutOfBoundsError(index: Int, count: Int) -> WasmKitError {
7195
T.createOutOfBoundsError(index: index, count: count)
@@ -589,7 +613,10 @@ struct MemoryEntity: ~Copyable {
589613
private var storage: Storage
590614
let maxPageCount: UInt64
591615
let limit: Limits
592-
let sharedMutex: Mutex<Void>?
616+
617+
/// If this memory is shared, the mmap-backed storage is held here.
618+
/// Multiple `MemoryEntity` instances (across threads) may reference the same `SharedMemoryStorage`.
619+
let sharedStorage: SharedMemoryEntity?
593620

594621
init(_ memoryType: MemoryType, engineConfiguration: EngineConfiguration, resourceLimiter: any ResourceLimiter) throws {
595622
let byteSize = Int(memoryType.min) * Self.pageSize
@@ -600,26 +627,80 @@ struct MemoryEntity: ~Copyable {
600627
let defaultMaxPageCount = Self.maxPageCount(isMemory64: memoryType.isMemory64)
601628
maxPageCount = memoryType.max ?? defaultMaxPageCount
602629
limit = memoryType
603-
sharedMutex = memoryType.shared ? Mutex<Void>(()) : nil
630+
sharedStorage = nil
631+
}
632+
633+
/// Creates a memory entity for a shared memory backed by pre-allocated `SharedMemoryStorage`.
634+
///
635+
/// The inline `storage` is an empty placeholder; every accessor routes through
636+
/// `sharedStorage` first.
637+
init(_ memoryType: MemoryType, sharedStorage: SharedMemoryEntity) {
638+
precondition(memoryType.shared)
639+
// The inline storage is never accessed for shared memories (all accessors
640+
// route through `sharedStorage`), so allocate an empty software-backed
641+
// placeholder to avoid reserving any mprotect address space.
642+
var placeholderConfiguration = EngineConfiguration()
643+
placeholderConfiguration.memoryBoundsChecking = .software
644+
self.storage = Storage(byteSize: 0, isMemory64: memoryType.isMemory64, engineConfiguration: placeholderConfiguration)
645+
self.maxPageCount = memoryType.max ?? Self.maxPageCount(isMemory64: memoryType.isMemory64)
646+
self.limit = memoryType
647+
self.sharedStorage = sharedStorage
604648
}
605649

606650
deinit {
651+
// Only deallocate inline storage for non-shared memories.
652+
// `SharedMemoryStorage` cleanup is managed by its own `BumpAllocator`.
653+
guard sharedStorage == nil else { return }
607654
storage.deallocate()
608655
}
609656

610-
var data: UnsafeBufferPointer<UInt8> { storage.data }
657+
var data: UnsafeBufferPointer<UInt8> {
658+
if let sharedStorage {
659+
return sharedStorage.withValue { shared in
660+
let count = shared.currentByteCount.load(ordering: .acquiring)
661+
return UnsafeBufferPointer(
662+
start: shared.basePointer.assumingMemoryBound(to: UInt8.self),
663+
count: count
664+
)
665+
}
666+
}
667+
return storage.data
668+
}
611669

612-
var baseAddress: UnsafeMutableRawPointer? { storage.baseAddress }
670+
var baseAddress: UnsafeMutableRawPointer? {
671+
if let sharedStorage {
672+
return sharedStorage.withValue { $0.basePointer }
673+
}
674+
return storage.baseAddress
675+
}
613676

614-
var byteCount: Int { storage.byteCount }
677+
var byteCount: Int {
678+
if let sharedStorage {
679+
return sharedStorage.withValue { $0.currentByteCount.load(ordering: .acquiring) }
680+
}
681+
return storage.byteCount
682+
}
615683

616684
var trapGuardReservationSize: Int {
617-
storage.trapGuardReservationSize
685+
if let sharedStorage {
686+
return sharedStorage.withValue { $0.reservationSize }
687+
}
688+
return storage.trapGuardReservationSize
618689
}
619690

620691
/// > Note:
621692
/// <https://webassembly.github.io/spec/core/exec/modules.html#grow-mem>
622693
mutating func grow(by pageCount: Int, resourceLimiter: any ResourceLimiter) throws -> Value {
694+
if let sharedStorage {
695+
let oldPages = try sharedStorage.withValue { shared in
696+
try shared.grow(by: pageCount, resourceLimiter: resourceLimiter)
697+
}
698+
if oldPages < 0 {
699+
return limit.isMemory64 ? .i64((-1 as Int64).unsigned) : .i32((-1 as Int32).unsigned)
700+
}
701+
return limit.isMemory64 ? .i64(UInt64(oldPages)) : .i32(UInt32(oldPages))
702+
}
703+
623704
let currentByteCount = byteCount
624705
let newPageCount = currentByteCount / Self.pageSize + pageCount
625706

@@ -757,6 +838,22 @@ public struct Memory: Equatable {
757838
)
758839
}
759840

841+
/// The shared memory entity backing this memory, if shared.
842+
package var sharedStorage: SharedMemoryEntity? {
843+
handle.withValue { $0.sharedStorage }
844+
}
845+
846+
/// Create a `Memory` in `store` that wraps an existing `SharedMemoryEntity`.
847+
///
848+
/// Used by `wasi_thread_spawn` to provide the same shared memory as an
849+
/// import to child `Store` instances.
850+
package init(store: Store, type: MemoryType, sharedStorage: SharedMemoryEntity) {
851+
self.init(
852+
handle: store.allocator.allocate(memoryType: type, sharedStorage: sharedStorage),
853+
allocator: store.allocator
854+
)
855+
}
856+
760857
/// Returns a copy of the memory data.
761858
@available(*, deprecated, message: "Use `withUnsafeBufferPointer(offset:count:_:)` or `withUnsafeMutableBufferPointer(offset:count:_:)` instead")
762859
public var data: [UInt8] {

Sources/WasmKit/Execution/Instructions/Instruction.swift

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,52 @@ enum Instruction: Equatable {
565565
case catchHandlers(Instruction.CatchHandlersOperand)
566566
/// Unregister exception handlers for a `try_table` block
567567
case catchHandlersEnd(Instruction.CatchHandlersEndOperand)
568+
/// WebAssembly Core Instruction `i32.load` on shared memory
569+
case i32LoadShared(Instruction.LoadOperand)
570+
/// WebAssembly Core Instruction `i64.load` on shared memory
571+
case i64LoadShared(Instruction.LoadOperand)
572+
/// WebAssembly Core Instruction `f32.load` on shared memory
573+
case f32LoadShared(Instruction.LoadOperand)
574+
/// WebAssembly Core Instruction `f64.load` on shared memory
575+
case f64LoadShared(Instruction.LoadOperand)
576+
/// WebAssembly Core Instruction `i32.load8_s` on shared memory
577+
case i32Load8SShared(Instruction.LoadOperand)
578+
/// WebAssembly Core Instruction `i32.load8_u` on shared memory
579+
case i32Load8UShared(Instruction.LoadOperand)
580+
/// WebAssembly Core Instruction `i32.load16_s` on shared memory
581+
case i32Load16SShared(Instruction.LoadOperand)
582+
/// WebAssembly Core Instruction `i32.load16_u` on shared memory
583+
case i32Load16UShared(Instruction.LoadOperand)
584+
/// WebAssembly Core Instruction `i64.load8_s` on shared memory
585+
case i64Load8SShared(Instruction.LoadOperand)
586+
/// WebAssembly Core Instruction `i64.load8_u` on shared memory
587+
case i64Load8UShared(Instruction.LoadOperand)
588+
/// WebAssembly Core Instruction `i64.load16_s` on shared memory
589+
case i64Load16SShared(Instruction.LoadOperand)
590+
/// WebAssembly Core Instruction `i64.load16_u` on shared memory
591+
case i64Load16UShared(Instruction.LoadOperand)
592+
/// WebAssembly Core Instruction `i64.load32_s` on shared memory
593+
case i64Load32SShared(Instruction.LoadOperand)
594+
/// WebAssembly Core Instruction `i64.load32_u` on shared memory
595+
case i64Load32UShared(Instruction.LoadOperand)
596+
/// WebAssembly Core Instruction `i32.store` on shared memory
597+
case i32StoreShared(Instruction.StoreOperand)
598+
/// WebAssembly Core Instruction `i64.store` on shared memory
599+
case i64StoreShared(Instruction.StoreOperand)
600+
/// WebAssembly Core Instruction `f32.store` on shared memory
601+
case f32StoreShared(Instruction.StoreOperand)
602+
/// WebAssembly Core Instruction `f64.store` on shared memory
603+
case f64StoreShared(Instruction.StoreOperand)
604+
/// WebAssembly Core Instruction `i32.store8` on shared memory
605+
case i32Store8Shared(Instruction.StoreOperand)
606+
/// WebAssembly Core Instruction `i32.store16` on shared memory
607+
case i32Store16Shared(Instruction.StoreOperand)
608+
/// WebAssembly Core Instruction `i64.store8` on shared memory
609+
case i64Store8Shared(Instruction.StoreOperand)
610+
/// WebAssembly Core Instruction `i64.store16` on shared memory
611+
case i64Store16Shared(Instruction.StoreOperand)
612+
/// WebAssembly Core Instruction `i64.store32` on shared memory
613+
case i64Store32Shared(Instruction.StoreOperand)
568614
}
569615

570616
extension Instruction {
@@ -1473,6 +1519,29 @@ extension Instruction {
14731519
case .throwRef(let immediate): return immediate
14741520
case .catchHandlers(let immediate): return immediate
14751521
case .catchHandlersEnd(let immediate): return immediate
1522+
case .i32LoadShared(let immediate): return immediate
1523+
case .i64LoadShared(let immediate): return immediate
1524+
case .f32LoadShared(let immediate): return immediate
1525+
case .f64LoadShared(let immediate): return immediate
1526+
case .i32Load8SShared(let immediate): return immediate
1527+
case .i32Load8UShared(let immediate): return immediate
1528+
case .i32Load16SShared(let immediate): return immediate
1529+
case .i32Load16UShared(let immediate): return immediate
1530+
case .i64Load8SShared(let immediate): return immediate
1531+
case .i64Load8UShared(let immediate): return immediate
1532+
case .i64Load16SShared(let immediate): return immediate
1533+
case .i64Load16UShared(let immediate): return immediate
1534+
case .i64Load32SShared(let immediate): return immediate
1535+
case .i64Load32UShared(let immediate): return immediate
1536+
case .i32StoreShared(let immediate): return immediate
1537+
case .i64StoreShared(let immediate): return immediate
1538+
case .f32StoreShared(let immediate): return immediate
1539+
case .f64StoreShared(let immediate): return immediate
1540+
case .i32Store8Shared(let immediate): return immediate
1541+
case .i32Store16Shared(let immediate): return immediate
1542+
case .i64Store8Shared(let immediate): return immediate
1543+
case .i64Store16Shared(let immediate): return immediate
1544+
case .i64Store32Shared(let immediate): return immediate
14761545
default: return nil
14771546
}
14781547
}
@@ -1757,6 +1826,29 @@ extension Instruction {
17571826
case .throwRef: return 271
17581827
case .catchHandlers: return 272
17591828
case .catchHandlersEnd: return 273
1829+
case .i32LoadShared: return 274
1830+
case .i64LoadShared: return 275
1831+
case .f32LoadShared: return 276
1832+
case .f64LoadShared: return 277
1833+
case .i32Load8SShared: return 278
1834+
case .i32Load8UShared: return 279
1835+
case .i32Load16SShared: return 280
1836+
case .i32Load16UShared: return 281
1837+
case .i64Load8SShared: return 282
1838+
case .i64Load8UShared: return 283
1839+
case .i64Load16SShared: return 284
1840+
case .i64Load16UShared: return 285
1841+
case .i64Load32SShared: return 286
1842+
case .i64Load32UShared: return 287
1843+
case .i32StoreShared: return 288
1844+
case .i64StoreShared: return 289
1845+
case .f32StoreShared: return 290
1846+
case .f64StoreShared: return 291
1847+
case .i32Store8Shared: return 292
1848+
case .i32Store16Shared: return 293
1849+
case .i64Store8Shared: return 294
1850+
case .i64Store16Shared: return 295
1851+
case .i64Store32Shared: return 296
17601852
}
17611853
}
17621854
}
@@ -2042,6 +2134,29 @@ extension Instruction {
20422134
case 271: return .throwRef(Instruction.ThrowRefOperand.load(from: &pc))
20432135
case 272: return .catchHandlers(Instruction.CatchHandlersOperand.load(from: &pc))
20442136
case 273: return .catchHandlersEnd(Instruction.CatchHandlersEndOperand.load(from: &pc))
2137+
case 274: return .i32LoadShared(Instruction.LoadOperand.load(from: &pc))
2138+
case 275: return .i64LoadShared(Instruction.LoadOperand.load(from: &pc))
2139+
case 276: return .f32LoadShared(Instruction.LoadOperand.load(from: &pc))
2140+
case 277: return .f64LoadShared(Instruction.LoadOperand.load(from: &pc))
2141+
case 278: return .i32Load8SShared(Instruction.LoadOperand.load(from: &pc))
2142+
case 279: return .i32Load8UShared(Instruction.LoadOperand.load(from: &pc))
2143+
case 280: return .i32Load16SShared(Instruction.LoadOperand.load(from: &pc))
2144+
case 281: return .i32Load16UShared(Instruction.LoadOperand.load(from: &pc))
2145+
case 282: return .i64Load8SShared(Instruction.LoadOperand.load(from: &pc))
2146+
case 283: return .i64Load8UShared(Instruction.LoadOperand.load(from: &pc))
2147+
case 284: return .i64Load16SShared(Instruction.LoadOperand.load(from: &pc))
2148+
case 285: return .i64Load16UShared(Instruction.LoadOperand.load(from: &pc))
2149+
case 286: return .i64Load32SShared(Instruction.LoadOperand.load(from: &pc))
2150+
case 287: return .i64Load32UShared(Instruction.LoadOperand.load(from: &pc))
2151+
case 288: return .i32StoreShared(Instruction.StoreOperand.load(from: &pc))
2152+
case 289: return .i64StoreShared(Instruction.StoreOperand.load(from: &pc))
2153+
case 290: return .f32StoreShared(Instruction.StoreOperand.load(from: &pc))
2154+
case 291: return .f64StoreShared(Instruction.StoreOperand.load(from: &pc))
2155+
case 292: return .i32Store8Shared(Instruction.StoreOperand.load(from: &pc))
2156+
case 293: return .i32Store16Shared(Instruction.StoreOperand.load(from: &pc))
2157+
case 294: return .i64Store8Shared(Instruction.StoreOperand.load(from: &pc))
2158+
case 295: return .i64Store16Shared(Instruction.StoreOperand.load(from: &pc))
2159+
case 296: return .i64Store32Shared(Instruction.StoreOperand.load(from: &pc))
20452160
default: fatalError("Unknown instruction opcode: \(opcode)")
20462161
}
20472162
}
@@ -2330,6 +2445,29 @@ extension Instruction {
23302445
case 271: return "throwRef"
23312446
case 272: return "catchHandlers"
23322447
case 273: return "catchHandlersEnd"
2448+
case 274: return "i32LoadShared"
2449+
case 275: return "i64LoadShared"
2450+
case 276: return "f32LoadShared"
2451+
case 277: return "f64LoadShared"
2452+
case 278: return "i32Load8SShared"
2453+
case 279: return "i32Load8UShared"
2454+
case 280: return "i32Load16SShared"
2455+
case 281: return "i32Load16UShared"
2456+
case 282: return "i64Load8SShared"
2457+
case 283: return "i64Load8UShared"
2458+
case 284: return "i64Load16SShared"
2459+
case 285: return "i64Load16UShared"
2460+
case 286: return "i64Load32SShared"
2461+
case 287: return "i64Load32UShared"
2462+
case 288: return "i32StoreShared"
2463+
case 289: return "i64StoreShared"
2464+
case 290: return "f32StoreShared"
2465+
case 291: return "f64StoreShared"
2466+
case 292: return "i32Store8Shared"
2467+
case 293: return "i32Store16Shared"
2468+
case 294: return "i64Store8Shared"
2469+
case 295: return "i64Store16Shared"
2470+
case 296: return "i64Store32Shared"
23332471
default: fatalError("Unknown instruction index: \(opcode)")
23342472
}
23352473
}

0 commit comments

Comments
 (0)