Repository navigation
Bump WebKit (oven-sh/WebKit#724 preview): URL host setters and URLPattern no longer turn IDNA-mapped delimiters into URL structure - #43883
Draft
robobun wants to merge 2 commits into
Conversation
Collaborator
Author
|
Status Reproduced on 1.4.3-canary.1+6d504dd98 (linux x64), with Node.js v26.10.0 as the reference: const u = new URL("https://base.test/p");
u.hostname = "bad.c\u2100.good.com";
u.href; // bun: "https://bad.ca/c.good.com/p" node: "https://base.test/p" (refused)
new URLPattern({ hostname: "*.trusted.example" }).test({ hostname: "evil.example\uFF20a.trusted.example" });
// bun: true node: false
const v = new URL("https://example.com:444/p");
v.hostname = "\u00DF%41.de";
v.host; // bun: "xn--a-4ka.de:444" node and new URL(): "xn--a-pfa.de:444"
The fix is oven-sh/WebKit#724. It merges cleanly, its CI is green, and it waits for a maintainer. This PR is its bun side (pin and tests) and stays a draft until that merges.
|
…the constructor Rows for oven-sh/WebKit#724, which deletes the IDNA pass that WTF::URL::setHost and setHostAndPort ran on the raw value before the URL parser saw it. - url.test.ts: 33 values through hostname=, host=, host= with a port and the constructor: non-ASCII with %XX, with tab or newline, the code points that IDNA maps to a delimiter (U+2100, the fullwidth forms), IPv6 literals with a fullwidth digit. Five special schemes. href stays an 8-bit string. - urlpattern.test.ts: 19 hostnames through the pattern constructor and exec() input, and a dictionary input that maps to text with '@'. - pathToFileURL.test.ts: a UNC host with a tab or a %XX escape. Every expected value is what Node.js v26.10.0 gives. 45 of these tests fail on WebKit 299c5323. Some rows come from #33757.
oven-sh/WebKit#724 deletes the IDNA pass that WTF::URL::setHost and setHostAndPort ran on the raw value. The preview pin is for CI only. It becomes the merged oven-sh/WebKit commit before this PR leaves draft.
robobun
force-pushed
the
robobun/8e4b8820/url-set-host-single-idna-pass
branch
from
September 27, 2026 11:33
e826196 to
c331329
Compare
Collaborator
Author
|
Updated 5:07 AM PT - Sep 27th, 2026
✅ @robobun, your commit c331329740d302a1dfd040e3ea6253b26dfa1e2e passed in 🧪 To try this PR locally: bunx bun-pr 43883That installs a local version of the PR into your bun-43883 --bun |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft until oven-sh/WebKit#724 merges.
WEBKIT_VERSIONis its preview build, for CI on every platform. The merged commit replaces it before this leaves draft.Problem
url.hostnameorurl.hostvalue can write the user name, path, query, fragment and port. On 1.4.3,hostname = "bad.c℀.good.com"(U+2100) onhttps://base.test/pgiveshttps://bad.ca/c.good.com/p. Node.js v26.10.0 refuses the value.new URLPattern({ hostname: "*.trusted.example" }).test({ hostname: "evil.example@a.trusted.example" })istrue. Node.js givesfalse.appendEncodedHostnameinWTF::URL::setHost(vendor/WebKit/Source/WTF/wtf/URL.cpp:496). It runs IDNA on the raw value, and the result is spliced into the URL string and parsed as URL structure. IDNA maps 17 BMP code points to text with a delimiter (℀toa/c,@to@). The pass also runs before percent-decoding:"ß%41.de"givesxn--a-4ka.de, the constructor givesxn--a-pfa.de.Fix
url.test.ts(33 values),urlpattern.test.ts(19 hostnames and the row above),pathToFileURL.test.ts(UNC hosts). Every expected value is from Node.js v26.10.0.f20ce774. With this pinbun bd teston the three files gives 499 pass, and the URL suites show no new failure.Background
URLsetters (URLDecomposition.cpp) and URLPattern (URLPatternCanonical.cpp:129) both callWTF::URL::setHost. It splices the value into the URL string and parses the whole string again.URLDecomposition.cpp(url: preprocess host/hostname setter input so IDNA matches the constructor #33757): it keeps the pass, and URLPattern does not go through that file.Downsides
hostname=costs what the constructor pays: +1.9% to +3.6% instructions and one to three more allocations. An ASCII value costs 13% less. Table in Notes."[::1]"(U+FF11) set[::1].Notes
Self-review, not taken. (1) Fold a host-state parse into oven-sh/WebKit#724: that PR stays a deletion so that it is quick to review, and names the rest as step 2. (2) Open no companion PR until oven-sh/WebKit#724 merges: this one is a draft, so a preview pin cannot merge, and it gives CI results for every platform.
Provenance and lineage. Found by differential tests against Node.js v26.10.0. No user reported it. The code is inherited from upstream WebKit, which still has it.
bad.c℀.good.comis the value from HackerOne 678487 that Node.js refuses inurl.parse()since nodejs/node#38631. bun's legacyurl.parse()already has that guard (src/js/node/url.ts:392). The WHATWG setters and URLPattern did not. This completes #38013 and #39273, which removed the IDNA pre-pass in front of the constructor.Reach. Plain IDN hosts and ASCII hosts give the same result before and after. The value must reach a setter as decoded text:
searchParams.get("h")for?h=bad.c%E2%84%80.good.comdoes. A raw header value does not, because header bytes decode as Latin-1 and the 17 code points are all above U+00FF. WPTurl/resources/setters_tests.json(c48d58747e) has no row for this class: 274 of 278 pass before and after.Concerns left to other work.
hostname = "/"onhttps://h/evil.example/pgiveshttps://evil.example/p). It is the same before and after this change, and it has a different cause:WTF::URL::setHostcuts and checks the raw value before the parser removes tab and newline. [WTF] URL::setHost parses the raw host: IDNA-mapped delimiters no longer become URL structure WebKit#724 names it as step 2. It belongs in WTF, because URLPattern does not go throughURLDecomposition.cpp. url: strip ASCII tab and newline in the host, hostname and pathname setters before inspecting the value #39458 covers the tab and newline part inURLDecomposition.cpptoday.host = "x:443"on anhttps:URL keeps the old port) is not touched here. url: follow the spec for the host/port setters, origin, and URLPattern groups #32826 owns it.Unblocks. The draft #43872 drops the
xn--reject layer (hasAcceptableHost). Without that layeru.hostname = "é.%78n--a"setsxn--9ca.xn--aon the current pin. With this pin the parser percent-decodes first, seesé.xn--a, ToASCII fails, and the set is refused.Measurements. Release build of
6d504dd98, relinked twice from the same link line with one object in front oflibWTF.a:URL.cppat299c5323andURL.cppwith the fix. Instructions are single-step counts on the main thread inside the native setter, JIT off, steady state. Allocator calls are hits onmi_malloc.hostname=other.examplebücher.exampleאבג.examplehost=with:8080other.examplebücher.exampleאבג.examplepathname=after onehostname=/one/twoThe extra allocation is the parser's copy of its result, which the constructor also pays. For
אבג.exampletwo more come from bun'shasValidPunycodeHost, which now gets an 8-bit host and makes it 16-bit for ICU, twice per set.hrefwas a 16-bit string after any host set and is 8-bit now (jscDescribe), which is why the laterpathname=is cheaper. Binary: text 80,684,744 to 80,681,159 bytes (-3,585).[policy-decision:webkit] gate passed · iteration 0 · 4 files touched
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 0
evidence per changed file