commit 81a35a86ea9385016f371213a2b72b14902bb955 (tree)
parent e3e9c7c33c029368ad644619ed20f3d7cccd3a47
Author: Andrew Kelley <andrew@ziglang.org>
Date: Mon, 5 Jan 2026 17:06:03 -0800
std.Io: introduce random and randomSecure
and use a thread-local CSPRNG for the former.
Diffstat:
7 files changed, 135 insertions(+), 17 deletions(-)
diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig
@@ -984,7 +984,8 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
};
// We do not know the final output paths yet, use temp paths to run the command.
- const rand_int = std.crypto.random.int(u64);
+ var rand_int: u64 = undefined;
+ io.random(@ptrCast(&rand_int));
const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
for (output_placeholders.items) |placeholder| {
@@ -1128,7 +1129,8 @@ pub fn rerunInFuzzMode(
}
const has_side_effects = false;
- const rand_int = std.crypto.random.int(u64);
+ var rand_int: u64 = undefined;
+ io.random(@ptrCast(&rand_int));
const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{
.progress_node = prog_node,
diff --git a/lib/std/Build/Step/WriteFile.zig b/lib/std/Build/Step/WriteFile.zig
@@ -293,7 +293,8 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
.tmp => {
step.result_cached = false;
- const rand_int = std.crypto.random.int(u64);
+ var rand_int: u64 = undefined;
+ io.random(@ptrCast(&rand_int));
const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path});
diff --git a/lib/std/Io.zig b/lib/std/Io.zig
@@ -731,7 +731,8 @@ pub const VTable = struct {
now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp,
sleep: *const fn (?*anyopaque, Timeout) SleepError!void,
- random: *const fn (?*anyopaque, buffer: []u8) RandomError!void,
+ random: *const fn (?*anyopaque, buffer: []u8) void,
+ randomSecure: *const fn (?*anyopaque, buffer: []u8) RandomSecureError!void,
netListenIp: *const fn (?*anyopaque, address: net.IpAddress, net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server,
netAccept: *const fn (?*anyopaque, server: net.Socket.Handle) net.Server.AcceptError!net.Stream,
@@ -2245,9 +2246,34 @@ pub fn unlockStderr(io: Io) void {
return io.vtable.unlockStderr(io.userdata);
}
-pub const RandomError = error{EntropyUnavailable} || Cancelable;
-
+/// Obtains entropy.
+///
+/// The implementation *may* store RNG state in process memory and use it to
+/// fill `buffer`.
+///
+/// The degree to which the entropy is cryptographically secure is determined
+/// by the `Io` implementation.
+///
/// Threadsafe.
-pub fn random(io: Io, buffer: []u8) RandomError!void {
+///
+/// See also `randomSecure`.
+pub fn random(io: Io, buffer: []u8) void {
return io.vtable.random(io.userdata, buffer);
}
+
+pub const RandomSecureError = error{EntropyUnavailable} || Cancelable;
+
+/// Obtains cryptographically secure entropy from outside the process.
+///
+/// Always makes a syscall, or otherwise avoids dependency on process memory,
+/// in order to obtain fresh randomness. Does not rely on stored RNG state.
+///
+/// Does not have any fallback mechanisms; returns `error.EntropyUnavailable`
+/// if any problems occur.
+///
+/// Threadsafe.
+///
+/// See also `random`.
+pub fn randomSecure(io: Io, buffer: []u8) RandomSecureError!void {
+ return io.vtable.randomSecure(io.userdata, buffer);
+}
diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig
@@ -1098,8 +1098,10 @@ pub fn symLinkAtomic(
const temp_path = temp_path_buf[0..temp_path_len];
+ var random_integer: u64 = undefined;
+
while (true) {
- const random_integer = std.crypto.random.int(u64);
+ io.random(@ptrCast(&random_integer));
temp_path[dirname.len + 1 ..][0..rand_len].* = std.fmt.hex(random_integer);
if (dir.symLink(io, target_path, temp_path, flags)) {
diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig
@@ -67,6 +67,21 @@ environ: Environ,
null_file: NullFile = .{},
random_file: RandomFile = .{},
+csprng: Csprng = .{},
+
+pub const Csprng = struct {
+ rng: std.Random.DefaultCsprng = .{
+ .state = undefined,
+ .offset = std.math.maxInt(usize),
+ },
+
+ pub const seed_len = std.Random.DefaultCsprng.secret_seed_length;
+
+ pub fn isInitialized(c: *const Csprng) bool {
+ return c.rng.offset == std.math.maxInt(usize);
+ }
+};
+
pub const Argv0 = switch (native_os) {
.openbsd, .haiku => struct {
value: ?[*:0]const u8,
@@ -595,7 +610,7 @@ const Thread = struct {
/// Always released when `Status.cancelation` is set to `.parked`.
futex_waiter: if (use_parking_futex) ?*parking_futex.Waiter else ?noreturn,
- csprng: std.Random.DefaultCsprng,
+ csprng: Csprng,
const Handle = Handle: {
if (std.Thread.use_pthreads) break :Handle std.c.pthread_t;
@@ -1326,10 +1341,7 @@ fn worker(t: *Threaded) void {
}),
.cancel_protection = .unblocked,
.futex_waiter = undefined,
- .csprng = .{
- .state = undefined,
- .offset = std.math.maxInt(usize),
- },
+ .csprng = .{},
};
Thread.current = &thread;
@@ -1484,6 +1496,7 @@ pub fn io(t: *Threaded) Io {
.sleep = sleep,
.random = random,
+ .randomSecure = randomSecure,
.netListenIp = switch (native_os) {
.windows => netListenIpWindows,
@@ -1634,6 +1647,7 @@ pub fn ioBasic(t: *Threaded) Io {
.sleep = sleep,
.random = random,
+ .randomSecure = randomSecure,
.netListenIp = netListenIpUnavailable,
.netListenUnix = netListenUnixUnavailable,
@@ -3584,8 +3598,9 @@ fn atomicFileInit(
dir: Dir,
close_dir_on_deinit: bool,
) Dir.CreateFileAtomicError!File.Atomic {
+ var random_integer: u64 = undefined;
while (true) {
- const random_integer = std.crypto.random.int(u64);
+ t_io.random(@ptrCast(&random_integer));
const tmp_sub_path = std.fmt.hex(random_integer);
const file = dir.createFile(t_io, &tmp_sub_path, .{
.permissions = permissions,
@@ -12468,7 +12483,8 @@ fn lookupDns(
for (family_records) |fr| {
if (options.family != fr.af) {
- const entropy = std.crypto.random.array(u8, 2);
+ var entropy: [2]u8 = undefined;
+ random(t, &entropy);
const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy);
queries_buffer[nq] = query_buffers[nq][0..len];
nq += 1;
@@ -15018,7 +15034,62 @@ pub fn environString(t: *Threaded, comptime name: []const u8) ?[:0]const u8 {
return @field(t.environ.string, name);
}
-fn random(userdata: ?*anyopaque, buffer: []u8) Io.RandomError!void {
+fn random(userdata: ?*anyopaque, buffer: []u8) void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const thread = Thread.current orelse return randomMainThread(t, buffer);
+ if (!thread.csprng.isInitialized()) {
+ @branchHint(.unlikely);
+ var seed: [Csprng.seed_len]u8 = undefined;
+ randomMainThread(t, &seed);
+ thread.csprng.rng = .init(seed);
+ }
+ thread.csprng.rng.fill(buffer);
+}
+
+fn randomMainThread(t: *Threaded, buffer: []u8) void {
+ t.mutex.lock();
+ defer t.mutex.unlock();
+
+ if (!t.csprng.isInitialized()) {
+ @branchHint(.unlikely);
+ var seed: [Csprng.seed_len]u8 = undefined;
+ {
+ t.mutex.unlock();
+ defer t.mutex.lock();
+
+ const prev = swapCancelProtection(t, .blocked);
+ defer _ = swapCancelProtection(t, prev);
+
+ randomSecure(t, &seed) catch |err| switch (err) {
+ error.Canceled => unreachable,
+ error.EntropyUnavailable => {
+ seed = @splat(0);
+ std.mem.writeInt(posix.pid_t, seed[0..@sizeOf(posix.pid_t)], posix.system.getpid(), .native);
+ const i_1 = @sizeOf(posix.pid_t);
+
+ var ts: posix.timespec = undefined;
+ const Sec = @TypeOf(ts.sec);
+ const Nsec = @TypeOf(ts.nsec);
+ const i_2 = i_1 + @sizeOf(Sec);
+ const i_3 = i_2 + @sizeOf(Nsec);
+ switch (posix.errno(posix.system.clock_gettime(.REALTIME, &ts))) {
+ .SUCCESS => {
+ std.mem.writeInt(Sec, seed[i_1..][0..@sizeOf(Sec)], ts.sec, .native);
+ std.mem.writeInt(Nsec, seed[i_2..][0..@sizeOf(Nsec)], ts.nsec, .native);
+ },
+ else => {},
+ }
+ std.mem.writeInt(usize, seed[i_3..][0..@sizeOf(usize)], @intFromPtr(t), .native);
+ },
+ };
+ }
+ t.csprng.rng = .init(seed);
+ }
+
+ t.csprng.rng.fill(buffer);
+}
+
+fn randomSecure(userdata: ?*anyopaque, buffer: []u8) Io.RandomSecureError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
if (is_windows) {
diff --git a/lib/std/Random.zig b/lib/std/Random.zig
@@ -33,6 +33,22 @@ pub const ziggurat = @import("Random/ziggurat.zig");
ptr: *anyopaque,
fillFn: *const fn (ptr: *anyopaque, buf: []u8) void,
+pub const IoSource = struct {
+ io: std.Io,
+
+ pub fn interface(this: *const @This()) std.Random {
+ return .{
+ .ptr = this,
+ .fillFn = fill,
+ };
+ }
+
+ fn fill(ptr: *anyopaque, buffer: []u8) void {
+ const this: *const @This() = @ptrCast(@alignCast(ptr));
+ this.io.random(buffer);
+ }
+};
+
pub fn init(pointer: anytype, comptime fillFn: fn (ptr: @TypeOf(pointer), buf: []u8) void) Random {
const Ptr = @TypeOf(pointer);
assert(@typeInfo(Ptr) == .pointer); // Must be a pointer
diff --git a/lib/std/Random/ChaCha.zig b/lib/std/Random/ChaCha.zig
@@ -20,7 +20,7 @@ pub const secret_seed_length = Cipher.key_length;
/// The seed must be uniform, secret and `secret_seed_length` bytes long.
pub fn init(secret_seed: [secret_seed_length]u8) Self {
- var self = Self{ .state = undefined, .offset = 0 };
+ var self: Self = .{ .state = undefined, .offset = 0 };
Cipher.stream(&self.state, 0, secret_seed, nonce);
return self;
}