authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-12-18 15:38:38-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-12-18 15:54:01-07:00
log53987c932c9d62cc9cdae3d523fb62756ce83ca9
tree280817d390ef900cf590c40f47cca4230b354b0e
parent2b8dcc76eba92ad44bd88756218de8d2bd8a1c10

std.crypto.random: introduce fork safety

Everybody gets what they want! * AT_RANDOM is completely ignored. * On Linux, MADV_WIPEONFORK is used to provide fork safety. * On pthread systems, `pthread_atfork` is used to provide fork safety. * For systems that do not have the capability to provide fork safety, the implementation falls back to calling getrandom() every time. * If madvise is unavailable or returns an error, or pthread_atfork fails for whatever reason, it falls back to calling getrandom() every time. * Applications may choose to opt-out of fork safety. * Applications may choose to opt-in to unconditionally calling getrandom() for every call to std.crypto.random.fillFn. * Added `std.meta.globalOption`. * Added `std.os.madvise` and related bits. * Bumped up the size of the main thread TLS buffer. See the comment there for justification. * Simpler hot path in TLS initialization.

10 files changed, 241 insertions(+), 71 deletions(-)

lib/std/c.zig+5
......@@ -264,6 +264,11 @@ pub extern "c" fn pthread_attr_setguardsize(attr: *pthread_attr_t, guardsize: us
264264pub extern "c" fn pthread_attr_destroy(attr: *pthread_attr_t) c_int;
265265pub extern "c" fn pthread_self() pthread_t;
266266pub extern "c" fn pthread_join(thread: pthread_t, arg_return: ?*?*c_void) c_int;
267pub extern "c" fn pthread_atfork(
268 prepare: ?fn () callconv(.C) void,
269 parent: ?fn () callconv(.C) void,
270 child: ?fn () callconv(.C) void,
271) c_int;
267272
268273pub extern "c" fn kqueue() c_int;
269274pub extern "c" fn kevent(
lib/std/c/linux.zig+6
......@@ -106,6 +106,12 @@ pub extern "c" fn prlimit(pid: pid_t, resource: rlimit_resource, new_limit: *con
106106pub extern "c" fn posix_memalign(memptr: *?*c_void, alignment: usize, size: usize) c_int;
107107pub extern "c" fn malloc_usable_size(?*const c_void) usize;
108108
109pub extern "c" fn madvise(
110 addr: *align(std.mem.page_size) c_void,
111 length: usize,
112 advice: c_uint,
113) c_int;
114
109115pub const pthread_attr_t = extern struct {
110116 __size: [56]u8,
111117 __align: c_long,
lib/std/crypto/tlcsprng.zig+123-29
......@@ -16,47 +16,141 @@ const mem = std.mem;
1616/// We use this as a layer of indirection because global const pointers cannot
1717/// point to thread-local variables.
1818pub var interface = std.rand.Random{ .fillFn = tlsCsprngFill };
19pub threadlocal var csprng_state: std.crypto.core.Gimli = undefined;
20pub threadlocal var csprng_state_initialized = false;
21fn tlsCsprngFill(r: *const std.rand.Random, buf: []u8) void {
19
20const os_has_fork = switch (std.Target.current.os.tag) {
21 .dragonfly,
22 .freebsd,
23 .ios,
24 .kfreebsd,
25 .linux,
26 .macos,
27 .netbsd,
28 .openbsd,
29 .solaris,
30 .tvos,
31 .watchos,
32 => true,
33
34 else => false,
35};
36const os_has_arc4random = std.builtin.link_libc and @hasDecl(std.c, "arc4random_buf");
37const want_fork_safety = os_has_fork and !os_has_arc4random and
38 (std.meta.globalOption("crypto_fork_safety", bool) orelse true);
39const maybe_have_wipe_on_fork = std.Target.current.os.isAtLeast(.linux, .{
40 .major = 4,
41 .minor = 14,
42}) orelse true;
43
44const WipeMe = struct {
45 init_state: enum { uninitialized, initialized, failed },
46 gimli: std.crypto.core.Gimli,
47};
48const wipe_align = if (maybe_have_wipe_on_fork) mem.page_size else @alignOf(WipeMe);
49
50threadlocal var wipe_me: WipeMe align(wipe_align) = .{
51 .gimli = undefined,
52 .init_state = .uninitialized,
53};
54
55fn tlsCsprngFill(_: *const std.rand.Random, buffer: []u8) void {
2256 if (std.builtin.link_libc and @hasDecl(std.c, "arc4random_buf")) {
2357 // arc4random is already a thread-local CSPRNG.
24 return std.c.arc4random_buf(buf.ptr, buf.len);
58 return std.c.arc4random_buf(buffer.ptr, buffer.len);
2559 }
26 if (!csprng_state_initialized) {
27 var seed: [seed_len]u8 = undefined;
28 // Because we panic on getrandom() failing, we provide the opportunity
29 // to override the default seed function. This also makes
30 // `std.crypto.random` available on freestanding targets, provided that
31 // the `cryptoRandomSeed` function is provided.
32 if (@hasDecl(root, "cryptoRandomSeed")) {
33 root.cryptoRandomSeed(&seed);
34 } else {
35 defaultSeed(&seed);
36 }
37 init(seed);
60 // Allow applications to decide they would prefer to have every call to
61 // std.crypto.random always make an OS syscall, rather than rely on an
62 // application implementation of a CSPRNG.
63 if (comptime std.meta.globalOption("crypto_always_getrandom", bool) orelse false) {
64 return fillWithOsEntropy(buffer);
3865 }
39 if (buf.len != 0) {
40 csprng_state.squeeze(buf);
66 switch (wipe_me.init_state) {
67 .uninitialized => {
68 if (want_fork_safety) {
69 if (maybe_have_wipe_on_fork) {
70 if (std.os.madvise(
71 @ptrCast([*]align(mem.page_size) u8, &wipe_me),
72 @sizeOf(@TypeOf(wipe_me)),
73 std.os.MADV_WIPEONFORK,
74 )) |_| {
75 return initAndFill(buffer);
76 } else |_| if (std.Thread.use_pthreads) {
77 return setupPthreadAtforkAndFill(buffer);
78 } else {
79 // Since we failed to set up fork safety, we fall back to always
80 // calling getrandom every time.
81 wipe_me.init_state = .failed;
82 return fillWithOsEntropy(buffer);
83 }
84 } else if (std.Thread.use_pthreads) {
85 return setupPthreadAtforkAndFill(buffer);
86 } else {
87 // We have no mechanism to provide fork safety, but we want fork safety,
88 // so we fall back to calling getrandom every time.
89 wipe_me.init_state = .failed;
90 return fillWithOsEntropy(buffer);
91 }
92 } else {
93 return initAndFill(buffer);
94 }
95 },
96 .initialized => {
97 return fillWithCsprng(buffer);
98 },
99 .failed => {
100 if (want_fork_safety) {
101 return fillWithOsEntropy(buffer);
102 } else {
103 unreachable;
104 }
105 },
106 }
107}
108
109fn setupPthreadAtforkAndFill(buffer: []u8) void {
110 const failed = std.c.pthread_atfork(null, null, childAtForkHandler) != 0;
111 if (failed) {
112 wipe_me.init_state = .failed;
113 return fillWithOsEntropy(buffer);
41114 } else {
42 csprng_state.permute();
115 return initAndFill(buffer);
43116 }
44 mem.set(u8, csprng_state.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
45117}
46118
47fn defaultSeed(buffer: *[seed_len]u8) void {
48 std.os.getrandom(buffer) catch @panic("getrandom() failed to seed thread-local CSPRNG");
119fn childAtForkHandler() callconv(.C) void {
120 const wipe_slice = @ptrCast([*]u8, &wipe_me)[0..@sizeOf(@TypeOf(wipe_me))];
121 std.crypto.utils.secureZero(u8, wipe_slice);
49122}
50123
51pub const seed_len = 16;
124fn fillWithCsprng(buffer: []u8) void {
125 if (buffer.len != 0) {
126 wipe_me.gimli.squeeze(buffer);
127 } else {
128 wipe_me.gimli.permute();
129 }
130 mem.set(u8, wipe_me.gimli.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
131}
132
133fn fillWithOsEntropy(buffer: []u8) void {
134 std.os.getrandom(buffer) catch @panic("getrandom() failed to provide entropy");
135}
52136
53pub fn init(seed: [seed_len]u8) void {
54 var initial_state: [std.crypto.core.Gimli.BLOCKBYTES]u8 = undefined;
55 mem.copy(u8, initial_state[0..seed_len], &seed);
56 mem.set(u8, initial_state[seed_len..], 0);
57 csprng_state = std.crypto.core.Gimli.init(initial_state);
137fn initAndFill(buffer: []u8) void {
138 var seed: [std.crypto.core.Gimli.BLOCKBYTES]u8 = undefined;
139 // Because we panic on getrandom() failing, we provide the opportunity
140 // to override the default seed function. This also makes
141 // `std.crypto.random` available on freestanding targets, provided that
142 // the `cryptoRandomSeed` function is provided.
143 if (@hasDecl(root, "cryptoRandomSeed")) {
144 root.cryptoRandomSeed(&seed);
145 } else {
146 fillWithOsEntropy(&seed);
147 }
148
149 wipe_me.gimli = std.crypto.core.Gimli.init(seed);
58150
59151 // This is at the end so that accidental recursive dependencies result
60152 // in stack overflows instead of invalid random data.
61 csprng_state_initialized = true;
153 wipe_me.init_state = .initialized;
154
155 return fillWithCsprng(buffer);
62156}
lib/std/meta.zig+8
......@@ -9,6 +9,7 @@ const debug = std.debug;
99const mem = std.mem;
1010const math = std.math;
1111const testing = std.testing;
12const root = @import("root");
1213
1314pub const trait = @import("meta/trait.zig");
1415pub const TrailerFlags = @import("meta/trailer_flags.zig").TrailerFlags;
......@@ -1085,3 +1086,10 @@ test "Tuple" {
10851086 TupleTester.assertTuple(.{ u32, f16 }, Tuple(&[_]type{ u32, f16 }));
10861087 TupleTester.assertTuple(.{ u32, f16, []const u8, void }, Tuple(&[_]type{ u32, f16, []const u8, void }));
10871088}
1089
1090/// TODO: https://github.com/ziglang/zig/issues/425
1091pub fn globalOption(comptime name: []const u8, comptime T: type) ?T {
1092 if (!@hasDecl(root, name))
1093 return null;
1094 return @as(T, @field(root, name));
1095}
lib/std/os.zig+48
......@@ -5845,3 +5845,51 @@ pub fn setrlimit(resource: rlimit_resource, limits: rlimit) SetrlimitError!void
58455845 else => |err| return unexpectedErrno(err),
58465846 }
58475847}
5848
5849pub const MadviseError = error{
5850 /// advice is MADV_REMOVE, but the specified address range is not a shared writable mapping.
5851 AccessDenied,
5852 /// advice is MADV_HWPOISON, but the caller does not have the CAP_SYS_ADMIN capability.
5853 PermissionDenied,
5854 /// A kernel resource was temporarily unavailable.
5855 SystemResources,
5856 /// One of the following:
5857 /// * addr is not page-aligned or length is negative
5858 /// * advice is not valid
5859 /// * advice is MADV_DONTNEED or MADV_REMOVE and the specified address range
5860 /// includes locked, Huge TLB pages, or VM_PFNMAP pages.
5861 /// * advice is MADV_MERGEABLE or MADV_UNMERGEABLE, but the kernel was not
5862 /// configured with CONFIG_KSM.
5863 /// * advice is MADV_FREE or MADV_WIPEONFORK but the specified address range
5864 /// includes file, Huge TLB, MAP_SHARED, or VM_PFNMAP ranges.
5865 InvalidSyscall,
5866 /// (for MADV_WILLNEED) Paging in this area would exceed the process's
5867 /// maximum resident set size.
5868 WouldExceedMaximumResidentSetSize,
5869 /// One of the following:
5870 /// * (for MADV_WILLNEED) Not enough memory: paging in failed.
5871 /// * Addresses in the specified range are not currently mapped, or
5872 /// are outside the address space of the process.
5873 OutOfMemory,
5874 /// The madvise syscall is not available on this version and configuration
5875 /// of the Linux kernel.
5876 MadviseUnavailable,
5877 /// The operating system returned an undocumented error code.
5878 Unexpected,
5879};
5880
5881/// Give advice about use of memory.
5882/// This syscall is optional and is sometimes configured to be disabled.
5883pub fn madvise(ptr: [*]align(mem.page_size) u8, length: usize, advice: u32) MadviseError!void {
5884 switch (errno(system.madvise(ptr, length, advice))) {
5885 0 => return,
5886 EACCES => return error.AccessDenied,
5887 EAGAIN => return error.SystemResources,
5888 EBADF => unreachable, // The map exists, but the area maps something that isn't a file.
5889 EINVAL => return error.InvalidSyscall,
5890 EIO => return error.WouldExceedMaximumResidentSetSize,
5891 ENOMEM => return error.OutOfMemory,
5892 ENOSYS => return error.MadviseUnavailable,
5893 else => |err| return unexpectedErrno(err),
5894 }
5895}
lib/std/os/bits/linux.zig+22
......@@ -2045,3 +2045,25 @@ pub const rlimit = extern struct {
20452045 /// Hard limit
20462046 max: rlim_t,
20472047};
2048
2049pub const MADV_NORMAL = 0;
2050pub const MADV_RANDOM = 1;
2051pub const MADV_SEQUENTIAL = 2;
2052pub const MADV_WILLNEED = 3;
2053pub const MADV_DONTNEED = 4;
2054pub const MADV_FREE = 8;
2055pub const MADV_REMOVE = 9;
2056pub const MADV_DONTFORK = 10;
2057pub const MADV_DOFORK = 11;
2058pub const MADV_MERGEABLE = 12;
2059pub const MADV_UNMERGEABLE = 13;
2060pub const MADV_HUGEPAGE = 14;
2061pub const MADV_NOHUGEPAGE = 15;
2062pub const MADV_DONTDUMP = 16;
2063pub const MADV_DODUMP = 17;
2064pub const MADV_WIPEONFORK = 18;
2065pub const MADV_KEEPONFORK = 19;
2066pub const MADV_COLD = 20;
2067pub const MADV_PAGEOUT = 21;
2068pub const MADV_HWPOISON = 100;
2069pub const MADV_SOFT_OFFLINE = 101;
lib/std/os/linux.zig+4
......@@ -1351,6 +1351,10 @@ pub fn prlimit(pid: pid_t, resource: rlimit_resource, new_limit: ?*const rlimit,
13511351 );
13521352}
13531353
1354pub fn madvise(address: [*]u8, len: usize, advice: u32) usize {
1355 return syscall3(.madvise, @ptrToInt(address), len, advice);
1356}
1357
13541358test "" {
13551359 if (builtin.os.tag == .linux) {
13561360 _ = @import("linux/test.zig");
lib/std/os/linux/tls.zig+24-15
......@@ -327,34 +327,43 @@ pub fn prepareTLS(area: []u8) usize {
327327 if (tls_tp_points_past_tcb) tls_image.data_offset else tls_image.tcb_offset;
328328}
329329
330var main_thread_tls_buffer: [256]u8 = undefined;
330// The main motivation for the size chosen here is this is how much ends up being
331// requested for the thread local variables of the std.crypto.random implementation.
332// I'm not sure why it ends up being so much; the struct itself is only 64 bytes.
333// I think it has to do with being page aligned and LLVM or LLD is not smart enough
334// to lay out the TLS data in a space conserving way. Anyway I think it's fine
335// because it's less than 3 pages of memory, and putting it in the ELF like this
336// is equivalent to moving the mmap call below into the kernel, avoiding syscall
337// overhead.
338var main_thread_tls_buffer: [0x2100]u8 align(mem.page_size) = undefined;
331339
332340pub fn initStaticTLS() void {
333341 initTLS();
334342
335 const alloc_tls_area: []u8 = blk: {
336 const full_alloc_size = tls_image.alloc_size + tls_image.alloc_align - 1;
337
343 const tls_area = blk: {
338344 // Fast path for the common case where the TLS data is really small,
339 // avoid an allocation and use our local buffer
340 if (full_alloc_size < main_thread_tls_buffer.len)
341 break :blk main_thread_tls_buffer[0..];
345 // avoid an allocation and use our local buffer.
346 if (tls_image.alloc_align <= mem.page_size and
347 tls_image.alloc_size <= main_thread_tls_buffer.len)
348 {
349 break :blk main_thread_tls_buffer[0..tls_image.alloc_size];
350 }
342351
343 break :blk os.mmap(
352 const alloc_tls_area = os.mmap(
344353 null,
345 full_alloc_size,
354 tls_image.alloc_size + tls_image.alloc_align - 1,
346355 os.PROT_READ | os.PROT_WRITE,
347356 os.MAP_PRIVATE | os.MAP_ANONYMOUS,
348357 -1,
349358 0,
350359 ) catch os.abort();
351 };
352360
353 // Make sure the slice is correctly aligned
354 const begin_addr = @ptrToInt(alloc_tls_area.ptr);
355 const begin_aligned_addr = mem.alignForward(begin_addr, tls_image.alloc_align);
356 const start = begin_aligned_addr - begin_addr;
357 const tls_area = alloc_tls_area[start .. start + tls_image.alloc_size];
361 // Make sure the slice is correctly aligned.
362 const begin_addr = @ptrToInt(alloc_tls_area.ptr);
363 const begin_aligned_addr = mem.alignForward(begin_addr, tls_image.alloc_align);
364 const start = begin_aligned_addr - begin_addr;
365 break :blk alloc_tls_area[start .. start + tls_image.alloc_size];
366 };
358367
359368 const tp_value = prepareTLS(tls_area);
360369 setThreadPointer(tp_value);
lib/std/start.zig-26
......@@ -216,12 +216,6 @@ fn posixCallMainAndExit() noreturn {
216216 std.os.linux.tls.initStaticTLS();
217217 }
218218
219 if (!@hasDecl(root, "use_AT_RANDOM_auxval") or root.use_AT_RANDOM_auxval) {
220 // Initialize the per-thread CSPRNG since Linux gave us the handy-dandy
221 // AT_RANDOM. This depends on the TLS initialization above.
222 initCryptoSeedFromAuxVal(std.os.linux.getauxval(std.elf.AT_RANDOM));
223 }
224
225219 // TODO This is disabled because what should we do when linking libc and this code
226220 // does not execute? And also it's causing a test failure in stack traces in release modes.
227221
......@@ -257,32 +251,12 @@ fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [][*:0]u8) u8 {
257251}
258252
259253fn main(c_argc: i32, c_argv: [*][*:0]u8, c_envp: [*:null]?[*:0]u8) callconv(.C) i32 {
260 // By default, we do not attempt to initialize tlcsprng from AT_RANDOM here because
261 // libc owns the start code, not us, and therefore libc owns the random bytes
262 // from AT_RANDOM.
263 if (builtin.os.tag == .linux and
264 @hasDecl(root, "use_AT_RANDOM_auxval") and
265 root.use_AT_RANDOM_auxval)
266 {
267 initCryptoSeedFromAuxVal(std.c.getauxval(std.elf.AT_RANDOM));
268 }
269254 var env_count: usize = 0;
270255 while (c_envp[env_count] != null) : (env_count += 1) {}
271256 const envp = @ptrCast([*][*:0]u8, c_envp)[0..env_count];
272257 return @call(.{ .modifier = .always_inline }, callMainWithArgs, .{ @intCast(usize, c_argc), c_argv, envp });
273258}
274259
275fn initCryptoSeedFromAuxVal(addr: usize) void {
276 if (addr == 0) return;
277 // "The address of sixteen bytes containing a random value."
278 const ptr = @intToPtr(*[16]u8, addr);
279 tlcsprng.init(ptr.*);
280 // Clear AT_RANDOM after we use it, otherwise our secure
281 // seed is sitting in memory ready for some other code in the
282 // program to reuse, and hence break our security.
283 std.crypto.utils.secureZero(u8, ptr);
284}
285
286260// General error message for a malformed return type
287261const bad_main_ret = "expected return type of main to be 'void', '!void', 'noreturn', 'u8', or '!u8'";
288262
test/stack_traces.zig+1-1
......@@ -282,7 +282,7 @@ pub fn addCases(cases: *tests.StackTracesContext) void {
282282 \\source.zig:10:8: [address] in main (test)
283283 \\ foo();
284284 \\ ^
285 \\start.zig:377:29: [address] in std.start.posixCallMainAndExit (test)
285 \\start.zig:342:29: [address] in std.start.posixCallMainAndExit (test)
286286 \\ return root.main();
287287 \\ ^
288288 \\start.zig:163:5: [address] in std.start._start (test)