From 4d2f3845633ecbbac1ea15c667befab64a40e90a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:32:44 +0000 Subject: [PATCH 1/8] Buffer#indexOf/lastIndexOf: rare-byte SIMD filter with Two-Way fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buffer#indexOf/includes went through highway_memmem, which SIMD-scanned for the needle's first byte and memcmp'd every candidate; lastIndexOf and the utf16le paths used std::find_end / std::search. All four are O(haystack * needle) on adversarial input: a 4 MiB 'a' haystack with a 4000-byte 'a…ab' needle took ~250 ms forward and ~7 s backward, a 1500x gap vs Node. highway_memmem, and new highway_memrmem / highway_memmem16 / highway_memrmem16, now use a two-anchor SIMD filter on the needle's two least-frequent bytes (full-needle histogram) so any distinguishing byte anywhere in the needle prunes to zero candidates. A false-positive budget caps total memcmp work at ~2*|haystack| and hands the remainder to a Two-Way (Crochemore-Perrin) search, which is O(n+m) worst case with O(1) space and handles reverse search via index-reversed views. The utf16le paths share the same kernel over uint16_t lanes. The libc memmem alias on Linux/macOS is the same symbol, so every in-process memmem call inherits the linear bound. --- src/jsc/bindings/BufferStringSearch.h | 161 +++++++++ src/jsc/bindings/JSBuffer.cpp | 44 ++- src/jsc/bindings/highway_strings.cpp | 328 ++++++++++++++---- test/js/node/buffer-indexof-worstcase.test.ts | 231 ++++++++++++ 4 files changed, 677 insertions(+), 87 deletions(-) create mode 100644 src/jsc/bindings/BufferStringSearch.h create mode 100644 test/js/node/buffer-indexof-worstcase.test.ts diff --git a/src/jsc/bindings/BufferStringSearch.h b/src/jsc/bindings/BufferStringSearch.h new file mode 100644 index 000000000000..3cbff966e01b --- /dev/null +++ b/src/jsc/bindings/BufferStringSearch.h @@ -0,0 +1,161 @@ +// Two-Way substring search (Crochemore & Perrin, 1991): guaranteed O(n + m) +// time, O(1) extra space. Used as the safety-net fallback for the SIMD +// substring kernels in highway_strings.cpp when their anchor filter keeps +// firing (adversarial input where both anchor bytes are common in the +// haystack), so Buffer#indexOf / includes / lastIndexOf stay linear even on +// inputs Node's kBMMaxShift-capped Boyer-Moore still goes quadratic on. +// +// Based on the public-domain musl libc implementation (src/string/memmem.c), +// adapted to support 16-bit units and reverse search via the same +// index-reversing Vector trick Node's SearchString uses. + +#pragma once + +#include +#include +#include +#include + +namespace bun { +namespace stringsearch { + +template +class Vector { +public: + Vector(const T* data, size_t length, bool isForward) + : start_(data) + , length_(length) + , is_forward_(isForward) + { + } + + size_t length() const { return length_; } + bool forward() const { return is_forward_; } + + T operator[](size_t index) const + { + return start_[is_forward_ ? index : (length_ - index - 1)]; + } + +private: + const T* start_; + size_t length_; + bool is_forward_; +}; + +// Critical factorization: on return, *period is the period of the maximal +// suffix and the function returns its start index. +template +static size_t MaximalSuffix(Vector needle, size_t* period, bool order) +{ + size_t ms = static_cast(-1); + size_t j = 0, k = 1, p = 1; + const size_t l = needle.length(); + while (j + k < l) { + Char a = needle[j + k]; + Char b = needle[ms + k]; + if (a == b) { + if (k == p) { + j += p; + k = 1; + } else { + k++; + } + } else if ((order ? a > b : a < b)) { + j += k; + k = 1; + p = j - ms; + } else { + ms = j++; + k = p = 1; + } + } + *period = p; + return ms; +} + +template +static size_t TwoWay(Vector haystack, Vector needle, size_t start_index) +{ + const size_t n = haystack.length(); + const size_t m = needle.length(); + + size_t p0, p1; + size_t ms0 = MaximalSuffix(needle, &p0, true); + size_t ms1 = MaximalSuffix(needle, &p1, false); + size_t ms, p; + if (ms0 + 1 > ms1 + 1) { + ms = ms0; + p = p0; + } else { + ms = ms1; + p = p1; + } + + // Is the needle periodic? (Is needle[0..ms] a prefix of needle[p..p+ms]?) + // ms may be (size_t)-1, in which case the prefix is empty and the needle + // is trivially periodic with period p. + bool periodic = true; + for (size_t i = 0, e = ms + 1; i < e; i++) { + if (needle[i] != needle[i + p]) { + periodic = false; + break; + } + } + size_t mem0; + if (periodic) { + mem0 = m - p; + } else { + mem0 = 0; + p = std::max(ms + 1, m - ms - 1) + 1; + } + + size_t pos = start_index; + size_t mem = 0; + while (pos + m <= n) { + // Right half: [max(ms+1, mem), m). + size_t k = std::max(ms + 1, mem); + while (k < m && needle[k] == haystack[pos + k]) k++; + if (k < m) { + pos += k - ms; + mem = 0; + continue; + } + // Left half: (mem, ms]. + k = ms + 1; + while (k > mem && needle[k - 1] == haystack[pos + k - 1]) k--; + if (k <= mem) return pos; + pos += p; + mem = mem0; + } + return n; +} + +} // namespace stringsearch + +// One-shot entry point matching Node's SearchString in node_buffer.cc. +// Returns the match index in [0, haystack_length - needle_length], or +// haystack_length if not found. Preconditions: needle_length > 0 and +// haystack_length >= needle_length (callers handle empty / oversized needles). +template +size_t SearchString(const Char* haystack, size_t haystack_length, + const Char* needle, size_t needle_length, + size_t start_index, bool is_forward) +{ + stringsearch::Vector v_needle(needle, needle_length, is_forward); + stringsearch::Vector v_haystack(haystack, haystack_length, is_forward); + size_t diff = haystack_length - needle_length; + size_t relative_start_index; + if (is_forward) { + relative_start_index = start_index; + } else if (diff < start_index) { + relative_start_index = 0; + } else { + relative_start_index = diff - start_index; + } + size_t pos = stringsearch::TwoWay(v_haystack, v_needle, relative_start_index); + if (pos == haystack_length) return haystack_length; + return is_forward ? pos : (diff - pos); +} + +} // namespace bun diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index ffdae2557a6a..a297dd446cbe 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -87,6 +87,9 @@ extern "C" bool Bun__Node__ZeroFillBuffers; // SIMD-optimized search functions from highway_strings.cpp extern "C" void* highway_memmem(const uint8_t* haystack, size_t haystack_len, const uint8_t* needle, size_t needle_len); +extern "C" size_t highway_memrmem(const uint8_t* haystack, size_t haystack_len, const uint8_t* needle, size_t needle_len); +extern "C" size_t highway_memmem16(const uint16_t* haystack, size_t haystack_len, const uint16_t* needle, size_t needle_len); +extern "C" size_t highway_memrmem16(const uint16_t* haystack, size_t haystack_len, const uint16_t* needle, size_t needle_len); extern "C" size_t highway_index_of_char(const uint8_t* haystack, size_t haystack_len, uint8_t needle); // export fn Bun__inspect_singleline(globalThis: *JSGlobalObject, value: JSValue) bun.String @@ -1566,6 +1569,8 @@ static int64_t indexOf(const uint8_t* thisPtr, int64_t thisLength, const uint8_t return byteOffset + static_cast(static_cast(result) - haystackPtr); } +// UCS2 searches operate on whole uint16_t units (Node's SearchString), +// so a match can only start on an even byte offset. static int64_t indexOf16(const uint8_t* thisPtr, int64_t thisLength, const uint8_t* valuePtr, int64_t valueLength, int64_t byteOffset) { if (thisLength == 1) return -1; @@ -1573,16 +1578,13 @@ static int64_t indexOf16(const uint8_t* thisPtr, int64_t thisLength, const uint8 thisLength /= 2; valueLength /= 2; byteOffset /= 2; - auto haystack = std::span((const uint16_t*)(thisPtr), thisLength).subspan(byteOffset); - auto needle = std::span((const uint16_t*)(valuePtr), valueLength); - auto it = std::search(haystack.begin(), haystack.end(), needle.begin(), needle.end()); - if (it == haystack.end()) return -1; - auto idx = byteOffset + std::distance(haystack.begin(), it); - return idx * 2; + const uint16_t* haystack = reinterpret_cast(thisPtr); + size_t result = highway_memmem16(haystack + byteOffset, static_cast(thisLength - byteOffset), + reinterpret_cast(valuePtr), static_cast(valueLength)); + if (result == ~static_cast(0)) return -1; + return (byteOffset + static_cast(result)) * 2; } -// UCS2 searches operate on whole uint16_t units (Node's SearchString), -// so a match can only start on an even byte offset. static int64_t lastIndexOf16(const uint8_t* thisPtr, int64_t thisLength, const uint8_t* valuePtr, int64_t valueLength, int64_t byteOffset) { if (thisLength == 1) return -1; @@ -1590,22 +1592,26 @@ static int64_t lastIndexOf16(const uint8_t* thisPtr, int64_t thisLength, const u thisLength /= 2; valueLength /= 2; byteOffset /= 2; - auto haystack = std::span((const uint16_t*)(thisPtr), std::min(thisLength, byteOffset + valueLength)); - auto needle = std::span((const uint16_t*)(valuePtr), valueLength); - auto it = std::find_end(haystack.begin(), haystack.end(), needle.begin(), needle.end()); - if (it == haystack.end()) return -1; - return std::distance(haystack.begin(), it) * 2; + int64_t haystackLen = std::min(thisLength, byteOffset + valueLength); + if (haystackLen < valueLength) return -1; + size_t result = highway_memrmem16(reinterpret_cast(thisPtr), static_cast(haystackLen), + reinterpret_cast(valuePtr), static_cast(valueLength)); + if (result == ~static_cast(0)) return -1; + return static_cast(result) * 2; } static int64_t lastIndexOf(const uint8_t* thisPtr, int64_t thisLength, const uint8_t* valuePtr, int64_t valueLength, int64_t byteOffset) { - auto start = thisPtr; - auto end = thisPtr + std::min(thisLength, byteOffset + valueLength); - auto it = std::find_end(start, end, valuePtr, valuePtr + valueLength); - if (it != end) { - return it - thisPtr; + int64_t haystackLen = std::min(thisLength, byteOffset + valueLength); + if (haystackLen < valueLength) return -1; + if (valueLength == 1) { + auto span = std::span(thisPtr, static_cast(haystackLen)); + return WTF::reverseFind(span, valuePtr[0]); } - return -1; + size_t result = highway_memrmem(thisPtr, static_cast(haystackLen), + valuePtr, static_cast(valueLength)); + if (result == ~static_cast(0)) return -1; + return static_cast(result); } // Port of the search-range handling in Node's src/node_buffer.cc diff --git a/src/jsc/bindings/highway_strings.cpp b/src/jsc/bindings/highway_strings.cpp index 5664d5e91a43..a2acda44a7c1 100644 --- a/src/jsc/bindings/highway_strings.cpp +++ b/src/jsc/bindings/highway_strings.cpp @@ -151,6 +151,57 @@ static inline uint64_t MaskBitsBelow(size_t k) return k >= 64 ? ~uint64_t { 0 } : ((uint64_t { 1 } << k) - 1); } +// --- memmem / memrmem anchor selection ----------------------------------- +// +// The SIMD substring kernels below verify two needle bytes per candidate +// position and only memcmp when both line up. Picking those two bytes from the +// needle's least-frequent values (by a histogram of the needle itself) makes +// the filter selective on the shapes that used to go quadratic: +// "aaa…ab" / "baaa…a" / "aa…b…aa" all reduce to zero candidates because the +// single 'b' becomes an anchor. When every needle byte is equally common the +// filter degrades to the old behaviour, and a false-positive budget hands off +// to the Two-Way search in BufferStringSearch.h to keep the worst case +// linear. The fallback is defined in the HWY_ONCE block. +template +size_t MemMemTwoWayFallback(const Char* haystack, size_t haystack_len, + const Char* needle, size_t needle_len, size_t start_index, bool is_forward); + +template +static inline void MemMemPickAnchors(const Char* needle, size_t needle_len, size_t* a, size_t* b) +{ + auto bucket = [](Char c) -> uint8_t { return static_cast(c); }; + + uint32_t histogram[256] = {}; + for (size_t i = 0; i < needle_len; i++) histogram[bucket(needle[i])]++; + + // First anchor: rarest byte, ties to the earliest offset. + size_t p0 = 0; + uint32_t best = histogram[bucket(needle[0])]; + for (size_t i = 1; i < needle_len; i++) { + uint32_t h = histogram[bucket(needle[i])]; + if (h < best) { + best = h; + p0 = i; + } + } + + // Second anchor: next-rarest at a different offset, ties to the latest + // offset to keep the two apart. + size_t p1 = (p0 == needle_len - 1) ? 0 : needle_len - 1; + uint32_t best2 = histogram[bucket(needle[p1])]; + for (size_t i = 0; i < needle_len; i++) { + if (i == p0) continue; + uint32_t h = histogram[bucket(needle[i])]; + if (h < best2 || (h == best2 && i > p1)) { + best2 = h; + p1 = i; + } + } + + *a = std::min(p0, p1); + *b = std::max(p0, p1); +} + } // namespace bun #endif // BUN_HIGHWAY_STRINGS_ANSI_SCALAR @@ -867,6 +918,111 @@ size_t IndexOfNeedsEscapeForJavaScriptStringImplQuote(const uint8_t* HWY_RESTRIC return IndexOfNeedsEscapeForJavaScriptStringImpl(text, text_len, quote_char); } +// --- Substring search (memmem / memrmem, 8- and 16-bit) -------------------- +// +// Two-anchor SIMD filter: a vector is loaded at candidate start + anchor_a and +// at start + anchor_b, and only positions where both anchor bytes match become +// memcmp candidates. Anchor offsets are the needle's two rarest bytes +// (MemMemPickAnchors), which brings the "aaa…ab"-style inputs that defeated a +// first-byte-only scan down to zero candidates. A budget on false positives +// keeps memcmp work under ~2·|haystack| and hands the remaining range to the +// Two-Way search in BufferStringSearch.h (same adaptive strategy Node +// uses) so the worst case stays linear even when both anchors are common. +// kNotFound marks a fallback request so the per-target code never instantiates +// the BM template. +static constexpr size_t kNotFound = ~static_cast(0); +static constexpr size_t kFallback = ~static_cast(1); + +template +static HWY_INLINE bool MemMemVerify(const Char* haystack, size_t pos, const Char* needle, size_t needle_len) +{ + return memcmp(haystack + pos, needle, needle_len * sizeof(Char)) == 0; +} + +template +size_t MemMemForward(const Char* haystack, size_t haystack_len, + const Char* needle, size_t needle_len, + size_t anchor_a, size_t anchor_b, size_t* resume) +{ + const hn::ScalableTag d; + const size_t N = hn::Lanes(d); + const auto va = hn::Set(d, needle[anchor_a]); + const auto vb = hn::Set(d, needle[anchor_b]); + const size_t last_start = haystack_len - needle_len; + + size_t budget = (haystack_len * 2) / needle_len + 32; + + size_t i = 0; + // Every lane's start <= last_start, so both anchor loads are in bounds. + while (i + N <= last_start + 1) { + auto mask = hn::And(hn::Eq(hn::LoadU(d, haystack + i + anchor_a), va), + hn::Eq(hn::LoadU(d, haystack + i + anchor_b), vb)); + while (!hn::AllFalse(d, mask)) { + const size_t lane = static_cast(hn::FindKnownFirstTrue(d, mask)); + const size_t pos = i + lane; + if (MemMemVerify(haystack, pos, needle, needle_len)) return pos; + if (HWY_UNLIKELY(--budget == 0)) { + *resume = pos + 1; + return kFallback; + } + mask = hn::AndNot(hn::SetOnlyFirst(mask), mask); + } + i += N; + } + for (; i <= last_start; ++i) { + if (haystack[i + anchor_a] == needle[anchor_a] && haystack[i + anchor_b] == needle[anchor_b]) { + if (MemMemVerify(haystack, i, needle, needle_len)) return i; + if (HWY_UNLIKELY(--budget == 0)) { + *resume = i + 1; + return kFallback; + } + } + } + return kNotFound; +} + +template +size_t MemMemReverse(const Char* haystack, size_t haystack_len, + const Char* needle, size_t needle_len, + size_t anchor_a, size_t anchor_b, size_t* resume) +{ + const hn::ScalableTag d; + const size_t N = hn::Lanes(d); + const auto va = hn::Set(d, needle[anchor_a]); + const auto vb = hn::Set(d, needle[anchor_b]); + const size_t last_start = haystack_len - needle_len; + + size_t budget = (haystack_len * 2) / needle_len + 32; + + size_t i = last_start + 1; + while (i >= N) { + i -= N; + auto mask = hn::And(hn::Eq(hn::LoadU(d, haystack + i + anchor_a), va), + hn::Eq(hn::LoadU(d, haystack + i + anchor_b), vb)); + while (!hn::AllFalse(d, mask)) { + const size_t lane = hn::FindKnownLastTrue(d, mask); + const size_t pos = i + lane; + if (MemMemVerify(haystack, pos, needle, needle_len)) return pos; + if (HWY_UNLIKELY(--budget == 0)) { + *resume = pos == 0 ? 0 : pos - 1; + return kFallback; + } + mask = hn::And(mask, hn::FirstN(d, lane)); + } + } + // Remaining starts [0, i); at most N-1 of them. + while (i-- > 0) { + if (haystack[i + anchor_a] == needle[anchor_a] && haystack[i + anchor_b] == needle[anchor_b]) { + if (MemMemVerify(haystack, i, needle, needle_len)) return i; + if (HWY_UNLIKELY(--budget == 0)) { + *resume = i == 0 ? 0 : i - 1; + return kFallback; + } + } + } + return kNotFound; +} + // Highway implementation of memmem // Returns a pointer to the first occurrence of `needle` in `haystack`, // or nullptr if not found. The return type is non-const `uint8_t*` @@ -875,84 +1031,88 @@ size_t IndexOfNeedsEscapeForJavaScriptStringImplQuote(const uint8_t* HWY_RESTRIC uint8_t* MemMemImpl(const uint8_t* haystack, size_t haystack_len, const uint8_t* needle, size_t needle_len) { - // --- Edge Cases --- - if (HWY_UNLIKELY(needle_len == 0)) { - return const_cast(haystack); + if (HWY_UNLIKELY(needle_len == 0)) return const_cast(haystack); + if (HWY_UNLIKELY(haystack_len < needle_len)) return nullptr; + if (HWY_UNLIKELY(needle_len == 1)) { + size_t index = IndexOfCharImpl(haystack, haystack_len, needle[0]); + return index != haystack_len ? const_cast(haystack + index) : nullptr; } - if (HWY_UNLIKELY(haystack_len < needle_len)) { - return nullptr; + + size_t a, b; + bun::MemMemPickAnchors(needle, needle_len, &a, &b); + size_t resume = 0; + size_t pos = MemMemForward(haystack, haystack_len, needle, needle_len, a, b, &resume); + if (pos == kNotFound) return nullptr; + if (HWY_UNLIKELY(pos == kFallback)) { + pos = bun::MemMemTwoWayFallback(haystack, haystack_len, needle, needle_len, resume, true); + return pos == haystack_len ? nullptr : const_cast(haystack + pos); } + return const_cast(haystack + pos); +} + +size_t MemRMemImpl(const uint8_t* haystack, size_t haystack_len, + const uint8_t* needle, size_t needle_len) +{ + if (HWY_UNLIKELY(needle_len == 0)) return haystack_len; + if (HWY_UNLIKELY(haystack_len < needle_len)) return kNotFound; if (HWY_UNLIKELY(needle_len == 1)) { - size_t index = IndexOfCharImpl(haystack, haystack_len, needle[0]); - if (index != haystack_len) { - return const_cast(haystack + index); + for (size_t i = haystack_len; i-- > 0;) { + if (haystack[i] == needle[0]) return i; } - return nullptr; + return kNotFound; } - // --- SIMD Setup --- - const hn::ScalableTag d; - const size_t N = hn::Lanes(d); - const uint8_t first_needle_char = needle[0]; - const hn::Vec v_first_needle = hn::Set(d, first_needle_char); - const size_t last_possible_start = haystack_len - needle_len; - - // --- SIMD Main Loop --- - size_t i = 0; - while (i + N <= haystack_len && i <= last_possible_start) { - const hn::Vec haystack_vec = hn::LoadU(d, haystack + i); - hn::Mask m_starts = hn::Eq(haystack_vec, v_first_needle); - - // Iterate through potential matches within this vector chunk using FindFirstTrue - while (!hn::AllFalse(d, m_starts)) { - const intptr_t bit_idx_ptr = hn::FindFirstTrue(d, m_starts); - // Loop condition guarantees FindFirstTrue finds something - HWY_ASSERT(bit_idx_ptr >= 0); - const size_t bit_idx = static_cast(bit_idx_ptr); - - const size_t potential_pos = i + bit_idx; - - // Double-check bounds (essential if N > needle_len, and correct otherwise) - if (potential_pos <= last_possible_start) { - if (memcmp(haystack + potential_pos, needle, needle_len) == 0) { - return const_cast(haystack + potential_pos); - } - } else { - // Optimization: If the first match found in this chunk is already - // beyond the last possible start, no subsequent match in this - // chunk can be valid. - goto remainder_check; // Exit both loops and proceed to scalar remainder - } + size_t a, b; + bun::MemMemPickAnchors(needle, needle_len, &a, &b); + size_t resume = 0; + size_t pos = MemMemReverse(haystack, haystack_len, needle, needle_len, a, b, &resume); + if (HWY_UNLIKELY(pos == kFallback)) { + pos = bun::MemMemTwoWayFallback(haystack, haystack_len, needle, needle_len, resume, false); + return pos == haystack_len ? kNotFound : pos; + } + return pos; +} - // Clear the found bit to find the next one in the next iteration. - // SetOnlyFirst creates a mask with only the first true bit set. - // AndNot removes that bit from m_starts. - const hn::Mask first_bit_mask = hn::SetOnlyFirst(m_starts); - m_starts = hn::AndNot(first_bit_mask, m_starts); - } // End while (!AllFalse) +size_t MemMem16Impl(const uint16_t* haystack, size_t haystack_len, + const uint16_t* needle, size_t needle_len) +{ + if (HWY_UNLIKELY(needle_len == 0)) return 0; + if (HWY_UNLIKELY(haystack_len < needle_len)) return kNotFound; - i += N; - } // End SIMD loop - -remainder_check: - // --- Scalar Remainder Loop --- - // Check any remaining bytes that couldn't form a full vector load - // or potential starts within the last vector load that weren't checked - // because they were past last_possible_start. - // Start `i` from where the SIMD loop *could* have last started a valid check. - size_t remainder_start = (i >= N) ? (i - N) : 0; - // Ensure we re-check any potential starts the SIMD loop might have skipped - // due to the bounds check optimization or being in the final partial vector. - for (; remainder_start <= last_possible_start; ++remainder_start) { - // Optimization: Check first character before expensive memcmp - if (haystack[remainder_start] == first_needle_char) { - if (memcmp(haystack + remainder_start, needle, needle_len) == 0) { - return const_cast(haystack + remainder_start); - } - } + size_t a, b; + if (needle_len == 1) { + a = b = 0; + } else { + bun::MemMemPickAnchors(needle, needle_len, &a, &b); } + size_t resume = 0; + size_t pos = MemMemForward(haystack, haystack_len, needle, needle_len, a, b, &resume); + if (HWY_UNLIKELY(pos == kFallback)) { + pos = bun::MemMemTwoWayFallback(haystack, haystack_len, needle, needle_len, resume, true); + return pos == haystack_len ? kNotFound : pos; + } + return pos; +} + +size_t MemRMem16Impl(const uint16_t* haystack, size_t haystack_len, + const uint16_t* needle, size_t needle_len) +{ + if (HWY_UNLIKELY(needle_len == 0)) return haystack_len; + if (HWY_UNLIKELY(haystack_len < needle_len)) return kNotFound; - return nullptr; // Not found + size_t a, b; + if (needle_len == 1) { + a = b = 0; + } else { + bun::MemMemPickAnchors(needle, needle_len, &a, &b); + } + size_t resume = 0; + size_t pos = MemMemReverse(haystack, haystack_len, needle, needle_len, a, b, &resume); + if (HWY_UNLIKELY(pos == kFallback)) { + pos = bun::MemMemTwoWayFallback(haystack, haystack_len, needle, needle_len, resume, false); + return pos == haystack_len ? kNotFound : pos; + } + return pos; } // Count of "visible" Latin-1 bytes for Bun.stringWidth (stringWidth.cpp): @@ -2010,10 +2170,27 @@ HWY_EXPORT(IndexOfSpaceOrNewlineOrNonASCIIImpl); HWY_EXPORT(LowerAscii16Impl); HWY_EXPORT(LowerAsciiImpl); HWY_EXPORT(MemMemImpl); +HWY_EXPORT(MemRMemImpl); +HWY_EXPORT(MemMem16Impl); +HWY_EXPORT(MemRMem16Impl); HWY_EXPORT(ScanCharFrequencyImpl); HWY_EXPORT(VisibleLatin1WidthExcludeANSIImpl); HWY_EXPORT(VisibleLatin1WidthImpl); HWY_EXPORT(VisibleUTF16WidthImpl); + +} // namespace bun +#include "BufferStringSearch.h" +namespace bun { + +template +size_t MemMemTwoWayFallback(const Char* haystack, size_t haystack_len, + const Char* needle, size_t needle_len, size_t start_index, bool is_forward) +{ + return bun::SearchString(haystack, haystack_len, needle, needle_len, start_index, is_forward); +} +template size_t MemMemTwoWayFallback(const uint8_t*, size_t, const uint8_t*, size_t, size_t, bool); +template size_t MemMemTwoWayFallback(const uint16_t*, size_t, const uint16_t*, size_t, size_t, bool); + // Define the C-callable wrappers that use HWY_DYNAMIC_DISPATCH. // These need to be defined *after* the HWY_EXPORT block and INSIDE namespace bun // so that HWY_DYNAMIC_DISPATCH(FuncImpl) correctly resolves to bun::N_*::FuncImpl. @@ -2025,6 +2202,21 @@ void* highway_memmem(const uint8_t* haystack, size_t haystack_len, const uint8_t return HWY_DYNAMIC_DISPATCH(MemMemImpl)(haystack, haystack_len, needle, needle_len); } +size_t highway_memrmem(const uint8_t* haystack, size_t haystack_len, const uint8_t* needle, size_t needle_len) +{ + return HWY_DYNAMIC_DISPATCH(MemRMemImpl)(haystack, haystack_len, needle, needle_len); +} + +size_t highway_memmem16(const uint16_t* haystack, size_t haystack_len, const uint16_t* needle, size_t needle_len) +{ + return HWY_DYNAMIC_DISPATCH(MemMem16Impl)(haystack, haystack_len, needle, needle_len); +} + +size_t highway_memrmem16(const uint16_t* haystack, size_t haystack_len, const uint16_t* needle, size_t needle_len) +{ + return HWY_DYNAMIC_DISPATCH(MemRMem16Impl)(haystack, haystack_len, needle, needle_len); +} + static void highway_copy_u16_to_u8_impl( const uint16_t* input, size_t count, diff --git a/test/js/node/buffer-indexof-worstcase.test.ts b/test/js/node/buffer-indexof-worstcase.test.ts new file mode 100644 index 000000000000..8c958fff03da --- /dev/null +++ b/test/js/node/buffer-indexof-worstcase.test.ts @@ -0,0 +1,231 @@ +import { describe, expect, test } from "bun:test"; +import { isASAN, isDebug } from "harness"; + +// Naive reference implementations for correctness cross-checks. +const refIndexOf = (hay: Uint8Array, ndl: Uint8Array, from = 0) => { + if (ndl.length === 0) return Math.min(Math.max(from, 0), hay.length); + outer: for (let i = Math.max(from, 0); i + ndl.length <= hay.length; i++) { + for (let j = 0; j < ndl.length; j++) if (hay[i + j] !== ndl[j]) continue outer; + return i; + } + return -1; +}; +const refLastIndexOf = (hay: Uint8Array, ndl: Uint8Array, from = hay.length) => { + if (ndl.length === 0) return Math.min(Math.max(from, 0), hay.length); + const start = Math.min(from, hay.length - ndl.length); + outer: for (let i = start; i >= 0; i--) { + for (let j = 0; j < ndl.length; j++) if (hay[i + j] !== ndl[j]) continue outer; + return i; + } + return -1; +}; + +describe("Buffer#indexOf / lastIndexOf adversarial worst case", () => { + // The search should not degrade to O(haystack * needle) on inputs where the + // needle's anchor bytes are the haystack's dominant byte. Node stays flat on + // these; with a naive first-byte-only filter (or std::find_end) a 16x longer + // needle costs ~16x more. + + // Debug/ASAN is ~50x slower at byte-by-byte loops; keep the haystack small + // enough to finish quickly there while still making a naive search blow the + // budget. + const HAY = isASAN || isDebug ? 512 * 1024 : 4 * 1024 * 1024; + const haystack = Buffer.alloc(HAY, 0x61); + const budgetMs = isASAN || isDebug ? 3000 : 400; + + const fw = (m: number) => { + const n = Buffer.alloc(m, 0x61); + n[m - 1] = 0x62; + return n; // 'a'*(m-1)+'b' — not present + }; + const bw = (m: number) => { + const n = Buffer.alloc(m, 0x61); + n[0] = 0x62; + return n; // 'b'+'a'*(m-1) — not present + }; + const mid = (m: number) => { + const n = Buffer.alloc(m, 0x61); + n[m >> 1] = 0x62; + return n; // 'a'*(m/2)+'b'+'a'*(m/2) — defeats a first+last-byte-only filter + }; + + for (const [name, shape] of [ + ["tail-mismatch", fw], + ["head-mismatch", bw], + ["mid-mismatch", mid], + ] as const) { + test(`indexOf stays sublinear in needle length (${name})`, () => { + for (const m of [64, 256, 1024, 4096, 16384]) { + const needle = shape(m); + const t0 = performance.now(); + const r = haystack.indexOf(needle); + const dt = performance.now() - t0; + expect(r).toBe(-1); + if (dt > budgetMs) { + throw new Error(`indexOf ${name} m=${m}: ${dt.toFixed(0)}ms (> ${budgetMs}ms)`); + } + } + }); + + test(`lastIndexOf stays sublinear in needle length (${name})`, () => { + for (const m of [64, 256, 1024, 4096, 16384]) { + const needle = shape(m); + const t0 = performance.now(); + const r = haystack.lastIndexOf(needle); + const dt = performance.now() - t0; + expect(r).toBe(-1); + if (dt > budgetMs) { + throw new Error(`lastIndexOf ${name} m=${m}: ${dt.toFixed(0)}ms (> ${budgetMs}ms)`); + } + } + }); + } + + test("indexOf / lastIndexOf growth ratio is bounded", () => { + const best = (f: () => void) => { + let b = Infinity; + for (let i = 0; i < 3; i++) { + const s = performance.now(); + f(); + b = Math.min(b, performance.now() - s); + } + return b; + }; + // On a linear algorithm the 16x longer needle should not cost >4x more. + const i250 = best(() => haystack.indexOf(fw(250))); + const i4000 = best(() => haystack.indexOf(fw(4000))); + const l250 = best(() => haystack.lastIndexOf(bw(250))); + const l4000 = best(() => haystack.lastIndexOf(bw(4000))); + expect(i4000 / Math.max(i250, 0.1)).toBeLessThan(4); + expect(l4000 / Math.max(l250, 0.1)).toBeLessThan(4); + }); + + test("includes on adversarial needle", () => { + const needle = mid(8192); + const t0 = performance.now(); + expect(haystack.includes(needle)).toBe(false); + const dt = performance.now() - t0; + if (dt > budgetMs) throw new Error(`includes m=8192: ${dt.toFixed(0)}ms (> ${budgetMs}ms)`); + }); + + test("uniform-byte needle forces the Two-Way fallback", () => { + // Needle is one repeated byte so both SIMD anchors are that byte; haystack + // repeats (m-1 of it + one other) so the filter fires almost everywhere and + // every memcmp fails, tripping the budget into the Two-Way search. With + // m >> 250 this stays linear where Node's capped Boyer-Moore does not. + for (const m of [257, 4096]) { + const unit = Buffer.alloc(m, 0x61); + unit[m - 1] = 0x62; + const hay = Buffer.alloc(unit.length * (isASAN || isDebug ? 128 : 1024), 0x61); + for (let i = 0; i < hay.length; i += unit.length) unit.copy(hay, i); + const needle = Buffer.alloc(m, 0x61); + + for (const fn of ["indexOf", "lastIndexOf"] as const) { + const t0 = performance.now(); + const r = hay[fn](needle); + const dt = performance.now() - t0; + expect(r).toBe(-1); + if (dt > budgetMs) throw new Error(`${fn} fallback m=${m}: ${dt.toFixed(0)}ms (> ${budgetMs}ms)`); + } + } + }); + + test("SIMD anchors defeated, Two-Way periodic branch", () => { + // haystack 'abab…' with one 'c' every `m` bytes, needle 'abab…' of length + // m: anchors are 'a'/'b' (flat histogram) and fire at every aligned + // position, memcmp fails near the 'c', budget trips into Two-Way's + // periodic case. + const m = 2048; + const hay = Buffer.alloc(m * (isASAN || isDebug ? 128 : 1024)); + for (let i = 0; i < hay.length; i++) hay[i] = 0x61 + (i & 1); + for (let i = m - 2; i < hay.length; i += m) hay[i] = 0x63; + const needle = Buffer.alloc(m); + for (let i = 0; i < m; i++) needle[i] = 0x61 + (i & 1); + + for (const fn of ["indexOf", "lastIndexOf"] as const) { + const t0 = performance.now(); + const r = hay[fn](needle); + const dt = performance.now() - t0; + expect(r).toBe(-1); + if (dt > budgetMs) throw new Error(`${fn} periodic m=${m}: ${dt.toFixed(0)}ms (> ${budgetMs}ms)`); + } + }); +}); + +describe("Buffer#indexOf / lastIndexOf correctness", () => { + // Deterministic pseudo-random cross-check against a naive implementation, + // covering the SIMD anchor filter, the Boyer-Moore fallback, short needles + // (< kBMMinPatternLength), utf16le, and byteOffset handling. + test("randomized cross-check vs naive", () => { + let seed = 0x1234_5678; + const rand = () => { + seed = (seed * 1103515245 + 12345) >>> 0; + return seed; + }; + const randByte = (alphabet: number) => 0x61 + (rand() % alphabet); + + for (let trial = 0; trial < 1500; trial++) { + const alphabet = 1 + (rand() % 4); // 1..4 distinct bytes → lots of partial matches + const hayLen = rand() % 200; + const ndlLen = rand() % 24; + const hay = Buffer.alloc(hayLen); + for (let i = 0; i < hayLen; i++) hay[i] = randByte(alphabet); + let ndl: Buffer; + if (ndlLen > 0 && hayLen >= ndlLen && rand() % 3 === 0) { + // Guaranteed-present needle: copy a slice of the haystack. + const start = rand() % (hayLen - ndlLen + 1); + ndl = Buffer.from(hay.subarray(start, start + ndlLen)); + } else { + ndl = Buffer.alloc(ndlLen); + for (let i = 0; i < ndlLen; i++) ndl[i] = randByte(alphabet); + } + const from = (rand() % (hayLen + 4)) - 2; + + const io = hay.indexOf(ndl, from); + const lo = hay.lastIndexOf(ndl, from); + const refI = refIndexOf(hay, ndl, from < 0 ? Math.max(hayLen + from, 0) : from); + const refL = refLastIndexOf(hay, ndl, from < 0 ? hayLen + from : from); + if (io !== refI || lo !== refL) { + throw new Error( + `trial=${trial} hay=${hay.toString("hex")} ndl=${ndl.toString("hex")} from=${from} ` + + `indexOf=${io} ref=${refI} lastIndexOf=${lo} ref=${refL}`, + ); + } + } + }); + + test("match at every boundary", () => { + // Exercise the SIMD-loop / scalar-remainder handoff for every match offset + // across a few needle lengths. + for (const m of [2, 3, 7, 8, 9, 31, 32, 33, 63, 64, 65]) { + const needle = Buffer.alloc(m, 0x62); + for (let pos = 0; pos <= 80; pos++) { + const hay = Buffer.alloc(pos + m + 80, 0x61); + needle.copy(hay, pos); + expect(hay.indexOf(needle)).toBe(pos); + expect(hay.lastIndexOf(needle)).toBe(pos); + expect(hay.includes(needle)).toBe(true); + } + } + }); + + test("utf16le indexOf / lastIndexOf still correct", () => { + const hay = Buffer.from("a".repeat(1000) + "needle" + "a".repeat(1000), "utf16le"); + const ndl = Buffer.from("needle", "utf16le"); + expect(hay.indexOf(ndl, 0, "utf16le")).toBe(1000 * 2); + expect(hay.lastIndexOf(ndl, hay.length, "utf16le")).toBe(1000 * 2); + expect(hay.indexOf(Buffer.from("x".repeat(20), "utf16le"), 0, "utf16le")).toBe(-1); + expect(hay.lastIndexOf(Buffer.from("x".repeat(20), "utf16le"), hay.length, "utf16le")).toBe(-1); + }); + + test("adversarial needle that is present", () => { + const m = 500; + const hay = Buffer.alloc(64 * 1024, 0x61); + const needle = Buffer.alloc(m, 0x61); + needle[m >> 1] = 0x62; + const at = 40000; + hay[at + (m >> 1)] = 0x62; + expect(hay.indexOf(needle)).toBe(at); + expect(hay.lastIndexOf(needle)).toBe(at); + }); +}); From 4bdda2c10d87799829c54ca8a167fc6e081e9e1a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:35:27 +0000 Subject: [PATCH 2/8] [autofix.ci] apply automated fixes --- src/jsc/bindings/BufferStringSearch.h | 6 ++++-- src/jsc/bindings/highway_strings.cpp | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/jsc/bindings/BufferStringSearch.h b/src/jsc/bindings/BufferStringSearch.h index 3cbff966e01b..69ed21838108 100644 --- a/src/jsc/bindings/BufferStringSearch.h +++ b/src/jsc/bindings/BufferStringSearch.h @@ -115,7 +115,8 @@ static size_t TwoWay(Vector haystack, Vector needle, size_t start_in while (pos + m <= n) { // Right half: [max(ms+1, mem), m). size_t k = std::max(ms + 1, mem); - while (k < m && needle[k] == haystack[pos + k]) k++; + while (k < m && needle[k] == haystack[pos + k]) + k++; if (k < m) { pos += k - ms; mem = 0; @@ -123,7 +124,8 @@ static size_t TwoWay(Vector haystack, Vector needle, size_t start_in } // Left half: (mem, ms]. k = ms + 1; - while (k > mem && needle[k - 1] == haystack[pos + k - 1]) k--; + while (k > mem && needle[k - 1] == haystack[pos + k - 1]) + k--; if (k <= mem) return pos; pos += p; mem = mem0; diff --git a/src/jsc/bindings/highway_strings.cpp b/src/jsc/bindings/highway_strings.cpp index a2acda44a7c1..65dcfd313d2c 100644 --- a/src/jsc/bindings/highway_strings.cpp +++ b/src/jsc/bindings/highway_strings.cpp @@ -172,7 +172,8 @@ static inline void MemMemPickAnchors(const Char* needle, size_t needle_len, size auto bucket = [](Char c) -> uint8_t { return static_cast(c); }; uint32_t histogram[256] = {}; - for (size_t i = 0; i < needle_len; i++) histogram[bucket(needle[i])]++; + for (size_t i = 0; i < needle_len; i++) + histogram[bucket(needle[i])]++; // First anchor: rarest byte, ties to the earliest offset. size_t p0 = 0; From 847e7ea1d89f254bd12c2f0a8f85c30e21283861 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:38:06 +0000 Subject: [PATCH 3/8] trim section-header comments in the search kernels --- src/jsc/bindings/BufferStringSearch.h | 24 ++++++-------------- src/jsc/bindings/highway_strings.cpp | 32 +++++++-------------------- 2 files changed, 15 insertions(+), 41 deletions(-) diff --git a/src/jsc/bindings/BufferStringSearch.h b/src/jsc/bindings/BufferStringSearch.h index 69ed21838108..f4988fec0396 100644 --- a/src/jsc/bindings/BufferStringSearch.h +++ b/src/jsc/bindings/BufferStringSearch.h @@ -1,13 +1,7 @@ -// Two-Way substring search (Crochemore & Perrin, 1991): guaranteed O(n + m) -// time, O(1) extra space. Used as the safety-net fallback for the SIMD -// substring kernels in highway_strings.cpp when their anchor filter keeps -// firing (adversarial input where both anchor bytes are common in the -// haystack), so Buffer#indexOf / includes / lastIndexOf stay linear even on -// inputs Node's kBMMaxShift-capped Boyer-Moore still goes quadratic on. -// -// Based on the public-domain musl libc implementation (src/string/memmem.c), -// adapted to support 16-bit units and reverse search via the same -// index-reversing Vector trick Node's SearchString uses. +// Two-Way substring search (Crochemore & Perrin, 1991): O(n + m) time, +// O(1) space. Linear-time fallback for the SIMD kernels in highway_strings.cpp. +// Derived from the public-domain musl libc memmem; templated on Char and +// driven through an index-reversing Vector so one body serves forward/reverse. #pragma once @@ -92,9 +86,7 @@ static size_t TwoWay(Vector haystack, Vector needle, size_t start_in p = p1; } - // Is the needle periodic? (Is needle[0..ms] a prefix of needle[p..p+ms]?) - // ms may be (size_t)-1, in which case the prefix is empty and the needle - // is trivially periodic with period p. + // Periodic if needle[0..ms] == needle[p..p+ms]; ms+1 wraps to 0 for ms == -1. bool periodic = true; for (size_t i = 0, e = ms + 1; i < e; i++) { if (needle[i] != needle[i + p]) { @@ -135,10 +127,8 @@ static size_t TwoWay(Vector haystack, Vector needle, size_t start_in } // namespace stringsearch -// One-shot entry point matching Node's SearchString in node_buffer.cc. -// Returns the match index in [0, haystack_length - needle_length], or -// haystack_length if not found. Preconditions: needle_length > 0 and -// haystack_length >= needle_length (callers handle empty / oversized needles). +// Returns the match index, or haystack_length if not found. +// Requires needle_length > 0 and haystack_length >= needle_length. template size_t SearchString(const Char* haystack, size_t haystack_length, const Char* needle, size_t needle_length, diff --git a/src/jsc/bindings/highway_strings.cpp b/src/jsc/bindings/highway_strings.cpp index 65dcfd313d2c..3e9ccbc339a3 100644 --- a/src/jsc/bindings/highway_strings.cpp +++ b/src/jsc/bindings/highway_strings.cpp @@ -151,21 +151,14 @@ static inline uint64_t MaskBitsBelow(size_t k) return k >= 64 ? ~uint64_t { 0 } : ((uint64_t { 1 } << k) - 1); } -// --- memmem / memrmem anchor selection ----------------------------------- -// -// The SIMD substring kernels below verify two needle bytes per candidate -// position and only memcmp when both line up. Picking those two bytes from the -// needle's least-frequent values (by a histogram of the needle itself) makes -// the filter selective on the shapes that used to go quadratic: -// "aaa…ab" / "baaa…a" / "aa…b…aa" all reduce to zero candidates because the -// single 'b' becomes an anchor. When every needle byte is equally common the -// filter degrades to the old behaviour, and a false-positive budget hands off -// to the Two-Way search in BufferStringSearch.h to keep the worst case -// linear. The fallback is defined in the HWY_ONCE block. +// Defined in the HWY_ONCE block (BufferStringSearch.h). template size_t MemMemTwoWayFallback(const Char* haystack, size_t haystack_len, const Char* needle, size_t needle_len, size_t start_index, bool is_forward); +// Two anchor offsets for the SIMD substring filter: the needle's two +// least-frequent bytes (low byte for uint16_t), first tie earliest / second +// tie latest, so any distinguishing byte anywhere in the needle is picked. template static inline void MemMemPickAnchors(const Char* needle, size_t needle_len, size_t* a, size_t* b) { @@ -175,7 +168,6 @@ static inline void MemMemPickAnchors(const Char* needle, size_t needle_len, size for (size_t i = 0; i < needle_len; i++) histogram[bucket(needle[i])]++; - // First anchor: rarest byte, ties to the earliest offset. size_t p0 = 0; uint32_t best = histogram[bucket(needle[0])]; for (size_t i = 1; i < needle_len; i++) { @@ -186,8 +178,6 @@ static inline void MemMemPickAnchors(const Char* needle, size_t needle_len, size } } - // Second anchor: next-rarest at a different offset, ties to the latest - // offset to keep the two apart. size_t p1 = (p0 == needle_len - 1) ? 0 : needle_len - 1; uint32_t best2 = histogram[bucket(needle[p1])]; for (size_t i = 0; i < needle_len; i++) { @@ -921,16 +911,10 @@ size_t IndexOfNeedsEscapeForJavaScriptStringImplQuote(const uint8_t* HWY_RESTRIC // --- Substring search (memmem / memrmem, 8- and 16-bit) -------------------- // -// Two-anchor SIMD filter: a vector is loaded at candidate start + anchor_a and -// at start + anchor_b, and only positions where both anchor bytes match become -// memcmp candidates. Anchor offsets are the needle's two rarest bytes -// (MemMemPickAnchors), which brings the "aaa…ab"-style inputs that defeated a -// first-byte-only scan down to zero candidates. A budget on false positives -// keeps memcmp work under ~2·|haystack| and hands the remaining range to the -// Two-Way search in BufferStringSearch.h (same adaptive strategy Node -// uses) so the worst case stays linear even when both anchors are common. -// kNotFound marks a fallback request so the per-target code never instantiates -// the BM template. +// Two-anchor SIMD filter: vectors at candidate + anchor_a and + anchor_b are +// ANDed so only positions where both rare bytes (MemMemPickAnchors) line up +// are memcmp'd. A false-positive budget bounds memcmp work at ~2·|haystack| +// and hands the remainder to MemMemTwoWayFallback for a linear worst case. static constexpr size_t kNotFound = ~static_cast(0); static constexpr size_t kFallback = ~static_cast(1); From b9c9414e124aa2ac0e0027458aad005151ce70b7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:45:17 +0000 Subject: [PATCH 4/8] address review: short-needle histogram fast-path, named sentinel, string-needle utf16 coverage --- src/jsc/bindings/JSBuffer.cpp | 7 ++++--- src/jsc/bindings/highway_strings.cpp | 13 +++++++++++-- test/js/node/buffer-indexof-worstcase.test.ts | 16 ++++++++++------ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index a297dd446cbe..1d63ec3ab642 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -91,6 +91,7 @@ extern "C" size_t highway_memrmem(const uint8_t* haystack, size_t haystack_len, extern "C" size_t highway_memmem16(const uint16_t* haystack, size_t haystack_len, const uint16_t* needle, size_t needle_len); extern "C" size_t highway_memrmem16(const uint16_t* haystack, size_t haystack_len, const uint16_t* needle, size_t needle_len); extern "C" size_t highway_index_of_char(const uint8_t* haystack, size_t haystack_len, uint8_t needle); +static constexpr size_t kHighwayNotFound = ~static_cast(0); // export fn Bun__inspect_singleline(globalThis: *JSGlobalObject, value: JSValue) bun.String extern "C" BunString Bun__inspect_singleline(JSC::JSGlobalObject* globalObject, JSC::JSValue value); @@ -1581,7 +1582,7 @@ static int64_t indexOf16(const uint8_t* thisPtr, int64_t thisLength, const uint8 const uint16_t* haystack = reinterpret_cast(thisPtr); size_t result = highway_memmem16(haystack + byteOffset, static_cast(thisLength - byteOffset), reinterpret_cast(valuePtr), static_cast(valueLength)); - if (result == ~static_cast(0)) return -1; + if (result == kHighwayNotFound) return -1; return (byteOffset + static_cast(result)) * 2; } @@ -1596,7 +1597,7 @@ static int64_t lastIndexOf16(const uint8_t* thisPtr, int64_t thisLength, const u if (haystackLen < valueLength) return -1; size_t result = highway_memrmem16(reinterpret_cast(thisPtr), static_cast(haystackLen), reinterpret_cast(valuePtr), static_cast(valueLength)); - if (result == ~static_cast(0)) return -1; + if (result == kHighwayNotFound) return -1; return static_cast(result) * 2; } @@ -1610,7 +1611,7 @@ static int64_t lastIndexOf(const uint8_t* thisPtr, int64_t thisLength, const uin } size_t result = highway_memrmem(thisPtr, static_cast(haystackLen), valuePtr, static_cast(valueLength)); - if (result == ~static_cast(0)) return -1; + if (result == kHighwayNotFound) return -1; return static_cast(result); } diff --git a/src/jsc/bindings/highway_strings.cpp b/src/jsc/bindings/highway_strings.cpp index 3e9ccbc339a3..b815c2b084fc 100644 --- a/src/jsc/bindings/highway_strings.cpp +++ b/src/jsc/bindings/highway_strings.cpp @@ -157,11 +157,20 @@ size_t MemMemTwoWayFallback(const Char* haystack, size_t haystack_len, const Char* needle, size_t needle_len, size_t start_index, bool is_forward); // Two anchor offsets for the SIMD substring filter: the needle's two -// least-frequent bytes (low byte for uint16_t), first tie earliest / second -// tie latest, so any distinguishing byte anywhere in the needle is picked. +// least-frequent bytes (ranked by low byte for uint16_t; the filter compares +// full lanes), first tie earliest / second tie latest, so any distinguishing +// byte anywhere in the needle is picked. template static inline void MemMemPickAnchors(const Char* needle, size_t needle_len, size_t* a, size_t* b) { + // Short needles: first/last is as selective and skips the 1 KiB zero-init; + // the false-positive budget still bounds total work. + if (needle_len <= 16) { + *a = 0; + *b = needle_len - 1; + return; + } + auto bucket = [](Char c) -> uint8_t { return static_cast(c); }; uint32_t histogram[256] = {}; diff --git a/test/js/node/buffer-indexof-worstcase.test.ts b/test/js/node/buffer-indexof-worstcase.test.ts index 8c958fff03da..6923cc20789a 100644 --- a/test/js/node/buffer-indexof-worstcase.test.ts +++ b/test/js/node/buffer-indexof-worstcase.test.ts @@ -153,9 +153,6 @@ describe("Buffer#indexOf / lastIndexOf adversarial worst case", () => { }); describe("Buffer#indexOf / lastIndexOf correctness", () => { - // Deterministic pseudo-random cross-check against a naive implementation, - // covering the SIMD anchor filter, the Boyer-Moore fallback, short needles - // (< kBMMinPatternLength), utf16le, and byteOffset handling. test("randomized cross-check vs naive", () => { let seed = 0x1234_5678; const rand = () => { @@ -210,12 +207,19 @@ describe("Buffer#indexOf / lastIndexOf correctness", () => { }); test("utf16le indexOf / lastIndexOf still correct", () => { - const hay = Buffer.from("a".repeat(1000) + "needle" + "a".repeat(1000), "utf16le"); + const hay = Buffer.from(Buffer.alloc(1000, "a").toString() + "needle" + Buffer.alloc(1000, "a").toString(), "utf16le"); + const miss = Buffer.alloc(20, "x").toString(); + // indexOfBuffer → indexOf16 / lastIndexOf16 const ndl = Buffer.from("needle", "utf16le"); expect(hay.indexOf(ndl, 0, "utf16le")).toBe(1000 * 2); expect(hay.lastIndexOf(ndl, hay.length, "utf16le")).toBe(1000 * 2); - expect(hay.indexOf(Buffer.from("x".repeat(20), "utf16le"), 0, "utf16le")).toBe(-1); - expect(hay.lastIndexOf(Buffer.from("x".repeat(20), "utf16le"), hay.length, "utf16le")).toBe(-1); + expect(hay.indexOf(Buffer.from(miss, "utf16le"), 0, "utf16le")).toBe(-1); + expect(hay.lastIndexOf(Buffer.from(miss, "utf16le"), hay.length, "utf16le")).toBe(-1); + // indexOfString → indexOf16 / lastIndexOf16 + expect(hay.indexOf("needle", 0, "utf16le")).toBe(1000 * 2); + expect(hay.lastIndexOf("needle", hay.length, "utf16le")).toBe(1000 * 2); + expect(hay.indexOf(miss, 0, "utf16le")).toBe(-1); + expect(hay.lastIndexOf(miss, hay.length, "utf16le")).toBe(-1); }); test("adversarial needle that is present", () => { From b062eed2954b4fb67af32ddbb2bab48b9bdb5cd9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:47:27 +0000 Subject: [PATCH 5/8] [autofix.ci] apply automated fixes --- test/js/node/buffer-indexof-worstcase.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/js/node/buffer-indexof-worstcase.test.ts b/test/js/node/buffer-indexof-worstcase.test.ts index 6923cc20789a..4170ec4d80b1 100644 --- a/test/js/node/buffer-indexof-worstcase.test.ts +++ b/test/js/node/buffer-indexof-worstcase.test.ts @@ -207,7 +207,10 @@ describe("Buffer#indexOf / lastIndexOf correctness", () => { }); test("utf16le indexOf / lastIndexOf still correct", () => { - const hay = Buffer.from(Buffer.alloc(1000, "a").toString() + "needle" + Buffer.alloc(1000, "a").toString(), "utf16le"); + const hay = Buffer.from( + Buffer.alloc(1000, "a").toString() + "needle" + Buffer.alloc(1000, "a").toString(), + "utf16le", + ); const miss = Buffer.alloc(20, "x").toString(); // indexOfBuffer → indexOf16 / lastIndexOf16 const ndl = Buffer.from("needle", "utf16le"); From c76e5cd919a167490b2e8e5794240c87d18cd2ab Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:59:31 +0000 Subject: [PATCH 6/8] verify-baseline: allowlist the new Highway MemRMem/MemMem16/MemRMem16 kernels Same HWY_DYNAMIC_DISPATCH gate as the existing MemMemImpl entries. --- .../verify-baseline-static/allowlist-aarch64.txt | 12 ++++++++++++ .../allowlist-x64-windows.txt | 15 +++++++++++++++ scripts/verify-baseline-static/allowlist-x64.txt | 16 ++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/scripts/verify-baseline-static/allowlist-aarch64.txt b/scripts/verify-baseline-static/allowlist-aarch64.txt index 38f503017e97..b5699b9d3012 100644 --- a/scripts/verify-baseline-static/allowlist-aarch64.txt +++ b/scripts/verify-baseline-static/allowlist-aarch64.txt @@ -8,6 +8,9 @@ # (133 symbols) # ---------------------------------------------------------------------------- _ZN3bun10N_SVE2_12810MemMemImplEPKhmS2_m [SVE] +_ZN3bun10N_SVE2_12811MemRMemImplEPKhmS2_m [SVE] +_ZN3bun10N_SVE2_12812MemMem16ImplEPKtmS2_m [SVE] +_ZN3bun10N_SVE2_12813MemRMem16ImplEPKtmS2_m [SVE] _ZN3bun10N_SVE2_12813JsonIndexImplEPKhmmPjPmS4_S3_ [SVE] _ZN3bun10N_SVE2_12814DecodeHex8ImplEPKhPhm [SVE] _ZN3bun10N_SVE2_12814LowerAsciiImplEPKhmPh [SVE] @@ -42,6 +45,9 @@ _ZN3bun10N_SVE2_12846IndexOfNeedsEscapeForJavaScriptStringImplQuoteEPKhmh [S _ZN3bun10N_SVE2_12849IndexOfInterestingCharacterInMultilineCommentImplEPKhm [SVE] _ZN3bun10N_SVE2_12849IndexOfNeedsEscapeForJavaScriptStringImplBacktickEPKhmh [SVE] _ZN3bun5N_SVE10MemMemImplEPKhmS2_m [SVE] +_ZN3bun5N_SVE11MemRMemImplEPKhmS2_m [SVE] +_ZN3bun5N_SVE12MemMem16ImplEPKtmS2_m [SVE] +_ZN3bun5N_SVE13MemRMem16ImplEPKtmS2_m [SVE] _ZN3bun5N_SVE14DecodeHex8ImplEPKhPhm [SVE] _ZN3bun5N_SVE14LowerAsciiImplEPKhmPh [SVE] _ZN3bun5N_SVE15CopyU16ToU8ImplEPKtmPh [SVE] @@ -75,6 +81,9 @@ _ZN3bun5N_SVE46IndexOfNeedsEscapeForJavaScriptStringImplQuoteEPKhmh [S _ZN3bun5N_SVE49IndexOfInterestingCharacterInMultilineCommentImplEPKhm [SVE] _ZN3bun5N_SVE49IndexOfNeedsEscapeForJavaScriptStringImplBacktickEPKhmh [SVE] _ZN3bun6N_SVE210MemMemImplEPKhmS2_m [SVE] +_ZN3bun6N_SVE211MemRMemImplEPKhmS2_m [SVE] +_ZN3bun6N_SVE212MemMem16ImplEPKtmS2_m [SVE] +_ZN3bun6N_SVE213MemRMem16ImplEPKtmS2_m [SVE] _ZN3bun6N_SVE214DecodeHex8ImplEPKhPhm [SVE] _ZN3bun6N_SVE214LowerAsciiImplEPKhmPh [SVE] _ZN3bun6N_SVE215CopyU16ToU8ImplEPKtmPh [SVE] @@ -108,6 +117,9 @@ _ZN3bun6N_SVE246IndexOfNeedsEscapeForJavaScriptStringImplQuoteEPKhmh [S _ZN3bun6N_SVE249IndexOfInterestingCharacterInMultilineCommentImplEPKhm [SVE] _ZN3bun6N_SVE249IndexOfNeedsEscapeForJavaScriptStringImplBacktickEPKhmh [SVE] _ZN3bun9N_SVE_25610MemMemImplEPKhmS2_m [SVE] +_ZN3bun9N_SVE_25611MemRMemImplEPKhmS2_m [SVE] +_ZN3bun9N_SVE_25612MemMem16ImplEPKtmS2_m [SVE] +_ZN3bun9N_SVE_25613MemRMem16ImplEPKtmS2_m [SVE] _ZN3bun9N_SVE_25614DecodeHex8ImplEPKhPhm [SVE] _ZN3bun9N_SVE_25614LowerAsciiImplEPKhmPh [SVE] _ZN3bun9N_SVE_25615CopyU16ToU8ImplEPKtmPh [SVE] diff --git a/scripts/verify-baseline-static/allowlist-x64-windows.txt b/scripts/verify-baseline-static/allowlist-x64-windows.txt index ec23a8f55201..c0ae33306d59 100644 --- a/scripts/verify-baseline-static/allowlist-x64-windows.txt +++ b/scripts/verify-baseline-static/allowlist-x64-windows.txt @@ -502,6 +502,9 @@ bun::N_AVX2::IndexOfSpaceOrNewlineOrNonASCIIImpl [AVX, AVX2] bun::N_AVX2::LowerAscii16Impl [AVX, AVX2] bun::N_AVX2::LowerAsciiImpl [AVX, AVX2] bun::N_AVX2::MemMemImpl [AVX, AVX2] +bun::N_AVX2::MemRMemImpl [AVX, AVX2] +bun::N_AVX2::MemMem16Impl [AVX, AVX2, BMI2] +bun::N_AVX2::MemRMem16Impl [AVX, AVX2, BMI2] bun::N_AVX2::JsonIndexImpl [AVX, AVX2, BMI1, BMI2] bun::N_AVX2::VisibleLatin1WidthExcludeANSIImpl [AVX, AVX2, BMI1, BMI2] bun::N_AVX2::VisibleLatin1WidthImpl [AVX, AVX2] @@ -536,6 +539,9 @@ bun::N_AVX3::IndexOfSpaceOrNewlineOrNonASCIIImpl [AVX, AVX51 bun::N_AVX3::LowerAscii16Impl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3::LowerAsciiImpl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3::MemMemImpl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3::MemRMemImpl [AVX, AVX512BW, AVX512F, BMI2] +bun::N_AVX3::MemMem16Impl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3::MemRMem16Impl [AVX, AVX512F] bun::N_AVX3::JsonIndexImpl [AVX, AVX512BW, AVX512F, AVX512VL, BMI1, BMI2] bun::N_AVX3::VisibleLatin1WidthExcludeANSIImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL, BMI1, BMI2] bun::N_AVX3::VisibleLatin1WidthImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL] @@ -570,6 +576,9 @@ bun::N_AVX3_DL::IndexOfSpaceOrNewlineOrNonASCIIImpl [AVX, AVX51 bun::N_AVX3_DL::LowerAscii16Impl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3_DL::LowerAsciiImpl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3_DL::MemMemImpl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3_DL::MemRMemImpl [AVX, AVX512BW, AVX512F, BMI2] +bun::N_AVX3_DL::MemMem16Impl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3_DL::MemRMem16Impl [AVX, AVX512F] bun::N_AVX3_DL::JsonIndexImpl [AVX, AVX512BW, AVX512F, AVX512VL, BMI1, BMI2, GFNI] bun::N_AVX3_DL::VisibleLatin1WidthExcludeANSIImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL, BMI1, BMI2] bun::N_AVX3_DL::VisibleLatin1WidthImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL] @@ -604,6 +613,9 @@ bun::N_AVX3_SPR::IndexOfSpaceOrNewlineOrNonASCIIImpl [AVX, AVX51 bun::N_AVX3_SPR::LowerAscii16Impl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3_SPR::LowerAsciiImpl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3_SPR::MemMemImpl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3_SPR::MemRMemImpl [AVX, AVX512BW, AVX512F, BMI2] +bun::N_AVX3_SPR::MemMem16Impl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3_SPR::MemRMem16Impl [AVX, AVX512F] bun::N_AVX3_SPR::JsonIndexImpl [AVX, AVX512BW, AVX512F, AVX512VL, BMI1, BMI2, GFNI] bun::N_AVX3_SPR::VisibleLatin1WidthExcludeANSIImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL, BMI1, BMI2] bun::N_AVX3_SPR::VisibleLatin1WidthImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL] @@ -638,6 +650,9 @@ bun::N_AVX3_ZEN4::IndexOfSpaceOrNewlineOrNonASCIIImpl [AVX, AVX51 bun::N_AVX3_ZEN4::LowerAscii16Impl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3_ZEN4::LowerAsciiImpl [AVX, AVX512BW, AVX512F, AVX512VL] bun::N_AVX3_ZEN4::MemMemImpl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3_ZEN4::MemRMemImpl [AVX, AVX512BW, AVX512F, BMI2] +bun::N_AVX3_ZEN4::MemMem16Impl [AVX, AVX512BW, AVX512F, BMI1] +bun::N_AVX3_ZEN4::MemRMem16Impl [AVX, AVX512F] bun::N_AVX3_ZEN4::JsonIndexImpl [AVX, AVX512BW, AVX512F, AVX512VL, BMI1, BMI2, GFNI] bun::N_AVX3_ZEN4::VisibleLatin1WidthExcludeANSIImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL, BMI1, BMI2] bun::N_AVX3_ZEN4::VisibleLatin1WidthImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL] diff --git a/scripts/verify-baseline-static/allowlist-x64.txt b/scripts/verify-baseline-static/allowlist-x64.txt index 76eebb1fdb42..1ea40bc2b1f0 100644 --- a/scripts/verify-baseline-static/allowlist-x64.txt +++ b/scripts/verify-baseline-static/allowlist-x64.txt @@ -462,6 +462,9 @@ ctiMasmProbeTrampolineAVX [AVX] # (203 symbols) # ---------------------------------------------------------------------------- _ZN3bun10N_AVX3_SPR10MemMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun10N_AVX3_SPR11MemRMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI2] +_ZN3bun10N_AVX3_SPR12MemMem16ImplEPKtmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun10N_AVX3_SPR13MemRMem16ImplEPKtmS2_m [AVX, AVX512F] _ZN3bun10N_AVX3_SPR13JsonIndexImplEPKhmmPjPmS4_S3_ [AVX, AVX512BW, AVX512F, BMI1, BMI2, GFNI] _ZN3bun10N_AVX3_SPR14DecodeHex8ImplEPKhPhm [AVX, AVX512BW, AVX512F, AVX512VL, AVX512_VBMI, GFNI] _ZN3bun10N_AVX3_SPR14LowerAsciiImplEPKhmPh [AVX, AVX512BW, AVX512F, AVX512VL] @@ -496,6 +499,9 @@ _ZN3bun10N_AVX3_SPR46IndexOfNeedsEscapeForJavaScriptStringImplQuoteEPKhmh [ _ZN3bun10N_AVX3_SPR49IndexOfInterestingCharacterInMultilineCommentImplEPKhm [AVX, AVX512BW, AVX512F] _ZN3bun10N_AVX3_SPR49IndexOfNeedsEscapeForJavaScriptStringImplBacktickEPKhmh [AVX, AVX512BW, AVX512F] _ZN3bun11N_AVX3_ZEN410MemMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun11N_AVX3_ZEN411MemRMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI2] +_ZN3bun11N_AVX3_ZEN412MemMem16ImplEPKtmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun11N_AVX3_ZEN413MemRMem16ImplEPKtmS2_m [AVX, AVX512F] _ZN3bun11N_AVX3_ZEN413JsonIndexImplEPKhmmPjPmS4_S3_ [AVX, AVX512BW, AVX512F, BMI1, BMI2, GFNI] _ZN3bun11N_AVX3_ZEN414DecodeHex8ImplEPKhPhm [AVX, AVX512BW, AVX512F, AVX512VL, AVX512_VBMI, GFNI] _ZN3bun11N_AVX3_ZEN414LowerAsciiImplEPKhmPh [AVX, AVX512BW, AVX512F, AVX512VL] @@ -530,6 +536,9 @@ _ZN3bun11N_AVX3_ZEN446IndexOfNeedsEscapeForJavaScriptStringImplQuoteEPKhmh [ _ZN3bun11N_AVX3_ZEN449IndexOfInterestingCharacterInMultilineCommentImplEPKhm [AVX, AVX512BW, AVX512F] _ZN3bun11N_AVX3_ZEN449IndexOfNeedsEscapeForJavaScriptStringImplBacktickEPKhmh [AVX, AVX512BW, AVX512F] _ZN3bun6N_AVX210MemMemImplEPKhmS2_m [AVX, AVX2] +_ZN3bun6N_AVX211MemRMemImplEPKhmS2_m [AVX, AVX2] +_ZN3bun6N_AVX212MemMem16ImplEPKtmS2_m [AVX, AVX2, BMI2] +_ZN3bun6N_AVX213MemRMem16ImplEPKtmS2_m [AVX, AVX2, BMI2] _ZN3bun6N_AVX213JsonIndexImplEPKhmmPjPmS4_S3_ [AVX, AVX2, BMI1, BMI2] _ZN3bun6N_AVX214DecodeHex8ImplEPKhPhm [AVX, AVX2] _ZN3bun6N_AVX214LowerAsciiImplEPKhmPh [AVX, AVX2] @@ -564,6 +573,10 @@ _ZN3bun6N_AVX246IndexOfNeedsEscapeForJavaScriptStringImplQuoteEPKhmh [ _ZN3bun6N_AVX249IndexOfInterestingCharacterInMultilineCommentImplEPKhm [AVX, AVX2] _ZN3bun6N_AVX249IndexOfNeedsEscapeForJavaScriptStringImplBacktickEPKhmh [AVX, AVX2] _ZN3bun6N_AVX310MemMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun6N_AVX311MemRMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI2] +_ZN3bun6N_AVX312MemMem16ImplEPKtmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun6N_AVX313MemMemReverseItEEmPKT_mS4_mmmPm [AVX, AVX512BW, AVX512F, BMI2] +_ZN3bun6N_AVX313MemRMem16ImplEPKtmS2_m [AVX, AVX512F] _ZN3bun6N_AVX313JsonIndexImplEPKhmmPjPmS4_S3_ [AVX, AVX512BW, AVX512F, BMI1, BMI2] _ZN3bun6N_AVX314DecodeHex8ImplEPKhPhm [AVX, AVX2, AVX512BW, AVX512F, AVX512VL] _ZN3bun6N_AVX314LowerAsciiImplEPKhmPh [AVX, AVX512BW, AVX512F, AVX512VL] @@ -631,6 +644,9 @@ _ZN3bun9N_AVX10_246IndexOfNeedsEscapeForJavaScriptStringImplQuoteEPKhmh [ _ZN3bun9N_AVX10_249IndexOfInterestingCharacterInMultilineCommentImplEPKhm [AVX, AVX512BW, AVX512F] _ZN3bun9N_AVX10_249IndexOfNeedsEscapeForJavaScriptStringImplBacktickEPKhmh [AVX, AVX512BW, AVX512F] _ZN3bun9N_AVX3_DL10MemMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun9N_AVX3_DL11MemRMemImplEPKhmS2_m [AVX, AVX512BW, AVX512F, BMI2] +_ZN3bun9N_AVX3_DL12MemMem16ImplEPKtmS2_m [AVX, AVX512BW, AVX512F, BMI1] +_ZN3bun9N_AVX3_DL13MemRMem16ImplEPKtmS2_m [AVX, AVX512F] _ZN3bun9N_AVX3_DL13JsonIndexImplEPKhmmPjPmS4_S3_ [AVX, AVX512BW, AVX512F, BMI1, BMI2, GFNI] _ZN3bun9N_AVX3_DL14DecodeHex8ImplEPKhPhm [AVX, AVX512BW, AVX512F, AVX512VL, AVX512_VBMI, GFNI] _ZN3bun9N_AVX3_DL14LowerAsciiImplEPKhmPh [AVX, AVX512BW, AVX512F, AVX512VL] From 2d636760bbf97a03e5dabd43d9b9091dc193bfd5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:10:27 +0000 Subject: [PATCH 7/8] verify-baseline(windows): allowlist the outlined MemMemReverse N_AVX3 instantiation --- scripts/verify-baseline-static/allowlist-x64-windows.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/verify-baseline-static/allowlist-x64-windows.txt b/scripts/verify-baseline-static/allowlist-x64-windows.txt index c0ae33306d59..d3aed5ccb403 100644 --- a/scripts/verify-baseline-static/allowlist-x64-windows.txt +++ b/scripts/verify-baseline-static/allowlist-x64-windows.txt @@ -542,6 +542,7 @@ bun::N_AVX3::MemMemImpl [AVX, AVX51 bun::N_AVX3::MemRMemImpl [AVX, AVX512BW, AVX512F, BMI2] bun::N_AVX3::MemMem16Impl [AVX, AVX512BW, AVX512F, BMI1] bun::N_AVX3::MemRMem16Impl [AVX, AVX512F] +bun::N_AVX3::MemMemReverse [AVX, AVX512BW, AVX512F, BMI2] bun::N_AVX3::JsonIndexImpl [AVX, AVX512BW, AVX512F, AVX512VL, BMI1, BMI2] bun::N_AVX3::VisibleLatin1WidthExcludeANSIImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL, BMI1, BMI2] bun::N_AVX3::VisibleLatin1WidthImpl [AVX, AVX2, AVX512BW, AVX512DQ, AVX512F, AVX512VL] From b01343b36963da4e97c7d4564b20d74c912d7c5d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:23:19 +0000 Subject: [PATCH 8/8] ci: retrigger