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 }