- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
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
"use strict";
// Compiles and runs the in-process WebAssembly DLP module. This script is loaded
// (via loadSubScript) into a null-principal sandbox living in a privilegedabout
// process by ContentAnalysisWasmChild.
//
// The sandbox has no chrome privileges (Cc/Ci/Components are unavailable), so
// failures are reported with plain Errors; the child actor turns them into the
// runner's callback.onError.
// The wasm module's exported ABI contract:
// ca_abi_version() -> i32
// ca_alloc(len) -> ptr ; ca_free(ptr, len)
// ca_analyze(reqPtr, reqLen, contentPtr, contentLen,
// rulesPtr, rulesCount, outLenPtr) -> respPtr
const ABI_VERSION = 1;
// Layout of the module's `#[repr(C)]` rule structs on wasm32 (4-byte pointers and
// usize). Must match content-analysis-wasm's FfiStr/FfiRule exactly.
// FfiStr { ptr: u32, len: u32 } // 8 bytes
// FfiRule { name: FfiStr, // @0
// operations: u32, // @8 (ptr to u32 array)
// operations_count: u32, // @12
// domains: u32, // @16 (ptr to FfiStr array)
// domains_count: u32, // @20
// content_patterns: u32, // @24 (ptr to FfiStr array)
// patterns_count: u32, // @28
// rule_type: u8, // @32 (0=report, 1=warn, 2=block)
// message: FfiStr } // @36
const FFI_STR_SIZE = 8;
const FFI_RULE_SIZE = 44;
const FFI_RULE_OPERATIONS_OFFSET = 8;
const FFI_RULE_OPERATIONS_COUNT_OFFSET = 12;
const FFI_RULE_DOMAINS_OFFSET = 16;
const FFI_RULE_DOMAINS_COUNT_OFFSET = 20;
const FFI_RULE_PATTERNS_OFFSET = 24;
const FFI_RULE_PATTERNS_COUNT_OFFSET = 28;
const FFI_RULE_TYPE_OFFSET = 32;
const FFI_RULE_MESSAGE_OFFSET = 36;
// The currently loaded module instance, kept across analyze() calls so the
// module is only recompiled when it changes (see loadModule).
let gInstance = null;
// Compile and instantiate the module, verifying the ABI version. Called by the
// child actor whenever the configured module changes.
// eslint-disable-next-line no-unused-vars
function loadModule(moduleBytes) {
const module = new WebAssembly.Module(moduleBytes);
const instance = new WebAssembly.Instance(module, {});
if (instance.exports.ca_abi_version() !== ABI_VERSION) {
throw new Error("Unexpected DLP wasm ABI version");
}
gInstance = instance;
}
/**
* Lower the caller-supplied rule objects into the module's FfiRule ABI in linear
* memory. Allocates every buffer up front (linear-memory addresses stay valid as
* memory grows), then writes them all with a single fresh view. Returns the
* rules-array pointer/count plus the allocations to free afterward.
*
* @param {*} ex The module with ca_alloc entry point
* @param {Array} aRules The rules to convert
* @returns {{ptr: number, count: number, allocations: Array<{ptr: number, len: number}>}}
*/
function marshalRules(ex, aRules) {
const allocations = [];
const alloc = len => {
if (!len) {
return 0;
}
const ptr = ex.ca_alloc(len);
allocations.push({ ptr, len });
return ptr;
};
const rules = aRules || [];
if (!rules.length) {
return { ptr: 0, count: 0, allocations };
}
const encoder = new TextEncoder();
const encodeAll = strs => Array.from(strs, s => encoder.encode(s));
// Phase 1: encode strings and reserve all buffers. Linear-memory addresses
// stay valid as memory grows, so we can write them all afterward.
const prepared = rules.map(rule => {
const nameBytes = encoder.encode(rule.name);
const operations = Array.from(rule.operations);
const domainBytes = encodeAll(rule.domains);
const patternBytes = encodeAll(rule.contentPatterns);
const messageBytes = rule.message ? encoder.encode(rule.message) : null;
return {
ruleType: rule.ruleType,
nameBytes,
namePtr: alloc(nameBytes.length),
operations,
operationsPtr: alloc(operations.length * 4),
domainBytes,
domainPtrs: domainBytes.map(b => alloc(b.length)),
domainsArrPtr: alloc(domainBytes.length * FFI_STR_SIZE),
patternBytes,
patternPtrs: patternBytes.map(b => alloc(b.length)),
patternsArrPtr: alloc(patternBytes.length * FFI_STR_SIZE),
messageBytes,
messagePtr: messageBytes ? alloc(messageBytes.length) : 0,
};
});
const rulesPtr = alloc(prepared.length * FFI_RULE_SIZE);
// Phase 2: write all bytes and structs with one view taken after allocation.
const bytes = new Uint8Array(ex.memory.buffer);
const view = new DataView(ex.memory.buffer);
const writeStr = (offset, ptr, len) => {
view.setUint32(offset, ptr, true);
view.setUint32(offset + 4, len, true);
};
// Write each string's bytes and its FfiStr-array entry at `arrPtr`.
const writeStrArray = (arrPtr, byteArrays, ptrs) => {
byteArrays.forEach((byteArray, i) => {
if (ptrs[i]) {
bytes.set(byteArray, ptrs[i]);
}
writeStr(arrPtr + i * FFI_STR_SIZE, ptrs[i], byteArray.length);
});
};
prepared.forEach((p, i) => {
if (p.namePtr) {
bytes.set(p.nameBytes, p.namePtr);
}
p.operations.forEach((op, j) => {
view.setUint32(p.operationsPtr + j * 4, op, true);
});
writeStrArray(p.domainsArrPtr, p.domainBytes, p.domainPtrs);
writeStrArray(p.patternsArrPtr, p.patternBytes, p.patternPtrs);
if (p.messagePtr) {
bytes.set(p.messageBytes, p.messagePtr);
}
const base = rulesPtr + i * FFI_RULE_SIZE;
writeStr(base, p.namePtr, p.nameBytes.length);
view.setUint32(base + FFI_RULE_OPERATIONS_OFFSET, p.operationsPtr, true);
view.setUint32(
base + FFI_RULE_OPERATIONS_COUNT_OFFSET,
p.operations.length,
true
);
view.setUint32(base + FFI_RULE_DOMAINS_OFFSET, p.domainsArrPtr, true);
view.setUint32(
base + FFI_RULE_DOMAINS_COUNT_OFFSET,
p.domainBytes.length,
true
);
view.setUint32(base + FFI_RULE_PATTERNS_OFFSET, p.patternsArrPtr, true);
view.setUint32(
base + FFI_RULE_PATTERNS_COUNT_OFFSET,
p.patternBytes.length,
true
);
view.setUint8(base + FFI_RULE_TYPE_OFFSET, p.ruleType);
writeStr(
base + FFI_RULE_MESSAGE_OFFSET,
p.messagePtr,
p.messageBytes ? p.messageBytes.length : 0
);
});
return { ptr: rulesPtr, count: prepared.length, allocations };
}
// Run the loaded module against a request. Returns the serialized
// ContentAnalysisResponse as a Uint8Array.
// eslint-disable-next-line no-unused-vars
function runAnalyze({ requestBytes, contentBytes, rules }) {
if (!gInstance) {
throw new Error("DLP wasm module not loaded");
}
const ex = gInstance.exports;
const req = Uint8Array.from(requestBytes);
const content = Uint8Array.from(contentBytes || []);
// Every buffer allocated so far, freed in the `finally` below even if a
// later step throws (e.g. the module traps during ca_analyze), so a failed
// analysis can't leak the module's linear memory.
const allocations = [];
const alloc = len => {
if (!len) {
return 0;
}
const ptr = ex.ca_alloc(len);
allocations.push({ ptr, len });
return ptr;
};
let respPtr = 0;
let outLen = 0;
try {
// WebAssembly.Memory.buffer is detached and replaced when memory grows, so
// re-create the view from ex.memory.buffer after every call that allocates.
const outLenPtr = alloc(4);
const reqPtr = alloc(req.length);
if (reqPtr) {
new Uint8Array(ex.memory.buffer).set(req, reqPtr);
}
const contentPtr = alloc(content.length);
if (contentPtr) {
new Uint8Array(ex.memory.buffer).set(content, contentPtr);
}
const marshalled = marshalRules(ex, rules);
allocations.push(...marshalled.allocations);
respPtr = ex.ca_analyze(
reqPtr,
req.length,
contentPtr,
content.length,
marshalled.ptr,
marshalled.count,
outLenPtr
);
outLen = new DataView(ex.memory.buffer).getUint32(outLenPtr, true);
return new Uint8Array(ex.memory.buffer, respPtr, outLen).slice();
} finally {
if (respPtr) {
ex.ca_free(respPtr, outLen);
}
for (const a of allocations) {
if (a.ptr) {
ex.ca_free(a.ptr, a.len);
}
}
}
}