From 7505ca59ac01719a9659e40a26a23dafbca2636a Mon Sep 17 00:00:00 2001 From: RHEL Packaging Agent Date: Thu, 6 Aug 2026 10:32:38 +0000 Subject: [PATCH] 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: - https://gitlab.freedesktop.org/xorg/lib/libxfont/-/commit/c2d222bb22c623d8a40f3275077fc7e6617f2c8a.patch Resolves: RHEL-222015 This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent. Assisted-by: Ymir --- libXfont2-2.0.3-CVE-2026-44950.patch | 511 +++++++++++++++++++++++++++ libXfont2.spec | 9 +- 2 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 libXfont2-2.0.3-CVE-2026-44950.patch diff --git a/libXfont2-2.0.3-CVE-2026-44950.patch b/libXfont2-2.0.3-CVE-2026-44950.patch new file mode 100644 index 0000000..8947f41 --- /dev/null +++ b/libXfont2-2.0.3-CVE-2026-44950.patch @@ -0,0 +1,511 @@ +From fa38750d94ec161fc57d3f53d44a00bb1dfc3d89 Mon Sep 17 00:00:00 2001 +From: Peter Hutterer +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 +Part-of: +--- + 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 ++#include ++#include ++#include ++ ++/* ++ * 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; ++} diff --git a/libXfont2.spec b/libXfont2.spec index 87e5b82..bd7ca37 100644 --- a/libXfont2.spec +++ b/libXfont2.spec @@ -1,7 +1,7 @@ Summary: X.Org X11 libXfont2 runtime library Name: libXfont2 Version: 2.0.3 -Release: 2%{?dist}.1 +Release: 2%{?dist}.2 License: MIT Group: System Environment/Libraries URL: http://www.x.org @@ -19,6 +19,9 @@ Patch1: 0001-bitscale-fix-integer-overflow-in-BitmapScaleBitmaps-.patch Patch2: 0002-pcfread-validate-bitmap-sizes-and-offsets-against-pe.patch Patch3: 0003-bitscale-add-bounds-check-to-computeProps-for-proper.patch +# https://gitlab.freedesktop.org/xorg/lib/libxfont/-/commit/c2d222bb22c623d8a40f3275077fc7e6617f2c8a +Patch4: libXfont2-2.0.3-CVE-2026-44950.patch + %description X.Org X11 libXfont2 runtime library @@ -60,6 +63,10 @@ rm -f $RPM_BUILD_ROOT%{_libdir}/*.la %{_libdir}/pkgconfig/xfont2.pc %changelog +* Thu Aug 06 2026 RHEL Packaging Agent - 2.0.3-2.2 +- CVE fix for: CVE-2026-44950 + Resolves: https://redhat.atlassian.net/browse/RHEL-222015 + * Wed Jul 08 2026 Olivier Fourdan - 2.0.3-2.1 - CVE fix for: CVE-2026-56001, CVE-2026-56002, CVE-2026-56003 Resolves: https://redhat.atlassian.net/browse/RHEL-191877