/*
* xoshiro256++ — a fast, non-cryptographic 64-bit PRNG.
*
* Reference:
* D. Blackman and S. Vigna, "Scrambled Linear Pseudorandom
* Number Generators", ACM Trans. on Math. Software 47:4
* (2021). https://vigna.di.unimi.it/ftp/papers/ScrambledLinear.pdf
*
* One 256-bit state per pthread, stored in '__thread' storage so
* concurrent generation across many Haskell capabilities never
* touches a shared cache line. Seeded on first use from
* @getrandom(2)@.
*
* Used by 'wireform-websocket' to roll per-frame masking keys
* without going through the global 'splitmix' MVar. Exposed
* generically because the same primitive is what any
* non-cryptographic random need on the hot path should reach
* for.
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <unistd.h>
#if defined(__linux__)
#include <sys/syscall.h>
#include <linux/random.h>
static int kernel_random(void *buf, size_t len)
{
long r = syscall(SYS_getrandom, buf, len, 0);
return r == (long)len ? 0 : -1;
}
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
#include <stdlib.h>
static int kernel_random(void *buf, size_t len)
{
arc4random_buf(buf, len);
return 0;
}
#else
/* Fall back to /dev/urandom for portability. */
#include <fcntl.h>
static int kernel_random(void *buf, size_t len)
{
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
size_t off = 0;
while (off < len) {
ssize_t n = read(fd, (char *)buf + off, len - off);
if (n <= 0) { close(fd); return -1; }
off += (size_t)n;
}
close(fd);
return 0;
}
#endif
/* TLS access model for the per-thread state below.
*
* A file-local 'static __thread' defaults to the local-exec model: a
* single FS/GS-relative MOV, the fastest TLS access (what the comment
* below describes). That is only valid when the object lands in the
* main executable's static TLS block. When the object is built
* position-independent for a shared library — GHC's dynamic
* ('.dyn_o' -> '.so') way, used whenever wireform-core sits in a
* Template-Haskell dependency closure — local-exec / local-dynamic
* emit 'R_X86_64_TPOFF32' / 'R_X86_64_DTPOFF32' relocations that ld
* cannot link into the '.so' on x86_64-linux ("relocation truncated
* to fit").
*
* '__PIC__' is defined by GCC / Clang exactly when compiling
* position-independent (the '.dyn_o' way) and undefined for the plain
* static '.o'. Keep the fast local-exec model for the static object;
* only step down to initial-exec ('R_X86_64_GOTTPOFF', a single GOT
* indirection that links into a '.so') when actually building PIC.
* initial-exec keeps the symbol file-local (unlike global-dynamic,
* which would require an exported symbol) and its few words of TLS sit
* within glibc's static-TLS surplus when the library is dlopen'd. */
#if defined(__PIC__)
#define WF_TLS_MODEL __attribute__((tls_model("initial-exec")))
#else
#define WF_TLS_MODEL
#endif
/* Per-thread state. 256-bit (4 × uint64_t). '__thread' is the
* GCC / Clang extension; on every modern Unix it lands in the
* TLS block one offset away from FS / GS register, so the
* dispatch is a single MOV (local-exec; see WF_TLS_MODEL above for
* the position-independent / shared-object case). */
static __thread uint64_t xoshiro_state[4] WF_TLS_MODEL;
static __thread int xoshiro_seeded WF_TLS_MODEL = 0;
static inline uint64_t rotl64(uint64_t x, int k)
{
return (x << k) | (x >> (64 - k));
}
/* Recover from a degenerate all-zero seed (xoshiro256++ has a
* fixed point at 0). Vigna's reference splitmix64 is the
* recommended way to expand a single 64-bit input into a 256-bit
* xoshiro seed. We only invoke this when 'kernel_random'
* returns all-zero, which on a working kernel never happens. */
static inline uint64_t splitmix64_step(uint64_t *x)
{
*x += 0x9E3779B97F4A7C15ULL;
uint64_t z = *x;
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
}
static void xoshiro_seed(void)
{
if (kernel_random(xoshiro_state, sizeof(xoshiro_state)) != 0
|| (xoshiro_state[0] | xoshiro_state[1]
| xoshiro_state[2] | xoshiro_state[3]) == 0)
{
/* Last-resort fallback: tid + nanotime through splitmix. */
uint64_t fallback = (uint64_t)(uintptr_t)&xoshiro_state;
for (int i = 0; i < 4; i++) {
xoshiro_state[i] = splitmix64_step(&fallback);
}
}
xoshiro_seeded = 1;
}
/* The xoshiro256++ next() generator. Pure register work after
* the (cached) TLS load. */
uint64_t hs_xoshiro256pp_next(void)
{
if (!xoshiro_seeded) xoshiro_seed();
const uint64_t *s = xoshiro_state;
const uint64_t result = rotl64(s[0] + s[3], 23) + s[0];
const uint64_t t = xoshiro_state[1] << 17;
xoshiro_state[2] ^= xoshiro_state[0];
xoshiro_state[3] ^= xoshiro_state[1];
xoshiro_state[1] ^= xoshiro_state[2];
xoshiro_state[0] ^= xoshiro_state[3];
xoshiro_state[2] ^= t;
xoshiro_state[3] = rotl64(xoshiro_state[3], 45);
return result;
}
/* Force reseed. Useful after fork() and for tests that want
* deterministic state — they can call this then immediately
* overwrite the seed via hs_xoshiro256pp_reseed. */
void hs_xoshiro256pp_reseed(void)
{
xoshiro_seeded = 0;
}