Back to Blog
medium SEVERITY6 min read

How integer overflow in _MALLOC() happens in C emulator memory allocation and how to fix it

A critical integer overflow vulnerability was discovered in `i286c/i286c.c` at line 216, where the expression `_MALLOC(size + 16)` could wrap around to a tiny value when `size` approaches `UINT32_MAX`. This undersized allocation leads to a massive heap buffer overflow when the emulator writes the expected number of bytes. The fix adds a simple overflow guard that checks whether `size + 16` would wrap before performing the allocation.

O
By Orbis AppSec
Published June 19, 2026Reviewed June 19, 2026

Answer Summary

This is an integer overflow vulnerability (CWE-120) in C where `_MALLOC(size + 16)` in `i286c/i286c.c:216` wraps around when `size` is near `UINT32_MAX`, causing an undersized heap allocation and subsequent buffer overflow. The fix adds a ternary check `(size <= (UINT32)(~0) - 16)` before calling `_MALLOC`, returning `NULL` if the addition would overflow.

Vulnerability at a Glance

cweCWE-120
fixAdded overflow guard that returns NULL when `size > UINT32_MAX - 16`
riskHeap buffer overflow allows arbitrary memory corruption
languageC
root causeUnchecked arithmetic overflow in `size + 16` before `_MALLOC()` call
vulnerabilityInteger overflow leading to heap buffer overflow

How Integer Overflow in _MALLOC() Happens in C Emulator Memory Allocation and How to Fix It

Introduction

In the i286c/i286c.c file — part of an i286 CPU emulator's memory management subsystem — we discovered a medium-severity integer overflow vulnerability at line 216. The function i286c_setextsize() allocates extended memory for the emulated CPU using the expression _MALLOC(size + 16, "EXTMEM"). When the size parameter (a UINT32 derived from configuration) approaches UINT32_MAX (0xFFFFFFFF), adding 16 wraps the value around to as little as 15 bytes. The emulator then proceeds to write size bytes into this tiny allocation — a catastrophic heap buffer overflow.

This is the kind of bug that sits silently in codebases for years. The allocation "succeeds" (malloc happily returns a pointer to 15 bytes), no error is raised, and the subsequent memory operations corrupt the heap with potentially gigabytes of data flowing into a handful of bytes.

The Vulnerability Explained

Here's the vulnerable code in i286c/i286c.c, inside the i286c_setextsize() function:

void i286c_setextsize(UINT32 size) {
    // ... existing cleanup code ...
    if (size != 0) {
        extmem = (UINT8 *)_MALLOC(size + 16, "EXTMEM");
    }
    if (extmem != NULL) {
        CPU_EXTMEM = extmem;
        // ... writes 'size' bytes into extmem ...
    }
}

The problem is on the _MALLOC line. Let's trace through what happens with a malicious size value:

  1. Attacker sets size = 0xFFFFFFF0 (UINT32_MAX - 15)
  2. Arithmetic: size + 16 = 0xFFFFFFF0 + 0x10 = 0x100000000
  3. Truncation: Since this is a 32-bit unsigned integer, it wraps to 0x00000000
  4. Result: _MALLOC(0, "EXTMEM") — allocates either 0 bytes or a minimal block
  5. Exploitation: The emulator then writes ~4GB of data into this allocation

Even more dangerous is the case where size = 0xFFFFFFFF:
- size + 16 = 0xFFFFFFFF + 0x10 = 0x0000000F (wraps to 15)
- _MALLOC(15, "EXTMEM") succeeds, returning a valid 15-byte buffer
- The emulator writes 4,294,967,295 bytes into those 15 bytes

Attack Scenario

An attacker who can influence the emulator's memory size configuration — through a config file, command-line parameter, or import file — can set the extended memory size to a value near UINT32_MAX. The emulator will:

  1. Allocate a tiny buffer (due to overflow)
  2. Write massive amounts of data past the end of that buffer
  3. Corrupt heap metadata, potentially enabling arbitrary code execution
  4. At minimum, crash the application; at worst, achieve remote code execution

This is particularly dangerous because the size value comes from user-controlled configuration (the emulator memory size setting), making it directly exploitable without any complex setup.

The Fix

The fix adds an overflow guard using a ternary expression that checks whether size + 16 would exceed UINT32_MAX before performing the addition:

Before:

extmem = (UINT8 *)_MALLOC(size + 16, "EXTMEM");

After:

extmem = (size <= (UINT32)(~0) - 16) ? (UINT8 *)_MALLOC(size + 16, "EXTMEM") : NULL;

Let's break down this fix:

  • (UINT32)(~0) evaluates to 0xFFFFFFFF — the maximum value for a 32-bit unsigned integer
  • (UINT32)(~0) - 16 equals 0xFFFFFFEF — the largest size that can safely have 16 added
  • If size > 0xFFFFFFEF, the addition would overflow, so we return NULL instead
  • If size <= 0xFFFFFFEF, the addition is safe and we proceed with _MALLOC

The existing code already handles extmem == NULL gracefully (it checks if (extmem != NULL) before using the pointer), so setting extmem to NULL on overflow integrates cleanly with the existing error path.

This is elegant because it:
1. Requires only a single line change
2. Leverages existing NULL-check error handling
3. Has zero performance impact on valid inputs
4. Completely eliminates the overflow regardless of the size value

Prevention & Best Practices

1. Always Check Arithmetic Before Allocation

Any time you compute an allocation size, verify the arithmetic won't overflow:

// Pattern: safe allocation with overflow check
if (size > SIZE_MAX - padding) {
    return NULL;  // or handle error
}
void *ptr = malloc(size + padding);

2. Use Safe Arithmetic Helpers

Consider creating or using overflow-checking arithmetic functions:

// GCC/Clang built-in
size_t alloc_size;
if (__builtin_add_overflow(size, 16, &alloc_size)) {
    return NULL;
}
void *ptr = malloc(alloc_size);

3. Validate Configuration Inputs

Before values reach allocation code, validate them against reasonable bounds:

#define MAX_EXTMEM_SIZE (256 * 1024 * 1024)  // 256MB max
if (size > MAX_EXTMEM_SIZE) {
    log_error("Extended memory size exceeds maximum");
    return;
}

4. Enable Compiler Warnings

Use -ftrapv (GCC) or UBSan (-fsanitize=unsigned-integer-overflow) during testing to catch overflow at runtime.

5. Static Analysis

Tools like Coverity, PVS-Studio, and Semgrep can detect patterns where user-influenced arithmetic feeds into allocation functions.

Key Takeaways

  • Never add padding to a size value without checking for overflow first — the size + 16 pattern in i286c_setextsize() is a textbook example of how a simple addition becomes exploitable.
  • Configuration-derived values are attacker-controlled — the emulator memory size setting flows directly into the allocation, making this trivially exploitable.
  • Existing NULL checks provide a natural error path — the fix leverages the existing if (extmem != NULL) guard, requiring zero architectural changes.
  • (UINT32)(~0) - N is the canonical overflow threshold — memorize this pattern for any code that adds a constant to a user-influenced size before allocation.
  • A single line of code prevented a heap corruption vulnerability — security fixes don't need to be complex to be effective.

How Orbis AppSec Detected This

  • Source: User-controlled emulator memory size configuration parameter passed to i286c_setextsize()
  • Sink: _MALLOC(size + 16, "EXTMEM") in i286c/i286c.c:216
  • Missing control: No validation that size + 16 would not overflow a UINT32, allowing integer wraparound to produce an undersized allocation
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Added a ternary overflow guard that returns NULL when size > UINT32_MAX - 16, preventing the undersized allocation entirely

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability in i286c/i286c.c demonstrates how a seemingly innocent expression — size + 16 — can become a critical security flaw when the input isn't bounded. The fix is a single ternary expression that costs nothing at runtime but completely eliminates the integer overflow. When writing C code that performs arithmetic on sizes before allocation, always ask: "What happens when this value is near its maximum?" If the answer involves wraparound, you need a guard.

References

Frequently Asked Questions

What is an integer overflow in memory allocation?

An integer overflow in memory allocation occurs when arithmetic on a size value (like adding padding bytes) wraps around the maximum integer value, resulting in a much smaller allocation than intended. Subsequent writes using the original size corrupt heap memory.

How do you prevent integer overflow in C memory allocation?

Always validate that arithmetic on size values won't exceed the maximum representable value before passing the result to malloc(). Use checks like `if (size > SIZE_MAX - padding)` before computing `size + padding`.

What CWE is integer overflow leading to buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) covers this pattern, though CWE-190 (Integer Overflow or Wraparound) describes the root cause mechanism.

Is using unsigned integers enough to prevent integer overflow?

No. Unsigned integers still wrap around at their maximum value — they just do so with defined behavior in C. You must explicitly check for overflow before performing the arithmetic.

Can static analysis detect integer overflow in malloc?

Yes. Many static analysis tools and SAST scanners can detect patterns where user-influenced values are used in arithmetic expressions passed to allocation functions without overflow checks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #212

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.