Skip to content
Open
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// -lto variants built with ThinLTO (per-module summaries for cross-language
// importing), and the Windows ICU data table filtered + per-item zstd
// compressed (lazily decompressed via bun_icu_decompress.cpp).
export const WEBKIT_VERSION = "4895f45dfbd0d1226c4d41799887bc0ecb9f341b";
export const WEBKIT_VERSION = "722f2a8a1a3da159a89b35730c5460a6ef58f0af";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
26 changes: 6 additions & 20 deletions src/runtime/node/node_crypto_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ pub mod random {
use super::*;
use crate::node::util::validators;
use bun_core::String as BunString;
use bun_jsc::{JSType, StringJsc as _, UUID, UUID7};
use bun_jsc::{JSType, StringJsc as _, UUID};

#[bun_jsc::host_fn]
pub(crate) fn random_int(
Expand Down Expand Up @@ -517,25 +517,11 @@ pub mod random {
}
}

// jsDateNow() is exactly what JS Date.now() returns, so the embedded
// timestamp is never behind a Date.now() sample taken by the caller.
let now_ms = global.js_date_now().max(0.0) as u64;
let mut entropy = [0u8; 10];
if disable_entropy_cache {
boringssl::rand_bytes(&mut entropy);
} else {
entropy
.copy_from_slice(&global.bun_vm().as_mut().rare_data().entropy_slice(10)[..10]);
}
let uuid = UUID7::init(now_ms, entropy);

let (mut str, bytes) = BunString::create_uninitialized_latin1(36);
uuid.print(
(&mut bytes[..36])
.try_into()
.expect("infallible: size matches"),
);
str.transfer_to_js(global)
// Same implementation as Bun.randomUUIDv7()'s default path; only
// the option validation above differs.
let timestamp = global.js_date_now().max(0.0) as u64;
let uuid = crate::webcore::crypto::uuid_v7_at(global, timestamp, disable_entropy_cache);
crate::webcore::crypto::uuid_v7_to_hex_js(global, &uuid)
}

pub(crate) fn assert_offset(
Expand Down
5 changes: 2 additions & 3 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5710,9 +5710,8 @@ pub fn jsdom_file_construct_(
}

if !set_last_modified {
// `lastModified` should be the current date in milliseconds if unspecified.
blob.last_modified
.set(bun_core::time::milli_timestamp() as f64);
// File API spec: default is "the equivalent of Date.now()".
blob.last_modified.set(global_this.js_date_now());
Comment thread
robobun marked this conversation as resolved.
}

if blob.content_type_slice().is_empty() {
Expand Down
46 changes: 34 additions & 12 deletions src/runtime/webcore/Crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,27 +247,49 @@ pub(crate) fn bun_random_uuid_v7(
.unwrap();
}

break 'brk u64::try_from(bun_core::time::milli_timestamp().max(0)).expect("int cast");
// js_date_now() is exactly Date.now() (same precise clock + any
// setSystemTime() override), so the input timestamp is never behind a
// caller's Date.now() sample. UUID7::init may bump it on rollover.
break 'brk global.js_date_now().max(0.0) as u64;
};

// SAFETY: `bun_vm()` never returns null for a Bun-owned global.
let entropy = global.bun_vm().as_mut().rare_data().entropy_slice(10);

let uuid = UUID7::init(timestamp, <[u8; 10]>::try_from(&entropy[0..10]).unwrap());
let uuid = uuid_v7_at(global, timestamp, false);

if encoding == Encoding::Hex {
let (mut str, bytes) = BunString::create_uninitialized_latin1(36);
uuid.print(
(&mut bytes[0..36])
.try_into()
.expect("infallible: size matches"),
);
return str.transfer_to_js(global);
return uuid_v7_to_hex_js(global, &uuid);
}

encoding.encode_with_max_size(global, 32, &uuid.bytes)
}

/// Shared core of `Bun.randomUUIDv7()` and `crypto.randomUUIDv7()`: 10 bytes
/// of entropy from the VM cache (or fresh BoringSSL bytes when bypassed),
/// fed to `UUID7::init` at `timestamp`.
pub(crate) fn uuid_v7_at(
global: &JSGlobalObject,
timestamp: u64,
disable_entropy_cache: bool,
) -> UUID7 {
let mut entropy = [0u8; 10];
if disable_entropy_cache {
bun_boringssl_sys::rand_bytes(&mut entropy);
} else {
entropy.copy_from_slice(&global.bun_vm().as_mut().rare_data().entropy_slice(10)[..10]);
}
UUID7::init(timestamp, entropy)
}

/// Renders `uuid` as the canonical 36-character string.
pub(crate) fn uuid_v7_to_hex_js(global: &JSGlobalObject, uuid: &UUID7) -> JsResult<JSValue> {
let (mut str, bytes) = BunString::create_uninitialized_latin1(36);
uuid.print(
(&mut bytes[..36])
.try_into()
.expect("infallible: size matches"),
);
str.transfer_to_js(global)
}

#[bun_jsc::host_fn(export = "Bun__randomUUIDv5")]
pub(crate) fn bun_random_uuid_v5(
global: &JSGlobalObject,
Expand Down
103 changes: 102 additions & 1 deletion test/js/bun/util/randomUUIDv7.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, isWindows } from "harness";

describe("randomUUIDv7", () => {
test("basic", () => {
Expand Down Expand Up @@ -210,4 +210,105 @@ describe("randomUUIDv7", () => {
expect(Number(stdout.trim())).toBeGreaterThan(1);
expect(exitCode).toBe(0);
});

// https://github.com/oven-sh/WebKit/pull/304
test.skipIf(!isWindows)("Date.now() is never ahead of performance.timeOrigin + performance.now()", async () => {
// Subprocess so timeOrigin is captured milliseconds before the loop and
// w32tm slew between VM init and test cannot drift the two clocks apart.
// Before, Date.now() ran ~0.4ms ahead in ~72% of samples.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const origin = performance.timeOrigin;
// origin + perf.now() at ~1.8e12 has ~0.0004ms double ULP; the old
// skew was ~0.4ms, so a 0.01ms threshold separates the two cleanly.
let firstAhead = null;
for (let i = 0; i < 50_000; i++) {
const d = Date.now();
const p = origin + performance.now();
if (d - p > 0.01 && firstAhead === null) firstAhead = { i, d, p, diff: +(d - p).toFixed(4) };
}
console.log(JSON.stringify(firstAhead));
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toBe(null);
expect(exitCode).toBe(0);
});
Comment thread
claude[bot] marked this conversation as resolved.

test("default timestamp is never behind Date.now()", async () => {
// All three default to js_date_now() (== Date.now()). UUID7::init may bump
// the embedded ts on 12-bit counter rollover (RFC 9562 §6.2), so only the
// lower bound is asserted for UUIDs; File.lastModified has no counter.
const N = isWindows ? 50_000 : 5_000;
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const crypto = require("node:crypto");
const tsOf = buf => buf.readUIntBE(0, 6);
const tsOfHex = s => parseInt(s.replaceAll("-", "").slice(0, 12), 16);
let bad = { bun: null, node: null, file: null };
for (let i = 0; i < ${N}; i++) {
const before = Date.now();
const b = tsOf(Bun.randomUUIDv7("buffer"));
const c = tsOfHex(crypto.randomUUIDv7());
const f = new File([], "x").lastModified;
const after = Date.now();
if (bad.bun === null && !(before <= b)) bad.bun = { i, before, b };
if (bad.node === null && !(before <= c)) bad.node = { i, before, c };
if (bad.file === null && !(before <= f && f <= after)) bad.file = { i, before, f, after };
if (bad.bun && bad.node && bad.file) break;
Comment thread
robobun marked this conversation as resolved.
}
console.log(JSON.stringify(bad));
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
Comment thread
robobun marked this conversation as resolved.
expect(JSON.parse(stdout)).toEqual({ bun: null, node: null, file: null });
expect(exitCode).toBe(0);
});

test("default timestamp respects setSystemTime()", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const { setSystemTime } = require("bun:test");
const crypto = require("node:crypto");
const tsOf = s => parseInt(s.replaceAll("-", "").slice(0, 12), 16);
const pin = 1_700_000_000_000;
setSystemTime(pin);
console.log(JSON.stringify({
dateNow: Date.now(),
bun: tsOf(Bun.randomUUIDv7()),
node: tsOf(crypto.randomUUIDv7()),
file: new File([], "x").lastModified,
}));
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({
dateNow: 1_700_000_000_000,
bun: 1_700_000_000_000,
node: 1_700_000_000_000,
file: 1_700_000_000_000,
});
expect(exitCode).toBe(0);
});
});
11 changes: 6 additions & 5 deletions test/js/web/fetch/blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,14 @@ describe("new File() lastModified option", () => {
expect(lm({ lastModified: input })).toBe(expected);
});

// The default comes from a native wall-clock read that may differ from JS
// Date.now() by a few ms on Windows; assert "current time" within a wide
// tolerance rather than an exact bracket.
test.each([[{ lastModified: undefined }], [{}]])("%p defaults to the current time", opts => {
const before = Date.now();
const value = lm(opts);
expect(Number.isFinite(value)).toBe(true);
expect(Math.abs(value - Date.now())).toBeLessThan(60_000);
const after = Date.now();
expect({ finite: Number.isFinite(value), bracketed: before <= value && value <= after }).toEqual({
finite: true,
bracketed: true,
});
});

test("valueOf throwing propagates", () => {
Expand Down
Loading