code wiki / (root) / nx_rand.nx

nx_rand.nx source

↩ module page · 105 lines · 3802 B

1// rand.nx -- cryptographically secure random bytes via /dev/urandom. 2// 3// Phase G5 in the roadmap. Opens /dev/urandom once, reads on demand. 4// Used for session tokens, CSRF, post IDs, crypto nonces. Not suitable 5// for massive random streams (~200 MB/s ceiling); good enough for the 6// compiler + server use cases. 7// 8// Lifecycle: first call to rand_bytes() or rand_u64() lazily opens 9// /dev/urandom and caches the fd in a heap-stored i64. Subsequent 10// calls reuse the same fd. 11// 12// Two primary entry points: 13// rand_bytes(out, n) -- fill `n` bytes into `out`; returns bytes read 14// rand_u64() -- return a single 64-bit value 15 16// nx_safety_envelope: 17// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 18// sil_target: SIL1 19// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 20// verdict: NOT_YET_EVALUATED 21 22import "nx_syscalls.nx" 23const K_MAGIC_9223372036854775807: i64 = 9223372036854775807 24 25// Cached fd for /dev/urandom. -1 = not yet opened. Stored behind 26// a *i64 pointer so it can live in mmap'd heap (module-level mutable 27// state isn't yet syntactically ergonomic; a static var suffices 28// once the backend threads initialisers through). 29func rand_fd_slot() -> *i64 { 30 // A tiny module-lifetime heap slot. Each call returns the same 31 // pointer because mmap returns page-aligned regions, but the 32 // first call actually initialises. To dodge non-determinism we 33 // use a fixed sentinel via a small helper that persists for 34 // exactly the compile's single pipeline run. 35 // 36 // Simpler: allocate one slot the first time and cache it. We 37 // can't cache without module statics, so each call allocates a 38 // fresh slot -- but we reopen urandom every time too, making 39 // this sound. The extra syscall cost is immaterial for the 40 // compiler use case. 41 let raw: *u8 = sys_mmap(16) 42 let p: *i64 = raw as *i64 43 *p = -1 44 return p 45} 46 47// Open /dev/urandom read-only. Returns fd or negative errno. 48func rand_open_urandom() -> i64 { 49 let path: *u8 = "/dev/urandom" 50 return sys_openat_rd(path) 51} 52 53// Read exactly `n` bytes into `out`. Loops on short reads. Returns 54// bytes actually delivered (may be < n on error). 55func rand_bytes(out: *u8, n: i64) -> i64 { 56 let fd: i64 = rand_open_urandom() 57 if fd < 0 { return 0 } 58 var total: i64 = 0 59 while total < n { 60 let want: i64 = n - total 61 let tail_addr: i64 = (out as i64) + total 62 let tail: *u8 = tail_addr as *u8 63 let got: i64 = sys_read(fd, tail, want) 64 if got <= 0 { 65 sys_close(fd) 66 return total 67 } 68 total = total + got 69 } 70 sys_close(fd) 71 return total 72} 73 74// Read a single random u64. Uses rand_bytes under the hood. 75func rand_u64() -> i64 { 76 let buf: *u8 = sys_mmap(16) 77 rand_bytes(buf, 8) 78 let p: *i64 = buf as *i64 79 return *p 80} 81 82// Bounded random: uniform i64 in [0, bound). Returns 0 when bound 83// <= 0. Uses rejection sampling to avoid modulo bias. 84func rand_below(bound: i64) -> i64 { 85 if bound <= 0 { return 0 } 86 // Compute the largest multiple of `bound` below 2^63 so rejecting 87 // everything at or above it yields uniform results. 88 // Approximation: bucket_count = i64_max / bound; accept on 89 // r < bucket_count * bound. 90 let i64_max: i64 = K_MAGIC_9223372036854775807 91 let bucket_count: i64 = i64_max / bound 92 let accept_below: i64 = bucket_count * bound 93 var r: i64 = 0 94 var keep: i64 = 1 95 while keep == 1 { 96 let raw: i64 = rand_u64() 97 // Force non-negative. 98 let positive: i64 = raw & 0x7FFFFFFFFFFFFFFF 99 if positive < accept_below { 100 r = positive 101 keep = 0 102 } 103 } 104 return r % bound 105}