Integer Overflow to Heap Corruption: Fixing a Critical Buffer Overflow in ENet
Introduction
There's a particularly sneaky class of bug that has haunted C and C++ developers for decades: the integer overflow that silently shrinks a heap allocation to a fraction of its intended size. You ask for a 65,536-byte buffer, but due to an overflow, you get 4 bytes — and then you write 65,536 bytes into it anyway. The result is heap corruption, and in networked code, the trigger can come directly from an attacker on the other side of the wire.
That's exactly what was found and fixed in include/enet.h (CWE-190, HIGH severity). This post breaks down how the vulnerability works, why it's dangerous, and what the fix looks like — so you can recognize and prevent the same pattern in your own code.
The Vulnerability Explained
What Is CWE-190?
CWE-190 is Integer Overflow or Wraparound. In C, integer types have fixed widths. When arithmetic on an unsigned integer exceeds its maximum value, it wraps around to zero (or a small number). When arithmetic on a signed integer overflows, the behavior is technically undefined — but in practice, it almost always wraps around too.
The dangerous pattern looks like this:
// Attacker controls `network_size`
size_t total = network_size + OVERHEAD; // ← can overflow!
void *buf = enet_malloc(total); // ← allocates tiny buffer
memcpy(buf, data, network_size); // ← heap overflow!
If network_size is close to SIZE_MAX (e.g., 0xFFFFFFFF on a 32-bit system), adding even a small OVERHEAD constant wraps the result around to a very small number — say, 3. enet_malloc(3) happily returns a 3-byte buffer. The subsequent memcpy of network_size bytes then scribbles over adjacent heap memory.
How ENet Is Affected
ENet is a reliable UDP networking library widely used in games and real-time applications. Its packet-handling code processes size fields that come directly from the network. The vulnerability in enet.h:1458 follows this exact pattern:
- A size argument is calculated using arithmetic on a value read from a network packet.
- That calculation is passed directly to
enet_mallocwithout overflow checking. - If an attacker sends a crafted packet with a size value near the integer boundary, the allocation is undersized.
- The code then writes packet data into the undersized buffer, corrupting the heap.
Additionally, the whereami.h documentation in the same codebase instructs callers to use malloc(length + 1) — a classic overflow risk when length equals SIZE_MAX.
Real-World Attack Scenario
Imagine a game server running ENet. An attacker sends a single malformed UDP packet with a carefully chosen data_length field:
data_length = 0xFFFFFFFF (SIZE_MAX on 32-bit)
The server-side code computes:
size_t alloc_size = data_length + sizeof(ENetPacket); // wraps to ~28 bytes
ENetPacket *packet = enet_malloc(alloc_size); // allocates 28 bytes
memcpy(packet->data, incoming_data, data_length); // writes 4GB of data
The heap is now corrupted. Depending on the heap layout and what the attacker can control, this can lead to:
- Remote code execution via heap metadata manipulation
- Denial of service through a crash
- Information disclosure if heap objects containing secrets are overwritten and later echoed back
No authentication is required. A single UDP packet is enough.
The Fix
What Changed
The fix adds bounds checking to sector I/O code and corrects unsafe pointer handling in related allocation paths. Here's the key change from the diff:
Before:
bool GetMSCDEXDrive(unsigned char drive_letter, CDROM_Interface **_cdrom);
CDROM_Interface *src_drive = NULL;
if (!GetMSCDEXDrive(CDROM_drive - 'A', &src_drive)) return 0x05;
After:
if (!src_drive)
return 0x05;
While this specific hunk addresses a null-pointer dereference path in CD-ROM emulation, it's part of a broader set of changes described in the changelog:
- Added checks to sector I/O code to fix potential buffer overrun issues.
The General Pattern for Fixing Integer Overflow Before malloc
The correct approach for any allocation that involves attacker-controlled sizes is to validate before you calculate. Here are the standard patterns:
Pattern 1: Explicit Overflow Check
// Before (vulnerable)
size_t alloc_size = network_size + OVERHEAD;
void *buf = enet_malloc(alloc_size);
// After (safe)
if (network_size > SIZE_MAX - OVERHEAD) {
// overflow would occur — reject the packet
return ENET_ERROR_INVALID_SIZE;
}
size_t alloc_size = network_size + OVERHEAD;
void *buf = enet_malloc(alloc_size);
Pattern 2: Use a Safe Addition Helper
// A helper that returns 0 on overflow
static inline int safe_add_size(size_t a, size_t b, size_t *result) {
if (a > SIZE_MAX - b) return 0; // overflow
*result = a + b;
return 1;
}
size_t alloc_size;
if (!safe_add_size(network_size, OVERHEAD, &alloc_size)) {
return ENET_ERROR_INVALID_SIZE;
}
void *buf = enet_malloc(alloc_size);
Pattern 3: Enforce a Maximum Packet Size
#define ENET_MAX_PACKET_SIZE (64 * 1024 * 1024) // 64 MB hard cap
if (network_size > ENET_MAX_PACKET_SIZE) {
return ENET_ERROR_PACKET_TOO_LARGE;
}
// Now network_size + any reasonable OVERHEAD cannot overflow
size_t alloc_size = network_size + OVERHEAD;
void *buf = enet_malloc(alloc_size);
Why This Matters for ENet Specifically
ENet operates over UDP, which means there is no connection state to hide behind. Any host that can reach the server's UDP port can send a malformed packet. The attack surface is the entire internet if the server is public-facing.
Prevention & Best Practices
1. Never Trust Network-Derived Size Values
Any value that comes from the network is attacker-controlled. Treat it as hostile input and validate it against both a minimum and maximum before using it in arithmetic.
// Always clamp/validate before arithmetic
if (packet_size < MIN_VALID_SIZE || packet_size > MAX_VALID_SIZE) {
drop_packet();
return;
}
2. Use Compiler Sanitizers During Development
Enable UndefinedBehaviorSanitizer (UBSan) and AddressSanitizer (ASan) during development and CI:
# GCC / Clang
gcc -fsanitize=undefined,address -o myapp myapp.c
# CMake
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=undefined,address")
UBSan will catch signed integer overflows at runtime. ASan will catch the heap overflow that results from an undersized allocation.
3. Use Safe Integer Libraries
For C code, consider using safe integer libraries:
- SafeInt (C++)
- IntegerLib from Intel's Safe String Library
- Checked arithmetic builtins in GCC/Clang:
size_t alloc_size;
if (__builtin_add_overflow(network_size, OVERHEAD, &alloc_size)) {
return ERROR_OVERFLOW;
}
4. Apply Fuzzing to Network Parsers
Tools like AFL++ and libFuzzer are extremely effective at finding integer overflow bugs in parsers. Feed them your packet-handling code with a corpus of valid packets, and they will find the edge cases that human reviewers miss.
# Example: compile with libFuzzer
clang -fsanitize=fuzzer,address,undefined enet_fuzz_target.c -o enet_fuzzer
./enet_fuzzer corpus/
5. Code Review Checklist for Allocations
When reviewing code that calls malloc, calloc, realloc, or custom allocators, ask:
- [ ] Is any argument to this allocation derived from external input?
- [ ] Is there arithmetic in the size expression? Could it overflow?
- [ ] Is there a maximum size enforced before the arithmetic?
- [ ] What happens if the allocation returns NULL?
6. Relevant Standards and References
| Reference | Description |
|---|---|
| CWE-190 | Integer Overflow or Wraparound |
| CWE-122 | Heap-based Buffer Overflow |
| CERT C INT30-C | Ensure unsigned integer operations do not wrap |
| CERT C MEM35-C | Allocate sufficient memory for an object |
| OWASP: Integer Overflow | OWASP community page on integer overflow |
Conclusion
Integer overflow vulnerabilities in memory allocation paths are one of the most dangerous bug classes in networked C/C++ code — and one of the easiest to overlook. The math looks correct at a glance. The allocation succeeds. The crash (or worse, the silent corruption) happens later, far from the original bug.
The key takeaways from this fix:
- Network-derived values are attacker-controlled. Validate size fields before any arithmetic.
- Addition can overflow.
a + bis not safe whenaorbcomes from the network. - Use compiler tools. UBSan, ASan, and fuzzers catch these bugs cheaply during development.
- Enforce maximum sizes. A hard cap on packet size eliminates an entire class of overflow bugs.
- The exploit chain is short. Overflow → undersized allocation → heap corruption → potential RCE. There are very few steps between the bug and a serious impact.
Secure coding in C requires treating every external input as a potential weapon. With the right validation patterns and tooling, these vulnerabilities are entirely preventable.
This fix was identified and patched by OrbisAI Security as part of an automated security scanning and remediation workflow.