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.
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_readcalloc(pages, page_size)overflow insize == -1branch (same function'ssize != -1branch usesr_mul_overflow)Verified against HEAD:
c1ebe0e5e463eef0f8c01d5ff7ccdf72d83c6791 (2026-09-25)*- Project: radare2 (radareorg/radare2)
calloc(pages_amount, page_size)— attacker-controlledpages_amountfrom PDB superblock,page_sizefrom MSF header)r_mul_overflow(n_pages, page_size, &alloc_size)guard in thesize != -1branch*Summary
stream_file_read()in radare2's PDB parser has two code paths for reading MSF stream data. Thesize != -1path usesr_mul_overflow()beforecalloc(). Thesize == -1path, 17 lines earlier in the same function, callscalloc(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.clines 36-39 —size == -1branch (UNGUARDED)Same function, lines 50-60 —
size != -1branch (GUARDED)The guarded branch explicitly checks
r_mul_overflow(n_pages, page_size, &alloc_size)ANDalloc_size > ST32_MAX. The unguarded branch has neither check.Attacker control
pages_amountroot_size / page_sizefrom PDB superblockint— attacker-controlled numeratorpage_sizepdb.c:559)int— validated as 1..UT16_MAXsizeint—-1triggers the vulnerable branchA 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 listssize = -1, the vulnerable branch is taken.Same-function sibling fix
The fix pattern already exists at line 55 of the same function. The
size == -1branch simply needs the samer_mul_overflow()guard: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_vaguard 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)
R_NEWS(T, f_nscns)andR_NEWS0(T, nb_section)— count from binary headers, zero overflow validation)coff.c:428—r_mul_overflow()in COFFinit_scn_hdr;elf.c:318-324—UT64_MUL+SIZE_MAX/sizeofin ELFinit_phdr;mach0.c:396-398—UT32_MUL+MACHO_MAX_SECTIONSin 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: COFFinit_scn_vahas 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 outlibr/bin/format/coff/coff.clines 616-637Sibling guard — same file,
init_scn_hdrat line 428:The guard was written, tested, and then explicitly disabled for
init_scn_va. The sibling functioninit_scn_hdrkept its guard active.Finding 2: OMF parser — zero overflow checks at 3 sites
libr/bin/format/omf/omf.clines 506, 515, 525All three
nb_*fields areut32from 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
r_mul_overflow?UT32_MUL/UT64_MUL?r_mul_overflowindirectUT64_MUL+SIZE_MAX/sizeofphnumcapped (extended count path)UT32_MULonlyr_mul_overflowindirectUT32_MULMACHO_MAX_SECTIONS = 255r_mul_overflowf_nscns &= 0xffon overflow#if 0)f_nsymscapped at 0xffffffRoot 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_hdrgot the guard,init_scn_vahad 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/#endifpair.Good for OMF — guard pattern exists in 3 sibling parsers. The fix is wrapping each
R_NEWS0call withr_mul_overflowidentical 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_NEWS0macro trap (one-character difference, opposite safety): WASM same-file sibling gap (locals useR_NEWS0safe, types useR_NEWSunsafe), ELF same-file gap (phdr/shdrR_NEWS0vs verneed rawcalloc), OMF/CoreSymbolication model plugins prove the safe pattern is available and practicalVerified against HEAD:
c1ebe0e5e463eef0f8c01d5ff7ccdf72d83c6791 (2026-09-25)*- Project: radare2 (radare2 5.9.x)
R_NEWS(T, N)→malloc(sizeof(T) * N)with raw multiply from file header count → integer overflow → undersized alloc → section/symbol data writes OOB)R_NEWS0(T, N)atlibr/include/r_types.h:363— usescalloc(N, sizeof(T))(overflow-safe); OMF plugin (libr/bin/format/omf/omf.c) — exclusively usesR_NEWS0for all 7+ array allocations; CoreSymbolication (libr/bin/format/mach0/coresymbolication.c) — exclusively usesR_NEWS0for all array allocations*Summary
radare2 defines two adjacent macros in
libr/include/r_types.h:One character apart. Opposite safety. No lint, no compiler warning, no static analysis flag to catch misuse. The OMF and CoreSymbolication plugins use
R_NEWS0exclusively — 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)
The WASM plugin's developer understood
R_NEWS0is 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
The model plugins: OMF and CoreSymbolication
These plugins prove that using
R_NEWS0consistently is practical and sufficient for binary format parsing. The unsafe plugins chose the wrong macro.The single-guarded-site counter-evidence
The macro trap: why
R_NEWSexistsR_NEWSwas likely created as an optimization —mallocis faster thancalloc(no zero-initialization). For binary format parsing where the buffer is immediately filled from file data, zero-initialization is unnecessary. Butmallochas no overflow protection, so the macro should have included a check: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_NEWS0works as the universal safe choice. The singleSIZE_MAX / sizeofguard 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*sizeofcumulative overflow in version-info parser + Mach-O realloc bypassesUT32_MULguard + MDMP/NSOR_NEWSwith binary-controlled sizesVerified against HEAD:
c1ebe0e5e463eef0f8c01d5ff7ccdf72d83c6791 (2026-09-25)*- Project: radare2 (radare2 5.9.x)
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_MULchecks product but realloc recomputesn * sizeofwithout reusing checked result; MDMP/NSO:R_NEWSwith binary-controlleddata_size/section size)R_NEWS0(T, N)— safecallocmacro; PE's ownR_NEW0usage for single-object allocations (same file, different pattern); Mach-O'sUT32_MULguard 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.cbuilds arrays by cumulative realloc in a loop over attacker-controlled binary data. Five sites share this pattern:Site 1: var Value array (line 2051)
Sites 2-5: cumulative realloc in parsing loops (lines 2138, 2206, 2298, 2388)
Site 6: security certificates realloc (line 3531)
Sibling guard: PE's own
R_NEW0for single objects (same file)Gap 2: Mach-O — UT32_MUL guard exists but realloc bypasses it
Gap 3: MDMP —
R_NEWSwith binary-controlleddata_sizeGap 4: NSO —
R_NEWSwith partial total_size checkGap 5: COFF — guard exists but is
#if 0disabledThe single guarded site: PDB omap.c
This is the single site in all of radare2 that explicitly checks
SIZE_MAX / sizeof(T)before a rawmalloc-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 0disabled guard is a direct admission of awareness. radare2's own OMF/CoreSymbolication plugins consistently useR_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.