Skip to main content

nova_rt_str_hash

Function nova_rt_str_hash 

Source
#[unsafe(no_mangle)]
pub unsafe extern "C" fn nova_rt_str_hash(s: *const NovaStr) -> i64
Expand description

Seeded, finalized hash of a Nova string’s bytes, as an i64.

std/core’s impl Hash for String reaches this through the str_hash builtin. An earlier version of this comment justified that by saying Nova cannot walk a string’s bytes itself, on the grounds that String has no length, indexing or iteration and is not FFI-safe. The FFI-safety half is true. The byte-walking half was true when written and is now stale, by two separate routes rather than the one an earlier draft of this paragraph named. The byte and char BUILTINS – str_chars, bytes_from_string_intrinsic, bytes_at – are Builtin::STD_ONLY members, and the resolver seeds every member of that array into every std module’s scope, which is how std/core comes to walk a string at its own impl Debug for String. Separately, std/bytes and std/strings are STD_MODULES entries whose public items reach other modules through import_std_module. STD_ONLY holds builtins, not modules; conflating the two is what that earlier draft did. The reason that survives does not rest on impossibility: the hash lives in one place, and reimplementing FNV-1a in Nova would pin a second copy of it free to diverge from this one. ADR 0005’s 2026-08-27 amendment records the same correction against the same sentence.

Seeded FNV-1a over the bytes, then splitmix64’s finalizer. The seed is per-process (see str_hash_seed) and the finalizer is what makes the seed reach the low bits Map selects buckets from; the reasoning and the measurements are at splitmix64_finalize. This used to be unseeded FNV-1a alone, described here as “adequate for a hash map’s bucket selection” – true of an honest workload and false of an adversarial one, which is why it changed.

STILL NOT COLLISION-RESISTANT, and still not for anything security-sensitive. What the seed and finalizer buy is resistance to a PRECOMPUTED collision set: the seed is drawn per process, so a colliding key set cannot be built offline against a process that has not started yet, and recovering the seed means first obtaining a hash from the running process. This paragraph used to close “mix64 – the Int, Bool and Char impls – is not seeded at all.” That is no longer true: those impls XOR nova_rt_int_hash_seed’s separate per-process draw into mix64’s input, so they carry the same precomputation resistance and the same recoverable seed as this one does. mix64 itself is still unseeded, which is a different statement – the seeding is in the impls that call it.

An adversary who can observe the process and adapt is OUT OF SCOPE, and the route is concrete rather than a category: one string hash read out of this process recovers the seed, because ("").hash() is the finalizer applied to the seed alone (see str_hash_seed), so an attacker who can obtain any String hash from a running process can construct collisions for it. The channel is ADR 0005’s one-shot fn hash(self) -> Int returning a whole 64-bit result in one call, which predates the seeding: seeding did not open the channel, it made the value behind it worth recovering. Precomputation resistance is the half that survives.

The u64 -> i64 reinterpretation at the end means the result may be negative, so a caller selecting buckets must mask (hash & (cap - 1), which is non-negative for any i64) rather than take a remainder.

§Safety

s must point to a valid NovaStr.