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 = "autobuild-preview-pr-304-36986ac9";
Comment thread
robobun marked this conversation as resolved.
Outdated

/**
* 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 @@
}

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());

Check warning on line 5714 in src/runtime/webcore/Blob.rs

View check run for this annotation

Claude / Claude Code Review

Stale comment in blob.test.ts describes the pre-PR File.lastModified clock behavior this PR removes

nit: `test/js/web/fetch/blob.test.ts:225-227` still says the `lastModified` default "comes from a native wall-clock read that may differ from JS Date.now() by a few ms on Windows" — that is exactly the behavior this line removes. The default is now `js_date_now()` (i.e. `Date.now()` exactly), and this PR's own new test asserts strict `before <= f && f <= after` bracketing for `File.lastModified`, so the comment and its 60s-tolerance rationale are stale. Worth deleting the comment (and optionally
Comment thread
robobun marked this conversation as resolved.
}

if blob.content_type_slice().is_empty() {
Expand Down
47 changes: 35 additions & 12 deletions src/runtime/webcore/Crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,27 +247,50 @@ 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`. Only argument validation differs
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
/// between the two public functions.
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
77 changes: 76 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,79 @@
expect(Number(stdout.trim())).toBeGreaterThan(1);
expect(exitCode).toBe(0);
});

// https://github.com/oven-sh/WebKit/pull/304
test("default timestamp is never behind Date.now()", async () => {
// Before oven-sh/WebKit#304, Windows Date.now() (WTF QPC-interpolated) ran
// up to ~1ms ahead of the native precise clock the runtime read for default
// timestamps, so before > embedded-timestamp in ~80% of samples. All three
// now default to js_date_now() which is Date.now() exactly. UUID7::init may
// bump the embedded timestamp on 12-bit counter rollover (RFC 9562 §6.2),
// so only the lower bound is asserted for the UUID paths; File.lastModified
// has no counter and is fully bracketed.

Check warning on line 222 in test/js/bun/util/randomUUIDv7.test.ts

View check run for this annotation

Claude / Claude Code Review

7-line test comment exceeds CLAUDE.md 3-line cap and contains bug history

nit: this comment is 7 lines; CLAUDE.md rule 13 caps code comments at 3. The first three lines ("Before oven-sh/WebKit#304 … in ~80% of samples") are bug history, which per REVIEW.md belongs in the PR description — the issue URL on line 214 already links it. The rollover-asymmetry rationale is worth keeping and fits in 3 lines, e.g. `// 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 a
Comment thread
robobun marked this conversation as resolved.
Outdated
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);
});
});
Loading