rand.nx source
↩ module page · 98 lines · 3577 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
16import "syscalls.nx"
17
18// Cached fd for /dev/urandom. -1 = not yet opened. Stored behind
19// a *i64 pointer so it can live in mmap'd heap (module-level mutable
20// state isn't yet syntactically ergonomic; a static var suffices
21// once the backend threads initialisers through).
22func rand_fd_slot() -> *i64 {
23 // A tiny module-lifetime heap slot. Each call returns the same
24 // pointer because mmap returns page-aligned regions, but the
25 // first call actually initialises. To dodge non-determinism we
26 // use a fixed sentinel via a small helper that persists for
27 // exactly the compile's single pipeline run.
28 //
29 // Simpler: allocate one slot the first time and cache it. We
30 // can't cache without module statics, so each call allocates a
31 // fresh slot -- but we reopen urandom every time too, making
32 // this sound. The extra syscall cost is immaterial for the
33 // compiler use case.
34 let raw: *u8 = sys_mmap(16)
35 let p: *i64 = raw as *i64
36 *p = -1
37 return p
38}
39
40// Open /dev/urandom read-only. Returns fd or negative errno.
41func rand_open_urandom() -> i64 {
42 let path: *u8 = "/dev/urandom"
43 return sys_openat_rd(path)
44}
45
46// Read exactly `n` bytes into `out`. Loops on short reads. Returns
47// bytes actually delivered (may be < n on error).
48func rand_bytes(out: *u8, n: i64) -> i64 {
49 let fd: i64 = rand_open_urandom()
50 if fd < 0 { return 0 }
51 var total: i64 = 0
52 while total < n {
53 let want: i64 = n - total
54 let tail_addr: i64 = (out as i64) + total
55 let tail: *u8 = tail_addr as *u8
56 let got: i64 = sys_read(fd, tail, want)
57 if got <= 0 {
58 sys_close(fd)
59 return total
60 }
61 total = total + got
62 }
63 sys_close(fd)
64 return total
65}
66
67// Read a single random u64. Uses rand_bytes under the hood.
68func rand_u64() -> i64 {
69 let buf: *u8 = sys_mmap(16)
70 rand_bytes(buf, 8)
71 let p: *i64 = buf as *i64
72 return *p
73}
74
75// Bounded random: uniform i64 in [0, bound). Returns 0 when bound
76// <= 0. Uses rejection sampling to avoid modulo bias.
77func rand_below(bound: i64) -> i64 {
78 if bound <= 0 { return 0 }
79 // Compute the largest multiple of `bound` below 2^63 so rejecting
80 // everything at or above it yields uniform results.
81 // Approximation: bucket_count = i64_max / bound; accept on
82 // r < bucket_count * bound.
83 let i64_max: i64 = 9223372036854775807
84 let bucket_count: i64 = i64_max / bound
85 let accept_below: i64 = bucket_count * bound
86 var r: i64 = 0
87 var keep: i64 = 1
88 while keep == 1 {
89 let raw: i64 = rand_u64()
90 // Force non-negative.
91 let positive: i64 = raw & 0x7FFFFFFFFFFFFFFF
92 if positive < accept_below {
93 r = positive
94 keep = 0
95 }
96 }
97 return r % bound
98}