Fix CVE-2026-8177: out-of-bounds UTF-8 heap read in perl-XML-LibXML

Backport upstream commit 059abf5f to fix CVE-2026-8177, an
out-of-bounds UTF-8 read in domParseChar(). The patch replaces
the vulnerable domParseChar() function with libxml2's
xmlValidateName() to prevent heap reads past the NUL terminator
on truncated or malformed multi-byte UTF-8 sequences, and adds
65 regression tests covering the affected DOM entry points.

CVE: CVE-2026-8177
Upstream patches:
 - 059abf5f93.patch
Resolves: RHEL-186536

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

Assisted-by: Ymir
This commit is contained in:
RHEL Packaging Agent 2026-07-29 11:05:39 +00:00
parent 00a43baeeb
commit 153259b4d2
2 changed files with 317 additions and 1 deletions

View File

@ -0,0 +1,308 @@
From 804bc449f98f86553c8abadd4ce69b620d1e398f Mon Sep 17 00:00:00 2001
From: Toddr Bot <toddbot@rinaldo.us>
Date: Tue, 19 May 2026 19:32:08 +0000
Subject: [PATCH] fix: replace domParseChar with xmlValidateName to prevent OOB
UTF-8 read
domParseChar() read continuation bytes for multi-byte UTF-8 sequences without
validating they exist or have required 10xxxxxx form. Truncated sequences like
"a\xF0" caused heap reads past NUL terminator. Replace with libxml2's
xmlValidateName(), which correctly handles all UTF-8 edge cases.
Adds 65 regression tests covering truncation points, invalid continuations, and
5 affected DOM entry points (createElement, createAttribute, setNodeName, etc).
Fixes https://github.com/cpan-authors/XML-LibXML/issues/146
---
LibXML.xs | 33 +------------
dom.c | 88 ---------------------------------
dom.h | 32 ------------
t/48_security_oob_utf8_gh146.t | 90 ++++++++++++++++++++++++++++++++++
4 files changed, 92 insertions(+), 151 deletions(-)
create mode 100644 t/48_security_oob_utf8_gh146.t
diff --git a/LibXML.xs b/LibXML.xs
index 51bb3be..66b8873 100644
--- a/LibXML.xs
+++ b/LibXML.xs
@@ -989,40 +989,11 @@ LibXML_cleanup_parser() {
int
LibXML_test_node_name( xmlChar * name )
{
- xmlChar * cur = name;
- int tc = 0;
- int len = 0;
-
- if ( cur == NULL || *cur == 0 ) {
- /* warn("name is empty" ); */
- return(0);
- }
-
- tc = domParseChar( cur, &len );
-
- if ( !( IS_LETTER( tc ) || (tc == '_') || (tc == ':')) ) {
- /* warn( "is not a letter\n" ); */
+ if ( name == NULL || *name == 0 ) {
return(0);
}
- tc = 0;
- cur += len;
-
- while (*cur != 0 ) {
- tc = domParseChar( cur, &len );
-
- if (!(IS_LETTER(tc) || IS_DIGIT(tc) || (tc == '_') ||
- (tc == '-') || (tc == ':') || (tc == '.') ||
- IS_COMBINING(tc) || IS_EXTENDER(tc)) ) {
- /* warn( "is not a letter\n" ); */
- return(0);
- }
- tc = 0;
- cur += len;
- }
-
- /* warn("name is ok"); */
- return(1);
+ return xmlValidateName( name, 0 ) == 0;
}
/* Assumes that the node has a proxy. */
diff --git a/dom.c b/dom.c
index 10eb33d..0a818b2 100644
--- a/dom.c
+++ b/dom.c
@@ -235,94 +235,6 @@ domReconcileNs(xmlNodePtr tree)
xmlFreeNsList(unused);
}
-/**
- * NAME domParseChar
- * TYPE function
- * SYNOPSIS
- * int utf8char = domParseChar( curchar, &len );
- *
- * The current char value, if using UTF-8 this may actually span
- * multiple bytes in the given string. This function parses an utf8
- * character from a string into a UTF8 character (an integer). It uses
- * a slightly modified version of libxml2's character parser. libxml2
- * itself does not provide any function to parse characters dircetly
- * from a string and test if they are valid utf8 characters.
- *
- * XML::LibXML uses this function rather than perls native UTF8
- * support for two reasons:
- * 1) perls UTF8 handling functions often lead to encoding errors,
- * which partly comes, that they are badly documented.
- * 2) not all perl versions XML::LibXML intends to run with have native
- * UTF8 support.
- *
- * domParseChar() allows to use the very same code with all versions
- * of perl :)
- *
- * Returns the current char value and its length
- *
- * NOTE: If the character passed to this function is not a UTF
- * character, the return value will be 0 and the length of the
- * character is -1!
- */
-int
-domParseChar( xmlChar *cur, int *len )
-{
- unsigned char c;
- unsigned int val;
-
- /*
- * We are supposed to handle UTF8, check it's valid
- * From rfc2044: encoding of the Unicode values on UTF-8:
- *
- * UCS-4 range (hex.) UTF-8 octet sequence (binary)
- * 0000 0000-0000 007F 0xxxxxxx
- * 0000 0080-0000 07FF 110xxxxx 10xxxxxx
- * 0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
- *
- * Check for the 0x110000 limit too
- */
-
- if ( cur == NULL || *cur == 0 ) {
- *len = 0;
- return(0);
- }
-
- c = *cur;
- if ( c & 0x80 ) {
- if ((c & 0xe0) == 0xe0) {
- if ((c & 0xf0) == 0xf0) {
- /* 4-byte code */
- *len = 4;
- val = (cur[0] & 0x7) << 18;
- val |= (cur[1] & 0x3f) << 12;
- val |= (cur[2] & 0x3f) << 6;
- val |= cur[3] & 0x3f;
- } else {
- /* 3-byte code */
- *len = 3;
- val = (cur[0] & 0xf) << 12;
- val |= (cur[1] & 0x3f) << 6;
- val |= cur[2] & 0x3f;
- }
- } else {
- /* 2-byte code */
- *len = 2;
- val = (cur[0] & 0x1f) << 6;
- val |= cur[1] & 0x3f;
- }
- if ( !IS_CHAR(val) ) {
- *len = -1;
- return(0);
- }
- return(val);
- }
- else {
- /* 1-byte code */
- *len = 1;
- return((int)c);
- }
-}
-
/**
* Name: domReadWellBalancedString
* Synopsis: xmlNodePtr domReadWellBalancedString( xmlDocPtr doc, xmlChar *string )
diff --git a/dom.h b/dom.h
index 29cf2b5..cf0f474 100644
--- a/dom.h
+++ b/dom.h
@@ -54,38 +54,6 @@ extern "C" {
void
domReconcileNs(xmlNodePtr tree);
-/**
- * NAME domParseChar
- * TYPE function
- * SYNOPSIS
- * int utf8char = domParseChar( curchar, &len );
- *
- * The current char value, if using UTF-8 this may actually span
- * multiple bytes in the given string. This function parses an utf8
- * character from a string into a UTF8 character (an integer). It uses
- * a slightly modified version of libxml2's character parser. libxml2
- * itself does not provide any function to parse characters dircetly
- * from a string and test if they are valid utf8 characters.
- *
- * XML::LibXML uses this function rather than perls native UTF8
- * support for two reasons:
- * 1) perls UTF8 handling functions often lead to encoding errors,
- * which partly comes, that they are badly documented.
- * 2) not all perl versions XML::LibXML intends to run with have native
- * UTF8 support.
- *
- * domParseChar() allows to use the very same code with all versions
- * of perl :)
- *
- * Returns the current char value and its length
- *
- * NOTE: If the character passed to this function is not a UTF
- * character, the return value will be 0 and the length of the
- * character is -1!
- */
-int
-domParseChar( xmlChar *characters, int *len );
-
xmlNodePtr
domReadWellBalancedString( xmlDocPtr doc, xmlChar* string, int repair );
diff --git a/t/48_security_oob_utf8_gh146.t b/t/48_security_oob_utf8_gh146.t
new file mode 100644
index 0000000..74fb6d4
--- /dev/null
+++ b/t/48_security_oob_utf8_gh146.t
@@ -0,0 +1,90 @@
+# Security regression test for GitHub issue #146:
+# Out-of-bounds heap read via hand-rolled UTF-8 walker on truncated sequences.
+#
+# The original domParseChar() read continuation bytes for multi-byte UTF-8
+# sequences without verifying they exist or are valid. A truncated sequence
+# (e.g., "a\xF0") caused reads past the NUL terminator into uninitialized
+# heap memory. This affects all DOM methods that validate node names via
+# LibXML_test_node_name(): createElement, createAttribute, setNodeName,
+# createElementNS, createAttributeNS, etc.
+#
+# Impact: denial of service (crash on unmapped memory) and potential
+# information disclosure (reading adjacent heap allocations).
+#
+# Fixed by replacing the hand-rolled UTF-8 walker (domParseChar) with
+# libxml2's own xmlValidateName(), which correctly handles all UTF-8
+# edge cases.
+#
+# NOTE: This test verifies that malformed UTF-8 does not crash the process
+# (the actual security issue). Whether a given sequence is rejected depends
+# on the linked libxml2 version — older 2.9.x builds may accept some
+# sequences that newer versions reject.
+
+use strict;
+use warnings;
+
+use Test::More;
+use XML::LibXML;
+
+# Truncated UTF-8 sequences that previously caused OOB heap reads.
+# Each entry: [ bytes, description ]
+my @truncated_sequences = (
+ [ "a\xC0", "truncated 2-byte (leader only)" ],
+ [ "a\xC2", "truncated 2-byte (valid leader, missing continuation)" ],
+ [ "a\xE0", "truncated 3-byte (leader only)" ],
+ [ "a\xE0\x80", "truncated 3-byte (leader + 1 continuation)" ],
+ [ "a\xF0", "truncated 4-byte (leader only)" ],
+ [ "a\xF0\x80", "truncated 4-byte (leader + 1 continuation)" ],
+ [ "a\xF0\x80\x80", "truncated 4-byte (leader + 2 continuations)" ],
+);
+
+# Invalid continuation bytes — the leader is valid but the continuations
+# are not 10xxxxxx.
+my @invalid_continuations = (
+ [ "a\xC2\x41", "2-byte with ASCII continuation" ],
+ [ "a\xE0\x41\x80", "3-byte with ASCII in first continuation" ],
+ [ "a\xE0\x80\x41", "3-byte with ASCII in second continuation" ],
+ [ "a\xF0\x41\x80\x80", "4-byte with ASCII in first continuation" ],
+ [ "a\xF0\x80\x41\x80", "4-byte with ASCII in second continuation" ],
+ [ "a\xF0\x80\x80\x41", "4-byte with ASCII in third continuation" ],
+);
+
+my @all_bad = (@truncated_sequences, @invalid_continuations);
+
+my @methods = qw( createElement setNodeName createElementNS
+ createAttribute createAttributeNS );
+
+# TEST:$bad_count=13
+# TEST:$method_count=5
+plan tests => scalar(@all_bad) * scalar(@methods);
+
+my $doc = XML::LibXML::Document->new();
+my $nsURI = "http://example.com/ns";
+
+for my $case (@all_bad) {
+ my ($bytes, $desc) = @$case;
+
+ for my $method (@methods) {
+ eval {
+ if ($method eq 'createElement') {
+ $doc->createElement($bytes);
+ }
+ elsif ($method eq 'setNodeName') {
+ my $node = $doc->createElement("tmp");
+ $node->setNodeName($bytes);
+ }
+ elsif ($method eq 'createElementNS') {
+ $doc->createElementNS($nsURI, $bytes);
+ }
+ elsif ($method eq 'createAttribute') {
+ $doc->createAttribute($bytes, "value");
+ }
+ elsif ($method eq 'createAttributeNS') {
+ $doc->createAttributeNS($nsURI, $bytes, "value");
+ }
+ };
+
+ # TEST*$bad_count*$method_count
+ pass("$method survives $desc without crashing");
+ }
+}

View File

@ -8,7 +8,7 @@ Name: perl-XML-LibXML
# it might not be needed anymore
# this module is maintained, the other is not
Version: 2.0206
Release: 5%{?dist}
Release: 6%{?dist}
Epoch: 1
Summary: Perl interface to the libxml2 library
License: (GPL+ or Artistic) and MIT
@ -19,6 +19,9 @@ Source0: https://cpan.metacpan.org/authors/id/S/SH/SHLOMIF/XML-LibXML-%{v
Patch0: XML-LibXML-2.0202-Parse-an-ampersand-entity-in-SAX-interface.patch
# To reduce dependencies replace Alien::Libxml2 with pkg-config
Patch1: XML-LibXML-2.0206-Use-pkgconfig-instead-of-Alien-Libxml2.patch
# CVE-2026-8177 - fix OOB UTF-8 read in domParseChar
# https://github.com/cpan-authors/XML-LibXML/commit/059abf5f9336e2213794b5b545c707394cca3ac7
Patch2: perl-XML-LibXML-2.0206-CVE-2026-8177.patch
BuildRequires: coreutils
BuildRequires: findutils
BuildRequires: glibc-common
@ -95,6 +98,7 @@ validating XML parser and the high performance DOM implementation.
%setup -q -n XML-LibXML-%{version}
%patch0 -p1
%patch1 -p1
%patch2 -p1
chmod -x *.c
for i in Changes; do
/usr/bin/iconv -f iso8859-1 -t utf-8 $i > $i.conv && /bin/mv -f $i.conv $i
@ -135,6 +139,10 @@ fi
%{_mandir}/man3/*.3*
%changelog
* Wed Jul 29 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 1:2.0206-6
- Fix out-of-bounds UTF-8 read in domParseChar (CVE-2026-8177)
Related: RHEL-186536
* Mon Aug 09 2021 Mohan Boddu <mboddu@redhat.com> - 1:2.0206-5
- Rebuilt for IMA sigs, glibc 2.34, aarch64 flags
Related: rhbz#1991688