Back to Blog
critical SEVERITY9 min read

How out-of-bounds reads happen in C gettext .mo file parsers and how to fix it

A missing bounds check in the gettext `.mo` file parser inside `compose/asc-utils-l10n.c` allowed a malformed or truncated file to trigger out-of-bounds reads from heap memory. The vulnerability affected two distinct read sites — a `memcpy` of the full `AscLocaleGettextHeader` struct at line 131 and a 4-byte offset read at line 224 — neither of which validated that the source buffer was large enough. The fix adds explicit size checks before both reads, rejecting invalid files with a descriptive

O
By Orbis AppSec
Published June 18, 2026Reviewed June 18, 2026

Answer Summary

This is a heap out-of-bounds read vulnerability (CWE-125) in the C function `asc_l10n_parse_file_gettext()` inside `compose/asc-utils-l10n.c`. The root cause is that `memcpy(&h, data, sizeof(AscLocaleGettextHeader))` and a subsequent 4-byte offset read both execute without first verifying that `data_len` is large enough to satisfy the copy. The fix captures the buffer length via `g_bytes_get_data(bytes, &data_len)` and inserts an explicit `if (data_len < sizeof(AscLocaleGettextHeader))` guard — and a matching `if (m + 4 > len)` guard for the QM section parser — so that malformed input is rejected before any unsafe memory access occurs.

Vulnerability at a Glance

cweCWE-125
fixCapture `data_len` from `g_bytes_get_data()` and add `data_len < sizeof(AscLocaleGettextHeader)` and `m + 4 > len` guards before each unsafe read
riskHeap memory disclosure or crash when parsing a malformed gettext .mo or Qt .qm locale file
languageC
root cause`g_bytes_get_data()` was called without capturing the buffer length, so subsequent `memcpy` and offset reads had no upper bound to check against
vulnerabilityOut-of-bounds read via unchecked memcpy on attacker-controlled file data

How out-of-bounds reads happen in C gettext .mo file parsers and how to fix it

Summary

A missing bounds check in the gettext .mo file parser inside compose/asc-utils-l10n.c allowed a malformed or truncated file to trigger out-of-bounds reads from heap memory. The vulnerability affected two distinct read sites — a memcpy of the full AscLocaleGettextHeader struct at line 131 and a 4-byte offset read at line 224 — neither of which validated that the source buffer was large enough. The fix adds explicit size checks before both reads, rejecting invalid files with a descriptive error instead of reading past the end of allocated memory.


Introduction

The compose/asc-utils-l10n.c file is responsible for parsing localization data — specifically gettext .mo binary files and Qt .qm files — as part of the AppStream Compose toolchain. It is a natural target for malformed-input attacks because it processes files that can be supplied by third parties, packagers, or build pipelines. A flaw in asc_l10n_parse_file_gettext() created a situation where a single truncated .mo file could cause the parser to read heap memory it was never supposed to touch.

The problematic pattern is subtle and common in C: g_bytes_get_data() returns both a pointer and a length, but the length parameter is optional — you can pass NULL if you don't care about it. The original code did exactly that:

data = g_bytes_get_data (bytes, NULL);  // length silently discarded

From that point forward, every read from data was unbounded. The very next substantive operation was a memcpy that assumed the buffer was at least sizeof(AscLocaleGettextHeader) bytes long — an assumption that held for well-formed files but silently broke for truncated or crafted ones.


The Vulnerability Explained

What the parser does

When asc_l10n_parse_file_gettext() is called, it reads a .mo file into a GBytes buffer, then immediately copies the first sizeof(AscLocaleGettextHeader) bytes into a local struct h to inspect the magic number and determine byte order:

/* VULNERABLE — before the fix */
data = g_bytes_get_data (bytes, NULL);   // (1) length discarded

/* we only strictly need the header */
memcpy (&h, data, sizeof (AscLocaleGettextHeader));  // (2) no size check

If the file on disk is shorter than sizeof(AscLocaleGettextHeader) bytes — whether because it was truncated in transit, deliberately crafted by an attacker, or simply corrupt — the memcpy at step (2) reads past the end of the heap allocation backing bytes. The GLib GBytes allocator does not pad buffers, so adjacent heap metadata or unrelated allocations sit immediately after the buffer boundary.

A second vulnerable site existed in the Qt .qm section parser (asc_l10n_parse_file_qt):

/* VULNERABLE — before the fix */
AscLocaleQmSection section = _read_uint8 (data, &m);
guint32 section_len = _read_uint32 (data, &m);  // no check that m+4 <= len

Here, _read_uint32 advances the cursor m by 4 bytes. If m was already within 4 bytes of len, the read extended past the buffer boundary.

How an attacker exploits this

An attacker who can supply a .mo or .qm file to the AppStream Compose pipeline — for example, by contributing a malicious package to a distribution repository, or by intercepting a build artifact — can craft a file that is exactly 1 byte long. When asc_l10n_parse_file_gettext() processes it:

  1. g_bytes_get_data() returns a 1-byte heap allocation.
  2. memcpy(&h, data, sizeof(AscLocaleGettextHeader)) copies sizeof(AscLocaleGettextHeader) bytes (typically 28 bytes on a 64-bit system) starting from that 1-byte buffer.
  3. The 27 bytes past the buffer boundary are read from adjacent heap memory.

Depending on heap layout, those 27 bytes could contain fragments of other allocations — file paths, authentication tokens, or other sensitive strings processed earlier in the same compose run. In a crash scenario, the contents may appear in a core dump or error log. In a more targeted exploit, the attacker can use the magic-number branch (h.magic == 0x950412de) as an oracle to infer heap contents.

Real-world impact: The AppStream Compose tool runs as part of distribution metadata generation pipelines. A successful exploit could leak heap contents from the compose process, crash the pipeline, or — in combination with a write primitive — escalate to arbitrary code execution.


The Fix

The fix addresses both vulnerable sites with minimal, surgical changes.

Fix 1 — Capture the buffer length and guard the header memcpy

Before:

data = g_bytes_get_data (bytes, NULL);

/* we only strictly need the header */
memcpy (&h, data, sizeof (AscLocaleGettextHeader));

After:

gsize data_len = 0;
// ...
data = g_bytes_get_data (bytes, &data_len);  // length is now captured

/* we only strictly need the header */
if (data_len < sizeof (AscLocaleGettextHeader)) {
    g_set_error_literal (error,
                         ASC_COMPOSE_ERROR,
                         ASC_COMPOSE_ERROR_FAILED,
                         "Gettext file is too small to be valid");
    return FALSE;
}
memcpy (&h, data, sizeof (AscLocaleGettextHeader));

The key change is passing &data_len instead of NULL to g_bytes_get_data(). This one-character change unlocks all subsequent bounds checking. The guard immediately before memcpy ensures that if the file is shorter than the header struct, the function returns a descriptive GError rather than reading invalid memory. The memcpy itself is unchanged — it was never the problem; the missing precondition was.

Fix 2 — Guard the Qt section length read

Before:

AscLocaleQmSection section = _read_uint8 (data, &m);
guint32 section_len = _read_uint32 (data, &m);

After:

guint32 section_len;
AscLocaleQmSection section = _read_uint8 (data, &m);
if (m + 4 > len)
    break;
section_len = _read_uint32 (data, &m);

Note also that section_len was moved from a declaration-with-initializer inside the loop body to a plain declaration before the guard. This is necessary in C89/C90-compatible code where declarations must precede statements, but it also makes the control flow clearer: we declare the variable, check whether reading it is safe, and only then perform the read.

The break exits the section-parsing loop cleanly, allowing the caller to handle whatever translations were successfully parsed up to that point rather than crashing or returning garbage.

Why these two fixes are sufficient

Both vulnerable reads shared the same root cause: the buffer length was available (via g_bytes_get_data and the len variable respectively) but was not consulted before the read. The fix does not change the parsing logic — it simply inserts the missing precondition checks that should have been there from the start.


Key Takeaways

  • Passing NULL as the length parameter to g_bytes_get_data() is safe only if you never index into the returned pointer — in asc_l10n_parse_file_gettext(), the pointer was immediately used in a memcpy, making the discarded length a direct vulnerability.
  • Both the gettext .mo parser and the Qt .qm parser in asc-utils-l10n.c shared the same class of bug: a read that assumed the buffer was large enough without checking. Always audit all read sites in a parser when you find one vulnerability.
  • The fix required zero changes to the parsing logic — only the addition of &data_len in one function call and two if guards. Bounds checks are cheap; heap over-reads are not.
  • Binary locale files are attacker-controlled input in any system that processes third-party packages. Treat them with the same skepticism as network input.
  • The _read_uint32 helper advancing m without a bounds check is a pattern that can hide in many cursor-based parsers — audit every call site of such helpers when the cursor approaches the buffer end.

How Orbis AppSec Detected This

  • Source: A .mo or .qm locale file read from disk via asc_unit_read_data() in asc_l10n_parse_file_gettext() — an externally supplied binary file with no prior size validation.
  • Sink: memcpy (&h, data, sizeof (AscLocaleGettextHeader)) at compose/asc-utils-l10n.c:131, and _read_uint32 (data, &m) in the Qt section loop — both operating on a pointer whose backing buffer length was unknown at the call site.
  • Missing control: The length out-parameter of g_bytes_get_data() was passed as NULL, so no upper bound was available to check before either read. No guard existed between the data pointer acquisition and the first memory copy.
  • CWE: CWE-125 — Out-of-bounds Read
  • Fix: Passed &data_len to g_bytes_get_data() and inserted if (data_len < sizeof(AscLocaleGettextHeader)) and if (m + 4 > len) guards before the respective unsafe reads.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

Out-of-bounds reads in binary file parsers are among the most common and consequential memory-safety bugs in C codebases. The vulnerability in asc_l10n_parse_file_gettext() is a textbook example: a single optional parameter (NULL instead of &data_len) silently removed the only mechanism by which the code could have known it was about to read past the end of a heap buffer. The fix is three lines of guard code — but those three lines are the difference between a parser that crashes or leaks memory on malformed input and one that fails safely with a clear error message.

For developers writing binary format parsers in C, the lesson is clear: always capture buffer lengths, always check them before every read, and fuzz your parsers with truncated inputs before shipping. The cost of adding a bounds check is microseconds; the cost of shipping without one can be a heap disclosure in a production build pipeline.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #750

Related Articles

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

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.

critical

How buffer overflow in Intel SGX enclave ECALLs happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in Intel SGX enclave functions `ecall_encrypt_data` and `ecall_decrypt_data` in `backend/sgx/enclave/enclave.c`. The functions performed memory operations without validating that the provided buffer lengths matched the actual allocated buffer sizes, allowing an attacker controlling the untrusted application to trigger heap corruption within the secure enclave by passing oversized length parameters.

critical

How buffer overflow happens in C SGX enclave memcpy and how to fix it

A critical buffer overflow vulnerability was discovered in `backend/sgx/enclave/enclave.c` where the `ecall_store_data` function performed `memcpy` operations without proper bounds checking against the actual destination buffer size. An attacker could supply a malicious `data_len` parameter to overflow the enclave's secure storage buffer, potentially corrupting trusted execution environment memory. The fix replaces a hardcoded magic number check with a precise size comparison against the actual