Commit fa825cb8a05 for php.net

commit fa825cb8a05ba993c30074fd400f08472f9babdb
Merge: 2f0362e2b1a 5982cdace63
Author: Alexandre Daubois <alex.daubois@gmail.com>
Date:   Mon Sep 14 11:00:36 2026 +0200

    Merge branch 'PHP-8.4' into PHP-8.5

    * PHP-8.4:
      lexbor: Merge upstream memory safety fixes 8a14bc0 and f67ce4b

diff --cc NEWS
index 511a899e527,1176aba9c44..387caeeca43
--- a/NEWS
+++ b/NEWS
@@@ -17,9 -15,12 +17,14 @@@ PH
    . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state
      such as dateType, timeType, calendar and the message pattern.
      (Ilia Alshanetsky)
 +  . Fixed a crash when converting with a cloned UConverter that uses
 +    toUCallback/fromUCallback. (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 --cc ext/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
index 53359883782,04136b29f58..9aec14cca5d
--- a/ext/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
+++ b/ext/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
@@@ -1,7 -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 01/10] Expose line and column information for use in PHP
 -Subject: [PATCH 1/8] Expose line and column information for use in PHP
++Subject: [PATCH 01/12] Expose line and column information for use in PHP

  ---
   source/lexbor/dom/interfaces/node.h  |  2 ++
diff --cc ext/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
index 8814d595535,615655d7f2e..9f4da029446
--- a/ext/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
+++ b/ext/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
@@@ -1,7 -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 02/10] Track implied added nodes for options use in PHP
 -Subject: [PATCH 2/8] Track implied added nodes for options use in PHP
++Subject: [PATCH 02/12] Track implied added nodes for options use in PHP

  ---
   source/lexbor/html/tree.h                            | 3 +++
diff --cc ext/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
index aa480232049,73c5afa19e1..fe7068d9bda
--- a/ext/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
+++ b/ext/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
@@@ -1,8 -1,8 +1,8 @@@
  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 03/10] Patch utilities and data structure to be able to
 -Subject: [PATCH 3/8] Patch utilities and data structure to be able to generate
 - smaller lookup tables
++Subject: [PATCH 03/12] 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.
  And change the actual field types to 32-bits. This decreases the hash
diff --cc ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
index 1a28b21ccdc,cc0a65a7cd5..35482388531
--- a/ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
+++ b/ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
@@@ -1,7 -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 04/10] Remove unused upper case tag static data
 -Subject: [PATCH 4/8] Remove unused upper case tag static data
++Subject: [PATCH 04/12] Remove unused upper case tag static data

  ---
   source/lexbor/tag/res.h | 2 ++
diff --cc ext/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
index a1dda1fcd11,b84120bf6c8..8e9b9524104
--- a/ext/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
+++ b/ext/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
@@@ -1,7 -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 05/10] Shrink size of static binary search tree
 -Subject: [PATCH 5/8] Shrink size of static binary search tree
++Subject: [PATCH 05/12] Shrink size of static binary search tree

  This also makes it more efficient on the data cache.
  ---
diff --cc ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
index 57f1e0e92fc,196a5a8a62d..0f4e4cd8661
--- a/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
+++ b/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
@@@ -1,7 -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 06/10] Patch out unused CSS style code
 -Subject: [PATCH 6/8] Patch out unused CSS style code
++Subject: [PATCH 06/12] Patch out unused CSS style code

  ---
   source/lexbor/css/rule.h | 2 ++
diff --cc ext/lexbor/patches/0007-URL-fixed-setters-for-empty-hosts.patch
index b55f5aac589,00000000000..44f0f918458
mode 100644,000000..100644
--- a/ext/lexbor/patches/0007-URL-fixed-setters-for-empty-hosts.patch
+++ b/ext/lexbor/patches/0007-URL-fixed-setters-for-empty-hosts.patch
@@@ -1,191 -1,0 +1,191 @@@
 +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 +From: Alexander Borisov <lex.borisov@gmail.com>
 +Date: Fri, 26 Jun 2026 18:55:56 +0300
- Subject: [PATCH 07/10] URL: fixed setters for empty hosts.
++Subject: [PATCH 07/12] URL: fixed setters for empty hosts.
 +MIME-Version: 1.0
 +Content-Type: text/plain; charset=UTF-8
 +Content-Transfer-Encoding: 8bit
 +
 +Empty non-special hosts were represented as empty opaque hosts, so
 +lxb_url_cannot_have_user_pass_port() allowed username, password, and port
 +setters to modify scheme://.
 +
 +For fixed this store empty opaque-host input as LXB_URL_HOST_TYPE_EMPTY.
 +
 +Per report from Máté Kocsis (@kocsismate).
 +
 +This relates to #387 issue on GitHub.
 +---
 + source/lexbor/url/url.c           | 35 ++++++++++++++++++---
 + test/files/lexbor/url/changes.ton | 52 +++++++++++++++++++++++++++++--
 + test/files/lexbor/url/url.ton     |  8 ++++-
 + 3 files changed, 86 insertions(+), 9 deletions(-)
 +
 +diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
 +index 5a11434..86bcf8f 100644
 +--- a/source/lexbor/url/url.c
 ++++ b/source/lexbor/url/url.c
 +@@ -1115,11 +1115,13 @@ lxb_url_host_copy(const lxb_url_host_t *src, lxb_url_host_t *dst,
 +
 +     dst->type = src->type;
 +
 +-    if (src->type <= LXB_URL_HOST_TYPE_OPAQUE) {
 +-        if (src->type == LXB_URL_HOST_TYPE__UNDEF) {
 +-            return LXB_STATUS_OK;
 +-        }
 ++    if (src->type == LXB_URL_HOST_TYPE__UNDEF
 ++        || src->type == LXB_URL_HOST_TYPE_EMPTY)
 ++    {
 ++        return LXB_STATUS_OK;
 ++    }
 +
 ++    if (src->type <= LXB_URL_HOST_TYPE_OPAQUE) {
 +         return lxb_url_str_copy(&src->u.domain,
 +                                 &dst->u.domain, dst_mraw);
 +     }
 +@@ -1152,6 +1154,24 @@ lxb_url_host_set_empty(lxb_url_host_t *host, lexbor_mraw_t *mraw)
 +     host->type = LXB_URL_HOST_TYPE_EMPTY;
 + }
 +
 ++lxb_inline bool
 ++lxb_url_host_is_empty(const lxb_url_host_t *host)
 ++{
 ++    if (host->type == LXB_URL_HOST_TYPE_EMPTY) {
 ++        return true;
 ++    }
 ++
 ++    if (host->type == LXB_URL_HOST_TYPE_DOMAIN) {
 ++        return host->u.domain.length == 0;
 ++    }
 ++
 ++    if (host->type == LXB_URL_HOST_TYPE_OPAQUE) {
 ++        return host->u.opaque.length == 0;
 ++    }
 ++
 ++    return false;
 ++}
 ++
 + static bool
 + lxb_url_host_eq(lxb_url_host_t *host, const lxb_char_t *data, size_t length)
 + {
 +@@ -1251,7 +1271,7 @@ lxb_url_normalized_windows_drive_letter(const lxb_char_t *data,
 + static bool
 + lxb_url_cannot_have_user_pass_port(lxb_url_t *url)
 + {
 +-    return url->host.type == LXB_URL_HOST_TYPE_EMPTY
 ++    return lxb_url_host_is_empty(&url->host)
 +     || url->host.type == LXB_URL_HOST_TYPE__UNDEF
 +     || url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE;
 + }
 +@@ -3978,6 +3998,11 @@ lxb_url_opaque_host_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
 +     lxb_status_t status;
 +     const lxb_char_t *p;
 +
 ++    if (data == end) {
 ++        lxb_url_host_set_empty(host, mraw);
 ++        return LXB_STATUS_OK;
 ++    }
 ++
 +     p = data;
 +
 +     while (p < end) {
 +diff --git a/test/files/lexbor/url/changes.ton b/test/files/lexbor/url/changes.ton
 +index 07bc944..1a0b6e3 100644
 +--- a/test/files/lexbor/url/changes.ton
 ++++ b/test/files/lexbor/url/changes.ton
 +@@ -1,5 +1,5 @@
 + [
 +-    /* Test count: 1 */
 ++    /* Test count: 47 */
 +     /* 1 */
 +     {
 +         "url": "https://user:pass@lexbor.com/docs/html/path?x=y&a=b#best-fragment",
 +@@ -982,9 +982,53 @@
 +         "failed": false
 +     },
 +     /* 45 */
 ++    {
 ++        "url": "scheme://",
 ++        "done": "scheme://",
 ++        "change": {
 ++            "href": null,
 ++            "protocol": null,
 ++            "username": "user",
 ++            "password": "pass",
 ++            "host": null,
 ++            "hostname": null,
 ++            "port": "433",
 ++            "pathname": null,
 ++            "search": null,
 ++            "hash": null
 ++        },
 ++        "scheme": "scheme",
 ++        "host": "",
 ++        "path": "",
 ++        "failed": false
 ++    },
 ++    /* 46 */
 ++    {
 ++        "url": "scheme://host",
 ++        "done": "scheme://host:433",
 ++        "change": {
 ++            "href": null,
 ++            "protocol": null,
 ++            "username": null,
 ++            "password": null,
 ++            "host": null,
 ++            "hostname": null,
 ++            "port": "433",
 ++            "pathname": null,
 ++            "search": null,
 ++            "hash": null
 ++        },
 ++        "scheme": "scheme",
 ++        "host": "host",
 ++        "port": 433,
 ++        "has_port": true,
 ++        "path": "",
 ++        "failed": false
 ++    },
 ++    /* 47 */
 +     {
 +         "url": "https://example.com:432",
 +-        "done": "https://example.com:432",
 ++        "done": "https://example.com:432/",
 +         "change": {
 +             "href": null,
 +             "protocol": null,
 +@@ -999,7 +1043,9 @@
 +         },
 +         "scheme": "https",
 +         "host": "example.com",
 +-        "port": "432",
 ++        "port": 432,
 ++        "has_port": true,
 ++        "path": "/",
 +         "failed": true
 +     }
 + ]
 +diff --git a/test/files/lexbor/url/url.ton b/test/files/lexbor/url/url.ton
 +index 2baa4bc..85794c5 100644
 +--- a/test/files/lexbor/url/url.ton
 ++++ b/test/files/lexbor/url/url.ton
 +@@ -1,5 +1,5 @@
 + [
 +-    /* Test count: 7 */
 ++    /* Test count: 8 */
 +     /* 1 */
 +     {
 +         "url": "https://user:pass@lexbor.com:450/docs/lexbor/?search=lxb_status_t#version",
 +@@ -74,5 +74,11 @@
 +         "path": "",
 +         "failed": false,
 +         "encoding": "utf-8"
 ++    },
 ++    /* 8 */
 ++    {
 ++        "url": "scheme://:433",
 ++        "failed": true,
 ++        "encoding": "utf-8"
 +     }
 + ]
diff --cc ext/lexbor/patches/0008-URL-fixed-uninitialized-memory-in-the-path-buffer-gr.patch
index d967c8ca4eb,00000000000..df1f6a1c2ac
mode 100644,000000..100644
--- a/ext/lexbor/patches/0008-URL-fixed-uninitialized-memory-in-the-path-buffer-gr.patch
+++ b/ext/lexbor/patches/0008-URL-fixed-uninitialized-memory-in-the-path-buffer-gr.patch
@@@ -1,37 -1,0 +1,37 @@@
 +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 +From: Alexander Borisov <lex.borisov@gmail.com>
 +Date: Fri, 5 Jun 2026 22:13:32 +0300
- Subject: [PATCH 08/10] URL: fixed uninitialized memory in the path buffer
++Subject: [PATCH 08/12] URL: fixed uninitialized memory in the path buffer
 + growth.
 +
 +When a path was long enough to outgrow the on-stack buffer, the first
 +move to the heap didn't copy what was already written, so the start of
 +the path could contain garbage. Also fixed the 'last' pointer after the
 +move.
 +
 +Per report from Recep Asan (recep@asan.me)
 +---
 + source/lexbor/url/url.c | 3 ++-
 + 1 file changed, 2 insertions(+), 1 deletion(-)
 +
 +diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
 +index 86bcf8f..b6c0a1e 100644
 +--- a/source/lexbor/url/url.c
 ++++ b/source/lexbor/url/url.c
 +@@ -499,6 +499,7 @@ lxb_url_scheme_length = sizeof(lxb_url_scheme_res) / sizeof(lxb_url_scheme_data_
 +             if (tmp == NULL) {                                                \
 +                 return NULL;                                                  \
 +             }                                                                 \
 ++            memcpy(tmp, (sbuf_begin), offset);                                \
 +         }                                                                     \
 +         else {                                                                \
 +             tmp = lexbor_realloc((sbuf_begin), new_len);                      \
 +@@ -509,7 +510,7 @@ lxb_url_scheme_length = sizeof(lxb_url_scheme_res) / sizeof(lxb_url_scheme_data_
 +         }                                                                     \
 +                                                                               \
 +         (sbuf) = tmp + offset;                                                \
 +-        (last) = sbuf + lst;                                                  \
 ++        (last) = tmp + lst;                                                   \
 +         (sbuf_begin) = tmp;                                                   \
 +         (sbuf_end) = tmp + new_len;                                           \
 +     }                                                                         \
diff --cc ext/lexbor/patches/0009-Fix-parsing-for-URL-containing-empty-host-and-userin.patch
index 1e7aa4d1087,00000000000..9abcd37fe3f
mode 100644,000000..100644
--- a/ext/lexbor/patches/0009-Fix-parsing-for-URL-containing-empty-host-and-userin.patch
+++ b/ext/lexbor/patches/0009-Fix-parsing-for-URL-containing-empty-host-and-userin.patch
@@@ -1,48 -1,0 +1,48 @@@
 +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 +From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= <kocsismate@woohoolabs.com>
 +Date: Thu, 9 Jul 2026 21:51:05 +0200
- Subject: [PATCH 09/10] Fix parsing for URL containing empty host and userinfo
++Subject: [PATCH 09/12] Fix parsing for URL containing empty host and userinfo
 +
 +The returned error code (LXB_URL_ERROR_TYPE_INVALID_CREDENTIALS) apparently contradicts the specification:
 +
 +"If atSignSeen is true and buffer is the empty string, host-missing validation error, return failure."
 +---
 + source/lexbor/url/url.c                     | 2 +-
 + test/files/lexbor/url/username_password.ton | 8 +++++++-
 + 2 files changed, 8 insertions(+), 2 deletions(-)
 +
 +diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
 +index b6c0a1e..19ec238 100644
 +--- a/source/lexbor/url/url.c
 ++++ b/source/lexbor/url/url.c
 +@@ -1814,7 +1814,7 @@ again:
 +         if (at_sign) {
 +             if (begin == p || begin == p - 1) {
 +                 status = lxb_url_log_append(parser, p,
 +-                                            LXB_URL_ERROR_TYPE_INVALID_CREDENTIALS);
 ++                                            LXB_URL_ERROR_TYPE_HOST_MISSING);
 +                 if (status != LXB_STATUS_OK) {
 +                     lxb_url_parse_return(orig_data, buf, status);
 +                 }
 +diff --git a/test/files/lexbor/url/username_password.ton b/test/files/lexbor/url/username_password.ton
 +index 28a27fd..5a5e63e 100644
 +--- a/test/files/lexbor/url/username_password.ton
 ++++ b/test/files/lexbor/url/username_password.ton
 +@@ -1,5 +1,5 @@
 + [
 +-    /* Test count: 11 */
 ++    /* Test count: 12 */
 +     /* 1 */
 +     {
 +         "url": "https://user:password@lexbor.com",
 +@@ -124,4 +124,10 @@
 +         "failed": false,
 +         "encoding": "utf-8"
 +     }
 ++    /* 12 */
 ++    {
 ++        "url": "https://user:pass@",
 ++        "failed": true,
 ++        "encoding": "utf-8"
 ++    }
 + ]
diff --cc ext/lexbor/patches/0010-Percent-encode-the-caret-in-the-path.patch
index 04d91bda68a,00000000000..48777d5de6d
mode 100644,000000..100644
--- a/ext/lexbor/patches/0010-Percent-encode-the-caret-in-the-path.patch
+++ b/ext/lexbor/patches/0010-Percent-encode-the-caret-in-the-path.patch
@@@ -1,29 -1,0 +1,29 @@@
 +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 +From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= <kocsismate@woohoolabs.com>
 +Date: Fri, 10 Jul 2026 22:31:16 +0200
- Subject: [PATCH 10/10] Percent-encode the caret in the path
++Subject: [PATCH 10/12] Percent-encode the caret in the path
 +
 +The caret (^) is part of the path percent-encode set:
 +
 +"The path percent-encode set is a percent-encode set consisting of the query percent-encode set and U+003F (?), U+005E (^), U+0060 (`), U+007B ({), and U+007D (})."
 +
 +Until now, this character wasn't percent-encoded in the path likely due to a copy-paste error. This is mistake is fixed by adding LXB_URL_MAP_PATH to the lxb_url_map entry for the caret.
 +
 +Originally reported at https://github.com/php/php-src/issues/22628
 +---
 + source/lexbor/url/url.c | 2 +-
 + 1 file changed, 1 insertion(+), 1 deletion(-)
 +
 +diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
 +index 19ec238..de19239 100644
 +--- a/source/lexbor/url/url.c
 ++++ b/source/lexbor/url/url.c
 +@@ -159,7 +159,7 @@ static const uint8_t lxb_url_map[256] =
 +     LXB_URL_MAP_USERINFO|LXB_URL_MAP_COMPONENT|LXB_URL_MAP_X_WWW_FORM, /* 0x5b ([) */
 +     LXB_URL_MAP_USERINFO|LXB_URL_MAP_COMPONENT|LXB_URL_MAP_X_WWW_FORM, /* 0x5c (\) */
 +     LXB_URL_MAP_USERINFO|LXB_URL_MAP_COMPONENT|LXB_URL_MAP_X_WWW_FORM, /* 0x5d (]) */
 +-    LXB_URL_MAP_USERINFO|LXB_URL_MAP_COMPONENT|LXB_URL_MAP_X_WWW_FORM, /* 0x5e (^) */
 ++    LXB_URL_MAP_USERINFO|LXB_URL_MAP_PATH|LXB_URL_MAP_COMPONENT|LXB_URL_MAP_X_WWW_FORM, /* 0x5e (^) */
 +     LXB_URL_MAP_UNDEF, /* 0x5f (_) */
 +     LXB_URL_MAP_PATH|LXB_URL_MAP_FRAGMENT|LXB_URL_MAP_USERINFO|LXB_URL_MAP_COMPONENT|LXB_URL_MAP_X_WWW_FORM, /* 0x60 (`) */
 +     LXB_URL_MAP_UNDEF, /* 0x61 (a) */
diff --cc ext/lexbor/patches/0011-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch
index 00000000000,cc689714b6c..3928daa7eab
mode 000000,100644..100644
--- a/ext/lexbor/patches/0011-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch
+++ b/ext/lexbor/patches/0011-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch
@@@ -1,0 -1,35 +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()
++Subject: [PATCH 11/12] 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 --cc ext/lexbor/patches/0012-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch
index 00000000000,352ebd95b2c..32f3aff4ee7
mode 000000,100644..100644
--- a/ext/lexbor/patches/0012-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch
+++ b/ext/lexbor/patches/0012-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch
@@@ -1,0 -1,97 +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
++Subject: [PATCH 12/12] 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;