libXfont2/libXfont2-2.0.3-CVE-2026-44950.patch
RHEL Packaging Agent 7505ca59ac Fix CVE-2026-44950: heap buffer overflow in fs_read_glyphs()
Backport upstream fix for CVE-2026-44950 to libXfont2 2.0.3.
The fix adds a bounds-check for cumulative glyph data writes
in fs_read_glyphs() to prevent heap overflow from overlapping
source offsets sent by a malicious font server. A new regression
test (test-fserve-read-glyphs.c) is included to verify the fix.

CVE: CVE-2026-44950
Upstream patches:
 - c2d222bb22.patch
Resolves: RHEL-222015

This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.

Assisted-by: Ymir
2026-08-06 10:48:36 +00:00

512 lines
16 KiB
Diff

From fa38750d94ec161fc57d3f53d44a00bb1dfc3d89 Mon Sep 17 00:00:00 2001
From: Peter Hutterer <peter.hutterer@who-t.net>
Date: Mon, 13 Jul 2026 15:50:09 +1000
Subject: [PATCH] fserve: bounds-check cumulative glyph data writes in
fs_read_glyphs
fs_read_glyphs() copies each glyph's bitmap into a single allbits
buffer allocated to rep->nbytes bytes. The per-glyph guard validates
only that the source slice (position, length) lies within the pbitmaps
source buffer. It does not check whether the running destination cursor
has exceeded the allocation.
A malicious font server can send overlapping source offsets (e.g. 1000
glyphs each referencing {position:0, length:64} with nbytes=64). Each
individual source range passes validation, but the cumulative writes
total 64000 bytes into a 64-byte destination buffer.
Interestingly there was an unconditional debug printf in place that
sort-of warned about this but didn't prevent this. Let's remove that and
instead use the actual check to bail out before we run OOB.
A regression test is included that sends 100 glyphs each referencing
the same 64-byte source slice into a 64-byte destination buffer, and
verifies the library rejects the overflow.
CVE-2026-44950
Found-by: Zhixi "Jace" Sun, independent security researcher
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Part-of: <https://gitlab.freedesktop.org/xorg/lib/libxfont/-/merge_requests/36>
---
src/fc/fserve.c | 23 +-
test/test-fserve-read-glyphs.c | 412 +++++++++++++++++++++++++++++++++
2 files changed, 426 insertions(+), 9 deletions(-)
create mode 100644 test/test-fserve-read-glyphs.c
diff --git a/src/fc/fserve.c b/src/fc/fserve.c
index 708fc35..f17cffa 100644
--- a/src/fc/fserve.c
+++ b/src/fc/fserve.c
@@ -1923,10 +1923,7 @@ fs_read_glyphs(FontPathElementPtr fpe, FSBlockDataPtr blockrec)
fsOffset32 local_off;
char *off_adr;
pointer pbitmaps;
- char *bits, *allbits;
-#ifdef DEBUG
- char *origallbits;
-#endif
+ char *bits, *allbits, *origallbits;
int i,
err;
int nranges = 0;
@@ -2016,8 +2013,8 @@ fs_read_glyphs(FontPathElementPtr fpe, FSBlockDataPtr blockrec)
goto bail;
}
-#ifdef DEBUG
origallbits = allbits;
+#ifdef DEBUG
fprintf (stderr, "Reading %d glyphs in %d bytes for %s\n",
(int) rep->num_chars, (int) rep->nbytes, fsd->name);
#endif
@@ -2038,6 +2035,18 @@ fs_read_glyphs(FontPathElementPtr fpe, FSBlockDataPtr blockrec)
(local_off.position < rep->nbytes) &&
(local_off.length <= (rep->nbytes - local_off.position)))
{
+ /* Check that the destination buffer has enough room
+ for this glyph to prevent a heap overflow from
+ overlapping source offsets. */
+ if (local_off.length >
+ rep->nbytes - (allbits - origallbits))
+ {
+ ErrorF("fserve: glyph data overflow: "
+ "cumulative write exceeds nbytes (%u)\n",
+ (unsigned) rep->nbytes);
+ err = AllocError;
+ goto bail;
+ }
bits = allbits;
allbits += local_off.length;
memcpy(bits, (char *)pbitmaps + local_off.position,
@@ -2065,10 +2074,6 @@ fs_read_glyphs(FontPathElementPtr fpe, FSBlockDataPtr blockrec)
}
off_adr += SIZEOF(fsOffset32);
}
-#ifdef DEBUG
- fprintf (stderr, "Used %d bytes instead of %d\n",
- (int) (allbits - origallbits), (int) rep->nbytes);
-#endif
if (blockrec->type == FS_OPEN_FONT)
{
diff --git a/test/test-fserve-read-glyphs.c b/test/test-fserve-read-glyphs.c
new file mode 100644
index 0000000..21c3410
--- /dev/null
+++ b/test/test-fserve-read-glyphs.c
@@ -0,0 +1,412 @@
+/*
+ * Security regression tests for fs_read_glyphs() in src/fc/fserve.c.
+ *
+ * Approach: include fserve.c directly to access the static fs_read_glyphs()
+ * function. Pre-fill the FSFpeRec.inBuf with a crafted protocol reply so
+ * fs_get_reply() returns it without any network I/O.
+ *
+ * Copyright (c) 2026, Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Include fserve.c directly to access the static fs_read_glyphs().
+ * All non-static symbols from fserve.c are hidden in libXfont2.so
+ * (via the linker version script), so there are no duplicate symbol
+ * conflicts when linking against the library.
+ */
+#include "src/fc/fserve.c"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/*
+ * Set up an FSFpeRec with its inBuf pre-filled with the given data.
+ * fs_get_reply() will return this data without attempting any network I/O
+ * because fs_inqueued(conn) >= size.
+ */
+static void
+setup_conn(FSFpeRec *conn, const void *reply_data, long reply_size)
+{
+ memset(conn, 0, sizeof(*conn));
+
+ /*
+ * fs_get_reply() checks: conn->fs_fd != -1 && conn->fs_listening
+ * Use a dup'd fd so it's valid but harmless.
+ */
+ conn->fs_fd = dup(STDERR_FILENO);
+ conn->fs_listening = TRUE;
+
+ /* Pre-fill the input buffer with our crafted reply */
+ conn->inBuf.buf = malloc(reply_size);
+ if (!conn->inBuf.buf) {
+ fprintf(stderr, "FAIL: malloc for inBuf\n");
+ exit(1);
+ }
+ memcpy(conn->inBuf.buf, reply_data, reply_size);
+ conn->inBuf.size = reply_size;
+ conn->inBuf.insert = reply_size;
+ conn->inBuf.remove = 0;
+ conn->inNeed = 0;
+
+ /* Allocate a minimal output buffer to keep _fs_flush happy */
+ conn->outBuf.buf = calloc(1, FS_BUF_INC);
+ conn->outBuf.size = FS_BUF_INC;
+ conn->outBuf.insert = 0;
+ conn->outBuf.remove = 0;
+}
+
+static void
+cleanup_conn(FSFpeRec *conn)
+{
+ if (conn->fs_fd >= 0)
+ close(conn->fs_fd);
+ free(conn->inBuf.buf);
+ free(conn->outBuf.buf);
+}
+
+/*
+ * Set up the minimum font state needed by fs_read_glyphs():
+ * - FontPathElementRec (fpe) with fpe->private = conn
+ * - FontRec (pfont) with info, fontPrivate, fpePrivate
+ * - FSFontRec (fsfont) with encoding[] array
+ * - FSFontDataRec (fsd)
+ * - FSBlockDataRec (blockrec) of type FS_OPEN_FONT
+ * - FSBlockedFontRec (bfont) embedded in blockrec->data
+ */
+struct test_font_state {
+ FontPathElementRec fpe;
+ FontRec pfont;
+ FSFontRec fsfont;
+ FSFontDataRec fsd;
+ FSBlockDataRec blockrec;
+ FSBlockedFontRec bfont;
+ CharInfoPtr encoding;
+};
+
+static void
+setup_font_state(struct test_font_state *s, FSFpeRec *conn,
+ int num_encoding)
+{
+ int i;
+
+ memset(s, 0, sizeof(*s));
+
+ /* Font path element */
+ s->fpe.name = (char *)"test-fserve";
+ s->fpe.name_length = strlen(s->fpe.name);
+ s->fpe.private = conn;
+
+ /* Font data (fpePrivate) */
+ s->fsd.name = (char *)"test-font";
+ s->fsd.namelen = strlen(s->fsd.name);
+ s->fsd.glyphs_to_get = 0;
+
+ /* Encoding array -- this is what num_extents sized */
+ s->encoding = calloc(num_encoding, sizeof(CharInfoRec));
+ if (!s->encoding) {
+ fprintf(stderr, "FAIL: calloc encoding\n");
+ exit(1);
+ }
+ /* Mark all glyphs as having nonzero metrics and undefined bits
+ * so fs_read_glyphs will try to process them */
+ for (i = 0; i < num_encoding; i++) {
+ s->encoding[i].metrics.ascent = 10;
+ s->encoding[i].metrics.descent = 2;
+ s->encoding[i].metrics.characterWidth = 8;
+ s->encoding[i].metrics.leftSideBearing = 0;
+ s->encoding[i].metrics.rightSideBearing = 8;
+ s->encoding[i].bits = &_fs_glyph_undefined;
+ }
+
+ /* FSFontRec */
+ s->fsfont.encoding = s->encoding;
+ s->fsfont.num_encoding = num_encoding;
+ s->fsfont.pDefault = NULL;
+ s->fsfont.inkMetrics = s->encoding;
+ s->fsfont.glyphs = NULL;
+
+ /* FontRec */
+ s->pfont.fontPrivate = &s->fsfont;
+ s->pfont.fpePrivate = &s->fsd;
+ s->pfont.fpe = &s->fpe;
+ s->pfont.info.firstRow = 0;
+ s->pfont.info.lastRow = 0;
+ s->pfont.info.firstCol = 0;
+ s->pfont.info.lastCol = num_encoding > 0 ? num_encoding - 1 : 0;
+ s->pfont.info.maxbounds.ascent = 20;
+ s->pfont.info.maxbounds.descent = 10;
+ s->pfont.info.maxbounds.characterWidth = 20;
+
+ /* Block record -- simulating FS_OPEN_FONT path */
+ s->bfont.pfont = &s->pfont;
+ s->bfont.flags = FontLoadBitmaps;
+ s->bfont.state = FS_GLYPHS_REPLY;
+ s->bfont.freeFont = FALSE;
+
+ s->blockrec.type = FS_OPEN_FONT;
+ s->blockrec.data = (pointer)&s->bfont;
+ s->blockrec.client = NULL;
+ s->blockrec.sequenceNumber = 0;
+ s->blockrec.errcode = 0;
+ s->blockrec.depending = NULL;
+ s->blockrec.next = NULL;
+}
+
+static void
+cleanup_font_state(struct test_font_state *s)
+{
+ FSGlyphPtr g, next;
+
+ /* Free any glyph allocations made by fs_alloc_glyphs */
+ for (g = s->fsfont.glyphs; g; g = next) {
+ next = g->next;
+ free(g);
+ }
+ free(s->encoding);
+}
+
+/*
+ * Build a crafted fsQueryXBitmaps16Reply in a buffer.
+ * Returns the total buffer size. Caller must free *out_buf.
+ *
+ * The reply contains:
+ * - fsQueryXBitmaps16Reply header
+ * - num_chars fsOffset32 entries
+ * - nbytes of bitmap data
+ */
+static long
+build_reply(char **out_buf,
+ CARD32 num_chars, CARD32 nbytes,
+ CARD32 off_position, CARD32 off_length)
+{
+ long hdr_size = SIZEOF(fsQueryXBitmaps16Reply);
+ long offsets_size = SIZEOF(fsOffset32) * num_chars;
+ /* Bitmap data area: at least nbytes, but we need off_position + off_length
+ * to be valid source, so ensure bitmap area is large enough */
+ long bitmap_size = nbytes;
+ long total = hdr_size + offsets_size + bitmap_size;
+ long total_padded = (total + 3) & ~3; /* pad to 4 bytes */
+ char *buf;
+ fsQueryXBitmaps16Reply *rep;
+ fsOffset32 off;
+ long i;
+
+ buf = calloc(1, total_padded);
+ if (!buf) {
+ fprintf(stderr, "FAIL: calloc reply buffer\n");
+ exit(1);
+ }
+
+ /* Fill header */
+ rep = (fsQueryXBitmaps16Reply *)buf;
+ rep->type = FS_Reply; /* normal reply (0), not FS_Error (1) */
+ rep->sequenceNumber = 0;
+ rep->length = total_padded >> 2; /* length in 32-bit words */
+ rep->replies_hint = 0;
+ rep->num_chars = num_chars;
+ rep->nbytes = nbytes;
+
+ /* Fill offset entries -- all pointing to the same source range */
+ off.position = off_position;
+ off.length = off_length;
+ for (i = 0; i < (long)num_chars; i++) {
+ memcpy(buf + hdr_size + i * SIZEOF(fsOffset32),
+ &off, SIZEOF(fsOffset32));
+ }
+
+ /* Fill bitmap data with recognizable pattern */
+ memset(buf + hdr_size + offsets_size, 0xAA, bitmap_size);
+
+ *out_buf = buf;
+ return total_padded;
+}
+
+/*
+ * Test 1: num_chars > num_encoding
+ *
+ * Allocate encoding[] with 2 entries, but send a reply with
+ * num_chars = 100. Without the fix, this would read/write
+ * encoding[2..99] out of bounds.
+ */
+static int
+test_num_chars_exceeds_encoding(void)
+{
+ FSFpeRec conn;
+ struct test_font_state state;
+ char *reply_buf;
+ long reply_size;
+ int result;
+ int num_encoding = 2;
+ CARD32 num_chars = 100;
+ CARD32 nbytes = num_chars * 16; /* enough bitmap data */
+
+ /* Build a reply with num_chars=100 but valid source data */
+ reply_size = build_reply(&reply_buf, num_chars, nbytes, 0, 16);
+ setup_conn(&conn, reply_buf, reply_size);
+ setup_font_state(&state, &conn, num_encoding);
+
+ result = fs_read_glyphs(&state.fpe, &state.blockrec);
+
+ cleanup_font_state(&state);
+ cleanup_conn(&conn);
+ free(reply_buf);
+
+ if (result != Successful) {
+ printf("ok 1 - num_chars (%u) > num_encoding (%d) rejected\n",
+ (unsigned)num_chars, num_encoding);
+ return 0;
+ } else {
+ printf("not ok 1 - num_chars (%u) > num_encoding (%d) "
+ "should have been rejected\n",
+ (unsigned)num_chars, num_encoding);
+ return 1;
+ }
+}
+
+/*
+ * Test 2: cumulative glyph data overflow
+ *
+ * Allocate allbits with nbytes=64, but send 100 glyphs each
+ * with offset {position:0, length:64}. Each individual source
+ * range is valid, but the cumulative writes total 6400 bytes
+ * into a 64-byte buffer.
+ */
+static int
+test_cumulative_allbits_overflow(void)
+{
+ FSFpeRec conn;
+ struct test_font_state state;
+ char *reply_buf;
+ long reply_size;
+ int result;
+ int num_encoding = 100; /* match num_chars so encoding[] is fine */
+ CARD32 num_chars = 100;
+ CARD32 nbytes = 64; /* tiny destination buffer */
+
+ /* All offsets point to {position:0, length:64} -- each source
+ * range is valid but they overlap, causing 100*64=6400 bytes
+ * to be written to a 64-byte buffer */
+ reply_size = build_reply(&reply_buf, num_chars, nbytes, 0, 64);
+ setup_conn(&conn, reply_buf, reply_size);
+ setup_font_state(&state, &conn, num_encoding);
+
+ result = fs_read_glyphs(&state.fpe, &state.blockrec);
+
+ cleanup_font_state(&state);
+ cleanup_conn(&conn);
+ free(reply_buf);
+
+ if (result != Successful) {
+ printf("ok 2 - cumulative allbits overflow (100 * 64 into 64) rejected\n");
+ return 0;
+ } else {
+ printf("not ok 2 - cumulative allbits overflow (100 * 64 into 64) "
+ "should have been rejected\n");
+ return 1;
+ }
+}
+
+/*
+ * Test 3: legitimate reply should still be accepted
+ *
+ * num_chars == num_encoding, each glyph has unique non-overlapping
+ * offsets, and total data fits in nbytes.
+ */
+static int
+test_legitimate_reply(void)
+{
+ FSFpeRec conn;
+ struct test_font_state state;
+ char *reply_buf;
+ long hdr_size = SIZEOF(fsQueryXBitmaps16Reply);
+ long offsets_size;
+ int result;
+ int num_encoding = 4;
+ CARD32 num_chars = 4;
+ CARD32 glyph_size = 16;
+ CARD32 nbytes = num_chars * glyph_size;
+ long total, total_padded;
+ fsQueryXBitmaps16Reply *rep;
+ fsOffset32 off;
+ int i;
+
+ offsets_size = SIZEOF(fsOffset32) * num_chars;
+ total = hdr_size + offsets_size + nbytes;
+ total_padded = (total + 3) & ~3;
+
+ reply_buf = calloc(1, total_padded);
+ if (!reply_buf) {
+ fprintf(stderr, "FAIL: calloc\n");
+ return 1;
+ }
+
+ rep = (fsQueryXBitmaps16Reply *)reply_buf;
+ rep->type = FS_Reply;
+ rep->sequenceNumber = 0;
+ rep->length = total_padded >> 2;
+ rep->replies_hint = 0;
+ rep->num_chars = num_chars;
+ rep->nbytes = nbytes;
+
+ /* Each glyph gets its own non-overlapping slice */
+ for (i = 0; i < (int)num_chars; i++) {
+ off.position = i * glyph_size;
+ off.length = glyph_size;
+ memcpy(reply_buf + hdr_size + i * SIZEOF(fsOffset32),
+ &off, SIZEOF(fsOffset32));
+ }
+ memset(reply_buf + hdr_size + offsets_size, 0xBB, nbytes);
+
+ setup_conn(&conn, reply_buf, total_padded);
+ setup_font_state(&state, &conn, num_encoding);
+
+ result = fs_read_glyphs(&state.fpe, &state.blockrec);
+
+ cleanup_font_state(&state);
+ cleanup_conn(&conn);
+ free(reply_buf);
+
+ if (result == Successful) {
+ printf("ok 3 - legitimate reply (4 glyphs, non-overlapping) accepted\n");
+ return 0;
+ } else {
+ printf("not ok 3 - legitimate reply (4 glyphs, non-overlapping) "
+ "rejected with error %d\n", result);
+ return 1;
+ }
+}
+
+int
+main(int argc, char **argv)
+{
+ int failures = 0;
+
+ printf("1..3\n");
+
+ failures += test_num_chars_exceeds_encoding();
+ failures += test_cumulative_allbits_overflow();
+ failures += test_legitimate_reply();
+
+ return failures ? 1 : 0;
+}