motiejus/zig

fork of https://codeberg.org/ziglang/zig
git clone https://git.jakstys.lt/motiejus/zig.git
Log | Tree | Refs | README | LICENSE

commit 816565dd077f561a46a9f31d9ecce32f152f9553 (tree)
parent 867501d9d2f757f99af59b1904b251aa63262e23
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Sun,  4 Jan 2026 12:14:03 -0800

std: move entropy to Io

Diffstat:
Mlib/compiler/build_runner.zig | 1-
Mlib/std/Io.zig | 9+++++++++
Mlib/std/Io/Threaded.zig | 257++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mlib/std/Io/test.zig | 11+++++++++++
Mlib/std/Random.zig | 4+---
Mlib/std/crypto.zig | 11-----------
Dlib/std/crypto/tlcsprng.zig | 169-------------------------------------------------------------------------------
Mlib/std/posix.zig | 101-------------------------------------------------------------------------------
Mlib/std/std.zig | 6------
9 files changed, 263 insertions(+), 306 deletions(-)

diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig @@ -21,7 +21,6 @@ pub const dependencies = @import("@dependencies"); pub const std_options: std.Options = .{ .side_channels_mitigations = .none, .http_disable_tls = true, - .crypto_fork_safety = false, }; pub fn main(init: process.Init.Minimal) !void { diff --git a/lib/std/Io.zig b/lib/std/Io.zig @@ -731,6 +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, + 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, netBindIp: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, @@ -2242,3 +2244,10 @@ pub fn tryLockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancel pub fn unlockStderr(io: Io) void { return io.vtable.unlockStderr(io.userdata); } + +pub const RandomError = error{EntropyUnavailable} || Cancelable; + +/// Threadsafe. +pub fn random(io: Io, buffer: []u8) RandomError!void { + return io.vtable.random(io.userdata, buffer); +} diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig @@ -65,6 +65,7 @@ argv0: Argv0, environ: Environ, null_file: NullFile = .{}, +dev_urandom_fd: dev_urandom_fd_t, pub const Argv0 = switch (native_os) { .openbsd, .haiku => struct { @@ -585,6 +586,10 @@ const Thread = struct { /// Always released when `Status.cancelation` is set to `.parked`. futex_waiter: if (use_parking_futex) ?*parking_futex.Waiter else ?noreturn, + random_buffer: [128]u8, + /// How many bytes of `random_buffer` are filled. + random_i: usize, + const Handle = Handle: { if (std.Thread.use_pthreads) break :Handle std.c.pthread_t; if (builtin.target.os.tag == .windows) break :Handle windows.HANDLE; @@ -1285,6 +1290,9 @@ pub fn deinit(t: *Threaded) void { if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null); } t.null_file.deinit(); + if (use_dev_urandom and t.dev_urandom_fd != -1) { + posix.close(t.dev_urandom_fd); + } t.* = undefined; } @@ -1466,6 +1474,8 @@ pub fn io(t: *Threaded) Io { .now = now, .sleep = sleep, + .random = random, + .netListenIp = switch (native_os) { .windows => netListenIpWindows, else => netListenIpPosix, @@ -1614,6 +1624,8 @@ pub fn ioBasic(t: *Threaded) Io { .now = now, .sleep = sleep, + .random = random, + .netListenIp = netListenIpUnavailable, .netListenUnix = netListenUnixUnavailable, .netAccept = netAcceptUnavailable, @@ -1704,6 +1716,26 @@ const linux_copy_file_range_use_c = std.c.versionCheck(if (builtin.abi.isAndroid }); const linux_copy_file_range_sys = if (linux_copy_file_range_use_c) std.c else std.os.linux; +const statx_use_c = std.c.versionCheck(if (builtin.abi.isAndroid()) + .{ .major = 30, .minor = 0, .patch = 0 } +else + .{ .major = 2, .minor = 28, .patch = 0 }); + +const getrandom_use_libc = @TypeOf(posix.system.getrandom) != void and (native_os != .linux or + std.c.versionCheck(if (builtin.abi.isAndroid()) .{ + .major = 28, + .minor = 0, + .patch = 0, + } else .{ + .major = 2, + .minor = 25, + .patch = 0, + })); + +const use_dev_urandom = getrandom_use_libc and native_os == .linux; + +const dev_urandom_fd_t = if (use_dev_urandom) posix.fd_t else void; + fn async( userdata: ?*anyopaque, result: []u8, @@ -2538,11 +2570,7 @@ fn dirStatFileLinux( const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; const linux = std.os.linux; - const use_c = std.c.versionCheck(if (builtin.abi.isAndroid()) - .{ .major = 30, .minor = 0, .patch = 0 } - else - .{ .major = 2, .minor = 28, .patch = 0 }); - const sys = if (use_c) std.c else std.os.linux; + const sys = if (statx_use_c) std.c else std.os.linux; var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); @@ -2778,11 +2806,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; const linux = std.os.linux; - const use_c = std.c.versionCheck(if (builtin.abi.isAndroid()) - .{ .major = 30, .minor = 0, .patch = 0 } - else - .{ .major = 2, .minor = 28, .patch = 0 }); - const sys = if (use_c) std.c else std.os.linux; + const sys = if (statx_use_c) std.c else std.os.linux; const syscall: Syscall = try .start(); while (true) { @@ -6318,11 +6342,6 @@ fn fchmodatFallback( mode: posix.mode_t, ) Dir.SetFilePermissionsError!void { comptime assert(native_os == .linux); - const use_c = std.c.versionCheck(if (builtin.abi.isAndroid()) - .{ .major = 30, .minor = 0, .patch = 0 } - else - .{ .major = 2, .minor = 28, .patch = 0 }); - const sys = if (use_c) std.c else std.os.linux; // Fallback to changing permissions using procfs: // @@ -6369,6 +6388,7 @@ fn fchmodatFallback( defer posix.close(path_fd); const path_mode = mode: { + const sys = if (statx_use_c) std.c else std.os.linux; const syscall: Syscall = try .start(); while (true) { var statx = std.mem.zeroes(std.os.linux.Statx); @@ -14935,6 +14955,213 @@ 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 { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + if (is_windows) { + // Call RtlGenRandom() instead of CryptGetRandom() on Windows + // https://github.com/rust-lang-nursery/rand/issues/111 + // https://bugzilla.mozilla.org/show_bug.cgi?id=504270 + const max_read_size: windows.ULONG = std.math.maxInt(windows.ULONG); + var i: usize = 0; + while (i < buffer.len) { + const buf = buffer[i..]; + const request_n: windows.ULONG = @min(buf.len, max_read_size); + const syscall: Syscall = try .start(); + const result = windows.advapi32.RtlGenRandom(buf.ptr, request_n); + syscall.finish(); + if (result == 0) { + // `RtlGenRandom` has been observed to fail in situations where + // the system is under heavy load. Unfortunately, it does not + // call `SetLastError`, so it is not possible to get more + // specific error information; it could actually be due to an + // out-of-memory condition, for example. + return error.EntropyUnavailable; + } + i += request_n; + } + return; + } + + if (builtin.link_libc and @TypeOf(posix.system.arc4random_buf) != void) { + posix.system.arc4random_buf(buffer.ptr, buffer.len); + return; + } + + if (native_os == .wasi) { + const syscall: Syscall = try .start(); + while (true) switch (std.os.wasi.random_get(buffer.ptr, buffer.len)) { + .SUCCESS => return syscall.finish(), + .INTR => { + try syscall.checkCancel(); + continue; + }, + else => return syscall.fail(error.EntropyUnavailable), + }; + } + + if (@TypeOf(posix.system.getrandom) != void) { + const getrandom = if (getrandom_use_libc) std.c.getrandom else std.os.linux.getrandom; + var i: usize = 0; + const syscall: Syscall = try .start(); + while (i < buffer.len) { + const buf = buffer[i..]; + const rc = getrandom(buf.ptr, buf.len, 0); + switch (posix.errno(rc)) { + .SUCCESS => { + syscall.finish(); + const n: usize = @intCast(rc); + i += n; + continue; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + else => return syscall.fail(error.EntropyUnavailable), + } + } + return; + } + + if (native_os == .emscripten) { + const err = posix.errno(std.c.getentropy(buffer.ptr, buffer.len)); + switch (err) { + .SUCCESS => return, + else => return error.EntropyUnavailable, + } + } + + const urandom_fd = try getRandomFd(t); + + var i: usize = 0; + while (buffer.len - i != 0) { + const syscall: Syscall = try .start(); + const rc = posix.system.read(urandom_fd, buffer[i..].ptr, buffer.len - i); + switch (posix.errno(rc)) { + .SUCCESS => { + syscall.finish(); + const n: usize = @intCast(rc); + if (n == 0) { + if (buffer.len - i != 0) { + return error.EntropyUnavailable; + } else { + return; + } + } + i += n; + continue; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + else => return syscall.fail(error.EntropyUnavailable), + } + } +} + +fn getRandomFd(t: *Threaded) posix.fd_t { + { + t.mutex.lock(); + defer t.mutex.unlock(); + + if (t.dev_urandom_fd == -2) return error.EntropyUnavailable; + if (t.dev_urandom_fd != -1) return t.dev_urandom_fd; + } + + const fd: posix.fd_t = fd: { + const syscall: Syscall = try .start(); + while (true) { + const rc = openat_sym(posix.AT.FDCWD, "/dev/urandom", .{ + .ACCMODE = .RDONLY, + .CLOEXEC = true, + }, 0); + switch (posix.errno(rc)) { + .SUCCESS => { + syscall.finish(); + break :fd @intCast(rc); + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + else => { + syscall.endSyscall(); + t.dev_urandom_fd = -2; + return error.EntropyUnavailable; + }, + } + } + }; + errdefer posix.close(fd); + + switch (native_os) { + .linux => { + const sys = if (statx_use_c) std.c else std.os.linux; + const syscall: Syscall = try .start(); + while (true) { + var statx = std.mem.zeroes(std.os.linux.Statx); + switch (sys.errno(sys.statx(fd, "", std.os.linux.AT.EMPTY_PATH, .{ .TYPE = true }, &statx))) { + .SUCCESS => { + syscall.finish(); + if (!statx.mask.TYPE) return error.Unexpected; + t.mutex.lock(); // Another thread might have won the race. + defer t.mutex.unlock(); + if (t.dev_urandom_fd >= 0) { + posix.close(fd); + return t.dev_urandom_fd; + } else if (!posix.S.ISCHR(statx.mode)) { + t.dev_urandom_fd = -2; + return error.EntropyUnavailable; + } else { + t.dev_urandom_fd = fd; + return fd; + } + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + else => { + t.dev_urandom_fd = -2; + return error.EntropyUnavailable; + }, + } + } + }, + else => { + const syscall: Syscall = try .start(); + while (true) { + var stat = std.mem.zeroes(posix.Stat); + switch (posix.errno(fstat_sym(fd, &stat))) { + .SUCCESS => { + syscall.finish(); + if (t.dev_urandom_fd >= 0) { + posix.close(fd); + return t.dev_urandom_fd; + } else if (!posix.S.ISCHR(stat.mode)) { + t.dev_urandom_fd = -2; + return error.EntropyUnavailable; + } else { + t.dev_urandom_fd = fd; + return fd; + } + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + else => { + t.dev_urandom_fd = -2; + return error.EntropyUnavailable; + }, + } + } + }, + } +} + test { _ = @import("Threaded/test.zig"); } diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig @@ -564,3 +564,14 @@ test "tasks spawned in group after Group.cancel are canceled" { try io.sleep(.fromMilliseconds(10), .awake); // let that first sleep start up try group.concurrent(io, global.waitThenSpawn, .{ io, &group }); } + +test "CSPRNG" { + const io = testing.io; + + var random = io.random(); + + const a = random.int(u64); + const b = random.int(u64); + const c = random.int(u64); + try std.testing.expect(a ^ b ^ c != 0); +} diff --git a/lib/std/Random.zig b/lib/std/Random.zig @@ -1,15 +1,13 @@ //! The engines provided here should be initialized from an external source. -//! For a thread-local cryptographically secure pseudo random number generator, -//! use `std.crypto.random`. //! Be sure to use a CSPRNG when required, otherwise using a normal PRNG will //! be faster and use substantially less stack space. +const Random = @This(); const std = @import("std.zig"); const math = std.math; const mem = std.mem; const assert = std.debug.assert; const maxInt = std.math.maxInt; -const Random = @This(); /// Fast unbiased random numbers. pub const DefaultPrng = Xoshiro256; diff --git a/lib/std/crypto.zig b/lib/std/crypto.zig @@ -235,9 +235,6 @@ pub const nacl = struct { /// Finite-field arithmetic. pub const ff = @import("crypto/ff.zig"); -/// This is a thread-local, cryptographically secure pseudo random number generator. -pub const random = @import("crypto/tlcsprng.zig").interface; - /// Encoding and decoding pub const codecs = @import("crypto/codecs.zig"); @@ -364,20 +361,12 @@ test { _ = secureZero; _ = timing_safe; _ = ff; - _ = random; _ = errors; _ = tls; _ = Certificate; _ = codecs; } -test "CSPRNG" { - const a = random.int(u64); - const b = random.int(u64); - const c = random.int(u64); - try std.testing.expect(a ^ b ^ c != 0); -} - test "issue #4532: no index out of bounds" { const types = [_]type{ hash.Md5, diff --git a/lib/std/crypto/tlcsprng.zig b/lib/std/crypto/tlcsprng.zig @@ -1,169 +0,0 @@ -//! Thread-local cryptographically secure pseudo-random number generator. -//! This file has public declarations that are intended to be used internally -//! by the standard library; this namespace is not intended to be exposed -//! directly to standard library users. - -const std = @import("std"); -const builtin = @import("builtin"); -const mem = std.mem; -const native_os = builtin.os.tag; -const posix = std.posix; - -/// We use this as a layer of indirection because global const pointers cannot -/// point to thread-local variables. -pub const interface: std.Random = .{ - .ptr = undefined, - .fillFn = tlsCsprngFill, -}; - -const os_has_fork = @TypeOf(posix.fork) != void; -const os_has_arc4random = builtin.link_libc and (@TypeOf(std.c.arc4random_buf) != void); -const want_fork_safety = os_has_fork and !os_has_arc4random and std.options.crypto_fork_safety; -const maybe_have_wipe_on_fork = builtin.os.isAtLeast(.linux, .{ - .major = 4, - .minor = 14, - .patch = 0, -}) orelse true; - -const Rng = std.Random.DefaultCsprng; - -const Context = struct { - init_state: enum(u8) { uninitialized = 0, initialized, failed }, - rng: Rng, -}; - -var install_atfork_handler = std.once(struct { - // Install the global handler only once. - // The same handler is shared among threads and is inherinted by fork()-ed - // processes. - fn do() void { - const r = std.c.pthread_atfork(null, null, childAtForkHandler); - std.debug.assert(r == 0); - } -}.do); - -threadlocal var wipe_mem: []align(std.heap.page_size_min) u8 = &[_]u8{}; - -fn tlsCsprngFill(_: *anyopaque, buffer: []u8) void { - if (os_has_arc4random) { - // arc4random is already a thread-local CSPRNG. - return std.c.arc4random_buf(buffer.ptr, buffer.len); - } - // Allow applications to decide they would prefer to have every call to - // std.crypto.random always make an OS syscall, rather than rely on an - // application implementation of a CSPRNG. - if (std.options.crypto_always_getrandom) { - return std.options.cryptoRandomSeed(buffer); - } - - if (wipe_mem.len == 0) { - // Not initialized yet. - if (want_fork_safety and maybe_have_wipe_on_fork) { - // Allocate a per-process page, madvise operates with page - // granularity. - wipe_mem = posix.mmap( - null, - @sizeOf(Context), - posix.PROT.READ | posix.PROT.WRITE, - .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, - -1, - 0, - ) catch { - // Could not allocate memory for the local state, fall back to - // the OS syscall. - return std.options.cryptoRandomSeed(buffer); - }; - // The memory is already zero-initialized. - } else { - // Use a static thread-local buffer. - const S = struct { - threadlocal var buf: Context align(std.heap.page_size_min) = .{ - .init_state = .uninitialized, - .rng = undefined, - }; - }; - wipe_mem = mem.asBytes(&S.buf); - } - } - const ctx: *Context = @ptrCast(wipe_mem.ptr); - - switch (ctx.init_state) { - .uninitialized => { - if (!want_fork_safety) { - return initAndFill(buffer); - } - - if (maybe_have_wipe_on_fork) wof: { - // Qemu user-mode emulation ignores any valid/invalid madvise - // hint and returns success. Check if this is the case by - // passing bogus parameters, we expect EINVAL as result. - if (posix.madvise(wipe_mem.ptr, 0, 0xffffffff)) |_| { - break :wof; - } else |_| {} - - if (posix.madvise(wipe_mem.ptr, wipe_mem.len, posix.MADV.WIPEONFORK)) |_| { - return initAndFill(buffer); - } else |_| {} - } - - if (std.Thread.use_pthreads) { - return setupPthreadAtforkAndFill(buffer); - } - - // Since we failed to set up fork safety, we fall back to always - // calling getrandom every time. - ctx.init_state = .failed; - return std.options.cryptoRandomSeed(buffer); - }, - .initialized => { - return fillWithCsprng(buffer); - }, - .failed => { - if (want_fork_safety) { - return std.options.cryptoRandomSeed(buffer); - } else { - unreachable; - } - }, - } -} - -fn setupPthreadAtforkAndFill(buffer: []u8) void { - install_atfork_handler.call(); - return initAndFill(buffer); -} - -fn childAtForkHandler() callconv(.c) void { - // The atfork handler is global, this function may be called after - // fork()-ing threads that never initialized the CSPRNG context. - if (wipe_mem.len == 0) return; - std.crypto.secureZero(u8, wipe_mem); -} - -fn fillWithCsprng(buffer: []u8) void { - const ctx: *Context = @ptrCast(wipe_mem.ptr); - return ctx.rng.fill(buffer); -} - -pub fn defaultRandomSeed(buffer: []u8) void { - posix.getrandom(buffer) catch @panic("getrandom() failed to provide entropy"); -} - -fn initAndFill(buffer: []u8) void { - var seed: [Rng.secret_seed_length]u8 = undefined; - // Because we panic on getrandom() failing, we provide the opportunity - // to override the default seed function. This also makes - // `std.crypto.random` available on freestanding targets, provided that - // the `std.options.cryptoRandomSeed` function is provided. - std.options.cryptoRandomSeed(&seed); - - const ctx: *Context = @ptrCast(wipe_mem.ptr); - ctx.rng = Rng.init(seed); - std.crypto.secureZero(u8, &seed); - - // This is at the end so that accidental recursive dependencies result - // in stack overflows instead of invalid random data. - ctx.init_state = .initialized; - - return fillWithCsprng(buffer); -} diff --git a/lib/std/posix.zig b/lib/std/posix.zig @@ -361,107 +361,6 @@ pub fn reboot(cmd: RebootCommand) RebootError!void { } } -pub const GetRandomError = OpenError; - -/// Obtain a series of random bytes. These bytes can be used to seed user-space -/// random number generators or for cryptographic purposes. -/// When linking against libc, this calls the -/// appropriate OS-specific library call. Otherwise it uses the zig standard -/// library implementation. -pub fn getrandom(buffer: []u8) GetRandomError!void { - if (native_os == .windows) { - return windows.ProcessPrng(buffer); - } - if (builtin.link_libc and @TypeOf(system.arc4random_buf) != void) { - system.arc4random_buf(buffer.ptr, buffer.len); - return; - } - if (native_os == .wasi) switch (wasi.random_get(buffer.ptr, buffer.len)) { - .SUCCESS => return, - else => |err| return unexpectedErrno(err), - }; - if (@TypeOf(system.getrandom) != void) { - var buf = buffer; - const use_c = native_os != .linux or - std.c.versionCheck(if (builtin.abi.isAndroid()) .{ .major = 28, .minor = 0, .patch = 0 } else .{ .major = 2, .minor = 25, .patch = 0 }); - - while (buf.len != 0) { - const num_read: usize, const err = if (use_c) res: { - const rc = std.c.getrandom(buf.ptr, buf.len, 0); - break :res .{ @bitCast(rc), errno(rc) }; - } else res: { - const rc = linux.getrandom(buf.ptr, buf.len, 0); - break :res .{ rc, linux.errno(rc) }; - }; - - switch (err) { - .SUCCESS => buf = buf[num_read..], - .INVAL => unreachable, - .FAULT => unreachable, - .INTR => continue, - else => return unexpectedErrno(err), - } - } - return; - } - if (native_os == .emscripten) { - const err = errno(std.c.getentropy(buffer.ptr, buffer.len)); - switch (err) { - .SUCCESS => return, - else => return unexpectedErrno(err), - } - } - return getRandomBytesDevURandom(buffer); -} - -fn getRandomBytesDevURandom(buf: []u8) GetRandomError!void { - const fd = try openZ("/dev/urandom", .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); - defer close(fd); - - switch (native_os) { - .linux => { - var stx = std.mem.zeroes(linux.Statx); - const rc = linux.statx( - fd, - "", - linux.AT.EMPTY_PATH, - .{ .TYPE = true }, - &stx, - ); - switch (errno(rc)) { - .SUCCESS => {}, - .ACCES => unreachable, - .BADF => unreachable, - .FAULT => unreachable, - .INVAL => unreachable, - .LOOP => unreachable, - .NAMETOOLONG => unreachable, - .NOENT => unreachable, - .NOMEM => return error.SystemResources, - .NOTDIR => unreachable, - else => |err| return unexpectedErrno(err), - } - if (!S.ISCHR(stx.mode)) { - return error.NoDevice; - } - }, - else => { - const st = fstat(fd) catch |err| switch (err) { - error.Streaming => return error.NoDevice, - else => |e| return e, - }; - if (!S.ISCHR(st.mode)) { - return error.NoDevice; - } - }, - } - - var i: usize = 0; - while (i < buf.len) { - i += read(fd, buf[i..]) catch return error.Unexpected; - } -} - pub const RaiseError = UnexpectedError; pub fn raise(sig: SIG) RaiseError!void { diff --git a/lib/std/std.zig b/lib/std/std.zig @@ -137,12 +137,6 @@ pub const Options = struct { fmt_max_depth: usize = fmt.default_max_depth, - cryptoRandomSeed: fn (buffer: []u8) void = @import("crypto/tlcsprng.zig").defaultRandomSeed, - - crypto_always_getrandom: bool = false, - - crypto_fork_safety: bool = true, - /// By default, std.http.Client will support HTTPS connections. Set this option to `true` to /// disable TLS support. ///