From 854136fb9483de1f098dd0746680714776c8fc95 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 10 Apr 2026 18:39:58 -0400 Subject: [PATCH] Fix GH-12703 and optimize parse_url control-char replacement Two changes to ext/standard/url.c. 1. Fix GH-12703: parse_url mishandles colon inside path. parse_url('/page:1') returns false instead of the path, and parse_url('//www.example.com/foo:65535/') reports a spurious port of 65535. Both stem from the scheme walk's error branch routing /-prefixed inputs into the `parse_port` fallback. Guard the `parse_port` branch with `*s != '/'`. A leading slash can't start a scheme, so the input must be a path or a //host authority. The existing branch ordering then lets the //host check catch `//...` inputs and `just_path` catch the rest. 2. Speed up php_replace_controlchars on typical URLs. Each parse calls php_replace_controlchars once per component (scheme, host, user, pass, path, query, fragment), and each call walked the bytes through iscntrl(). iscntrl() hits __ctype_b_loc() for a locale-aware lookup, and callgrind showed it at ~14% of total instructions on a realistic URL workload. Replace the call with an inline `*s < 0x20 || *s == 0x7f` compare. URL components are bytes, not locale-dependent text, so C-locale semantics are what we want regardless of the process locale. The Zend language scanner uses the same pattern (`yych <= 0x1F`). Benchmark across 13 realistic URL shapes: 12-22% faster, 16% average. No behavior change in the C/POSIX locale. Closes GH-12703 --- ext/standard/tests/url/gh12703.phpt | 104 ++++++++++++++++++++++++++++ ext/standard/url.c | 12 +++- 2 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/url/gh12703.phpt diff --git a/ext/standard/tests/url/gh12703.phpt b/ext/standard/tests/url/gh12703.phpt new file mode 100644 index 000000000000..95a5e151c09d --- /dev/null +++ b/ext/standard/tests/url/gh12703.phpt @@ -0,0 +1,104 @@ +--TEST-- +GH-12703 (parse_url mishandles colon inside path) +--FILE-- + +--EXPECT-- +array(1) { + ["path"]=> + string(7) "/page:1" +} +string(7) "/page:1" +NULL +array(2) { + ["path"]=> + string(7) "/page:1" + ["query"]=> + string(7) "foo=bar" +} +array(1) { + ["path"]=> + string(2) "/:" +} +array(1) { + ["path"]=> + string(4) "/:80" +} +array(2) { + ["host"]=> + string(15) "www.example.com" + ["path"]=> + string(11) "/foo:65535/" +} +array(2) { + ["host"]=> + string(15) "www.example.com" + ["path"]=> + string(7) "/foo:1/" +} +array(2) { + ["host"]=> + string(15) "www.example.com" + ["path"]=> + string(11) "/foo:65536/" +} +array(2) { + ["host"]=> + string(4) "host" + ["path"]=> + string(9) "/a:1/b:2/" +} +array(3) { + ["host"]=> + string(15) "www.example.com" + ["port"]=> + int(8080) + ["path"]=> + string(11) "/foo:65535/" +} +array(8) { + ["scheme"]=> + string(6) "scheme" + ["host"]=> + string(4) "host" + ["port"]=> + int(8080) + ["user"]=> + string(4) "user" + ["pass"]=> + string(4) "pass" + ["path"]=> + string(8) "/a:1/b:2" + ["query"]=> + string(3) "q=1" + ["fragment"]=> + string(1) "f" +} diff --git a/ext/standard/url.c b/ext/standard/url.c index 4ddf7f80c64f..cfc7a74d1c4b 100644 --- a/ext/standard/url.c +++ b/ext/standard/url.c @@ -54,9 +54,15 @@ static void php_replace_controlchars(char *str, size_t len) ZEND_ASSERT(str != NULL); + /* Replace ASCII C0 control chars (0x00..0x1F) and DEL (0x7F). An inline + * comparison is used instead of iscntrl() because (a) it avoids the + * per-byte locale lookup through __ctype_b_loc(), and (b) URL components + * are bytes, not locale-dependent text, so the C-locale semantics are + * what we want regardless of the process locale. The compiler can also + * auto-vectorize this simple form. */ while (s < e) { - if (iscntrl(*s)) { - *s='_'; + if (UNEXPECTED(*s < 0x20 || *s == 0x7f)) { + *s = '_'; } s++; } @@ -104,7 +110,7 @@ PHPAPI php_url *php_url_parse_ex2(char const *str, size_t length, bool *has_port while (p < e) { /* scheme = 1*[ lowalpha | digit | "+" | "-" | "." ] */ if (!isalpha(*p) && !isdigit(*p) && *p != '+' && *p != '.' && *p != '-') { - if (e + 1 < ue && e < binary_strcspn(s, ue, "?#")) { + if (*s != '/' && e + 1 < ue && e < binary_strcspn(s, ue, "?#")) { goto parse_port; } else if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */ s += 2;