WebAssembly / WebAssembly/wabt
`compact imports` broken
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 8.1k
- Forks
- 827
- Avg merge
- 4d 6h
- Merged PRs (30d)
- 18
Description
WABT 1.0.41
--enable-compact-imports produces modules that no conforming decoder accepts, and rejects the compact-import proposal's own reference modules. Both sides of wabt make the same off-spec assumption, so the two agree with each other and the problem is invisible to wabt's round-trip tests.
Found while working on Wasm3 v0.9.0
This, unfortunately, also affects --enable-all, resulting in broken modules.
Below is the AI-assisted analysis.
What the proposal says
proposals/compact-import-section/Overview.md:
;; After
importsec ::== section_2(list(imports))
imports ::== nm1:name nm2:name externtype ;; single-item encoding (existing)
| nm1:name nm:name 0x7F list(nm2:name externtype) -- if nm = "" ;; compact encoding 1
| nm1:name nm:name 0x7E externtype list(nm2:name) -- if nm = "" ;; compact encoding 2
The section is list(imports), and imports is the whole alternation — so the section's leading
u32 counts entries, where one compact run is a single entry that may carry many imports.
Every reference module in the proposal's spec suite follows that. Decoding
proposals/compact-import-section/*.wasm from the wg-3.0 spec tests, each module whose entry count
differs from its import count declares the entry count:
binary-compact-imports.4.wasm declared=1 entries=1 imports=2
imports-compact.1.wasm declared=2 entries=2 imports=6
imports-compact.11.wasm declared=2 entries=2 imports=3
imports-compact.14.wasm declared=1 entries=1 imports=5
imports-compact.23.wasm declared=4 entries=4 imports=6
1. wat2wasm writes a count no conforming decoder can use
(module
(import "env" "a" (func))
(import "env" "b" (func))
)
$ wat2wasm --enable-compact-imports m.wat -o m.wasm
$ xxd m.wasm
00000000: 0061 736d 0100 0000 0104 0160 0000 0210 .asm.......`....
00000010: 0203 656e 7600 7f02 0161 0000 0162 0000 ..env....a...b..
Import section:
02 10 ; section 2, 16 bytes
02 ; count = 2 <-- should be 1: the section holds one entry
03 65 6e 76 ; "env"
00 ; "" (empty item name)
7f ; compact marker
02 ; 2 items
01 61 00 00 ; "a", func, type 0
01 62 00 00 ; "b", func, type 0
A decoder that reads list(imports) reads the one entry present and then tries to read a second,
running off the end of the section:
$ wasm3 m.wasm
Error: underrun while parsing Wasm binary
Every module with two or more adjacent imports sharing a module name comes out this way.
2. The reader rejects conforming modules
Take the same three-module-name example encoded per the proposal — three entries (one run of two,
then two single imports), count 3:
00 61 73 6d 01 00 00 00
01 04 01 60 00 00
02 21 ; section 2, 33 bytes
03 ; 3 entries
03 65 6e 76 00 7f 02 01 61 00 00 01 62 00 00 ; run: "env"."a", "env"."b"
03 73 79 73 01 63 00 00 ; "sys"."c"
04 73 6f 6c 6f 01 64 00 00 ; "solo"."d"
$ wasm2wat --enable-all conformant.wasm
0000028: error: unfinished section (expected end: 0x31)
The proposal's own modules fail the same way:
$ wasm2wat --enable-all imports-compact.23.wasm
000003c: error: unfinished section (expected end: 0x4e)
$ wasm-objdump -x -j import imports-compact.23.wasm
000003c: error: unfinished section (expected end: 0x4e)
imports-compact.23.wasm: file format wasm 0x1
Section Details:
Import[4]:
- func[0] sig=0 <mixed.f0> <- mixed.f0
- func[1] sig=0 <mixed.f1> <- mixed.f1
- global[0] i32 mutable=0 <- mixed.g2
- func[2] sig=0 <mixed.f2> <- mixed.f2
Four imports were read and the fourth entry was never reached.
Conversely, the reader accepts modules that are malformed under the proposal — it accepts
everything wat2wasm writes, which is how the two stay consistent.
Where it comes from
src/binary-writer.cc:1500 writes the total import count, then emits one entry per group:
WriteU32Leb128(stream_, module_->imports.size(), "num imports");
src/binary-reader.cc:2782 reads that count and terminates the loop on imports read, since i is
incremented once per import inside the compact runs:
Index num_imports;
CHECK_RESULT(ReadCount(&num_imports, "import count"));
CALLBACK(OnImportCount, num_imports);
Index i = 0;
while (i < num_imports) {
...
for (Index j = 0; j < num_compact_imports; ++j) {
CHECK_RESULT(ReadStr(&field_name, "compact import field name"));
CHECK_RESULT(ReadExternalKind(&kind, "compact import kind", "import"));
CHECK_RESULT(ReadImport(i++, module_name, field_name, kind));
}
...
}
Both 0x7F and 0x7E paths are affected.
Suggested fix
- Writer: count entries, not imports — walk the imports once to count the groups, and write
that as the section count. - Reader: loop
num_entriestimes over entries, keeping the separate running index for the
imports handed toReadImport.
One wrinkle for the reader: OnImportCount currently promises the number of imports, which the
compact encoding no longer makes available before the section is parsed. Either its contract
changes to "entries" (and consumers that size vectors from it stop relying on it being exact), or
the section is pre-scanned.
Notes
- This changes the bytes wabt emits for previously written compact modules, but those modules are
not decodable by anything else today. - A regression test that decodes a module with a compact run followed by another entry would have
caught both halves; a wat2wasm→wasm2wat round trip does not, since the writer and reader share
the assumption.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the compact-import encoding in proposals/compact-import-section/Overview.md, then inspect src/binary-writer.cc:1500 and src/binary-reader.cc:2782. Add regression coverage for a compact run followed by another entry, including both compact markers. Done means wat2wasm output and the proposal reference modules are accepted by conforming readers and wasm2wat handles their entry counts correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, wasm
- Domain
- compilers, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100