wasm/execution/interpreter_loop/
table.rs

1use core::{num::NonZeroU64, ops::ControlFlow};
2
3use crate::{
4    assert_validated::UnwrapValidatedExt,
5    core::{
6        indices::{ElemIdx, TableIdx},
7        reader::types::opcode,
8        utils::ToUsizeExt,
9    },
10    execution::interpreter_loop::{
11        define_instruction_fn, elem_drop, table_init, Args, InterpreterLoopOutcome,
12    },
13    value::Ref,
14    TrapError, Value,
15};
16
17define_instruction_fn! {
18    table_get,
19    fuel_check = flat(opcode::TABLE_GET),
20    |Args {
21         store_inner,
22         modules,
23         resumable,
24         wasm,
25         current_module,
26         ..
27     }| {
28        // SAFETY: Validation guarantees there to be a valid table index
29        // next.
30        let table_idx = unsafe { TableIdx::read_unchecked(wasm) };
31        // SAFETY: The current module address must come from the current
32        // store, because it is the only parameter to this function that
33        // can contain module addresses. All stores guarantee all
34        // addresses in them to be valid within themselves.
35        let module = unsafe { modules.get(*current_module) };
36
37        // SAFETY: Validation guarantees the table index to be valid in
38        // the current module.
39        let table_addr = *unsafe { module.table_addrs.get(table_idx) };
40        // SAFETY: This table address was just read from the current
41        // store. Therefore, it is valid in the current store.
42        let tab = unsafe { store_inner.tables.get(table_addr) };
43
44        let i: i32 = resumable.stack.pop_value().try_into().unwrap_validated();
45
46        let val = tab
47            .elem
48            .get(i.cast_unsigned().into_usize())
49            .ok_or(TrapError::TableOrElementAccessOutOfBounds)?;
50
51        resumable.stack.push_value((*val).into())?;
52        trace!(
53            "Instruction: table.get '{}' [{}] -> [{}]",
54            table_idx,
55            i,
56            val
57        );
58        Ok(ControlFlow::Continue(()))
59    }
60}
61
62define_instruction_fn! {
63    table_set,
64    fuel_check = flat(opcode::TABLE_SET),
65    |Args {
66         store_inner,
67         modules,
68         resumable,
69         wasm,
70         current_module,
71         ..
72     }| {
73        // SAFETY: Validation guarantees there to be valid table index
74        // next.
75        let table_idx = unsafe { TableIdx::read_unchecked(wasm) };
76        // SAFETY: The current module address must come from the current
77        // store, because it is the only parameter to this function that
78        // can contain module addresses. All stores guarantee all
79        // addresses in them to be valid within themselves.
80        let module = unsafe { modules.get(*current_module) };
81
82        // SAFETY: Validation guarantees the table index to be valid in
83        // the current module.
84        let table_addr = *unsafe { module.table_addrs.get(table_idx) };
85        // SAFETY: This table address was just read from the current
86        // store. Therefore, it is valid in the current store.
87        let tab = unsafe { store_inner.tables.get_mut(table_addr) };
88
89        let val: Ref = resumable.stack.pop_value().try_into().unwrap_validated();
90        let i: i32 = resumable.stack.pop_value().try_into().unwrap_validated();
91
92        tab.elem
93            .get_mut(i.cast_unsigned().into_usize())
94            .ok_or(TrapError::TableOrElementAccessOutOfBounds)
95            .map(|r| *r = val)?;
96        trace!(
97            "Instruction: table.set '{}' [{} {}] -> []",
98            table_idx,
99            i,
100            val
101        );
102        Ok(ControlFlow::Continue(()))
103    }
104}
105
106define_instruction_fn! {
107    table_size,
108    fuel_check = flat_fc(opcode::fc_extensions::TABLE_SIZE),
109    |Args {
110         wasm,
111         resumable,
112         modules,
113         current_module,
114         store_inner,
115         ..
116     }| {
117        // SAFETY: Validation guarantees there to be valid table
118        // index next.
119        let table_idx = unsafe { TableIdx::read_unchecked(wasm) };
120
121        // SAFETY: The current module address must come from the current
122        // store, because it is the only parameter to this function that
123        // can contain module addresses. All stores guarantee all
124        // addresses in them to be valid within themselves.
125        let module = unsafe { modules.get(*current_module) };
126
127        // SAFETY: Validation guarantees the table index to be
128        // valid in the current module.
129        let table_addr = *unsafe { module.table_addrs.get(table_idx) };
130        // SAFETY: This table address was just read from the
131        // current store. Therefore, it is valid in the current
132        // store.
133        let tab = unsafe { store_inner.tables.get_mut(table_addr) };
134
135        let sz = tab.elem.len() as u32;
136
137        resumable.stack.push_value(Value::I32(sz))?;
138
139        trace!("Instruction: table.size '{}' [] -> [{}]", table_idx, sz);
140        Ok(ControlFlow::Continue(()))
141    }
142}
143
144define_instruction_fn! {
145    table_grow,
146    fuel_check = omit,
147    |Args {
148         resumable,
149         wasm,
150         modules,
151         current_module,
152         store_inner,
153         ..
154     }| {
155        // SAFETY: Validation guarantees there to be a valid
156        // table index next.
157        let table_idx = unsafe { TableIdx::read_unchecked(wasm) };
158
159        // SAFETY: The current module address must come from the current
160        // store, because it is the only parameter to this function that
161        // can contain module addresses. All stores guarantee all
162        // addresses in them to be valid within themselves.
163        let module = unsafe { modules.get(*current_module) };
164
165        // SAFETY: Validation guarantees the table index to be
166        // valid in the current module.
167        let table_addr = *unsafe { module.table_addrs.get(table_idx) };
168        // SAFETY: This table address was just read from the
169        // current store. Therefore, it is valid in the current
170        // store.
171        let tab = unsafe { store_inner.tables.get_mut(table_addr) };
172
173        let sz = tab.elem.len() as u32;
174
175        let n: u32 = resumable.stack.pop_value().try_into().unwrap_validated();
176        let cost = T::get_fc_extension_flat_cost(opcode::fc_extensions::TABLE_GROW)
177            + u64::from(n)
178                * T::get_fc_extension_cost_per_element(opcode::fc_extensions::TABLE_GROW);
179        if let Some(fuel) = &mut resumable.maybe_fuel {
180            if *fuel >= cost {
181                *fuel -= cost;
182            } else {
183                resumable.stack.push_value(Value::I32(n)).unwrap_validated(); // we are pushing back what was just popped, this can't panic.
184                return Ok(ControlFlow::Break(InterpreterLoopOutcome::OutOfFuel {
185                    required_fuel: NonZeroU64::new(cost - *fuel).expect(
186                        "the last check guarantees that the current fuel is smaller than cost",
187                    ),
188                }));
189            }
190        }
191
192        let val: Ref = resumable.stack.pop_value().try_into().unwrap_validated();
193
194        // TODO this instruction is non-deterministic w.r.t. spec, and can fail if the embedder wills it.
195        // for now we execute it always according to the following match expr.
196        // if the grow operation fails, err := Value::I32(2^32-1) is pushed to the resumable.stack per spec
197        match tab.grow(n, val) {
198            Ok(_) => {
199                resumable.stack.push_value(Value::I32(sz))?;
200            }
201            Err(_) => {
202                resumable.stack.push_value(Value::I32(u32::MAX))?;
203            }
204        }
205        Ok(ControlFlow::Continue(()))
206    }
207}
208
209define_instruction_fn! {
210    table_fill,
211    fuel_check = omit,
212    |Args {
213         resumable,
214         wasm,
215         modules,
216         current_module,
217         store_inner,
218         ..
219     }| {
220        // SAFETY: Validation guarantees there to be a valid
221        // table index next.
222        let table_idx = unsafe { TableIdx::read_unchecked(wasm) };
223
224        // SAFETY: The current module address must come from the current
225        // store, because it is the only parameter to this function that
226        // can contain module addresses. All stores guarantee all
227        // addresses in them to be valid within themselves.
228        let module = unsafe { modules.get(*current_module) };
229
230        // SAFETY: Validation guarantees the table index to be
231        // valid in the current module.
232        let table_addr = *unsafe { module.table_addrs.get(table_idx) };
233        // SAFETY: This table address was just read from the
234        // current store. Therefore, it is valid in the current
235        // store.
236        let tab = unsafe { store_inner.tables.get_mut(table_addr) };
237
238        let len: u32 = resumable.stack.pop_value().try_into().unwrap_validated();
239        let cost = T::get_fc_extension_flat_cost(opcode::fc_extensions::TABLE_FILL)
240            + u64::from(len)
241                * T::get_fc_extension_cost_per_element(opcode::fc_extensions::TABLE_FILL);
242        if let Some(fuel) = &mut resumable.maybe_fuel {
243            if *fuel >= cost {
244                *fuel -= cost;
245            } else {
246                resumable
247                    .stack
248                    .push_value(Value::I32(len))
249                    .unwrap_validated(); // we are pushing back what was just popped, this can't panic.
250                return Ok(ControlFlow::Break(InterpreterLoopOutcome::OutOfFuel {
251                    required_fuel: NonZeroU64::new(cost - *fuel).expect(
252                        "the last check guarantees that the current fuel is smaller than cost",
253                    ),
254                }));
255            }
256        }
257
258        let val: Ref = resumable.stack.pop_value().try_into().unwrap_validated();
259        let dst: u32 = resumable.stack.pop_value().try_into().unwrap_validated();
260
261        let end = (dst.into_usize())
262            .checked_add(len.into_usize())
263            .ok_or(TrapError::TableOrElementAccessOutOfBounds)?;
264
265        tab.elem
266            .get_mut(dst.into_usize()..end)
267            .ok_or(TrapError::TableOrElementAccessOutOfBounds)?
268            .fill(val);
269
270        trace!(
271            "Instruction table.fill '{}' [{} {} {}] -> []",
272            table_idx,
273            dst,
274            val,
275            len
276        );
277        Ok(ControlFlow::Continue(()))
278    }
279}
280
281// https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-table-mathsf-table-copy-x-y
282define_instruction_fn! {
283    table_copy,
284    fuel_check = omit,
285    |Args {
286         resumable,
287         wasm,
288         modules,
289         current_module,
290         store_inner,
291         ..
292     }| {
293        // SAFETY: Validation guarantees there to be a valid
294        // table index next.
295        let table_x_idx = unsafe { TableIdx::read_unchecked(wasm) };
296        // SAFETY: Validation guarantees there to be a valid
297        // table index next.
298        let table_y_idx = unsafe { TableIdx::read_unchecked(wasm) };
299
300        // SAFETY: The current module address must come from the current
301        // store, because it is the only parameter to this function that
302        // can contain module addresses. All stores guarantee all
303        // addresses in them to be valid within themselves.
304        let module = unsafe { modules.get(*current_module) };
305
306        // SAFETY: Validation guarantees the table index to be
307        // valid in the current module.
308        let table_addr_x = *unsafe { module.table_addrs.get(table_x_idx) };
309        // SAFETY: Validation guarantees the table index to be
310        // valid in the current module.
311        let table_addr_y = *unsafe { module.table_addrs.get(table_y_idx) };
312
313        // SAFETY: This table address was just read from the
314        // current store. Therefore, it is valid in the current
315        // store.
316        let tab_x_elem_len = unsafe { store_inner.tables.get(table_addr_x) }.elem.len();
317        // SAFETY: This table address was just read from the
318        // current store. Therefore, it is valid in the current
319        // store.
320        let tab_y_elem_len = unsafe { store_inner.tables.get(table_addr_y) }.elem.len();
321
322        let n: u32 = resumable.stack.pop_value().try_into().unwrap_validated(); // size
323        let cost = T::get_fc_extension_flat_cost(opcode::fc_extensions::TABLE_COPY)
324            + u64::from(n)
325                * T::get_fc_extension_cost_per_element(opcode::fc_extensions::TABLE_COPY);
326        if let Some(fuel) = &mut resumable.maybe_fuel {
327            if *fuel >= cost {
328                *fuel -= cost;
329            } else {
330                resumable.stack.push_value(Value::I32(n)).unwrap_validated(); // we are pushing back what was just popped, this can't panic.
331                return Ok(ControlFlow::Break(InterpreterLoopOutcome::OutOfFuel {
332                    required_fuel: NonZeroU64::new(cost - *fuel).expect(
333                        "the last check guarantees that the current fuel is smaller than cost",
334                    ),
335                }));
336            }
337        }
338
339        let s: u32 = resumable.stack.pop_value().try_into().unwrap_validated(); // source
340        let d: u32 = resumable.stack.pop_value().try_into().unwrap_validated(); // destination
341
342        let src_res = match s.checked_add(n) {
343            Some(res) => {
344                if res > tab_y_elem_len as u32 {
345                    return Err(TrapError::TableOrElementAccessOutOfBounds.into());
346                } else {
347                    res.into_usize()
348                }
349            }
350            _ => return Err(TrapError::TableOrElementAccessOutOfBounds.into()),
351        };
352
353        let dst_res = match d.checked_add(n) {
354            Some(res) => {
355                if res > tab_x_elem_len as u32 {
356                    return Err(TrapError::TableOrElementAccessOutOfBounds.into());
357                } else {
358                    res.into_usize()
359                }
360            }
361            _ => return Err(TrapError::TableOrElementAccessOutOfBounds.into()),
362        };
363
364        if table_addr_x == table_addr_y {
365            // SAFETY: This table address was just read from the
366            // current store. Therefore, it is valid in the
367            // current store.
368            let table = unsafe { store_inner.tables.get_mut(table_addr_x) };
369
370            table.elem.copy_within(s as usize..src_res, d as usize);
371        } else {
372            let dst_addr = table_addr_x;
373            let src_addr = table_addr_y;
374
375            // SAFETY: These table addresses were just read from
376            // the current store. Therefore, they are valid in
377            // the current store.
378            let (src_table, dst_table) =
379                unsafe { store_inner.tables.get_two_mut(src_addr, dst_addr) }
380                    .expect("both addrs to never be equal");
381
382            dst_table.elem[d.into_usize()..dst_res]
383                .copy_from_slice(&src_table.elem[s.into_usize()..src_res]);
384        }
385
386        trace!(
387            "Instruction: table.copy '{}' '{}' [{} {} {}] -> []",
388            table_x_idx,
389            table_y_idx,
390            d,
391            s,
392            n
393        );
394        Ok(ControlFlow::Continue(()))
395    }
396}
397
398// https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-table-mathsf-table-init-x-y
399// https://webassembly.github.io/spec/core/binary/instructions.html#table-instructions
400// in binary format it seems that elemidx is first ???????
401// this is ONLY for passive elements
402define_instruction_fn! {
403    table_init_fn,
404    fuel_check = omit,
405    |Args {
406         resumable,
407         wasm,
408         store_inner,
409         modules,
410         current_module,
411         ..
412     }| {
413        // SAFETY: Validation guarantees there to be a valid
414        // element index next.
415        let elem_idx = unsafe { ElemIdx::read_unchecked(wasm) };
416        // SAFETY: Validation guarantees there to be a valid
417        // table index next.
418        let table_idx = unsafe { TableIdx::read_unchecked(wasm) };
419
420        let n: u32 = resumable.stack.pop_value().try_into().unwrap_validated(); // size
421        let cost = T::get_fc_extension_flat_cost(opcode::fc_extensions::TABLE_INIT)
422            + u64::from(n)
423                * T::get_fc_extension_cost_per_element(opcode::fc_extensions::TABLE_INIT);
424        if let Some(fuel) = &mut resumable.maybe_fuel {
425            if *fuel >= cost {
426                *fuel -= cost;
427            } else {
428                resumable.stack.push_value(Value::I32(n)).unwrap_validated(); // we are pushing back what was just popped, this can't panic.
429                return Ok(ControlFlow::Break(InterpreterLoopOutcome::OutOfFuel {
430                    required_fuel: NonZeroU64::new(cost - *fuel).expect(
431                        "the last check guarantees that the current fuel is smaller than cost",
432                    ),
433                }));
434            }
435        }
436
437        let s: i32 = resumable.stack.pop_value().try_into().unwrap_validated(); // offset
438        let d: i32 = resumable.stack.pop_value().try_into().unwrap_validated(); // dst
439
440        // SAFETY: All requirements are met:
441        // 1. The current module address must come from the
442        //    current store, because it is the only parameter to
443        //    this function that can contain module addresses. All
444        //    stores guarantee all addresses in them to be valid
445        //    within themselves.
446        // 2. Validation guarantees the table index to be valid
447        //    in the current module instance.
448        // 3./5. The table/element addresses are valid for a
449        //       similar reason that the module address is valid:
450        //       they are stored in the current module instance,
451        //       which is also part of the current store.
452        // 4. Validation guarantees the element index to be
453        //    valid in the current module instance.
454        unsafe {
455            table_init(
456                modules,
457                &mut store_inner.tables,
458                &store_inner.elements,
459                *current_module,
460                elem_idx,
461                table_idx,
462                n,
463                s,
464                d,
465            )?
466        };
467        Ok(ControlFlow::Continue(()))
468    }
469}
470
471define_instruction_fn! {
472    elem_drop_fn,
473    fuel_check = flat_fc(opcode::fc_extensions::ELEM_DROP),
474    |Args {
475         wasm,
476         modules,
477         current_module,
478         store_inner,
479         ..
480     }| {
481        // SAFETY: Validation guarantees there a valid element
482        // index next.
483        let elem_idx = unsafe { ElemIdx::read_unchecked(wasm) };
484
485        // SAFETY: All requirements are met:
486        // 1. The current module address must come from the
487        //    current store, because it is the only parameter to
488        //    this function that can contain module addresses. All
489        //    stores guarantee all addresses in them to be valid
490        //    within themselves.
491        // 2. Validation guarantees the element index to be
492        //    valid in the current module instance.
493        // 3. The element address is valid for a similar reason
494        //    that the module address is valid: it is stored in the
495        //    current module instance, which is also part of the
496        //    current store.
497        unsafe {
498            elem_drop(
499                modules,
500                &mut store_inner.elements,
501                *current_module,
502                elem_idx,
503            );
504        }
505        Ok(ControlFlow::Continue(()))
506    }
507}