github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/wasm/table.go (about)

     1  package wasm
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"sync"
     7  
     8  	"github.com/tetratelabs/wazero/api"
     9  	"github.com/tetratelabs/wazero/internal/leb128"
    10  )
    11  
    12  // Table describes the limits of elements and its type in a table.
    13  type Table struct {
    14  	Min  uint32
    15  	Max  *uint32
    16  	Type RefType
    17  }
    18  
    19  // RefType is either RefTypeFuncref or RefTypeExternref as of WebAssembly core 2.0.
    20  type RefType = byte
    21  
    22  const (
    23  	// RefTypeFuncref represents a reference to a function.
    24  	RefTypeFuncref = ValueTypeFuncref
    25  	// RefTypeExternref represents a reference to a host object, which is not currently supported in wazero.
    26  	RefTypeExternref = ValueTypeExternref
    27  )
    28  
    29  func RefTypeName(t RefType) (ret string) {
    30  	switch t {
    31  	case RefTypeFuncref:
    32  		ret = "funcref"
    33  	case RefTypeExternref:
    34  		ret = "externref"
    35  	default:
    36  		ret = fmt.Sprintf("unknown(0x%x)", t)
    37  	}
    38  	return
    39  }
    40  
    41  // ElementMode represents a mode of element segment which is either active, passive or declarative.
    42  //
    43  // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/syntax/modules.html#element-segments
    44  type ElementMode = byte
    45  
    46  const (
    47  	// ElementModeActive is the mode which requires the runtime to initialize table with the contents in .Init field combined with OffsetExpr.
    48  	ElementModeActive ElementMode = iota
    49  	// ElementModePassive is the mode which doesn't require the runtime to initialize table, and only used with OpcodeTableInitName.
    50  	ElementModePassive
    51  	// ElementModeDeclarative is introduced in reference-types proposal which can be used to declare function indexes used by OpcodeRefFunc.
    52  	ElementModeDeclarative
    53  )
    54  
    55  // ElementSegment are initialization instructions for a TableInstance
    56  //
    57  // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-elem
    58  type ElementSegment struct {
    59  	// OffsetExpr returns the table element offset to apply to Init indices.
    60  	// Note: This can be validated prior to instantiation unless it includes OpcodeGlobalGet (an imported global).
    61  	OffsetExpr ConstantExpression
    62  
    63  	// TableIndex is the table's index to which this element segment is applied.
    64  	// Note: This is used if and only if the Mode is active.
    65  	TableIndex Index
    66  
    67  	// Followings are set/used regardless of the Mode.
    68  
    69  	// Init indices are (nullable) table elements where each index is the function index by which the module initialize the table.
    70  	Init []Index
    71  
    72  	// Type holds the type of this element segment, which is the RefType in WebAssembly 2.0.
    73  	Type RefType
    74  
    75  	// Mode is the mode of this element segment.
    76  	Mode ElementMode
    77  }
    78  
    79  const (
    80  	// ElementInitNullReference represents the null reference in ElementSegment's Init.
    81  	// In Wasm spec, an init item represents either Function's Index or null reference,
    82  	// and in wazero, we limit the maximum number of functions available in a module to
    83  	// MaximumFunctionIndex. Therefore, it is safe to use 1 << 31 to represent the null
    84  	// reference in Element segments.
    85  	ElementInitNullReference Index = 1 << 31
    86  	// elementInitImportedGlobalReferenceType represents an init item which is resolved via an imported global constexpr.
    87  	// The actual function reference stored at Global is only known at instantiation-time, so we set this flag
    88  	// to items of ElementSegment.Init at binary decoding, and unwrap this flag at instantiation to resolve the value.
    89  	//
    90  	// This might collide the init element resolved via ref.func instruction which is resolved with the func index at decoding,
    91  	// but in practice, that is not allowed in wazero thanks to our limit MaximumFunctionIndex. Thus, it is safe to set this flag
    92  	// in init element to indicate as such.
    93  	elementInitImportedGlobalReferenceType Index = 1 << 30
    94  )
    95  
    96  // unwrapElementInitGlobalReference takes an item of the init vector of an ElementSegment,
    97  // and returns the Global index if it is supposed to get generated from a global.
    98  // ok is true if the given init item is as such.
    99  func unwrapElementInitGlobalReference(init Index) (_ Index, ok bool) {
   100  	if init&elementInitImportedGlobalReferenceType == elementInitImportedGlobalReferenceType {
   101  		return init &^ elementInitImportedGlobalReferenceType, true
   102  	}
   103  	return init, false
   104  }
   105  
   106  // WrapGlobalIndexAsElementInit wraps the given index as an init item which is resolved via an imported global value.
   107  // See the comments on elementInitImportedGlobalReferenceType for more details.
   108  func WrapGlobalIndexAsElementInit(init Index) Index {
   109  	return init | elementInitImportedGlobalReferenceType
   110  }
   111  
   112  // IsActive returns true if the element segment is "active" mode which requires the runtime to initialize table
   113  // with the contents in .Init field.
   114  func (e *ElementSegment) IsActive() bool {
   115  	return e.Mode == ElementModeActive
   116  }
   117  
   118  // TableInstance represents a table of (RefTypeFuncref) elements in a module.
   119  //
   120  // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-instances%E2%91%A0
   121  type TableInstance struct {
   122  	// References holds references whose type is either RefTypeFuncref or RefTypeExternref (unsupported).
   123  	//
   124  	// Currently, only function references are supported.
   125  	References []Reference
   126  
   127  	// Min is the minimum (function) elements in this table and cannot grow to accommodate ElementSegment.
   128  	Min uint32
   129  
   130  	// Max if present is the maximum (function) elements in this table, or nil if unbounded.
   131  	Max *uint32
   132  
   133  	// Type is either RefTypeFuncref or RefTypeExternRef.
   134  	Type RefType
   135  
   136  	// The following is only used when the table is exported.
   137  
   138  	// involvingModuleInstances is a set of module instances which are involved in the table instance.
   139  	// This is critical for safety purpose because once a table is imported, it can hold any reference to
   140  	// any function in the owner and importing module instances. Therefore, these module instance,
   141  	// transitively the compiled modules, must be alive as long as the table instance is alive.
   142  	involvingModuleInstances []*ModuleInstance
   143  	// involvingModuleInstancesMutex is a mutex to protect involvingModuleInstances.
   144  	involvingModuleInstancesMutex sync.RWMutex
   145  }
   146  
   147  // ElementInstance represents an element instance in a module.
   148  //
   149  // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/runtime.html#element-instances
   150  type ElementInstance = []Reference
   151  
   152  // Reference is the runtime representation of RefType which is either RefTypeFuncref or RefTypeExternref.
   153  type Reference = uintptr
   154  
   155  // validateTable ensures any ElementSegment is valid. This caches results via Module.validatedActiveElementSegments.
   156  // Note: limitsType are validated by decoders, so not re-validated here.
   157  func (m *Module) validateTable(enabledFeatures api.CoreFeatures, tables []Table, maximumTableIndex uint32) error {
   158  	if len(tables) > int(maximumTableIndex) {
   159  		return fmt.Errorf("too many tables in a module: %d given with limit %d", len(tables), maximumTableIndex)
   160  	}
   161  
   162  	importedTableCount := m.ImportTableCount
   163  
   164  	// Create bounds checks as these can err prior to instantiation
   165  	funcCount := m.ImportFunctionCount + m.SectionElementCount(SectionIDFunction)
   166  	globalsCount := m.ImportGlobalCount + m.SectionElementCount(SectionIDGlobal)
   167  
   168  	// Now, we have to figure out which table elements can be resolved before instantiation and also fail early if there
   169  	// are any imported globals that are known to be invalid by their declarations.
   170  	for i := range m.ElementSection {
   171  		elem := &m.ElementSection[i]
   172  		idx := Index(i)
   173  		initCount := uint32(len(elem.Init))
   174  
   175  		// Any offset applied is to the element, not the function index: validate here if the funcidx is sound.
   176  		for ei, init := range elem.Init {
   177  			if init == ElementInitNullReference {
   178  				continue
   179  			}
   180  			index, ok := unwrapElementInitGlobalReference(init)
   181  			if ok {
   182  				if index >= globalsCount {
   183  					return fmt.Errorf("%s[%d].init[%d] global index %d out of range", SectionIDName(SectionIDElement), idx, ei, index)
   184  				}
   185  			} else {
   186  				if elem.Type == RefTypeExternref {
   187  					return fmt.Errorf("%s[%d].init[%d] must be ref.null but was %d", SectionIDName(SectionIDElement), idx, ei, init)
   188  				}
   189  				if index >= funcCount {
   190  					return fmt.Errorf("%s[%d].init[%d] func index %d out of range", SectionIDName(SectionIDElement), idx, ei, index)
   191  				}
   192  			}
   193  		}
   194  
   195  		if elem.IsActive() {
   196  			if len(tables) <= int(elem.TableIndex) {
   197  				return fmt.Errorf("unknown table %d as active element target", elem.TableIndex)
   198  			}
   199  
   200  			t := tables[elem.TableIndex]
   201  			if t.Type != elem.Type {
   202  				return fmt.Errorf("element type mismatch: table has %s but element has %s",
   203  					RefTypeName(t.Type), RefTypeName(elem.Type),
   204  				)
   205  			}
   206  
   207  			// global.get needs to be discovered during initialization
   208  			oc := elem.OffsetExpr.Opcode
   209  			if oc == OpcodeGlobalGet {
   210  				globalIdx, _, err := leb128.LoadUint32(elem.OffsetExpr.Data)
   211  				if err != nil {
   212  					return fmt.Errorf("%s[%d] couldn't read global.get parameter: %w", SectionIDName(SectionIDElement), idx, err)
   213  				} else if err = m.verifyImportGlobalI32(SectionIDElement, idx, globalIdx); err != nil {
   214  					return err
   215  				}
   216  			} else if oc == OpcodeI32Const {
   217  				// Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L117 we must pass if imported
   218  				// table has set its min=0. Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L142, we
   219  				// have to do fail if module-defined min=0.
   220  				if !enabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes) && elem.TableIndex >= importedTableCount {
   221  					// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
   222  					o, _, err := leb128.LoadInt32(elem.OffsetExpr.Data)
   223  					if err != nil {
   224  						return fmt.Errorf("%s[%d] couldn't read i32.const parameter: %w", SectionIDName(SectionIDElement), idx, err)
   225  					}
   226  					offset := Index(o)
   227  					if err = checkSegmentBounds(t.Min, uint64(initCount)+uint64(offset), idx); err != nil {
   228  						return err
   229  					}
   230  				}
   231  			} else {
   232  				return fmt.Errorf("%s[%d] has an invalid const expression: %s", SectionIDName(SectionIDElement), idx, InstructionName(oc))
   233  			}
   234  		}
   235  	}
   236  	return nil
   237  }
   238  
   239  // buildTable returns TableInstances if the module defines or imports a table.
   240  //   - importedTables: returned as `tables` unmodified.
   241  //   - importedGlobals: include all instantiated, imported globals.
   242  //
   243  // If the result `init` is non-nil, it is the `tableInit` parameter of Engine.NewModuleEngine.
   244  //
   245  // Note: An error is only possible when an ElementSegment.OffsetExpr is out of range of the TableInstance.Min.
   246  func (m *ModuleInstance) buildTables(module *Module, skipBoundCheck bool) (err error) {
   247  	idx := module.ImportTableCount
   248  	for i := range module.TableSection {
   249  		tsec := &module.TableSection[i]
   250  		// The module defining the table is the one that sets its Min/Max etc.
   251  		m.Tables[idx] = &TableInstance{
   252  			References: make([]Reference, tsec.Min), Min: tsec.Min, Max: tsec.Max,
   253  			Type: tsec.Type,
   254  		}
   255  		idx++
   256  	}
   257  
   258  	if !skipBoundCheck {
   259  		for elemI := range module.ElementSection { // Do not loop over the value since elementSegments is a slice of value.
   260  			elem := &module.ElementSection[elemI]
   261  			table := m.Tables[elem.TableIndex]
   262  			var offset uint32
   263  			if elem.OffsetExpr.Opcode == OpcodeGlobalGet {
   264  				// Ignore error as it's already validated.
   265  				globalIdx, _, _ := leb128.LoadUint32(elem.OffsetExpr.Data)
   266  				global := m.Globals[globalIdx]
   267  				offset = uint32(global.Val)
   268  			} else { // i32.const
   269  				// Ignore error as it's already validated.
   270  				o, _, _ := leb128.LoadInt32(elem.OffsetExpr.Data)
   271  				offset = uint32(o)
   272  			}
   273  
   274  			// Check to see if we are out-of-bounds
   275  			initCount := uint64(len(elem.Init))
   276  			if err = checkSegmentBounds(table.Min, uint64(offset)+initCount, Index(elemI)); err != nil {
   277  				return
   278  			}
   279  		}
   280  	}
   281  	return
   282  }
   283  
   284  // checkSegmentBounds fails if the capacity needed for an ElementSegment.Init is larger than limitsType.Min
   285  //
   286  // WebAssembly 1.0 (20191205) doesn't forbid growing to accommodate element segments, and spectests are inconsistent.
   287  // For example, the spectests enforce elements within Table limitsType.Min, but ignore Import.DescTable min. What this
   288  // means is we have to delay offset checks on imported tables until we link to them.
   289  // e.g. https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L117 wants pass on min=0 for import
   290  // e.g. https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L142 wants fail on min=0 module-defined
   291  func checkSegmentBounds(min uint32, requireMin uint64, idx Index) error { // uint64 in case offset was set to -1
   292  	if requireMin > uint64(min) {
   293  		return fmt.Errorf("%s[%d].init exceeds min table size", SectionIDName(SectionIDElement), idx)
   294  	}
   295  	return nil
   296  }
   297  
   298  func (m *Module) verifyImportGlobalI32(sectionID SectionID, sectionIdx Index, idx uint32) error {
   299  	ig := uint32(math.MaxUint32) // +1 == 0
   300  	for i := range m.ImportSection {
   301  		imp := &m.ImportSection[i]
   302  		if imp.Type == ExternTypeGlobal {
   303  			ig++
   304  			if ig == idx {
   305  				if imp.DescGlobal.ValType != ValueTypeI32 {
   306  					return fmt.Errorf("%s[%d] (global.get %d): import[%d].global.ValType != i32", SectionIDName(sectionID), sectionIdx, idx, i)
   307  				}
   308  				return nil
   309  			}
   310  		}
   311  	}
   312  	return fmt.Errorf("%s[%d] (global.get %d): out of range of imported globals", SectionIDName(sectionID), sectionIdx, idx)
   313  }
   314  
   315  // Grow appends the `initialRef` by `delta` times into the References slice.
   316  // Returns -1 if the operation is not valid, otherwise the old length of the table.
   317  //
   318  // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/instructions.html#xref-syntax-instructions-syntax-instr-table-mathsf-table-grow-x
   319  func (t *TableInstance) Grow(delta uint32, initialRef Reference) (currentLen uint32) {
   320  	currentLen = uint32(len(t.References))
   321  	if delta == 0 {
   322  		return
   323  	}
   324  
   325  	if newLen := int64(currentLen) + int64(delta); // adding as 64bit ints to avoid overflow.
   326  	newLen >= math.MaxUint32 || (t.Max != nil && newLen > int64(*t.Max)) {
   327  		return 0xffffffff // = -1 in signed 32-bit integer.
   328  	}
   329  	t.References = append(t.References, make([]uintptr, delta)...)
   330  
   331  	// Uses the copy trick for faster filling the new region with the initial value.
   332  	// https://gist.github.com/taylorza/df2f89d5f9ab3ffd06865062a4cf015d
   333  	newRegion := t.References[currentLen:]
   334  	newRegion[0] = initialRef
   335  	for i := 1; i < len(newRegion); i *= 2 {
   336  		copy(newRegion[i:], newRegion[:i])
   337  	}
   338  	return
   339  }