github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/wasm/store.go (about) 1 package wasm 2 3 import ( 4 "context" 5 "encoding/binary" 6 "fmt" 7 "sync" 8 "sync/atomic" 9 10 "github.com/wasilibs/wazerox/api" 11 "github.com/wasilibs/wazerox/internal/close" 12 "github.com/wasilibs/wazerox/internal/internalapi" 13 "github.com/wasilibs/wazerox/internal/leb128" 14 internalsys "github.com/wasilibs/wazerox/internal/sys" 15 "github.com/wasilibs/wazerox/sys" 16 ) 17 18 // nameToModuleShrinkThreshold is the size the nameToModule map can grow to 19 // before it starts to be monitored for shrinking. 20 // The capacity will never be smaller than this once the threshold is met. 21 const nameToModuleShrinkThreshold = 100 22 23 type ( 24 // Store is the runtime representation of "instantiated" Wasm module and objects. 25 // Multiple modules can be instantiated within a single store, and each instance, 26 // (e.g. function instance) can be referenced by other module instances in a Store via Module.ImportSection. 27 // 28 // Every type whose name ends with "Instance" suffix belongs to exactly one store. 29 // 30 // Note that store is not thread (concurrency) safe, meaning that using single Store 31 // via multiple goroutines might result in race conditions. In that case, the invocation 32 // and access to any methods and field of Store must be guarded by mutex. 33 // 34 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#store%E2%91%A0 35 Store struct { 36 // moduleList ensures modules are closed in reverse initialization order. 37 moduleList *ModuleInstance // guarded by mux 38 39 // nameToModule holds the instantiated Wasm modules by module name from Instantiate. 40 // It ensures no race conditions instantiating two modules of the same name. 41 nameToModule map[string]*ModuleInstance // guarded by mux 42 43 // nameToModuleCap tracks the growth of the nameToModule map in order to 44 // track when to shrink it. 45 nameToModuleCap int // guarded by mux 46 47 // EnabledFeatures are read-only to allow optimizations. 48 EnabledFeatures api.CoreFeatures 49 50 // Engine is a global context for a Store which is in responsible for compilation and execution of Wasm modules. 51 Engine Engine 52 53 // typeIDs maps each FunctionType.String() to a unique FunctionTypeID. This is used at runtime to 54 // do type-checks on indirect function calls. 55 typeIDs map[string]FunctionTypeID 56 57 // functionMaxTypes represents the limit on the number of function types in a store. 58 // Note: this is fixed to 2^27 but have this a field for testability. 59 functionMaxTypes uint32 60 61 // mux is used to guard the fields from concurrent access. 62 mux sync.RWMutex 63 } 64 65 // ModuleInstance represents instantiated wasm module. 66 // The difference from the spec is that in wazero, a ModuleInstance holds pointers 67 // to the instances, rather than "addresses" (i.e. index to Store.Functions, Globals, etc) for convenience. 68 // 69 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-moduleinst 70 // 71 // This implements api.Module. 72 ModuleInstance struct { 73 internalapi.WazeroOnlyType 74 75 ModuleName string 76 Exports map[string]*Export 77 Globals []*GlobalInstance 78 MemoryInstance *MemoryInstance 79 Tables []*TableInstance 80 81 // Engine implements function calls for this module. 82 Engine ModuleEngine 83 84 // TypeIDs is index-correlated with types and holds typeIDs which is uniquely assigned to a type by store. 85 // This is necessary to achieve fast runtime type checking for indirect function calls at runtime. 86 TypeIDs []FunctionTypeID 87 88 // DataInstances holds data segments bytes of the module. 89 // This is only used by bulk memory operations. 90 // 91 // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/runtime.html#data-instances 92 DataInstances []DataInstance 93 94 // ElementInstances holds the element instance, and each holds the references to either functions 95 // or external objects (unimplemented). 96 ElementInstances []ElementInstance 97 98 // Sys is exposed for use in special imports such as WASI, assemblyscript 99 // and gojs. 100 // 101 // # Notes 102 // 103 // - This is a part of ModuleInstance so that scope and Close is coherent. 104 // - This is not exposed outside this repository (as a host function 105 // parameter) because we haven't thought through capabilities based 106 // security implications. 107 Sys *internalsys.Context 108 109 // Closed is used both to guard moduleEngine.CloseWithExitCode and to store the exit code. 110 // 111 // The update value is closedType + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed. 112 // 113 // Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations. 114 // See /RATIONALE.md 115 Closed atomic.Uint64 116 117 // CodeCloser is non-nil when the code should be closed after this module. 118 CodeCloser api.Closer 119 120 // s is the Store on which this module is instantiated. 121 s *Store 122 // prev and next hold the nodes in the linked list of ModuleInstance held by Store. 123 prev, next *ModuleInstance 124 // Source is a pointer to the Module from which this ModuleInstance derives. 125 Source *Module 126 127 // CloseNotifier is an experimental hook called once on close. 128 CloseNotifier close.Notifier 129 } 130 131 // DataInstance holds bytes corresponding to the data segment in a module. 132 // 133 // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/runtime.html#data-instances 134 DataInstance = []byte 135 136 // GlobalInstance represents a global instance in a store. 137 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-instances%E2%91%A0 138 GlobalInstance struct { 139 Type GlobalType 140 // Val holds a 64-bit representation of the actual value. 141 Val uint64 142 // ValHi is only used for vector type globals, and holds the higher bits of the vector. 143 ValHi uint64 144 } 145 146 // FunctionTypeID is a uniquely assigned integer for a function type. 147 // This is wazero specific runtime object and specific to a store, 148 // and used at runtime to do type-checks on indirect function calls. 149 FunctionTypeID uint32 150 ) 151 152 // The wazero specific limitations described at RATIONALE.md. 153 const maximumFunctionTypes = 1 << 27 154 155 // GetFunctionTypeID is used by emscripten. 156 func (m *ModuleInstance) GetFunctionTypeID(t *FunctionType) FunctionTypeID { 157 id, err := m.s.GetFunctionTypeID(t) 158 if err != nil { 159 // This is not recoverable in practice since the only error GetFunctionTypeID returns is 160 // when there's too many function types in the store. 161 panic(err) 162 } 163 return id 164 } 165 166 func (m *ModuleInstance) buildElementInstances(elements []ElementSegment) { 167 m.ElementInstances = make([][]Reference, len(elements)) 168 for i, elm := range elements { 169 if elm.Type == RefTypeFuncref && elm.Mode == ElementModePassive { 170 // Only passive elements can be access as element instances. 171 // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/syntax/modules.html#element-segments 172 inits := elm.Init 173 inst := make([]Reference, len(inits)) 174 m.ElementInstances[i] = inst 175 for j, idx := range inits { 176 if idx != ElementInitNullReference { 177 inst[j] = m.Engine.FunctionInstanceReference(idx) 178 } 179 } 180 } 181 } 182 } 183 184 func (m *ModuleInstance) applyElements(elems []ElementSegment) { 185 for elemI := range elems { 186 elem := &elems[elemI] 187 if !elem.IsActive() || 188 // Per https://github.com/WebAssembly/spec/issues/1427 init can be no-op. 189 len(elem.Init) == 0 { 190 continue 191 } 192 var offset uint32 193 if elem.OffsetExpr.Opcode == OpcodeGlobalGet { 194 // Ignore error as it's already validated. 195 globalIdx, _, _ := leb128.LoadUint32(elem.OffsetExpr.Data) 196 global := m.Globals[globalIdx] 197 offset = uint32(global.Val) 198 } else { 199 // Ignore error as it's already validated. 200 o, _, _ := leb128.LoadInt32(elem.OffsetExpr.Data) 201 offset = uint32(o) 202 } 203 204 table := m.Tables[elem.TableIndex] 205 references := table.References 206 if int(offset)+len(elem.Init) > len(references) { 207 // ErrElementOffsetOutOfBounds is the error raised when the active element offset exceeds the table length. 208 // Before CoreFeatureReferenceTypes, this was checked statically before instantiation, after the proposal, 209 // this must be raised as runtime error (as in assert_trap in spectest), not even an instantiation error. 210 // https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L264-L274 211 // 212 // In wazero, we ignore it since in any way, the instantiated module and engines are fine and can be used 213 // for function invocations. 214 return 215 } 216 217 if table.Type == RefTypeExternref { 218 for i := 0; i < len(elem.Init); i++ { 219 references[offset+uint32(i)] = Reference(0) 220 } 221 } else { 222 for i, init := range elem.Init { 223 if init == ElementInitNullReference { 224 continue 225 } 226 227 var ref Reference 228 if index, ok := unwrapElementInitGlobalReference(init); ok { 229 global := m.Globals[index] 230 ref = Reference(global.Val) 231 } else { 232 ref = m.Engine.FunctionInstanceReference(index) 233 } 234 references[offset+uint32(i)] = ref 235 } 236 } 237 } 238 } 239 240 // validateData ensures that data segments are valid in terms of memory boundary. 241 // Note: this is used only when bulk-memory/reference type feature is disabled. 242 func (m *ModuleInstance) validateData(data []DataSegment) (err error) { 243 for i := range data { 244 d := &data[i] 245 if !d.IsPassive() { 246 offset := int(executeConstExpressionI32(m.Globals, &d.OffsetExpression)) 247 ceil := offset + len(d.Init) 248 if offset < 0 || ceil > len(m.MemoryInstance.Buffer) { 249 return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i) 250 } 251 } 252 } 253 return 254 } 255 256 // applyData uses the given data segments and mutate the memory according to the initial contents on it 257 // and populate the `DataInstances`. This is called after all the validation phase passes and out of 258 // bounds memory access error here is not a validation error, but rather a runtime error. 259 func (m *ModuleInstance) applyData(data []DataSegment) error { 260 m.DataInstances = make([][]byte, len(data)) 261 for i := range data { 262 d := &data[i] 263 m.DataInstances[i] = d.Init 264 if !d.IsPassive() { 265 offset := executeConstExpressionI32(m.Globals, &d.OffsetExpression) 266 if offset < 0 || int(offset)+len(d.Init) > len(m.MemoryInstance.Buffer) { 267 return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i) 268 } 269 copy(m.MemoryInstance.Buffer[offset:], d.Init) 270 } 271 } 272 return nil 273 } 274 275 // GetExport returns an export of the given name and type or errs if not exported or the wrong type. 276 func (m *ModuleInstance) getExport(name string, et ExternType) (*Export, error) { 277 exp, ok := m.Exports[name] 278 if !ok { 279 return nil, fmt.Errorf("%q is not exported in module %q", name, m.ModuleName) 280 } 281 if exp.Type != et { 282 return nil, fmt.Errorf("export %q in module %q is a %s, not a %s", name, m.ModuleName, ExternTypeName(exp.Type), ExternTypeName(et)) 283 } 284 return exp, nil 285 } 286 287 func NewStore(enabledFeatures api.CoreFeatures, engine Engine) *Store { 288 return &Store{ 289 nameToModule: map[string]*ModuleInstance{}, 290 nameToModuleCap: nameToModuleShrinkThreshold, 291 EnabledFeatures: enabledFeatures, 292 Engine: engine, 293 typeIDs: map[string]FunctionTypeID{}, 294 functionMaxTypes: maximumFunctionTypes, 295 } 296 } 297 298 // Instantiate uses name instead of the Module.NameSection ModuleName as it allows instantiating the same module under 299 // different names safely and concurrently. 300 // 301 // * ctx: the default context used for function calls. 302 // * name: the name of the module. 303 // * sys: the system context, which will be closed (SysContext.Close) on ModuleInstance.Close. 304 // 305 // Note: Module.Validate must be called prior to instantiation. 306 func (s *Store) Instantiate( 307 ctx context.Context, 308 module *Module, 309 name string, 310 sys *internalsys.Context, 311 typeIDs []FunctionTypeID, 312 ) (*ModuleInstance, error) { 313 // Instantiate the module and add it to the store so that other modules can import it. 314 m, err := s.instantiate(ctx, module, name, sys, typeIDs) 315 if err != nil { 316 return nil, err 317 } 318 319 // Now that the instantiation is complete without error, add it. 320 if err = s.registerModule(m); err != nil { 321 _ = m.Close(ctx) 322 return nil, err 323 } 324 return m, nil 325 } 326 327 func (s *Store) instantiate( 328 ctx context.Context, 329 module *Module, 330 name string, 331 sysCtx *internalsys.Context, 332 typeIDs []FunctionTypeID, 333 ) (m *ModuleInstance, err error) { 334 m = &ModuleInstance{ModuleName: name, TypeIDs: typeIDs, Sys: sysCtx, s: s, Source: module} 335 336 m.Tables = make([]*TableInstance, int(module.ImportTableCount)+len(module.TableSection)) 337 m.Globals = make([]*GlobalInstance, int(module.ImportGlobalCount)+len(module.GlobalSection)) 338 m.Engine, err = s.Engine.NewModuleEngine(module, m) 339 if err != nil { 340 return nil, err 341 } 342 343 if err = m.resolveImports(module); err != nil { 344 return nil, err 345 } 346 347 err = m.buildTables(module, 348 // As of reference-types proposal, boundary check must be done after instantiation. 349 s.EnabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes)) 350 if err != nil { 351 return nil, err 352 } 353 354 m.buildGlobals(module, m.Engine.FunctionInstanceReference) 355 m.buildMemory(module) 356 m.Exports = module.Exports 357 358 // As of reference types proposal, data segment validation must happen after instantiation, 359 // and the side effect must persist even if there's out of bounds error after instantiation. 360 // https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L395-L405 361 if !s.EnabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes) { 362 if err = m.validateData(module.DataSection); err != nil { 363 return nil, err 364 } 365 } 366 367 // After engine creation, we can create the funcref element instances and initialize funcref type globals. 368 m.buildElementInstances(module.ElementSection) 369 370 // Now all the validation passes, we are safe to mutate memory instances (possibly imported ones). 371 if err = m.applyData(module.DataSection); err != nil { 372 return nil, err 373 } 374 375 m.applyElements(module.ElementSection) 376 377 m.Engine.DoneInstantiation() 378 379 // Execute the start function. 380 if module.StartSection != nil { 381 funcIdx := *module.StartSection 382 ce := m.Engine.NewFunction(funcIdx) 383 _, err = ce.Call(ctx) 384 if exitErr, ok := err.(*sys.ExitError); ok { // Don't wrap an exit error! 385 return nil, exitErr 386 } else if err != nil { 387 return nil, fmt.Errorf("start %s failed: %w", module.funcDesc(SectionIDFunction, funcIdx), err) 388 } 389 } 390 return 391 } 392 393 func (m *ModuleInstance) resolveImports(module *Module) (err error) { 394 for moduleName, imports := range module.ImportPerModule { 395 var importedModule *ModuleInstance 396 importedModule, err = m.s.module(moduleName) 397 if err != nil { 398 return err 399 } 400 401 for _, i := range imports { 402 var imported *Export 403 imported, err = importedModule.getExport(i.Name, i.Type) 404 if err != nil { 405 return 406 } 407 408 switch i.Type { 409 case ExternTypeFunc: 410 expectedType := &module.TypeSection[i.DescFunc] 411 src := importedModule.Source 412 actual := src.typeOfFunction(imported.Index) 413 if !actual.EqualsSignature(expectedType.Params, expectedType.Results) { 414 err = errorInvalidImport(i, fmt.Errorf("signature mismatch: %s != %s", expectedType, actual)) 415 return 416 } 417 418 m.Engine.ResolveImportedFunction(i.IndexPerType, imported.Index, importedModule.Engine) 419 case ExternTypeTable: 420 expected := i.DescTable 421 importedTable := importedModule.Tables[imported.Index] 422 if expected.Type != importedTable.Type { 423 err = errorInvalidImport(i, fmt.Errorf("table type mismatch: %s != %s", 424 RefTypeName(expected.Type), RefTypeName(importedTable.Type))) 425 return 426 } 427 428 if expected.Min > importedTable.Min { 429 err = errorMinSizeMismatch(i, expected.Min, importedTable.Min) 430 return 431 } 432 433 if expected.Max != nil { 434 expectedMax := *expected.Max 435 if importedTable.Max == nil { 436 err = errorNoMax(i, expectedMax) 437 return 438 } else if expectedMax < *importedTable.Max { 439 err = errorMaxSizeMismatch(i, expectedMax, *importedTable.Max) 440 return 441 } 442 } 443 m.Tables[i.IndexPerType] = importedTable 444 case ExternTypeMemory: 445 expected := i.DescMem 446 importedMemory := importedModule.MemoryInstance 447 448 if expected.Min > memoryBytesNumToPages(uint64(len(importedMemory.Buffer))) { 449 err = errorMinSizeMismatch(i, expected.Min, importedMemory.Min) 450 return 451 } 452 453 if expected.Max < importedMemory.Max { 454 err = errorMaxSizeMismatch(i, expected.Max, importedMemory.Max) 455 return 456 } 457 m.MemoryInstance = importedMemory 458 m.Engine.ResolveImportedMemory(importedModule.Engine) 459 case ExternTypeGlobal: 460 expected := i.DescGlobal 461 importedGlobal := importedModule.Globals[imported.Index] 462 463 if expected.Mutable != importedGlobal.Type.Mutable { 464 err = errorInvalidImport(i, fmt.Errorf("mutability mismatch: %t != %t", 465 expected.Mutable, importedGlobal.Type.Mutable)) 466 return 467 } 468 469 if expected.ValType != importedGlobal.Type.ValType { 470 err = errorInvalidImport(i, fmt.Errorf("value type mismatch: %s != %s", 471 ValueTypeName(expected.ValType), ValueTypeName(importedGlobal.Type.ValType))) 472 return 473 } 474 m.Globals[i.IndexPerType] = importedGlobal 475 } 476 } 477 } 478 return 479 } 480 481 func errorMinSizeMismatch(i *Import, expected, actual uint32) error { 482 return errorInvalidImport(i, fmt.Errorf("minimum size mismatch: %d > %d", expected, actual)) 483 } 484 485 func errorNoMax(i *Import, expected uint32) error { 486 return errorInvalidImport(i, fmt.Errorf("maximum size mismatch: %d, but actual has no max", expected)) 487 } 488 489 func errorMaxSizeMismatch(i *Import, expected, actual uint32) error { 490 return errorInvalidImport(i, fmt.Errorf("maximum size mismatch: %d < %d", expected, actual)) 491 } 492 493 func errorInvalidImport(i *Import, err error) error { 494 return fmt.Errorf("import %s[%s.%s]: %w", ExternTypeName(i.Type), i.Module, i.Name, err) 495 } 496 497 // executeConstExpressionI32 executes the ConstantExpression which returns ValueTypeI32. 498 // The validity of the expression is ensured when calling this function as this is only called 499 // during instantiation phrase, and the validation happens in compilation (validateConstExpression). 500 func executeConstExpressionI32(importedGlobals []*GlobalInstance, expr *ConstantExpression) (ret int32) { 501 switch expr.Opcode { 502 case OpcodeI32Const: 503 ret, _, _ = leb128.LoadInt32(expr.Data) 504 case OpcodeGlobalGet: 505 id, _, _ := leb128.LoadUint32(expr.Data) 506 g := importedGlobals[id] 507 ret = int32(g.Val) 508 } 509 return 510 } 511 512 // initialize initializes the value of this global instance given the const expr and imported globals. 513 // funcRefResolver is called to get the actual funcref (engine specific) from the OpcodeRefFunc const expr. 514 // 515 // Global initialization constant expression can only reference the imported globals. 516 // See the note on https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#constant-expressions%E2%91%A0 517 func (g *GlobalInstance) initialize(importedGlobals []*GlobalInstance, expr *ConstantExpression, funcRefResolver func(funcIndex Index) Reference) { 518 switch expr.Opcode { 519 case OpcodeI32Const: 520 // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md 521 v, _, _ := leb128.LoadInt32(expr.Data) 522 g.Val = uint64(uint32(v)) 523 case OpcodeI64Const: 524 // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md 525 v, _, _ := leb128.LoadInt64(expr.Data) 526 g.Val = uint64(v) 527 case OpcodeF32Const: 528 g.Val = uint64(binary.LittleEndian.Uint32(expr.Data)) 529 case OpcodeF64Const: 530 g.Val = binary.LittleEndian.Uint64(expr.Data) 531 case OpcodeGlobalGet: 532 id, _, _ := leb128.LoadUint32(expr.Data) 533 importedG := importedGlobals[id] 534 switch importedG.Type.ValType { 535 case ValueTypeI32: 536 g.Val = uint64(uint32(importedG.Val)) 537 case ValueTypeI64: 538 g.Val = importedG.Val 539 case ValueTypeF32: 540 g.Val = importedG.Val 541 case ValueTypeF64: 542 g.Val = importedG.Val 543 case ValueTypeV128: 544 g.Val, g.ValHi = importedG.Val, importedG.ValHi 545 case ValueTypeFuncref, ValueTypeExternref: 546 g.Val = importedG.Val 547 } 548 case OpcodeRefNull: 549 switch expr.Data[0] { 550 case ValueTypeExternref, ValueTypeFuncref: 551 g.Val = 0 // Reference types are opaque 64bit pointer at runtime. 552 } 553 case OpcodeRefFunc: 554 v, _, _ := leb128.LoadUint32(expr.Data) 555 g.Val = uint64(funcRefResolver(v)) 556 case OpcodeVecV128Const: 557 g.Val, g.ValHi = binary.LittleEndian.Uint64(expr.Data[0:8]), binary.LittleEndian.Uint64(expr.Data[8:16]) 558 } 559 } 560 561 // String implements api.Global. 562 func (g *GlobalInstance) String() string { 563 switch g.Type.ValType { 564 case ValueTypeI32, ValueTypeI64: 565 return fmt.Sprintf("global(%d)", g.Val) 566 case ValueTypeF32: 567 return fmt.Sprintf("global(%f)", api.DecodeF32(g.Val)) 568 case ValueTypeF64: 569 return fmt.Sprintf("global(%f)", api.DecodeF64(g.Val)) 570 default: 571 panic(fmt.Errorf("BUG: unknown value type %X", g.Type.ValType)) 572 } 573 } 574 575 func (s *Store) GetFunctionTypeIDs(ts []FunctionType) ([]FunctionTypeID, error) { 576 ret := make([]FunctionTypeID, len(ts)) 577 for i := range ts { 578 t := &ts[i] 579 inst, err := s.GetFunctionTypeID(t) 580 if err != nil { 581 return nil, err 582 } 583 ret[i] = inst 584 } 585 return ret, nil 586 } 587 588 func (s *Store) GetFunctionTypeID(t *FunctionType) (FunctionTypeID, error) { 589 s.mux.RLock() 590 key := t.key() 591 id, ok := s.typeIDs[key] 592 s.mux.RUnlock() 593 if !ok { 594 s.mux.Lock() 595 defer s.mux.Unlock() 596 // Check again in case another goroutine has already added the type. 597 if id, ok = s.typeIDs[key]; ok { 598 return id, nil 599 } 600 l := len(s.typeIDs) 601 if uint32(l) >= s.functionMaxTypes { 602 return 0, fmt.Errorf("too many function types in a store") 603 } 604 id = FunctionTypeID(l) 605 s.typeIDs[key] = id 606 } 607 return id, nil 608 } 609 610 // CloseWithExitCode implements the same method as documented on wazero.Runtime. 611 func (s *Store) CloseWithExitCode(ctx context.Context, exitCode uint32) (err error) { 612 s.mux.Lock() 613 defer s.mux.Unlock() 614 // Close modules in reverse initialization order. 615 for m := s.moduleList; m != nil; m = m.next { 616 // If closing this module errs, proceed anyway to close the others. 617 if e := m.closeWithExitCode(ctx, exitCode); e != nil && err == nil { 618 // TODO: use multiple errors handling in Go 1.20. 619 err = e // first error 620 } 621 } 622 s.moduleList = nil 623 s.nameToModule = nil 624 s.nameToModuleCap = 0 625 s.typeIDs = nil 626 return 627 }