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