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}