Heap Corruption in Dynamic App Loaders: How Unvalidated Binary Size Fields Open the Door to Memory Attacks
Introduction
When a system loads executable code at runtime — think plugin systems, firmware updaters, or embedded app frameworks — it must parse metadata from the binary itself to know how much memory to allocate and how much data to copy. This is a powerful capability, but it comes with a sharp edge: what happens when that metadata is wrong, or worse, deliberately crafted by an attacker?
This post walks through a critical heap corruption vulnerability found in a dynamic application loader written in C, explains exactly how it could be exploited, and breaks down the code changes that fixed it. Whether you write embedded firmware, OS-level loaders, or any C code that processes external data, the lessons here apply directly to your work.
The Vulnerability Explained
What Went Wrong
The vulnerability lives in component/dynamic_app/app_loader/app_loader.c, a component responsible for loading application binaries into memory at runtime. To do its job, the loader reads a binary image (the "tinf image") and extracts metadata fields from it — things like data_size, rel_data_size, got_entries, and bss_size.
The critical mistake: these size values were read directly from the untrusted binary and used immediately in memory operations without any validation.
Here's the problematic pattern that appeared in two separate places:
// BEFORE: No validation — apps_info_size is used blindly
memset(apps_info, 0x0, apps_info_size);
memcpy(apps_info, app_data_base + 1, apps_info_size);
And separately, when calculating how much heap memory to allocate:
// BEFORE: Integer overflow risk, then unsafe allocation
uint32_t app_data_size = tinf->rel_data_size + tinf->data_size +
tinf->got_entries + tinf->bss_size;
StackType_t *app_rel_data_base = malloc((app_data_size + DEFAULT_STACK_SIZE) * 4);
memset(app_rel_data_base, 0x0, (app_data_size + DEFAULT_STACK_SIZE) * 4);
There are actually two distinct vulnerabilities here working in combination:
Vulnerability 1: Missing Bounds Check Before memcpy
The apps_info_size parameter represents the size of a destination buffer. The tinf->data_size field comes from the binary being loaded. If apps_info_size is larger than what tinf->data_size actually covers, the memcpy will read beyond the valid source region — or the memset will zero out memory beyond the intended destination buffer.
An attacker can craft a binary where data_size is set to a small value while the loader's apps_info_size is large, causing the copy to walk off the end of the source data into adjacent heap memory.
Vulnerability 2: Integer Overflow in Size Calculation
The second issue is more subtle but equally dangerous:
uint32_t app_data_size = tinf->rel_data_size + tinf->data_size +
tinf->got_entries + tinf->bss_size;
// Then later:
malloc((app_data_size + DEFAULT_STACK_SIZE) * 4);
If an attacker sets the individual size fields in the binary to large values, their sum can wrap around to zero (or a small number) due to 32-bit unsigned integer overflow. The malloc then allocates a tiny buffer, but the subsequent memset uses the original (large) computed size — writing far beyond the end of the allocated region.
This is a classic integer overflow leading to heap buffer overflow.
Real-World Impact
On an embedded system (which this loader targets, given the use of FreeRTOS primitives like StackType_t and xTaskCreate), heap corruption can lead to:
- Arbitrary code execution — overwriting function pointers or vtables stored on the heap
- Privilege escalation — corrupting security-sensitive data structures
- Denial of service — crashing the device or causing undefined behavior
- Persistent compromise — on devices with persistent storage, a corrupted heap can persist across reboots
The attack surface is anyone who can supply an app binary to the loader. Depending on the system, this might be a firmware update mechanism, a removable storage device, or a network-delivered payload.
Attack Scenario
Imagine an IoT device that supports third-party "apps" loaded from an SD card. An attacker places a crafted binary on the SD card with the following fields set maliciously:
tinf->data_size = 0x00000001 // Tiny — only 1 word of data
tinf->rel_data_size = 0x3FFFFFFF // Huge
tinf->got_entries = 0x3FFFFFFF // Huge
tinf->bss_size = 0x00000002 // Small
The sum rel_data_size + data_size + got_entries + bss_size overflows a 32-bit unsigned integer, wrapping to a small value. malloc allocates, say, 16 bytes. The subsequent memset then zeros out gigabytes of address space (or until it segfaults/corrupts critical data). Game over.
The Fix
The patch addresses both vulnerabilities with targeted, minimal changes that are easy to audit.
Fix 1: Bounds Validation Before memcpy
// AFTER: Validate that apps_info_size fits within the binary's data region
if (apps_info_size > (int)(tinf->data_size * sizeof(uint32_t) - sizeof(uint32_t))) {
RTK_LOGE(TAG, "App data_size too small for apps_info!\n");
return APP_INVALID;
}
memset(apps_info, 0x0, apps_info_size);
memcpy(apps_info, app_data_base + 1, apps_info_size);
This check ensures that before any memory operation occurs, the requested apps_info_size is consistent with what the binary claims to contain. If the binary's data_size is too small to hold the data we're about to copy, we reject the binary immediately with APP_INVALID. No memory operation happens on untrusted sizes.
Fix 2: Integer Overflow Detection
// AFTER: Detect overflow before it can cause harm
uint32_t app_data_size = tinf->rel_data_size + tinf->data_size +
tinf->got_entries + tinf->bss_size;
uint32_t alloc_words = app_data_size + DEFAULT_STACK_SIZE;
if (alloc_words < app_data_size) { // Overflow check
RTK_LOGE(TAG, "App data size overflow!\n");
return APP_INVALID;
}
This is a classic and elegant technique for detecting unsigned integer overflow in C: after the addition, check if the result is smaller than one of the operands. If alloc_words < app_data_size, the addition wrapped around, and we know an overflow occurred. The binary is rejected before any allocation happens.
Fix 3: calloc Instead of malloc + memset
// BEFORE:
StackType_t *app_rel_data_base = malloc((app_data_size + DEFAULT_STACK_SIZE) * 4);
memset(app_rel_data_base, 0x0, (app_data_size + DEFAULT_STACK_SIZE) * 4);
// AFTER:
StackType_t *app_rel_data_base = calloc(alloc_words, sizeof(uint32_t));
This change is elegant for two reasons:
-
callocis overflow-safe: Unlikemalloc(n * size),calloc(n, size)is required by the C standard to detect multiplication overflow and returnNULLif it occurs. This provides an additional safety net. -
Eliminates the redundant
memset:calloczero-initializes the allocation automatically. The separatememsetaftermallocwas not only redundant — it was using the same potentially-overflowed size value, making it dangerous. Removing it eliminates that risk entirely.
The Full Diff at a Glance
| Location | Before | After |
|---|---|---|
Pre-memcpy check |
None | Bounds validation against tinf->data_size |
| Allocation size | (app_data_size + DEFAULT_STACK_SIZE) * 4 |
calloc(alloc_words, sizeof(uint32_t)) with overflow check |
| Zero-initialization | Separate memset after malloc |
Built into calloc |
| Error handling | Silent overflow | Explicit APP_INVALID return with log message |
Prevention & Best Practices
This vulnerability is representative of a broad class of bugs that appear whenever code processes externally-supplied binary data. Here's how to avoid them:
1. Never Trust Size Fields From External Data
Any value read from a file, network packet, or external binary must be treated as attacker-controlled. Validate every size field against known constraints before using it in any memory operation.
// Pattern: Always validate before use
if (external_size > MAX_ALLOWED_SIZE || external_size > buffer_capacity) {
return ERROR_INVALID;
}
memcpy(dest, src, external_size); // Now safe
2. Check for Integer Overflow Before Arithmetic on Sizes
When combining size values, overflow is a real risk. Use the "result smaller than operand" trick for unsigned integers:
uint32_t total = a + b;
if (total < a) { /* overflow */ return ERROR; }
For more complex calculations, consider using compiler builtins:
// GCC/Clang built-in overflow detection
uint32_t result;
if (__builtin_add_overflow(a, b, &result)) {
return ERROR_OVERFLOW;
}
3. Prefer calloc Over malloc + memset
calloc(n, size) is safer than malloc(n * size) because:
- It handles multiplication overflow internally
- It zero-initializes, removing the need for a separate memset
- It removes one opportunity to use an overflowed size value
4. Validate Binary Formats With a Parser, Not Ad-Hoc Checks
For complex binary formats, consider writing a dedicated validation function that checks all fields before any processing begins. This "parse, then use" pattern keeps validation logic centralized and auditable.
// Better pattern: validate everything upfront
if (!validate_tinf_header(tinf, tinf_img_size)) {
return APP_INVALID;
}
// Now proceed with confidence
5. Use Static Analysis Tools
Tools that can catch these issues automatically:
- Coverity — detects integer overflow and buffer overflows in C/C++
- CodeQL — GitHub's semantic code analysis, excellent for taint tracking
- AddressSanitizer (ASan) — runtime detection of heap overflows during testing
- UBSan — detects undefined behavior including integer overflow at runtime
- Frama-C — formal verification for C, can prove absence of buffer overflows
6. Relevant Security Standards
This vulnerability maps to well-known weakness categories:
- CWE-122: Heap-based Buffer Overflow
- CWE-190: Integer Overflow or Wraparound
- CWE-20: Improper Input Validation
- OWASP: A03:2021 – Injection (binary injection is a form of this)
- CERT C: Rule INT30-C (Ensure unsigned integer operations do not wrap), Rule ARR38-C (Guarantee functions do not form invalid pointers)
Conclusion
This vulnerability is a textbook example of why input validation is not optional when processing external binary data. The original code was likely written with trusted inputs in mind — but in security, "trusted" is a dangerous assumption. Any code path that can be reached with attacker-supplied data must validate every field it uses.
The fix here is minimal, targeted, and effective:
- Bounds check before copy — ensures source data is large enough
- Overflow check before allocation — ensures the computed size is sane
- calloc instead of malloc + memset — eliminates an entire class of size-confusion bugs
The broader lesson: when your code loads, parses, or processes binary data from any external source — files, network, removable media — treat every field as hostile until proven otherwise. A few lines of validation code can be the difference between a secure system and a compromised one.
Found a similar pattern in your codebase? Audit every place where externally-supplied size values flow into
malloc,memcpy,memset, or similar functions. The effort is small; the payoff is enormous.
This vulnerability was identified and fixed using automated security scanning. The fix was verified by re-scan and LLM-assisted code review.