lumbda/docs/tickets/0002-os-entropy-seed.md
russell@unturf.com 4960381c67 portal-rng: add (random-seed-from-os!) across all three tiers
Ticket 0002 — reads 8 bytes from /dev/urandom (little-endian u64) and
seeds xoshiro256**. Opt-in kernel entropy for stochastic runs; the
default stays deterministic (k=0 at startup), so ticket 0001's
portal-reproducibility contract is unchanged.

Real-world flow now one call away:
  Machine A: (random-seed-from-os!) + run simulation + portal-save
  Machine B: portal-resume — same stream, bit-for-bit

All three impls fail loud on /dev/urandom trouble (LispErr in Python
and C, stderr + exit(1) in asm) — no silent fallback to a weak seed.

Tests:
- tests/functional.lsp: 2 new shared asserts (entropic + replay)
- asm/test.sh: 2 new asm-local checks (149 total, was 147)
- make test-all green across Python (205), C (205), asm (149)

Whitepaper §7.5 gains one sentence noting the OS-seed path.
unmoad: zero new findings in added code.
2026-04-20 15:50:26 -04:00

4.2 KiB

0002 — (random-seed-from-os!) pulls kernel entropy

Status: open Reporter: fox (response to 0001 follow-up) Implementer: blackops Opened: 2026-04-20

Problem

Ticket 0001 gave us xoshiro256** with deterministic startup seed k = 0 and explicit (random-seed! k). Two processes running the same program produce the same stream. That is the point — it is what lets a portal round-trip reproduce state across machines.

But determinism has a dual cost: a program that wants a different stream per run must seed itself from outside. Without a kernel-entropy hook, users have to hand-roll one by opening /dev/urandom and folding bytes into an integer. That is defect-prone (wrong endianness, short read not handled, FD left open) and duplicates in every caller.

fox's note: "a random seed from os on at least one side is very important" — because the common real-world flow is:

  1. Machine A seeds from kernel entropy (random-seed-from-os!).
  2. Machine A runs a stochastic simulation for a while.
  3. Machine A portal-saves — state captured, including xoshiro256** state.
  4. Machine B portal-resumes — continues the same stream bit-for-bit.

Step 1 is missing today. This ticket adds it.

Goals

  1. One builtin, all three impls: (random-seed-from-os!).
  2. Reads 8 bytes from /dev/urandom, interprets as little-endian u64, calls existing rng_seed. No new PRNG, just a different seed source.
  3. Returns void; user captures (random-state) if they want to replay.
  4. Fail-loud on I/O error (missing /dev/urandom, short read) — we do not silently fall back to a weak seed.

Non-goals

  • Blocking /dev/random (entropy-exhaustion semantics). urandom is the right default; man 4 urandom confirms it is cryptographically strong after boot.
  • getrandom(2) syscall. Portable to older kernels matters more than saving one open/read/close.
  • Any silent use of OS entropy on startup. The existing k = 0 default stays; determinism remains the default, kernel entropy is opt-in.
  • /dev/urandom emulation on non-Linux. Lumbda targets Linux x86_64; other platforms earn their own ticket.

API

(random-seed-from-os!) → void

Reads exactly 8 bytes from /dev/urandom, little-endian, passes the resulting u64 to the same rng_seed path as (random-seed!). After return, (random-state) reflects the new state, and the portal system captures it unchanged.

Errors (fail-loud):

  • /dev/urandom cannot be opened → error + exit(1) in asm; LispErr in Python + C.
  • Short read (fewer than 8 bytes) → same treatment.

Implementation

Python (~3 lines):

def _rng_seed_from_os():
    with open('/dev/urandom', 'rb') as f:
        b = f.read(8)
    if len(b) != 8: raise LispErr('random-seed-from-os!: short read')
    _rng_seed(int.from_bytes(b, 'little', signed=False))

C (~10 lines): open / read / close on /dev/urandom, then call existing rng_seed(uint64_t).

Asm (~30 lines): SYS_OPEN, SYS_READ, SYS_CLOSE with an 8-byte stack slot, then call rng_seed. New data: s_dev_urandom: .asciz "/dev/urandom". Error message follows the existing err_rng_* pattern.

Test plan

  1. Unit(random-seed-from-os!) runs without error in each tier.
  2. Entropic — two consecutive (random-seed-from-os!) calls in the same process produce different streams (with overwhelming probability). Asserted via (not (equal? state-a state-b)).
  3. Determinism preserved(random-seed-from-os!) + capture via (random-state) + reseed with (random-state! saved) produces the original stream. This proves the OS seed plugs cleanly into the portal path from 0001.

Deliverables

  • docs/tickets/0002-os-entropy-seed.md (this ticket)
  • Python: lumbda.py — helper + builtin binding
  • C: c/portal.c — helper + builtin + register_portal_builtins
  • Asm: asm/lumbda.s — new BI_RANDOMSEEDFROMOS, dispatch, bi_random_seed_from_os, data strings
  • tests/functional.lsp — 2 shared assertions
  • asm/test.sh — 2 asm-local checks
  • Whitepaper §7.5 — one sentence noting the OS-seed option
  • make test-all green
  • unmoad clean on changed files