WebAssembly / WebAssembly/wabt

`compact imports` broken

Open
#2,845 1 comment 0 reactions 0 assignees View on GitHub

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_entries times over entries, keeping the separate running index for the
    imports handed to ReadImport.

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.