GHSA-hgcj-22cg-4hfv: GUIX binary resource loader: unvalidated glyph range causes an integer underflow and a heap out-of-bounds write when loading an untrusted font or theme
GHSA ID |
|
|---|---|
CVE ID |
CVE‑2026‑91189 |
Severity |
High |
CVSS score |
7.0 ( |
CWE |
|
Published |
2026‑10‑02 |
Last updated |
2026‑10‑02 |
Description
Summary
The GUIX binary resource loader never checks that a font page’s glyph range is well formed. gx_page_header_first_glyph and gx_page_header_last_glyph are read verbatim from the file and only the upper bound of last_glyph is validated, so a page declaring first_glyph > last_glyph makes last - first + 1 underflow to a very large unsigned count.
That count is multiplied by a glyph record size to reserve the load buffer, and the multiply is unchecked, so it wraps to a small value and the loader allocates a buffer far smaller than the data it is about to write. The load pass then recomputes the count in a narrower type and writes attacker-controlled glyph records into the undersized buffer.
Details
The size pass and the load pass disagree about the type they count in, which is what turns an absurd span into a usable primitive. The size pass computes the record count in a wide type and wraps the reservation multiply; the load pass truncates the same count to USHORT and writes that many records into whatever was reserved.
On a 32-bit ARM EABI build sizeof(GX_GLYPH) is 12, and a span giving a count of 357913942 multiplies to 2^32 + 8 — so the reservation wraps to 8 and the theme buffer is allocated at 148 bytes, while the load pass truncates the count to 21846 records to write. The first record already lands out of bounds.
64-bit builds are self-limiting. With sizeof(GX_GLYPH) of 16, wrapping the reservation down to a small value requires a count near a multiple of 2^28, and any such count has its low 16 bits clear — so the USHORT truncation yields zero and no glyph is written. The defect is present on 64-bit but not exploitable by this route, which leaves GUIX’s 32-bit MCU deployment class as the exposed one.
Demonstrated reach versus upper bound. These describe the same input, not the same run, and are not interchangeable. The upper bound is 21846 records at 12 bytes each, or roughly 262 KB, which is what the load pass would write if every record carried a valid magic number. A 303-byte theme file writes only the records it can actually encode, because each write is preceded by a magic-number check that returns on failure. That is far short of the upper bound and still enough to reach a groomed neighbour, so the demonstrated control-flow hijack stands on the smaller figure.
Impact
A load-a-resource primitive that turns an untrusted font or theme file into controlled heap corruption. The overflow is contiguous, its length is attacker-influenced and its content comes from the file. GUIX allocates from a shared TX_BYTE_POOL with no redzones and no allocator hardening, so a groomed neighbour holding a function pointer is reachable; on the no-ASLR, execute-from-RAM MCUs GUIX targets, an attacker-chosen program-counter value is directly usable. A 303-byte theme file was shown to overwrite a groomed widget event-process function pointer and take control of it.
There is a lesser consequence that needs no grooming: a corrupted font’s gx_glyph_map is dereferenced at render time, giving an out-of-bounds read.
Affected configurations
GX_BINARY_RESOURCE_SUPPORT is opt-out rather than opt-in, so the loader is compiled in unless an integrator went out of their way to remove it.
Reaching the wrap additionally requires the extended-Unicode configuration, because the page header fields are typed GX_CHAR_CODE, which is only 32 bits wide under GX_EXTENDED_UNICODE_SUPPORT; otherwise it is 16 or 8 bits and the span cannot grow large enough to wrap the multiply. In practice that means builds carrying CJK or emoji coverage.
Every GUIX release up to and including 6.5.1.202602a.
Severity
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H — 7.0, High.
AC:H reflects the heap grooming a control-flow hijack needs, not the corruption itself, which is deterministic. UI:R reflects that the resource has to be loaded.
Patches
Fixed in GUIX 6.5.2.202603. The glyph range is validated where the page header is parsed, rejecting first_glyph > last_glyph; both passes read page headers through _gx_binres_page_header_load(), so a malformed range reaches neither and both public entry points are covered without either being touched. All three reservation multiplies are now checked rather than only the accumulation. Both passes count in UINT, removing the divergence the primitive depends on.
The span is deliberately not capped: one wider than 0xFFFF is well formed, and glyph drawing indexes a page through a GX_CHAR_CODE wide enough to reach it, so a cap would refuse a legitimate font.
The reporter’s regression inputs are covered in-tree by three tests, one per font configuration, because the build configurations compile disjoint sets of glyph formats and no single one compiles them all. They drive the load pass as well as the size pass, so the writers are exercised and not only the arithmetic. Reverting the loader change fails 7 cases in the default and extended Unicode builds and 9 with kerning.
One supplied input also reaches an out-of-bounds read that is independent of this chain and is not fixed here: _gx_binres_theme_load() receives no length for the resource, so a page declaring more glyph records than the file contains reads past the end of it. That reproduces with a well-formed narrow span, on affected and fixed versions alike, and needs a length on the public API. It is tracked as GHSA-m779-jvp3-pmqr.
Workarounds
Load binary resource files only from a trusted source. Where binary resources are not used at all, building with GX_DISABLE_BINARY_RESOURCE_SUPPORT removes the loader. Builds without extended Unicode support cannot reach the multiply wrap, though the unvalidated range remains.
Credits
Reported by @adawn0106, who supplied the reproducer and the control-flow-hijack demonstration, confirmed the build ABI, contributed the regression inputs, and reviewed this write-up. Fixed by Frédéric Desbiens (@fdesbiens). The fix and the in-tree regression tests were produced with AI assistance under human review, in accordance with the Eclipse Foundation’s Generative AI Usage Guidelines.