Skip to content

Commit b5c2d4f

Browse files
committed
Fix MVP element-segment encoding for table64 active segments
When walrus emits an active element segment whose table is table 0, it tells `wasm-encoder` to use the MVP encoding (variant `0x00`) which carries an implicit table index of 0 and requires the offset constant to be `i32`. If table 0 is a `table64` table the offset must be `i64`, so the MVP encoding is not applicable; the resulting binary is rejected by compliant engines (V8 reports `invalid table elements limits flags` because it tries to decode the i64 offset bytes as an i32 const expression and then reads the following byte as element-section limits). Use `wasm-encoder`'s explicit-table-index form (variant `0x02`) unconditionally for `table64` tables. Non-`table64` table-0 active segments continue to use the backwards-compatible MVP encoding. Test: `crates/tests/tests/table64_active_segment.rs` asserts at the byte level that the emitted segment kind tag is not `0x00` for a table64 active segment. The test also round-trips through walrus.
1 parent 8b17b8b commit b5c2d4f

3 files changed

Lines changed: 133 additions & 5 deletions

File tree

crates/tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ serde_json = { version = "1.0.40", features = ['preserve_order'] }
1616
tempfile = "3.1.0"
1717
walrus = { path = "../.." }
1818
walrus-tests-utils = { path = "../tests-utils" }
19+
wasmparser = "0.245.1"
1920
wasmprinter = "0.245"
2021
wat = "1.0.85"
2122

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! Repro for an emit bug with active element segments on `table64`
2+
//! tables.
3+
//!
4+
//! When an active element segment targets table 0, walrus tells
5+
//! `wasm-encoder` to use the MVP encoding (variant 0) which carries an
6+
//! implicit table index of 0 and requires the offset constant to be
7+
//! `i32`. If table 0 is a `table64` table, the offset must be `i64`,
8+
//! so the MVP encoding is not applicable; walrus must emit the
9+
//! explicit-table-index form (variant 2) instead.
10+
//!
11+
//! Currently walrus picks the MVP form unconditionally for table 0,
12+
//! producing a wasm binary that real engines like V8 reject with
13+
//! errors like `invalid table elements limits flags` because they
14+
//! decode the (i64) offset as if it were an i32 const expression and
15+
//! then read the next byte as the "elements limits flags". `wasmparser`
16+
//! happens to accept the malformed binary, so this test asserts at the
17+
//! byte level on the segment-kind tag instead of relying on a parser.
18+
//!
19+
//! Expected: emitted wasm uses the explicit-table-index element form
20+
//! (variant 2) for table64 active segments.
21+
22+
use walrus::{
23+
ConstExpr, ElementItems, ElementKind, FunctionBuilder, Module, ModuleConfig, RefType,
24+
};
25+
26+
#[test]
27+
fn table64_active_segment_round_trips() {
28+
let mut config = ModuleConfig::new();
29+
config.generate_producers_section(false);
30+
let mut module = Module::with_config(config.clone());
31+
32+
// A single function to put in the table.
33+
let func_id =
34+
FunctionBuilder::new(&mut module.types, &[], &[]).finish(vec![], &mut module.funcs);
35+
module.exports.add("f", func_id);
36+
37+
// table64 funcref table starting at size 1.
38+
let table_id = module
39+
.tables
40+
.add_local(/* table64 */ true, 1, Some(1), RefType::FUNCREF);
41+
42+
// Active element segment at offset 0, table index 0.
43+
let elem_id = module.elements.add(
44+
ElementKind::Active {
45+
table: table_id,
46+
offset: ConstExpr::Value(walrus::ir::Value::I64(0)),
47+
},
48+
ElementItems::Functions(vec![func_id]),
49+
);
50+
module
51+
.tables
52+
.get_mut(table_id)
53+
.elem_segments
54+
.insert(elem_id);
55+
56+
let wasm = module.emit_wasm();
57+
58+
// Locate the element section and inspect its first segment's kind
59+
// byte. The MVP encoding is 0x00 (which requires an i32 offset);
60+
// any of 0x02/0x06 are the explicit-table-index forms that work
61+
// with table64. wasm-encoder would also use 0x04/0x05/0x07 for
62+
// expression-form segments without a function vector, but we use
63+
// the function-vector form here.
64+
let mut p = wasmparser::Parser::new(0);
65+
let mut cur = &wasm[..];
66+
let mut kind_byte = None;
67+
loop {
68+
match p.parse(cur, true).unwrap() {
69+
wasmparser::Chunk::Parsed { consumed, payload } => {
70+
if let wasmparser::Payload::ElementSection(reader) = &payload {
71+
// Element section bytes start with the count LEB,
72+
// then each segment begins with the kind byte.
73+
let range = reader.range();
74+
// First byte after the count LEB:
75+
let bytes = &wasm[range.start..range.end];
76+
// Count LEB is small (1) here.
77+
let (_count, count_len) = leb128_u32(bytes);
78+
kind_byte = Some(bytes[count_len]);
79+
break;
80+
}
81+
cur = &cur[consumed..];
82+
if matches!(payload, wasmparser::Payload::End(_)) {
83+
break;
84+
}
85+
}
86+
_ => break,
87+
}
88+
}
89+
let kind = kind_byte.expect("module must have an element section");
90+
assert_ne!(
91+
kind, 0x00,
92+
"walrus emitted the MVP element-segment form (variant 0) for a \
93+
table64 active segment; engines like V8 reject this. The fix is \
94+
to use the explicit-table-index form (variant 2) whenever the \
95+
target table is a table64 (or whenever the offset is i64)."
96+
);
97+
98+
// Sanity check: also round-trip through walrus itself.
99+
let module2 = config
100+
.parse(&wasm)
101+
.expect("emitted wasm must round-trip through walrus");
102+
let table = module2.tables.iter().next().expect("should have a table");
103+
assert!(table.table64, "table64 flag must survive round-trip");
104+
}
105+
106+
fn leb128_u32(bytes: &[u8]) -> (u32, usize) {
107+
let mut result = 0u32;
108+
let mut shift = 0;
109+
let mut i = 0;
110+
loop {
111+
let b = bytes[i];
112+
result |= ((b & 0x7f) as u32) << shift;
113+
i += 1;
114+
if b & 0x80 == 0 {
115+
return (result, i);
116+
}
117+
shift += 7;
118+
}
119+
}

src/module/elements.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,20 @@ impl Emit for ModuleElements {
254254
) {
255255
match kind {
256256
ElementKind::Active { table, offset } => {
257-
// When the table index is 0, set this to `None` to tell `wasm-encoder` to use
258-
// the backwards-compatible MVP encoding.
259-
let table_index =
260-
Some(cx.indices.get_table_index(*table)).filter(|&index| index != 0);
257+
// When the table index is 0 *and* the table is
258+
// a regular i32 table, set this to `None` to
259+
// tell `wasm-encoder` to use the backwards-
260+
// compatible MVP encoding. For table64 tables
261+
// the MVP encoding is not applicable (its
262+
// offset must be i32), so we always include
263+
// the explicit table index, which selects the
264+
// bulk-memory form that carries an i64 offset.
265+
let table_index = cx.indices.get_table_index(*table);
266+
let is_table64 = cx.module.tables.get(*table).table64;
267+
let encoded_table_index =
268+
Some(table_index).filter(|&idx| idx != 0 || is_table64);
261269
wasm_element_section.active(
262-
table_index,
270+
encoded_table_index,
263271
&offset.to_wasmencoder_type(cx),
264272
els,
265273
);

0 commit comments

Comments
 (0)