Version in base suite: 20.19.2+dfsg-1 Version in overlay suite: 20.19.2+dfsg-1+deb13u1 Base version: nodejs_20.19.2+dfsg-1+deb13u1 Target version: nodejs_20.19.2+dfsg-1+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/n/nodejs/nodejs_20.19.2+dfsg-1+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/n/nodejs/nodejs_20.19.2+dfsg-1+deb13u2.dsc changelog | 14 copyright | 29 patches/sec/50-crypto-use-timing-safe-comparison-HMAC.patch | 31 patches/sec/51-fix-array-index-hash-collision.patch | 2318 ++++++++++ patches/sec/52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch | 147 patches/sec/53-include-permission-check-on-lib-fs-promises.patch | 605 ++ patches/sec/54-add-permission-check-to-realpath-native.patch | 57 patches/sec/55-handle-NGHTTP2_ERR_FLOW_CONTROL-error-code.patch | 118 patches/sec/56-tls-wrap-SNICallback-invocation-in-try-catch.patch | 170 patches/series | 7 10 files changed, 3496 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp0eez2n9b/nodejs_20.19.2+dfsg-1+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp0eez2n9b/nodejs_20.19.2+dfsg-1+deb13u2.dsc: no acceptable signature found diff -Nru nodejs-20.19.2+dfsg/debian/changelog nodejs-20.19.2+dfsg/debian/changelog --- nodejs-20.19.2+dfsg/debian/changelog 2026-03-05 10:05:11.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/changelog 2026-03-24 21:11:25.000000000 +0000 @@ -1,3 +1,17 @@ +nodejs (20.19.2+dfsg-1+deb13u2) trixie-security; urgency=medium + + * Upstream security patches: + + CVE-2026-21713: use timing-safe comparison in Web Cryptography HMAC + + CVE-2026-21717: fix array index hash collision + + CVE-2026-21710: http: use null prototype for headersDistinct/trailersDistinct + + CVE-2026-21716: include permission check on lib/fs/promises + + CVE-2026-21715: add permission check to realpath.native + + CVE-2026-21714: handle NGHTTP2_ERR_FLOW_CONTROL error code + + CVE-2026-21637: tls wrap SNICallback invocation in try/catch + * copyright: add rapidhash from sec/51 patch + + -- Jérémy Lal Tue, 24 Mar 2026 22:11:25 +0100 + nodejs (20.19.2+dfsg-1+deb13u1) trixie-security; urgency=medium * Upstream security patches: diff -Nru nodejs-20.19.2+dfsg/debian/copyright nodejs-20.19.2+dfsg/debian/copyright --- nodejs-20.19.2+dfsg/debian/copyright 2026-01-21 10:43:23.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/copyright 2026-03-24 20:57:43.000000000 +0000 @@ -102,6 +102,35 @@ Copyright: 2006-2017, the V8 Project Authors License: BSD-3-clause~Google +Files: deps/v8/third_party/rapidhash-v8/* +Copyright: 2024 Nicolas De Carli +License: BSD-2-clause + +Files: deps/v8/third_party/rapidhash-v8/secret.h +Copyright: 王一 Wang Yi +License: unlicense + This is free and unencumbered software released into the public domain. + . + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any means. + . + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + Files: tools/v8_gypfiles/GN-scraper.py Copyright: 2019 Refael Ackeramnn License: Expat diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/50-crypto-use-timing-safe-comparison-HMAC.patch nodejs-20.19.2+dfsg/debian/patches/sec/50-crypto-use-timing-safe-comparison-HMAC.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/50-crypto-use-timing-safe-comparison-HMAC.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/50-crypto-use-timing-safe-comparison-HMAC.patch 2026-03-24 20:42:12.000000000 +0000 @@ -0,0 +1,31 @@ +From cfb51fa9ce1da2a8c810ec35bcc7c000f8c94faf Mon Sep 17 00:00:00 2001 +From: Filip Skokan +Date: Fri, 20 Feb 2026 12:32:14 +0100 +Subject: [PATCH] crypto: use timing-safe comparison in Web Cryptography HMAC + +Use `CRYPTO_memcmp` instead of `memcmp` in `HMAC` +Web Cryptography algorithm implementations. + +Ref: https://hackerone.com/reports/3533945 +PR-URL: https://github.com/nodejs-private/node-private/pull/831 +Refs: https://hackerone.com/reports/3533945 +Reviewed-By: Marco Ippolito +CVE-ID: CVE-2026-21713 +--- + src/crypto/crypto_hmac.cc | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc +index 0b42a662385867..e66755bfa5bd05 100644 +--- a/src/crypto/crypto_hmac.cc ++++ b/src/crypto/crypto_hmac.cc +@@ -268,7 +268,8 @@ Maybe HmacTraits::EncodeOutput( + *result = Boolean::New( + env->isolate(), + out->size() > 0 && out->size() == params.signature.size() && +- memcmp(out->data(), params.signature.data(), out->size()) == 0); ++ CRYPTO_memcmp( ++ out->data(), params.signature.data(), out->size()) == 0); + break; + default: + UNREACHABLE(); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/51-fix-array-index-hash-collision.patch nodejs-20.19.2+dfsg/debian/patches/sec/51-fix-array-index-hash-collision.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/51-fix-array-index-hash-collision.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/51-fix-array-index-hash-collision.patch 2026-03-24 20:49:14.000000000 +0000 @@ -0,0 +1,2318 @@ +From af5c144ebcf9814ef5dc74555bbdcd2a4cb20a12 Mon Sep 17 00:00:00 2001 +From: Joyee Cheung +Date: Thu, 29 Jan 2026 03:30:37 +0100 +Subject: [PATCH] deps,build,test: fix array index hash collision + +This enables v8_enable_seeded_array_index_hash and add a test for it. + +Fixes: https://hackerone.com/reports/3511792 + +deps: V8: backport 0a8b1cdcc8b2 + +Original commit message: + + implement rapidhash secret generation + + Bug: 409717082 + Change-Id: I471f33d66de32002f744aeba534c1d34f71e27d2 + Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6733490 + Reviewed-by: Leszek Swirski + Commit-Queue: snek + Cr-Commit-Position: refs/heads/main@{#101499} + +Refs: https://github.com/v8/v8/commit/0a8b1cdcc8b243c62cf045fa8beb50600e11758a +Co-authored-by: Joyee Cheung + +deps: V8: backport 185f0fe09b72 + +Original commit message: + + [numbers] Refactor HashSeed as a lightweight view over ByteArray + + Instead of copying the seed and secrets into a struct with value + fields, HashSeed now stores a pointer pointing either into the + read-only ByteArray, or the static default seed for off-heap + HashSeed::Default() calls. The underlying storage is always + 8-byte aligned so we can cast it directly into a struct. + + Change-Id: I5896a7f2ae24296eb4c80b757a5d90ac70a34866 + Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7609720 + Reviewed-by: Leszek Swirski + Commit-Queue: Joyee Cheung + Cr-Commit-Position: refs/heads/main@{#105531} + +Refs: https://github.com/v8/v8/commit/185f0fe09b72fb869fdcf9a89f40ff2295436bca +Co-authored-by: Joyee Cheung + +deps: V8: backport 1361b2a49d02 + +Original commit message: + + [strings] improve array index hash distribution + + Previously, the hashes stored in a Name's raw_hash_field for decimal + numeric strings (potential array indices) consist of the literal + integer value along with the length of the string. This means + consecutive numeric strings can have consecutive hash values, which + can lead to O(n^2) probing for insertion in the worst case when e.g. + a non-numeric string happen to land in the these buckets. + + This patch adds a build-time flag v8_enable_seeded_array_index_hash that + scrambles the 24-bit array-index value stored in a Name's raw_hash_field + to improve the distribution. + + x ^= x >> kShift; x = (x * m1) & kMask; // round 1 + x ^= x >> kShift; x = (x * m2) & kMask; // round 2 + x ^= x >> kShift; // finalize + + To decode, apply the same steps with the modular inverses of m1 and m2 + in reverse order. + + x ^= x >> kShift; x = (x * m2_inv) & kMask; // round 1 + x ^= x >> kShift; x = (x * m1_inv) & kMask; // round 2 + x ^= x >> kShift; // finalize + + where kShift = kArrayIndexValueBits / 2, kMask = kArrayIndexValueMask, + m1, m2 (both odd) are the lower bits of the rapidhash secrets, m1_inv, + m2_inv (modular inverses) are precomputed modular inverse of m1 and m2. + The pre-computed values are appended to the hash_seed ByteArray in + ReadOnlyRoots and accessed in generated code to reduce overhead. + In call sites that don't already have access to the seeds, we read them + from the current isolate group/isolate's read only roots. + + To consolidate the code that encode/decode these hashes, this patch + adds MakeArrayIndexHash/DecodeArrayIndexFromHashField in C++ and CSA + that perform seeding/unseeding if enabled, and updates places where + encoding/decoding of array index is needed to use them. + + Bug: 477515021 + Change-Id: I350afe511951a54c4378396538152cc56565fd55 + Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7564330 + Reviewed-by: Leszek Swirski + Commit-Queue: Joyee Cheung + Cr-Commit-Position: refs/heads/main@{#105596} + +Refs: https://github.com/v8/v8/commit/1361b2a49d020a718dc5495713eae0fa67d697b9 +Co-authored-by: Joyee Cheung + +deps: V8: cherry-pick aac14dd95e5b + +Original commit message: + + [string] add 3rd round to seeded array index hash + + Since we already have 3 derived secrets, and arithmetics are + relatively cheap, add a 3rd round to the xorshift-multiply + seeding scheme. This brings the bias from ~3.4 to ~0.4. + + Bug: 477515021 + Change-Id: I1ef48954bcee8768d8c90db06ac8adb02f06cebf + Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7655117 + Reviewed-by: Chengzhong Wu + Commit-Queue: Joyee Cheung + Reviewed-by: Leszek Swirski + Cr-Commit-Position: refs/heads/main@{#105824} + +Refs: https://github.com/v8/v8/commit/aac14dd95e5be0d487eba6bcdaf9cef4f8bd806c +PR-URL: https://github.com/nodejs-private/node-private/pull/834 +CVE-ID: CVE-2026-21717 +--- + common.gypi | 2 +- + deps/v8/.gitignore | 1 + + deps/v8/BUILD.bazel | 10 + + deps/v8/BUILD.gn | 18 +- + deps/v8/src/DEPS | 11 +- + deps/v8/src/ast/ast-value-factory.cc | 6 +- + deps/v8/src/ast/ast-value-factory.h | 13 +- + deps/v8/src/builtins/number.tq | 4 +- + deps/v8/src/codegen/code-stub-assembler.cc | 67 +++- + deps/v8/src/codegen/code-stub-assembler.h | 6 + + deps/v8/src/codegen/external-reference.cc | 3 +- + deps/v8/src/debug/debug-interface.cc | 3 +- + deps/v8/src/heap/factory-base.cc | 13 +- + deps/v8/src/heap/factory-base.h | 6 +- + deps/v8/src/heap/factory.cc | 3 +- + deps/v8/src/heap/heap.cc | 13 - + deps/v8/src/heap/heap.h | 3 - + deps/v8/src/heap/read-only-heap-inl.h | 12 +- + deps/v8/src/heap/read-only-heap.h | 5 + + deps/v8/src/heap/setup-heap-internal.cc | 7 +- + deps/v8/src/json/json-parser.cc | 1 - + deps/v8/src/json/json-parser.h | 1 - + deps/v8/src/numbers/hash-seed-inl.h | 56 ++-- + deps/v8/src/numbers/hash-seed.cc | 114 +++++++ + deps/v8/src/numbers/hash-seed.h | 102 +++++++ + deps/v8/src/objects/dictionary-inl.h | 4 +- + deps/v8/src/objects/name.h | 14 +- + deps/v8/src/objects/name.tq | 41 ++- + deps/v8/src/objects/objects.cc | 16 - + deps/v8/src/objects/string-inl.h | 15 +- + deps/v8/src/objects/string-table.cc | 8 +- + deps/v8/src/objects/string.cc | 14 +- + deps/v8/src/parsing/parse-info.h | 7 +- + .../profiler/heap-snapshot-generator-inl.h | 3 +- + deps/v8/src/profiler/strings-storage.cc | 2 +- + deps/v8/src/runtime/runtime.cc | 4 +- + .../v8/src/snapshot/read-only-deserializer.cc | 3 +- + .../src/snapshot/shared-heap-deserializer.cc | 3 +- + deps/v8/src/strings/string-hasher-inl.h | 89 +++++- + deps/v8/src/strings/string-hasher.h | 60 +++- + deps/v8/src/torque/torque-parser.cc | 5 + + deps/v8/src/utils/utils.h | 2 - + deps/v8/src/wasm/wasm-module.cc | 2 +- + deps/v8/test/cctest/heap/test-heap.cc | 2 +- + .../test/cctest/test-code-stub-assembler.cc | 2 +- + deps/v8/test/cctest/test-serialize.cc | 4 +- + deps/v8/test/cctest/test-shared-strings.cc | 2 +- + deps/v8/test/cctest/test-strings.cc | 66 ++-- + deps/v8/third_party/rapidhash-v8/LICENSE | 34 +++ + deps/v8/third_party/rapidhash-v8/OWNERS | 2 + + .../third_party/rapidhash-v8/README.chromium | 20 ++ + deps/v8/third_party/rapidhash-v8/rapidhash.h | 285 ++++++++++++++++++ + deps/v8/third_party/rapidhash-v8/secret.h | 198 ++++++++++++ + test/fixtures/array-hash-collision.js | 27 ++ + test/pummel/test-array-hash-collision.js | 27 ++ + tools/make-v8.sh | 17 +- + tools/v8_gypfiles/features.gypi | 6 + + 57 files changed, 1295 insertions(+), 169 deletions(-) + create mode 100644 deps/v8/src/numbers/hash-seed.cc + create mode 100644 deps/v8/src/numbers/hash-seed.h + create mode 100644 deps/v8/third_party/rapidhash-v8/LICENSE + create mode 100644 deps/v8/third_party/rapidhash-v8/OWNERS + create mode 100644 deps/v8/third_party/rapidhash-v8/README.chromium + create mode 100644 deps/v8/third_party/rapidhash-v8/rapidhash.h + create mode 100644 deps/v8/third_party/rapidhash-v8/secret.h + create mode 100644 test/fixtures/array-hash-collision.js + create mode 100644 test/pummel/test-array-hash-collision.js + +--- a/common.gypi ++++ b/common.gypi +@@ -36,7 +36,7 @@ + + # Reset this number to 0 on major V8 upgrades. + # Increment by one for each non-official patch applied to deps/v8. +- 'v8_embedder_string': '-node.26', ++ 'v8_embedder_string': '-node.38', + + ##### V8 defaults for Node.js ##### + +--- a/deps/v8/.gitignore ++++ b/deps/v8/.gitignore +@@ -78,6 +78,7 @@ + !/third_party/googletest/src/googletest/include/gtest + /third_party/googletest/src/googletest/include/gtest/* + !/third_party/googletest/src/googletest/include/gtest/gtest_prod.h ++!/third_party/rapidhash-v8 + !/third_party/test262-harness + !/third_party/v8 + !/third_party/wasm-api +--- a/deps/v8/BUILD.bazel ++++ b/deps/v8/BUILD.bazel +@@ -164,6 +164,11 @@ + default = False, + ) + ++v8_flag( ++ name = "v8_enable_seeded_array_index_hash", ++ default = False, ++) ++ + v8_int( + name = "v8_typed_array_max_size_in_heap", + default = 64, +@@ -339,6 +344,7 @@ + "v8_enable_verify_heap": "VERIFY_HEAP", + "v8_enable_verify_predictable": "VERIFY_PREDICTABLE", + "v8_enable_webassembly": "V8_ENABLE_WEBASSEMBLY", ++ "v8_enable_seeded_array_index_hash": "V8_ENABLE_SEEDED_ARRAY_INDEX_HASH", + "v8_jitless": "V8_JITLESS", + }, + defines = [ +@@ -1685,6 +1691,8 @@ + "src/numbers/conversions-inl.h", + "src/numbers/conversions.cc", + "src/numbers/conversions.h", ++ "src/numbers/hash-seed.h", ++ "src/numbers/hash-seed.cc", + "src/numbers/hash-seed-inl.h", + "src/numbers/integer-literal-inl.h", + "src/numbers/integer-literal.h", +@@ -2245,6 +2253,8 @@ + "src/execution/pointer-authentication-dummy.h", + "src/heap/third-party/heap-api.h", + "src/heap/third-party/heap-api-stub.cc", ++ "third_party/rapidhash-v8/rapidhash.h", ++ "third_party/rapidhash-v8/secret.h", + ] + select({ + "@v8//bazel/config:v8_target_ia32": [ + "src/baseline/ia32/baseline-assembler-ia32-inl.h", +--- a/deps/v8/BUILD.gn ++++ b/deps/v8/BUILD.gn +@@ -399,6 +399,12 @@ + # iOS (non-simulator) does not have executable pages for 3rd party + # applications yet so disable jit. + v8_jitless = v8_enable_lite_mode || target_is_ios_device ++ ++ # Use a hard-coded secret value when hashing. ++ v8_use_default_hasher_secret = true ++ ++ # Enable seeded array index hash. ++ v8_enable_seeded_array_index_hash = false + } + + # Derived defaults. +@@ -802,6 +808,7 @@ + # List of defines that can appear in externally visible header files and that + # are controlled by args.gn. + external_v8_defines = [ ++ "V8_USE_DEFAULT_HASHER_SECRET=${v8_use_default_hasher_secret}", + "V8_ENABLE_CHECKS", + "V8_COMPRESS_POINTERS", + "V8_COMPRESS_POINTERS_IN_SHARED_CAGE", +@@ -818,7 +825,9 @@ + "V8_ENABLE_CONSERVATIVE_STACK_SCANNING", + ] + +-enabled_external_v8_defines = [] ++enabled_external_v8_defines = [ ++ "V8_USE_DEFAULT_HASHER_SECRET=${v8_use_default_hasher_secret}", ++] + + if (v8_enable_v8_checks) { + enabled_external_v8_defines += [ "V8_ENABLE_CHECKS" ] +@@ -970,6 +979,9 @@ + if (v8_enable_lite_mode) { + defines += [ "V8_LITE_MODE" ] + } ++ if (v8_enable_seeded_array_index_hash) { ++ defines += [ "V8_ENABLE_SEEDED_ARRAY_INDEX_HASH" ] ++ } + if (v8_enable_gdbjit) { + defines += [ "ENABLE_GDB_JIT_INTERFACE" ] + } +@@ -3381,6 +3393,7 @@ + "src/numbers/conversions-inl.h", + "src/numbers/conversions.h", + "src/numbers/hash-seed-inl.h", ++ "src/numbers/hash-seed.h", + "src/numbers/math-random.h", + "src/objects/all-objects-inl.h", + "src/objects/allocation-site-inl.h", +@@ -3710,6 +3723,8 @@ + "src/temporal/temporal-parser.h", + "src/third_party/siphash/halfsiphash.h", + "src/third_party/utf8-decoder/utf8-decoder.h", ++ "third_party/rapidhash-v8/rapidhash.h", ++ "third_party/rapidhash-v8/secret.h", + "src/torque/runtime-macro-shims.h", + "src/tracing/trace-event.h", + "src/tracing/traced-value.h", +@@ -4872,6 +4887,7 @@ + "src/logging/runtime-call-stats.cc", + "src/logging/tracing-flags.cc", + "src/numbers/conversions.cc", ++ "src/numbers/hash-seed.cc", + "src/numbers/math-random.cc", + "src/objects/backing-store.cc", + "src/objects/bigint.cc", +--- a/deps/v8/src/DEPS ++++ b/deps/v8/src/DEPS +@@ -100,5 +100,14 @@ + ], + "etw-jit-metadata-win\.h": [ + "+src/libplatform/etw/etw-provider-win.h", +- ] ++ ], ++ "string-hasher-inl\.h": [ ++ "+third_party/rapidhash-v8/rapidhash.h", ++ ], ++ "heap\.cc": [ ++ "+third_party/rapidhash-v8/secret.h", ++ ], ++ "hash-seed\.cc": [ ++ "+third_party/rapidhash-v8/secret.h", ++ ], + } +--- a/deps/v8/src/ast/ast-value-factory.cc ++++ b/deps/v8/src/ast/ast-value-factory.cc +@@ -82,7 +82,8 @@ + // can't be convertible to an array index. + if (!IsIntegerIndex()) return false; + if (length() <= Name::kMaxCachedArrayIndexLength) { +- *index = Name::ArrayIndexValueBits::decode(raw_hash_field_); ++ *index = StringHasher::DecodeArrayIndexFromHashField( ++ raw_hash_field_, HashSeed(ReadOnlyHeap::GetReadOnlyRoots())); + return true; + } + // Might be an index, but too big to cache it. Do the slow conversion. This +@@ -291,7 +292,8 @@ + return result; + } + +-AstStringConstants::AstStringConstants(Isolate* isolate, uint64_t hash_seed) ++AstStringConstants::AstStringConstants(Isolate* isolate, ++ const HashSeed hash_seed) + : zone_(isolate->allocator(), ZONE_NAME), + string_table_(), + hash_seed_(hash_seed) { +--- a/deps/v8/src/ast/ast-value-factory.h ++++ b/deps/v8/src/ast/ast-value-factory.h +@@ -35,6 +35,7 @@ + #include "src/common/globals.h" + #include "src/handles/handles.h" + #include "src/numbers/conversions.h" ++#include "src/numbers/hash-seed.h" + #include "src/objects/name.h" + + // Ast(Raw|Cons)String and AstValueFactory are for storing strings and +@@ -290,7 +291,7 @@ + + class AstStringConstants final { + public: +- AstStringConstants(Isolate* isolate, uint64_t hash_seed); ++ AstStringConstants(Isolate* isolate, const HashSeed hash_seed); + AstStringConstants(const AstStringConstants&) = delete; + AstStringConstants& operator=(const AstStringConstants&) = delete; + +@@ -299,13 +300,13 @@ + AST_STRING_CONSTANTS(F) + #undef F + +- uint64_t hash_seed() const { return hash_seed_; } ++ const HashSeed hash_seed() const { return hash_seed_; } + const AstRawStringMap* string_table() const { return &string_table_; } + + private: + Zone zone_; + AstRawStringMap string_table_; +- uint64_t hash_seed_; ++ const HashSeed hash_seed_; + + #define F(name, str) AstRawString* name##_string_; + AST_STRING_CONSTANTS(F) +@@ -315,12 +316,12 @@ + class AstValueFactory { + public: + AstValueFactory(Zone* zone, const AstStringConstants* string_constants, +- uint64_t hash_seed) ++ const HashSeed hash_seed) + : AstValueFactory(zone, zone, string_constants, hash_seed) {} + + AstValueFactory(Zone* ast_raw_string_zone, Zone* single_parse_zone, + const AstStringConstants* string_constants, +- uint64_t hash_seed) ++ const HashSeed hash_seed) + : string_table_(string_constants->string_table()), + strings_(nullptr), + strings_end_(&strings_), +@@ -418,7 +419,7 @@ + Zone* ast_raw_string_zone_; + Zone* single_parse_zone_; + +- uint64_t hash_seed_; ++ const HashSeed hash_seed_; + }; + + extern template EXPORT_TEMPLATE_DECLARE( +--- a/deps/v8/src/builtins/number.tq ++++ b/deps/v8/src/builtins/number.tq +@@ -298,7 +298,7 @@ + const hash: NameHash = s.raw_hash_field; + if (IsIntegerIndex(hash) && + hash.array_index_length < kMaxCachedArrayIndexLength) { +- const arrayIndex: uint32 = hash.array_index_value; ++ const arrayIndex: uint32 = DecodeArrayIndexFromHashField(hash); + return SmiFromUint32(arrayIndex); + } + // Fall back to the runtime to convert string to a number. +@@ -349,7 +349,7 @@ + const hash: NameHash = s.raw_hash_field; + if (IsIntegerIndex(hash) && + hash.array_index_length < kMaxCachedArrayIndexLength) { +- const arrayIndex: uint32 = hash.array_index_value; ++ const arrayIndex: uint32 = DecodeArrayIndexFromHashField(hash); + return SmiFromUint32(arrayIndex); + } + // Fall back to the runtime. +--- a/deps/v8/src/codegen/code-stub-assembler.cc ++++ b/deps/v8/src/codegen/code-stub-assembler.cc +@@ -2185,6 +2185,66 @@ + return var_hash.value(); + } + ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++// Mirror C++ StringHasher::SeedArrayIndexValue. ++TNode CodeStubAssembler::SeedArrayIndexValue(TNode value) { ++ // Load m1, m2 and m3 from the hash seed byte array. In the compiled code ++ // these will always come from the read-only roots. ++ TNode hash_seed = CAST(LoadRoot(RootIndex::kHashSeed)); ++ intptr_t base_offset = ByteArray::kHeaderSize - kHeapObjectTag; ++ TNode m1 = Load( ++ hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM1Offset)); ++ TNode m2 = Load( ++ hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM2Offset)); ++ TNode m3 = Load( ++ hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM3Offset)); ++ ++ TNode x = value; ++ // 3-round xorshift-multiply. ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ x = Word32And(Uint32Mul(Unsigned(x), m1), ++ Uint32Constant(Name::kArrayIndexValueMask)); ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ x = Word32And(Uint32Mul(Unsigned(x), m2), ++ Uint32Constant(Name::kArrayIndexValueMask)); ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ x = Word32And(Uint32Mul(Unsigned(x), m3), ++ Uint32Constant(Name::kArrayIndexValueMask)); ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ ++ return Unsigned(x); ++} ++ ++// Mirror C++ StringHasher::UnseedArrayIndexValue. ++TNode CodeStubAssembler::UnseedArrayIndexValue(TNode value) { ++ // Load m1_inv, m2_inv and m3_inv from the hash seed byte array. In the ++ // compiled code these will always come from the read-only roots. ++ TNode hash_seed = CAST(LoadRoot(RootIndex::kHashSeed)); ++ intptr_t base_offset = ByteArray::kHeaderSize - kHeapObjectTag; ++ TNode m1_inv = Load( ++ hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM1InvOffset)); ++ TNode m2_inv = Load( ++ hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM2InvOffset)); ++ TNode m3_inv = Load( ++ hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM3InvOffset)); ++ ++ TNode x = value; ++ // 3-round xorshift-multiply (inverse). ++ // Xorshift is an involution when kShift is at least half of the value width. ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ x = Word32And(Uint32Mul(Unsigned(x), m3_inv), ++ Uint32Constant(Name::kArrayIndexValueMask)); ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ x = Word32And(Uint32Mul(Unsigned(x), m2_inv), ++ Uint32Constant(Name::kArrayIndexValueMask)); ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ x = Word32And(Uint32Mul(Unsigned(x), m1_inv), ++ Uint32Constant(Name::kArrayIndexValueMask)); ++ x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift))); ++ return Unsigned(x); ++} ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ + TNode CodeStubAssembler::LoadNameHashAssumeComputed(TNode name) { + TNode hash_field = LoadNameRawHash(name); + CSA_DCHECK(this, IsClearWord32(hash_field, Name::kHashNotComputedMask)); +@@ -7620,8 +7680,7 @@ + GotoIf(IsSetWord32(raw_hash_field, Name::kDoesNotContainCachedArrayIndexMask), + &runtime); + +- var_result = SmiTag(Signed( +- DecodeWordFromWord32(raw_hash_field))); ++ var_result = SmiFromUint32(DecodeArrayIndexFromHashField(raw_hash_field)); + Goto(&end); + + BIND(&runtime); +@@ -8496,8 +8555,8 @@ + + BIND(&if_has_cached_index); + { +- TNode index = Signed( +- DecodeWordFromWord32(raw_hash_field)); ++ TNode index = Signed(ChangeUint32ToWord( ++ DecodeArrayIndexFromHashField(raw_hash_field))); + CSA_DCHECK(this, IntPtrLessThan(index, IntPtrConstant(INT_MAX))); + *var_index = index; + Goto(if_keyisindex); +--- a/deps/v8/src/codegen/code-stub-assembler.h ++++ b/deps/v8/src/codegen/code-stub-assembler.h +@@ -4293,6 +4293,12 @@ + TNode property_details, + Label* needs_resize); + ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ // Mirror C++ StringHasher::SeedArrayIndexValue and UnseedArrayIndexValue. ++ TNode SeedArrayIndexValue(TNode value); ++ TNode UnseedArrayIndexValue(TNode value); ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ + private: + friend class CodeStubArguments; + +--- a/deps/v8/src/codegen/external-reference.cc ++++ b/deps/v8/src/codegen/external-reference.cc +@@ -1096,7 +1096,8 @@ + + static uint32_t ComputeSeededIntegerHash(Isolate* isolate, int32_t key) { + DisallowGarbageCollection no_gc; +- return ComputeSeededHash(static_cast(key), HashSeed(isolate)); ++ return ComputeSeededHash(static_cast(key), ++ HashSeed(isolate).seed()); + } + + FUNCTION_REFERENCE(compute_integer_hash, ComputeSeededIntegerHash) +--- a/deps/v8/src/debug/debug-interface.cc ++++ b/deps/v8/src/debug/debug-interface.cc +@@ -899,7 +899,8 @@ + wire_bytes.GetFunctionBytes(&func); + // TODO(herhut): Maybe also take module, name and signature into account. + return i::StringHasher::HashSequentialString(function_bytes.begin(), +- function_bytes.length(), 0); ++ function_bytes.length(), ++ internal::HashSeed::Default()); + } + + int WasmScript::CodeOffset() const { +--- a/deps/v8/src/heap/factory-base.cc ++++ b/deps/v8/src/heap/factory-base.cc +@@ -229,7 +229,8 @@ + + template + Handle FactoryBase::NewByteArray(int length, +- AllocationType allocation) { ++ AllocationType allocation, ++ AllocationAlignment alignment) { + if (length < 0 || length > ByteArray::kMaxLength) { + FATAL("Fatal JavaScript invalid size error %d", length); + UNREACHABLE(); +@@ -237,7 +238,7 @@ + if (length == 0) return impl()->empty_byte_array(); + int size = ALIGN_TO_ALLOCATION_ALIGNMENT(ByteArray::SizeFor(length)); + HeapObject result = AllocateRawWithImmortalMap( +- size, allocation, read_only_roots().byte_array_map()); ++ size, allocation, read_only_roots().byte_array_map(), alignment); + DisallowGarbageCollection no_gc; + ByteArray array = ByteArray::cast(result); + array.set_length(length); +@@ -972,7 +973,8 @@ + if (raw.raw_hash_field() == String::kEmptyHashField && + number.value() >= 0) { + uint32_t raw_hash_field = StringHasher::MakeArrayIndexHash( +- static_cast(number.value()), raw.length()); ++ static_cast(number.value()), raw.length(), ++ HashSeed(read_only_roots())); + raw.set_raw_hash_field(raw_hash_field); + } + } +@@ -1123,8 +1125,9 @@ + + template + HeapObject FactoryBase::AllocateRawArray(int size, +- AllocationType allocation) { +- HeapObject result = AllocateRaw(size, allocation); ++ AllocationType allocation, ++ AllocationAlignment alignment) { ++ HeapObject result = AllocateRaw(size, allocation, alignment); + if (!V8_ENABLE_THIRD_PARTY_HEAP_BOOL && + (size > + isolate()->heap()->AsHeap()->MaxRegularHeapObjectSize(allocation)) && +--- a/deps/v8/src/heap/factory-base.h ++++ b/deps/v8/src/heap/factory-base.h +@@ -159,7 +159,8 @@ + + // The function returns a pre-allocated empty byte array for length = 0. + Handle NewByteArray( +- int length, AllocationType allocation = AllocationType::kYoung); ++ int length, AllocationType allocation = AllocationType::kYoung, ++ AllocationAlignment alignment = kTaggedAligned); + + Handle NewBytecodeArray(int length, const byte* raw_bytecodes, + int frame_size, int parameter_count, +@@ -327,7 +328,8 @@ + static constexpr int kNumberToStringBufferSize = 32; + + // Allocate memory for an uninitialized array (e.g., a FixedArray or similar). +- HeapObject AllocateRawArray(int size, AllocationType allocation); ++ HeapObject AllocateRawArray(int size, AllocationType allocation, ++ AllocationAlignment alignment = kTaggedAligned); + HeapObject AllocateRawFixedArray(int length, AllocationType allocation); + HeapObject AllocateRawWeakArrayList(int length, AllocationType allocation); + +--- a/deps/v8/src/heap/factory.cc ++++ b/deps/v8/src/heap/factory.cc +@@ -3467,7 +3467,8 @@ + if (value <= JSArray::kMaxArrayIndex && + raw.raw_hash_field() == String::kEmptyHashField) { + uint32_t raw_hash_field = StringHasher::MakeArrayIndexHash( +- static_cast(value), raw.length()); ++ static_cast(value), raw.length(), ++ HashSeed(read_only_roots())); + raw.set_raw_hash_field(raw_hash_field); + } + } +--- a/deps/v8/src/heap/heap.cc ++++ b/deps/v8/src/heap/heap.cc +@@ -5630,19 +5630,6 @@ + heap_allocator_.Setup(); + } + +-void Heap::InitializeHashSeed() { +- DCHECK(!deserialization_complete_); +- uint64_t new_hash_seed; +- if (v8_flags.hash_seed == 0) { +- int64_t rnd = isolate()->random_number_generator()->NextInt64(); +- new_hash_seed = static_cast(rnd); +- } else { +- new_hash_seed = static_cast(v8_flags.hash_seed); +- } +- ReadOnlyRoots(this).hash_seed().copy_in( +- 0, reinterpret_cast(&new_hash_seed), kInt64Size); +-} +- + // static + void Heap::InitializeOncePerProcess() { + #ifdef V8_ENABLE_ALLOCATION_TIMEOUT +--- a/deps/v8/src/heap/heap.h ++++ b/deps/v8/src/heap/heap.h +@@ -822,9 +822,6 @@ + // Prepares the heap, setting up for deserialization. + void InitializeMainThreadLocalHeap(LocalHeap* main_thread_local_heap); + +- // (Re-)Initialize hash seed from flag or RNG. +- void InitializeHashSeed(); +- + // Invoked once for the process from V8::Initialize. + static void InitializeOncePerProcess(); + +--- a/deps/v8/src/heap/read-only-heap-inl.h ++++ b/deps/v8/src/heap/read-only-heap-inl.h +@@ -28,15 +28,21 @@ + // static + ReadOnlyRoots ReadOnlyHeap::GetReadOnlyRoots(HeapObject object) { + #ifdef V8_SHARED_RO_HEAP ++ return GetReadOnlyRoots(); ++#else ++ return ReadOnlyRoots(GetHeapFromWritableObject(object)); ++#endif // V8_SHARED_RO_HEAP ++} ++ ++#ifdef V8_SHARED_RO_HEAP ++ReadOnlyRoots ReadOnlyHeap::GetReadOnlyRoots() { + auto* shared_ro_heap = SoleReadOnlyHeap::shared_ro_heap_; + // If this check fails in code that runs during initialization use + // EarlyGetReadOnlyRoots instead. + DCHECK(shared_ro_heap && shared_ro_heap->roots_init_complete()); + return ReadOnlyRoots(shared_ro_heap->read_only_roots_); +-#else +- return ReadOnlyRoots(GetHeapFromWritableObject(object)); +-#endif // V8_SHARED_RO_HEAP + } ++#endif // V8_SHARED_RO_HEAP + + } // namespace internal + } // namespace v8 +--- a/deps/v8/src/heap/read-only-heap.h ++++ b/deps/v8/src/heap/read-only-heap.h +@@ -76,6 +76,11 @@ + // must be initialized + V8_EXPORT_PRIVATE inline static ReadOnlyRoots GetReadOnlyRoots( + HeapObject object); ++ ++#ifdef V8_SHARED_RO_HEAP ++ V8_EXPORT_PRIVATE inline static ReadOnlyRoots GetReadOnlyRoots(); ++#endif // V8_SHARED_RO_HEAP ++ + // Returns the current isolates roots table during initialization as opposed + // to the shared one in case the latter is not initialized yet. + V8_EXPORT_PRIVATE inline static ReadOnlyRoots EarlyGetReadOnlyRoots( +--- a/deps/v8/src/heap/setup-heap-internal.cc ++++ b/deps/v8/src/heap/setup-heap-internal.cc +@@ -13,6 +13,7 @@ + #include "src/init/heap-symbols.h" + #include "src/init/setup-isolate.h" + #include "src/interpreter/interpreter.h" ++#include "src/numbers/hash-seed.h" + #include "src/objects/arguments.h" + #include "src/objects/call-site-info.h" + #include "src/objects/cell-inl.h" +@@ -709,8 +710,10 @@ + // Hash seed for strings + + Factory* factory = isolate()->factory(); +- set_hash_seed(*factory->NewByteArray(kInt64Size, AllocationType::kReadOnly)); +- InitializeHashSeed(); ++ set_hash_seed(*factory->NewByteArray(HashSeed::kTotalSize, ++ AllocationType::kReadOnly, ++ AllocationAlignment::kDoubleAligned)); ++ HashSeed::InitializeRoots(isolate()); + + // Important strings and symbols + for (const ConstantStringInit& entry : kImportantConstantStringTable) { +--- a/deps/v8/src/json/json-parser.cc ++++ b/deps/v8/src/json/json-parser.cc +@@ -310,7 +310,6 @@ + template + JsonParser::JsonParser(Isolate* isolate, Handle source) + : isolate_(isolate), +- hash_seed_(HashSeed(isolate)), + object_constructor_(isolate_->object_function()), + original_source_(source) { + size_t start = 0; +--- a/deps/v8/src/json/json-parser.h ++++ b/deps/v8/src/json/json-parser.h +@@ -382,7 +382,6 @@ + int position() const { return static_cast(cursor_ - chars_); } + + Isolate* isolate_; +- const uint64_t hash_seed_; + JsonToken next_; + // Indicates whether the bytes underneath source_ can relocate during GC. + bool chars_may_relocate_; +--- a/deps/v8/src/numbers/hash-seed-inl.h ++++ b/deps/v8/src/numbers/hash-seed-inl.h +@@ -5,48 +5,36 @@ + #ifndef V8_NUMBERS_HASH_SEED_INL_H_ + #define V8_NUMBERS_HASH_SEED_INL_H_ + +-#include +- +-// The #includes below currently lead to cyclic transitive includes, so +-// HashSeed() ends up being required before it is defined, so we have to +-// declare it here. This is a workaround; if we needed this permanently then +-// we should put that line into a "hash-seed.h" header; but we won't need +-// it for long. +-// TODO(jkummerow): Get rid of this by breaking circular include dependencies. +-namespace v8 { +-namespace internal { +- +-class Isolate; +-class LocalIsolate; +-class ReadOnlyRoots; +- +-inline uint64_t HashSeed(Isolate* isolate); +-inline uint64_t HashSeed(LocalIsolate* isolate); +-inline uint64_t HashSeed(ReadOnlyRoots roots); +- +-} // namespace internal +-} // namespace v8 +- +-// See comment above for why this isn't at the top of the file. ++#include "src/numbers/hash-seed.h" + #include "src/objects/fixed-array-inl.h" + #include "src/roots/roots-inl.h" + + namespace v8 { + namespace internal { + +-inline uint64_t HashSeed(Isolate* isolate) { +- return HashSeed(ReadOnlyRoots(isolate)); +-} +- +-inline uint64_t HashSeed(LocalIsolate* isolate) { +- return HashSeed(ReadOnlyRoots(isolate)); +-} +- +-inline uint64_t HashSeed(ReadOnlyRoots roots) { +- uint64_t seed; +- roots.hash_seed().copy_out(0, reinterpret_cast(&seed), kInt64Size); +- return seed; +-} ++inline HashSeed::HashSeed(Isolate* isolate) ++ : HashSeed(ReadOnlyRoots(isolate)) {} ++ ++inline HashSeed::HashSeed(LocalIsolate* isolate) ++ : HashSeed(ReadOnlyRoots(isolate)) {} ++ ++inline HashSeed::HashSeed(ReadOnlyRoots roots) ++ : data_(reinterpret_cast( ++ roots.hash_seed().GetDataStartAddress())) {} ++ ++inline HashSeed HashSeed::Default() { return HashSeed(kDefaultData); } ++ ++inline uint64_t HashSeed::seed() const { return data_->seed; } ++inline const uint64_t* HashSeed::secret() const { return data_->secrets; } ++ ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++inline uint32_t HashSeed::m1() const { return data_->m1; } ++inline uint32_t HashSeed::m1_inv() const { return data_->m1_inv; } ++inline uint32_t HashSeed::m2() const { return data_->m2; } ++inline uint32_t HashSeed::m2_inv() const { return data_->m2_inv; } ++inline uint32_t HashSeed::m3() const { return data_->m3; } ++inline uint32_t HashSeed::m3_inv() const { return data_->m3_inv; } ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH + + } // namespace internal + } // namespace v8 +--- /dev/null ++++ b/deps/v8/src/numbers/hash-seed.cc +@@ -0,0 +1,114 @@ ++// Copyright 2026 the V8 project authors. All rights reserved. ++// Use of this source code is governed by a BSD-style license that can be ++// found in the LICENSE file. ++ ++#include "src/numbers/hash-seed.h" ++ ++#include ++ ++#include "src/execution/isolate.h" ++#include "src/flags/flags.h" ++#include "src/numbers/hash-seed-inl.h" ++#include "src/objects/name.h" ++#include "third_party/rapidhash-v8/secret.h" ++ ++namespace v8 { ++namespace internal { ++ ++namespace { ++ ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++// Calculate the modular inverse using Newton's method. ++constexpr uint32_t modular_inverse(uint32_t m) { ++ uint32_t x = (3 * m) ^ 2; // 5 correct bits ++ x = x * (2 - m * x); // 10 correct bits ++ x = x * (2 - m * x); // 20 correct bits ++ x = x * (2 - m * x); // 40 correct bits ++ return x; ++} ++ ++constexpr uint32_t truncate_for_derived_secrets(uint64_t s) { ++ return static_cast(s) & Name::kArrayIndexValueMask; ++} ++ ++// Derive a multiplier from a rapidhash secret and ensure it's odd. ++constexpr uint32_t derive_multiplier(uint64_t secret) { ++ return truncate_for_derived_secrets(secret) | 1; ++} ++ ++// Compute the modular inverse of the derived multiplier. ++constexpr uint32_t derive_multiplier_inverse(uint64_t secret) { ++ return truncate_for_derived_secrets( ++ modular_inverse(derive_multiplier(secret))); ++} ++ ++constexpr bool is_modular_inverse(uint32_t m, uint32_t m_inv) { ++ return ((m * m_inv) & Name::kArrayIndexValueMask) == 1; ++} ++ ++constexpr void DeriveSecretsForArrayIndexHash(HashSeed::Data* data) { ++ data->m1 = derive_multiplier(data->secrets[0]); ++ data->m1_inv = derive_multiplier_inverse(data->secrets[0]); ++ data->m2 = derive_multiplier(data->secrets[1]); ++ data->m2_inv = derive_multiplier_inverse(data->secrets[1]); ++ data->m3 = derive_multiplier(data->secrets[2]); ++ data->m3_inv = derive_multiplier_inverse(data->secrets[2]); ++} ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ ++static constexpr HashSeed::Data kDefaultSeed = [] { ++ HashSeed::Data d{}; ++ d.seed = 0; ++ d.secrets[0] = RAPIDHASH_DEFAULT_SECRET[0]; ++ d.secrets[1] = RAPIDHASH_DEFAULT_SECRET[1]; ++ d.secrets[2] = RAPIDHASH_DEFAULT_SECRET[2]; ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ DeriveSecretsForArrayIndexHash(&d); ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ return d; ++}(); ++ ++} // anonymous namespace ++ ++static_assert(HashSeed::kSecretsCount == arraysize(RAPIDHASH_DEFAULT_SECRET)); ++const HashSeed::Data* const HashSeed::kDefaultData = &kDefaultSeed; ++ ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++// Compile-time verification that m * m_inv === 1 for the derived secrets. ++static_assert(is_modular_inverse(kDefaultSeed.m1, kDefaultSeed.m1_inv)); ++static_assert(is_modular_inverse(kDefaultSeed.m2, kDefaultSeed.m2_inv)); ++static_assert(is_modular_inverse(kDefaultSeed.m3, kDefaultSeed.m3_inv)); ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ ++// static ++void HashSeed::InitializeRoots(Isolate* isolate) { ++ DCHECK(!isolate->heap()->deserialization_complete()); ++ uint64_t seed; ++ if (v8_flags.hash_seed == 0) { ++ int64_t rnd = isolate->random_number_generator()->NextInt64(); ++ seed = static_cast(rnd); ++ } else { ++ seed = static_cast(v8_flags.hash_seed); ++ } ++ ++ // Write the seed and derived secrets into the read-only roots ByteArray. ++ Data* data = const_cast(HashSeed(isolate).data_); ++ ++#if V8_USE_DEFAULT_HASHER_SECRET ++ // Copy from the default seed and just override the meta seed. ++ *data = kDefaultSeed; ++ data->seed = seed; ++#else ++ data->seed = seed; ++ rapidhash_make_secret(seed, data->secrets); ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ DeriveSecretsForArrayIndexHash(data); ++ DCHECK(is_modular_inverse(data->m1, data->m1_inv)); ++ DCHECK(is_modular_inverse(data->m2, data->m2_inv)); ++ DCHECK(is_modular_inverse(data->m3, data->m3_inv)); ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++#endif // V8_USE_DEFAULT_HASHER_SECRET ++} ++ ++} // namespace internal ++} // namespace v8 +--- /dev/null ++++ b/deps/v8/src/numbers/hash-seed.h +@@ -0,0 +1,102 @@ ++// Copyright 2019 the V8 project authors. All rights reserved. ++// Use of this source code is governed by a BSD-style license that can be ++// found in the LICENSE file. ++ ++#ifndef V8_NUMBERS_HASH_SEED_H_ ++#define V8_NUMBERS_HASH_SEED_H_ ++ ++#include ++#include ++#include ++ ++#include "src/base/macros.h" ++ ++namespace v8 { ++namespace internal { ++ ++class Isolate; ++class LocalIsolate; ++class ReadOnlyRoots; ++ ++// A lightweight view over the hash_seed ByteArray in read-only roots. ++class V8_EXPORT_PRIVATE HashSeed { ++ public: ++ inline explicit HashSeed(Isolate* isolate); ++ inline explicit HashSeed(LocalIsolate* isolate); ++ inline explicit HashSeed(ReadOnlyRoots roots); ++ ++ static inline HashSeed Default(); ++ ++ inline uint64_t seed() const; ++ inline const uint64_t* secret() const; ++ ++ bool operator==(const HashSeed& b) const { return data_ == b.data_; } ++ ++ static constexpr int kSecretsCount = 3; ++ ++ // The ReadOnlyRoots::hash_seed() byte array can be interpreted ++ // as a HashSeed::Data struct. ++ // Since this maps over either the read-only roots or over a static byte ++ // array, and in both cases, must be allocated at 8-byte boundaries, ++ // we don't use V8_OBJECT here. ++ struct Data { ++ // meta seed from --hash-seed, 0 = generate at startup ++ uint64_t seed; ++ // When V8_USE_DEFAULT_HASHER_SECRET is enabled, these are just ++ // RAPIDHASH_DEFAULT_SECRET. Otherwise they are derived from the seed ++ // using rapidhash_make_secret(). ++ uint64_t secrets[kSecretsCount]; ++ ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ // Additional precomputed secrets for seeding the array index value hashes. ++ uint32_t m1; // lower kArrayIndexValueBits bits of secret[0], must be odd ++ uint32_t m1_inv; // modular inverse of m1 mod 2^kArrayIndexValueBits ++ uint32_t m2; // lower kArrayIndexValueBits bits of secret[1], must be odd ++ uint32_t m2_inv; // modular inverse of m2 mod 2^kArrayIndexValueBits ++ uint32_t m3; // lower kArrayIndexValueBits bits of secret[2], must be odd ++ uint32_t m3_inv; // modular inverse of m3 mod 2^kArrayIndexValueBits ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ }; ++ ++ static constexpr int kTotalSize = sizeof(Data); ++ ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ // Byte offsets from the data start, for CSA that loads fields at raw ++ // offsets from the ByteArray data start. ++ static constexpr int kDerivedM1Offset = offsetof(Data, m1); ++ static constexpr int kDerivedM1InvOffset = offsetof(Data, m1_inv); ++ static constexpr int kDerivedM2Offset = offsetof(Data, m2); ++ static constexpr int kDerivedM2InvOffset = offsetof(Data, m2_inv); ++ static constexpr int kDerivedM3Offset = offsetof(Data, m3); ++ static constexpr int kDerivedM3InvOffset = offsetof(Data, m3_inv); ++ ++ inline uint32_t m1() const; ++ inline uint32_t m1_inv() const; ++ inline uint32_t m2() const; ++ inline uint32_t m2_inv() const; ++ inline uint32_t m3() const; ++ inline uint32_t m3_inv() const; ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ ++ // Generates a hash seed (from --hash-seed or the RNG) and writes it ++ // together with derived secrets into the isolate's hash_seed in ++ // its read-only roots. ++ static void InitializeRoots(Isolate* isolate); ++ ++ private: ++ // Pointer into the Data overlaying the ByteArray data (either ++ // points to read-only roots or to kDefaultData). ++ const Data* data_; ++ explicit HashSeed(const Data* data) : data_(data) {} ++ ++ // Points to the static constexpr default seed. ++ static const Data* const kDefaultData; ++}; ++ ++static_assert(std::is_trivially_copyable_v); ++static_assert(alignof(HashSeed::Data) == alignof(uint64_t)); ++ ++} // namespace internal ++} // namespace v8 ++ ++#endif // V8_NUMBERS_HASH_SEED_H_ +--- a/deps/v8/src/objects/dictionary-inl.h ++++ b/deps/v8/src/objects/dictionary-inl.h +@@ -275,14 +275,14 @@ + } + + uint32_t NumberDictionaryBaseShape::Hash(ReadOnlyRoots roots, uint32_t key) { +- return ComputeSeededHash(key, HashSeed(roots)); ++ return ComputeSeededHash(key, HashSeed(roots).seed()); + } + + uint32_t NumberDictionaryBaseShape::HashForObject(ReadOnlyRoots roots, + Object other) { + DCHECK(other.IsNumber()); + return ComputeSeededHash(static_cast(other.Number()), +- HashSeed(roots)); ++ HashSeed(roots).seed()); + } + + Handle NumberDictionaryBaseShape::AsHandle(Isolate* isolate, +--- a/deps/v8/src/objects/name.h ++++ b/deps/v8/src/objects/name.h +@@ -153,7 +153,19 @@ + // For strings which are array indexes the hash value has the string length + // mixed into the hash, mainly to avoid a hash value of zero which would be + // the case for the string '0'. 24 bits are used for the array index value. +- static const int kArrayIndexValueBits = 24; ++ static constexpr int kArrayIndexValueBits = 24; ++ // Mask for extracting the lower kArrayIndexValueBits of a value. ++ static constexpr uint32_t kArrayIndexValueMask = ++ (1u << kArrayIndexValueBits) - 1; ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ // Half-width shift used by the seeded xorshift-multiply mixing. ++ static constexpr int kArrayIndexHashShift = kArrayIndexValueBits / 2; ++ // The shift must be at least the half width for the xorshift to be an ++ // involution. ++ static_assert(kArrayIndexHashShift * 2 >= kArrayIndexValueBits, ++ "kArrayIndexHashShift must be at least half of " ++ "kArrayIndexValueBits"); ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH + static const int kArrayIndexLengthBits = + kBitsPerInt - kArrayIndexValueBits - HashFieldTypeBits::kSize; + +--- a/deps/v8/src/objects/name.tq ++++ b/deps/v8/src/objects/name.tq +@@ -54,6 +54,9 @@ + return (hash & kDoesNotContainCachedArrayIndexMask) == 0; + } + ++extern macro SeedArrayIndexValue(uint32): uint32; ++extern macro UnseedArrayIndexValue(uint32): uint32; ++ + const kArrayIndexValueBitsShift: uint32 = kNofHashBitFields; + const kArrayIndexLengthBitsShift: uint32 = + kNofHashBitFields + kArrayIndexValueBits; +@@ -71,8 +74,10 @@ + return hash.hash_field_type == HashFieldType::kIntegerIndex; + } + +-macro MakeArrayIndexHash(value: uint32, length: uint32): NameHash { +- // This is in sync with StringHasher::MakeArrayIndexHash. ++// This is in sync with the private StringHasher::MakeArrayIndexHash without ++// seeding. Do not call directly, use the @export MakeArrayIndexHash wrapper ++// below. ++macro MakeArrayIndexHashRaw(value: uint32, length: uint32): NameHash { + dcheck(length <= kMaxArrayIndexSize); + const one: uint32 = 1; + dcheck(TenToThe(kMaxCachedArrayIndexLength) < (one << kArrayIndexValueBits)); +@@ -86,3 +91,35 @@ + dcheck(IsIntegerIndex(hash)); + return hash; + } ++ ++// This is in sync with the private StringHasher::DecodeArrayIndexFromHashField ++// without seeding. Do not call directly, use the @export ++// DecodeArrayIndexFromHashField wrapper below. ++macro DecodeArrayIndexFromHashFieldRaw(rawHashField: uint32): uint32 { ++ const hash: NameHash = %RawDownCast(rawHashField); ++ dcheck(ContainsCachedArrayIndex(rawHashField) || IsIntegerIndex(hash)); ++ return hash.array_index_value; ++} ++ ++// Mirror C++ public StringHasher::MakeArrayIndexHash. ++@export ++macro MakeArrayIndexHash(value: uint32, length: uint32): NameHash { ++ @if(V8_ENABLE_SEEDED_ARRAY_INDEX_HASH) { ++ return MakeArrayIndexHashRaw(SeedArrayIndexValue(value), length); ++ } ++ @ifnot(V8_ENABLE_SEEDED_ARRAY_INDEX_HASH) { ++ return MakeArrayIndexHashRaw(value, length); ++ } ++} ++ ++// Mirror C++ public StringHasher::DecodeArrayIndexFromHashField. ++@export ++macro DecodeArrayIndexFromHashField(rawHashField: uint32): uint32 { ++ const value: uint32 = DecodeArrayIndexFromHashFieldRaw(rawHashField); ++ @if(V8_ENABLE_SEEDED_ARRAY_INDEX_HASH) { ++ return UnseedArrayIndexValue(value); ++ } ++ @ifnot(V8_ENABLE_SEEDED_ARRAY_INDEX_HASH) { ++ return value; ++ } ++} +--- a/deps/v8/src/objects/objects.cc ++++ b/deps/v8/src/objects/objects.cc +@@ -4817,22 +4817,6 @@ + return dest.ptr(); + } + +-uint32_t StringHasher::MakeArrayIndexHash(uint32_t value, int length) { +- // For array indexes mix the length into the hash as an array index could +- // be zero. +- DCHECK_GT(length, 0); +- DCHECK_LE(length, String::kMaxArrayIndexSize); +- DCHECK(TenToThe(String::kMaxCachedArrayIndexLength) < +- (1 << String::kArrayIndexValueBits)); +- +- value <<= String::ArrayIndexValueBits::kShift; +- value |= length << String::ArrayIndexLengthBits::kShift; +- +- DCHECK(String::IsIntegerIndex(value)); +- DCHECK_EQ(length <= String::kMaxCachedArrayIndexLength, +- Name::ContainsCachedArrayIndex(value)); +- return value; +-} + + STATIC_ASSERT_FIELD_OFFSETS_EQUAL(HeapNumber::kValueOffset, + Oddball::kToNumberRawOffset); +--- a/deps/v8/src/objects/string-inl.h ++++ b/deps/v8/src/objects/string-inl.h +@@ -353,7 +353,7 @@ + template + class SequentialStringKey final : public StringTableKey { + public: +- SequentialStringKey(const base::Vector& chars, uint64_t seed, ++ SequentialStringKey(const base::Vector& chars, const HashSeed seed, + bool convert = false) + : SequentialStringKey(StringHasher::HashSequentialString( + chars.begin(), chars.length(), seed), +@@ -766,13 +766,14 @@ + + #ifdef ENABLE_SLOW_DCHECKS + uint32_t String::FlatContent::ComputeChecksum() const { +- constexpr uint64_t hashseed = 1; + uint32_t hash; + if (state_ == ONE_BYTE) { +- hash = StringHasher::HashSequentialString(onebyte_start, length_, hashseed); ++ hash = StringHasher::HashSequentialString(onebyte_start, length_, ++ HashSeed::Default()); + } else { + DCHECK_EQ(TWO_BYTE, state_); +- hash = StringHasher::HashSequentialString(twobyte_start, length_, hashseed); ++ hash = StringHasher::HashSequentialString(twobyte_start, length_, ++ HashSeed::Default()); + } + DCHECK_NE(kChecksumVerificationDisabled, hash); + return hash; +@@ -1440,7 +1441,8 @@ + DisallowGarbageCollection no_gc; + uint32_t field = raw_hash_field(); + if (ContainsCachedArrayIndex(field)) { +- *index = ArrayIndexValueBits::decode(field); ++ *index = StringHasher::DecodeArrayIndexFromHashField( ++ field, HashSeed(EarlyGetReadOnlyRoots())); + return true; + } + if (IsHashFieldComputed(field) && !IsIntegerIndex(field)) { +@@ -1452,7 +1454,8 @@ + bool String::AsIntegerIndex(size_t* index) { + uint32_t field = raw_hash_field(); + if (ContainsCachedArrayIndex(field)) { +- *index = ArrayIndexValueBits::decode(field); ++ *index = StringHasher::DecodeArrayIndexFromHashField( ++ field, HashSeed(EarlyGetReadOnlyRoots())); + return true; + } + if (IsHashFieldComputed(field) && !IsIntegerIndex(field)) { +--- a/deps/v8/src/objects/string-table.cc ++++ b/deps/v8/src/objects/string-table.cc +@@ -728,7 +728,7 @@ + return internalized.ptr(); + } + +- uint64_t seed = HashSeed(isolate); ++ const HashSeed seed = HashSeed(isolate); + + std::unique_ptr buffer; + const Char* chars; +@@ -750,11 +750,13 @@ + } + // TODO(verwaest): Internalize to one-byte when possible. + SequentialStringKey key(raw_hash_field, +- base::Vector(chars, length), seed); ++ base::Vector(chars, length), ++ seed.seed()); + + // String could be an array index. + if (Name::ContainsCachedArrayIndex(raw_hash_field)) { +- return Smi::FromInt(String::ArrayIndexValueBits::decode(raw_hash_field)) ++ return Smi::FromInt(StringHasher::DecodeArrayIndexFromHashField( ++ raw_hash_field, seed)) + .ptr(); + } + +--- a/deps/v8/src/objects/string.cc ++++ b/deps/v8/src/objects/string.cc +@@ -818,7 +818,8 @@ + (len == 1 || data[0] != '0')) { + // String hash is not calculated yet but all the data are present. + // Update the hash field to speed up sequential convertions. +- uint32_t raw_hash_field = StringHasher::MakeArrayIndexHash(d, len); ++ uint32_t raw_hash_field = ++ StringHasher::MakeArrayIndexHash(d, len, HashSeed(isolate)); + #ifdef DEBUG + subject->EnsureHash(); // Force hash calculation. + DCHECK_EQ(subject->raw_hash_field(), raw_hash_field); +@@ -1680,7 +1681,8 @@ + namespace { + + template +-uint32_t HashString(String string, size_t start, int length, uint64_t seed, ++uint32_t HashString(String string, size_t start, int length, ++ const HashSeed seed, + PtrComprCageBase cage_base, + const SharedStringAccessGuardIfNeeded& access_guard) { + DisallowGarbageCollection no_gc; +@@ -1726,7 +1728,7 @@ + DCHECK_IMPLIES(!v8_flags.shared_string_table, !HasHashCode()); + + // Store the hash code in the object. +- uint64_t seed = HashSeed(EarlyGetReadOnlyRoots()); ++ const HashSeed seed = HashSeed(EarlyGetReadOnlyRoots()); + size_t start = 0; + String string = *this; + PtrComprCageBase cage_base = GetPtrComprCageBase(string); +@@ -1772,7 +1774,8 @@ + if (length <= kMaxCachedArrayIndexLength) { + uint32_t field = EnsureRawHash(); // Force computation of hash code. + if (!IsIntegerIndex(field)) return false; +- *index = ArrayIndexValueBits::decode(field); ++ *index = StringHasher::DecodeArrayIndexFromHashField( ++ field, HashSeed(EarlyGetReadOnlyRoots())); + return true; + } + if (length == 0 || length > kMaxArrayIndexSize) return false; +@@ -1786,7 +1789,8 @@ + if (length <= kMaxCachedArrayIndexLength) { + uint32_t field = EnsureRawHash(); // Force computation of hash code. + if (!IsIntegerIndex(field)) return false; +- *index = ArrayIndexValueBits::decode(field); ++ *index = StringHasher::DecodeArrayIndexFromHashField( ++ field, HashSeed(EarlyGetReadOnlyRoots())); + return true; + } + if (length == 0 || length > kMaxIntegerIndexSize) return false; +--- a/deps/v8/src/parsing/parse-info.h ++++ b/deps/v8/src/parsing/parse-info.h +@@ -12,6 +12,7 @@ + #include "src/base/logging.h" + #include "src/common/globals.h" + #include "src/handles/handles.h" ++#include "src/numbers/hash-seed.h" + #include "src/objects/function-kind.h" + #include "src/objects/function-syntax-kind.h" + #include "src/objects/script.h" +@@ -200,7 +201,7 @@ + AstValueFactory* ast_value_factory() const { + return ast_value_factory_.get(); + } +- uint64_t hash_seed() const { return hash_seed_; } ++ HashSeed hash_seed() const { return hash_seed_; } + AccountingAllocator* allocator() const { return allocator_; } + const AstStringConstants* ast_string_constants() const { + return ast_string_constants_; +@@ -210,7 +211,7 @@ + LazyCompileDispatcher* dispatcher() const { return dispatcher_; } + + private: +- uint64_t hash_seed_; ++ const HashSeed hash_seed_; + AccountingAllocator* allocator_; + V8FileLogger* v8_file_logger_; + LazyCompileDispatcher* dispatcher_; +@@ -245,7 +246,7 @@ + const UnoptimizedCompileFlags& flags() const { return flags_; } + + // Getters for reusable state. +- uint64_t hash_seed() const { return reusable_state_->hash_seed(); } ++ HashSeed hash_seed() const { return reusable_state_->hash_seed(); } + AccountingAllocator* allocator() const { + return reusable_state_->allocator(); + } +--- a/deps/v8/src/profiler/heap-snapshot-generator-inl.h ++++ b/deps/v8/src/profiler/heap-snapshot-generator-inl.h +@@ -55,8 +55,7 @@ + uint32_t HeapSnapshotJSONSerializer::StringHash(const void* string) { + const char* s = reinterpret_cast(string); + int len = static_cast(strlen(s)); +- return StringHasher::HashSequentialString(s, len, +- v8::internal::kZeroHashSeed); ++ return StringHasher::HashSequentialString(s, len, HashSeed::Default()); + } + + int HeapSnapshotJSONSerializer::to_node_index(const HeapEntry* e) { +--- a/deps/v8/src/profiler/strings-storage.cc ++++ b/deps/v8/src/profiler/strings-storage.cc +@@ -137,7 +137,7 @@ + + inline uint32_t ComputeStringHash(const char* str, int len) { + uint32_t raw_hash_field = +- StringHasher::HashSequentialString(str, len, kZeroHashSeed); ++ StringHasher::HashSequentialString(str, len, HashSeed::Default()); + return Name::HashBits::decode(raw_hash_field); + } + +--- a/deps/v8/src/runtime/runtime.cc ++++ b/deps/v8/src/runtime/runtime.cc +@@ -65,8 +65,8 @@ + } + + uint32_t Hash() { +- return StringHasher::HashSequentialString( +- data_, length_, v8::internal::kZeroHashSeed); ++ return StringHasher::HashSequentialString(data_, length_, ++ HashSeed::Default()); + } + + const unsigned char* data_; +--- a/deps/v8/src/snapshot/read-only-deserializer.cc ++++ b/deps/v8/src/snapshot/read-only-deserializer.cc +@@ -10,6 +10,7 @@ + #include "src/heap/heap-inl.h" // crbug.com/v8/8499 + #include "src/heap/read-only-heap.h" + #include "src/logging/counters-scopes.h" ++#include "src/numbers/hash-seed.h" + #include "src/objects/slots.h" + #include "src/roots/static-roots.h" + #include "src/snapshot/snapshot-data.h" +@@ -73,7 +74,7 @@ + PostProcessNewObjectsIfStaticRootsEnabled(); + + if (should_rehash()) { +- isolate()->heap()->InitializeHashSeed(); ++ HashSeed::InitializeRoots(isolate()); + Rehash(); + } + } +--- a/deps/v8/src/snapshot/shared-heap-deserializer.cc ++++ b/deps/v8/src/snapshot/shared-heap-deserializer.cc +@@ -24,7 +24,8 @@ + DeserializeDeferredObjects(); + + if (should_rehash()) { +- // Hash seed was initialized in ReadOnlyDeserializer. ++ // The hash seed has already been initialized in ReadOnlyDeserializer, thus ++ // there is no need to call `HashSeed::InitializeRoots(isolate());`. + Rehash(); + } + } +--- a/deps/v8/src/strings/string-hasher-inl.h ++++ b/deps/v8/src/strings/string-hasher-inl.h +@@ -10,6 +10,7 @@ + // Comment inserted to prevent header reordering. + #include + ++#include "src/common/globals.h" + #include "src/objects/name-inl.h" + #include "src/objects/string-inl.h" + #include "src/strings/char-predicates-inl.h" +@@ -46,9 +47,89 @@ + return String::CreateHashFieldValue(hash, String::HashFieldType::kHash); + } + ++uint32_t StringHasher::MakeArrayIndexHash(uint32_t value, uint32_t length) { ++ // For array indexes mix the length into the hash as an array index could ++ // be zero. ++ DCHECK_LE(length, String::kMaxArrayIndexSize); ++ DCHECK(TenToThe(String::kMaxCachedArrayIndexLength) < ++ (1 << String::kArrayIndexValueBits)); ++ ++ value <<= String::ArrayIndexValueBits::kShift; ++ value |= length << String::ArrayIndexLengthBits::kShift; ++ ++ DCHECK(String::IsIntegerIndex(value)); ++ DCHECK_EQ(length <= String::kMaxCachedArrayIndexLength, ++ Name::ContainsCachedArrayIndex(value)); ++ return value; ++} ++ ++uint32_t StringHasher::DecodeArrayIndexFromHashField(uint32_t raw_hash_field) { ++ DCHECK(String::ContainsCachedArrayIndex(raw_hash_field) || ++ String::IsIntegerIndex(raw_hash_field)); ++ return String::ArrayIndexValueBits::decode(raw_hash_field); ++} ++ ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++uint32_t StringHasher::SeedArrayIndexValue(uint32_t value, ++ const HashSeed seed) { ++ uint32_t m1 = seed.m1(); ++ uint32_t m2 = seed.m2(); ++ uint32_t m3 = seed.m3(); ++ constexpr uint32_t kShift = Name::kArrayIndexHashShift; ++ constexpr uint32_t kMask = Name::kArrayIndexValueMask; ++ // 3-round xorshift-multiply. ++ uint32_t x = value; ++ x ^= x >> kShift; ++ x = (x * m1) & kMask; ++ x ^= x >> kShift; ++ x = (x * m2) & kMask; ++ x ^= x >> kShift; ++ x = (x * m3) & kMask; ++ x ^= x >> kShift; ++ return x; ++} ++ ++uint32_t StringHasher::UnseedArrayIndexValue(uint32_t value, ++ const HashSeed seed) { ++ uint32_t m1_inv = seed.m1_inv(); ++ uint32_t m2_inv = seed.m2_inv(); ++ uint32_t m3_inv = seed.m3_inv(); ++ uint32_t x = value; ++ constexpr uint32_t kShift = Name::kArrayIndexHashShift; ++ constexpr uint32_t kMask = Name::kArrayIndexValueMask; ++ // 3-round xorshift-multiply (inverse). ++ // Xorshift is an involution when kShift is at least half of the value width. ++ x ^= x >> kShift; ++ x = (x * m3_inv) & kMask; ++ x ^= x >> kShift; ++ x = (x * m2_inv) & kMask; ++ x ^= x >> kShift; ++ x = (x * m1_inv) & kMask; ++ x ^= x >> kShift; ++ return x; ++} ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ ++uint32_t StringHasher::MakeArrayIndexHash( ++ uint32_t value, int length, [[maybe_unused]] const HashSeed seed) { ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ value = SeedArrayIndexValue(value, seed); ++#endif ++ return MakeArrayIndexHash(value, static_cast(length)); ++} ++ ++uint32_t StringHasher::DecodeArrayIndexFromHashField( ++ uint32_t raw_hash_field, [[maybe_unused]] const HashSeed seed) { ++ uint32_t value = DecodeArrayIndexFromHashField(raw_hash_field); ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ value = UnseedArrayIndexValue(value, seed); ++#endif ++ return value; ++} ++ + template + uint32_t StringHasher::HashSequentialString(const char_t* chars_raw, int length, +- uint64_t seed) { ++ const HashSeed seed) { + static_assert(std::is_integral::value); + static_assert(sizeof(char_t) <= 2); + using uchar = typename std::make_unsigned::type; +@@ -63,7 +144,7 @@ + int i = 1; + do { + if (i == length) { +- return MakeArrayIndexHash(index, length); ++ return MakeArrayIndexHash(index, length, seed); + } + } while (TryAddArrayIndexChar(&index, chars[i++])); + } +@@ -80,7 +161,7 @@ + // Perform a regular hash computation, and additionally check + // if there are non-digit characters. + String::HashFieldType type = String::HashFieldType::kIntegerIndex; +- uint32_t running_hash = static_cast(seed); ++ uint32_t running_hash = static_cast(seed.seed()); + uint64_t index_big = 0; + const uchar* end = &chars[length]; + while (chars != end) { +@@ -112,7 +193,7 @@ + } + + // Non-index hash. +- uint32_t running_hash = static_cast(seed); ++ uint32_t running_hash = static_cast(seed.seed()); + const uchar* end = &chars[length]; + while (chars != end) { + running_hash = AddCharacterCore(running_hash, *chars++); +--- a/deps/v8/src/strings/string-hasher.h ++++ b/deps/v8/src/strings/string-hasher.h +@@ -6,6 +6,7 @@ + #define V8_STRINGS_STRING_HASHER_H_ + + #include "src/common/globals.h" ++#include "src/numbers/hash-seed.h" + + namespace v8 { + +@@ -23,12 +24,25 @@ + StringHasher() = delete; + template + static inline uint32_t HashSequentialString(const char_t* chars, int length, +- uint64_t seed); ++ const HashSeed seed); + +- // Calculated hash value for a string consisting of 1 to ++ // Calculate the hash value for a string consisting of 1 to + // String::kMaxArrayIndexSize digits with no leading zeros (except "0"). +- // value is represented decimal value. +- static uint32_t MakeArrayIndexHash(uint32_t value, int length); ++ // ++ // The entire hash field consists of (from least significant bit to most): ++ // - HashFieldType::kIntegerIndex ++ // - kArrayIndexValueBits::kSize bits containing the hash value ++ // - The length of the decimal string ++ // ++ // When V8_ENABLE_SEEDED_ARRAY_INDEX_HASH is enabled, the numeric value ++ // is scrambled using secrets derived from the hash seed. When it's disabled ++ // the public overloads ignore the seed, whose retrieval should be optimized ++ // away in common configurations. ++ static V8_INLINE uint32_t MakeArrayIndexHash(uint32_t value, int length, ++ const HashSeed seed); ++ // Decode array index value from raw hash field and reverse seeding, if any. ++ static V8_INLINE uint32_t ++ DecodeArrayIndexFromHashField(uint32_t raw_hash_field, const HashSeed seed); + + // No string is allowed to have a hash of zero. That value is reserved + // for internal properties. If the hash calculation yields zero then we +@@ -40,14 +54,48 @@ + V8_INLINE static uint32_t GetHashCore(uint32_t running_hash); + + static inline uint32_t GetTrivialHash(int length); ++ ++ private: ++ // Raw encode/decode without seeding. Use the public overloads above. ++ static V8_INLINE uint32_t MakeArrayIndexHash(uint32_t value, uint32_t length); ++ static V8_INLINE uint32_t ++ DecodeArrayIndexFromHashField(uint32_t raw_hash_field); ++ ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ // When V8_ENABLE_SEEDED_ARRAY_INDEX_HASH is enabled, the numeric value ++ // will be scrambled with 3 rounds of xorshift-multiply. ++ // ++ // x ^= x >> kShift; x = (x * m1) & kMask; // round 1 ++ // x ^= x >> kShift; x = (x * m2) & kMask; // round 2 ++ // x ^= x >> kShift; x = (x * m3) & kMask; // round 3 ++ // x ^= x >> kShift; // finalize ++ // ++ // To decode, apply the same steps with the modular inverses of m1, m2 ++ // and m3 in reverse order. ++ // ++ // x ^= x >> kShift; x = (x * m3_inv) & kMask; // round 1 ++ // x ^= x >> kShift; x = (x * m2_inv) & kMask; // round 2 ++ // x ^= x >> kShift; x = (x * m1_inv) & kMask; // round 3 ++ // x ^= x >> kShift; // finalize ++ // ++ // where kShift = kArrayIndexValueBits / 2, kMask = kArrayIndexValueMask, ++ // m1, m2, m3 (all odd) are derived from the Isolate's rapidhash secrets. ++ // m1_inv, m2_inv, m3_inv (modular inverses) are precomputed so that ++ // UnseedArrayIndexValue can quickly recover the original value. ++ static V8_INLINE uint32_t SeedArrayIndexValue(uint32_t value, ++ const HashSeed seed); ++ // Decode array index value from seeded raw hash field. ++ static V8_INLINE uint32_t UnseedArrayIndexValue(uint32_t value, ++ const HashSeed seed); ++#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH + }; + + // Useful for std containers that require something ()'able. + struct SeededStringHasher { +- explicit SeededStringHasher(uint64_t hashseed) : hashseed_(hashseed) {} ++ explicit SeededStringHasher(const HashSeed hashseed) : hashseed_(hashseed) {} + inline std::size_t operator()(const char* name) const; + +- uint64_t hashseed_; ++ const HashSeed hashseed_; + }; + + // Useful for std containers that require something ()'able. +--- a/deps/v8/src/torque/torque-parser.cc ++++ b/deps/v8/src/torque/torque-parser.cc +@@ -73,6 +73,11 @@ + #endif + build_flags_["V8_ENABLE_SANDBOX"] = V8_ENABLE_SANDBOX_BOOL; + build_flags_["DEBUG"] = DEBUG_BOOL; ++#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH ++ build_flags_["V8_ENABLE_SEEDED_ARRAY_INDEX_HASH"] = true; ++#else ++ build_flags_["V8_ENABLE_SEEDED_ARRAY_INDEX_HASH"] = false; ++#endif + } + static bool GetFlag(const std::string& name, const char* production) { + auto it = Get().build_flags_.find(name); +--- a/deps/v8/src/utils/utils.h ++++ b/deps/v8/src/utils/utils.h +@@ -231,8 +231,6 @@ + // ---------------------------------------------------------------------------- + // Hash function. + +-static const uint64_t kZeroHashSeed = 0; +- + // Thomas Wang, Integer Hash Functions. + // http://www.concentric.net/~Ttwang/tech/inthash.htm` + inline uint32_t ComputeUnseededHash(uint32_t key) { +--- a/deps/v8/src/wasm/wasm-module.cc ++++ b/deps/v8/src/wasm/wasm-module.cc +@@ -662,7 +662,7 @@ + size_t GetWireBytesHash(base::Vector wire_bytes) { + return StringHasher::HashSequentialString( + reinterpret_cast(wire_bytes.begin()), wire_bytes.length(), +- kZeroHashSeed); ++ HashSeed::Default()); + } + + int NumFeedbackSlots(const WasmModule* module, int func_index) { +--- /dev/null ++++ b/deps/v8/third_party/rapidhash-v8/LICENSE +@@ -0,0 +1,34 @@ ++/* ++ * rapidhash - Very fast, high quality, platform independant hashing algorithm. ++ * Copyright (C) 2024 Nicolas De Carli ++ * ++ * Based on 'wyhash', by Wang Yi ++ * ++ * BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php) ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ * ++ * You can contact the author at: ++ * - rapidhash source repository: https://github.com/Nicoshev/rapidhash ++ */ +--- /dev/null ++++ b/deps/v8/third_party/rapidhash-v8/OWNERS +@@ -0,0 +1,2 @@ ++leszeks@chromium.org ++mlippautz@chromium.org +--- /dev/null ++++ b/deps/v8/third_party/rapidhash-v8/README.chromium +@@ -0,0 +1,20 @@ ++Name: rapidhash ++URL: https://github.com/Nicoshev/rapidhash/blob/master/rapidhash.h ++Version: N/A ++Date: 2024-07-08 ++Revision: 588978411df8683777429f729be5213eb1bfd5f3 ++License: BSD 2-clause ++License File: LICENSE ++Shipped: Yes ++Security Critical: yes ++ ++Description: ++A fast hash function. ++ ++This is a fork of upstream, with the parts that we don't need or want ++removed. We do not intend to sync regularly with upstream git. This ++particular version is copied over from Chromium. ++ ++Local Modifications: ++- Copied over from Chromium's third_party/rapidhash with all its changes. ++- Removed base/ includes and replaced with V8 versions. +--- /dev/null ++++ b/deps/v8/third_party/rapidhash-v8/rapidhash.h +@@ -0,0 +1,285 @@ ++/* ++ * rapidhash - Very fast, high quality, platform-independent hashing algorithm. ++ * Copyright (C) 2024 Nicolas De Carli ++ * ++ * Based on 'wyhash', by Wang Yi ++ * ++ * BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php) ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ * ++ * You can contact the author at: ++ * - rapidhash source repository: https://github.com/Nicoshev/rapidhash ++ */ ++ ++#ifndef _THIRD_PARTY_RAPIDHASH_H ++#define _THIRD_PARTY_RAPIDHASH_H 1 ++ ++#include ++#include ++#include ++ ++#include ++#include ++ ++#include "include/v8config.h" ++#include "src/base/logging.h" ++ ++// Chromium has some local modifications to upstream rapidhash, ++// mostly around the concept of HashReaders (including slightly ++// more comments for ease of understanding). Generally, rapidhash ++// hashes bytes without really caring what these bytes are, ++// but often in Chromium, we want to hash _strings_, and strings ++// can have multiple representations. In particular, the WTF ++// StringImpl class (and by extension, String and AtomicString) ++// can have three different states: ++// ++// 1. Latin1 (or ASCII) code points, in 8 bits per character (LChar). ++// 2. The same code points, in 16 bits per character (UChar). ++// 3. Strings actually including non-Latin1 code points, in 16 bits ++// per character (UChar, UTF-16 encoding). ++// ++// The problem is that we'd like to hash case #1 and #2 to actually ++// return the same hash. There are several ways we could deal with this: ++// ++// a) Just ignore the problem and hash everything as raw bytes; ++// case #2 is fairly rare, and some algorithms (e.g. opportunistic ++// deduplication) could live with some false negatives. ++// b) Expand all strings to UTF-16 before hashing. This was the ++// strategy for the old StringHasher (using SuperFastHash), ++// as it naturally worked in 16-bit increments and it is probably ++// the simplest. However, this both halves the throughput of the ++// hasher and incurs conversion costs. ++// c) Detect #2 and convert those cases to #1 (UTF-16 to Latin1 ++// conversion), and apart from that, juts hash bytes. ++// ++// b) is used in e.g. CaseFoldingHash, but c) is the one we use the most ++// often. Most strings that we want to hash are 8-bit (e.g. HTML tags), so ++// that makes the common case fast. And for UChar strings, it is fairly fast ++// to make a pre-pass over the string to check for the existence of any ++// non-Latin1 characters. Of course, #1 and #3 can just be hashed as raw ++// bytes, as strings from those groups can never be equal anyway. ++// ++// To support these 8-to-16 and 16-to-8 conversions, and also things like ++// lowercasing on the fly, we have modified rapidhash to be templatized ++// on a HashReader, supplying bytes to the hash function. For the default ++// case of just hashing raw bytes, this is simply reading. But for other ++// conversions, the reader is doing slightly more work, such as packing ++// or unpacking. See e.g. ConvertTo8BitHashReader in string_hasher.h. ++// ++// Note that this reader could be made constexpr if we needed to evaluate ++// hashes at compile-time. ++struct PlainHashReader { ++ // If this is different from 1 (only 1, 2 and 4 are really reasonable ++ // options), it means the reader consumes its input at a faster rate than ++ // would be normally expected. For instance, if it is 2, it means that when ++ // the reader produces 64 bits, it needs to move its input 128 bits ++ // ahead. This is used when e.g. a reader converts from UTF-16 to ASCII, ++ // by removing zeros. The input length must divide the compression factor. ++ static constexpr unsigned kCompressionFactor = 1; ++ ++ // This is the opposite of kExpansionFactor. It does not make sense to have ++ // both kCompressionFactor and kExpansionFactor different from 1. ++ // The output length must divide the expansion factor. ++ static constexpr unsigned kExpansionFactor = 1; ++ ++ // Read 64 little-endian bits from the input, taking into account ++ // the expansion/compression if relevant. Unaligned reads must be ++ // supported. ++ static inline uint64_t Read64(const uint8_t* p) { ++ uint64_t v; ++ memcpy(&v, p, 8); ++ return v; ++ } ++ ++ // Similarly, read 32 little-endian (unaligned) bits. Note that ++ // this must return uint64_t, _not_ uint32_t, as the hasher assumes ++ // it can freely shift such numbers up and down. ++ static inline uint64_t Read32(const uint8_t* p) { ++ uint32_t v; ++ memcpy(&v, p, 4); ++ return v; ++ } ++ ++ // Read 1, 2 or 3 bytes from the input, and distribute them somewhat ++ // reasonably across the resulting 64-bit number. Note that this is ++ // done in a branch-free fashion, so that it always reads three bytes ++ // but never reads past the end. ++ // ++ // This is only used in the case where we hash a string of exactly ++ // 1, 2 or 3 bytes; if the hash is e.g. 7 bytes, two overlapping 32-bit ++ // reads will be done instead. Note that if you have kCompressionFactor == 2, ++ // this means that this will only ever be called with k = 2. ++ static inline uint64_t ReadSmall(const uint8_t* p, size_t k) { ++ return (uint64_t{p[0]} << 56) | (uint64_t{p[k >> 1]} << 32) | p[k - 1]; ++ } ++}; ++ ++/* ++ * Likely and unlikely macros. ++ */ ++#define _likely_(x) __builtin_expect(x, 1) ++#define _unlikely_(x) __builtin_expect(x, 0) ++ ++/* ++ * 64*64 -> 128bit multiply function. ++ * ++ * @param A Address of 64-bit number. ++ * @param B Address of 64-bit number. ++ * ++ * Calculates 128-bit C = *A * *B. ++ */ ++inline std::pair rapid_mul128(uint64_t A, uint64_t B) { ++#if defined(__SIZEOF_INT128__) ++ __uint128_t r = A; ++ r *= B; ++ return {static_cast(r), static_cast(r >> 64)}; ++#else ++ // High and low 32 bits of A and B. ++ uint64_t a_high = A >> 32, b_high = B >> 32, a_low = (uint32_t)A, ++ b_low = (uint32_t)B; ++ ++ // Intermediate products. ++ uint64_t result_high = a_high * b_high; ++ uint64_t result_m0 = a_high * b_low; ++ uint64_t result_m1 = b_high * a_low; ++ uint64_t result_low = a_low * b_low; ++ ++ // Final sum. The lower 64-bit addition can overflow twice, ++ // so accumulate carry as we go. ++ uint64_t high = result_high + (result_m0 >> 32) + (result_m1 >> 32); ++ uint64_t t = result_low + (result_m0 << 32); ++ high += (t < result_low); // Carry. ++ uint64_t low = t + (result_m1 << 32); ++ high += (low < t); // Carry. ++ ++ return {low, high}; ++#endif ++} ++ ++/* ++ * Multiply and xor mix function. ++ * ++ * @param A 64-bit number. ++ * @param B 64-bit number. ++ * ++ * Calculates 128-bit C = A * B. ++ * Returns 64-bit xor between high and low 64 bits of C. ++ */ ++inline uint64_t rapid_mix(uint64_t A, uint64_t B) { ++ std::tie(A, B) = rapid_mul128(A, B); ++ return A ^ B; ++} ++ ++/* ++ * rapidhash main function. ++ * ++ * @param key Buffer to be hashed. ++ * @param len Number of input bytes coming from the reader. ++ * @param seed 64-bit seed used to alter the hash result predictably. ++ * @param secret Triplet of 64-bit secrets used to alter hash result ++ * predictably. ++ * ++ * Returns a 64-bit hash. ++ * ++ * The data flow is separated so that we never mix input data with pointers; ++ * ++ * a, b, seed, secret[0], secret[1], secret[2], see1 and see2 are affected ++ * by the input data. ++ * ++ * p is only ever indexed by len, delta (comes from len only), i (comes from ++ * len only) or integral constants. len is const, so no data can flow into it. ++ * ++ * No other reads from memory take place. No writes to memory take place. ++ */ ++template ++V8_INLINE uint64_t rapidhash(const uint8_t* p, const size_t len, uint64_t seed, ++ const uint64_t secret[3]) { ++ // For brevity. ++ constexpr unsigned x = Reader::kCompressionFactor; ++ constexpr unsigned y = Reader::kExpansionFactor; ++ DCHECK_EQ(len % y, 0u); ++ ++ seed ^= rapid_mix(seed ^ secret[0], secret[1]) ^ len; ++ uint64_t a, b; ++ if (_likely_(len <= 16)) { ++ if (_likely_(len >= 4)) { ++ // Read the first and last 32 bits (they may overlap). ++ const uint8_t* plast = p + (len - 4) * x / y; ++ a = (Reader::Read32(p) << 32) | Reader::Read32(plast); ++ ++ // This is equivalent to: delta = (len >= 8) ? 4 : 0; ++ const uint64_t delta = ((len & 24) >> (len >> 3)) * x / y; ++ b = ((Reader::Read32(p + delta) << 32) | Reader::Read32(plast - delta)); ++ } else if (_likely_(len > 0)) { ++ // 1, 2 or 3 bytes. ++ a = Reader::ReadSmall(p, len); ++ b = 0; ++ } else { ++ a = b = 0; ++ } ++ } else { ++ size_t i = len; ++ if (_unlikely_(i > 48)) { ++ uint64_t see1 = seed, see2 = seed; ++ do { ++ seed = rapid_mix(Reader::Read64(p) ^ secret[0], ++ Reader::Read64(p + 8 * x / y) ^ seed); ++ see1 = rapid_mix(Reader::Read64(p + 16 * x / y) ^ secret[1], ++ Reader::Read64(p + 24 * x / y) ^ see1); ++ see2 = rapid_mix(Reader::Read64(p + 32 * x / y) ^ secret[2], ++ Reader::Read64(p + 40 * x / y) ^ see2); ++ p += 48 * x / y; ++ i -= 48; ++ } while (_likely_(i >= 48)); ++ seed ^= see1 ^ see2; ++ } ++ if (i > 16) { ++ seed = rapid_mix(Reader::Read64(p) ^ secret[2], ++ Reader::Read64(p + 8 * x / y) ^ seed ^ secret[1]); ++ if (i > 32) { ++ seed = rapid_mix(Reader::Read64(p + 16 * x / y) ^ secret[2], ++ Reader::Read64(p + 24 * x / y) ^ seed); ++ } ++ } ++ ++ // Read the last 2x64 bits. Note that due to the division by y, ++ // this must be a signed quantity (or the division would become ++ // unsigned, possibly moving the sign bit down into the address). ++ // Similarly for x. ++ a = Reader::Read64(p + (static_cast(i) - 16) * ++ static_cast(x) / static_cast(y)); ++ b = Reader::Read64(p + (static_cast(i) - 8) * ++ static_cast(x) / static_cast(y)); ++ } ++ a ^= secret[1]; ++ b ^= seed; ++ std::tie(a, b) = rapid_mul128(a, b); ++ return rapid_mix(a ^ secret[0] ^ len, b ^ secret[1]); ++} ++ ++#undef _likely_ ++#undef _unlikely_ ++ ++#endif // _THIRD_PARTY_RAPIDHASH_H +--- /dev/null ++++ b/deps/v8/third_party/rapidhash-v8/secret.h +@@ -0,0 +1,198 @@ ++/* ++ * This is free and unencumbered software released into the public domain. ++ * ++ * Anyone is free to copy, modify, publish, use, compile, sell, or ++ * distribute this software, either in source code form or as a compiled ++ * binary, for any purpose, commercial or non-commercial, and by any ++ * means. ++ * ++ * In jurisdictions that recognize copyright laws, the author or authors ++ * of this software dedicate any and all copyright interest in the ++ * software to the public domain. We make this dedication for the benefit ++ * of the public at large and to the detriment of our heirs and ++ * successors. We intend this dedication to be an overt act of ++ * relinquishment in perpetuity of all present and future rights to this ++ * software under copyright law. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ++ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR ++ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ++ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR ++ * OTHER DEALINGS IN THE SOFTWARE. ++ * ++ * For more information, please refer to ++ * ++ * author: 王一 Wang Yi ++ * contributors: Reini Urban, Dietrich Epp, Joshua Haberman, Tommy Ettinger, ++ * Daniel Lemire, Otmar Ertl, cocowalla, leo-yuriev, Diego Barrios Romero, ++ * paulie-g, dumblob, Yann Collet, ivte-ms, hyb, James Z.M. Gao, easyaspi314 ++ * (Devin), TheOneric ++ */ ++ ++#ifndef _THIRD_PARTY_RAPIDHASH_SECRET_H ++#define _THIRD_PARTY_RAPIDHASH_SECRET_H 1 ++ ++#include "rapidhash.h" ++#include "src/base/bits.h" ++ ++// Default secret parameters. If we wanted to, we could generate our own ++// versions of these at renderer startup in order to perturb the hash ++// and make it more DoS-resistant (similar to what base/hash.h does), ++// but generating new ones takes a little bit of time (~200 µs on a desktop ++// machine as of 2024), and good-quality random numbers may not be copious ++// from within the sandbox. The secret concept is inherited from wyhash, ++// described by the wyhash author here: ++// ++// https://github.com/wangyi-fudan/wyhash/issues/139 ++// ++// The rules are: ++// ++// 1. Each byte must be “balanced”, i.e., have exactly 4 bits set. ++// (This is trivially done by just precompting a LUT with the ++// possible bytes and picking from those.) ++// ++// 2. Each 64-bit group should have a Hamming distance of 32 to ++// all the others (i.e., popcount(secret[i] ^ secret[j]) == 32). ++// This is just done by rejection sampling. ++// ++// 3. Each 64-bit group should be prime. It's not obvious that this ++// is really needed for the hash, as opposed to wyrand which also ++// uses the same secret, but according to the author, it is ++// “a feeling to be perfect”. This naturally means the last byte ++// cannot be divisible by 2, but apart from that, is easiest ++// checked by testing a few small factors and then the Miller-Rabin ++// test, which rejects nearly all bad candidates in the first iteration ++// and is fast as long as we have 64x64 -> 128 bit muls and modulos. ++ ++static constexpr uint64_t RAPIDHASH_DEFAULT_SECRET[3] = { ++ 0x2d358dccaa6c78a5ull, 0x8bb84b93962eacc9ull, 0x4b33a62ed433d4a3ull}; ++ ++#if !V8_USE_DEFAULT_HASHER_SECRET ++ ++namespace detail { ++ ++static inline uint64_t wyrand(uint64_t* seed) { ++ *seed += 0x2d358dccaa6c78a5ull; ++ return rapid_mix(*seed, *seed ^ 0x8bb84b93962eacc9ull); ++} ++ ++static inline unsigned long long mul_mod(unsigned long long a, ++ unsigned long long b, ++ unsigned long long m) { ++ unsigned long long r = 0; ++ while (b) { ++ if (b & 1) { ++ unsigned long long r2 = r + a; ++ if (r2 < r) r2 -= m; ++ r = r2 % m; ++ } ++ b >>= 1; ++ if (b) { ++ unsigned long long a2 = a + a; ++ if (a2 < a) a2 -= m; ++ a = a2 % m; ++ } ++ } ++ return r; ++} ++ ++static inline unsigned long long pow_mod(unsigned long long a, ++ unsigned long long b, ++ unsigned long long m) { ++ unsigned long long r = 1; ++ while (b) { ++ if (b & 1) r = mul_mod(r, a, m); ++ b >>= 1; ++ if (b) a = mul_mod(a, a, m); ++ } ++ return r; ++} ++ ++static unsigned sprp(unsigned long long n, unsigned long long a) { ++ unsigned long long d = n - 1; ++ unsigned char s = 0; ++ while (!(d & 0xff)) { ++ d >>= 8; ++ s += 8; ++ } ++ if (!(d & 0xf)) { ++ d >>= 4; ++ s += 4; ++ } ++ if (!(d & 0x3)) { ++ d >>= 2; ++ s += 2; ++ } ++ if (!(d & 0x1)) { ++ d >>= 1; ++ s += 1; ++ } ++ unsigned long long b = pow_mod(a, d, n); ++ if ((b == 1) || (b == (n - 1))) return 1; ++ unsigned char r; ++ for (r = 1; r < s; r++) { ++ b = mul_mod(b, b, n); ++ if (b <= 1) return 0; ++ if (b == (n - 1)) return 1; ++ } ++ return 0; ++} ++ ++static unsigned is_prime(unsigned long long n) { ++ if (n < 2 || !(n & 1)) return 0; ++ if (n < 4) return 1; ++ if (!sprp(n, 2)) return 0; ++ if (n < 2047) return 1; ++ if (!sprp(n, 3)) return 0; ++ if (!sprp(n, 5)) return 0; ++ if (!sprp(n, 7)) return 0; ++ if (!sprp(n, 11)) return 0; ++ if (!sprp(n, 13)) return 0; ++ if (!sprp(n, 17)) return 0; ++ if (!sprp(n, 19)) return 0; ++ if (!sprp(n, 23)) return 0; ++ if (!sprp(n, 29)) return 0; ++ if (!sprp(n, 31)) return 0; ++ if (!sprp(n, 37)) return 0; ++ return 1; ++} ++ ++} // namespace detail ++ ++static inline void rapidhash_make_secret(uint64_t seed, uint64_t* secret) { ++ uint8_t c[] = {15, 23, 27, 29, 30, 39, 43, 45, 46, 51, 53, 54, ++ 57, 58, 60, 71, 75, 77, 78, 83, 85, 86, 89, 90, ++ 92, 99, 101, 102, 105, 106, 108, 113, 114, 116, 120, 135, ++ 139, 141, 142, 147, 149, 150, 153, 154, 156, 163, 165, 166, ++ 169, 170, 172, 177, 178, 180, 184, 195, 197, 198, 201, 202, ++ 204, 209, 210, 212, 216, 225, 226, 228, 232, 240}; ++ for (size_t i = 0; i < 3; i++) { ++ uint8_t ok; ++ do { ++ ok = 1; ++ secret[i] = 0; ++ for (size_t j = 0; j < 64; j += 8) ++ secret[i] |= static_cast(c[detail::wyrand(&seed) % sizeof(c)]) ++ << j; ++ if (secret[i] % 2 == 0) { ++ ok = 0; ++ continue; ++ } ++ for (size_t j = 0; j < i; j++) { ++ if (v8::base::bits::CountPopulation(secret[j] ^ secret[i]) != 32) { ++ ok = 0; ++ break; ++ } ++ } ++ if (ok && !detail::is_prime(secret[i])) { ++ ok = 0; ++ } ++ } while (!ok); ++ } ++} ++ ++#endif // !V8_USE_DEFAULT_HASHER_SECRET ++ ++#endif // _THIRD_PARTY_RAPIDHASH_SECRET_H +--- /dev/null ++++ b/test/fixtures/array-hash-collision.js +@@ -0,0 +1,27 @@ ++'use strict'; ++ ++// See https://hackerone.com/reports/3511792 ++ ++const payload = []; ++const val = 1234; ++const MOD = 2 ** 19; ++const CHN = 2 ** 17; ++const REP = 2 ** 17; ++ ++if (process.argv[2] === 'benign') { ++ for (let i = 0; i < CHN + REP; i++) { ++ payload.push(`${val + i}`); ++ } ++} else { ++ let j = val + MOD; ++ for (let i = 1; i < CHN; i++) { ++ payload.push(`${j}`); ++ j = (j + i) % MOD; ++ } ++ for (let k = 0; k < REP; k++) { ++ payload.push(`${val}`); ++ } ++} ++ ++const string = JSON.stringify({ data: payload }); ++JSON.parse(string); +--- /dev/null ++++ b/test/pummel/test-array-hash-collision.js +@@ -0,0 +1,27 @@ ++'use strict'; ++ ++// This is a regression test for https://hackerone.com/reports/3511792 ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++const { performance } = require('perf_hooks'); ++const fixtures = require('../common/fixtures'); ++ ++const fixturePath = fixtures.path('array-hash-collision.js'); ++ ++const t0 = performance.now(); ++const benignResult = spawnSync(process.execPath, [fixturePath, 'benign']); ++const benignTime = performance.now() - t0; ++assert.strictEqual(benignResult.status, 0); ++console.log(`Benign test completed in ${benignTime.toFixed(2)}ms.`); ++ ++const t1 = performance.now(); ++const maliciousResult = spawnSync(process.execPath, [fixturePath, 'malicious'], { ++ timeout: Math.ceil(benignTime * 10), ++}); ++const maliciousTime = performance.now() - t1; ++console.log(`Malicious test completed in ${maliciousTime.toFixed(2)}ms.`); ++ ++assert.strictEqual(maliciousResult.status, 0, 'Hash flooding regression detected: ' + ++ `Benign took ${benignTime}ms, malicious took more than ${maliciousTime}ms.`); +--- a/tools/make-v8.sh ++++ b/tools/make-v8.sh +@@ -5,6 +5,8 @@ + BUILD_ARCH_TYPE=$1 + V8_BUILD_OPTIONS=$2 + ++EXTRA_V8_OPTS="v8_enable_static_roots=false v8_enable_seeded_array_index_hash=true v8_use_default_hasher_secret=false" ++ + cd deps/v8 || exit + find . -type d -name .git -print0 | xargs -0 rm -rf + ../../tools/v8/fetch_deps.py . +--- a/tools/v8_gypfiles/features.gypi ++++ b/tools/v8_gypfiles/features.gypi +@@ -194,6 +194,9 @@ + # Use Siphash as added protection against hash flooding attacks. + 'v8_use_siphash%': 0, + ++ # Enable seeded array index hash. ++ 'v8_enable_seeded_array_index_hash%': 1, ++ + # Use Perfetto (https://perfetto.dev) as the default TracingController. Not + # currently implemented. + 'v8_use_perfetto%': 0, +@@ -436,6 +439,9 @@ + ['v8_use_siphash==1', { + 'defines': ['V8_USE_SIPHASH',], + }], ++ ['v8_enable_seeded_array_index_hash==1', { ++ 'defines': ['V8_ENABLE_SEEDED_ARRAY_INDEX_HASH',], ++ }], + ['v8_enable_shared_ro_heap==1', { + 'defines': ['V8_SHARED_RO_HEAP',], + }], diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch nodejs-20.19.2+dfsg/debian/patches/sec/52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch 2026-03-24 20:59:24.000000000 +0000 @@ -0,0 +1,147 @@ +From 00ad47a28eb2e3dc0ff5610d58c53341acf3cf8d Mon Sep 17 00:00:00 2001 +From: Matteo Collina +Date: Thu, 19 Feb 2026 15:49:43 +0100 +Subject: [PATCH] http: use null prototype for headersDistinct/trailersDistinct + +Use { __proto__: null } instead of {} when initializing the +headersDistinct and trailersDistinct destination objects. + +A plain {} inherits from Object.prototype, so when a __proto__ +header is received, dest["__proto__"] resolves to Object.prototype +(truthy), causing _addHeaderLineDistinct to call .push() on it, +which throws an uncaught TypeError and crashes the process. + +Ref: https://hackerone.com/reports/3560402 +PR-URL: https://github.com/nodejs-private/node-private/pull/821 +Refs: https://hackerone.com/reports/3560402 +Reviewed-By: Marco Ippolito +Reviewed-By: Rafael Gonzaga +CVE-ID: CVE-2026-21710 +--- + lib/_http_incoming.js | 4 +-- + .../test-http-headers-distinct-proto.js | 36 +++++++++++++++++++ + test/parallel/test-http-multiple-headers.js | 16 ++++----- + 3 files changed, 46 insertions(+), 10 deletions(-) + create mode 100644 test/parallel/test-http-headers-distinct-proto.js + +diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js +index 1dd04fdf3e4228..994a24f028c343 100644 +--- a/lib/_http_incoming.js ++++ b/lib/_http_incoming.js +@@ -128,7 +128,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', { + __proto__: null, + get: function() { + if (!this[kHeadersDistinct]) { +- this[kHeadersDistinct] = {}; ++ this[kHeadersDistinct] = { __proto__: null }; + + const src = this.rawHeaders; + const dst = this[kHeadersDistinct]; +@@ -168,7 +168,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', { + __proto__: null, + get: function() { + if (!this[kTrailersDistinct]) { +- this[kTrailersDistinct] = {}; ++ this[kTrailersDistinct] = { __proto__: null }; + + const src = this.rawTrailers; + const dst = this[kTrailersDistinct]; +diff --git a/test/parallel/test-http-headers-distinct-proto.js b/test/parallel/test-http-headers-distinct-proto.js +new file mode 100644 +index 00000000000000..bd4cb82bd6e6b3 +--- /dev/null ++++ b/test/parallel/test-http-headers-distinct-proto.js +@@ -0,0 +1,36 @@ ++'use strict'; ++ ++const common = require('../common'); ++const assert = require('assert'); ++const http = require('http'); ++const net = require('net'); ++ ++// Regression test: sending a __proto__ header must not crash the server ++// when accessing req.headersDistinct or req.trailersDistinct. ++ ++const server = http.createServer(common.mustCall((req, res) => { ++ const headers = req.headersDistinct; ++ assert.strictEqual(Object.getPrototypeOf(headers), null); ++ assert.deepStrictEqual(Object.getOwnPropertyDescriptor(headers, '__proto__').value, ['test']); ++ res.end(); ++})); ++ ++server.listen(0, common.mustCall(() => { ++ const port = server.address().port; ++ ++ const client = net.connect(port, common.mustCall(() => { ++ client.write( ++ 'GET / HTTP/1.1\r\n' + ++ 'Host: localhost\r\n' + ++ '__proto__: test\r\n' + ++ 'Connection: close\r\n' + ++ '\r\n', ++ ); ++ })); ++ ++ client.on('end', common.mustCall(() => { ++ server.close(); ++ })); ++ ++ client.resume(); ++})); +diff --git a/test/parallel/test-http-multiple-headers.js b/test/parallel/test-http-multiple-headers.js +index f9c654ba2f8730..bc49ba1e43dc4e 100644 +--- a/test/parallel/test-http-multiple-headers.js ++++ b/test/parallel/test-http-multiple-headers.js +@@ -26,13 +26,13 @@ const server = createServer( + host, + 'transfer-encoding': 'chunked' + }); +- assert.deepStrictEqual(req.headersDistinct, { ++ assert.deepStrictEqual(req.headersDistinct, Object.assign({ __proto__: null }, { + 'connection': ['close'], + 'x-req-a': ['eee', 'fff', 'ggg', 'hhh'], + 'x-req-b': ['iii; jjj; kkk; lll'], + 'host': [host], +- 'transfer-encoding': ['chunked'] +- }); ++ 'transfer-encoding': ['chunked'], ++ })); + + req.on('end', function() { + assert.deepStrictEqual(req.rawTrailers, [ +@@ -45,7 +45,7 @@ const server = createServer( + ); + assert.deepStrictEqual( + req.trailersDistinct, +- { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] } ++ Object.assign({ __proto__: null }, { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] }) + ); + + res.setHeader('X-Res-a', 'AAA'); +@@ -132,14 +132,14 @@ server.listen(0, common.mustCall(() => { + 'x-res-d': 'JJJ; KKK; LLL', + 'transfer-encoding': 'chunked' + }); +- assert.deepStrictEqual(res.headersDistinct, { ++ assert.deepStrictEqual(res.headersDistinct, Object.assign({ __proto__: null }, { + 'x-res-a': [ 'AAA', 'BBB', 'CCC' ], + 'x-res-b': [ 'DDD; EEE; FFF; GGG' ], + 'connection': [ 'close' ], + 'x-res-c': [ 'HHH', 'III' ], + 'x-res-d': [ 'JJJ; KKK; LLL' ], +- 'transfer-encoding': [ 'chunked' ] +- }); ++ 'transfer-encoding': [ 'chunked' ], ++ })); + + res.on('end', function() { + assert.deepStrictEqual(res.rawTrailers, [ +@@ -153,7 +153,7 @@ server.listen(0, common.mustCall(() => { + ); + assert.deepStrictEqual( + res.trailersDistinct, +- { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] } ++ Object.assign({ __proto__: null }, { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] }) + ); + server.close(); + }); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/53-include-permission-check-on-lib-fs-promises.patch nodejs-20.19.2+dfsg/debian/patches/sec/53-include-permission-check-on-lib-fs-promises.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/53-include-permission-check-on-lib-fs-promises.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/53-include-permission-check-on-lib-fs-promises.patch 2026-03-24 21:01:08.000000000 +0000 @@ -0,0 +1,605 @@ +From 012330956669e06864a674917de352d2d69ff51c Mon Sep 17 00:00:00 2001 +From: RafaelGSS +Date: Mon, 5 Jan 2026 20:36:07 -0300 +Subject: [PATCH] permission: include permission check on lib/fs/promises + +PR-URL: https://github.com/nodejs-private/node-private/pull/840 +CVE-ID: CVE-2026-21716 +--- + lib/internal/fs/promises.js | 13 ++ + src/node_file-inl.h | 33 +++-- + src/node_file.cc | 2 - + test/fixtures/permission/fs-read.js | 200 +++++++++++++++++++++++++ + test/fixtures/permission/fs-write.js | 211 ++++++++++++++++++++++++++- + 5 files changed, 438 insertions(+), 21 deletions(-) + +--- a/lib/internal/fs/promises.js ++++ b/lib/internal/fs/promises.js +@@ -17,6 +17,7 @@ + Symbol, + Uint8Array, + FunctionPrototypeBind, ++ uncurryThis, + } = primordials; + + const { fs: constants } = internalBinding('constants'); +@@ -30,6 +31,8 @@ + + const binding = internalBinding('fs'); + const { Buffer } = require('buffer'); ++const { isBuffer: BufferIsBuffer } = Buffer; ++const BufferToString = uncurryThis(Buffer.prototype.toString); + + const { + codes: { +@@ -1012,6 +1015,10 @@ + + async function lstat(path, options = { bigint: false }) { + path = getValidatedPath(path); ++ if (permission.isEnabled() && !permission.has('fs.read', path)) { ++ const resource = pathModule.toNamespacedPath(BufferIsBuffer(path) ? BufferToString(path) : path); ++ throw new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource); ++ } + const result = await PromisePrototypeThen( + binding.lstat(pathModule.toNamespacedPath(path), + options.bigint, kUsePromises), +@@ -1065,6 +1072,9 @@ + } + + async function fchmod(handle, mode) { ++ if (permission.isEnabled()) { ++ throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.'); ++ } + mode = parseFileMode(mode, 'mode'); + return await PromisePrototypeThen( + binding.fchmod(handle.fd, mode, kUsePromises), +@@ -1105,6 +1115,9 @@ + async function fchown(handle, uid, gid) { + validateInteger(uid, 'uid', -1, kMaxUserId); + validateInteger(gid, 'gid', -1, kMaxUserId); ++ if (permission.isEnabled()) { ++ throw new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.'); ++ } + return await PromisePrototypeThen( + binding.fchown(handle.fd, uid, gid, kUsePromises), + undefined, +--- a/src/node_file-inl.h ++++ b/src/node_file-inl.h +@@ -287,21 +287,27 @@ + int index, + bool use_bigint) { + v8::Local value = args[index]; ++ FSReqBase* result = nullptr; + if (value->IsObject()) { +- return Unwrap(value.As()); +- } +- +- Realm* realm = Realm::GetCurrent(args); +- BindingData* binding_data = realm->GetBindingData(); ++ result = Unwrap(value.As()); ++ } else { ++ Realm* realm = Realm::GetCurrent(args); ++ BindingData* binding_data = realm->GetBindingData(); + +- if (value->StrictEquals(realm->isolate_data()->fs_use_promises_symbol())) { +- if (use_bigint) { +- return FSReqPromise::New(binding_data, use_bigint); +- } else { +- return FSReqPromise::New(binding_data, use_bigint); ++ if (value->StrictEquals(realm->isolate_data()->fs_use_promises_symbol())) { ++ if (use_bigint) { ++ result = ++ FSReqPromise::New(binding_data, use_bigint); ++ } else { ++ result = ++ FSReqPromise::New(binding_data, use_bigint); ++ } + } + } +- return nullptr; ++ if (result != nullptr) { ++ result->SetReturnValue(args); ++ } ++ return result; + } + + // Returns nullptr if the operation fails from the start. +@@ -320,10 +326,7 @@ + uv_req->path = nullptr; + after(uv_req); // after may delete req_wrap if there is an error + req_wrap = nullptr; +- } else { +- req_wrap->SetReturnValue(args); + } +- + return req_wrap; + } + +--- a/src/node_file.cc ++++ b/src/node_file.cc +@@ -2418,8 +2418,6 @@ + uv_req->path = nullptr; + AfterInteger(uv_req); // after may delete req_wrap_async if there is + // an error +- } else { +- req_wrap_async->SetReturnValue(args); + } + } else { // write(fd, string, pos, enc, undefined, ctx) + CHECK_EQ(argc, 6); +--- a/test/fixtures/permission/fs-read.js ++++ b/test/fixtures/permission/fs-read.js +@@ -4,6 +4,8 @@ + + const assert = require('assert'); + const fs = require('fs'); ++const fsPromises = require('node:fs/promises'); ++ + const path = require('path'); + + const blockedFile = process.env.BLOCKEDFILE; +@@ -446,6 +448,204 @@ + })); + } + ++// fsPromises.readFile ++{ ++ assert.rejects(async () => { ++ await fsPromises.readFile(blockedFile); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.readFile(blockedFileURL); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.stat ++{ ++ assert.rejects(async () => { ++ await fsPromises.stat(blockedFile); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.stat(blockedFileURL); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.stat(path.join(blockedFolder, 'anyfile')); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.access ++{ ++ assert.rejects(async () => { ++ await fsPromises.access(blockedFile, fs.constants.R_OK); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.access(blockedFileURL, fs.constants.R_OK); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.access(path.join(blockedFolder, 'anyfile'), fs.constants.R_OK); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.copyFile ++{ ++ assert.rejects(async () => { ++ await fsPromises.copyFile(blockedFile, path.join(blockedFolder, 'any-other-file')); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.copyFile(blockedFileURL, path.join(blockedFolder, 'any-other-file')); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.cp ++{ ++ assert.rejects(async () => { ++ await fsPromises.cp(blockedFile, path.join(blockedFolder, 'any-other-file')); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.cp(blockedFileURL, path.join(blockedFolder, 'any-other-file')); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.open ++{ ++ assert.rejects(async () => { ++ await fsPromises.open(blockedFile, 'r'); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.open(blockedFileURL, 'r'); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.open(path.join(blockedFolder, 'anyfile'), 'r'); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.opendir ++{ ++ assert.rejects(async () => { ++ await fsPromises.opendir(blockedFolder); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFolder), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.readdir ++{ ++ assert.rejects(async () => { ++ await fsPromises.readdir(blockedFolder); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFolder), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.readdir(blockedFolder, { recursive: true }); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFolder), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.rename ++{ ++ assert.rejects(async () => { ++ await fsPromises.rename(blockedFile, 'newfile'); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.rename(blockedFileURL, 'newfile'); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })).then(common.mustCall()); ++} ++ ++// fsPromises.lstat ++{ ++ assert.rejects(async () => { ++ await fsPromises.lstat(blockedFile); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.lstat(blockedFileURL); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ })).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.lstat(path.join(blockedFolder, 'anyfile')); ++ }, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ })).then(common.mustCall()); ++} ++ + // fs.lstat + { + assert.throws(() => { +--- a/test/fixtures/permission/fs-write.js ++++ b/test/fixtures/permission/fs-write.js +@@ -5,6 +5,7 @@ + + const assert = require('assert'); + const fs = require('fs'); ++const fsPromises = require('node:fs/promises'); + const path = require('path'); + + const regularFolder = process.env.ALLOWEDFOLDER; +@@ -197,6 +198,13 @@ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemWrite', + })); ++ ++ assert.rejects(async () => { ++ await fsPromises.mkdtemp(path.join(blockedFolder, 'any-folder')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ }); + } + + // fs.rename +@@ -330,7 +338,7 @@ + permission: 'FileSystemWrite', + })); + assert.rejects(async () => { +- await fs.promises.open(blockedFile, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW); ++ await fsPromises.open(blockedFile, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW); + }, { + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemWrite', +@@ -369,7 +377,7 @@ + permission: 'FileSystemWrite', + }); + assert.rejects(async () => { +- await fs.promises.chmod(blockedFile, 0o755); ++ await fsPromises.chmod(blockedFile, 0o755); + }, { + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemWrite', +@@ -384,7 +392,7 @@ + permission: 'FileSystemWrite', + })); + assert.rejects(async () => { +- await fs.promises.lchmod(blockedFile, 0o755); ++ await fsPromises.lchmod(blockedFile, 0o755); + }, { + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemWrite', +@@ -409,7 +417,7 @@ + permission: 'FileSystemWrite', + }); + assert.rejects(async () => { +- await fs.promises.appendFile(blockedFile, 'new data'); ++ await fsPromises.appendFile(blockedFile, 'new data'); + }, { + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemWrite', +@@ -598,4 +606,199 @@ + }, { + code: 'ERR_ACCESS_DENIED', + }); ++} ++ ++// fsPromises.writeFile ++{ ++ assert.rejects(async () => { ++ await fsPromises.writeFile(blockedFile, 'example'); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.writeFile(blockedFileURL, 'example'); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.writeFile(path.join(blockedFolder, 'anyfile'), 'example'); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')), ++ }).then(common.mustCall()); ++} ++ ++// fsPromises.utimes ++{ ++ assert.rejects(async () => { ++ await fsPromises.utimes(blockedFile, new Date(), new Date()); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.utimes(blockedFileURL, new Date(), new Date()); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.utimes(path.join(blockedFolder, 'anyfile'), new Date(), new Date()); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')), ++ }).then(common.mustCall()); ++} ++ ++// fsPromises.lutimes ++{ ++ assert.rejects(async () => { ++ await fsPromises.lutimes(blockedFile, new Date(), new Date()); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.lutimes(blockedFileURL, new Date(), new Date()); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++} ++ ++// fsPromises.mkdir ++{ ++ assert.rejects(async () => { ++ await fsPromises.mkdir(path.join(blockedFolder, 'any-folder')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'any-folder')), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.mkdir(path.join(relativeProtectedFolder, 'any-folder')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(relativeProtectedFolder, 'any-folder')), ++ }).then(common.mustCall()); ++} ++ ++// fsPromises.rename ++{ ++ assert.rejects(async () => { ++ await fsPromises.rename(blockedFile, path.join(blockedFile, 'renamed')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.rename(blockedFileURL, path.join(blockedFile, 'renamed')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.rename(regularFile, path.join(blockedFolder, 'renamed')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'renamed')), ++ }).then(common.mustCall()); ++} ++ ++// fsPromises.copyFile ++{ ++ assert.rejects(async () => { ++ await fsPromises.copyFile(regularFile, path.join(blockedFolder, 'any-file')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'any-file')), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.copyFile(regularFile, path.join(relativeProtectedFolder, 'any-file')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(relativeProtectedFolder, 'any-file')), ++ }).then(common.mustCall()); ++} ++ ++// fsPromises.cp ++{ ++ assert.rejects(async () => { ++ await fsPromises.cp(regularFile, path.join(blockedFolder, 'any-file')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(blockedFolder, 'any-file')), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.cp(regularFile, path.join(relativeProtectedFolder, 'any-file')); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(path.join(relativeProtectedFolder, 'any-file')), ++ }).then(common.mustCall()); ++} ++ ++// fsPromises.unlink ++{ ++ assert.rejects(async () => { ++ await fsPromises.unlink(blockedFile); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++ assert.rejects(async () => { ++ await fsPromises.unlink(blockedFileURL); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemWrite', ++ resource: path.toNamespacedPath(blockedFile), ++ }).then(common.mustCall()); ++} ++ ++// FileHandle.chmod (fchmod) with read-only fd ++{ ++ assert.rejects(async () => { ++ // blocked file is allowed to read ++ const fh = await fsPromises.open(blockedFile, 'r'); ++ try { ++ await fh.chmod(0o777); ++ } finally { ++ await fh.close(); ++ } ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ }).then(common.mustCall()); ++} ++ ++// FileHandle.chown (fchown) with read-only fd ++{ ++ assert.rejects(async () => { ++ // blocked file is allowed to read ++ const fh = await fsPromises.open(blockedFile, 'r'); ++ try { ++ await fh.chown(999, 999); ++ } finally { ++ await fh.close(); ++ } ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ }).then(common.mustCall()); + } +\ No newline at end of file diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/54-add-permission-check-to-realpath-native.patch nodejs-20.19.2+dfsg/debian/patches/sec/54-add-permission-check-to-realpath-native.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/54-add-permission-check-to-realpath-native.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/54-add-permission-check-to-realpath-native.patch 2026-03-24 21:03:08.000000000 +0000 @@ -0,0 +1,57 @@ +From 00830712bc623ba04b08856462a56b79e29f5cc3 Mon Sep 17 00:00:00 2001 +From: RafaelGSS +Date: Mon, 5 Jan 2026 18:18:39 -0300 +Subject: [PATCH] permission: add permission check to realpath.native + +Signed-off-by: RafaelGSS +PR-URL: https://github.com/nodejs-private/node-private/pull/838 +CVE-ID: CVE-2026-21715 +--- + src/node_file.cc | 8 ++++++++ + test/fixtures/permission/fs-read.js | 14 ++++++++++++++ + 2 files changed, 22 insertions(+) + +--- a/src/node_file.cc ++++ b/src/node_file.cc +@@ -1914,11 +1914,19 @@ + + if (argc > 2) { // realpath(path, encoding, req) + FSReqBase* req_wrap_async = GetReqWrap(args, 2); ++ CHECK_NOT_NULL(req_wrap_async); ++ ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS( ++ env, ++ req_wrap_async, ++ permission::PermissionScope::kFileSystemRead, ++ path.ToStringView()); + FS_ASYNC_TRACE_BEGIN1( + UV_FS_REALPATH, req_wrap_async, "path", TRACE_STR_COPY(*path)) + AsyncCall(env, req_wrap_async, args, "realpath", encoding, AfterStringPtr, + uv_fs_realpath, *path); + } else { // realpath(path, encoding, undefined, ctx) ++ THROW_IF_INSUFFICIENT_PERMISSIONS( ++ env, permission::PermissionScope::kFileSystemRead, path.ToStringView()); + FSReqWrapSync req_wrap_sync("realpath", *path); + FS_SYNC_TRACE_BEGIN(realpath); + int err = +--- a/test/fixtures/permission/fs-read.js ++++ b/test/fixtures/permission/fs-read.js +@@ -673,4 +673,18 @@ + fs.lstat(regularFile, (err) => { + assert.ifError(err); + }); ++} ++ ++// fs.realpath.native ++{ ++ fs.realpath.native(blockedFile, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ permission: 'FileSystemRead', ++ resource: path.toNamespacedPath(blockedFile), ++ })); ++ ++ // doesNotThrow ++ fs.realpath.native(regularFile, (err) => { ++ assert.ifError(err); ++ }); + } +\ No newline at end of file diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/55-handle-NGHTTP2_ERR_FLOW_CONTROL-error-code.patch nodejs-20.19.2+dfsg/debian/patches/sec/55-handle-NGHTTP2_ERR_FLOW_CONTROL-error-code.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/55-handle-NGHTTP2_ERR_FLOW_CONTROL-error-code.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/55-handle-NGHTTP2_ERR_FLOW_CONTROL-error-code.patch 2026-03-24 21:04:37.000000000 +0000 @@ -0,0 +1,118 @@ +From a0c73425da4c95fbcf6c13b7fe8921301290b8e6 Mon Sep 17 00:00:00 2001 +From: RafaelGSS +Date: Wed, 11 Mar 2026 11:22:23 -0300 +Subject: [PATCH] src: handle NGHTTP2_ERR_FLOW_CONTROL error code + +Refs: https://hackerone.com/reports/3531737 +PR-URL: https://github.com/nodejs-private/node-private/pull/832 +CVE-ID: CVE-2026-21714 +--- + src/node_http2.cc | 6 ++ + .../test-http2-window-update-overflow.js | 84 +++++++++++++++++++ + 2 files changed, 90 insertions(+) + create mode 100644 test/parallel/test-http2-window-update-overflow.js + +--- a/src/node_http2.cc ++++ b/src/node_http2.cc +@@ -1116,8 +1116,14 @@ + // The GOAWAY frame includes an error code that indicates the type of error" + // The GOAWAY frame is already sent by nghttp2. We emit the error + // to liberate the Http2Session to destroy. ++ // ++ // ERR_FLOW_CONTROL: A WINDOW_UPDATE on stream 0 pushed the connection-level ++ // flow control window past 2^31-1. nghttp2 sends GOAWAY internally but ++ // without propagating this error the Http2Session would never be destroyed, ++ // causing a memory leak. + if (nghttp2_is_fatal(lib_error_code) || + lib_error_code == NGHTTP2_ERR_STREAM_CLOSED || ++ lib_error_code == NGHTTP2_ERR_FLOW_CONTROL || + lib_error_code == NGHTTP2_ERR_PROTO) { + Environment* env = session->env(); + Isolate* isolate = env->isolate(); +--- /dev/null ++++ b/test/parallel/test-http2-window-update-overflow.js +@@ -0,0 +1,84 @@ ++'use strict'; ++ ++const common = require('../common'); ++ ++if (!common.hasCrypto) ++ common.skip('missing crypto'); ++ ++const http2 = require('http2'); ++const net = require('net'); ++ ++// Regression test: a connection-level WINDOW_UPDATE that causes the flow ++// control window to exceed 2^31-1 must destroy the Http2Session (not leak it). ++// ++// nghttp2 responds with GOAWAY(FLOW_CONTROL_ERROR) internally but previously ++// Node's OnInvalidFrame callback only propagated errors for ++// NGHTTP2_ERR_STREAM_CLOSED and NGHTTP2_ERR_PROTO. The missing ++// NGHTTP2_ERR_FLOW_CONTROL case left the session unreachable after the GOAWAY, ++// causing a memory leak. ++ ++const server = http2.createServer(); ++ ++server.on('session', common.mustCall((session) => { ++ session.on('error', common.mustCall()); ++ session.on('close', common.mustCall(() => server.close())); ++})); ++ ++server.listen(0, common.mustCall(() => { ++ const conn = net.connect({ ++ port: server.address().port, ++ allowHalfOpen: true, ++ }); ++ ++ // HTTP/2 client connection preface. ++ conn.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'); ++ ++ // Empty SETTINGS frame (9-byte header, 0-byte payload). ++ const settingsFrame = Buffer.alloc(9); ++ settingsFrame[3] = 0x04; // type: SETTINGS ++ conn.write(settingsFrame); ++ ++ let inbuf = Buffer.alloc(0); ++ let state = 'settingsHeader'; ++ let settingsFrameLength; ++ ++ conn.on('data', (chunk) => { ++ inbuf = Buffer.concat([inbuf, chunk]); ++ ++ switch (state) { ++ case 'settingsHeader': ++ if (inbuf.length < 9) return; ++ settingsFrameLength = inbuf.readUIntBE(0, 3); ++ inbuf = inbuf.slice(9); ++ state = 'readingSettings'; ++ // Fallthrough ++ case 'readingSettings': { ++ if (inbuf.length < settingsFrameLength) return; ++ inbuf = inbuf.slice(settingsFrameLength); ++ state = 'done'; ++ ++ // ACK the server SETTINGS. ++ const ack = Buffer.alloc(9); ++ ack[3] = 0x04; // type: SETTINGS ++ ack[4] = 0x01; // flag: ACK ++ conn.write(ack); ++ ++ // WINDOW_UPDATE on stream 0 (connection level) with increment 2^31-1. ++ // Default connection window is 65535, so the new total would be ++ // 65535 + 2147483647 = 2147549182 > 2^31-1, triggering ++ // NGHTTP2_ERR_FLOW_CONTROL inside nghttp2. ++ const windowUpdate = Buffer.alloc(13); ++ windowUpdate.writeUIntBE(4, 0, 3); // length = 4 ++ windowUpdate[3] = 0x08; // type: WINDOW_UPDATE ++ windowUpdate[4] = 0x00; // flags: none ++ windowUpdate.writeUIntBE(0, 5, 4); // stream id: 0 ++ windowUpdate.writeUIntBE(0x7FFFFFFF, 9, 4); // increment: 2^31-1 ++ conn.write(windowUpdate); ++ } ++ } ++ }); ++ ++ // The server must close the connection after sending GOAWAY. ++ conn.on('end', common.mustCall(() => conn.end())); ++ conn.on('close', common.mustCall()); ++})); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/56-tls-wrap-SNICallback-invocation-in-try-catch.patch nodejs-20.19.2+dfsg/debian/patches/sec/56-tls-wrap-SNICallback-invocation-in-try-catch.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/56-tls-wrap-SNICallback-invocation-in-try-catch.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/56-tls-wrap-SNICallback-invocation-in-try-catch.patch 2026-03-24 21:05:48.000000000 +0000 @@ -0,0 +1,170 @@ +From cc3f294507c715908b2b31a5301e295b3de04152 Mon Sep 17 00:00:00 2001 +From: Matteo Collina +Date: Tue, 17 Feb 2026 14:26:17 +0100 +Subject: [PATCH] tls: wrap SNICallback invocation in try/catch + +Wrap the owner._SNICallback() invocation in loadSNI() with try/catch +to route exceptions through owner.destroy() instead of letting them +become uncaught exceptions. This completes the fix from CVE-2026-21637 +which added try/catch protection to callALPNCallback, +onPskServerCallback, and onPskClientCallback but missed loadSNI(). + +Without this fix, a remote unauthenticated attacker can crash any +Node.js TLS server whose SNICallback may throw on unexpected input +by sending a single TLS ClientHello with a crafted server_name value. + +Fixes: https://hackerone.com/reports/3556769 +Refs: https://hackerone.com/reports/3473882 +CVE-ID: CVE-2026-21637 +PR-URL: https://github.com/nodejs-private/node-private/pull/839 +Reviewed-By: Rafael Gonzaga +CVE-ID: CVE-2026-21637 +--- + lib/_tls_wrap.js | 30 ++++--- + ...ls-psk-alpn-callback-exception-handling.js | 90 +++++++++++++++++++ + 2 files changed, 107 insertions(+), 13 deletions(-) + +diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js +index d9c7e32174d558..33d5ae8ec61003 100644 +--- a/lib/_tls_wrap.js ++++ b/lib/_tls_wrap.js +@@ -209,23 +209,27 @@ function loadSNI(info) { + return requestOCSP(owner, info); + + let once = false; +- owner._SNICallback(servername, (err, context) => { +- if (once) +- return owner.destroy(new ERR_MULTIPLE_CALLBACK()); +- once = true; ++ try { ++ owner._SNICallback(servername, (err, context) => { ++ if (once) ++ return owner.destroy(new ERR_MULTIPLE_CALLBACK()); ++ once = true; + +- if (err) +- return owner.destroy(err); ++ if (err) ++ return owner.destroy(err); + +- if (owner._handle === null) +- return owner.destroy(new ERR_SOCKET_CLOSED()); ++ if (owner._handle === null) ++ return owner.destroy(new ERR_SOCKET_CLOSED()); + +- // TODO(indutny): eventually disallow raw `SecureContext` +- if (context) +- owner._handle.sni_context = context.context || context; ++ // TODO(indutny): eventually disallow raw `SecureContext` ++ if (context) ++ owner._handle.sni_context = context.context || context; + +- requestOCSP(owner, info); +- }); ++ requestOCSP(owner, info); ++ }); ++ } catch (err) { ++ owner.destroy(err); ++ } + } + + +diff --git a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js +index 153853a3a9a4f0..12d4ede43f417e 100644 +--- a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js ++++ b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js +@@ -335,4 +335,94 @@ describe('TLS callback exception handling', () => { + + await promise; + }); ++ ++ // Test 7: SNI callback throwing should emit tlsClientError ++ it('SNICallback throwing emits tlsClientError', async (t) => { ++ const server = tls.createServer({ ++ key: fixtures.readKey('agent2-key.pem'), ++ cert: fixtures.readKey('agent2-cert.pem'), ++ SNICallback: (servername, cb) => { ++ throw new Error('Intentional SNI callback error'); ++ }, ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('tlsClientError', common.mustCall((err, socket) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.strictEqual(err.message, 'Intentional SNI callback error'); ++ socket.destroy(); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ server.on('secureConnection', () => { ++ reject(new Error('secureConnection should not fire')); ++ }); ++ ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ servername: 'evil.attacker.com', ++ rejectUnauthorized: false, ++ }); ++ ++ client.on('error', () => {}); ++ ++ await promise; ++ }); ++ ++ // Test 8: SNI callback with validation error should emit tlsClientError ++ it('SNICallback validation error emits tlsClientError', async (t) => { ++ const server = tls.createServer({ ++ key: fixtures.readKey('agent2-key.pem'), ++ cert: fixtures.readKey('agent2-cert.pem'), ++ SNICallback: (servername, cb) => { ++ // Simulate common developer pattern: throw on unknown servername ++ if (servername !== 'expected.example.com') { ++ throw new Error(`Unknown servername: ${servername}`); ++ } ++ cb(null, null); ++ }, ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('tlsClientError', common.mustCall((err, socket) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.ok(err.message.includes('Unknown servername')); ++ socket.destroy(); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ server.on('secureConnection', () => { ++ reject(new Error('secureConnection should not fire')); ++ }); ++ ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ servername: 'unexpected.domain.com', ++ rejectUnauthorized: false, ++ }); ++ ++ client.on('error', () => {}); ++ ++ await promise; ++ }); + }); diff -Nru nodejs-20.19.2+dfsg/debian/patches/series nodejs-20.19.2+dfsg/debian/patches/series --- nodejs-20.19.2+dfsg/debian/patches/series 2026-03-05 10:05:11.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/series 2026-03-24 21:07:05.000000000 +0000 @@ -32,3 +32,10 @@ sec/37-rethrow-stack-overflow-exceptions-in-async-hooks.patch sec/38-refactor-unsafe-buffer-creation-to-remove-zero-fill-toggle.patch sec/44-v8-riscv-fix-sp-handling-in-macroassembler-leave-frame.patch +sec/50-crypto-use-timing-safe-comparison-HMAC.patch +sec/51-fix-array-index-hash-collision.patch +sec/52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch +sec/53-include-permission-check-on-lib-fs-promises.patch +sec/54-add-permission-check-to-realpath-native.patch +sec/55-handle-NGHTTP2_ERR_FLOW_CONTROL-error-code.patch +sec/56-tls-wrap-SNICallback-invocation-in-try-catch.patch