UNPKG

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
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