wa-lang.org/wazero@v1.0.2/internal/wasm/table.go (about)

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