mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-24 08:13:59 +00:00
Restore crypto.randomUUID before the Studio bundle evaluates (#9075)
This commit is contained in:
parent
ea958c7f34
commit
31c42e872b
9 changed files with 142 additions and 22 deletions
|
|
@ -2481,6 +2481,15 @@ _ARTIFACT_PREVIEW_FRAME_HTML = """<!doctype html>
|
|||
installStorageFallback("localStorage");
|
||||
installStorageFallback("sessionStorage");
|
||||
};
|
||||
// randomUUID is unavailable in this opaque HTTP context. The strict CSP
|
||||
// forbids crypto-boot.js, so install the same fallback inline.
|
||||
const installRandomUUIDFallback = () => {
|
||||
if (!window.crypto || typeof crypto.randomUUID === "function") return;
|
||||
const randomByte = () => crypto.getRandomValues(new Uint8Array(1))[0];
|
||||
crypto.randomUUID = () =>
|
||||
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
|
||||
(+c ^ (randomByte() & (15 >> (+c / 4)))).toString(16));
|
||||
};
|
||||
// Stamp the load this frame was served for. A report still in flight
|
||||
// when the canvas is swapped would otherwise be read as the new one's.
|
||||
const loadVersion = new URLSearchParams(location.search).get("v") || "";
|
||||
|
|
@ -2501,6 +2510,8 @@ _ARTIFACT_PREVIEW_FRAME_HTML = """<!doctype html>
|
|||
document.addEventListener("securitypolicyviolation", reportBlocked, true);
|
||||
};
|
||||
installStorageFallbacks();
|
||||
// Survives the document.open() in render(), so once is enough.
|
||||
installRandomUUIDFallback();
|
||||
window.addEventListener("message", (event) => {
|
||||
const data = event.data;
|
||||
if (!data || data.type !== "unsloth:artifact-html" || typeof data.html !== "string") return;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ reaches this route now, fenced HTML included, not just approved render_html
|
|||
output."""
|
||||
|
||||
import asyncio
|
||||
import pathlib
|
||||
|
||||
import routes.inference as inf_mod
|
||||
|
||||
|
|
@ -122,3 +123,25 @@ def test_the_permissive_policy_widens_every_hostless_scheme_but_one():
|
|||
if scheme not in value.split()
|
||||
}
|
||||
assert gaps == {"worker-src": "data:"}
|
||||
|
||||
|
||||
def test_the_shell_restores_randomuuid_for_insecure_canvases():
|
||||
# This test cannot execute the shell, so pin the fallback's required pieces.
|
||||
shell = inf_mod._ARTIFACT_PREVIEW_FRAME_HTML
|
||||
assert 'typeof crypto.randomUUID === "function"' in shell
|
||||
assert "crypto.randomUUID = () =>" in shell
|
||||
assert "installRandomUUIDFallback();" in shell
|
||||
|
||||
|
||||
def test_the_shell_generator_matches_the_app_one():
|
||||
# The strict CSP forbids sharing crypto-boot.js, so keep both copies aligned.
|
||||
shell = inf_mod._ARTIFACT_PREVIEW_FRAME_HTML
|
||||
boot = (
|
||||
pathlib.Path(__file__).resolve().parents[2] / "frontend/public/crypto-boot.js"
|
||||
).read_text(encoding = "utf-8")
|
||||
for expression in (
|
||||
'"10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>',
|
||||
"(+c ^ (randomByte() & (15 >> (+c / 4)))).toString(16)",
|
||||
):
|
||||
assert expression in boot
|
||||
assert expression in shell
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Unsloth</title>
|
||||
<!-- Applies the stored theme before the bundle loads; external file
|
||||
because the backend CSP is script-src 'self'. -->
|
||||
<!-- Classic boot scripts satisfy script-src 'self' and run before modules. -->
|
||||
<script src="/crypto-boot.js"></script>
|
||||
<script src="/theme-boot.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
17
studio/frontend/public/crypto-boot.js
Normal file
17
studio/frontend/public/crypto-boot.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
// randomUUID is unavailable on insecure HTTP origins. Install it before the
|
||||
// module graph, which may call it during evaluation.
|
||||
if (globalThis.crypto && typeof globalThis.crypto.randomUUID !== "function") {
|
||||
const cryptoRef = globalThis.crypto;
|
||||
const randomByte = () =>
|
||||
typeof cryptoRef.getRandomValues === "function"
|
||||
? cryptoRef.getRandomValues(new Uint8Array(1))[0]
|
||||
: Math.floor(Math.random() * 256);
|
||||
|
||||
cryptoRef.randomUUID = () =>
|
||||
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
|
||||
(+c ^ (randomByte() & (15 >> (+c / 4)))).toString(16),
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="/crypto-boot.js"></script>
|
||||
<title>strip-ansi smoke</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="/crypto-boot.js"></script>
|
||||
<title>chat autoscroll smoke</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="/crypto-boot.js"></script>
|
||||
<title>deep research freeze smoke</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -11,26 +11,6 @@ import { initializeLocale } from "./i18n";
|
|||
import { isTauri } from "./lib/api-base";
|
||||
import { watchOverlayScrollbarGutter } from "./lib/overlay-scrollbar";
|
||||
|
||||
const globalCrypto = globalThis.crypto as Crypto | undefined;
|
||||
|
||||
if (globalCrypto && typeof globalCrypto.randomUUID !== "function") {
|
||||
// Some envs ship `crypto` without `randomUUID()`. Provide a best-effort v4
|
||||
// UUID using `getRandomValues` when available.
|
||||
const cryptoRef = globalCrypto;
|
||||
|
||||
function getRandomByte(): number {
|
||||
if (typeof cryptoRef.getRandomValues === "function") {
|
||||
return cryptoRef.getRandomValues(new Uint8Array(1))[0];
|
||||
}
|
||||
return Math.floor(Math.random() * 256);
|
||||
}
|
||||
|
||||
cryptoRef.randomUUID = (() =>
|
||||
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
|
||||
(+c ^ (getRandomByte() & (15 >> (+c / 4)))).toString(16),
|
||||
)) as Crypto["randomUUID"];
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) {
|
||||
throw new Error("Root element not found");
|
||||
|
|
|
|||
86
studio/frontend/tests/crypto-uuid-boot.test.ts
Normal file
86
studio/frontend/tests/crypto-uuid-boot.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createContext, runInContext } from "node:vm";
|
||||
|
||||
const read = (relative: string): string =>
|
||||
readFileSync(fileURLToPath(new URL(relative, import.meta.url)), "utf8");
|
||||
|
||||
const HTML_COMMENT = /<!--[\s\S]*?-->/g;
|
||||
const POLYFILL_TAG = /<script\b[^>]*src="\/crypto-boot\.js"[^>]*>/;
|
||||
const ENTRY_TAG = /<script\b[^>]*\btype="module"[^>]*>/;
|
||||
const APP_ENTRY = /<script\b[^>]*src="\/src\/main\.tsx"[^>]*>/;
|
||||
const ASYNC_ATTR = /\basync\b/;
|
||||
const UUID_V4 =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
// Check every HTML entry as raw markup so comments cannot satisfy the patterns.
|
||||
const PAGES = readdirSync(new URL("../", import.meta.url))
|
||||
.filter((name) => name.endsWith(".html"))
|
||||
.map((name) => [name, read(`../${name}`).replace(HTML_COMMENT, "")] as const);
|
||||
const BOOT_SCRIPT = read("../public/crypto-boot.js");
|
||||
|
||||
function boot(cryptoStub: unknown): { randomUUID?: () => string } {
|
||||
const sandbox = { crypto: cryptoStub } as {
|
||||
crypto: { randomUUID?: () => string };
|
||||
};
|
||||
runInContext(BOOT_SCRIPT, createContext(sandbox));
|
||||
return sandbox.crypto;
|
||||
}
|
||||
|
||||
test("every page loads the polyfill before its module entry", () => {
|
||||
assert.ok(PAGES.length > 0, "no HTML entries found");
|
||||
const index = PAGES.find(([name]) => name === "index.html")?.[1];
|
||||
assert.match(index ?? "", APP_ENTRY, "index.html must keep the app entry");
|
||||
for (const [name, markup] of PAGES) {
|
||||
const polyfill = markup.search(POLYFILL_TAG);
|
||||
const entry = markup.search(ENTRY_TAG);
|
||||
assert.ok(polyfill !== -1, `${name} must load /crypto-boot.js`);
|
||||
assert.ok(entry !== -1, `${name} must load a module entry`);
|
||||
assert.ok(polyfill < entry, `${name} loads the polyfill too late`);
|
||||
}
|
||||
});
|
||||
|
||||
test("no page lets the polyfill or its entry opt out of ordered execution", () => {
|
||||
for (const [name, markup] of PAGES) {
|
||||
assert.doesNotMatch(POLYFILL_TAG.exec(markup)?.[0] ?? "", ASYNC_ATTR, name);
|
||||
assert.doesNotMatch(ENTRY_TAG.exec(markup)?.[0] ?? "", ASYNC_ATTR, name);
|
||||
}
|
||||
});
|
||||
|
||||
const STREAM = [
|
||||
0x9e, 0x37, 0x79, 0xb9, 0x7f, 0x4a, 0x7c, 0x15, 0xf3, 0x9c, 0xc0, 0x60, 0x5c,
|
||||
0xed, 0xc8, 0x34,
|
||||
];
|
||||
|
||||
let cursor = 0;
|
||||
const generate = boot({
|
||||
getRandomValues: (array: Uint8Array) => {
|
||||
for (let i = 0; i < array.length; i += 1) {
|
||||
array[i] = STREAM[cursor % STREAM.length];
|
||||
cursor += 1;
|
||||
}
|
||||
return array;
|
||||
},
|
||||
}).randomUUID;
|
||||
|
||||
test("the drawn bytes are what the UUID is built from", () => {
|
||||
// A fixed byte stream catches changes to UUID masking and folding.
|
||||
cursor = 0;
|
||||
const uuid = generate?.() ?? "";
|
||||
assert.equal(uuid, "f799fac5-2c00-4cd8-8e79-8fac53c00cd8");
|
||||
assert.match(uuid, UUID_V4);
|
||||
});
|
||||
|
||||
test("the polyfill leaves a real randomUUID alone", () => {
|
||||
const native = () => "native";
|
||||
const patched = boot({
|
||||
randomUUID: native,
|
||||
getRandomValues: () => undefined,
|
||||
});
|
||||
assert.equal(patched.randomUUID, native);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue