From 219322c8d0640dc569aa0c0d69ae52101a475032 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 16 Jun 2026 23:15:29 +0200 Subject: [PATCH 1/2] fix: harden LLRT runtime boundary Rationale: LLRT is the intended default executor, but the native wrapper still allowed dynamic imports of host capability modules and the request bridge exposed more header surface than necessary. This tightens the sandbox boundary before downstream adoption. Rejected: Publishing native packages directly from the local macOS workspace; the existing GitHub release workflow is the safer path because it builds each platform artifact on its matching runner. Risk: This disables LLRT module loading for call_json and hides response headers from sandbox code unless explicitly allowed. Callers that depended on arbitrary dynamic imports or implicit response headers must opt into a different executor/header allowlist. Tested: mise exec -- task ci; pnpm --filter @robinbraemer/llrt run create:native-packages && pnpm --filter @robinbraemer/llrt run verify:native-artifacts; cargo check --manifest-path packages/llrt/native/Cargo.toml; cargo fmt --manifest-path packages/llrt/native/Cargo.toml; focused LLRT/codemode vitest red-green runs. --- packages/codemode/package.json | 4 +- packages/codemode/src/codemode.ts | 9 ++- packages/codemode/src/request-bridge.ts | 49 +++++++++--- packages/codemode/src/types.ts | 10 ++- packages/codemode/test/executor-contract.ts | 26 +++++++ .../codemode/test/package-publication.test.ts | 2 +- packages/codemode/test/request-bridge.test.ts | 78 +++++++++++++++++++ packages/llrt/native/Cargo.lock | 2 +- packages/llrt/native/Cargo.toml | 2 +- packages/llrt/native/src/runtime.rs | 7 +- packages/llrt/npm/darwin-arm64/package.json | 7 +- packages/llrt/npm/darwin-x64/package.json | 7 +- .../llrt/npm/linux-arm64-gnu/package.json | 7 +- packages/llrt/npm/linux-x64-gnu/package.json | 7 +- packages/llrt/package.json | 7 +- .../llrt/scripts/verify-native-artifacts.mjs | 15 ++++ packages/llrt/test/call-json.test.ts | 30 +++++++ 17 files changed, 242 insertions(+), 27 deletions(-) diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 07dd874..6a6fc3a 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,6 +1,6 @@ { "name": "@robinbraemer/codemode", - "version": "0.3.0", + "version": "0.3.1", "description": "Code Mode MCP tools from OpenAPI specs. Two tools (search + execute) replace hundreds of individual MCP tools.", "type": "module", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "url": "https://github.com/cnap-tech/codemode.git" }, "peerDependencies": { - "@robinbraemer/llrt": "^0.1.0", + "@robinbraemer/llrt": "^0.1.1", "isolated-vm": "6", "quickjs-emscripten": ">=0.31" }, diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index d7b79a0..34cd555 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -1,5 +1,9 @@ import { createExecutor } from "./executor/auto.js"; -import { createRequestBridge, type SandboxRequestOptions } from "./request-bridge.js"; +import { + createRequestBridge, + type RequestBridgeOptions, + type SandboxRequestOptions, +} from "./request-bridge.js"; import { extractTags, processSpec } from "./spec.js"; import { createExecuteToolDefinition, createSearchToolDefinition } from "./tools.js"; import { truncateResponse } from "./truncate.js"; @@ -79,7 +83,7 @@ export class CodeMode { // so the request counter resets each time. private bridgeHandler: RequestHandler; private bridgeBaseUrl: string; - private bridgeOptions: { maxRequests?: number; maxResponseBytes?: number; allowedHeaders?: string[] }; + private bridgeOptions: RequestBridgeOptions; // Cached processed spec & context for tool descriptions private processedSpec: Record | null = null; @@ -102,6 +106,7 @@ export class CodeMode { maxRequests: options.maxRequests, maxResponseBytes: options.maxResponseBytes, allowedHeaders: options.allowedHeaders, + exposedResponseHeaders: options.exposedResponseHeaders, }; } diff --git a/packages/codemode/src/request-bridge.ts b/packages/codemode/src/request-bridge.ts index a15eaf2..03c3c48 100644 --- a/packages/codemode/src/request-bridge.ts +++ b/packages/codemode/src/request-bridge.ts @@ -31,6 +31,8 @@ export interface RequestBridgeOptions { maxResponseBytes?: number; /** Allowed headers whitelist. When undefined, uses default blocklist. */ allowedHeaders?: string[]; + /** Response headers exposed to sandbox code. Default: none. */ + exposedResponseHeaders?: string[]; } const ALLOWED_METHODS = new Set([ @@ -53,6 +55,11 @@ const BLOCKED_HEADER_PATTERNS = [ /^connection$/i, /^upgrade$/i, /^te$/i, + /^forwarded$/i, + /^content-length$/i, + /^x-http-method-override$/i, + /^x-original-url$/i, + /^x-rewrite-url$/i, ]; const DEFAULT_MAX_REQUESTS = 50; @@ -139,16 +146,18 @@ function validatePath(path: string): void { */ function filterHeaders( headers: Record | undefined, - allowedHeaders: string[] | undefined, + allowedHeaders: Set | undefined, ): Record { if (!headers) return {}; + const isBlocked = (key: string) => BLOCKED_HEADER_PATTERNS.some((p) => p.test(key)); + if (allowedHeaders) { - // Whitelist mode: only forward explicitly allowed headers - const allowed = new Set(allowedHeaders.map((h) => h.toLowerCase())); + // Whitelist mode: only forward explicitly allowed headers after the + // hard denylist has removed credential, routing, and hop-by-hop headers. const filtered: Record = {}; for (const [key, value] of Object.entries(headers)) { - if (allowed.has(key.toLowerCase())) { + if (allowedHeaders.has(key.toLowerCase()) && !isBlocked(key)) { filtered[key] = value; } } @@ -158,8 +167,23 @@ function filterHeaders( // Blocklist mode: strip dangerous headers const filtered: Record = {}; for (const [key, value] of Object.entries(headers)) { - const blocked = BLOCKED_HEADER_PATTERNS.some((p) => p.test(key)); - if (!blocked) { + if (!isBlocked(key)) { + filtered[key] = value; + } + } + return filtered; +} + +function filterResponseHeaders( + headers: Headers, + exposedResponseHeaders: Set | undefined, +): Record { + if (!exposedResponseHeaders) return {}; + + const filtered: Record = {}; + for (const key of exposedResponseHeaders) { + const value = headers.get(key); + if (value !== null) { filtered[key] = value; } } @@ -183,7 +207,12 @@ export function createRequestBridge( ): RequestBridgeFn { const maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS; const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES; - const allowedHeaders = options.allowedHeaders; + const allowedHeaders = options.allowedHeaders + ? new Set(options.allowedHeaders.map((h) => h.toLowerCase())) + : undefined; + const exposedResponseHeaders = options.exposedResponseHeaders + ? new Set(options.exposedResponseHeaders.map((h) => h.toLowerCase())) + : undefined; let requestCount = 0; @@ -234,11 +263,7 @@ export function createRequestBridge( // Call the host handler const response = await handler(url.toString(), init); - // Parse response headers - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); + const responseHeaders = filterResponseHeaders(response.headers, exposedResponseHeaders); // Read response body with streaming size limit to avoid host OOM. // Abort as soon as accumulated bytes exceed the limit. diff --git a/packages/codemode/src/types.ts b/packages/codemode/src/types.ts index cd7aac9..361c3ab 100644 --- a/packages/codemode/src/types.ts +++ b/packages/codemode/src/types.ts @@ -166,11 +166,17 @@ export interface CodeModeOptions { /** * Allowed headers whitelist. When set, only these headers are forwarded. - * When undefined, a default blocklist strips dangerous headers - * (Authorization, Cookie, Host, X-Forwarded-*, Proxy-*). + * Credential, routing override, forwarding, and hop-by-hop headers are + * always stripped even when listed here. */ allowedHeaders?: string[]; + /** + * Response headers exposed to sandbox code. + * Default: none. + */ + exposedResponseHeaders?: string[]; + /** * Maximum $ref resolution depth. * Default: 50. diff --git a/packages/codemode/test/executor-contract.ts b/packages/codemode/test/executor-contract.ts index b614a79..f9a7cf6 100644 --- a/packages/codemode/test/executor-contract.ts +++ b/packages/codemode/test/executor-contract.ts @@ -209,6 +209,32 @@ export function executorContract( expect(result3.result).toBe("undefined"); }); + it("cannot dynamically import host capability modules", async () => { + const executor = factory(); + const result = await executor.execute( + `async () => { + for (const specifier of ["node:fs", "fs", "node:process"]) { + try { + const imported = await import(specifier); + if ( + typeof imported.readFileSync === "function" || + typeof imported.default?.readFileSync === "function" || + typeof imported.env === "object" + ) { + return { leaked: specifier }; + } + } catch { + // Expected: sandboxed execution cannot resolve host modules. + } + } + return { blocked: true }; + }`, + {}, + ); + expect(result.error).toBeUndefined(); + expect(result.result).toEqual({ blocked: true }); + }); + it("chains multiple async host calls", async () => { const executor = factory(); const result = await executor.execute( diff --git a/packages/codemode/test/package-publication.test.ts b/packages/codemode/test/package-publication.test.ts index e9c936e..ce25418 100644 --- a/packages/codemode/test/package-publication.test.ts +++ b/packages/codemode/test/package-publication.test.ts @@ -10,7 +10,7 @@ const publishWorkflowPath = join(root, ".github/workflows/publish.yml"); describe("codemode package publication", () => { it("publishes the LLRT executor release with a compatible optional peer range", () => { expect(codemodePackageJson.version).toMatch(/^(?!0\.2\.0$)\d+\.\d+\.\d+(?:[-+].*)?$/); - expect(codemodePackageJson.peerDependencies["@robinbraemer/llrt"]).toBe("^0.1.0"); + expect(codemodePackageJson.peerDependencies["@robinbraemer/llrt"]).toBe("^0.1.1"); expect(codemodePackageJson.devDependencies["@robinbraemer/llrt"]).toBe("workspace:*"); }); diff --git a/packages/codemode/test/request-bridge.test.ts b/packages/codemode/test/request-bridge.test.ts index c269302..1702045 100644 --- a/packages/codemode/test/request-bridge.test.ts +++ b/packages/codemode/test/request-bridge.test.ts @@ -147,6 +147,11 @@ describe("header filtering", () => { "connection": "keep-alive", "upgrade": "websocket", "te": "trailers", + "forwarded": "for=1.2.3.4;host=evil.com", + "content-length": "999", + "x-http-method-override": "DELETE", + "x-original-url": "/admin", + "x-rewrite-url": "/admin", "x-custom": "safe", "accept": "application/json", }, @@ -168,6 +173,11 @@ describe("header filtering", () => { expect(body.headers["connection"]).toBeUndefined(); expect(body.headers["upgrade"]).toBeUndefined(); expect(body.headers["te"]).toBeUndefined(); + expect(body.headers["forwarded"]).toBeUndefined(); + expect(body.headers["content-length"]).toBeUndefined(); + expect(body.headers["x-http-method-override"]).toBeUndefined(); + expect(body.headers["x-original-url"]).toBeUndefined(); + expect(body.headers["x-rewrite-url"]).toBeUndefined(); expect(body.headers["x-custom"]).toBe("safe"); expect(body.headers["accept"]).toBe("application/json"); }); @@ -194,6 +204,74 @@ describe("header filtering", () => { expect(body.headers["authorization"]).toBeUndefined(); expect(body.headers["x-custom"]).toBeUndefined(); }); + + it("never forwards protected headers even when allowedHeaders includes them", async () => { + const bridge = createRequestBridge(echoHandler, "http://localhost", { + allowedHeaders: ["authorization", "cookie", "host", "forwarded", "accept"], + }); + + const res = await bridge({ + method: "GET", + path: "/test", + headers: { + "authorization": "Bearer secret", + "cookie": "session=abc", + "host": "evil.com", + "forwarded": "for=1.2.3.4;host=evil.com", + "accept": "application/json", + }, + }); + + const body = res.body as { headers: Record }; + expect(body.headers["authorization"]).toBeUndefined(); + expect(body.headers["cookie"]).toBeUndefined(); + expect(body.headers["host"]).toBeUndefined(); + expect(body.headers["forwarded"]).toBeUndefined(); + expect(body.headers["accept"]).toBe("application/json"); + }); +}); + +describe("response header filtering", () => { + it("does not expose response headers to sandbox code by default", async () => { + const bridge = createRequestBridge( + () => + Response.json( + { ok: true }, + { + headers: { + "set-cookie": "session=secret", + "x-internal-trace": "trace-secret", + }, + }, + ), + "http://localhost", + ); + + const res = await bridge({ method: "GET", path: "/test" }); + + expect(res.headers).toEqual({}); + }); + + it("exposes only explicitly allowed response headers", async () => { + const bridge = createRequestBridge( + () => + Response.json( + { ok: true }, + { + headers: { + "etag": '"abc123"', + "set-cookie": "session=secret", + }, + }, + ), + "http://localhost", + { exposedResponseHeaders: ["etag"] }, + ); + + const res = await bridge({ method: "GET", path: "/test" }); + + expect(res.headers).toEqual({ etag: '"abc123"' }); + }); }); const largeHandler: RequestHandler = () => { diff --git a/packages/llrt/native/Cargo.lock b/packages/llrt/native/Cargo.lock index da6d868..ae0c789 100644 --- a/packages/llrt/native/Cargo.lock +++ b/packages/llrt/native/Cargo.lock @@ -1623,7 +1623,7 @@ dependencies = [ [[package]] name = "llrt_node" -version = "0.1.0" +version = "0.1.1" dependencies = [ "llrt_core", "llrt_json", diff --git a/packages/llrt/native/Cargo.toml b/packages/llrt/native/Cargo.toml index 5c6c057..8e75a98 100644 --- a/packages/llrt/native/Cargo.toml +++ b/packages/llrt/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "llrt_node" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "MIT" diff --git a/packages/llrt/native/src/runtime.rs b/packages/llrt/native/src/runtime.rs index 7dcd204..57e1cf2 100644 --- a/packages/llrt/native/src/runtime.rs +++ b/packages/llrt/native/src/runtime.rs @@ -6,7 +6,10 @@ use std::{ time::{Duration, Instant}, }; -use llrt_core::vm::{Vm, VmOptions}; +use llrt_core::{ + modules::module_builder::ModuleBuilder, + vm::{Vm, VmOptions}, +}; use llrt_json::{parse::json_parse, stringify::json_stringify}; use napi::{ bindgen_prelude::Promise as NapiPromise, bindgen_prelude::*, @@ -95,6 +98,8 @@ async fn call_json_inner( .unwrap_or(64 * 1024 * 1024); let vm = Vm::from_options(VmOptions { + module_builder: ModuleBuilder::new(), + allow_module_loading: false, max_stack_size: max_stack_bytes, ..VmOptions::default() }) diff --git a/packages/llrt/npm/darwin-arm64/package.json b/packages/llrt/npm/darwin-arm64/package.json index 06eced6..d1ba9c8 100644 --- a/packages/llrt/npm/darwin-arm64/package.json +++ b/packages/llrt/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@robinbraemer/llrt-darwin-arm64", - "version": "0.1.0", + "version": "0.1.1", "cpu": [ "arm64" ], @@ -18,6 +18,11 @@ ], "author": "Robin Braemer", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cnap-tech/codemode.git", + "directory": "packages/llrt" + }, "publishConfig": { "access": "public" }, diff --git a/packages/llrt/npm/darwin-x64/package.json b/packages/llrt/npm/darwin-x64/package.json index dcc73a9..7b656c4 100644 --- a/packages/llrt/npm/darwin-x64/package.json +++ b/packages/llrt/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@robinbraemer/llrt-darwin-x64", - "version": "0.1.0", + "version": "0.1.1", "cpu": [ "x64" ], @@ -18,6 +18,11 @@ ], "author": "Robin Braemer", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cnap-tech/codemode.git", + "directory": "packages/llrt" + }, "publishConfig": { "access": "public" }, diff --git a/packages/llrt/npm/linux-arm64-gnu/package.json b/packages/llrt/npm/linux-arm64-gnu/package.json index a947c0b..96bde61 100644 --- a/packages/llrt/npm/linux-arm64-gnu/package.json +++ b/packages/llrt/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@robinbraemer/llrt-linux-arm64-gnu", - "version": "0.1.0", + "version": "0.1.1", "cpu": [ "arm64" ], @@ -18,6 +18,11 @@ ], "author": "Robin Braemer", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cnap-tech/codemode.git", + "directory": "packages/llrt" + }, "publishConfig": { "access": "public" }, diff --git a/packages/llrt/npm/linux-x64-gnu/package.json b/packages/llrt/npm/linux-x64-gnu/package.json index 54e8b80..63d2e34 100644 --- a/packages/llrt/npm/linux-x64-gnu/package.json +++ b/packages/llrt/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@robinbraemer/llrt-linux-x64-gnu", - "version": "0.1.0", + "version": "0.1.1", "cpu": [ "x64" ], @@ -18,6 +18,11 @@ ], "author": "Robin Braemer", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cnap-tech/codemode.git", + "directory": "packages/llrt" + }, "publishConfig": { "access": "public" }, diff --git a/packages/llrt/package.json b/packages/llrt/package.json index 3a6fed5..15afb63 100644 --- a/packages/llrt/package.json +++ b/packages/llrt/package.json @@ -1,6 +1,6 @@ { "name": "@robinbraemer/llrt", - "version": "0.1.0", + "version": "0.1.1", "description": "TypeScript-friendly Node bindings for AWS LLRT.", "type": "module", "main": "./dist/index.js", @@ -46,6 +46,11 @@ ], "author": "Robin Braemer", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cnap-tech/codemode.git", + "directory": "packages/llrt" + }, "napi": { "binaryName": "llrt_node", "targets": [ diff --git a/packages/llrt/scripts/verify-native-artifacts.mjs b/packages/llrt/scripts/verify-native-artifacts.mjs index 45381b1..577208c 100644 --- a/packages/llrt/scripts/verify-native-artifacts.mjs +++ b/packages/llrt/scripts/verify-native-artifacts.mjs @@ -90,6 +90,21 @@ async function verifyNativePackage( expectArrayEqual(packageJson.os, [expectedPackage.os], `${relativePackageDir} os`); expectArrayEqual(packageJson.cpu, [expectedPackage.cpu], `${relativePackageDir} cpu`); expectEqual(packageJson.license, rootPackageJson.license, `${relativePackageDir} license`); + expectEqual( + packageJson.repository?.type, + rootPackageJson.repository?.type, + `${relativePackageDir} repository.type`, + ); + expectEqual( + packageJson.repository?.url, + rootPackageJson.repository?.url, + `${relativePackageDir} repository.url`, + ); + expectEqual( + packageJson.repository?.directory, + rootPackageJson.repository?.directory, + `${relativePackageDir} repository.directory`, + ); expectEqual( packageJson.publishConfig?.access, rootPackageJson.publishConfig?.access, diff --git a/packages/llrt/test/call-json.test.ts b/packages/llrt/test/call-json.test.ts index 1a9c555..d5673e8 100644 --- a/packages/llrt/test/call-json.test.ts +++ b/packages/llrt/test/call-json.test.ts @@ -94,6 +94,36 @@ describe("LlrtRuntime.callJson native execution", () => { expect(second).toMatchObject({ ok: true, value: "clean" }); }); + it("denies dynamic imports of host capability modules", async () => { + const runtime = new LlrtRuntime({ wallTimeMs: 1000, memoryMB: 8 }); + + const result = await runtime.callJson< + Record, + { blocked: true } | { leaked: string } + >( + `async () => { + for (const specifier of ["node:fs", "fs", "node:process"]) { + try { + const imported = await import(specifier); + if ( + typeof imported.readFileSync === "function" || + typeof imported.default?.readFileSync === "function" || + typeof imported.env === "object" + ) { + return { leaked: specifier }; + } + } catch { + // Expected: untrusted execution cannot resolve host modules. + } + } + return { blocked: true }; + }`, + {}, + ); + + expect(result).toMatchObject({ ok: true, value: { blocked: true } }); + }); + it("returns a typed timeout when guest code exceeds the wall-time limit", async () => { const runtime = new LlrtRuntime({ wallTimeMs: 1, memoryMB: 8 }); From bf4fc4689a7568cba61b894882521d75578cb35f Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 16 Jun 2026 23:26:45 +0200 Subject: [PATCH 2/2] fix: apply LLRT vendor patch during prepare Rationale: CI refreshes the pinned LLRT checkout before native target builds, which erased the local vm.rs hardening patch. Track the patch in the package and apply it idempotently after checkout so native release builds use the same hardened source. Risk: prepare:llrt-source now fails if the patch no longer applies to the pinned upstream revision, which is intentional because the native runtime hardening would otherwise silently disappear. Tested: git -C packages/llrt/vendor/llrt apply --reverse packages/llrt/patches/disable-default-module-loading.patch && pnpm --filter @robinbraemer/llrt run prepare:llrt-source && git -C packages/llrt/vendor/llrt diff --check && LLRT_TARGET=aarch64-apple-darwin pnpm --filter @robinbraemer/llrt run build:native:target; mise exec -- task ci. --- .../disable-default-module-loading.patch | 98 +++++++++++++++++++ packages/llrt/scripts/prepare-llrt-source.mjs | 27 +++++ 2 files changed, 125 insertions(+) create mode 100644 packages/llrt/patches/disable-default-module-loading.patch diff --git a/packages/llrt/patches/disable-default-module-loading.patch b/packages/llrt/patches/disable-default-module-loading.patch new file mode 100644 index 0000000..16a39f1 --- /dev/null +++ b/packages/llrt/patches/disable-default-module-loading.patch @@ -0,0 +1,98 @@ +diff --git a/llrt_core/src/vm.rs b/llrt_core/src/vm.rs +index c323af3..88f7b6c 100644 +--- a/llrt_core/src/vm.rs ++++ b/llrt_core/src/vm.rs +@@ -34,6 +34,7 @@ pub struct Vm { + + pub struct VmOptions { + pub module_builder: ModuleBuilder, ++ pub allow_module_loading: bool, + pub max_stack_size: usize, + pub gc_threshold_mb: usize, + } +@@ -58,6 +59,7 @@ impl Default for VmOptions { + + Self { + module_builder, ++ allow_module_loading: true, + max_stack_size: 512 * 1024, + gc_threshold_mb: { + const DEFAULT_GC_THRESHOLD_MB: usize = 20; +@@ -82,40 +84,48 @@ impl Vm { + http::init()?; + security::init()?; + +- let mut file_resolver = FileResolver::default(); +- let mut paths: Vec<&str> = Vec::with_capacity(10); +- +- paths.push("."); +- +- let task_root = env::var(Self::ENV_LAMBDA_TASK_ROOT).unwrap_or_else(|_| String::from("")); +- let task_root = task_root.as_str(); +- if cfg!(debug_assertions) { +- paths.push("bundle"); +- } else { +- paths.push("/opt"); +- } +- +- if !task_root.is_empty() { +- paths.push(task_root); +- } +- +- for path in paths.iter() { +- file_resolver.add_path(*path); +- } +- + let (module_resolver, module_loader, global_attachment) = vm_options.module_builder.build(); +- let resolver = ( +- module_resolver, +- EmbeddedResolver, +- PackageResolver, +- file_resolver, +- ); +- let loader = (module_loader, EmbeddedLoader, PackageLoader); + + let runtime = AsyncRuntime::new()?; + runtime.set_max_stack_size(vm_options.max_stack_size).await; + runtime.set_gc_threshold(vm_options.gc_threshold_mb).await; +- runtime.set_loader(resolver, loader).await; ++ if vm_options.allow_module_loading { ++ let mut file_resolver = FileResolver::default(); ++ let mut paths: Vec<&str> = Vec::with_capacity(10); ++ ++ paths.push("."); ++ ++ let task_root = ++ env::var(Self::ENV_LAMBDA_TASK_ROOT).unwrap_or_else(|_| String::from("")); ++ let task_root = task_root.as_str(); ++ if cfg!(debug_assertions) { ++ paths.push("bundle"); ++ } else { ++ paths.push("/opt"); ++ } ++ ++ if !task_root.is_empty() { ++ paths.push(task_root); ++ } ++ ++ for path in paths.iter() { ++ file_resolver.add_path(*path); ++ } ++ ++ runtime ++ .set_loader( ++ ( ++ module_resolver, ++ EmbeddedResolver, ++ PackageResolver, ++ file_resolver, ++ ), ++ (module_loader, EmbeddedLoader, PackageLoader), ++ ) ++ .await; ++ } else { ++ runtime.set_loader(module_resolver, module_loader).await; ++ } + + let ctx = AsyncContext::full(&runtime).await?; + ctx.with(|ctx| { diff --git a/packages/llrt/scripts/prepare-llrt-source.mjs b/packages/llrt/scripts/prepare-llrt-source.mjs index 00593eb..7a5e78d 100644 --- a/packages/llrt/scripts/prepare-llrt-source.mjs +++ b/packages/llrt/scripts/prepare-llrt-source.mjs @@ -10,9 +10,11 @@ const LLRT_REVISION = "80c113ddee03ff1926068193f50fe35f41ca2105"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const vendorRoot = resolve(packageRoot, "vendor"); const llrtRoot = resolve(vendorRoot, "llrt"); +const patchesRoot = resolve(packageRoot, "patches"); await mkdir(vendorRoot, { recursive: true }); await ensureCheckout(); +await applyLocalPatches(); await ensureGeneratedBundle(); async function ensureCheckout() { @@ -34,6 +36,31 @@ async function ensureCheckout() { run("git", ["checkout", "--detach", LLRT_REVISION], llrtRoot); } +async function applyLocalPatches() { + applyPatchOnce(resolve(patchesRoot, "disable-default-module-loading.patch")); +} + +function applyPatchOnce(patchPath) { + const checkResult = spawnSync("git", ["apply", "--check", patchPath], { + cwd: llrtRoot, + stdio: "ignore", + }); + if (checkResult.status === 0) { + run("git", ["apply", patchPath], llrtRoot); + return; + } + + const reverseCheckResult = spawnSync("git", ["apply", "--reverse", "--check", patchPath], { + cwd: llrtRoot, + stdio: "ignore", + }); + if (reverseCheckResult.status === 0) { + return; + } + + throw new Error(`Unable to apply LLRT patch: ${patchPath}`); +} + async function ensureGeneratedBundle() { if (existsSync(resolve(llrtRoot, "bundle/js/@llrt/std.js"))) { return;