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

high

How insecure-use-string-copy-fn happens in C and how to fix it

A high-severity vulnerability was identified in `plugin/bin/install.c` where `strcpy()` and `strncpy()` were used to handle path strings without proper bounds checking or guaranteed null-termination. The fix replaces `strcpy()` with direct character assignment and `strncpy()` with `snprintf()`, eliminating both buffer overflow and missing null-terminator risks in the plugin installation workflow.

critical

How NULL pointer dereference from unchecked malloc() happens in C and how to fix it

A critical memory safety vulnerability was discovered in `bench/tokenizer/tokenizer.c` where `malloc()` was called without checking its return value before passing the pointer to `memcpy()`. If allocation fails and `malloc()` returns NULL, the subsequent `memcpy()` writes to address zero, causing heap corruption or potential arbitrary code execution. The fix adds a single NULL check immediately after allocation, exiting cleanly on failure rather than proceeding with a dangerously invalid pointer

high

How insecure string copy functions happen in C and how to fix it

A high-severity buffer overflow vulnerability was discovered in `tools/claude-vscode-wrapper.c`, where an unbounded `strcpy()` call copied a file path into a fixed-size `MAX_PATH` buffer without any size validation. The fix replaces `strcpy()` with `snprintf()` and swaps `malloc()` for `calloc()`, ensuring both string operations and memory allocation are bounds-safe and zero-initialized.

critical

How Stack Buffer Overflows Happen in C with sprintf() and How to Fix Them

A critical stack buffer overflow was discovered in `libuv/Learn-libuv/docs/code/tty-gravity/main.c` where `sprintf()` wrote ANSI escape sequences and user-controlled variables into a fixed 500-byte buffer without any bounds checking. An attacker controlling the `pos`, `width`, or `message` variables could overflow the stack, overwrite return addresses, and potentially achieve arbitrary code execution. The fix replaces `sprintf()` with `snprintf()` and adds explicit length validation to ensure wr

high

How insecure string copy functions happen in C and how to fix them

A high-severity buffer overflow risk was discovered in `login/main.c` where `strcpy()` was used to copy the `HOME` environment variable into a fixed-size 512-byte buffer without any bounds checking. An attacker controlling the `HOME` environment variable could overflow `pwd_file_name`, potentially corrupting memory or hijacking execution. The fix replaces the two-step `strcpy`/`strcat` pattern with a single, bounds-safe `snprintf` call.

high

How c.lang.security.use-after-free.use-after-free happens in C and how to fix it

A use-after-free vulnerability was discovered in `ggml-alloc.c` where `galloc->leaf_allocs` could be referenced after being freed during graph memory reallocation. The fix nullifies the pointer immediately after `free()` and uses explicit `sizeof(struct leaf_alloc)` to prevent undefined behavior. This defensive hardening eliminates an exploit primitive in a speech-to-text processing pipeline.