Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Parent-process driver for the in-process WebAssembly DLP module. Called by the
* C++ WasmModuleBackend, which passes a serialized ContentAnalysisRequest and
* expects a serialized ContentAnalysisResponse back.
*
* This runs in the parent process so it cannot compile or run the module -
* that work is done by the ContentAnalysisWasm actor.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
});
// keep in sync with mozilla::contentanalysis::kWasmModuleExtensionId
const EXTENSION_ID = "dlp-wasm-provider@mozilla.org";
const REQUIRE_SIGNATURE_PREF =
"browser.contentanalysis.wasm_module_extension_require_signature";
// Name the module is expected to have inside an extension's package.
const EXTENSION_MODULE_FILENAME = "content_analysis_wasm.wasm";
const ACTOR_NAME = "ContentAnalysisWasm";
/**
* Read a file packaged inside an installed extension (identified by ID) as raw
* bytes. Also checks the signature if the pref is set.
*
* @param {string} extensionId The extension ID to load
* @param {string} path The path of the file to load from the extension
* @returns {{moduleBytes: Uint8Array, extensionVersion: string}}
*/
function readExtensionBytes(extensionId, path) {
const extension =
lazy.ExtensionParent.GlobalManager.getExtension(extensionId);
if (!extension) {
return null;
}
const requireSignature = Services.prefs.getBoolPref(
REQUIRE_SIGNATURE_PREF,
true
);
if (requireSignature) {
const signedState = extension.addonData?.signedState;
if (!(signedState >= lazy.AddonManager.SIGNEDSTATE_SYSTEM)) {
throw Components.Exception(
`DLP wasm extension '${extensionId}' is not acceptably signed ` +
`(signedState=${signedState}); refusing to load its module`,
Cr.NS_ERROR_INVALID_SIGNATURE
);
}
}
const channel = lazy.NetUtil.newChannel({
uri: extension.getURL(path),
loadUsingSystemPrincipal: true,
});
const stream = channel.open();
const bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
bstream.setInputStream(stream);
const bytes = bstream.readByteArray(stream.available());
bstream.close();
stream.close();
return {
moduleBytes: Uint8Array.from(bytes),
extensionVersion: extension.version,
};
}
/**
* nsIContentAnalysisWasmRunner implementation. See the module comment above.
*/
export class ContentAnalysisWasmRunner {
QueryInterface = ChromeUtils.generateQI(["nsIContentAnalysisWasmRunner"]);
async analyze(aRequestBytes, aContentBytes, aRules) {
// Reading + signature verification stay in the parent (the trust decision):
// WebExtensionPolicy/AddonManager are parent-process only.
const extensionInfo = readExtensionBytes(
EXTENSION_ID,
EXTENSION_MODULE_FILENAME
);
if (!extensionInfo) {
throw Components.Exception(
`DLP WASM extension '${EXTENSION_ID}' is not installed or not enabled`,
Cr.NS_ERROR_NOT_AVAILABLE
);
}
const { moduleBytes, extensionVersion } = extensionInfo;
const actor = await this.#getActor();
// Resolves with a Uint8Array, which the C++ caller reads in bulk.
return actor.sendQuery("Analyze", {
version: extensionVersion,
moduleBytes,
requestBytes: Uint8Array.from(aRequestBytes),
contentBytes: Uint8Array.from(aContentBytes || []),
rules: toPlainRules(aRules),
});
}
async #getActor() {
// We use the privilegedabout process for this because:
// - it's essentially always running, so it won't add any overhead
// - it has the ability to compile and run WASM
const keepAlive = await ChromeUtils.ensureHeadlessContentProcess(
lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
);
if (!keepAlive?.domProcess?.canSend) {
throw Components.Exception(
"could not start a content process for the DLP wasm module",
Cr.NS_ERROR_NOT_AVAILABLE
);
}
return keepAlive.domProcess.getActor(ACTOR_NAME);
}
}
// Convert the C++-supplied nsIContentAnalysisRule objects into plain,
// structured-cloneable objects to ship to the content process.
function toPlainRules(aRules) {
return Array.from(aRules || [], rule => ({
name: rule.name,
operations: Array.from(rule.operations),
domains: Array.from(rule.domains),
contentPatterns: Array.from(rule.contentPatterns),
ruleType: rule.verdict,
message: rule.message,
}));
}