Commit 5982cdace63 for php.net

commit 5982cdace631c07317f10955cb9a46caa42807d5
Author: Alexandre Daubois <alex.daubois@gmail.com>
Date:   Fri Sep 11 10:51:29 2026 +0200

    lexbor: Merge upstream memory safety fixes 8a14bc0 and f67ce4b

diff --git a/NEWS b/NEWS
index 77689d91498..1176aba9c44 100644
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,11 @@ PHP                                                                        NEWS
     such as dateType, timeType, calendar and the message pattern.
     (Ilia Alshanetsky)

+- Lexbor:
+  . Merge patches 8a14bc0 and f67ce4b, fixing a heap buffer overflow in
+    :lexbor-contains() parsing and buffer overflows in malformed decode
+    replay. (alexandre-daubois)
+
 - MBString:
   . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in
     a truncated UTF-8 sequence). (Lazizbek Ergashev)
diff --git a/ext/dom/lexbor/lexbor/css/selectors/pseudo_state.c b/ext/dom/lexbor/lexbor/css/selectors/pseudo_state.c
index 263ca52f35e..2321ddf01bb 100644
--- a/ext/dom/lexbor/lexbor/css/selectors/pseudo_state.c
+++ b/ext/dom/lexbor/lexbor/css/selectors/pseudo_state.c
@@ -227,13 +227,12 @@ lxb_css_selectors_state_pseudo_class_function_lexbor_contains(lxb_css_parser_t *
     contains->insensitive = false;
     str = &contains->str;

-    str->data = lexbor_mraw_alloc(parser->memory->mraw,
-                                  sizeof(lexbor_str_t));
+    str->data = lexbor_mraw_alloc(parser->memory->mraw, length + 1);
     if (str->data == NULL) {
         return lxb_css_parser_memory_fail(parser);
     }

-    memcpy(str->data, data, length + 1);
+    memcpy(str->data, data, length);

     str->length = length;
     str->data[length] = '\0';
diff --git a/ext/dom/lexbor/lexbor/encoding/decode.c b/ext/dom/lexbor/lexbor/encoding/decode.c
index 3e48971e365..05c4b9bde77 100644
--- a/ext/dom/lexbor/lexbor/encoding/decode.c
+++ b/ext/dom/lexbor/lexbor/encoding/decode.c
@@ -912,6 +912,13 @@ lxb_encoding_decode_iso_2022_jp(lxb_encoding_decode_t *ctx,
                     }
                     LXB_ENCODING_DECODE_ERROR_END();

+                    if (ctx->buffer_used >= ctx->buffer_length) {
+                        iso->prepand = iso->lead;
+                        iso->lead = 0x00;
+
+                        return LXB_STATUS_SMALL_BUFFER;
+                    }
+
                     byte = iso->lead;
                     iso->lead = 0x00;

@@ -1279,6 +1286,12 @@ lxb_encoding_decode_utf_16(lxb_encoding_decode_t *ctx, bool is_be,
             }
             LXB_ENCODING_DECODE_ERROR_END();

+            if (ctx->buffer_used >= ctx->buffer_length) {
+                ctx->u.lead = lead + 0x01;
+
+                return LXB_STATUS_SMALL_BUFFER;
+            }
+
             goto lead_state;
         }

@@ -1723,6 +1736,13 @@ lxb_encoding_decode_gb18030(lxb_encoding_decode_t *ctx,
             }
             LXB_ENCODING_DECODE_ERROR_END();

+            if (ctx->buffer_used >= ctx->buffer_length) {
+                ctx->prepend = true;
+                ctx->u.gb18030.first = second;
+
+                return LXB_STATUS_SMALL_BUFFER;
+            }
+
             first = second;

             goto prepend_first;
@@ -1756,11 +1776,8 @@ lxb_encoding_decode_gb18030(lxb_encoding_decode_t *ctx,
             }
             LXB_ENCODING_DECODE_ERROR_END();

-            LXB_ENCODING_DECODE_APPEND_WO_CHECK(ctx, second);
-
-            if (ctx->buffer_used == ctx->buffer_length) {
+            if (ctx->buffer_used >= ctx->buffer_length) {
                 ctx->prepend = true;
-                ctx->have_error = true;

                 /* First is a fake for trigger */
                 ctx->u.gb18030.first = 0x01;
@@ -1770,6 +1787,18 @@ lxb_encoding_decode_gb18030(lxb_encoding_decode_t *ctx,
                 return LXB_STATUS_SMALL_BUFFER;
             }

+            LXB_ENCODING_DECODE_APPEND_WO_CHECK(ctx, second);
+
+            if (ctx->buffer_used >= ctx->buffer_length) {
+                ctx->prepend = true;
+
+                ctx->u.gb18030.first = third;
+                ctx->u.gb18030.second = 0x00;
+                ctx->u.gb18030.third = 0x00;
+
+                return LXB_STATUS_SMALL_BUFFER;
+            }
+
             first = third;

             goto prepend_first;
diff --git a/ext/dom/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch b/ext/dom/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
index 1b35913e91c..04136b29f58 100644
--- a/ext/dom/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
+++ b/ext/dom/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
@@ -1,7 +1,7 @@
 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
 Date: Sat, 26 Aug 2023 15:08:59 +0200
-Subject: [PATCH 1/6] Expose line and column information for use in PHP
+Subject: [PATCH 1/8] Expose line and column information for use in PHP

 ---
  source/lexbor/dom/interfaces/node.h  |  2 ++
diff --git a/ext/dom/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch b/ext/dom/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
index 24bc2163051..615655d7f2e 100644
--- a/ext/dom/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
+++ b/ext/dom/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
@@ -1,7 +1,7 @@
 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
 Date: Mon, 14 Aug 2023 20:18:51 +0200
-Subject: [PATCH 2/6] Track implied added nodes for options use in PHP
+Subject: [PATCH 2/8] Track implied added nodes for options use in PHP

 ---
  source/lexbor/html/tree.h                            | 3 +++
diff --git a/ext/dom/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch b/ext/dom/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
index 9c67ba740c4..73c5afa19e1 100644
--- a/ext/dom/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
+++ b/ext/dom/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
@@ -1,7 +1,7 @@
 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
 Date: Thu, 24 Aug 2023 22:57:48 +0200
-Subject: [PATCH 3/6] Patch utilities and data structure to be able to generate
+Subject: [PATCH 3/8] Patch utilities and data structure to be able to generate
  smaller lookup tables

 Changed the generation script to check if everything fits in 32-bits.
diff --git a/ext/dom/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch b/ext/dom/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
index 4640a03647b..cc0a65a7cd5 100644
--- a/ext/dom/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
+++ b/ext/dom/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
@@ -1,7 +1,7 @@
 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
 Date: Wed, 29 Nov 2023 21:26:47 +0100
-Subject: [PATCH 4/6] Remove unused upper case tag static data
+Subject: [PATCH 4/8] Remove unused upper case tag static data

 ---
  source/lexbor/tag/res.h | 2 ++
diff --git a/ext/dom/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch b/ext/dom/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
index d276ba8f2ef..b84120bf6c8 100644
--- a/ext/dom/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
+++ b/ext/dom/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
@@ -1,7 +1,7 @@
 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
 Date: Wed, 29 Nov 2023 21:29:31 +0100
-Subject: [PATCH 5/6] Shrink size of static binary search tree
+Subject: [PATCH 5/8] Shrink size of static binary search tree

 This also makes it more efficient on the data cache.
 ---
diff --git a/ext/dom/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch b/ext/dom/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
index 827375f3f05..196a5a8a62d 100644
--- a/ext/dom/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
+++ b/ext/dom/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
@@ -1,7 +1,7 @@
 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
 Date: Sun, 7 Jan 2024 21:59:28 +0100
-Subject: [PATCH 6/6] Patch out unused CSS style code
+Subject: [PATCH 6/8] Patch out unused CSS style code

 ---
  source/lexbor/css/rule.h | 2 ++
diff --git a/ext/dom/lexbor/patches/0007-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch b/ext/dom/lexbor/patches/0007-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch
new file mode 100644
index 00000000000..cc689714b6c
--- /dev/null
+++ b/ext/dom/lexbor/patches/0007-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch
@@ -0,0 +1,35 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov <lex.borisov@gmail.com>
+Date: Fri, 5 Jun 2026 22:34:23 +0300
+Subject: [PATCH 7/8] CSS: fixed heap buffer overflow in :lexbor-contains()
+ parsing.
+
+The contains string buffer was allocated by the size of the string
+structure instead of the content length, so any value longer than
+that overflowed the buffer.
+
+Per report from Xiansheng Cao (@HMF2021)
+---
+ source/lexbor/css/selectors/pseudo_state.c | 5 ++---
+ 1 file changed, 2 insertions(+), 3 deletions(-)
+
+diff --git a/source/lexbor/css/selectors/pseudo_state.c b/source/lexbor/css/selectors/pseudo_state.c
+index 263ca52..2321ddf 100644
+--- a/source/lexbor/css/selectors/pseudo_state.c
++++ b/source/lexbor/css/selectors/pseudo_state.c
+@@ -227,13 +227,12 @@ again:
+     contains->insensitive = false;
+     str = &contains->str;
+
+-    str->data = lexbor_mraw_alloc(parser->memory->mraw,
+-                                  sizeof(lexbor_str_t));
++    str->data = lexbor_mraw_alloc(parser->memory->mraw, length + 1);
+     if (str->data == NULL) {
+         return lxb_css_parser_memory_fail(parser);
+     }
+
+-    memcpy(str->data, data, length + 1);
++    memcpy(str->data, data, length);
+
+     str->length = length;
+     str->data[length] = '\0';
diff --git a/ext/dom/lexbor/patches/0008-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch b/ext/dom/lexbor/patches/0008-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch
new file mode 100644
index 00000000000..352ebd95b2c
--- /dev/null
+++ b/ext/dom/lexbor/patches/0008-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch
@@ -0,0 +1,97 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov <lex.borisov@gmail.com>
+Date: Wed, 10 Jun 2026 19:50:10 +0300
+Subject: [PATCH 8/8] Encoding: fixed buffer overflows in malformed decode
+ replay.
+
+Fixed out-of-bounds writes in buffering decoders when replacement output
+fills the caller-provided codepoint buffer and decoder replay continues in
+the same call.
+
+Affected decoders:
+- GB18030 malformed third/fourth byte replay.
+- ISO-2022-JP malformed escape replay.
+- UTF-16BE/LE invalid surrogate replay.
+
+Per report from @hurric9-droid on GitHub.
+---
+ source/lexbor/encoding/decode.c | 37 +++++++++++++++++++++++++++++----
+ 1 file changed, 33 insertions(+), 4 deletions(-)
+
+diff --git a/source/lexbor/encoding/decode.c b/source/lexbor/encoding/decode.c
+index 3e48971..05c4b9b 100644
+--- a/source/lexbor/encoding/decode.c
++++ b/source/lexbor/encoding/decode.c
+@@ -912,6 +912,13 @@ lxb_encoding_decode_iso_2022_jp(lxb_encoding_decode_t *ctx,
+                     }
+                     LXB_ENCODING_DECODE_ERROR_END();
+
++                    if (ctx->buffer_used >= ctx->buffer_length) {
++                        iso->prepand = iso->lead;
++                        iso->lead = 0x00;
++
++                        return LXB_STATUS_SMALL_BUFFER;
++                    }
++
+                     byte = iso->lead;
+                     iso->lead = 0x00;
+
+@@ -1279,6 +1286,12 @@ lxb_encoding_decode_utf_16(lxb_encoding_decode_t *ctx, bool is_be,
+             }
+             LXB_ENCODING_DECODE_ERROR_END();
+
++            if (ctx->buffer_used >= ctx->buffer_length) {
++                ctx->u.lead = lead + 0x01;
++
++                return LXB_STATUS_SMALL_BUFFER;
++            }
++
+             goto lead_state;
+         }
+
+@@ -1723,6 +1736,13 @@ lxb_encoding_decode_gb18030(lxb_encoding_decode_t *ctx,
+             }
+             LXB_ENCODING_DECODE_ERROR_END();
+
++            if (ctx->buffer_used >= ctx->buffer_length) {
++                ctx->prepend = true;
++                ctx->u.gb18030.first = second;
++
++                return LXB_STATUS_SMALL_BUFFER;
++            }
++
+             first = second;
+
+             goto prepend_first;
+@@ -1756,11 +1776,8 @@ lxb_encoding_decode_gb18030(lxb_encoding_decode_t *ctx,
+             }
+             LXB_ENCODING_DECODE_ERROR_END();
+
+-            LXB_ENCODING_DECODE_APPEND_WO_CHECK(ctx, second);
+-
+-            if (ctx->buffer_used == ctx->buffer_length) {
++            if (ctx->buffer_used >= ctx->buffer_length) {
+                 ctx->prepend = true;
+-                ctx->have_error = true;
+
+                 /* First is a fake for trigger */
+                 ctx->u.gb18030.first = 0x01;
+@@ -1770,6 +1787,18 @@ lxb_encoding_decode_gb18030(lxb_encoding_decode_t *ctx,
+                 return LXB_STATUS_SMALL_BUFFER;
+             }
+
++            LXB_ENCODING_DECODE_APPEND_WO_CHECK(ctx, second);
++
++            if (ctx->buffer_used >= ctx->buffer_length) {
++                ctx->prepend = true;
++
++                ctx->u.gb18030.first = third;
++                ctx->u.gb18030.second = 0x00;
++                ctx->u.gb18030.third = 0x00;
++
++                return LXB_STATUS_SMALL_BUFFER;
++            }
++
+             first = third;
+
+             goto prepend_first;
diff --git a/ext/dom/tests/modern/css_selectors/lexbor_contains.phpt b/ext/dom/tests/modern/css_selectors/lexbor_contains.phpt
new file mode 100644
index 00000000000..795a8f110e3
--- /dev/null
+++ b/ext/dom/tests/modern/css_selectors/lexbor_contains.phpt
@@ -0,0 +1,16 @@
+--TEST--
+CSS Selectors - Pseudo classes: :lexbor-contains() with an argument longer than its string header
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+
+$dom = Dom\HTMLDocument::createFromString('<p>needle</p>', LIBXML_NOERROR);
+
+var_dump($dom->querySelectorAll(':lexbor-contains("' . str_repeat('needle', 1024) . '")')->length);
+var_dump($dom->querySelectorAll(':lexbor-contains("needle")')->length);
+
+?>
+--EXPECT--
+int(0)
+int(0)