From f8c6c50011593ebb9fbf6d2d9846f6e6bd1071eb Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 2 Jul 2026 01:34:38 -0700 Subject: [PATCH 1/2] fix: strengthen Map/Set key hashing against collision DoS --- quickjs.c | 6 ++-- tests/test_builtin.js | 72 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/quickjs.c b/quickjs.c index 9a834a78d..fbabe4c41 100644 --- a/quickjs.c +++ b/quickjs.c @@ -52678,10 +52678,10 @@ static uint32_t map_hash_key(JSContext *ctx, JSValueConst key) h = JS_VALUE_GET_INT(key); break; case JS_TAG_STRING: - h = hash_string(JS_VALUE_GET_STRING(key), 0); + h = hash_string(JS_VALUE_GET_STRING(key), JS_VALUE_GET_STRING(key)->len); break; case JS_TAG_STRING_ROPE: - h = hash_string_rope(key, 0); + h = hash_string_rope(key, JS_VALUE_GET_STRING_ROPE(key)->len); break; case JS_TAG_OBJECT: case JS_TAG_SYMBOL: @@ -52704,7 +52704,7 @@ static uint32_t map_hash_key(JSContext *ctx, JSValueConst key) d = NAN; hash_float64: u.d = d; - h = (u.u32[0] ^ u.u32[1]) * 3163; + h = (u.u32[0] * 3163 + u.u32[1] * 82589933) * 3163; return h ^= JS_TAG_FLOAT64; default: h = 0; diff --git a/tests/test_builtin.js b/tests/test_builtin.js index fb93e6206..4cbdc0710 100644 --- a/tests/test_builtin.js +++ b/tests/test_builtin.js @@ -959,6 +959,16 @@ function test_symbol() assert(b.toString(), "Symbol(aaa)"); } +function float64_from_words(hi, lo) +{ + var buffer, view; + buffer = new ArrayBuffer(8); + view = new DataView(buffer); + view.setUint32(0, hi); + view.setUint32(4, lo); + return view.getFloat64(0); +} + function test_map() { var a, i, n, tab, o, v; @@ -973,6 +983,42 @@ function test_map() assert(a.get(-2147483647 - 1), 1); assert(a.get(-2147483647.5 - 0.5), 1); + a = new Map(); + tab = ["k", "\0k", "\0\0k"]; + for(i = 0; i < tab.length; i++) { + a.set(tab[i], i); + } + assert(a.size, tab.length); + for(i = 0; i < tab.length; i++) { + assert(a.get(tab[i]), i); + } + + a = new Map(); + tab = [ + [0, "zero"], + [float64_from_words(0x00000001, 0x00000001), "subnormal"], + [float64_from_words(0x3ff00000, 0x3ff00000), "normal1"], + [float64_from_words(0x40080000, 0x40080000), "normal2"], + ]; + for(i = 0; i < tab.length; i++) { + a.set(tab[i][0], tab[i][1]); + } + assert(a.size, tab.length); + for(i = 0; i < tab.length; i++) { + assert(a.get(tab[i][0]), tab[i][1]); + } + + a = new Map(); + a.set(NaN, 1); + a.set(Number.NaN, 2); + assert(a.size, 1); + assert(a.get(NaN), 2); + a.set(-0, 3); + a.set(+0, 4); + assert(a.size, 2); + assert(a.get(-0), 4); + assert(a.get(+0), 4); + a = new Map(); tab = []; for(i = 0; i < n; i++) { @@ -1034,6 +1080,7 @@ function test_weak_map() function test_set() { + var i, set, tab; const iter = { a: [4, 5, 6], nextCalls: 0, @@ -1101,6 +1148,31 @@ function test_set() keys() { return [].values() }, }) } + + tab = ["k", "\0k", "\0\0k"]; + set = new Set(tab); + assert(set.size, tab.length); + for(i = 0; i < tab.length; i++) { + assert(set.has(tab[i])); + } + + tab = [ + 0, + float64_from_words(0x00000001, 0x00000001), + float64_from_words(0x3ff00000, 0x3ff00000), + float64_from_words(0x40080000, 0x40080000), + ]; + set = new Set(tab); + assert(set.size, tab.length); + for(i = 0; i < tab.length; i++) { + assert(set.has(tab[i])); + } + + set = new Set([NaN, Number.NaN, -0, +0]); + assert(set.size, 2); + assert(set.has(NaN)); + assert(set.has(-0)); + assert(set.has(+0)); } function test_weak_set() From df4c9433a8fe491b9b0975e7b80b98792338763d Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 2 Jul 2026 01:43:16 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20self-review=20=E2=80=94=20?= =?UTF-8?q?use=20avalanche=20mix=20for=20float64=20Map/Set=20key=20hash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- quickjs.c | 12 +++++++++++- tests/test_builtin.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index fbabe4c41..ffb91df81 100644 --- a/quickjs.c +++ b/quickjs.c @@ -52665,6 +52665,16 @@ static JSValueConst map_normalize_key_const(JSContext *ctx, JSValueConst key) } /* XXX: better hash ? */ +static uint32_t map_hash_u64(uint64_t h) +{ + h ^= h >> 33; + h *= 0xff51afd7ed558ccdULL; + h ^= h >> 33; + h *= 0xc4ceb9fe1a85ec53ULL; + h ^= h >> 33; + return (uint32_t)(h ^ (h >> 32)); +} + static uint32_t map_hash_key(JSContext *ctx, JSValueConst key) { uint32_t tag = JS_VALUE_GET_NORM_TAG(key); @@ -52704,7 +52714,7 @@ static uint32_t map_hash_key(JSContext *ctx, JSValueConst key) d = NAN; hash_float64: u.d = d; - h = (u.u32[0] * 3163 + u.u32[1] * 82589933) * 3163; + h = map_hash_u64(u.u64); return h ^= JS_TAG_FLOAT64; default: h = 0; diff --git a/tests/test_builtin.js b/tests/test_builtin.js index 4cbdc0710..f22d35e6d 100644 --- a/tests/test_builtin.js +++ b/tests/test_builtin.js @@ -969,6 +969,13 @@ function float64_from_words(hi, lo) return view.getFloat64(0); } +function float64_map_hash_regression_value(i) +{ + var word; + word = (0x3ff00000 + (i << 10)) >>> 0; + return float64_from_words(word, word); +} + function test_map() { var a, i, n, tab, o, v; @@ -1008,6 +1015,18 @@ function test_map() assert(a.get(tab[i][0]), tab[i][1]); } + a = new Map(); + n = 4096; + for(i = 0; i < n; i++) { + v = float64_map_hash_regression_value(i); + a.set(v, i); + } + assert(a.size, n); + for(i = 0; i < n; i++) { + v = float64_map_hash_regression_value(i); + assert(a.get(v), i); + } + a = new Map(); a.set(NaN, 1); a.set(Number.NaN, 2); @@ -1168,6 +1187,16 @@ function test_set() assert(set.has(tab[i])); } + tab = []; + for(i = 0; i < 4096; i++) { + tab.push(float64_map_hash_regression_value(i)); + } + set = new Set(tab); + assert(set.size, tab.length); + for(i = 0; i < tab.length; i++) { + assert(set.has(tab[i])); + } + set = new Set([NaN, Number.NaN, -0, +0]); assert(set.size, 2); assert(set.has(NaN));