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 via strcpy() happens in C zlib and how to fix it

A critical buffer overflow vulnerability was discovered in `general/libzlib/gzlib.c` where multiple `strcpy()` and `strcat()` calls operated without bounds checking. An attacker controlling file paths or error messages could overflow destination buffers, potentially achieving arbitrary code execution. The fix replaces these unsafe string operations with bounded `memcpy()` calls that respect pre-calculated buffer lengths.

critical

How buffer overflow in rcdevice.c request parser happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `src/main/io/rcdevice.c` at line 489, where the RC device request parser wrote incoming data into a fixed-size buffer without validating against the hard-coded maximum capacity `RCDEVICE_PROTOCOL_MAX_DATA_SIZE`. An attacker controlling the device's I/O data stream could overflow the buffer by sending a payload longer than `expectedDataLength`, potentially achieving arbitrary code execution. The fix adds a second bounds check against the

critical

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

A critical buffer overflow vulnerability was discovered in the `readline()` function of `mdbx_load.c`, where an `fgets()` call passed a size parameter exceeding the actual allocated buffer by one byte. This off-by-one error could allow an attacker to trigger heap corruption by supplying oversized input via stdin, potentially leading to arbitrary code execution. The fix corrects the size parameter from `buf->iov_len + 1` to `buf->iov_len`, ensuring reads never exceed buffer boundaries.

critical

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

A critical heap buffer overflow vulnerability was discovered in the `dupstr()` function inside `tools/strliteral.c`, where `strcpy()` was called on a heap-allocated buffer without first verifying that `malloc()` had succeeded. If `malloc()` returned `NULL`, the subsequent `strcpy()` would write into address zero — corrupting memory and potentially enabling arbitrary code execution. The fix replaces the unsafe `strcpy()` call with a `NULL` check followed by a bounds-safe `memcpy()`, closing the v

medium

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

A stack buffer overflow in `PMenu_Do_Update()` within `src/menu/menu.c` allowed repeated unchecked `sprintf()` calls to overflow a fixed 1400-byte stack buffer when menu entries contained long text strings. The fix replaces every `sprintf(string + strlen(string), ...)` call with `snprintf(string + len, sizeof(string) - len, ...)`, tracking remaining buffer space and preventing writes beyond the buffer boundary.

critical

How out-of-bounds array access happens in C kernel modules and how to fix it

A high-severity out-of-bounds array access vulnerability was discovered in the natflow_conntrack.c kernel module where the `ct->proto.tcp.state` value was used directly as an array index without bounds validation. An attacker capable of manipulating TCP connection state could trigger reads beyond the `tcp_conntrack_names[]` array, potentially leaking kernel memory or causing system crashes. The fix adds a simple bounds check using `ARRAY_SIZE()` before array access.