Back to Blog
critical SEVERITY6 min read

Critical Integer Sign Bug in runtime_malloc(): How a Missing Check Enables Heap Corruption

A critical vulnerability in `runtime/zenith_runtime.c` allowed the `runtime_malloc()` function to accept negative size values, which when cast to an unsigned type could either trigger a massive failed allocation or produce a dangerously undersized buffer ripe for overflow. The fix adds a simple but essential guard clause that rejects non-positive sizes before they ever reach `malloc()`. Left unpatched, this class of bug can lead to heap metadata corruption, process crashes, or even arbitrary cod

O
By Orbis AppSec
Published May 28, 2026Reviewed June 3, 2026

Answer Summary

This is a critical integer sign vulnerability (CWE-195: Signed to Unsigned Conversion Error) in the C function `runtime_malloc()` inside `runtime/zenith_runtime.c`. When a caller passes a negative `int` size, C's implicit conversion to `size_t` (an unsigned type) produces a massive value — either causing a failed allocation or, in edge cases, returning a tiny buffer that overflows on first use. The fix is a single guard clause added before the `malloc()` call: `if (size <= 0) return NULL;`. This prevents any non-positive size from reaching the allocator, eliminating the heap corruption risk entirely.

Vulnerability at a Glance

cweCWE-195
fixAdded a guard clause `if (size <= 0) return NULL;` before the malloc() call in runtime_malloc()
riskHeap corruption, process crash, or arbitrary code execution via undersized buffer
languageC
root causeruntime_malloc() accepted a signed int size parameter without validating it was positive before passing it to malloc()
vulnerabilityInteger Sign Error / Signed-to-Unsigned Conversion

Introduction

There is a category of security bug that looks almost embarrassingly small on paper — a single missing if statement — yet carries consequences that can range from a denial-of-service crash all the way to full process compromise. The vulnerability patched in this post belongs squarely in that category.

The culprit is runtime_malloc(), a thin wrapper around the standard C malloc() function in runtime/zenith_runtime.c. The function accepted a signed long parameter and cast it directly to the unsigned size_t type that malloc() expects — with no check to confirm the value was positive. That single omission opened the door to heap corruption.

If you write C or C++, maintain a runtime or plugin host, or simply want to understand why integer type mismatches are a perennial source of critical CVEs, read on.


The Vulnerability Explained

What Went Wrong

Here is the original function in its entirety:

// VULNERABLE — runtime/zenith_runtime.c (before fix)
void* runtime_malloc(long size) { return malloc((size_t)size); }

The problem lives in the implicit contract between long and size_t:

Type Signedness Typical range (64-bit)
long Signed −9,223,372,036,854,775,808 to +9,223,372,036,854,775,807
size_t Unsigned 0 to 18,446,744,073,709,551,615

When a negative long value is cast to size_t, C's unsigned integer wrap-around rules apply. The result is a very large positive number — often near the maximum value of the type.

Two Distinct Attack Paths

Path 1 — Enormous allocation → NULL dereference

Pass -1 as the size. After casting, size_t receives 0xFFFFFFFFFFFFFFFF (18.4 exabytes on a 64-bit system). malloc() cannot satisfy that request and returns NULL. Because the return value is never checked, any caller that immediately dereferences the pointer triggers undefined behavior — typically a segmentation fault and process crash.

// Attacker-controlled call
void* buf = runtime_malloc(-1);
memcpy(buf, data, len);  // CRASH: NULL dereference

Path 2 — Undersized allocation → heap buffer overflow

This path is subtler and more dangerous. A carefully chosen small negative value wraps around to a small positive size_t:

-65528 (long)    0xFFFFFFFFFFFF0008    but on many platforms
-65528 cast to size_t (64-bit) = 18446744073709486088

// However on 32-bit or with truncation scenarios:
(uint32_t)(long)-65528    0xFFFF0008  = 4294901768  still large

// The classic 16-bit-era variant (still relevant in embedded):
(uint16_t)(int16_t)-8    0xFFF8 = 65528

More practically, consider a plugin host that reads a size field from an untrusted message and forwards it to runtime_malloc(). An attacker submits a size of -8. After the cast, malloc(8) succeeds and returns a valid 8-byte buffer. The caller, trusting the original -8 value (or a derived expected-size), then writes far more data into that buffer — overflowing the heap allocation and corrupting adjacent heap metadata or live objects.

Heap metadata corruption is one of the most powerful primitives available to an attacker: it can be escalated to arbitrary read/write and, under the right conditions, to arbitrary code execution.

CWE Classification

This vulnerability maps to several Common Weakness Enumeration entries:

  • CWE-195: Signed to Unsigned Conversion Error
  • CWE-122: Heap-based Buffer Overflow (the downstream consequence)
  • CWE-476: NULL Pointer Dereference (the other downstream consequence)
  • CWE-190: Integer Overflow or Wraparound

The Fix

The patch is a single, focused guard clause inserted before the cast ever happens:

// BEFORE — vulnerable
void* runtime_malloc(long size) { return malloc((size_t)size); }

// AFTER — fixed
void* runtime_malloc(long size) { if (size <= 0) return NULL; return malloc((size_t)size); }

Why This Works

The guard if (size <= 0) return NULL enforces a precondition at the only entry point into the allocation path:

  1. Zero is rejected. A zero-byte allocation is technically implementation-defined in C (it may return a valid unique pointer or NULL). Returning NULL for zero is the safest and most predictable behavior for a runtime wrapper.
  2. Negative values are rejected. No negative long can ever reach the (size_t) cast, eliminating the wrap-around entirely.
  3. Callers already handle NULL. Any well-written caller of runtime_malloc() must already handle a NULL return (e.g., out-of-memory). Returning NULL for invalid sizes slots cleanly into that existing error path without requiring call-site changes.

A Note on Robustness

For a production runtime, you might go one step further and add an upper-bound sanity check:

// Defensive production version
#define RUNTIME_MAX_ALLOC (1UL << 30)  // 1 GiB — tune to your use case

void* runtime_malloc(long size) {
    if (size <= 0 || (unsigned long)size > RUNTIME_MAX_ALLOC) {
        return NULL;
    }
    return malloc((size_t)size);
}

This caps runaway allocations and makes the function's contract explicit in the code itself.


Prevention & Best Practices

1. Prefer Unsigned Types for Sizes at API Boundaries

If a parameter represents a memory size, it should almost never be signed. Use size_t or uint32_t/uint64_t from the start:

// Better API design — sign confusion is impossible
void* runtime_malloc(size_t size) {
    if (size == 0) return NULL;
    return malloc(size);
}

Callers with a long value are then forced to perform the conversion themselves, making the potential sign issue visible at the call site rather than hidden inside the wrapper.

2. Enable Compiler Warnings

Modern compilers can catch signed/unsigned mismatches at compile time:

# GCC / Clang
-Wall -Wextra -Wsign-conversion -Wconversion

# MSVC
/W4 /analyze

-Wsign-conversion in particular will flag implicit conversions between signed and unsigned integer types — exactly the class of bug seen here.

3. Use Static Analysis

Tools that would have flagged this before it shipped:

Tool How it helps
Clang Static Analyzer Detects signed-to-unsigned casts that can produce unexpected values
Coverity Dedicated checker for integer type conversion issues
CodeQL Query SignedToUnsignedCast for project-wide auditing
PVS-Studio V106 and related diagnostics for dangerous casts

4. Fuzz the Allocation Path

Any function that accepts an externally influenced size and calls malloc() is a prime fuzzing target. Add it to your fuzzing harness and include negative values, zero, and boundary values like LONG_MIN, -1, 0, 1, and LONG_MAX in your seed corpus.

// Example libFuzzer entry point
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    if (size < sizeof(long)) return 0;
    long alloc_size;
    memcpy(&alloc_size, data, sizeof(long));
    void* p = runtime_malloc(alloc_size);
    if (p) runtime_free(p);
    return 0;
}

5. Always Check malloc() Return Values

Even with the sign guard in place, malloc() can still return NULL on legitimate out-of-memory conditions. Every call site should handle this:

void* buf = runtime_malloc(requested_size);
if (buf == NULL) {
    // handle allocation failure gracefully
    return ERROR_OUT_OF_MEMORY;
}

6. Consult the Standards


Conclusion

The runtime_malloc() vulnerability is a textbook example of how a type mismatch between a signed input and an unsigned allocation function can cascade from "looks fine" to "critical severity" in a single step. The fix is minimal — one guard clause — but the protection it provides is substantial: it closes off both the NULL-dereference crash path and the heap buffer overflow path simultaneously.

Key takeaways for every C and C++ developer:

  • Match your types. If a value represents a memory size, use an unsigned type.
  • Validate before you cast. Never pass an externally influenced value through a sign-changing cast without first confirming it is in a safe range.
  • Turn on conversion warnings. Let the compiler do the first pass of sign-mismatch detection for free.
  • Fuzz your allocation wrappers. Negative and boundary values are exactly what attackers will try.

A single if (size <= 0) return NULL; prevented what could have been a heap corruption exploit. That is the power — and the responsibility — of secure coding at the systems level.

Frequently Asked Questions

What is an integer sign vulnerability in C?

An integer sign vulnerability occurs when a signed integer (which can be negative) is passed to a function expecting an unsigned value, like `size_t` for `malloc()`. In C, a negative `int` silently converts to a very large `size_t`, causing allocations to fail catastrophically or — in rare cases — succeed with a tiny buffer that overflows immediately.

How do you prevent signed-to-unsigned conversion errors in C?

Always validate that size arguments are positive before passing them to memory allocation functions. Add a guard clause such as `if (size <= 0) return NULL;` at the top of any allocation wrapper. Prefer using `size_t` as the parameter type for size arguments to make the intent explicit, and enable compiler warnings like `-Wsign-conversion`.

What CWE is integer sign vulnerability?

The primary CWE is CWE-195 (Signed to Unsigned Conversion Error), which is a child of CWE-681 (Incorrect Conversion Between Numeric Types). Related weaknesses include CWE-190 (Integer Overflow or Wraparound) and CWE-122 (Heap-based Buffer Overflow), which can result from the initial sign error.

Is using size_t as the parameter type enough to prevent this vulnerability?

Changing the parameter type to `size_t` helps by making it harder for callers to accidentally pass a negative value, but it is not sufficient on its own. A caller could still pass a zero, or cast a negative value explicitly. A defensive guard clause checking `size > 0` (or `size != 0` for `size_t`) should always be present inside the allocator wrapper itself.

Can static analysis detect integer sign vulnerabilities like this one?

Yes. Static analysis tools such as Semgrep, Coverity, Clang's `-fsanitize=signed-integer-overflow`, and AddressSanitizer can all flag signed-to-unsigned conversions in allocation contexts. Orbis AppSec automatically detected this exact pattern in `runtime_malloc()` and opened a remediation pull request without manual intervention.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #19

Related Articles

critical

How buffer overflow happens in C ieee80211_input() and how to fix it

A critical buffer overflow vulnerability was discovered in `src/firmware/src/net/ieee80211.c` at line 1584, where the `ieee80211_input()` function processed raw 802.11 data frames without verifying that the incoming frame was large enough to contain a valid `ieee80211_frame` header. An attacker within wireless range could craft undersized or malformed frames to trigger memory corruption, potentially leading to remote code execution on the firmware. The fix adds a single, targeted bounds check th

critical

How buffer overflow in FuzzIxml.c sprintf() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `fuzzer/FuzzIxml.c` where `sprintf()` wrote a PID-formatted filename into a fixed 256-byte stack buffer without any bounds checking. The fix replaces `sprintf()` with `snprintf()`, explicitly passing the buffer size to prevent any overflow. While exploitation in this specific fuzzer context requires local access, the pattern is a textbook example of CWE-120 that developers should recognize and eliminate everywhere it appears.

critical

How buffer overflow happens in C libficus.c sprintf() and how to fix it

A buffer overflow vulnerability was discovered in `runtime/ficus/impl/libficus.c` where `sprintf()` was used to write a formatted compiler version string into a fixed-size stack buffer without any bounds checking. The fix replaces both vulnerable `sprintf()` calls with `snprintf()`, passing `sizeof(cver)` as the maximum write length to ensure the buffer can never be overrun. This change eliminates the risk of stack memory corruption that could be triggered by an attacker with control over the bu

critical

How buffer overflow via strcpy() happens in C Kconfig parsing and how to fix it

A critical buffer overflow vulnerability was discovered in the Linux kernel's Kconfig build system where `strcpy()` copied user-controlled symbol values into a fixed-size buffer without bounds checking. This flaw in `scripts/kconfig/symbol.c` could allow attackers to overwrite adjacent memory when processing malicious Kconfig files. The fix replaces the unsafe `strcpy()` with `memcpy()` using explicit length calculations.

high

How integer overflow in malloc happens in C bipartite matching and how to fix it

A high-severity integer overflow vulnerability was discovered in the bipartite matching algorithm implementation where unchecked multiplication operations for memory allocation could wrap around, causing undersized buffer allocations and subsequent heap overflow. The fix replaces vulnerable `malloc(sizeof(int) * V)` patterns with safe `calloc(V, sizeof(int))` calls and adds proper bounds validation to prevent exploitation.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).