Ensures no race condition adding a module of the same name (#344)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-03-08 08:21:09 +08:00
committed by GitHub
parent 2846939df2
commit e7afd75e25
2 changed files with 70 additions and 50 deletions

View File

@@ -94,10 +94,6 @@ func (f *exportedFunction) Call(ctx context.Context, params ...uint64) ([]uint64
//
// TOOD(adrian): make this goroutine-safe like store.Instantiate.
func (s *Store) NewHostModule(moduleName string, nameToGoFunc map[string]interface{}) (*HostModule, error) {
if err := s.requireModuleUnused(moduleName); err != nil {
return nil, err
}
exportCount := len(nameToGoFunc)
ret := &HostModule{name: moduleName, NameToFunction: make(map[string]*FunctionInstance, exportCount)}
hostModule := &ModuleInstance{
@@ -106,9 +102,14 @@ func (s *Store) NewHostModule(moduleName string, nameToGoFunc map[string]interfa
hostModule: ret,
}
if err := s.requireModuleName(moduleName); err != nil {
return nil, err
}
for name, goFunc := range nameToGoFunc {
hf, err := newGoFunc(goFunc)
if err != nil {
s.deleteModule(moduleName)
return nil, fmt.Errorf("func[%s] %w", name, err)
}
@@ -124,11 +125,13 @@ func (s *Store) NewHostModule(moduleName string, nameToGoFunc map[string]interfa
ret.NameToFunction[name] = f
if err = s.compileHostFunction(f); err != nil {
s.deleteModule(moduleName)
return nil, err
}
}
s.modules[moduleName] = hostModule
// Now that the instantiation is complete without error, add it. This makes it visible for import.
s.addModule(hostModule)
return ret, nil
}
@@ -149,15 +152,6 @@ func (s *Store) compileHostFunction(f *FunctionInstance) (err error) {
return nil
}
func (s *Store) requireModuleUnused(moduleName string) error {
s.mux.RLock()
defer s.mux.RUnlock()
if _, ok := s.modules[moduleName]; ok {
return fmt.Errorf("module %s has already been instantiated", moduleName)
}
return nil
}
// HostModule implements wasm.HostModule
type HostModule struct {
// name is for String and Store.ReleaseModule

View File

@@ -37,8 +37,11 @@ type (
// EnabledFeatures are read-only to allow optimizations.
EnabledFeatures Features
// moduleNames ensures no race conditions instantiating two modules of the same name
moduleNames map[string]struct{} // guarded by mux
// modules holds the instantiated Wasm modules by module name from Instantiate.
modules map[string]*ModuleInstance
modules map[string]*ModuleInstance // guarded by mux
// typeIDs maps each FunctionType.String() to a unique FunctionTypeID. This is used at runtime to
// do type-checks on indirect function calls.
@@ -236,41 +239,41 @@ const (
maximumFunctionTypes = 1 << 27
)
// newModuleInstance bundles all the instances for a module and creates a new module instance.
func newModuleInstance(name string, module *Module, importedFunctions, functions []*FunctionInstance,
// addSections adds section elements to the ModuleInstance
func (m *ModuleInstance) addSections(module *Module, importedFunctions, functions []*FunctionInstance,
importedGlobals, globals []*GlobalInstance, importedTable, table *TableInstance,
memory, importedMemory *MemoryInstance, typeInstances []*TypeInstance, moduleImports map[*ModuleInstance]struct{}) *ModuleInstance {
memory, importedMemory *MemoryInstance, typeInstances []*TypeInstance, moduleImports map[*ModuleInstance]struct{}) {
instance := &ModuleInstance{Name: name, Types: typeInstances, dependencies: moduleImports}
m.Types = typeInstances
m.dependencies = moduleImports
instance.Functions = append(instance.Functions, importedFunctions...)
m.Functions = append(m.Functions, importedFunctions...)
for i, f := range functions {
// Associate each function with the type instance and the module instance's pointer.
f.Module = instance
f.Module = m
f.TypeID = typeInstances[module.FunctionSection[i]].TypeID
instance.Functions = append(instance.Functions, f)
m.Functions = append(m.Functions, f)
}
instance.Globals = append(instance.Globals, importedGlobals...)
instance.Globals = append(instance.Globals, globals...)
m.Globals = append(m.Globals, importedGlobals...)
m.Globals = append(m.Globals, globals...)
if importedTable != nil {
instance.Table = importedTable
m.Table = importedTable
} else {
instance.Table = table
m.Table = table
}
if importedMemory != nil {
instance.Memory = importedMemory
m.Memory = importedMemory
} else {
instance.Memory = memory
m.Memory = memory
}
instance.buildExportInstances(module.ExportSection)
return instance
m.buildExports(module.ExportSection)
}
func (m *ModuleInstance) buildExportInstances(exports map[string]*Export) {
func (m *ModuleInstance) buildExports(exports map[string]*Export) {
m.Exports = make(map[string]*ExportInstance, len(exports))
for _, exp := range exports {
index := exp.Index
@@ -365,6 +368,7 @@ func NewStore(ctx context.Context, engine Engine, enabledFeatures Features) *Sto
ctx: ctx,
engine: engine,
EnabledFeatures: enabledFeatures,
moduleNames: map[string]struct{}{},
modules: map[string]*ModuleInstance{},
typeIDs: map[string]FunctionTypeID{},
maximumFunctionIndex: maximumFunctionIndex,
@@ -383,20 +387,23 @@ func (s *Store) checkFunctionIndexOverflow(newInstanceNum int) error {
return nil
}
// Instantiate uses name instead of the Module.NameSection ModuleName as it allows instantiating the same module under
// different names safely and concurrently.
func (s *Store) Instantiate(module *Module, name string) (*PublicModule, error) {
// Note: we do not take lock here in order to enable concurrent instantiation and compilation
// of multiuple modules. When necessary, we take read or write locks in each method of store used here.
if err := s.requireModuleUnused(name); err != nil {
if err := s.requireModuleName(name); err != nil {
return nil, err
}
// Note: we do not take lock here in order to enable concurrent instantiation and compilation
// of multiple modules. When necessary, we take read or write locks in each method of store used here.
if err := s.checkFunctionIndexOverflow(len(module.FunctionSection)); err != nil {
s.deleteModule(name)
return nil, err
}
types, err := s.getTypes(module.TypeSection)
if err != nil {
s.deleteModule(name)
return nil, err
}
@@ -410,6 +417,7 @@ func (s *Store) Instantiate(module *Module, name string) (*PublicModule, error)
}
}()
if err != nil {
s.deleteModule(name)
return nil, err
}
@@ -417,14 +425,17 @@ func (s *Store) Instantiate(module *Module, name string) (*PublicModule, error)
module.buildFunctions(), module.buildGlobals(importedGlobals), module.buildTable(), module.buildMemory()
// Now we have all instances from imports and local ones, so ready to create a new ModuleInstance.
instance := newModuleInstance(name, module, importedFunctions, functions, importedGlobals,
instance := &ModuleInstance{Name: name}
instance.addSections(module, importedFunctions, functions, importedGlobals,
globals, importedTable, table, importedMemory, memory, types, moduleImports)
if err = instance.validateElements(module.ElementSection); err != nil {
s.deleteModule(name)
return nil, err
}
if err := instance.validateData(module.DataSection); err != nil {
if err = instance.validateData(module.DataSection); err != nil {
s.deleteModule(name)
return nil, err
}
@@ -432,11 +443,10 @@ func (s *Store) Instantiate(module *Module, name string) (*PublicModule, error)
s.addFunctions(functions...) // Need to assign funcaddr to each instance before compilation.
for i, f := range functions {
// TODO: maybe better consider spawning multiple goroutines for compilations to accelerate.
if err := s.engine.Compile(f); err != nil {
if err = s.engine.Compile(f); err != nil {
s.deleteModule(name)
// On the failure, release the assigned funcaddr and already compiled functions.
if err := s.releaseFunctions(functions[:i]...); err != nil {
return nil, err
}
_ = s.releaseFunctions(functions[:i]...) // ignore any release error so we can report the original one.
idx := module.SectionElementCount(SectionIDFunction) - 1
return nil, fmt.Errorf("compilation failed at index %d/%d: %w", i, idx, err)
}
@@ -446,8 +456,8 @@ func (s *Store) Instantiate(module *Module, name string) (*PublicModule, error)
instance.applyElements(module.ElementSection)
instance.applyData(module.DataSection)
// Persist the module instance.
s.addModule(instance)
// Build the default context for calls to this module.
instance.Ctx = NewModuleContext(s.ctx, s.engine, instance)
// Plus, we can finalize the module import reference count.
moduleImportsFinalized = true
@@ -455,10 +465,14 @@ func (s *Store) Instantiate(module *Module, name string) (*PublicModule, error)
// Execute the start function.
if module.StartSection != nil {
funcIdx := *module.StartSection
if _, err := s.engine.Call(instance.Ctx, instance.Functions[funcIdx]); err != nil {
if _, err = s.engine.Call(instance.Ctx, instance.Functions[funcIdx]); err != nil {
s.deleteModule(name)
return nil, fmt.Errorf("module[%s] start function failed: %w", name, err)
}
}
// Now that the instantiation is complete without error, add it. This makes it visible for import.
s.addModule(instance)
return &PublicModule{s: s, instance: instance}, nil
}
@@ -487,7 +501,7 @@ func (s *Store) ReleaseModule(moduleName string) error {
return fmt.Errorf("unable to release function instance: %w", err)
}
s.deleteModule(m)
s.deleteModule(moduleName)
return nil
}
@@ -541,16 +555,28 @@ func (s *Store) addFunctions(fs ...*FunctionInstance) {
}
}
func (s *Store) deleteModule(m *ModuleInstance) {
// deleteModule makes the moduleName available for instantiation again.
func (s *Store) deleteModule(moduleName string) {
s.mux.Lock()
defer s.mux.Unlock()
delete(s.modules, m.Name)
delete(s.modules, moduleName)
delete(s.moduleNames, moduleName)
}
func (s *Store) addModule(m *ModuleInstance) {
// Build the default context for calls to this module.
m.Ctx = NewModuleContext(s.ctx, s.engine, m)
// requireModuleName is a pre-flight check to reserve a module.
// This must be reverted on error with deleteModule if initialization fails.
func (s *Store) requireModuleName(moduleName string) error {
s.mux.Lock()
defer s.mux.Unlock()
if _, ok := s.moduleNames[moduleName]; ok {
return fmt.Errorf("module %s has already been instantiated", moduleName)
}
s.moduleNames[moduleName] = struct{}{}
return nil
}
// addModule makes the module visible for import
func (s *Store) addModule(m *ModuleInstance) {
s.mux.Lock()
defer s.mux.Unlock()
s.modules[m.Name] = m