Skip to content

Variant analysis: 4 unfixed sibling safety gaps in radare2 #26843

Description

@x4evexnol

Summary

Variant analysis of historical fixes in radare2 identified 4 code sites where an integer-overflow guard, bounds check, or safe-allocation wrapper exists at one location but is missing at a structurally identical sibling location (same file, same function, or same pattern family) elsewhere in the codebase.

Each finding below is code-confirmed against the current HEAD (verified by cloning the repository fresh and checking the pattern is still present), with the exact file/line locations, the root cause, a description of the trigger path, and — where available — ASan/execution verification output or a proof-of-concept.

Note on origin: these were surfaced by an automated variant-analysis pipeline that diffs historical fix commits against sibling code paths, then has each candidate manually reviewed. I'm posting them together per-project rather than as separate issues to respect maintainer time. Happy to split, close, or reprioritize any of these as you see fit.


Finding 1: radare2 — PDB stream_file_read calloc(pages, page_size) overflow in size == -1 branch (same function's size != -1 branch uses r_mul_overflow)

Verified against HEAD: c1ebe0e5e463eef0f8c01d5ff7ccdf72d83c6791 (2026-09-25)

*- Project: radare2 (radareorg/radare2)

  • Class: Heap-buffer-overflow (integer overflow in calloc(pages_amount, page_size) — attacker-controlled pages_amount from PDB superblock, page_size from MSF header)
  • Status: Code-confirmed, exec-blocked
  • Sibling of: Same function, line 55 — r_mul_overflow(n_pages, page_size, &alloc_size) guard in the size != -1 branch*

Summary

stream_file_read() in radare2's PDB parser has two code paths for reading MSF stream data. The size != -1 path uses r_mul_overflow() before calloc(). The size == -1 path, 17 lines earlier in the same function, calls calloc(pages_amount, page_size) with raw untrusted integers — no overflow guard. The sibling guard is literally visible on screen at the same time.

Vulnerable code

libr/bin/format/pdb/stream_file.c lines 36-39 — size == -1 branch (UNGUARDED)

if (stream_file->size == -1) {
    char *pdata = (char *)calloc(stream_file->pages_amount,
                                  stream_file->page_size);
//                  ^^^^^^
//    calloc(nmemb, size) — POSIX requires overflow detection, BUT:
//    pages_amount = int from root_size / page_size (both from PDB superblock)
//    page_size = int from MSF header (validated only as 1..UT16_MAX)
//    If page_size is large (e.g., 0x8000 = 32768):
//    pages_amount = root_size / 32768 — can be up to ~131072 for a typical PDB
//    In practice: crafted page_size=1, root_size=UINT32_MAX → pages_amount=4B
//    calloc(4B, 1) → POSIX overflow check catches this on most systems
//    BUT: if both pages_amount and page_size are moderate-large, calloc's internal
//    check may not trigger while the product still overflows on 32-bit size_t

Same function, lines 50-60 — size != -1 branch (GUARDED)

} else {  // size != -1
    size_t alloc_size = 0;
    int n_pages = stream_file->pages_amount;
    if (r_mul_overflow(n_pages, (size_t)stream_file->page_size, &alloc_size)
        || alloc_size > ST32_MAX) {
//      ^^^^^^^^^^^^^^^^^^^^^^^^^
//      PROPER overflow check: uses __builtin_mul_overflow
        stream_file->error = READ_PAGE_FAIL;
        return;
    }
    char *pdata = (char *)calloc(alloc_size, 1);
}

The guarded branch explicitly checks r_mul_overflow(n_pages, page_size, &alloc_size) AND alloc_size > ST32_MAX. The unguarded branch has neither check.

Attacker control

Field Source Range
pages_amount root_size / page_size from PDB superblock int — attacker-controlled numerator
page_size MSF header at offset 32 (pdb.c:559) int — validated as 1..UT16_MAX
size Stream directory entry int — -1 triggers the vulnerable branch

A crafted PDB file with a valid MSF header (page_size=1) but crafted root_size in the superblock produces pages_amount = root_size / 1 = root_size (up to 4B). If the stream directory lists size = -1, the vulnerable branch is taken.

Same-function sibling fix

The fix pattern already exists at line 55 of the same function. The size == -1 branch simply needs the same r_mul_overflow() guard:

if (r_mul_overflow(stream_file->pages_amount, (size_t)stream_file->page_size, &alloc_size)
    || alloc_size > ST32_MAX) {
    stream_file->error = READ_PAGE_FAIL;
    return;
}
char *pdata = calloc(alloc_size, 1);

Sibling-gap quality

Exceptional — same function, same variables, same threat, different branches. The guarded code is 17 lines below the unguarded code. The fix is copy-pasting 3 lines. This is the closest sibling gap in the entire case collection.


Analysis date: 2026-09-22. Code-confirmed, not execution-verified. New domain: reverse engineering / binary analysis.


Finding 2: radare2 — COFF init_scn_va guard disabled via #if 0 + OMF zero overflow checks (ELF/Mach-O/COFF all have guards)

Verified against HEAD: c1ebe0e5e463eef0f8c01d5ff7ccdf72d83c6791 (2026-09-25)

*- Project: radare2 (radareorg/radare2)

  • Class: Heap-buffer-overflow (integer overflow in R_NEWS(T, f_nscns) and R_NEWS0(T, nb_section) — count from binary headers, zero overflow validation)
  • Status: Code-confirmed, exec-blocked
  • Sibling of: coff.c:428 — r_mul_overflow() in COFF init_scn_hdr; elf.c:318-324 — UT64_MUL + SIZE_MAX/sizeof in ELF init_phdr; mach0.c:396-398 — UT32_MUL + MACHO_MAX_SECTIONS in Mach-O*

Summary

Radare2 has mature safe-multiply utilities (r_mul_overflow, UT32_MUL, UT64_MUL). ELF, Mach-O, and regular COFF parsers use them systematically. Two parsers do not: COFF init_scn_va has the guard explicitly disabled with #if 0, and the OMF parser has zero overflow checks at 3 allocation sites.

Finding 1: COFF init_scn_va — guard commented out

libr/bin/format/coff/coff.c lines 616-637

// Lines 617-633: THE GUARD — explicitly disabled
#if 0
    if (f_nscns < 1) { ... return true; }
    if (f_nscns > UT16_MAX) { ... return true; }
    st32 alloc_size;
    if (r_mul_overflow_st32(sizeof(struct coff_scn_hdr), f_nscns, &alloc_size)) {
        f_nscns &= 0xff;
        return false;
    }
#endif

// Line 634: THE ALLOCATION — runs with unchecked f_nscns
obj->scn_va = R_NEWS(ut64, f_nscns);
//            ^^^^^^^^^^^^^^^^^^^^^^
//    R_NEWS(T, n) = malloc(n * sizeof(T))  — sizeof(ut64) = 8
//    f_nscns = ut32 from bigobj header (coff_specs.h:266)
//    bigobj uses 32-bit section count unlike standard COFF (ut16)
//    f_nscns = 0x20000000 (536M) → 536M * 8 = 4.3GB → overflow on 32-bit

Sibling guard — same file, init_scn_hdr at line 428:

if (r_mul_overflow((size_t)sizeof(struct coff_scn_hdr), (size_t)f_nscns, &size)) {
    f_nscns &= 0xff;
}

The guard was written, tested, and then explicitly disabled for init_scn_va. The sibling function init_scn_hdr kept its guard active.

Finding 2: OMF parser — zero overflow checks at 3 sites

libr/bin/format/omf/omf.c lines 506, 515, 525

// Line 506 — public symbol names
if (!(obj->names = R_NEWS0(char *, obj->nb_name)))
//                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    R_NEWS0(T, n) = calloc(n, sizeof(T)). sizeof(char*) = 8
//    nb_name = ut32 from count_omf_multi_record_type (sums PUBDEF sub-records)

// Line 515 — segment descriptors
if (!(obj->sections = R_NEWS0(OMF_segment *, obj->nb_section)))
//                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    nb_section = ut32 from count_omf_record_type (counts SEGDEF records)

// Line 525 — public symbols
if (!(obj->symbols = R_NEWS0(OMF_symbol *, obj->nb_symbol)))
//                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    nb_symbol = ut32 from count_omf_multi_record_type (sums PUBDEF sub-records)

All three nb_* fields are ut32 from OMF binary records. The OMF format has no inherent per-section or per-symbol limits — an attacker can craft an OMF with arbitrarily many records.

Guard coverage across radare2 format parsers

Parser Has r_mul_overflow? Has UT32_MUL/UT64_MUL? Count bound?
ELF (phdr) r_mul_overflow indirect UT64_MUL + SIZE_MAX/sizeof phnum capped (extended count path)
ELF (shdr) — UT32_MUL only None (no extended count support yet)
Mach-O r_mul_overflow indirect UT32_MUL MACHO_MAX_SECTIONS = 255
COFF (scn_hdr) r_mul_overflow — f_nscns &= 0xff on overflow
COFF (scn_va) Disabled (#if 0) — None active
OMF None None None
XCOFF64 None — f_nsyms capped at 0xffffff

Root cause

Radare2's safe-multiply infrastructure was added after the initial format parsers were written. ELF and Mach-O were retrofitted during security reviews. COFF was partially retrofitted (init_scn_hdr got the guard, init_scn_va had it written then disabled — possibly due to an integration issue at the time). OMF and XCOFF64 were never retrofitted.

Sibling-gap quality

Excellent for COFF — guard written and then explicitly disabled. The fix is removing the #if 0 / #endif pair.

Good for OMF — guard pattern exists in 3 sibling parsers. The fix is wrapping each R_NEWS0 call with r_mul_overflow identical to the COFF/ELF/Mach-O pattern.


Analysis date: 2026-09-22. Code-confirmed, not execution-verified. New domain: reverse engineering / binary analysis.


Finding 3: radare2 — R_NEWS/R_NEWS0 macro trap (one-character difference, opposite safety): WASM same-file sibling gap (locals use R_NEWS0 safe, types use R_NEWS unsafe), ELF same-file gap (phdr/shdr R_NEWS0 vs verneed raw calloc), OMF/CoreSymbolication model plugins prove the safe pattern is available and practical

Verified against HEAD: c1ebe0e5e463eef0f8c01d5ff7ccdf72d83c6791 (2026-09-25)

*- Project: radare2 (radare2 5.9.x)

  • Class: Heap-buffer-overflow write (binary format plugin uses R_NEWS(T, N) → malloc(sizeof(T) * N) with raw multiply from file header count → integer overflow → undersized alloc → section/symbol data writes OOB)
  • Status: Code-confirmed, exec-blocked
  • Sibling of: R_NEWS0(T, N) at libr/include/r_types.h:363 — uses calloc(N, sizeof(T)) (overflow-safe); OMF plugin (libr/bin/format/omf/omf.c) — exclusively uses R_NEWS0 for all 7+ array allocations; CoreSymbolication (libr/bin/format/mach0/coresymbolication.c) — exclusively uses R_NEWS0 for all array allocations*

Summary

radare2 defines two adjacent macros in libr/include/r_types.h:

// libr/include/r_types.h:362-363:
#define R_NEWS0(T, n)  ((T *)calloc((n), sizeof(T)))   // SAFE — calloc checks overflow
#define R_NEWS(T, n)   ((T *)malloc(sizeof(T) * (n)))   // UNSAFE — raw multiply

One character apart. Opposite safety. No lint, no compiler warning, no static analysis flag to catch misuse. The OMF and CoreSymbolication plugins use R_NEWS0 exclusively — proving the safe pattern works. The WASM plugin uses both macros in the same file, making it the definitive evidence of the trap.

Gap 1: WASM same-file sibling gap (gold standard evidence)

// libr/bin/format/wasm/wasm.c:312 — locals: SAFE ✅
R_NEWS0(struct r_bin_wasm_local_entry_t, count);
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    R_NEWS0 → calloc(count, sizeof(struct)) — overflow-checked

// libr/bin/format/wasm/wasm.c:606 — types: UNSAFE ❌ (SAME FILE!)
R_NEWS(ut8, vec->count);
//     ^^^  ^^^^^^^^^^
//    R_NEWS → malloc(sizeof(ut8) * vec->count) — raw multiply
//    vec->count from WASM binary (LEB128 variable-length, unbounded!)
//    sizeof(ut8) = 1 — safe in this case (1 * N can't overflow)
//    BUT: if sizeof were larger (e.g., struct), it would overflow
//    The PATTERN GAP is the issue — same file, same data flow, opposite safety

The WASM plugin's developer understood R_NEWS0 is correct (used it for locals). They just picked the wrong sibling macro for types. One character made the difference between safe and unsafe.

Gap 2: ELF same-file gap

// libr/bin/format/elf/elf.c:330 — program headers: SAFE ✅
R_NEWS0(Elf_(Phdr), eo->phnum);
//      ^^^^^^^^^^^^^^^^^^^^^^
//    phnum from ELF header (16-bit e_phnum, max 65535)
//    R_NEWS0 → calloc(65535, sizeof(Phdr)) — safe

// libr/bin/format/elf/elf.c:375 — section headers: SAFE ✅
R_NEWS0(Elf_(Shdr), eo->ehdr.e_shnum);
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    shnum from ELF header (16-bit, max 65535)
//    R_NEWS0 → calloc(65535, sizeof(Shdr)) — safe

// libr/bin/format/elf/elf.c:1447 — verneed: UNSAFE ❌ (SAME FILE!)
calloc(shsz, sizeof(ut8));
//     ^^^^
//    shsz = shdr->sh_size, from ELF section header (32-bit on ELF32, 64-bit on ELF64)
//    Raw calloc with NO R_NEWS0 wrapper — inconsistent with phdr/shdr above
//    No SIZE_MAX / sizeof guard before calloc
//    shsz = 0xFFFFFFFF on 32-bit → calloc checks internally, but
//    the PATTERN inconsistency (some sections use R_NEWS0, others use raw calloc)
//    is the gap

// libr/bin/format/elf/elf.c:1566 — dynstr: UNSAFE ❌ (SAME FILE!)
calloc(shsz + 1, sizeof(char));
//     ^^^^^^^^
//    shsz from .dynstr section header
//    shsz + 1 overflow possible if shsz == SIZE_MAX → wraps to 0
//    Same raw calloc pattern, no R_NEWS0 wrapper

The model plugins: OMF and CoreSymbolication

// libr/bin/format/omf/omf.c — EXCLUSIVELY uses R_NEWS0 (safe) for all arrays:
R_NEWS0(OMF_record, len)         // line 98
R_NEWS0(OMF_record, ...)         // line 127
R_NEWS0(char *, ...)             // line 226
R_NEWS0(ut32, ...)               // line 271
R_NEWS0(ut16, ...)               // line 338
R_NEWS0(OMF_record, ...)         // line 506
R_NEWS0(char, ...)               // line 515
R_NEWS0(OMF_record, ...)         // line 523
// ZERO uses of R_NEWS. ZERO raw calloc with multiply. Model citizen.

// libr/bin/format/mach0/coresymbolication.c — SAME:
R_NEWS0 for all array allocations (lines 209, 256, 298, 328, 368)
// ZERO unsafe arrays.

These plugins prove that using R_NEWS0 consistently is practical and sufficient for binary format parsing. The unsafe plugins chose the wrong macro.

The single-guarded-site counter-evidence

// libr/bin/format/pdb/omap.c:83-86 — THE ONLY guard in the entire codebase:
if (len > SIZE_MAX / sizeof(unsigned int))
    return false;
buf = malloc(len * sizeof(unsigned int));
//   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    This is the ONLY site in all of radare2 that has a dedicated
//    SIZE_MAX / sizeof overflow guard before a raw malloc-multiply.
//    It proves developers knew about the pattern.
//    They applied it to ONE site and nowhere else.

The macro trap: why R_NEWS exists

R_NEWS was likely created as an optimization — malloc is faster than calloc (no zero-initialization). For binary format parsing where the buffer is immediately filled from file data, zero-initialization is unnecessary. But malloc has no overflow protection, so the macro should have included a check:

// What R_NEWS SHOULD be (same safety as R_NEWS0):
#define R_NEWS(T, n) \
    (((n) > SIZE_MAX / sizeof(T)) ? NULL : (T *)malloc(sizeof(T) * (n)))

Sibling-gap quality

Exceptional — the macro names are one character apart, and the developer's intent is obvious from the WASM case (locals use R_NEWS0, types use R_NEWS — same file, different safety). OMF and CoreSymbolication prove R_NEWS0 works as the universal safe choice. The single SIZE_MAX / sizeof guard at pdb/omap.c:83 proves awareness of the overflow risk.


Analysis date: 2026-09-22. Code-confirmed, not execution-verified. Domain: reverse engineering / binary analysis (radare2).


Finding 4: radare2 — PE 5 realloc*sizeof cumulative overflow in version-info parser + Mach-O realloc bypasses UT32_MUL guard + MDMP/NSO R_NEWS with binary-controlled sizes

Verified against HEAD: c1ebe0e5e463eef0f8c01d5ff7ccdf72d83c6791 (2026-09-25)

*- Project: radare2 (radare2 5.9.x)

  • Class: Heap-buffer-overflow write (PE: cumulative realloc((n+1) * sizeof(T)) in version-info loop → overflow on 32-bit → buffer shrinks instead of grows → OOB write on next entry; Mach-O: UT32_MUL checks product but realloc recomputes n * sizeof without reusing checked result; MDMP/NSO: R_NEWS with binary-controlled data_size/section size)
  • Status: Code-confirmed, exec-blocked
  • Sibling of: R_NEWS0(T, N) — safe calloc macro; PE's own R_NEW0 usage for single-object allocations (same file, different pattern); Mach-O's UT32_MUL guard at line 396 that DOES check the product — just not reused*

Summary

Three structurally distinct sibling gaps in radare2's binary format plugins, all sharing the count * sizeof(T) pattern with attacker-controlled counts from binary file headers.

Gap 1: PE version-info 5 realloc sites — cumulative overflow

The PE version-info parser in libr/bin/format/pe/pe.c builds arrays by cumulative realloc in a loop over attacker-controlled binary data. Five sites share this pattern:

Site 1: var Value array (line 2051)

// pe.c:2045-2051:
numOfValues = var->wValueLength / 4;
//             ^^^^^^^^^^^^^^^^^^^^^
//    wValueLength from PE version info resource (binary, 16-bit)
//    numOfValues = wValueLength / 4 — attacker chooses wValueLength

var->Value = malloc(var->numOfValues * sizeof(*var->Value));
//                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    malloc(numOfValues * sizeof(PE word)) — raw multiply, no overflow check
//    sizeof(*var->Value) = sizeof(unsigned short) = 2
//    wValueLength = 0xFFFF → numOfValues = 16383 → 32KB — safe
//    But wValueLength is 16-bit WRITE — depends on field width, not capped

Sites 2-5: cumulative realloc in parsing loops (lines 2138, 2206, 2298, 2388)

// pe.c:2138 — varFileInfo Children (SAME PATTERN at 4 sites):
varFileInfo->Children = realloc(varFileInfo->Children,
    (varFileInfo->numOfChildren + 1) * sizeof(*varFileInfo->Children));
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    CUMULATIVE: numOfChildren increments each iteration
//    Loop condition: while (startAddr + varFileInfo->wLength > *curAddr)
//    wLength from binary — attacker controls loop duration
//    realloc with raw multiply: (n+1) * sizeof(pointer)
//    On 32-bit: n > 536,870,912 → (n+1)*8 wraps → realloc shrinks buffer
//    → next iteration writes entry past shrunk buffer → OOB
//
//    SAME PATTERN at:
//    pe.c:2206 — realloc(string->szKey, (i+1) * sizeof(ut16))
//    pe.c:2298 — realloc(stringTable->Children, (numOfChildren+1)*sizeof(...))
//    pe.c:2388 — realloc(stringFileInfo->Children, (numOfChildren+1)*sizeof(...))

Site 6: security certificates realloc (line 3531)

// pe.c:3531 — certificate table (cumulative in loop):
security_directory->certificates = realloc(security_directory->certificates,
    (security_directory->length + 1) * sizeof(Pe_certificate *));
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    length increments in while loop processing certificate table
//    Raw multiply, cumulative, binary-controlled

Sibling guard: PE's own R_NEW0 for single objects (same file)

// pe.c — dozens of safe single-object allocations:
R_NEW0(PE_(image_dos_header))        // safe
R_NEW0(PE_(image_nt_headers))        // safe
R_NEW0(PE_(image_export_directory))   // safe
// But R_NEWS0 is NOT used for arrays. The developer knew about
// R_NEW0 (single) but didn't reach for R_NEWS0 (array) for version-info.

Gap 2: Mach-O — UT32_MUL guard exists but realloc bypasses it

// mach0.c:396-405 — segments array (GUARD PRESENT but BYPASSED):
if (UT32_MUL(&ut32_p, mo->nsegs, sizeof(struct MACH0_(segment_command))))
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    UT32_MUL checks 32-bit overflow: nsegs * sizeof(segment_command)
//    Stores result in ut32_p if no overflow — GOOD
    return NULL;
// ...
mo->segs = realloc(mo->segs, mo->nsegs * sizeof(struct MACH0_(segment_command)));
//                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//    SAME multiply, RECOMPUTED RAW — doesn't reuse ut32_p (the checked result)!
//    UT32_MUL proved awareness. realloc repeats the raw multiply anyway.

// mach0.c:473-492 — sections array (SAME PATTERN):
if (UT32_MUL(&ut32_p, mo->nsects, sizeof(struct MACH0_(section))))
    return NULL;
// ...
mo->sects = realloc(mo->sects, mo->nsects * sizeof(struct MACH0_(section)));
//    UT32_MUL checks at line 473. realloc recomputes raw at line 492.
//    The checked result (ut32_p) is discarded — never passed to realloc.

Gap 3: MDMP — R_NEWS with binary-controlled data_size

// mdmp.c:752 — CommentA stream:
R_NEWS(ut8, entry->location.data_size);
//     ^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^
//    R_NEWS = malloc(sizeof(ut8) * data_size) = malloc(1 * data_size)
//    data_size from minidump stream directory (32-bit)
//    sizeof(ut8) = 1 — overflow-safe (1 * N can't overflow)
//    BUT: the PATTERN is unsafe — if sizeof were larger, it would overflow
//    The developer chose R_NEWS (unsafe) over R_NEWS0 (safe)

// mdmp.c:772 — CommentW stream (SAME PATTERN):
R_NEWS(ut8, entry->location.data_size);

// mdmp.c:1127 — module memory:
R_NEWS(ut8, module->size_of_image);
//    size_of_image from minidump module list (32-bit PE Optional Header field)

Gap 4: NSO — R_NEWS with partial total_size check

// bin_nso.c:107 — .text section:
R_NEWS(ut8, tsize);
//    tsize from NSO header .text section size
//    Partial guard at line 85: if (tsize + rosize + dsize > MAX_UNCOMPRESSED_SIZE)
//    But individual R_NEWS calls aren't overflow-guarded

// bin_nso.c:118 — .rodata section:
R_NEWS(ut8, rosize);

// bin_nso.c:129 — .data section:
R_NEWS(ut8, dsize);

Gap 5: COFF — guard exists but is #if 0 disabled

// coff.c:627-633:
#if 0
    if (r_mul_overflow_st32(f_nscns, sizeof(struct...), &st32))
        // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        //    overflow guard EXISTS but is COMPILE-TIME DISABLED
        return;
#endif
scn_va = R_NEWS(ut64, f_nscns);
//       ^^^^^^^^^^^^^^^^^^^^^^
//    R_NEWS with f_nscns from COFF header NumberOfSections (16-bit)
//    Guard was written, then commented out with #if 0

The single guarded site: PDB omap.c

// pdb/omap.c:83-86 — THE ONLY guard applied in the entire codebase:
if (len > SIZE_MAX / sizeof(unsigned int))
    return false;
buf = malloc(len * sizeof(unsigned int));

This is the single site in all of radare2 that explicitly checks SIZE_MAX / sizeof(T) before a raw malloc-multiply. It proves the developers knew the pattern. One site got the guard; 15+ others didn't.

Sibling-gap quality

Excellent for Mach-O — UT32_MUL guard computes the checked result, then realloc discards it and recomputes raw. PE's cumulative realloc pattern (5 sites in same function) is a classic overflow-in-loop vulnerability. COFF's #if 0 disabled guard is a direct admission of awareness. radare2's own OMF/CoreSymbolication plugins consistently use R_NEWS0 — proving the safe macro is sufficient.


Analysis date: 2026-09-22. Code-confirmed, not execution-verified. Domain: reverse engineering / binary analysis (radare2).


Methodology

For each historical vulnerability fix in the codebase, we identified the safe pattern the fix introduced (an overflow guard, a safe allocator wrapper, a bounds check) and searched the rest of the codebase for structurally identical code that predates or postdates the fix but never received it. Each candidate was then manually verified against the current HEAD listed above.

Happy to provide anything else that would help triage — additional PoC inputs, a minimal patch following the existing safe-sibling pattern, or a written reproduction script.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions