diff options
-rw-r--r-- | src/bun.js/rare_data.zig | 60 | ||||
-rw-r--r-- | src/bun.js/uuid.zig | 24 | ||||
-rw-r--r-- | src/bun.js/webcore.zig | 52 | ||||
-rw-r--r-- | src/deps/boringssl.translated.zig | 13 | ||||
-rw-r--r-- | src/global.zig | 8 | ||||
-rw-r--r-- | src/http/websocket_http_client.zig | 3 | ||||
-rw-r--r-- | test/bun.js/web-globals.test.js | 28 |
7 files changed, 158 insertions, 30 deletions
diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index cfca61ce0..8237d99a9 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -7,6 +7,7 @@ const Syscall = @import("./node/syscall.zig"); const JSC = @import("javascript_core"); const std = @import("std"); const BoringSSL = @import("boringssl"); +const bun = @import("../global.zig"); const WebSocketClientMask = @import("../http/websocket_http_client.zig").Mask; boring_ssl_engine: ?*BoringSSL.ENGINE = null, @@ -16,11 +17,70 @@ stdin_store: ?*Blob.Store = null, stdout_store: ?*Blob.Store = null, websocket_mask: WebSocketClientMask = WebSocketClientMask{}, +uuid_entropy_cache: ?*EntropyCache = null, + // TODO: make this per JSGlobalObject instead of global // This does not handle ShadowRealm correctly! tail_cleanup_hook: ?*CleanupHook = null, cleanup_hook: ?*CleanupHook = null, +pub fn nextUUID(this: *RareData) [16]u8 { + if (this.uuid_entropy_cache == null) { + this.uuid_entropy_cache = default_allocator.create(EntropyCache) catch unreachable; + this.uuid_entropy_cache.?.init(); + } + + return this.uuid_entropy_cache.?.get(); +} + +pub fn entropySlice(this: *RareData, len: usize) []u8 { + if (this.uuid_entropy_cache == null) { + this.uuid_entropy_cache = default_allocator.create(EntropyCache) catch unreachable; + this.uuid_entropy_cache.?.init(); + } + + return this.uuid_entropy_cache.?.slice(len); +} + +pub const EntropyCache = struct { + pub const buffered_uuids_count = 16; + pub const size = buffered_uuids_count * 128; + + cache: [size]u8 = undefined, + index: usize = 0, + + pub fn init(instance: *EntropyCache) void { + instance.fill(); + } + + pub fn fill(this: *EntropyCache) void { + bun.rand(&this.cache); + this.index = 0; + } + + pub fn slice(this: *EntropyCache, len: usize) []u8 { + if (len > this.cache.len) { + return &[_]u8{}; + } + + if (this.index + len > this.cache.len) { + this.fill(); + } + const result = this.cache[this.index..][0..len]; + this.index += len; + return result; + } + + pub fn get(this: *EntropyCache) [16]u8 { + if (this.index + 16 > this.cache.len) { + this.fill(); + } + const result = this.cache[this.index..][0..16].*; + this.index += 16; + return result; + } +}; + pub const CleanupHook = struct { next: ?*CleanupHook = null, ctx: ?*anyopaque, diff --git a/src/bun.js/uuid.zig b/src/bun.js/uuid.zig index 740309396..fa59520c2 100644 --- a/src/bun.js/uuid.zig +++ b/src/bun.js/uuid.zig @@ -3,16 +3,17 @@ const std = @import("std"); const crypto = std.crypto; const fmt = std.fmt; const testing = std.testing; +const bun = @import("../global.zig"); pub const Error = error{InvalidUUID}; const UUID = @This(); -bytes: [16]u8, +bytes: [16]u8 = undefined, pub fn init() UUID { var uuid = UUID{ .bytes = undefined }; - crypto.random.bytes(&uuid.bytes); + bun.rand(&uuid.bytes); // Version 4 uuid.bytes[6] = (uuid.bytes[6] & 0x0f) | 0x40; // Variant 1 @@ -67,22 +68,29 @@ pub fn format( ) !void { _ = options; // currently unused - if (layout.len != 0 and layout[0] != 's') + if (comptime layout.len != 0 and layout[0] != 's') @compileError("Unsupported format specifier for UUID type: '" ++ layout ++ "'."); - var buf: [36]u8 = undefined; + self.print(&buf); + + try fmt.format(writer, "{s}", .{buf}); +} + +pub fn print( + self: UUID, + buf: *[36]u8, +) void { const hex = "0123456789abcdef"; + const bytes = self.bytes; buf[8] = '-'; buf[13] = '-'; buf[18] = '-'; buf[23] = '-'; inline for (encoded_pos) |i, j| { - buf[i + 0] = hex[self.bytes[j] >> 4]; - buf[i + 1] = hex[self.bytes[j] & 0x0f]; + buf[comptime i + 0] = hex[bytes[j] >> 4]; + buf[comptime i + 1] = hex[bytes[j] & 0x0f]; } - - try fmt.format(writer, "{s}", .{buf}); } pub fn parse(buf: []const u8) Error!UUID { diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index ac5238dac..4d06003b5 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -358,7 +358,7 @@ pub const Prompt = struct { pub const Crypto = struct { const UUID = @import("./uuid.zig"); - + const BoringSSL = @import("boringssl"); pub const Class = JSC.NewClass(void, .{ .name = "crypto" }, .{ .getRandomValues = JSC.DOMCall("Crypto", @This(), "getRandomValues", JSC.JSValue, JSC.DOMEffect.top), .randomUUID = JSC.DOMCall("Crypto", @This(), "randomUUID", *JSC.JSString, JSC.DOMEffect.top), @@ -389,20 +389,40 @@ pub const Crypto = struct { return JSC.JSValue.jsUndefined(); }; var slice = array_buffer.byteSlice(); - if (slice.len > 0) - std.crypto.random.bytes(slice); + + switch (slice.len) { + 0 => {}, + 1...JSC.RareData.EntropyCache.size / 8 => { + if (arguments.len > 1) { + bun.rand(slice); + } else { + std.mem.copy(u8, slice, globalThis.bunVM().rareData().entropySlice(slice.len)); + } + }, + else => { + bun.rand(slice); + }, + } return arguments[0]; } pub fn getRandomValuesWithoutTypeChecks( - _: *JSC.JSGlobalObject, + globalThis: *JSC.JSGlobalObject, _: *anyopaque, array: *JSC.JSUint8Array, ) callconv(.C) JSC.JSValue { var slice = array.slice(); - if (slice.len > 0) - std.crypto.random.bytes(slice); + switch (slice.len) { + 0 => {}, + // 512 bytes or less we reuse from the same cache as UUID generation. + 1...JSC.RareData.EntropyCache.size / 8 => { + std.mem.copy(u8, slice, globalThis.bunVM().rareData().entropySlice(slice.len)); + }, + else => { + bun.rand(slice); + }, + } return @intToEnum(JSC.JSValue, @bitCast(i64, @ptrToInt(array))); } @@ -412,20 +432,24 @@ pub const Crypto = struct { _: JSC.JSValue, _: []const JSC.JSValue, ) JSC.JSValue { - var uuid = UUID.init(); - var out: [128]u8 = undefined; - var str = std.fmt.bufPrint(&out, "{s}", .{uuid}) catch unreachable; - return JSC.ZigString.init(str).toValueGC(globalThis); + var out: [36]u8 = undefined; + const uuid: UUID = .{ + .bytes = globalThis.bunVM().rareData().nextUUID(), + }; + uuid.print(&out); + return JSC.ZigString.init(&out).toValueGC(globalThis); } pub fn randomUUIDWithoutTypeChecks( globalThis: *JSC.JSGlobalObject, _: *anyopaque, ) callconv(.C) JSC.JSValue { - var uuid = UUID.init(); - var out: [128]u8 = undefined; - var str = std.fmt.bufPrint(&out, "{s}", .{uuid}) catch unreachable; - return JSC.ZigString.init(str).toValueGC(globalThis); + var out: [36]u8 = undefined; + const uuid: UUID = .{ + .bytes = globalThis.bunVM().rareData().nextUUID(), + }; + uuid.print(&out); + return JSC.ZigString.init(&out).toValueGC(globalThis); } pub fn call( diff --git a/src/deps/boringssl.translated.zig b/src/deps/boringssl.translated.zig index 4561df8ca..7b5f55a27 100644 --- a/src/deps/boringssl.translated.zig +++ b/src/deps/boringssl.translated.zig @@ -18618,6 +18618,19 @@ pub const ssl_comp_st = struct_ssl_comp_st; pub const stack_st_SSL_COMP = struct_stack_st_SSL_COMP; pub const ssl_conf_ctx_st = struct_ssl_conf_ctx_st; +pub extern fn RAND_bytes(buf: [*]u8, len: usize) c_int; + +/// RAND_enable_fork_unsafe_buffering enables efficient buffered reading of +/// /dev/urandom. It adds an overhead of a few KB of memory per thread. It must +/// be called before the first call to |RAND_bytes|. +/// +/// |fd| must be -1. We no longer support setting the file descriptor with this +/// function. +/// +/// It has an unusual name because the buffer is unsafe across calls to |fork|. +/// Hence, this function should never be called by libraries. +pub extern fn RAND_enable_fork_unsafe_buffering(c_int) void; + // Manual modification pub const struct_bio_st = extern struct { diff --git a/src/global.zig b/src/global.zig index 9d68753ea..5db2de0b6 100644 --- a/src/global.zig +++ b/src/global.zig @@ -319,3 +319,11 @@ pub const LinearFifo = @import("./linear_fifo.zig").LinearFifo; pub fn hash(content: []const u8) u64 { return std.hash.Wyhash.hash(0, content); } + +pub const HiveArray = @import("./hive_array.zig").HiveArray; + +pub fn rand(bytes: []u8) void { + const BoringSSL = @import("boringssl"); + if (bytes.len > 512) + _ = BoringSSL.RAND_bytes(bytes.ptr, bytes.len); +} diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index f380b38e1..cba0b2cae 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -24,8 +24,7 @@ const Opcode = @import("./websocket.zig").Opcode; fn buildRequestBody(vm: *JSC.VirtualMachine, pathname: *const JSC.ZigString, host: *const JSC.ZigString, client_protocol: *const JSC.ZigString, client_protocol_hash: *u64) std.mem.Allocator.Error![]u8 { const allocator = vm.allocator; - var input_rand_buf: [16]u8 = undefined; - std.crypto.random.bytes(&input_rand_buf); + const input_rand_buf = vm.rareData().nextUUID(); const temp_buf_size = comptime std.base64.standard.Encoder.calcSize(16); var encoded_buf: [temp_buf_size]u8 = undefined; const accept_key = std.base64.standard.Encoder.encode(&encoded_buf, &input_rand_buf); diff --git a/test/bun.js/web-globals.test.js b/test/bun.js/web-globals.test.js index 178f5dd00..c5740b0fc 100644 --- a/test/bun.js/web-globals.test.js +++ b/test/bun.js/web-globals.test.js @@ -48,18 +48,34 @@ test("MessageEvent", () => { it("crypto.getRandomValues", () => { var foo = new Uint8Array(32); - // run it once - var array = crypto.getRandomValues(foo); - expect(array).toBe(foo); - expect(array.reduce((sum, a) => (sum += a === 0), 0) != foo.length).toBe( - true - ); + // run it once buffered and unbuffered + { + var array = crypto.getRandomValues(foo); + expect(array).toBe(foo); + expect(array.reduce((sum, a) => (sum += a === 0), 0) != foo.length).toBe( + true + ); + } // run it again to check that the fast path works for (var i = 0; i < 9000; i++) { var array = crypto.getRandomValues(foo); expect(array).toBe(foo); } + + // run it on a large input + expect( + !!crypto.getRandomValues(new Uint8Array(8096)).find((a) => a > 0) + ).toBe(true); + + { + // any additional input into getRandomValues() makes it unbuffered + var array = crypto.getRandomValues(foo, "unbuffered"); + expect(array).toBe(foo); + expect(array.reduce((sum, a) => (sum += a === 0), 0) != foo.length).toBe( + true + ); + } }); it("crypto.randomUUID", () => { |