Commit 73ee434c8c

Ben Noordhuis <info@bnoordhuis.nl>
2018-02-04 00:51:21
Use /dev/urandom and sysctl(RANDOM_UUID) on Linux.
Add fallback paths for when the getrandom(2) system call is not available. Try /dev/urandom first and sysctl(RANDOM_UUID) second. The sysctl issues a warning in the system logs with some kernels but that seems like an acceptable tradeoff for the fallback of a fallback.
1 parent 15eb28e
std/os/index.zig
@@ -78,17 +78,8 @@ error WouldBlock;
 pub fn getRandomBytes(buf: []u8) %void {
     switch (builtin.os) {
         Os.linux => while (true) {
-            // TODO check libc version and potentially call c.getrandom.
-            // See #397
-            const err = posix.getErrno(posix.getrandom(buf.ptr, buf.len, 0));
-            if (err > 0) {
-                return switch (err) {
-                    posix.EINVAL => unreachable,
-                    posix.EFAULT => unreachable,
-                    posix.EINTR  => continue,
-                    else         => unexpectedErrorPosix(err),
-                };
-            }
+            const err = posix.getErrno(posix.getRandomBytes(buf));
+            if (err > 0) return unexpectedErrorPosix(err);
             return;
         },
         Os.macosx, Os.ios => {
std/os/linux.zig
@@ -7,6 +7,7 @@ const arch = switch (builtin.arch) {
     else => @compileError("unsupported arch"),
 };
 pub use @import("linux_errno.zig");
+pub use @import("linux_random.zig");
 
 pub const PATH_MAX = 4096;
 
std/os/linux_random.zig
@@ -0,0 +1,246 @@
+const std = @import("../index.zig");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const linux = std.os.linux;
+const math = std.math;
+const mem = std.mem;
+const os = std.os;
+
+use @import("linux_errno.zig");
+
+const arch = switch (builtin.arch) {
+    builtin.Arch.x86_64 => @import("linux_x86_64.zig"),
+    builtin.Arch.i386 => @import("linux_i386.zig"),
+    else => @compileError("unsupported arch"),
+};
+
+const Method = enum {
+    Syscall,
+    Sysctl,
+    Urandom,
+};
+
+const Callback = fn(&i32, []u8) usize;
+
+const Context = struct {
+    syscall: Callback,
+    sysctl: Callback,
+    urandom: Callback,
+};
+
+pub fn getRandomBytes(buf: []u8) usize {
+    const ctx = Context {
+        .syscall = syscall,
+        .sysctl = sysctl,
+        .urandom = urandom,
+    };
+    return withContext(ctx, buf);
+}
+
+fn withContext(comptime ctx: Context, buf: []u8) usize {
+    if (buf.len == 0) return 0;
+
+    var fd: i32 = -1;
+    defer if (fd != -1) {
+        const _ = linux.close(fd);  // Ignore errors, can't do anything sensible.
+    };
+
+    // TODO(bnoordhuis) Remember the method across invocations so we don't make
+    // unnecessary system calls that are going to fail with ENOSYS anyway.
+    var method = Method.Syscall;
+    var i: usize = 0;
+    while (i < buf.len) {
+        const rc = switch (method) {
+            Method.Syscall => ctx.syscall(&fd, buf[i..]),
+            Method.Sysctl => ctx.sysctl(&fd, buf[i..]),
+            Method.Urandom => ctx.urandom(&fd, buf[i..]),
+        };
+        if (rc == 0) return usize(-EIO);  // Can't really happen.
+        if (!isErr(rc)) {
+            i += rc;
+            continue;
+        }
+        if (rc == usize(-EINTR)) continue;
+        if (rc == usize(-ENOSYS) and method == Method.Syscall) {
+            method = Method.Urandom;
+            continue;
+        }
+        if (method == Method.Urandom) {
+            method = Method.Sysctl;
+            continue;
+        }
+        return rc;  // Unexpected error.
+    }
+
+    return i;
+}
+
+fn syscall(_: &i32, buf: []u8) usize {
+    return arch.syscall3(arch.SYS_getrandom, @ptrToInt(&buf[0]), buf.len, 0);
+}
+
+// Note: reads only 14 bytes at a time.
+fn sysctl(_: &i32, buf: []u8) usize {
+    const __sysctl_args = extern struct {
+        name: &c_int,
+        nlen: c_int,
+        oldval: &u8,
+        oldlenp: &usize,
+        newval: ?&u8,
+        newlen: usize,
+        unused: [4]usize,
+    };
+
+    var name = [3]c_int { 1, 40, 6 };  // { CTL_KERN, KERN_RANDOM, RANDOM_UUID }
+    var uuid: [16]u8 = undefined;
+
+    const expected: usize = @sizeOf(@typeOf(uuid));
+    var len = expected;
+
+    var args = __sysctl_args {
+        .name = &name[0],
+        .nlen = c_int(name.len),
+        .oldval = &uuid[0],
+        .oldlenp = &len,
+        .newval = null,
+        .newlen = 0,
+        .unused = []usize {0} ** 4,
+    };
+
+    const rc = arch.syscall1(arch.SYS__sysctl, @ptrToInt(&args));
+    if (rc != 0) return rc;
+    if (len != expected) return 0;  // Can't happen.
+
+    // uuid[] is now a type 4 UUID; bytes 6 and 8 (counting from zero)
+    // contain 4 and 5 bits of entropy, respectively.  For ease of use,
+    // we skip those and only use 14 of the 16 bytes.
+    uuid[6] = uuid[14];
+    uuid[8] = uuid[15];
+
+    const n = math.min(buf.len, usize(14));
+    @memcpy(&buf[0], &uuid[0], n);
+    return n;
+}
+
+fn urandom(fd: &i32, buf: []u8) usize {
+    if (*fd == -1) {
+        const flags = linux.O_CLOEXEC|linux.O_RDONLY;
+        const rc = linux.open(c"/dev/urandom", flags, 0);
+        if (isErr(rc)) return rc;
+        *fd = i32(rc);
+    }
+    // read() doesn't like reads > INT_MAX.
+    const n = math.min(buf.len, usize(0x7FFFFFFF));
+    return linux.read(*fd, &buf[0], n);
+}
+
+fn isErr(rc: usize) bool {
+    return rc > usize(-4096);
+}
+
+test "os.linux.getRandomBytes" {
+    try check(42, getRandomBytesTrampoline);
+}
+
+test "os.linux.getRandomBytes syscall" {
+    try check(42, syscall);
+}
+
+test "os.linux.getRandomBytes sysctl" {
+    try check(14, sysctl);
+}
+
+test "os.linux.getRandomBytes /dev/urandom" {
+    try check(42, urandom);
+}
+
+test "os.linux.getRandomBytes state machine" {
+    const ctx = Context {
+        .syscall = fortytwo,
+        .urandom = fail,
+        .sysctl = fail,
+    };
+    var buf = []u8 {0};
+    assert(1 == withContext(ctx, buf[0..]));
+    assert(42 == buf[0]);
+}
+
+test "os.linux.getRandomBytes no-syscall state machine" {
+    const ctx = Context {
+        .syscall = enosys,
+        .urandom = fortytwo,
+        .sysctl = fail,
+    };
+    var buf = []u8 {0};
+    assert(1 == withContext(ctx, buf[0..]));
+    assert(42 == buf[0]);
+}
+
+test "os.linux.getRandomBytes no-urandom state machine" {
+    const ctx = Context {
+        .syscall = enosys,
+        .urandom = einval,
+        .sysctl = fortytwo,
+    };
+    var buf = []u8 {0};
+    assert(1 == withContext(ctx, buf[0..]));
+    assert(42 == buf[0]);
+}
+
+test "os.linux.getRandomBytes no-sysctl state machine" {
+    const ctx = Context {
+        .syscall = enosys,
+        .urandom = einval,
+        .sysctl = einval,
+    };
+    var buf = []u8 {0};
+    assert(usize(-EINVAL) == withContext(ctx, buf[0..]));
+    assert(0 == buf[0]);
+}
+
+fn einval(_: &i32, buf: []u8) usize {
+    return usize(-EINVAL);
+}
+
+fn enosys(_: &i32, buf: []u8) usize {
+    return usize(-ENOSYS);
+}
+
+fn fail(_: &i32, buf: []u8) usize {
+    os.abort();
+}
+
+fn fortytwo(_: &i32, buf: []u8) usize {
+    assert(buf.len == 1);
+    buf[0] = 42;
+    return 1;
+}
+
+fn check(comptime N: usize, cb: Callback) %void {
+    if (builtin.os == builtin.Os.linux) {
+        var fd: i32 = -1;
+        defer if (fd != -1) {
+            const _ = linux.close(fd);  // Ignore errors, can't do anything sensible.
+        };
+
+        var bufs = [3][N]u8 {
+            []u8 {0} ** N,
+            []u8 {0} ** N,
+            []u8 {0} ** N,
+        };
+
+        for (bufs) |*buf| {
+            const err = cb(&fd, (*buf)[0..]);
+            assert(err == N);
+        }
+
+        for (bufs) |*a|
+            for (bufs) |*b|
+                if (a != b)
+                    assert(!mem.eql(u8, *a, *b));
+    }
+}
+
+fn getRandomBytesTrampoline(_: &i32, buf: []u8) usize {
+    return getRandomBytes(buf);
+}
CMakeLists.txt
@@ -441,6 +441,7 @@ set(ZIG_STD_FILES
     "os/index.zig"
     "os/linux.zig"
     "os/linux_errno.zig"
+    "os/linux_random.zig"
     "os/linux_i386.zig"
     "os/linux_x86_64.zig"
     "os/path.zig"