From 3a0ad2f8693851c559fb6a5a5aa53ee2e45ea775 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:10:30 +0000 Subject: [PATCH] jsc-stress: add Yarr \B + surrogate match-length underflow regression test On ARM64, /\B|x{1,2}?/u.exec("a\u{10ffff}b")[0].length returned 4294967295. The Yarr JIT non-BMP first-character optimization (ARM64-only, YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP) advanced matchStart by firstCharacterAdditionalReadSize in the BodyAlternativeEnd backtrack trampoline without advancing m_regs.index, so a zero-width first alternative like \B could return output[0] > output[1] and the match length underflowed to (unsigned)-1. Fixed upstream in WebKit 116394de195b (webkit.org/b/312604), pulled into Bun by the WEBKIT_VERSION bump to d81bcc3d833c in #33133. This adds a jsc-stress fixture so a future WebKit bump cannot silently regress it; before the bump every aarch64 CI lane reproduced the underflow. --- ...non-bmp-backtrack-trampoline-index-sync.js | 65 +++++++++++++++++++ test/js/bun/jsc-stress/jsc-stress.test.ts | 2 + 2 files changed, 67 insertions(+) create mode 100644 test/js/bun/jsc-stress/fixtures/yarr-non-bmp-backtrack-trampoline-index-sync.js diff --git a/test/js/bun/jsc-stress/fixtures/yarr-non-bmp-backtrack-trampoline-index-sync.js b/test/js/bun/jsc-stress/fixtures/yarr-non-bmp-backtrack-trampoline-index-sync.js new file mode 100644 index 000000000000..b3a4c91d1ecd --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/yarr-non-bmp-backtrack-trampoline-index-sync.js @@ -0,0 +1,65 @@ +// @bun +// Regression test for the Yarr JIT non-BMP first-character optimization. +// +// `/\B|x{1,2}?/u.exec("a\u{10ffff}b")[0].length` returned 4294967295. +// +// The BodyAlternativeEnd backtrack trampoline, when looping from a last +// alternative whose minimumSize is greater than the first's, advanced +// matchStart by firstCharacterAdditionalReadSize but left m_regs.index +// unchanged. A zero-width first alternative (e.g. \B) could then succeed +// with output[0] > output[1], and the computed match length underflowed to +// (unsigned)-1 = 4294967295. +// +// Fixed upstream in WebKit 116394de195b (webkit.org/b/312604), which landed +// in Bun via the WEBKIT_VERSION bump to d81bcc3d833c. +// +// The optimization is enabled when ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP), +// which today is ARM64-only; before the fix every aarch64 CI lane reproduced +// the underflow. On other architectures this exercises the non-optimized path +// and must also pass. + +function check(re, input, label) { + var m = re.exec(input); + if (m === null) return; + if (typeof m[0] !== "string") + throw new Error(label + ": match[0] is not a string"); + // The whole-match slice must never have end < start. When it does, + // jsSubstringOfResolved wraps and .length becomes (unsigned)-1. + if (m[0].length > input.length) + throw new Error(label + ": match[0].length=" + m[0].length + " exceeds input.length=" + input.length); + if (m.index + m[0].length > input.length) + throw new Error(label + ": match extends past end (index=" + m.index + " len=" + m[0].length + ")"); +} + +var inputs = [ + "a\u{10ffff}b", + "a\u{10ffff}", + "\u{10ffff}b", + "\u{10ffff}", + "a\u{10ffff}\u{10ffff}b", + "aa\u{10ffff}bb", +]; + +for (var i = 0; i < testLoopCount; ++i) { + for (var j = 0; j < inputs.length; ++j) { + var s = inputs[j]; + // delta == 1 (last alt minSize 1, first alt minSize 0) + check(/\B|x{1,2}?/u, s, "\\B|x{1,2}? on " + JSON.stringify(s)); + // delta == 2 (last alt minSize 2, first alt minSize 0) + check(/\B|xy{1,2}?/u, s, "\\B|xy{1,2}? on " + JSON.stringify(s)); + // Second alternative that matches past the pair. + check(/\B|b{1,2}?/u, s, "\\B|b{1,2}? on " + JSON.stringify(s)); + // /m disables the optimization; must behave identically to the interpreter. + check(/\B|x{1,2}?/mu, s, "\\B|x{1,2}?/mu on " + JSON.stringify(s)); + } + + // Direct assertion for the originally-reported case: any match must be empty. + var m = /\B|x{1,2}?/u.exec("a\u{10ffff}b"); + if (m !== null && m[0].length !== 0) + throw new Error("expected empty match, got length " + m[0].length); + + // Upstream's reduction (WebKit bug 312604): m[0] is "", so m[0][0] is undefined. + var u = /\B|c./u.exec("a\ud800\udc00"); + if (u !== null && u[0][0] !== undefined) + throw new Error("expected undefined, got " + JSON.stringify(u[0][0])); +} diff --git a/test/js/bun/jsc-stress/jsc-stress.test.ts b/test/js/bun/jsc-stress/jsc-stress.test.ts index 32736352c60b..c92360720e48 100644 --- a/test/js/bun/jsc-stress/jsc-stress.test.ts +++ b/test/js/bun/jsc-stress/jsc-stress.test.ts @@ -59,6 +59,8 @@ const jsFixtures = [ // FTL - RegExp "ftl-regexp-exec.js", "ftl-regexp-test.js", + // Yarr JIT + "yarr-non-bmp-backtrack-trampoline-index-sync.js", // FTL - Arguments "ftl-getmyargumentslength.js", "ftl-getmyargumentslength-inline.js",