From 6f54ba3a9846ed270548a967cadc267ffe5a715c Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 11 Sep 2026 10:51:29 +0200 Subject: [PATCH] lexbor: Merge upstream memory safety fixes 8a14bc0 and f67ce4b --- NEWS | 5 + .../lexbor/css/selectors/pseudo_state.c | 5 +- ext/dom/lexbor/lexbor/encoding/decode.c | 37 ++++++- ...nd-column-information-for-use-in-PHP.patch | 2 +- ...d-added-nodes-for-options-use-in-PHP.patch | 2 +- ...and-data-structure-to-be-able-to-gen.patch | 2 +- ...ve-unused-upper-case-tag-static-data.patch | 2 +- ...nk-size-of-static-binary-search-tree.patch | 2 +- ...0006-Patch-out-unused-CSS-style-code.patch | 2 +- ...uffer-overflow-in-lexbor-contains-pa.patch | 35 +++++++ ...uffer-overflows-in-malformed-decode-.patch | 97 +++++++++++++++++++ .../modern/css_selectors/lexbor_contains.phpt | 16 +++ 12 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 ext/dom/lexbor/patches/0007-CSS-fixed-heap-buffer-overflow-in-lexbor-contains-pa.patch create mode 100644 ext/dom/lexbor/patches/0008-Encoding-fixed-buffer-overflows-in-malformed-decode-.patch create mode 100644 ext/dom/tests/modern/css_selectors/lexbor_contains.phpt diff --git a/NEWS b/NEWS index 1fd676d2a649..32b7b732b74d 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,11 @@ PHP NEWS registrations are freed while still reachable from the cycle collector. (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) + 24 Sep 2026, PHP 8.4.26 diff --git a/ext/dom/lexbor/lexbor/css/selectors/pseudo_state.c b/ext/dom/lexbor/lexbor/css/selectors/pseudo_state.c index 263ca52f35e4..2321ddf01bbe 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 3e48971e3657..05c4b9bde771 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 1b35913e91c7..04136b29f58f 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 24bc21630513..615655d7f2ec 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 9c67ba740c4a..73c5afa19e12 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 4640a03647b9..cc0a65a7cd5c 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 d276ba8f2efa..b84120bf6c8c 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 827375f3f05c..196a5a8a62de 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 000000000000..cc689714b6cc --- /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 +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 000000000000..352ebd95b2c4 --- /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 +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 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)