zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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:
Mlib/std/Build/Step/Run.zig | 6++++--
Mlib/std/Build/Step/WriteFile.zig | 3++-
Mlib/std/Io.zig | 34++++++++++++++++++++++++++++++----
Mlib/std/Io/Dir.zig | 4+++-
Mlib/std/Io/Threaded.zig | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mlib/std/Random.zig | 16++++++++++++++++
Mlib/std/Random/ChaCha.zig | 2+-
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; }