metrale_storage/tier/
entropy.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: OS entropy for per-process key salts; its caller is the SSM decode
4//! tier's client salt (`decode_client_salt` in metrale-model-engine).
5//!
6//! Owner: storage, tiered-cache core.
7//! Invariants:
8//! - `random_u64` returns OS entropy or an error, never a fallback value.
9//!
10//! Kept apart from `tier::hash`, which must return the same value for the same input
11//! forever; a salt must differ per process, so two clients of one model sharing a
12//! paging peer derive different keys.
13
14use anyhow::Result;
15
16/// 2026-09-25: 8 bytes of OS entropy as a `u64`. Linux: `libc::getrandom` with no
17/// flags, retried on `EINTR` and partial reads. Other unix: `/dev/urandom`. Windows:
18/// the `getrandom` crate. A failure is an error.
19pub fn random_u64() -> Result<u64> {
20    let mut buf = [0u8; 8];
21    fill(&mut buf)?;
22    Ok(u64::from_le_bytes(buf))
23}
24
25#[cfg(target_os = "linux")]
26fn fill(buf: &mut [u8]) -> Result<()> {
27    let mut got = 0usize;
28    while got < buf.len() {
29        let r = unsafe { libc::getrandom(buf[got..].as_mut_ptr().cast(), buf.len() - got, 0) };
30        if r < 0 {
31            let e = std::io::Error::last_os_error();
32            if e.raw_os_error() == Some(libc::EINTR) {
33                continue;
34            }
35            anyhow::bail!("getrandom failed (refusing a degraded zero-entropy salt): {e}");
36        }
37        got += r as usize;
38    }
39    Ok(())
40}
41
42#[cfg(all(unix, not(target_os = "linux")))]
43fn fill(buf: &mut [u8]) -> Result<()> {
44    use std::io::Read;
45    let mut f = std::fs::File::open("/dev/urandom")
46        .map_err(|e| anyhow::anyhow!("open /dev/urandom failed (refusing a degraded salt): {e}"))?;
47    f.read_exact(buf)
48        .map_err(|e| anyhow::anyhow!("read /dev/urandom failed (refusing a degraded salt): {e}"))
49}
50
51/// 2026-09-25: Windows: the `getrandom` crate, a direct dependency of this crate.
52#[cfg(windows)]
53fn fill(buf: &mut [u8]) -> Result<()> {
54    getrandom::fill(buf)
55        .map_err(|e| anyhow::anyhow!("BCryptGenRandom failed (refusing a degraded salt): {e}"))
56}
57
58#[cfg(test)]
59#[path = "entropy_tests.rs"]
60mod tests;