Critical Integer Overflow in C: How a Simple Multiplication Almost Caused Heap Corruption
Introduction
At first glance, the line of code that caused this vulnerability looks completely harmless:
new_size = new_cap * sizeof(char *);
One multiplication. No pointers dereferenced, no user input directly involved, no obvious footgun. Yet on a 32-bit Android system, this single expression could be weaponized by an attacker to corrupt the heap, potentially enabling arbitrary code execution — all by feeding the application a carefully crafted session file.
This is the story of CWE-190: Integer Overflow or Wraparound, one of the oldest and most persistently dangerous vulnerability classes in systems programming. It was patched in src/asb_governor.c, and understanding why it matters is essential for any developer writing C, C++, or any language that exposes raw memory operations.
The Vulnerability Explained
Background: Dynamic Arrays in C
The vulnerable code implements a classic dynamic array (also called a growable buffer). The pattern is familiar to any C programmer:
- Start with a small initial capacity (e.g., 4 slots).
- When the array fills up, double the capacity.
- Call
reallocto resize the backing memory. - Continue filling the array.
This pattern is efficient and widely used. The problem arises at step 3, specifically in how the new allocation size is computed.
The Vulnerable Code
The vulnerable logic, located at src/asb_governor.c:478, looked something like this:
// VULNERABLE CODE — Do not use
size_t new_cap = old_cap * 2;
char **new_buf = realloc(buf, new_cap * sizeof(char *));
if (!new_buf) {
// handle allocation failure
}
buf = new_buf;
cap = new_cap;
The critical flaw: new_cap * sizeof(char *) is computed without any overflow check.
Why This Is Dangerous on 32-bit Systems
On a 32-bit Android system, size_t is a 32-bit unsigned integer, meaning it can hold values from 0 to 4,294,967,295 (2³² − 1). sizeof(char *) is 4 bytes on a 32-bit platform.
Now consider what happens when new_cap reaches a large value through repeated doublings. If new_cap is, say, 0x40000001 (1,073,741,825):
new_cap * sizeof(char *)
= 0x40000001 * 4
= 0x100000004
But 0x100000004 doesn't fit in a 32-bit size_t. It wraps around to 0x00000004 — just 4 bytes.
realloc is then called asking for a 4-byte buffer. It happily obliges. The code then proceeds to write hundreds of millions of pointers into that 4-byte allocation. This is heap corruption.
The Attack Scenario
The vulnerability is triggered by reading a crafted session file. Here's how an attacker could exploit it:
- Craft a malicious session file containing an extremely large number of lines — enough to push
new_cappast the overflow boundary through repeated doublings. - Deliver the file to the application (e.g., by replacing a session file on a rooted device, via a man-in-the-middle attack on session sync, or through any other file-delivery mechanism the application exposes).
- The application reads the file, doubling its buffer capacity with each reallocation.
- At the overflow boundary,
reallocreceives a wrapped-around size and returns a tiny allocation. - Subsequent writes go far beyond the allocated buffer, corrupting adjacent heap metadata and data.
- Heap corruption can be leveraged for a variety of follow-on attacks, including arbitrary code execution, privilege escalation, or denial of service.
The number of lines required to trigger this on a 32-bit system is large but not impractical — especially if the attacker can automate file generation and the application processes session files automatically in the background.
Real-World Impact
- Heap corruption is one of the most powerful primitives an attacker can have. Modern heap exploitation techniques (House of Force, tcache poisoning, etc.) can turn heap corruption into arbitrary write, and from there into code execution.
- On Android, this could mean a malicious app or a compromised sync service gains the privileges of the vulnerable process.
- The vulnerability is silent — no crash, no error log, just quietly corrupted memory that may not manifest until much later, making it extremely hard to diagnose without a dedicated security review.
The Fix
What Changed
The fix adds an explicit integer overflow check before the realloc call. The corrected logic looks like this:
// SAFE CODE — After the fix
size_t new_cap = old_cap * 2;
// Check for integer overflow before computing allocation size
if (new_cap == 0 || new_cap > SIZE_MAX / sizeof(char *)) {
// Overflow would occur — abort safely
handle_error("Integer overflow in buffer capacity calculation");
return ERROR_OVERFLOW;
}
size_t alloc_size = new_cap * sizeof(char *);
char **new_buf = realloc(buf, alloc_size);
if (!new_buf) {
handle_error("Memory allocation failed");
return ERROR_NOMEM;
}
buf = new_buf;
cap = new_cap;
How the Fix Works
The key insight is the guard condition:
if (new_cap > SIZE_MAX / sizeof(char *))
This checks whether new_cap * sizeof(char *) would exceed SIZE_MAX before performing the multiplication. This is the standard safe pattern for detecting integer overflow in C:
Instead of checking after the multiplication (too late — the damage is done), divide the maximum safe value by one operand and compare it to the other.
If new_cap is greater than SIZE_MAX / sizeof(char *), then the multiplication would overflow, and the code returns an error instead of proceeding with a dangerously undersized allocation.
Additionally, the fix checks new_cap == 0, which guards against the edge case where old_cap itself was already at the maximum and doubling wrapped to zero.
Before vs. After at a Glance
| Aspect | Before (Vulnerable) | After (Fixed) |
|---|---|---|
| Overflow check | ❌ None | ✅ Pre-multiplication guard |
| Allocation size | Could wrap to tiny value | Always proportional to new_cap |
| Error handling | Silent heap corruption | Explicit error return |
| 32-bit safety | ❌ Unsafe | ✅ Safe |
Prevention & Best Practices
1. Always Validate Before Multiplying for Allocation Sizes
The golden rule for allocation size arithmetic in C:
// UNSAFE
size_t alloc = count * element_size;
void *buf = malloc(alloc);
// SAFE
if (count > SIZE_MAX / element_size) {
// overflow would occur
return ERROR;
}
size_t alloc = count * element_size;
void *buf = malloc(alloc);
This pattern should be second nature whenever you compute an allocation size from a variable count.
2. Use Safe Integer Libraries
For C/C++ projects, consider using established safe integer libraries:
- SafeInt (Microsoft) — C++ template library for safe integer operations.
- CERT's
intsafe.h(Windows) — Windows-specific safe integer functions. __builtin_mul_overflow(GCC/Clang) — Compiler built-in for detecting overflow:
size_t alloc_size;
if (__builtin_mul_overflow(new_cap, sizeof(char *), &alloc_size)) {
// overflow detected
return ERROR;
}
void *new_buf = realloc(buf, alloc_size);
3. Impose Reasonable Upper Bounds
Even with overflow checks, it's good practice to cap the maximum capacity at a sensible limit:
#define MAX_SESSION_LINES 10000000UL // 10 million lines is already extreme
if (new_cap > MAX_SESSION_LINES) {
return ERROR_TOO_LARGE;
}
This provides defense-in-depth: even if the overflow check were somehow bypassed, the size limit prevents runaway memory consumption.
4. Enable Compiler Sanitizers During Development
The UndefinedBehaviorSanitizer (UBSan) can catch integer overflows at runtime during testing:
# Compile with UBSan
clang -fsanitize=undefined,integer src/asb_governor.c -o asb_governor_test
# Or with GCC
gcc -fsanitize=undefined src/asb_governor.c -o asb_governor_test
UBSan will print a detailed error and abort the program the moment an integer overflow occurs, making these bugs trivial to find in testing.
5. Use Static Analysis Tools
Several static analysis tools can detect this class of vulnerability before code even runs:
- Coverity — Detects integer overflow in allocation contexts.
- CodeQL — GitHub's semantic code analysis engine with queries for CWE-190.
- Clang Static Analyzer — Free, built into LLVM.
- Flawfinder — Lightweight C/C++ security scanner.
6. Write Regression Tests for Boundary Conditions
The regression test suite included with this fix is an excellent model. It tests:
- Power-of-2 boundaries (1, 2, 4, 8, 16, ..., 131072) — exactly where capacity doublings occur.
- Near-overflow values (
2^31 - 1,2^32 - 1,2^63 - 1). - Adversarial content (path traversal strings, format strings, SQL injection payloads, null bytes).
- Empty and degenerate inputs (empty string, newlines only, single line).
This kind of parametric boundary testing is exactly what's needed to catch integer overflow bugs before they reach production.
7. Know the Relevant Standards
This vulnerability maps to well-documented security standards:
- CWE-190: Integer Overflow or Wraparound — The canonical classification for this bug class.
- CERT C Coding Standard INT30-C — "Ensure that unsigned integer operations do not wrap."
- CERT C Coding Standard MEM07-C — Specifically addresses allocation size overflow.
- OWASP: Integer Overflow — OWASP's treatment of the vulnerability class.
Conclusion
The vulnerability fixed in this PR is a textbook example of why integer arithmetic in C demands explicit, defensive validation — especially when the result feeds into a memory allocation. The code was doing everything else right: using realloc correctly, doubling capacity efficiently, handling allocation failures. But one missing overflow check was enough to turn a routine buffer resize into a potential code execution primitive on 32-bit systems.
The key takeaways for developers:
- Never trust multiplication for allocation sizes without an overflow pre-check — use the
if (n > SIZE_MAX / size)pattern or compiler built-ins. - 32-bit platforms are still real — embedded systems, older Android devices, and IoT hardware are 32-bit, and overflow boundaries are much easier to reach there.
- Heap corruption is serious — it's not "just a crash." In the hands of a skilled attacker, heap corruption is often a path to arbitrary code execution.
- Sanitizers and static analysis are your friends — UBSan, CodeQL, and Coverity can catch these bugs automatically during development.
- Regression tests at boundaries matter — the test suite accompanying this fix is a model worth emulating: test power-of-2 boundaries, near-overflow values, and adversarial inputs systematically.
Security bugs like this one are a reminder that in systems programming, the most dangerous vulnerabilities often hide in the most ordinary-looking code. A disciplined habit of overflow-checking every allocation computation is a small investment with an enormous security payoff.
This vulnerability was identified and patched as part of an automated security review. The fix was verified by build, scanner re-scan, and LLM code review. The accompanying regression test suite guards against future regressions of this vulnerability class.