Commit b7eab32f42

LemonBoy <thatlemon@gmail.com>
2021-05-16 11:51:39
std: Allocate tlscsprng memory as needed
Let mmap allocate a block of memory that's wide enough to use with MADV_WIPEONFORK, madvise granularity is the current system page size (using a static buffer of mem.page_size bytes would be wrong, that's the minimum page size). As a result, we don't zero some random chunk of memory every time we fork the process. Fixes #7609
1 parent fe1a166
Changed files (1)
lib
std
lib/std/crypto/tlcsprng.zig
@@ -12,6 +12,7 @@
 const std = @import("std");
 const root = @import("root");
 const mem = std.mem;
+const os = std.os;
 
 /// We use this as a layer of indirection because global const pointers cannot
 /// point to thread-local variables.
@@ -42,16 +43,12 @@ const maybe_have_wipe_on_fork = std.Target.current.os.isAtLeast(.linux, .{
     .minor = 14,
 }) orelse true;
 
-const WipeMe = struct {
-    init_state: enum { uninitialized, initialized, failed },
+const Context = struct {
+    init_state: enum(u8) { uninitialized = 0, initialized, failed },
     gimli: std.crypto.core.Gimli,
 };
-const wipe_align = if (maybe_have_wipe_on_fork) mem.page_size else @alignOf(WipeMe);
 
-threadlocal var wipe_me: WipeMe align(wipe_align) = .{
-    .gimli = undefined,
-    .init_state = .uninitialized,
-};
+threadlocal var wipe_mem: []align(mem.page_size) u8 = &[_]u8{};
 
 fn tlsCsprngFill(_: *const std.rand.Random, buffer: []u8) void {
     if (std.builtin.link_libc and @hasDecl(std.c, "arc4random_buf")) {
@@ -64,35 +61,69 @@ fn tlsCsprngFill(_: *const std.rand.Random, buffer: []u8) void {
     if (comptime std.meta.globalOption("crypto_always_getrandom", bool) orelse false) {
         return fillWithOsEntropy(buffer);
     }
-    switch (wipe_me.init_state) {
+
+    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 = os.mmap(
+                null,
+                @sizeOf(Context),
+                os.PROT_READ | os.PROT_WRITE,
+                os.MAP_PRIVATE | os.MAP_ANONYMOUS,
+                -1,
+                0,
+            ) catch |err| {
+                // Could not allocate memory for the local state, fall back to
+                // the OS syscall.
+                return fillWithOsEntropy(buffer);
+            };
+            // The memory is already zero-initialized.
+        } else {
+            // Use a static thread-local buffer.
+            const S = struct {
+                threadlocal var buf: Context align(mem.page_size) = .{
+                    .init_state = .uninitialized,
+                    .gimli = undefined,
+                };
+            };
+            wipe_mem = mem.asBytes(&S.buf);
+        }
+    }
+    const ctx = @ptrCast(*Context, wipe_mem.ptr);
+
+    switch (ctx.init_state) {
         .uninitialized => {
-            if (want_fork_safety) {
-                if (maybe_have_wipe_on_fork) {
-                    if (std.os.madvise(
-                        @ptrCast([*]align(mem.page_size) u8, &wipe_me),
-                        @sizeOf(@TypeOf(wipe_me)),
-                        std.os.MADV_WIPEONFORK,
-                    )) |_| {
-                        return initAndFill(buffer);
-                    } else |_| if (std.Thread.use_pthreads) {
-                        return setupPthreadAtforkAndFill(buffer);
-                    } else {
-                        // Since we failed to set up fork safety, we fall back to always
-                        // calling getrandom every time.
-                        wipe_me.init_state = .failed;
-                        return fillWithOsEntropy(buffer);
-                    }
-                } else if (std.Thread.use_pthreads) {
-                    return setupPthreadAtforkAndFill(buffer);
-                } else {
-                    // We have no mechanism to provide fork safety, but we want fork safety,
-                    // so we fall back to calling getrandom every time.
-                    wipe_me.init_state = .failed;
-                    return fillWithOsEntropy(buffer);
-                }
-            } else {
+            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 (os.madvise(wipe_mem.ptr, 0, 0xffffffff)) |_| {
+                    break :wof;
+                } else |_| {}
+
+                os.madvise(
+                    wipe_mem.ptr,
+                    wipe_mem.len,
+                    os.MADV_WIPEONFORK,
+                ) catch |_| {
+                    return initAndFill(buffer);
+                };
+            }
+
+            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 fillWithOsEntropy(buffer);
         },
         .initialized => {
             return fillWithCsprng(buffer);
@@ -110,7 +141,8 @@ fn tlsCsprngFill(_: *const std.rand.Random, buffer: []u8) void {
 fn setupPthreadAtforkAndFill(buffer: []u8) void {
     const failed = std.c.pthread_atfork(null, null, childAtForkHandler) != 0;
     if (failed) {
-        wipe_me.init_state = .failed;
+        const ctx = @ptrCast(*Context, wipe_mem.ptr);
+        ctx.init_state = .failed;
         return fillWithOsEntropy(buffer);
     } else {
         return initAndFill(buffer);
@@ -118,21 +150,21 @@ fn setupPthreadAtforkAndFill(buffer: []u8) void {
 }
 
 fn childAtForkHandler() callconv(.C) void {
-    const wipe_slice = @ptrCast([*]u8, &wipe_me)[0..@sizeOf(@TypeOf(wipe_me))];
-    std.crypto.utils.secureZero(u8, wipe_slice);
+    std.crypto.utils.secureZero(u8, wipe_mem);
 }
 
 fn fillWithCsprng(buffer: []u8) void {
+    const ctx = @ptrCast(*Context, wipe_mem.ptr);
     if (buffer.len != 0) {
-        wipe_me.gimli.squeeze(buffer);
+        ctx.gimli.squeeze(buffer);
     } else {
-        wipe_me.gimli.permute();
+        ctx.gimli.permute();
     }
-    mem.set(u8, wipe_me.gimli.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
+    mem.set(u8, ctx.gimli.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
 }
 
 fn fillWithOsEntropy(buffer: []u8) void {
-    std.os.getrandom(buffer) catch @panic("getrandom() failed to provide entropy");
+    os.getrandom(buffer) catch @panic("getrandom() failed to provide entropy");
 }
 
 fn initAndFill(buffer: []u8) void {
@@ -147,11 +179,12 @@ fn initAndFill(buffer: []u8) void {
         fillWithOsEntropy(&seed);
     }
 
-    wipe_me.gimli = std.crypto.core.Gimli.init(seed);
+    const ctx = @ptrCast(*Context, wipe_mem.ptr);
+    ctx.gimli = std.crypto.core.Gimli.init(seed);
 
     // This is at the end so that accidental recursive dependencies result
     // in stack overflows instead of invalid random data.
-    wipe_me.init_state = .initialized;
+    ctx.init_state = .initialized;
 
     return fillWithCsprng(buffer);
 }