github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/wasm/memory.go (about) 1 package wasm 2 3 import ( 4 "container/list" 5 "encoding/binary" 6 "fmt" 7 "math" 8 "reflect" 9 "sync" 10 "sync/atomic" 11 "time" 12 "unsafe" 13 14 "github.com/tetratelabs/wazero/api" 15 "github.com/tetratelabs/wazero/experimental" 16 "github.com/tetratelabs/wazero/internal/internalapi" 17 "github.com/tetratelabs/wazero/internal/wasmruntime" 18 ) 19 20 const ( 21 // MemoryPageSize is the unit of memory length in WebAssembly, 22 // and is defined as 2^16 = 65536. 23 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 24 MemoryPageSize = uint32(65536) 25 // MemoryLimitPages is maximum number of pages defined (2^16). 26 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem 27 MemoryLimitPages = uint32(65536) 28 // MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize". 29 MemoryPageSizeInBits = 16 30 ) 31 32 // compile-time check to ensure MemoryInstance implements api.Memory 33 var _ api.Memory = &MemoryInstance{} 34 35 type waiters struct { 36 mux sync.Mutex 37 l *list.List 38 } 39 40 // MemoryInstance represents a memory instance in a store, and implements api.Memory. 41 // 42 // Note: In WebAssembly 1.0 (20191205), there may be up to one Memory per store, which means the precise memory is always 43 // wasm.Store Memories index zero: `store.Memories[0]` 44 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0. 45 type MemoryInstance struct { 46 internalapi.WazeroOnlyType 47 48 Buffer []byte 49 Min, Cap, Max uint32 50 Shared bool 51 // definition is known at compile time. 52 definition api.MemoryDefinition 53 54 // Mux is used in interpreter mode to prevent overlapping calls to atomic instructions, 55 // introduced with WebAssembly threads proposal. 56 Mux sync.Mutex 57 58 // waiters implements atomic wait and notify. It is implemented similarly to golang.org/x/sync/semaphore, 59 // with a fixed weight of 1 and no spurious notifications. 60 waiters sync.Map 61 62 expBuffer experimental.LinearMemory 63 } 64 65 // NewMemoryInstance creates a new instance based on the parameters in the SectionIDMemory. 66 func NewMemoryInstance(memSec *Memory, allocator experimental.MemoryAllocator) *MemoryInstance { 67 minBytes := MemoryPagesToBytesNum(memSec.Min) 68 capBytes := MemoryPagesToBytesNum(memSec.Cap) 69 maxBytes := MemoryPagesToBytesNum(memSec.Max) 70 71 var buffer []byte 72 var expBuffer experimental.LinearMemory 73 if allocator != nil { 74 expBuffer = allocator.Allocate(capBytes, maxBytes) 75 buffer = expBuffer.Reallocate(minBytes) 76 } else if memSec.IsShared { 77 // Shared memory needs a fixed buffer, so allocate with the maximum size. 78 // 79 // The rationale as to why we can simply use make([]byte) to a fixed buffer is that Go's GC is non-relocating. 80 // That is not a part of Go spec, but is well-known thing in Go community (wazero's compiler heavily relies on it!) 81 // * https://github.com/go4org/unsafe-assume-no-moving-gc 82 // 83 // Also, allocating Max here isn't harmful as the Go runtime uses mmap for large allocations, therefore, 84 // the memory buffer allocation here is virtual and doesn't consume physical memory until it's used. 85 // * https://github.com/golang/go/blob/8121604559035734c9677d5281bbdac8b1c17a1e/src/runtime/malloc.go#L1059 86 // * https://github.com/golang/go/blob/8121604559035734c9677d5281bbdac8b1c17a1e/src/runtime/malloc.go#L1165 87 buffer = make([]byte, minBytes, maxBytes) 88 } else { 89 buffer = make([]byte, minBytes, capBytes) 90 } 91 return &MemoryInstance{ 92 Buffer: buffer, 93 Min: memSec.Min, 94 Cap: memoryBytesNumToPages(uint64(cap(buffer))), 95 Max: memSec.Max, 96 Shared: memSec.IsShared, 97 expBuffer: expBuffer, 98 } 99 } 100 101 // Definition implements the same method as documented on api.Memory. 102 func (m *MemoryInstance) Definition() api.MemoryDefinition { 103 return m.definition 104 } 105 106 // Size implements the same method as documented on api.Memory. 107 func (m *MemoryInstance) Size() uint32 { 108 return uint32(len(m.Buffer)) 109 } 110 111 // ReadByte implements the same method as documented on api.Memory. 112 func (m *MemoryInstance) ReadByte(offset uint32) (byte, bool) { 113 if !m.hasSize(offset, 1) { 114 return 0, false 115 } 116 return m.Buffer[offset], true 117 } 118 119 // ReadUint16Le implements the same method as documented on api.Memory. 120 func (m *MemoryInstance) ReadUint16Le(offset uint32) (uint16, bool) { 121 if !m.hasSize(offset, 2) { 122 return 0, false 123 } 124 return binary.LittleEndian.Uint16(m.Buffer[offset : offset+2]), true 125 } 126 127 // ReadUint32Le implements the same method as documented on api.Memory. 128 func (m *MemoryInstance) ReadUint32Le(offset uint32) (uint32, bool) { 129 return m.readUint32Le(offset) 130 } 131 132 // ReadFloat32Le implements the same method as documented on api.Memory. 133 func (m *MemoryInstance) ReadFloat32Le(offset uint32) (float32, bool) { 134 v, ok := m.readUint32Le(offset) 135 if !ok { 136 return 0, false 137 } 138 return math.Float32frombits(v), true 139 } 140 141 // ReadUint64Le implements the same method as documented on api.Memory. 142 func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) { 143 return m.readUint64Le(offset) 144 } 145 146 // ReadFloat64Le implements the same method as documented on api.Memory. 147 func (m *MemoryInstance) ReadFloat64Le(offset uint32) (float64, bool) { 148 v, ok := m.readUint64Le(offset) 149 if !ok { 150 return 0, false 151 } 152 return math.Float64frombits(v), true 153 } 154 155 // Read implements the same method as documented on api.Memory. 156 func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) { 157 if !m.hasSize(offset, uint64(byteCount)) { 158 return nil, false 159 } 160 return m.Buffer[offset : offset+byteCount : offset+byteCount], true 161 } 162 163 // WriteByte implements the same method as documented on api.Memory. 164 func (m *MemoryInstance) WriteByte(offset uint32, v byte) bool { 165 if !m.hasSize(offset, 1) { 166 return false 167 } 168 m.Buffer[offset] = v 169 return true 170 } 171 172 // WriteUint16Le implements the same method as documented on api.Memory. 173 func (m *MemoryInstance) WriteUint16Le(offset uint32, v uint16) bool { 174 if !m.hasSize(offset, 2) { 175 return false 176 } 177 binary.LittleEndian.PutUint16(m.Buffer[offset:], v) 178 return true 179 } 180 181 // WriteUint32Le implements the same method as documented on api.Memory. 182 func (m *MemoryInstance) WriteUint32Le(offset, v uint32) bool { 183 return m.writeUint32Le(offset, v) 184 } 185 186 // WriteFloat32Le implements the same method as documented on api.Memory. 187 func (m *MemoryInstance) WriteFloat32Le(offset uint32, v float32) bool { 188 return m.writeUint32Le(offset, math.Float32bits(v)) 189 } 190 191 // WriteUint64Le implements the same method as documented on api.Memory. 192 func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool { 193 return m.writeUint64Le(offset, v) 194 } 195 196 // WriteFloat64Le implements the same method as documented on api.Memory. 197 func (m *MemoryInstance) WriteFloat64Le(offset uint32, v float64) bool { 198 return m.writeUint64Le(offset, math.Float64bits(v)) 199 } 200 201 // Write implements the same method as documented on api.Memory. 202 func (m *MemoryInstance) Write(offset uint32, val []byte) bool { 203 if !m.hasSize(offset, uint64(len(val))) { 204 return false 205 } 206 copy(m.Buffer[offset:], val) 207 return true 208 } 209 210 // WriteString implements the same method as documented on api.Memory. 211 func (m *MemoryInstance) WriteString(offset uint32, val string) bool { 212 if !m.hasSize(offset, uint64(len(val))) { 213 return false 214 } 215 copy(m.Buffer[offset:], val) 216 return true 217 } 218 219 // MemoryPagesToBytesNum converts the given pages into the number of bytes contained in these pages. 220 func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) { 221 return uint64(pages) << MemoryPageSizeInBits 222 } 223 224 // Grow implements the same method as documented on api.Memory. 225 func (m *MemoryInstance) Grow(delta uint32) (result uint32, ok bool) { 226 currentPages := m.Pages() 227 if delta == 0 { 228 return currentPages, true 229 } 230 231 // If exceeds the max of memory size, we push -1 according to the spec. 232 newPages := currentPages + delta 233 if newPages > m.Max || int32(delta) < 0 { 234 return 0, false 235 } else if m.expBuffer != nil { 236 buffer := m.expBuffer.Reallocate(MemoryPagesToBytesNum(newPages)) 237 if m.Shared { 238 if unsafe.SliceData(buffer) != unsafe.SliceData(m.Buffer) { 239 panic("shared memory cannot move, this is a bug in the memory allocator") 240 } 241 // We assume grow is called under a guest lock. 242 // But the memory length is accessed elsewhere, 243 // so use atomic to make the new length visible across threads. 244 atomicStoreLengthAndCap(&m.Buffer, uintptr(len(buffer)), uintptr(cap(buffer))) 245 m.Cap = memoryBytesNumToPages(uint64(cap(buffer))) 246 } else { 247 m.Buffer = buffer 248 m.Cap = newPages 249 } 250 return currentPages, true 251 } else if newPages > m.Cap { // grow the memory. 252 if m.Shared { 253 panic("shared memory cannot be grown, this is a bug in wazero") 254 } 255 m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(delta))...) 256 m.Cap = newPages 257 return currentPages, true 258 } else { // We already have the capacity we need. 259 if m.Shared { 260 // We assume grow is called under a guest lock. 261 // But the memory length is accessed elsewhere, 262 // so use atomic to make the new length visible across threads. 263 atomicStoreLength(&m.Buffer, uintptr(MemoryPagesToBytesNum(newPages))) 264 } else { 265 m.Buffer = m.Buffer[:MemoryPagesToBytesNum(newPages)] 266 } 267 return currentPages, true 268 } 269 } 270 271 // Pages implements the same method as documented on api.Memory. 272 func (m *MemoryInstance) Pages() (result uint32) { 273 return memoryBytesNumToPages(uint64(len(m.Buffer))) 274 } 275 276 // PagesToUnitOfBytes converts the pages to a human-readable form similar to what's specified. e.g. 1 -> "64Ki" 277 // 278 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 279 func PagesToUnitOfBytes(pages uint32) string { 280 k := pages * 64 281 if k < 1024 { 282 return fmt.Sprintf("%d Ki", k) 283 } 284 m := k / 1024 285 if m < 1024 { 286 return fmt.Sprintf("%d Mi", m) 287 } 288 g := m / 1024 289 if g < 1024 { 290 return fmt.Sprintf("%d Gi", g) 291 } 292 return fmt.Sprintf("%d Ti", g/1024) 293 } 294 295 // Below are raw functions used to implement the api.Memory API: 296 297 // Uses atomic write to update the length of a slice. 298 func atomicStoreLengthAndCap(slice *[]byte, length uintptr, cap uintptr) { 299 slicePtr := (*reflect.SliceHeader)(unsafe.Pointer(slice)) 300 capPtr := (*uintptr)(unsafe.Pointer(&slicePtr.Cap)) 301 atomic.StoreUintptr(capPtr, cap) 302 lenPtr := (*uintptr)(unsafe.Pointer(&slicePtr.Len)) 303 atomic.StoreUintptr(lenPtr, length) 304 } 305 306 // Uses atomic write to update the length of a slice. 307 func atomicStoreLength(slice *[]byte, length uintptr) { 308 slicePtr := (*reflect.SliceHeader)(unsafe.Pointer(slice)) 309 lenPtr := (*uintptr)(unsafe.Pointer(&slicePtr.Len)) 310 atomic.StoreUintptr(lenPtr, length) 311 } 312 313 // memoryBytesNumToPages converts the given number of bytes into the number of pages. 314 func memoryBytesNumToPages(bytesNum uint64) (pages uint32) { 315 return uint32(bytesNum >> MemoryPageSizeInBits) 316 } 317 318 // hasSize returns true if Len is sufficient for byteCount at the given offset. 319 // 320 // Note: This is always fine, because memory can grow, but never shrink. 321 func (m *MemoryInstance) hasSize(offset uint32, byteCount uint64) bool { 322 return uint64(offset)+byteCount <= uint64(len(m.Buffer)) // uint64 prevents overflow on add 323 } 324 325 // readUint32Le implements ReadUint32Le without using a context. This is extracted as both ints and floats are stored in 326 // memory as uint32le. 327 func (m *MemoryInstance) readUint32Le(offset uint32) (uint32, bool) { 328 if !m.hasSize(offset, 4) { 329 return 0, false 330 } 331 return binary.LittleEndian.Uint32(m.Buffer[offset : offset+4]), true 332 } 333 334 // readUint64Le implements ReadUint64Le without using a context. This is extracted as both ints and floats are stored in 335 // memory as uint64le. 336 func (m *MemoryInstance) readUint64Le(offset uint32) (uint64, bool) { 337 if !m.hasSize(offset, 8) { 338 return 0, false 339 } 340 return binary.LittleEndian.Uint64(m.Buffer[offset : offset+8]), true 341 } 342 343 // writeUint32Le implements WriteUint32Le without using a context. This is extracted as both ints and floats are stored 344 // in memory as uint32le. 345 func (m *MemoryInstance) writeUint32Le(offset uint32, v uint32) bool { 346 if !m.hasSize(offset, 4) { 347 return false 348 } 349 binary.LittleEndian.PutUint32(m.Buffer[offset:], v) 350 return true 351 } 352 353 // writeUint64Le implements WriteUint64Le without using a context. This is extracted as both ints and floats are stored 354 // in memory as uint64le. 355 func (m *MemoryInstance) writeUint64Le(offset uint32, v uint64) bool { 356 if !m.hasSize(offset, 8) { 357 return false 358 } 359 binary.LittleEndian.PutUint64(m.Buffer[offset:], v) 360 return true 361 } 362 363 // Wait32 suspends the caller until the offset is notified by a different agent. 364 func (m *MemoryInstance) Wait32(offset uint32, exp uint32, timeout int64, reader func(mem *MemoryInstance, offset uint32) uint32) uint64 { 365 w := m.getWaiters(offset) 366 w.mux.Lock() 367 368 cur := reader(m, offset) 369 if cur != exp { 370 w.mux.Unlock() 371 return 1 372 } 373 374 return m.wait(w, timeout) 375 } 376 377 // Wait64 suspends the caller until the offset is notified by a different agent. 378 func (m *MemoryInstance) Wait64(offset uint32, exp uint64, timeout int64, reader func(mem *MemoryInstance, offset uint32) uint64) uint64 { 379 w := m.getWaiters(offset) 380 w.mux.Lock() 381 382 cur := reader(m, offset) 383 if cur != exp { 384 w.mux.Unlock() 385 return 1 386 } 387 388 return m.wait(w, timeout) 389 } 390 391 func (m *MemoryInstance) wait(w *waiters, timeout int64) uint64 { 392 if w.l == nil { 393 w.l = list.New() 394 } 395 396 // The specification requires a trap if the number of existing waiters + 1 == 2^32, so we add a check here. 397 // In practice, it is unlikely the application would ever accumulate such a large number of waiters as it 398 // indicates several GB of RAM used just for the list of waiters. 399 // https://github.com/WebAssembly/threads/blob/main/proposals/threads/Overview.md#wait 400 if uint64(w.l.Len()+1) == 1<<32 { 401 w.mux.Unlock() 402 panic(wasmruntime.ErrRuntimeTooManyWaiters) 403 } 404 405 ready := make(chan struct{}) 406 elem := w.l.PushBack(ready) 407 w.mux.Unlock() 408 409 if timeout < 0 { 410 <-ready 411 return 0 412 } else { 413 select { 414 case <-ready: 415 return 0 416 case <-time.After(time.Duration(timeout)): 417 // While we could see if the channel completed by now and ignore the timeout, similar to x/sync/semaphore, 418 // the Wasm spec doesn't specify this behavior, so we keep things simple by prioritizing the timeout. 419 w.mux.Lock() 420 w.l.Remove(elem) 421 w.mux.Unlock() 422 return 2 423 } 424 } 425 } 426 427 func (m *MemoryInstance) getWaiters(offset uint32) *waiters { 428 wAny, ok := m.waiters.Load(offset) 429 if !ok { 430 // The first time an address is waited on, simultaneous waits will cause extra allocations. 431 // Further operations will be loaded above, which is also the general pattern of usage with 432 // mutexes. 433 wAny, _ = m.waiters.LoadOrStore(offset, &waiters{}) 434 } 435 436 return wAny.(*waiters) 437 } 438 439 // Notify wakes up at most count waiters at the given offset. 440 func (m *MemoryInstance) Notify(offset uint32, count uint32) uint32 { 441 wAny, ok := m.waiters.Load(offset) 442 if !ok { 443 return 0 444 } 445 w := wAny.(*waiters) 446 447 w.mux.Lock() 448 defer w.mux.Unlock() 449 if w.l == nil { 450 return 0 451 } 452 453 res := uint32(0) 454 for num := w.l.Len(); num > 0 && res < count; num = w.l.Len() { 455 w := w.l.Remove(w.l.Front()).(chan struct{}) 456 close(w) 457 res++ 458 } 459 460 return res 461 }