Skip to content

Commit af5c144

Browse files
joyeecheungmarco-ippolito
authored andcommitted
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 <leszeks@chromium.org> Commit-Queue: snek <snek@chromium.org> Cr-Commit-Position: refs/heads/main@{#101499} Refs: v8/v8@0a8b1cd Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com> 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 <leszeks@chromium.org> Commit-Queue: Joyee Cheung <joyee@igalia.com> Cr-Commit-Position: refs/heads/main@{#105531} Refs: v8/v8@185f0fe Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com> 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 <leszeks@chromium.org> Commit-Queue: Joyee Cheung <joyee@igalia.com> Cr-Commit-Position: refs/heads/main@{#105596} Refs: v8/v8@1361b2a Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com> 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 <cwu631@bloomberg.net> Commit-Queue: Joyee Cheung <joyee@igalia.com> Reviewed-by: Leszek Swirski <leszeks@chromium.org> Cr-Commit-Position: refs/heads/main@{#105824} Refs: v8/v8@aac14dd PR-URL: nodejs-private/node-private#834 CVE-ID: CVE-2026-21717
1 parent 0123309 commit af5c144

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1295
-169
lines changed

common.gypi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
# Reset this number to 0 on major V8 upgrades.
3838
# Increment by one for each non-official patch applied to deps/v8.
39-
'v8_embedder_string': '-node.34',
39+
'v8_embedder_string': '-node.38',
4040

4141
##### V8 defaults for Node.js #####
4242

deps/v8/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
!/third_party/googletest/src/googletest/include/gtest
7979
/third_party/googletest/src/googletest/include/gtest/*
8080
!/third_party/googletest/src/googletest/include/gtest/gtest_prod.h
81+
!/third_party/rapidhash-v8
8182
!/third_party/test262-harness
8283
!/third_party/v8
8384
!/third_party/wasm-api

deps/v8/BUILD.bazel

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ v8_flag(
164164
default = False,
165165
)
166166

167+
v8_flag(
168+
name = "v8_enable_seeded_array_index_hash",
169+
default = False,
170+
)
171+
167172
v8_int(
168173
name = "v8_typed_array_max_size_in_heap",
169174
default = 64,
@@ -339,6 +344,7 @@ v8_config(
339344
"v8_enable_verify_heap": "VERIFY_HEAP",
340345
"v8_enable_verify_predictable": "VERIFY_PREDICTABLE",
341346
"v8_enable_webassembly": "V8_ENABLE_WEBASSEMBLY",
347+
"v8_enable_seeded_array_index_hash": "V8_ENABLE_SEEDED_ARRAY_INDEX_HASH",
342348
"v8_jitless": "V8_JITLESS",
343349
},
344350
defines = [
@@ -1685,6 +1691,8 @@ filegroup(
16851691
"src/numbers/conversions-inl.h",
16861692
"src/numbers/conversions.cc",
16871693
"src/numbers/conversions.h",
1694+
"src/numbers/hash-seed.h",
1695+
"src/numbers/hash-seed.cc",
16881696
"src/numbers/hash-seed-inl.h",
16891697
"src/numbers/integer-literal-inl.h",
16901698
"src/numbers/integer-literal.h",
@@ -2245,6 +2253,8 @@ filegroup(
22452253
"src/execution/pointer-authentication-dummy.h",
22462254
"src/heap/third-party/heap-api.h",
22472255
"src/heap/third-party/heap-api-stub.cc",
2256+
"third_party/rapidhash-v8/rapidhash.h",
2257+
"third_party/rapidhash-v8/secret.h",
22482258
] + select({
22492259
"@v8//bazel/config:v8_target_ia32": [
22502260
"src/baseline/ia32/baseline-assembler-ia32-inl.h",

deps/v8/BUILD.gn

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,12 @@ declare_args() {
399399
# iOS (non-simulator) does not have executable pages for 3rd party
400400
# applications yet so disable jit.
401401
v8_jitless = v8_enable_lite_mode || target_is_ios_device
402+
403+
# Use a hard-coded secret value when hashing.
404+
v8_use_default_hasher_secret = true
405+
406+
# Enable seeded array index hash.
407+
v8_enable_seeded_array_index_hash = false
402408
}
403409

404410
# Derived defaults.
@@ -802,6 +808,7 @@ config("external_startup_data") {
802808
# List of defines that can appear in externally visible header files and that
803809
# are controlled by args.gn.
804810
external_v8_defines = [
811+
"V8_USE_DEFAULT_HASHER_SECRET=${v8_use_default_hasher_secret}",
805812
"V8_ENABLE_CHECKS",
806813
"V8_COMPRESS_POINTERS",
807814
"V8_COMPRESS_POINTERS_IN_SHARED_CAGE",
@@ -818,7 +825,9 @@ external_v8_defines = [
818825
"V8_ENABLE_CONSERVATIVE_STACK_SCANNING",
819826
]
820827

821-
enabled_external_v8_defines = []
828+
enabled_external_v8_defines = [
829+
"V8_USE_DEFAULT_HASHER_SECRET=${v8_use_default_hasher_secret}",
830+
]
822831

823832
if (v8_enable_v8_checks) {
824833
enabled_external_v8_defines += [ "V8_ENABLE_CHECKS" ]
@@ -970,6 +979,9 @@ config("features") {
970979
if (v8_enable_lite_mode) {
971980
defines += [ "V8_LITE_MODE" ]
972981
}
982+
if (v8_enable_seeded_array_index_hash) {
983+
defines += [ "V8_ENABLE_SEEDED_ARRAY_INDEX_HASH" ]
984+
}
973985
if (v8_enable_gdbjit) {
974986
defines += [ "ENABLE_GDB_JIT_INTERFACE" ]
975987
}
@@ -3381,6 +3393,7 @@ v8_header_set("v8_internal_headers") {
33813393
"src/numbers/conversions-inl.h",
33823394
"src/numbers/conversions.h",
33833395
"src/numbers/hash-seed-inl.h",
3396+
"src/numbers/hash-seed.h",
33843397
"src/numbers/math-random.h",
33853398
"src/objects/all-objects-inl.h",
33863399
"src/objects/allocation-site-inl.h",
@@ -3710,6 +3723,8 @@ v8_header_set("v8_internal_headers") {
37103723
"src/temporal/temporal-parser.h",
37113724
"src/third_party/siphash/halfsiphash.h",
37123725
"src/third_party/utf8-decoder/utf8-decoder.h",
3726+
"third_party/rapidhash-v8/rapidhash.h",
3727+
"third_party/rapidhash-v8/secret.h",
37133728
"src/torque/runtime-macro-shims.h",
37143729
"src/tracing/trace-event.h",
37153730
"src/tracing/traced-value.h",
@@ -4872,6 +4887,7 @@ v8_source_set("v8_base_without_compiler") {
48724887
"src/logging/runtime-call-stats.cc",
48734888
"src/logging/tracing-flags.cc",
48744889
"src/numbers/conversions.cc",
4890+
"src/numbers/hash-seed.cc",
48754891
"src/numbers/math-random.cc",
48764892
"src/objects/backing-store.cc",
48774893
"src/objects/bigint.cc",

deps/v8/src/DEPS

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,14 @@ specific_include_rules = {
100100
],
101101
"etw-jit-metadata-win\.h": [
102102
"+src/libplatform/etw/etw-provider-win.h",
103-
]
103+
],
104+
"string-hasher-inl\.h": [
105+
"+third_party/rapidhash-v8/rapidhash.h",
106+
],
107+
"heap\.cc": [
108+
"+third_party/rapidhash-v8/secret.h",
109+
],
110+
"hash-seed\.cc": [
111+
"+third_party/rapidhash-v8/secret.h",
112+
],
104113
}

deps/v8/src/ast/ast-value-factory.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ bool AstRawString::AsArrayIndex(uint32_t* index) const {
8282
// can't be convertible to an array index.
8383
if (!IsIntegerIndex()) return false;
8484
if (length() <= Name::kMaxCachedArrayIndexLength) {
85-
*index = Name::ArrayIndexValueBits::decode(raw_hash_field_);
85+
*index = StringHasher::DecodeArrayIndexFromHashField(
86+
raw_hash_field_, HashSeed(ReadOnlyHeap::GetReadOnlyRoots()));
8687
return true;
8788
}
8889
// Might be an index, but too big to cache it. Do the slow conversion. This
@@ -291,7 +292,8 @@ std::forward_list<const AstRawString*> AstConsString::ToRawStrings() const {
291292
return result;
292293
}
293294

294-
AstStringConstants::AstStringConstants(Isolate* isolate, uint64_t hash_seed)
295+
AstStringConstants::AstStringConstants(Isolate* isolate,
296+
const HashSeed hash_seed)
295297
: zone_(isolate->allocator(), ZONE_NAME),
296298
string_table_(),
297299
hash_seed_(hash_seed) {

deps/v8/src/ast/ast-value-factory.h

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "src/common/globals.h"
3636
#include "src/handles/handles.h"
3737
#include "src/numbers/conversions.h"
38+
#include "src/numbers/hash-seed.h"
3839
#include "src/objects/name.h"
3940

4041
// Ast(Raw|Cons)String and AstValueFactory are for storing strings and
@@ -290,7 +291,7 @@ using AstRawStringMap =
290291

291292
class AstStringConstants final {
292293
public:
293-
AstStringConstants(Isolate* isolate, uint64_t hash_seed);
294+
AstStringConstants(Isolate* isolate, const HashSeed hash_seed);
294295
AstStringConstants(const AstStringConstants&) = delete;
295296
AstStringConstants& operator=(const AstStringConstants&) = delete;
296297

@@ -299,13 +300,13 @@ class AstStringConstants final {
299300
AST_STRING_CONSTANTS(F)
300301
#undef F
301302

302-
uint64_t hash_seed() const { return hash_seed_; }
303+
const HashSeed hash_seed() const { return hash_seed_; }
303304
const AstRawStringMap* string_table() const { return &string_table_; }
304305

305306
private:
306307
Zone zone_;
307308
AstRawStringMap string_table_;
308-
uint64_t hash_seed_;
309+
const HashSeed hash_seed_;
309310

310311
#define F(name, str) AstRawString* name##_string_;
311312
AST_STRING_CONSTANTS(F)
@@ -315,12 +316,12 @@ class AstStringConstants final {
315316
class AstValueFactory {
316317
public:
317318
AstValueFactory(Zone* zone, const AstStringConstants* string_constants,
318-
uint64_t hash_seed)
319+
const HashSeed hash_seed)
319320
: AstValueFactory(zone, zone, string_constants, hash_seed) {}
320321

321322
AstValueFactory(Zone* ast_raw_string_zone, Zone* single_parse_zone,
322323
const AstStringConstants* string_constants,
323-
uint64_t hash_seed)
324+
const HashSeed hash_seed)
324325
: string_table_(string_constants->string_table()),
325326
strings_(nullptr),
326327
strings_end_(&strings_),
@@ -418,7 +419,7 @@ class AstValueFactory {
418419
Zone* ast_raw_string_zone_;
419420
Zone* single_parse_zone_;
420421

421-
uint64_t hash_seed_;
422+
const HashSeed hash_seed_;
422423
};
423424

424425
extern template EXPORT_TEMPLATE_DECLARE(

deps/v8/src/builtins/number.tq

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ transitioning javascript builtin NumberParseFloat(
298298
const hash: NameHash = s.raw_hash_field;
299299
if (IsIntegerIndex(hash) &&
300300
hash.array_index_length < kMaxCachedArrayIndexLength) {
301-
const arrayIndex: uint32 = hash.array_index_value;
301+
const arrayIndex: uint32 = DecodeArrayIndexFromHashField(hash);
302302
return SmiFromUint32(arrayIndex);
303303
}
304304
// Fall back to the runtime to convert string to a number.
@@ -349,7 +349,7 @@ transitioning builtin ParseInt(implicit context: Context)(
349349
const hash: NameHash = s.raw_hash_field;
350350
if (IsIntegerIndex(hash) &&
351351
hash.array_index_length < kMaxCachedArrayIndexLength) {
352-
const arrayIndex: uint32 = hash.array_index_value;
352+
const arrayIndex: uint32 = DecodeArrayIndexFromHashField(hash);
353353
return SmiFromUint32(arrayIndex);
354354
}
355355
// Fall back to the runtime.

deps/v8/src/codegen/code-stub-assembler.cc

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,6 +2185,66 @@ TNode<IntPtrT> CodeStubAssembler::LoadJSReceiverIdentityHash(
21852185
return var_hash.value();
21862186
}
21872187

2188+
#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH
2189+
// Mirror C++ StringHasher::SeedArrayIndexValue.
2190+
TNode<Uint32T> CodeStubAssembler::SeedArrayIndexValue(TNode<Uint32T> value) {
2191+
// Load m1, m2 and m3 from the hash seed byte array. In the compiled code
2192+
// these will always come from the read-only roots.
2193+
TNode<ByteArray> hash_seed = CAST(LoadRoot(RootIndex::kHashSeed));
2194+
intptr_t base_offset = ByteArray::kHeaderSize - kHeapObjectTag;
2195+
TNode<Uint32T> m1 = Load<Uint32T>(
2196+
hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM1Offset));
2197+
TNode<Uint32T> m2 = Load<Uint32T>(
2198+
hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM2Offset));
2199+
TNode<Uint32T> m3 = Load<Uint32T>(
2200+
hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM3Offset));
2201+
2202+
TNode<Word32T> x = value;
2203+
// 3-round xorshift-multiply.
2204+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2205+
x = Word32And(Uint32Mul(Unsigned(x), m1),
2206+
Uint32Constant(Name::kArrayIndexValueMask));
2207+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2208+
x = Word32And(Uint32Mul(Unsigned(x), m2),
2209+
Uint32Constant(Name::kArrayIndexValueMask));
2210+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2211+
x = Word32And(Uint32Mul(Unsigned(x), m3),
2212+
Uint32Constant(Name::kArrayIndexValueMask));
2213+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2214+
2215+
return Unsigned(x);
2216+
}
2217+
2218+
// Mirror C++ StringHasher::UnseedArrayIndexValue.
2219+
TNode<Uint32T> CodeStubAssembler::UnseedArrayIndexValue(TNode<Uint32T> value) {
2220+
// Load m1_inv, m2_inv and m3_inv from the hash seed byte array. In the
2221+
// compiled code these will always come from the read-only roots.
2222+
TNode<ByteArray> hash_seed = CAST(LoadRoot(RootIndex::kHashSeed));
2223+
intptr_t base_offset = ByteArray::kHeaderSize - kHeapObjectTag;
2224+
TNode<Uint32T> m1_inv = Load<Uint32T>(
2225+
hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM1InvOffset));
2226+
TNode<Uint32T> m2_inv = Load<Uint32T>(
2227+
hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM2InvOffset));
2228+
TNode<Uint32T> m3_inv = Load<Uint32T>(
2229+
hash_seed, IntPtrConstant(base_offset + HashSeed::kDerivedM3InvOffset));
2230+
2231+
TNode<Word32T> x = value;
2232+
// 3-round xorshift-multiply (inverse).
2233+
// Xorshift is an involution when kShift is at least half of the value width.
2234+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2235+
x = Word32And(Uint32Mul(Unsigned(x), m3_inv),
2236+
Uint32Constant(Name::kArrayIndexValueMask));
2237+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2238+
x = Word32And(Uint32Mul(Unsigned(x), m2_inv),
2239+
Uint32Constant(Name::kArrayIndexValueMask));
2240+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2241+
x = Word32And(Uint32Mul(Unsigned(x), m1_inv),
2242+
Uint32Constant(Name::kArrayIndexValueMask));
2243+
x = Word32Xor(x, Word32Shr(x, Uint32Constant(Name::kArrayIndexHashShift)));
2244+
return Unsigned(x);
2245+
}
2246+
#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH
2247+
21882248
TNode<Uint32T> CodeStubAssembler::LoadNameHashAssumeComputed(TNode<Name> name) {
21892249
TNode<Uint32T> hash_field = LoadNameRawHash(name);
21902250
CSA_DCHECK(this, IsClearWord32(hash_field, Name::kHashNotComputedMask));
@@ -7620,8 +7680,7 @@ TNode<Number> CodeStubAssembler::StringToNumber(TNode<String> input) {
76207680
GotoIf(IsSetWord32(raw_hash_field, Name::kDoesNotContainCachedArrayIndexMask),
76217681
&runtime);
76227682

7623-
var_result = SmiTag(Signed(
7624-
DecodeWordFromWord32<String::ArrayIndexValueBits>(raw_hash_field)));
7683+
var_result = SmiFromUint32(DecodeArrayIndexFromHashField(raw_hash_field));
76257684
Goto(&end);
76267685

76277686
BIND(&runtime);
@@ -8496,8 +8555,8 @@ void CodeStubAssembler::TryToName(TNode<Object> key, Label* if_keyisindex,
84968555

84978556
BIND(&if_has_cached_index);
84988557
{
8499-
TNode<IntPtrT> index = Signed(
8500-
DecodeWordFromWord32<String::ArrayIndexValueBits>(raw_hash_field));
8558+
TNode<IntPtrT> index = Signed(ChangeUint32ToWord(
8559+
DecodeArrayIndexFromHashField(raw_hash_field)));
85018560
CSA_DCHECK(this, IntPtrLessThan(index, IntPtrConstant(INT_MAX)));
85028561
*var_index = index;
85038562
Goto(if_keyisindex);

deps/v8/src/codegen/code-stub-assembler.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4293,6 +4293,12 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
42934293
TNode<Uint8T> property_details,
42944294
Label* needs_resize);
42954295

4296+
#ifdef V8_ENABLE_SEEDED_ARRAY_INDEX_HASH
4297+
// Mirror C++ StringHasher::SeedArrayIndexValue and UnseedArrayIndexValue.
4298+
TNode<Uint32T> SeedArrayIndexValue(TNode<Uint32T> value);
4299+
TNode<Uint32T> UnseedArrayIndexValue(TNode<Uint32T> value);
4300+
#endif // V8_ENABLE_SEEDED_ARRAY_INDEX_HASH
4301+
42964302
private:
42974303
friend class CodeStubArguments;
42984304

0 commit comments

Comments
 (0)