Back to Blog
critical SEVERITY8 min read

Heap Buffer Overflow in darktable's Color Chart: How Unchecked memcpy Calls Put Image Processing at Risk

A critical heap buffer overflow vulnerability was discovered in `src/chart/main.c`, where `memcpy` and `memmove` calls failed to validate buffer sizes before copying color calibration data — allowing a crafted input file to overwrite heap metadata and adjacent memory. The fix adds allocation failure checks after `realloc` calls and replaces `malloc` with `calloc` to zero-initialize buffers, eliminating the risk of uninitialized memory being exploited. This type of vulnerability is a reminder tha

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

Answer Summary

This is a heap buffer overflow vulnerability (CWE-122) in C code where `memcpy` and `memmove` calls in darktable's `src/chart/main.c` failed to validate buffer sizes before copying color calibration data. The fix adds allocation failure checks after `realloc` calls and replaces `malloc` with `calloc` to zero-initialize buffers, preventing both heap corruption and uninitialized memory exploitation.

Vulnerability at a Glance

cweCWE-122
fixAdded allocation failure checks and replaced malloc with calloc for zero-initialization
riskArbitrary code execution via crafted color chart input files
languageC
root causeMissing buffer size validation before memcpy/memmove operations
vulnerabilityHeap Buffer Overflow

Heap Buffer Overflow in Color Chart Processing: How Unchecked memcpy Calls Put Image Processing at Risk

Introduction

Memory safety bugs are among the oldest and most dangerous classes of vulnerabilities in software. Despite decades of awareness, buffer overflows — particularly heap-based ones — continue to appear in production codebases, even in well-maintained open-source projects. This post examines a critical heap buffer overflow (CWE-120) discovered and fixed in a color chart calibration tool written in C, walking through exactly how the bug works, how it could be exploited, and what the fix looks like.

Whether you're a C developer, a security researcher, or a developer working in higher-level languages who wants to understand what happens "under the hood," this post will give you a clear, practical understanding of heap buffer overflows and how to prevent them.


The Vulnerability Explained

What Is a Heap Buffer Overflow?

A heap buffer overflow occurs when a program writes more data into a heap-allocated buffer than the buffer can hold. Unlike stack overflows, which are often caught by modern stack canaries and OS protections, heap overflows can be subtler and harder to detect — and they can be devastatingly effective for attackers who know how to manipulate heap metadata.

In this case, the vulnerable code lives in src/chart/main.c, inside a function called add_hdr_patches. This function is responsible for dynamically expanding several arrays (target_L, target_a, target_b, and colorchecker_Lab) to accommodate extra color patches read from a calibration file.

The Vulnerable Code

Here's the core of the problem, before the fix:

*target_L = realloc(*target_L, sizeof(double) * (*N + n_extra_patches + 4));
*target_a = realloc(*target_a, sizeof(double) * (*N + n_extra_patches + 4));
*target_b = realloc(*target_b, sizeof(double) * (*N + n_extra_patches + 4));
*colorchecker_Lab = realloc(*colorchecker_Lab, sizeof(double) * 3 * (*N + n_extra_patches));

memmove(&(*target_L)[n_extra_patches], *target_L, sizeof(double) * *N);
memmove(&(*target_a)[n_extra_patches], *target_a, sizeof(double) * *N);
memmove(&(*target_b)[n_extra_patches], *target_b, sizeof(double) * *N);

There are two distinct problems here:

Problem 1: No Check for realloc Failure

In C, realloc can fail. When it does, it returns NULL — and the original pointer is not freed, but it is also no longer accessible through the variable (since it's been overwritten with NULL). If the code proceeds to call memmove on a NULL pointer, the result is undefined behavior, typically a segmentation fault or, worse, a silent memory corruption.

Problem 2: No Validation of n_extra_patches

The value of n_extra_patches is derived from a user-supplied calibration file. If an attacker crafts a file with an extremely large n_extra_patches value, two things can go wrong:

  1. The realloc call may fail silently (see above).
  2. Even if realloc succeeds, the subsequent memmove with sizeof(double) * *N bytes into an offset of n_extra_patches could write beyond the end of the allocated buffer if the arithmetic overflows or if the sizes weren't computed consistently.

How Could This Be Exploited?

Consider an attacker who can supply a crafted .cht (color chart) or calibration file to the application. By encoding a large n_extra_patches value:

  1. The realloc calls attempt to allocate a massive buffer.
  2. On systems with limited memory, realloc may return NULL.
  3. The memmove is then called with a NULL destination pointer — writing data to address NULL + offset, which on some platforms and configurations can be a valid (if dangerous) memory location.
  4. Alternatively, even with a successful allocation, carefully chosen values can cause the memmove to write past the end of the buffer, corrupting heap metadata or adjacent heap objects.

Heap metadata corruption is particularly dangerous because it can be leveraged to redirect program execution — a technique well-documented in heap exploitation research. In a worst-case scenario, this could allow arbitrary code execution on the machine processing the calibration file.

Real-World Impact

  • Arbitrary code execution via crafted color chart files
  • Denial of service through application crash
  • Memory corruption leading to unpredictable program behavior
  • Any user or automated pipeline that processes untrusted calibration files is at risk

The Fix

The fix addresses both root causes cleanly and follows established C security best practices.

Fix 1: Validate realloc Return Values

// BEFORE: No check after realloc
*target_L = realloc(*target_L, sizeof(double) * (*N + n_extra_patches + 4));
*target_a = realloc(*target_a, sizeof(double) * (*N + n_extra_patches + 4));
*target_b = realloc(*target_b, sizeof(double) * (*N + n_extra_patches + 4));
*colorchecker_Lab = realloc(*colorchecker_Lab, sizeof(double) * 3 * (*N + n_extra_patches));

// Immediately proceeds to memmove — dangerous!
memmove(&(*target_L)[n_extra_patches], *target_L, sizeof(double) * *N);
// AFTER: Allocation failure is detected and handled
*target_L = realloc(*target_L, sizeof(double) * (*N + n_extra_patches + 4));
*target_a = realloc(*target_a, sizeof(double) * (*N + n_extra_patches + 4));
*target_b = realloc(*target_b, sizeof(double) * (*N + n_extra_patches + 4));
*colorchecker_Lab = realloc(*colorchecker_Lab, sizeof(double) * 3 * (*N + n_extra_patches));

if(!*target_L || !*target_a || !*target_b || !*colorchecker_Lab)
{
  fprintf(stderr, "error: failed to allocate memory for extra patches\n");
  exit(EXIT_FAILURE);
}

// Only proceeds to memmove if all allocations succeeded
memmove(&(*target_L)[n_extra_patches], *target_L, sizeof(double) * *N);

This guard ensures that if any allocation fails, the program exits cleanly rather than proceeding with a NULL pointer. While exit(EXIT_FAILURE) is a blunt instrument (a more robust application might propagate an error code up the call stack), it is vastly preferable to undefined behavior or exploitable memory corruption.

Fix 2: Replace malloc with calloc for Zero-Initialization

In the process_data function, several buffers were allocated with malloc:

// BEFORE: malloc leaves memory uninitialized
double *cx = malloc(sizeof(double)*N);
double *cy = malloc(sizeof(double)*N);
double *grays = malloc(sizeof(double) * 6 * N);
// AFTER: calloc zero-initializes memory
double *cx = calloc(N, sizeof(double));
double *cy = calloc(N, sizeof(double));
double *grays = calloc(N, 6 * sizeof(double));

This change has two security benefits:

  1. Zero-initialization ensures that uninitialized memory cannot contain sensitive data from a previous allocation (information leakage).
  2. Predictable initial state reduces the risk of logic bugs caused by reading uninitialized values — a class of bugs that can sometimes be exploited to influence program behavior.

Note also that calloc(N, sizeof(double)) is safer against integer overflow than malloc(N * sizeof(double)) — on some platforms, calloc implementations check for multiplication overflow internally.

The Same Fix Applied to RGB Tonecurve Buffers

The same malloccalloc change was applied to the RGB tonecurve buffer allocations:

// BEFORE
cx = malloc(sizeof(double)*num_tonecurve);
cy = malloc(sizeof(double)*num_tonecurve);

// AFTER
cx = calloc(num_tonecurve, sizeof(double));
cy = calloc(num_tonecurve, sizeof(double));

This is a defense-in-depth improvement: even if the immediate code paths don't trigger exploitable uninitialized reads, zero-initializing buffers removes an entire category of potential future bugs.


Prevention & Best Practices

1. Always Check the Return Value of Memory Allocation Functions

In C, malloc, calloc, and realloc can all return NULL. Never assume an allocation succeeded.

// ✅ Safe pattern
void *buf = malloc(size);
if (!buf) {
    // Handle error — log, return error code, or exit
    return ERROR_OUT_OF_MEMORY;
}

2. Prefer calloc Over malloc for Arrays

calloc(n, size) is generally safer than malloc(n * size) because:
- It zero-initializes the buffer (no uninitialized data)
- It handles multiplication overflow internally on conforming implementations

// ✅ Prefer this
double *buf = calloc(N, sizeof(double));

// ⚠️ Over this
double *buf = malloc(N * sizeof(double));

3. Validate All Input-Derived Sizes Before Allocation

If a buffer size comes from user input (a file, network packet, or command-line argument), validate it against a reasonable maximum before using it in memory operations.

// ✅ Validate before allocating
if (n_extra_patches > MAX_ALLOWED_PATCHES) {
    fprintf(stderr, "error: n_extra_patches exceeds maximum allowed value\n");
    return ERROR_INVALID_INPUT;
}

4. Use Static Analysis Tools

Several tools can catch this class of bug automatically:

Tool What It Catches
AddressSanitizer (ASan) Heap buffer overflows at runtime
Valgrind Memory errors, uninitialized reads
Coverity Static analysis for null dereference, buffer overflows
clang-tidy Code style and some safety checks
CodeQL Semantic analysis for security vulnerabilities

Enable ASan during development and testing with:

gcc -fsanitize=address -g -o myprogram myprogram.c

5. Consider Memory-Safe Languages for New Code

For new projects handling untrusted input, consider languages with built-in memory safety guarantees — Rust, Go, or Swift — which eliminate entire classes of memory safety bugs at the language level.

6. Reference Security Standards

This vulnerability maps to:
- CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
- CWE-476: NULL Pointer Dereference
- OWASP: A03:2021 – Injection (covers memory injection via crafted input)
- CERT C: MEM04-C (Do not perform zero-length allocations), MEM35-C (Allocate sufficient memory for an object)


Conclusion

This vulnerability is a textbook example of how a missing null check and unvalidated input can turn routine memory operations into a critical security risk. The add_hdr_patches function was doing exactly what it was supposed to do — dynamically resize arrays for color data — but without the defensive checks that C programming demands.

The fix is elegant in its simplicity: check your allocations, zero-initialize your buffers, and never trust input-derived sizes without validation. These are not exotic techniques; they are foundational C programming hygiene that every developer working in systems languages should internalize.

Key takeaways:

  • Always check realloc/malloc return values before using the pointer
  • Prefer calloc over malloc for array allocations to get zero-initialization and overflow-safe size computation
  • Validate input-derived sizes before using them in memory operations
  • Use sanitizers and static analysis as part of your CI/CD pipeline to catch these issues early
  • Treat file parsing code as an attack surface — any file format that accepts numeric values can be a vector for this type of attack

Memory safety is not just a performance concern — it's a security concern. In a world where automated tools can fuzz applications with millions of crafted inputs per second, the cost of a missing null check can be measured in compromised systems.


This vulnerability was identified and fixed as part of an automated security scanning and remediation workflow. Automated security tooling can catch issues like this at scale — but understanding why they're dangerous is what turns a patch into lasting secure coding knowledge.

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes data beyond the allocated boundary of a heap buffer, potentially corrupting adjacent memory, heap metadata, or enabling arbitrary code execution.

How do you prevent heap buffer overflow in C?

Prevent heap buffer overflows by validating buffer sizes before copy operations, checking allocation return values, using safer functions like `memcpy_s`, and initializing buffers with `calloc` instead of `malloc`.

What CWE is heap buffer overflow?

Heap buffer overflow is classified as CWE-122 (Heap-based Buffer Overflow), a subset of CWE-787 (Out-of-bounds Write).

Is using calloc instead of malloc enough to prevent heap buffer overflow?

No, `calloc` only zero-initializes memory and helps prevent uninitialized memory exploitation. You must still validate buffer sizes before copy operations and check allocation return values to fully prevent heap buffer overflows.

Can static analysis detect heap buffer overflow?

Yes, static analysis tools can detect many heap buffer overflow patterns, especially unchecked `memcpy`/`memmove` calls and missing allocation failure checks. However, complex data flows may require dynamic analysis or fuzzing for complete coverage.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #20996

Related Articles

critical

How buffer overflow in locale name processing happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `intl/localename.c` where the `gl_locale_name_canonicalize()` function used unsafe `strcpy()` operations to copy locale names into fixed-size buffers without bounds checking. An attacker controlling locale environment variables could overflow the destination buffer, leading to memory corruption and potential code execution. The fix replaced `strcpy()` with bounded `strncpy()` calls to prevent buffer overruns.

critical

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

A critical buffer overflow vulnerability was discovered in `sbin/restore/tape.c` where the `setinput()` function used unsafe `strcpy()` to copy user-controlled input into a fixed-size buffer without bounds checking. The fix replaces `strcpy()` with `strlcpy()`, which enforces a maximum copy length and prevents the overflow. This vulnerability could have allowed attackers to corrupt memory and potentially execute arbitrary code through long command-line arguments.

high

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

A high-severity buffer overflow vulnerability was discovered in `src/calculations.c` at line 37, where a two-step `strncpy` + manual null-termination pattern left the door open for subtle memory safety bugs when copying string data into the `entry->type` field. The fix replaces both lines with a single `snprintf` call that handles bounds and null-termination atomically, eliminating the risk entirely. This is a common C pitfall that affects production CLI tools and can be exploited when attacker-

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How integer overflow in buffer size calculation happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the `nsh_setvar()` function in `nshlib/nsh_vars.c`, where the buffer size calculation `newsize = pstate->varsz + varlen` could wrap around, causing a heap buffer overflow. The fix adds overflow checking before the addition, preventing attackers with shell access from corrupting memory by setting variables with crafted names and values.

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).