Bug 2067200 - Return an empty WebAssembly binary module if scripting is disabled. r=spidermonkey-reviewers,frontend-codestyle-reviewers,dminor

Implement https://html.spec.whatwg.org/#creating-a-webassembly-module-script

Differential Revision: https://phabricator.services.mozilla.com/D323657
This commit is contained in:
Yoshi Cheng-Hao Huang
2026-09-07 13:25:14 +00:00
committed by allstars.chh@gmail.com
parent de14c16b69
commit 28e5b605fa
9 changed files with 274 additions and 20 deletions
Binary file not shown.
+2
View File
@@ -15,6 +15,8 @@ support-files = [
["test_scriptDisabled_modules.html"]
support-files = [
"!/dom/base/test/file_wasm_offthread_module.sjs",
"exports-fun.wasm",
"module_large1.mjs",
"scriptDisabledDiskCache_module.sjs",
"scriptDisabled_frame.html",
@@ -8,6 +8,8 @@
// returned promise: it can never settle, and nothing settles it afterwards.
window.importModule = url => import(url);
window.importSource = url => import.source(url);
window.startModuleScript = url => {
const script = document.createElement("script");
script.type = "module";
@@ -9,6 +9,11 @@
const SIMPLE_MODULE = "./scriptDisabled_module.mjs";
const CACHEABLE_MODULE = "./scriptDisabledDiskCache_module.sjs";
const LARGE_MODULE = "./module_large1.mjs";
const WASM_MODULE = "./exports-fun.wasm";
// Serve a large off-the-main-thread-compiled wasm module.
const LARGE_WASM_MODULE = "../file_wasm_offthread_module.sjs";
const WASM_ESM_PREF = "javascript.options.experimental.wasm_esm_integration";
function addFrame() {
return new Promise(resolve => {
@@ -223,5 +228,161 @@ add_task(async function moduleWithMemoryCacheEntry() {
frame.remove();
await SpecialPowers.popPrefEnv();
});
// https://html.spec.whatwg.org/#creating-a-webassembly-module-script
// Step 1. If scripting is disabled for settings, then set bodyBytes to the byte
// sequence 0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00.
add_task(async function wasmModuleCreatedEmpty() {
// The WebAssembly ESM integration, and so its pref, only exists on Nightly.
if (
SpecialPowers.Services.prefs.getPrefType(WASM_ESM_PREF) ==
SpecialPowers.Ci.nsIPrefBranch.PREF_INVALID
) {
info("skipped: the WebAssembly ESM integration is not available");
return;
}
await SpecialPowers.pushPrefEnv({
set: [
[WASM_ESM_PREF, true],
["javascript.options.experimental.source_phase_imports", true],
],
});
const countExports = frame =>
SpecialPowers.spawn(frame, [WASM_MODULE], async url => {
const source = await content.wrappedJSObject.importSource(url);
return content.wrappedJSObject.WebAssembly.Module.exports(source).length;
});
// Control, in its own frame so that it gets its own module map: the module
// served is not empty to begin with.
const control = await addFrame();
is(
await countExports(control),
1,
"the module has an export when script is enabled"
);
control.remove();
const frame = await addFrame();
// Start an import and block script in the same task.
await SpecialPowers.spawn(frame, [WASM_MODULE], async url => {
const fileName = url.split("/").pop();
const loadGroup =
content.docShell.QueryInterface(Ci.nsIDocumentLoader).loadGroup;
const moduleIsFetching = () => {
for (const request of loadGroup.requests) {
if (request.name.endsWith(fileName)) {
return true;
}
}
return false;
};
content.wrappedJSObject.importSource(url);
Cu.blockScriptForGlobal(content);
try {
ok(moduleIsFetching(), "the module's fetch is in flight");
await ContentTaskUtils.waitForCondition(
() => !moduleIsFetching(),
"the module's fetch completed while script was disabled"
);
} finally {
Cu.unblockScriptForGlobal(content);
}
});
// The empty wasm module is in this realm's module map, so importing again
// returns it rather than fetching.
is(
await countExports(frame),
0,
"the module was created as an empty WebAssembly module"
);
frame.remove();
await SpecialPowers.popPrefEnv();
});
// Only source phase wasm modules at or above OffThreadMinimumWasmLength are
// compiled off the main thread. Creating the empty module has to discard that
// pending compilation rather than adopt its result.
add_task(async function wasmModuleCompiledOffMainThreadCreatedEmpty() {
if (
SpecialPowers.Services.prefs.getPrefType(WASM_ESM_PREF) ==
SpecialPowers.Ci.nsIPrefBranch.PREF_INVALID
) {
info("skipped: the WebAssembly ESM integration is not available");
return;
}
await SpecialPowers.pushPrefEnv({
set: [
[WASM_ESM_PREF, true],
["javascript.options.experimental.source_phase_imports", true],
["javascript.options.parallel_parsing", true],
// Enables the ScriptLoaderTest notifications checked below.
["dom.expose_test_interfaces", true],
],
});
const frame = await addFrame();
await SpecialPowers.spawn(frame, [LARGE_WASM_MODULE], async url => {
const fileName = url.split("/").pop();
const traces = [];
const observer = (subject, topic, data) => traces.push(data);
Services.obs.addObserver(observer, "ScriptLoaderTest");
const loadGroup =
content.docShell.QueryInterface(Ci.nsIDocumentLoader).loadGroup;
const moduleIsFetching = () => {
for (const request of loadGroup.requests) {
if (request.name.endsWith(fileName)) {
return true;
}
}
return false;
};
const traced = event =>
traces.some(
data => data.includes(`event:${event}`) && data.includes(fileName)
);
content.wrappedJSObject.importSource(url);
Cu.blockScriptForGlobal(content);
try {
ok(moduleIsFetching(), "the module's fetch is in flight");
// Waiting for the empty wasm module is created.
await ContentTaskUtils.waitForCondition(
() => traced("compile:wasm empty"),
"the empty module was created while script was disabled"
);
ok(
traced("compile:off thread"),
"the module was compiled off the main thread"
);
} finally {
Cu.unblockScriptForGlobal(content);
Services.obs.removeObserver(observer, "ScriptLoaderTest");
}
// The empty module is in this realm's module map, so importing again
// returns it rather than fetching.
const source = await content.wrappedJSObject.importSource(url);
is(
content.wrappedJSObject.WebAssembly.Module.exports(source).length,
0,
"the off-thread compiled module was created as an empty WebAssembly module"
);
});
frame.remove();
await SpecialPowers.popPrefEnv();
});
</script>
<body></body>
+53 -20
View File
@@ -8,6 +8,7 @@
#include "GeckoProfiler.h"
#include "ScriptLoader.h"
#include "ScriptTrace.h" // TRACE_FOR_TEST
#include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions
#include "js/ContextOptions.h" // JS::ContextOptionsRef
#include "js/MemoryFunctions.h"
@@ -275,47 +276,79 @@ nsresult ModuleLoader::CompileEmptyJavaScriptModule(
return aModuleOut ? NS_OK : NS_ERROR_FAILURE;
}
#ifdef NIGHTLY_BUILD
nsresult ModuleLoader::CompileWasmModuleBytes(
JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest,
WasmBytesBuffer& aBytes, JS::MutableHandle<JSObject*> aModuleOut) {
JSObject* wasmModule;
if (aRequest->IsSourcePhaseRequest(aCx)) {
wasmModule = JS::CompileWasmModuleAsSource(aCx, aOptions, aBytes);
} else {
wasmModule = JS::CompileWasmModule(aCx, aOptions, aBytes);
}
if (!wasmModule) {
return NS_ERROR_FAILURE;
}
aModuleOut.set(wasmModule);
return NS_OK;
}
// https://html.spec.whatwg.org/#creating-a-webassembly-module-script
nsresult ModuleLoader::CompileEmptyWasmModule(
JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest,
JS::MutableHandle<JSObject*> aModuleOut) {
TRACE_FOR_TEST(aRequest, "compile:wasm empty");
// Step 1: If scripting is disabled, set bodyBytes to the byte sequence
// 0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00
static constexpr uint8_t kEmptyWasmModule[] = {0x00, 0x61, 0x73, 0x6D,
0x01, 0x00, 0x00, 0x00};
WasmBytesBuffer bytes;
if (!bytes.append(kEmptyWasmModule, sizeof(kEmptyWasmModule))) {
return NS_ERROR_OUT_OF_MEMORY;
}
return CompileWasmModuleBytes(aCx, aOptions, aRequest, bytes, aModuleOut);
}
#endif
nsresult ModuleLoader::CompileJavaScriptOrWasmModule(
JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions,
ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleOut) {
GetScriptLoader()->CalculateCacheFlag(aRequest);
if (!nsJSUtils::IsScriptable(aGlobal)) {
aRequest->GetScriptLoadContext()->MaybeCancelOffThreadScript();
#ifdef NIGHTLY_BUILD
// TODO: Bug 2067200, Creating an empty WebAssembly module if scripting is
// disabled.
if (aRequest->HasWasmMimeTypeEssence()) {
MOZ_ASSERT(aRequest->IsWasmBytes());
return NS_ERROR_FAILURE;
return CompileEmptyWasmModule(aCx, aOptions, aRequest, aModuleOut);
}
#endif
aRequest->GetScriptLoadContext()->MaybeCancelOffThreadScript();
return CompileEmptyJavaScriptModule(aCx, aOptions, aRequest, aModuleOut);
}
#ifdef NIGHTLY_BUILD
if (aRequest->HasWasmMimeTypeEssence()) {
MOZ_ASSERT(aRequest->IsWasmBytes());
if (aRequest->IsSourcePhaseRequest(aCx)) {
if (aRequest->GetScriptLoadContext()->mWasCompiledOMT) {
if (!aRequest->GetScriptLoadContext()->StealOffThreadWasmResult(
aCx, aModuleOut)) {
return NS_ERROR_FAILURE;
}
} else {
aModuleOut.set(JS::CompileWasmModuleAsSource(aCx, aOptions,
aRequest->WasmBytes()));
// Only source phase requests are compiled off-thread, and the request's
// bytes were moved into the WasmCompileTask, so the result has to be taken
// from the task rather than recompiled.
if (aRequest->GetScriptLoadContext()->mWasCompiledOMT) {
MOZ_ASSERT(aRequest->IsSourcePhaseRequest(aCx));
if (!aRequest->GetScriptLoadContext()->StealOffThreadWasmResult(
aCx, aModuleOut)) {
return NS_ERROR_FAILURE;
}
} else {
aModuleOut.set(
JS::CompileWasmModule(aCx, aOptions, aRequest->WasmBytes()));
}
if (!aModuleOut) {
return NS_ERROR_FAILURE;
return NS_OK;
}
return NS_OK;
return CompileWasmModuleBytes(aCx, aOptions, aRequest,
aRequest->WasmBytes(), aModuleOut);
}
#endif
MOZ_ASSERT(!aRequest->IsWasmBytes());
+11
View File
@@ -92,6 +92,17 @@ class ModuleLoader final : public JS::loader::ModuleLoaderBase {
nsresult CompileEmptyJavaScriptModule(
JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest,
JS::MutableHandle<JSObject*> aModuleOut);
#ifdef NIGHTLY_BUILD
using WasmBytesBuffer =
JS::loader::ScriptLoadRequest::ScriptTextBuffer<uint8_t>;
nsresult CompileWasmModuleBytes(JSContext* aCx, JS::CompileOptions& aOptions,
ModuleLoadRequest* aRequest,
WasmBytesBuffer& aBytes,
JS::MutableHandle<JSObject*> aModuleOut);
nsresult CompileEmptyWasmModule(JSContext* aCx, JS::CompileOptions& aOptions,
ModuleLoadRequest* aRequest,
JS::MutableHandle<JSObject*> aModuleOut);
#endif
nsresult CompileJsonModule(JSContext* aCx, JS::CompileOptions& aOptions,
ModuleLoadRequest* aRequest,
JS::MutableHandle<JSObject*> aModuleOut);
+1
View File
@@ -146,6 +146,7 @@ export default [
// ESLint parse does not support import source yet (Bug 2063547)
"dom/base/test/test_wasm_offthread_compile.html",
"dom/base/test/jsmodules/scriptDisabled_frame.html",
// Intentional broken files
"dom/base/test/file_js_cache_syntax_error.js",
@@ -0,0 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<!--
Loaded in a sandboxed iframe by ../scripting-disabled.tentative.html, where
scripting is disabled, so nothing in this document runs.
-->
<script type="module" src="./invalid-bytecode.wasm"></script>
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<title>WebAssembly module script in a document where scripting is disabled</title>
<meta name="author" title="Mozilla" href="mailto:yhuang@mozilla.com">
<meta name="help" href="https://html.spec.whatwg.org/#creating-a-webassembly-module-script">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<script>
// https://html.spec.whatwg.org/#creating-a-webassembly-module-script
// Step 1. If scripting is disabled for settings, then set bodyBytes to the byte
// sequence 0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00.
promise_test(async t => {
const frame = document.createElement("iframe");
// A sandbox without "allow-scripts" disables scripting for the frame;
// "allow-same-origin" keeps the module script's fetch same-origin.
frame.sandbox = "allow-same-origin";
frame.src = "resources/scripting-disabled.html";
t.add_cleanup(() => frame.remove());
const loaded = new Promise((resolve, reject) => {
frame.addEventListener("load", resolve, { once: true });
frame.addEventListener(
"error",
() => reject(new Error("the frame failed to load")),
{ once: true }
);
});
document.body.appendChild(frame);
await loaded;
assert_equals(
frame.contentDocument.readyState,
"complete",
"the document holding the module script loaded"
);
}, "An invalid WebAssembly module script does not fail when scripting is disabled");
</script>