rand: use the OS CSPRNG for uuid_v4, uuid_v7, UUIDSession and ulid#27152
Open
davlgd wants to merge 4 commits into
Open
rand: use the OS CSPRNG for uuid_v4, uuid_v7, UUIDSession and ulid#27152davlgd wants to merge 4 commits into
uuid_v4, uuid_v7, UUIDSession and ulid#27152davlgd wants to merge 4 commits into
Conversation
JalonSolov
reviewed
May 13, 2026
da6626f to
1a71e6b
Compare
1a71e6b to
4da5994
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The high-level identifier generators in
vlib/rand/(uuid_v4,uuid_v7,UUIDSession.next,ulidandulid_at_millisecond) currently draw their random bits fromdefault_rng, which is aWyRandRNG— a fast non-cryptographic PRNG seeded from the current time. This PR routes those bits throughcrypto.rand.readinstead, i.e. through the OS CSPRNG (getrandomon Linux,getentropyon macOS/BSD,BCryptGenRandomon Windows), without changing the public API or the produced format.Importing
crypto.randfromvlib/rand/would normally introduce an import cycle (crypto.randused to depend onmath.bigandencoding.binary, both of which transitively depend onrandvia test files orsync). The first commit breaks that cycle by trimmingcrypto.rand's own dependencies — see Commits below.Motivation
These identifiers are routinely used as session IDs, request IDs, file boundaries, primary keys and cache keys (see e.g.
vlib/veb/auth/auth.v,vlib/veb/request_id,vlib/net/websocket,vlib/net/http/request.v,vlib/v/pref/pref.v). In those contexts, unpredictability matters: a non-CSPRNG default lets an attacker who observes a few IDs reconstruct the PRNG state and predict subsequent ones.Each of the relevant specifications already points in this direction:
rand_aandrand_bfields.ulid/specis the most explicit: "If no cryptographically secure random number generator is available, ULIDs should not be generated."The major ecosystems all default to a CSPRNG.
Commits
The PR is three commits, bisect-clean (each one builds and passes its relevant tests on its own):
crypto.rand: split int_big into crypto.rand.bigint sub-module— movesint_big()(the only function incrypto.randthat needsmath.big) into a newcrypto.rand.bigintsub-module, and inlines the singleencoding.binary.big_endian_u64call socrypto.randno longer depends onencoding.binaryeither. Breaking change: callers ofcrypto.rand.int_bigneed to switch tocrypto.rand.bigint.int_big. Inside vlib that's a single testdata file (vlib/v/gen/c/testdata/struct_field_result_init.vv), updated in the same commit.crypto.rand.int_u64stays where it is.rand: use OS CSPRNG for uuid_v4, uuid_v7 and UUIDSession— the actual switch for the three UUID-family functions.rand: use OS CSPRNG for ulid and ulid_at_millisecond— same forulid. On the C backend the convenience functions now go through the CSPRNG; on the JS backend they keep the previous behaviour byte-for-byte.Changes per file
vlib/crypto/rand/utils.vimport math.bigandimport encoding.binary.binary.big_endian_u64call (3 lines).int_big()(moved to the new sub-module).vlib/crypto/rand/bigint/bigint.vcrypto.rand.bigint. Containspub fn int_big(n big.Integer) !big.Integer. Importscrypto.randandmath.big.vlib/crypto/rand/bigint/bigint_test.vutils_test.vcontent, renamed and re-pointed tobigint.int_big.vlib/v/gen/c/testdata/struct_field_result_init.vvrand.int_big(result)→bigint.int_big(result).vlib/rand/rand.c.vcsprng_u64_pair()returning 16 bytes of OS CSPRNG output as two big-endianu64s. Ifcrypto.rand.readreturns aReadError(OS entropy source unavailable), the helper panics rather than falling back to a non-cryptographic generator, which would defeat the guarantee.uuid_v4,uuid_v7andUUIDSession.nextto consume that helper instead ofdefault_rng.u64()/default_rng.u16().internal_ulid_format(time, rand_a, rand_b)frominternal_ulid_at_millisecond. The PRNG-based method is unchanged (it's a one-line wrapper around the formatter), and the new module-level convenienceulid()/ulid_at_millisecond()reuse the same formatter withcsprng_u64_pair()as the random source.vlib/rand/rand.vpub fn ulid()andpub fn ulid_at_millisecond()that used to delegate todefault_rng. They move to the backend-specific files so that each backend can pick its own entropy source. The method forms(mut rng PRNG) ulid()/(mut rng PRNG) ulid_at_millisecond()stay where they are — they are explicit opt-in and untouched.vlib/rand/rand.js.vpub fn ulid()andpub fn ulid_at_millisecond()that delegate todefault_rng. This preserves the current JS-backend behaviour byte-for-byte. Bringing a CSPRNG to the JS backend (viacrypto.getRandomValuesorcrypto.randomBytes) is a separate question and is intentionally out of scope.API and behaviour
fn () string) and produced formats (36-char dashed UUID, 26-char Crockford Base32 ULID) are identical.default_rngexactly as before.(mut rng PRNG) ulid()still draws from the user-provided PRNG. The opt-in path is preserved.A future optimisation would be to amortise the syscall via a userspace entropy buffer (as other implementations do — one syscall every 128 UUIDs). That is intentionally not part of this PR: it raises its own questions (thread-safety, zeroising consumed bytes, fork handling) and deserves a dedicated discussion. The current change is the smallest one that brings V in line with the spec recommendations.
Additional notes
panicon CSPRNG failure. Changinguuid_v4to return!stringwould break a lot of call sites, including code that uses it as a function pointer of typefn () string(e.g.veb/request_id).crypto.rand.readalready exposes an unrecoverableReadErrorwhen the OS entropy source fails; the UUID/ULID layer simply elevates that to apanicrather than silently degrading to a non-cryptographic source, which would defeat the whole point.v vetwarnings inrand.v(i32_in_range,f64cpdoc comments) are left untouched.