diff --git a/NEWS b/NEWS
index 37f27f3572c3..115e6e3cc4f1 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,13 @@ PHP NEWS
registrations are freed while still reachable from the cycle collector.
(Ilia Alshanetsky)
+- Lexbor:
+ . Merge patches 859f100, 8a14bc0, f67ce4b, a36e09a and b0f7412, fixing a heap
+ buffer overflow in :lexbor-contains() parsing, buffer overflows in malformed
+ decode replay, uninitialized memory in IDNA buffer growth, dropped usernames
+ containing an at sign and the URLSearchParams tail pointer.
+ (alexandre-daubois)
+
24 Sep 2026, PHP 8.5.11
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 000000000000..795a8f110e37
--- /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--
+needle
', 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)
diff --git a/ext/lexbor/lexbor/css/selectors/pseudo_state.c b/ext/lexbor/lexbor/css/selectors/pseudo_state.c
index 263ca52f35e4..2321ddf01bbe 100644
--- a/ext/lexbor/lexbor/css/selectors/pseudo_state.c
+++ b/ext/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/lexbor/lexbor/encoding/decode.c b/ext/lexbor/lexbor/encoding/decode.c
index 3e48971e3657..05c4b9bde771 100644
--- a/ext/lexbor/lexbor/encoding/decode.c
+++ b/ext/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/lexbor/lexbor/unicode/idna.c b/ext/lexbor/lexbor/unicode/idna.c
index 754f6b2026de..b31ae3338b92 100644
--- a/ext/lexbor/lexbor/unicode/idna.c
+++ b/ext/lexbor/lexbor/unicode/idna.c
@@ -117,12 +117,14 @@ lxb_unicode_idna_realloc(lxb_codepoint_t *buf, const lxb_codepoint_t *buffer,
lxb_codepoint_t *tmp;
nlen = ((*buf_end - buf) * 4) + len;
-
+
if (buf == buffer) {
tmp = lexbor_malloc(nlen * sizeof(lxb_codepoint_t));
if (tmp == NULL) {
return NULL;
}
+
+ memcpy(tmp, buf, (*buf_p - buf) * sizeof(lxb_codepoint_t));
}
else {
tmp = lexbor_realloc(buf, nlen * sizeof(lxb_codepoint_t));
@@ -458,13 +460,17 @@ lxb_unicode_idna_ascii_puny_cb(const lxb_char_t *data, size_t length, void *ctx,
if (asc->buf == asc->buffer) {
tmp = lexbor_malloc(nlen);
+ if (tmp == NULL) {
+ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
+ }
+
+ memcpy(tmp, asc->buf, asc->p - asc->buf);
}
else {
tmp = lexbor_realloc(asc->buf, nlen);
- }
-
- if (tmp == NULL) {
- return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
+ if (tmp == NULL) {
+ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
+ }
}
asc->p = tmp + (asc->p - asc->buf);
@@ -711,13 +717,17 @@ lxb_unicode_idna_to_unicode_cb(const lxb_codepoint_t *part, size_t len,
if (asc->buf == asc->buffer) {
tmp = lexbor_malloc(nlen);
+ if (tmp == NULL) {
+ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
+ }
+
+ memcpy(tmp, asc->buf, asc->p - asc->buf);
}
else {
tmp = lexbor_realloc(asc->buf, nlen);
- }
-
- if (tmp == NULL) {
- return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
+ if (tmp == NULL) {
+ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
+ }
}
asc->p = tmp + (asc->p - asc->buf);
diff --git a/ext/lexbor/lexbor/url/url.c b/ext/lexbor/lexbor/url/url.c
index de19239936a1..654e2e6970b7 100644
--- a/ext/lexbor/lexbor/url/url.c
+++ b/ext/lexbor/lexbor/url/url.c
@@ -1753,16 +1753,13 @@ lxb_url_parse_basic_h(lxb_url_parser_t *parser, lxb_url_t *url,
break;
}
- if (pswd == NULL || !at_sign) {
- tmp = (pswd != NULL) ? pswd - 1 : p;
-
- if (tmp > begin) {
- status = lxb_url_percent_encode_after_utf_8(begin, tmp,
- &url->username, url->mraw,
- LXB_URL_MAP_USERINFO, false);
- if (status != LXB_STATUS_OK) {
- lxb_url_parse_return(orig_data, buf, status);
- }
+ tmp = (pswd != NULL) ? pswd - 1 : p;
+ if (tmp > begin) {
+ status = lxb_url_percent_encode_after_utf_8(begin, tmp,
+ &url->username, url->mraw,
+ LXB_URL_MAP_USERINFO, false);
+ if (status != LXB_STATUS_OK) {
+ lxb_url_parse_return(orig_data, buf, status);
}
}
@@ -5106,6 +5103,8 @@ lxb_url_search_params_parse(lxb_url_search_params_t *search_params,
return status;
}
+ last = entry;
+
lexbor_str_init(&entry->value, mraw, 0);
if (entry->value.data == NULL) {
return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
diff --git 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
index 533598837822..6bc4929e9b07 100644
--- 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 @@
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 01/15] Expose line and column information for use in PHP
---
source/lexbor/dom/interfaces/node.h | 2 ++
diff --git 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
index 8814d5955354..29bc4b12adce 100644
--- 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 @@
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 02/15] Track implied added nodes for options use in PHP
---
source/lexbor/html/tree.h | 3 +++
diff --git 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
index aa4802320491..286fd2e16fd7 100644
--- 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,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 03/10] Patch utilities and data structure to be able to
+Subject: [PATCH 03/15] 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/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch b/ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
index 1a28b21ccdc5..34b58217aa6f 100644
--- 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 @@
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 04/15] Remove unused upper case tag static data
---
source/lexbor/tag/res.h | 2 ++
diff --git 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
index a1dda1fcd112..0c88f6031720 100644
--- 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 @@
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 05/15] Shrink size of static binary search tree
This also makes it more efficient on the data cache.
---
diff --git a/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch b/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
index 57f1e0e92fcb..3e31f7925889 100644
--- 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 @@
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 06/15] Patch out unused CSS style code
---
source/lexbor/css/rule.h | 2 ++
diff --git a/ext/lexbor/patches/0007-URL-fixed-setters-for-empty-hosts.patch b/ext/lexbor/patches/0007-URL-fixed-setters-for-empty-hosts.patch
index b55f5aac5894..309d3fdad75b 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,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexander Borisov
Date: Fri, 26 Jun 2026 18:55:56 +0300
-Subject: [PATCH 07/10] URL: fixed setters for empty hosts.
+Subject: [PATCH 07/15] URL: fixed setters for empty hosts.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
diff --git 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
index d967c8ca4eb9..5d5eb2301f70 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,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexander Borisov
Date: Fri, 5 Jun 2026 22:13:32 +0300
-Subject: [PATCH 08/10] URL: fixed uninitialized memory in the path buffer
+Subject: [PATCH 08/15] URL: fixed uninitialized memory in the path buffer
growth.
When a path was long enough to outgrow the on-stack buffer, the first
diff --git 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
index 1e7aa4d1087d..23a2466a7b4a 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,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?=
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/15] Fix parsing for URL containing empty host and userinfo
The returned error code (LXB_URL_ERROR_TYPE_INVALID_CREDENTIALS) apparently contradicts the specification:
diff --git 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
index 04d91bda68a9..764903f69676 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,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?=
Date: Fri, 10 Jul 2026 22:31:16 +0200
-Subject: [PATCH 10/10] Percent-encode the caret in the path
+Subject: [PATCH 10/15] Percent-encode the caret in the path
The caret (^) is part of the path percent-encode set:
diff --git a/ext/lexbor/patches/0011-URL-fixed-tail-pointer-in-URLSearchParams-for-delimi.patch b/ext/lexbor/patches/0011-URL-fixed-tail-pointer-in-URLSearchParams-for-delimi.patch
new file mode 100644
index 000000000000..ab11f88d126e
--- /dev/null
+++ b/ext/lexbor/patches/0011-URL-fixed-tail-pointer-in-URLSearchParams-for-delimi.patch
@@ -0,0 +1,29 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov
+Date: Fri, 5 Jun 2026 21:46:26 +0300
+Subject: [PATCH 11/15] URL: fixed tail pointer in URLSearchParams for
+ delimiter-free query.
+
+When a query had a single token without '=' or '&' (e.g. "?abc"), the
+internal tail pointer wasn't updated, so a later append() could lose the
+added parameter (and write through a stale pointer). Fixed by keeping the
+tail pointer in sync.
+
+Per report from Xiansheng Cao (@HMF2021)
+---
+ source/lexbor/url/url.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
+index de19239..fcae2d6 100644
+--- a/source/lexbor/url/url.c
++++ b/source/lexbor/url/url.c
+@@ -5106,6 +5106,8 @@ lxb_url_search_params_parse(lxb_url_search_params_t *search_params,
+ return status;
+ }
+
++ last = entry;
++
+ lexbor_str_init(&entry->value, mraw, 0);
+ if (entry->value.data == NULL) {
+ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
diff --git a/ext/lexbor/patches/0012-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch b/ext/lexbor/patches/0012-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch
new file mode 100644
index 000000000000..43b798227519
--- /dev/null
+++ b/ext/lexbor/patches/0012-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
+Date: Fri, 5 Jun 2026 22:34:23 +0300
+Subject: [PATCH 12/15] 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/lexbor/patches/0013-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch b/ext/lexbor/patches/0013-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch
new file mode 100644
index 000000000000..3bc0e44bb528
--- /dev/null
+++ b/ext/lexbor/patches/0013-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch
@@ -0,0 +1,97 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov
+Date: Wed, 10 Jun 2026 19:50:10 +0300
+Subject: [PATCH 13/15] 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/lexbor/patches/0014-URL-Fix-parsing-of-usernames-containing.patch b/ext/lexbor/patches/0014-URL-Fix-parsing-of-usernames-containing.patch
new file mode 100644
index 000000000000..dce9ec511e8f
--- /dev/null
+++ b/ext/lexbor/patches/0014-URL-Fix-parsing-of-usernames-containing.patch
@@ -0,0 +1,38 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Tim=20D=C3=BCsterhus?=
+Date: Fri, 31 Jul 2026 20:56:18 +0200
+Subject: [PATCH 14/15] URL: Fix parsing of usernames containing `@`
+
+Fixes lexbor/lexbor#399.
+---
+ source/lexbor/url/url.c | 17 +++++++----------
+ 1 file changed, 7 insertions(+), 10 deletions(-)
+
+diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
+index fcae2d6..654e2e6 100644
+--- a/source/lexbor/url/url.c
++++ b/source/lexbor/url/url.c
+@@ -1753,16 +1753,13 @@ again:
+ break;
+ }
+
+- if (pswd == NULL || !at_sign) {
+- tmp = (pswd != NULL) ? pswd - 1 : p;
+-
+- if (tmp > begin) {
+- status = lxb_url_percent_encode_after_utf_8(begin, tmp,
+- &url->username, url->mraw,
+- LXB_URL_MAP_USERINFO, false);
+- if (status != LXB_STATUS_OK) {
+- lxb_url_parse_return(orig_data, buf, status);
+- }
++ tmp = (pswd != NULL) ? pswd - 1 : p;
++ if (tmp > begin) {
++ status = lxb_url_percent_encode_after_utf_8(begin, tmp,
++ &url->username, url->mraw,
++ LXB_URL_MAP_USERINFO, false);
++ if (status != LXB_STATUS_OK) {
++ lxb_url_parse_return(orig_data, buf, status);
+ }
+ }
+
diff --git a/ext/lexbor/patches/0015-Unicode-fixed-uninitialized-memory-in-IDNA-buffer-gr.patch b/ext/lexbor/patches/0015-Unicode-fixed-uninitialized-memory-in-IDNA-buffer-gr.patch
new file mode 100644
index 000000000000..7d1ef98a49b7
--- /dev/null
+++ b/ext/lexbor/patches/0015-Unicode-fixed-uninitialized-memory-in-IDNA-buffer-gr.patch
@@ -0,0 +1,81 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov
+Date: Mon, 7 Sep 2026 22:11:39 +0300
+Subject: [PATCH 15/15] Unicode: fixed uninitialized memory in IDNA buffer
+ growth.
+
+When an IDNA buffer outgrew the stack allocation, the move to the heap
+did not copy the existing contents. The converted domain could therefore
+contain uninitialized heap data.
+
+Fixed copying of the codepoint, ASCII and UTF-8 buffers.
+
+Per report from Muhammad Daffa (@daffainfo).
+---
+ source/lexbor/unicode/idna.c | 28 +++++++++++++++++++---------
+ 1 file changed, 19 insertions(+), 9 deletions(-)
+
+diff --git a/source/lexbor/unicode/idna.c b/source/lexbor/unicode/idna.c
+index 754f6b2..b31ae33 100644
+--- a/source/lexbor/unicode/idna.c
++++ b/source/lexbor/unicode/idna.c
+@@ -117,12 +117,14 @@ lxb_unicode_idna_realloc(lxb_codepoint_t *buf, const lxb_codepoint_t *buffer,
+ lxb_codepoint_t *tmp;
+
+ nlen = ((*buf_end - buf) * 4) + len;
+-
++
+ if (buf == buffer) {
+ tmp = lexbor_malloc(nlen * sizeof(lxb_codepoint_t));
+ if (tmp == NULL) {
+ return NULL;
+ }
++
++ memcpy(tmp, buf, (*buf_p - buf) * sizeof(lxb_codepoint_t));
+ }
+ else {
+ tmp = lexbor_realloc(buf, nlen * sizeof(lxb_codepoint_t));
+@@ -458,13 +460,17 @@ lxb_unicode_idna_ascii_puny_cb(const lxb_char_t *data, size_t length, void *ctx,
+
+ if (asc->buf == asc->buffer) {
+ tmp = lexbor_malloc(nlen);
++ if (tmp == NULL) {
++ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
++ }
++
++ memcpy(tmp, asc->buf, asc->p - asc->buf);
+ }
+ else {
+ tmp = lexbor_realloc(asc->buf, nlen);
+- }
+-
+- if (tmp == NULL) {
+- return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
++ if (tmp == NULL) {
++ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
++ }
+ }
+
+ asc->p = tmp + (asc->p - asc->buf);
+@@ -711,13 +717,17 @@ lxb_unicode_idna_to_unicode_cb(const lxb_codepoint_t *part, size_t len,
+
+ if (asc->buf == asc->buffer) {
+ tmp = lexbor_malloc(nlen);
++ if (tmp == NULL) {
++ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
++ }
++
++ memcpy(tmp, asc->buf, asc->p - asc->buf);
+ }
+ else {
+ tmp = lexbor_realloc(asc->buf, nlen);
+- }
+-
+- if (tmp == NULL) {
+- return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
++ if (tmp == NULL) {
++ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
++ }
+ }
+
+ asc->p = tmp + (asc->p - asc->buf);
diff --git a/ext/uri/tests/whatwg/parsing/host_success_multibyte_long.phpt b/ext/uri/tests/whatwg/parsing/host_success_multibyte_long.phpt
new file mode 100644
index 000000000000..109cb39de1f0
--- /dev/null
+++ b/ext/uri/tests/whatwg/parsing/host_success_multibyte_long.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Test Uri\WhatWg\Url parsing - host - IDN host longer than the IDNA on-stack buffer
+--FILE--
+getAsciiHost();
+
+var_dump(strlen($host));
+var_dump(substr($host, 0, 12));
+var_dump(substr($host, -6));
+var_dump(substr_count($host, "a"));
+var_dump($url->getUnicodeHost() === str_repeat("é", 5000) . ".com");
+
+?>
+--EXPECT--
+int(5010)
+string(12) "xn--9caaaaaa"
+string(6) "aa.com"
+int(5000)
+bool(true)
diff --git a/ext/uri/tests/whatwg/parsing/username_success_at_sign.phpt b/ext/uri/tests/whatwg/parsing/username_success_at_sign.phpt
new file mode 100644
index 000000000000..910d984858f3
--- /dev/null
+++ b/ext/uri/tests/whatwg/parsing/username_success_at_sign.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\Url parsing - username - at sign in the username and the password
+--FILE--
+getUsername());
+var_dump($url->getPassword());
+var_dump($url->getAsciiHost());
+var_dump($url->toAsciiString());
+
+?>
+--EXPECT--
+string(11) "user%40name"
+string(11) "pass%40word"
+string(9) "localhost"
+string(41) "http://user%40name:pass%40word@localhost/"