hakojs
Version:
A secure, embeddable JavaScript engine that runs untrusted code inside WebAssembly sandboxes with fine-grained permissions and resource limits
487 lines (484 loc) • 19.1 kB
JavaScript
var __dispose = Symbol.dispose || /* @__PURE__ */ Symbol.for("Symbol.dispose");
var __asyncDispose = Symbol.dispose || /* @__PURE__ */ Symbol.for("Symbol.dispose");
var __using = (stack, value, async) => {
if (value != null) {
if (typeof value !== "object" && typeof value !== "function")
throw TypeError('Object expected to be assigned to "using" declaration');
var dispose;
if (async)
dispose = value[__asyncDispose];
if (dispose === undefined)
dispose = value[__dispose];
if (typeof dispose !== "function")
throw TypeError("Object not disposable");
stack.push([async, dispose, value]);
} else if (async) {
stack.push([async]);
}
return value;
};
var __callDispose = (stack, error, hasError) => {
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
}, fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e), next = (it) => {
while (it = stack.pop()) {
try {
var result = it[1] && it[1].call(it[2]);
if (it[0])
return Promise.resolve(result).then(next, (e) => (fail(e), next()));
} catch (e) {
fail(e);
}
}
if (hasError)
throw error;
};
return next();
};
// src/vm/context.ts
import { HakoError } from "../etc/errors";
import {
evalOptionsToFlags
} from "../etc/types";
import { HakoDeferredPromise } from "../helpers/deferred-promise";
import { VMIterator } from "../helpers/iterator-helper";
import {
DisposableResult,
Scope
} from "../mem/lifetime";
import { VMValue } from "./value";
import { ValueFactory } from "./value-factory";
class VMContext {
container;
ctxPtr;
isReleased = false;
__runtime;
valueFactory;
_Symbol = undefined;
_SymbolIterator = undefined;
_SymbolAsyncIterator = undefined;
opaqueDataPointer = undefined;
constructor(container, runtime, ctxPtr) {
this.container = container;
this.__runtime = runtime;
this.ctxPtr = ctxPtr;
this.valueFactory = new ValueFactory(this, container);
this.container.callbacks.registerContext(ctxPtr, this);
}
get pointer() {
return this.ctxPtr;
}
get runtime() {
return this.__runtime;
}
setMaxStackSize(size) {
this.container.exports.HAKO_ContextSetMaxStackSize(this.pointer, size);
}
setVirtualStackSize(size) {
this.container.exports.HAKO_SetVirtualStackSize(this.pointer, size);
}
setOpaqueData(opaque) {
if (this.opaqueDataPointer) {
this.freeOpaqueData();
}
this.opaqueDataPointer = this.container.memory.allocateString(this.ctxPtr, opaque);
this.container.exports.HAKO_SetContextData(this.pointer, this.opaqueDataPointer);
}
getOpaqueData() {
if (!this.opaqueDataPointer) {
return;
}
return this.container.memory.readString(this.opaqueDataPointer);
}
freeOpaqueData() {
if (this.opaqueDataPointer) {
this.container.memory.freeMemory(this.ctxPtr, this.opaqueDataPointer);
this.opaqueDataPointer = undefined;
this.container.exports.HAKO_SetContextData(this.pointer, 0);
}
}
evalCode(code, options = {}) {
if (code.length === 0) {
return DisposableResult.success(this.undefined());
}
const codemem = this.container.memory.writeNullTerminatedString(this.ctxPtr, code);
let fileName = options.fileName || "file://eval";
if (!fileName.startsWith("file://")) {
fileName = `file://${fileName}`;
}
const filenamePtr = this.container.memory.allocateString(this.ctxPtr, fileName);
const flags = evalOptionsToFlags(options);
const detectModule = options.detectModule ?? false;
try {
const resultPtr = this.container.exports.HAKO_Eval(this.ctxPtr, codemem.pointer, codemem.length, filenamePtr, detectModule ? 1 : 0, flags);
const exceptionPtr = this.container.error.getLastErrorPointer(this.ctxPtr, resultPtr);
if (exceptionPtr !== 0) {
this.container.memory.freeValuePointer(this.ctxPtr, resultPtr);
return DisposableResult.fail(new VMValue(this, exceptionPtr, "owned"), (error) => this.unwrapResult(error));
}
return DisposableResult.success(new VMValue(this, resultPtr, "owned"));
} finally {
this.container.memory.freeMemory(this.ctxPtr, codemem.pointer);
this.container.memory.freeMemory(this.ctxPtr, filenamePtr);
}
}
compileToByteCode(code, options = {}) {
if (code.length === 0) {
return DisposableResult.success(new Uint8Array(0));
}
const codemem = this.container.memory.writeNullTerminatedString(this.ctxPtr, code);
let fileName = options.fileName || "eval";
if (!fileName.startsWith("file://")) {
fileName = `file://${fileName}`;
}
const filemem = this.container.memory.writeNullTerminatedString(this.ctxPtr, fileName);
const flags = evalOptionsToFlags(options);
const detectModule = options.detectModule ?? true;
const bytecodeLength = this.container.memory.allocatePointerArray(this.ctxPtr, 1);
try {
const bytecodePtr = this.container.exports.HAKO_CompileToByteCode(this.ctxPtr, codemem.pointer, codemem.length, filemem.pointer, detectModule ? 1 : 0, flags, bytecodeLength);
if (bytecodePtr === 0) {
const exceptionPtr = this.container.error.getLastErrorPointer(this.ctxPtr);
if (exceptionPtr !== 0) {
return DisposableResult.fail(new VMValue(this, exceptionPtr, "owned"), (error) => this.unwrapResult(error));
}
return DisposableResult.fail(this.newError(new Error("Compilation failed")), (error) => this.unwrapResult(error));
}
const length = this.container.memory.readPointer(bytecodeLength);
const bytecode = this.container.memory.copy(bytecodePtr, length);
this.container.memory.freeMemory(this.ctxPtr, bytecodePtr);
return DisposableResult.success(bytecode);
} finally {
this.container.memory.freeMemory(this.ctxPtr, codemem.pointer);
this.container.memory.freeMemory(this.ctxPtr, filemem.pointer);
this.container.memory.freeMemory(this.ctxPtr, bytecodeLength);
}
}
evalByteCode(bytecode, options = {}) {
if (bytecode.length === 0) {
return DisposableResult.success(this.undefined());
}
const bytecodePtr = this.container.memory.writeBytes(this.ctxPtr, bytecode);
try {
const resultPtr = this.container.exports.HAKO_EvalByteCode(this.ctxPtr, bytecodePtr, bytecode.byteLength, options.loadOnly ? 1 : 0);
const exceptionPtr = this.container.error.getLastErrorPointer(this.ctxPtr, resultPtr);
if (exceptionPtr !== 0) {
this.container.memory.freeValuePointer(this.ctxPtr, resultPtr);
return DisposableResult.fail(new VMValue(this, exceptionPtr, "owned"), (error) => this.unwrapResult(error));
}
return DisposableResult.success(new VMValue(this, resultPtr, "owned"));
} finally {
this.container.memory.freeMemory(this.ctxPtr, bytecodePtr);
}
}
unwrapResult(result) {
if (result.error) {
const context = "context" in result.error ? result.error.context : this;
const error = this.container.error.getExceptionDetails(context.ctxPtr, result.error.getHandle());
result.error.dispose();
throw error;
}
return result.value;
}
callFunction(func, thisArg = null, ...args) {
return Scope.withScope((scope) => {
if (!thisArg) {
thisArg = scope.manage(this.undefined());
}
const thisPtr = thisArg.getHandle();
let argvPtr = 0;
if (args.length > 0) {
argvPtr = this.container.memory.allocatePointerArray(this.ctxPtr, args.length);
scope.add(() => this.container.memory.freeMemory(this.ctxPtr, argvPtr));
for (let i = 0;i < args.length; i++) {
this.container.memory.writePointerToArray(argvPtr, i, args[i].getHandle());
}
}
const resultPtr = this.container.exports.HAKO_Call(this.pointer, func.getHandle(), thisPtr, args.length, argvPtr);
const exceptionPtr = this.container.error.getLastErrorPointer(this.pointer, resultPtr);
if (exceptionPtr !== 0) {
this.container.memory.freeValuePointer(this.pointer, resultPtr);
return DisposableResult.fail(new VMValue(this, exceptionPtr, "owned"), (error) => this.unwrapResult(error));
}
return DisposableResult.success(new VMValue(this, resultPtr, "owned"));
});
}
resolvePromise(promiseLikeHandle) {
let __stack = [];
try {
if (!promiseLikeHandle.isPromise()) {
throw new TypeError(`Expected a Promise-like value, received ${promiseLikeHandle.type}`);
}
const vmResolveResult = __using(__stack, Scope.withScope((scope) => {
let __stack2 = [];
try {
const global = __using(__stack2, this.getGlobalObject(), 0);
const vmPromise = scope.manage(global.getProperty("Promise"));
const vmPromiseResolve = scope.manage(vmPromise?.getProperty("resolve"));
return this.callFunction(vmPromiseResolve, vmPromise, promiseLikeHandle);
} catch (_catch2) {
var _err2 = _catch2, _hasErr2 = 1;
} finally {
__callDispose(__stack2, _err2, _hasErr2);
}
}), 0);
if (vmResolveResult.error) {
return Promise.resolve(vmResolveResult);
}
const resolvedPromise = vmResolveResult.value;
const state = resolvedPromise.getPromiseState();
if (state === "fulfilled") {
const result = resolvedPromise.getPromiseResult();
if (result) {
return Promise.resolve(this.success(result));
}
return Promise.resolve(this.success(this.newValue(undefined)));
}
if (state === "rejected") {
const error = resolvedPromise.getPromiseResult();
if (error) {
return Promise.resolve(this.fail(error));
}
return Promise.resolve(this.fail(this.newValue(undefined)));
}
return new Promise((resolve) => {
Scope.withScope((scope) => {
const resolveHandle = scope.manage(this.newFunction("resolve", (value) => {
resolve(this.success(value?.dup()));
}));
const rejectHandle = scope.manage(this.newFunction("reject", (error) => {
resolve(this.fail(error?.dup()));
}));
const promiseHandle = scope.manage(resolvedPromise);
const promiseThenHandle = scope.manage(promiseHandle.getProperty("then"));
this.callFunction(promiseThenHandle, promiseHandle, resolveHandle, rejectHandle).unwrap().dispose();
});
});
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
}
getLastError(maybe_exception) {
let pointer = 0;
if (maybe_exception === undefined) {
pointer = this.container.error.getLastErrorPointer(this.ctxPtr);
} else {
pointer = maybe_exception instanceof VMValue ? maybe_exception.getHandle() : maybe_exception;
}
if (pointer === 0) {
return;
}
return Scope.withScope((scope) => {
const isError = this.container.exports.HAKO_IsError(this.ctxPtr, pointer);
const lastError = isError ? pointer : this.container.error.getLastErrorPointer(this.ctxPtr, maybe_exception instanceof VMValue ? maybe_exception.getHandle() : maybe_exception);
if (lastError === 0) {
return;
}
scope.add(() => this.container.memory.freeValuePointer(this.ctxPtr, lastError));
return this.container.error.getExceptionDetails(this.ctxPtr, lastError);
});
}
getModuleNamespace(moduleValue) {
const resultPtr = this.container.exports.HAKO_GetModuleNamespace(this.ctxPtr, moduleValue.getHandle());
const exceptionPtr = this.container.error.getLastErrorPointer(this.ctxPtr, resultPtr);
if (exceptionPtr !== 0) {
const error = this.container.error.getExceptionDetails(this.ctxPtr, exceptionPtr);
this.container.memory.freeValuePointer(this.ctxPtr, resultPtr);
this.container.memory.freeValuePointer(this.ctxPtr, exceptionPtr);
throw error;
}
return new VMValue(this, resultPtr, "owned");
}
getGlobalObject() {
return this.valueFactory.getGlobalObject();
}
newError(error) {
return this.valueFactory.fromNativeValue(error);
}
throwError(error) {
if (typeof error === "string") {
let __stack = [];
try {
const errorObj = __using(__stack, this.newError(new Error(error)), 0);
return this.throwError(errorObj);
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
}
const exceptionPtr = this.container.exports.HAKO_Throw(this.ctxPtr, error.getHandle());
return new VMValue(this, exceptionPtr, "owned");
}
getIterator(iterableHandle) {
if (!this._SymbolIterator) {
this._SymbolIterator = this.getWellKnownSymbol("iterator");
}
const SymbolIterator = this._SymbolIterator;
return Scope.withScope((scope) => {
const methodHandle = scope.manage(iterableHandle.getProperty(SymbolIterator));
const iteratorCallResult = this.callFunction(methodHandle, iterableHandle);
if (iteratorCallResult.error) {
return iteratorCallResult;
}
return this.success(new VMIterator(iteratorCallResult.value, this));
});
}
getWellKnownSymbol(name) {
let __stack = [];
try {
if (this._Symbol) {
return this._Symbol.getProperty(name);
}
const globalObject = __using(__stack, this.getGlobalObject(), 0);
this._Symbol = globalObject.getProperty("Symbol");
return this._Symbol.getProperty(name);
} catch (_catch) {
var _err = _catch, _hasErr = 1;
} finally {
__callDispose(__stack, _err, _hasErr);
}
}
newObject() {
const ptr = this.container.exports.HAKO_NewObject(this.ctxPtr);
return new VMValue(this, ptr, "owned");
}
newObjectWithPrototype(proto) {
const ptr = this.container.exports.HAKO_NewObjectProto(this.ctxPtr, proto.getHandle());
return new VMValue(this, ptr, "owned");
}
newArray() {
return this.valueFactory.fromNativeValue([]);
}
newArrayBuffer(data) {
return this.valueFactory.fromNativeValue(data);
}
newNumber(value) {
return this.valueFactory.fromNativeValue(value);
}
newString(value) {
return this.valueFactory.fromNativeValue(value);
}
newSymbol(description, isGlobal = false) {
const key = (typeof description === "symbol" ? description.description : description) ?? "";
return this.valueFactory.fromNativeValue(isGlobal ? Symbol.for(key) : Symbol(key), {
isGlobal
});
}
dump(value) {
const cstring = this.container.exports.HAKO_Dump(this.pointer, value.getHandle());
const result = this.container.memory.readString(cstring);
this.container.memory.freeCString(this.pointer, cstring);
return JSON.parse(result);
}
newFunction(name, callback) {
return this.valueFactory.fromNativeValue(callback, { name });
}
newPromise(value) {
const deferredPromise = Scope.withScope((scope) => {
const resolveFuncsPtr = this.container.memory.allocatePointerArray(this.ctxPtr, 2);
scope.add(() => this.container.memory.freeMemory(this.ctxPtr, resolveFuncsPtr));
const promisePtr = this.container.exports.HAKO_NewPromiseCapability(this.ctxPtr, resolveFuncsPtr);
const view = new DataView(this.container.exports.memory.buffer);
const resolvePtr = view.getUint32(resolveFuncsPtr, true);
const rejectPtr = view.getUint32(resolveFuncsPtr + 4, true);
const promise = new VMValue(this, promisePtr, "owned");
const resolveFunc = new VMValue(this, resolvePtr, "owned");
const rejectFunc = new VMValue(this, rejectPtr, "owned");
return new HakoDeferredPromise({
context: this,
promiseHandle: promise,
resolveHandle: resolveFunc,
rejectHandle: rejectFunc
});
});
if (value && typeof value === "function") {
value = new Promise(value);
}
if (value) {
Promise.resolve(value).then(deferredPromise.resolve, (error) => error instanceof VMValue ? deferredPromise.reject(error) : deferredPromise.reject(this.newError(error)));
}
return deferredPromise;
}
undefined() {
return this.valueFactory.fromNativeValue(undefined);
}
null() {
return this.valueFactory.fromNativeValue(null);
}
true() {
return this.valueFactory.fromNativeValue(true);
}
false() {
return this.valueFactory.fromNativeValue(false);
}
borrowValue(ptr) {
return new VMValue(this, ptr, "borrowed");
}
duplicateValue(ptr) {
const duped = this.container.exports.HAKO_DupValuePointer(this.ctxPtr, ptr);
return new VMValue(this, duped, "owned");
}
newValue(value, options) {
return this.valueFactory.fromNativeValue(value, options);
}
bjsonEncode(value) {
return Scope.withScope((scope) => {
const lengthPtr = this.container.memory.allocatePointerArray(this.ctxPtr, 1);
scope.add(() => this.container.memory.freeMemory(this.ctxPtr, lengthPtr));
const bufferPtr = this.container.exports.HAKO_BJSON_Encode(this.ctxPtr, value.getHandle(), lengthPtr);
if (bufferPtr === 0) {
const lastError = this.getLastError();
if (lastError) {
throw new HakoError("BJSON encoding failed", { cause: lastError });
}
throw new HakoError("BJSON encoding failed");
}
const length = this.container.memory.readPointer(lengthPtr);
const result = this.container.memory.copy(bufferPtr, length);
this.container.memory.freeMemory(this.ctxPtr, bufferPtr);
return result;
});
}
bjsonDecode(data) {
return Scope.withScope((scope) => {
const bufferPtr = this.container.memory.writeBytes(this.ctxPtr, data);
scope.add(() => this.container.memory.freeMemory(this.ctxPtr, bufferPtr));
const resultPtr = this.container.exports.HAKO_BJSON_Decode(this.ctxPtr, bufferPtr, data.byteLength);
const error = this.getLastError(resultPtr);
if (error) {
throw new HakoError("BJSON decoding failed", { cause: error });
}
return new VMValue(this, resultPtr, "owned");
});
}
release() {
if (!this.isReleased) {
this.valueFactory.dispose();
this._Symbol?.dispose();
this._SymbolAsyncIterator?.dispose();
this._SymbolIterator?.dispose();
this.container.callbacks.unregisterContext(this.ctxPtr);
this.freeOpaqueData();
this.container.exports.HAKO_FreeContext(this.ctxPtr);
this.runtime.dropContext(this);
this.isReleased = true;
}
}
[Symbol.dispose]() {
this.release();
}
success(value) {
return DisposableResult.success(value);
}
fail(error) {
return DisposableResult.fail(error, (error2) => this.unwrapResult(error2));
}
}
export {
VMContext
};
//# debugId=900CDB751204665C64756E2164756E21
//# sourceMappingURL=context.js.map