Admit checked restricted artifacts without individual code approvals
This commit is contained in:
+1
-12
@@ -96,18 +96,7 @@ let
|
|||||||
esbuild = "${pkgs.esbuild}/bin/esbuild";
|
esbuild = "${pkgs.esbuild}/bin/esbuild";
|
||||||
shell = "${pkgs.runtimeShell}";
|
shell = "${pkgs.runtimeShell}";
|
||||||
serviceAdapter = ./portable-service.mjs;
|
serviceAdapter = ./portable-service.mjs;
|
||||||
restricted =
|
restrictedSDK = "${(import ./restricted-execution { inherit pkgs core; }).sdk}/core.mjs";
|
||||||
if service.restricted or false then
|
|
||||||
let
|
|
||||||
executor = import ./restricted-execution { inherit pkgs core; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
runtime = "${executor.runtime}/bin/quixos-restricted-registry";
|
|
||||||
sdk = "${executor.sdk}/core.mjs";
|
|
||||||
limits = service.limits or { };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
null;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
compiled =
|
compiled =
|
||||||
|
|||||||
+41
-4
@@ -128,8 +128,7 @@ for (const { target, mode, settings } of builds) {
|
|||||||
const bundlePath =
|
const bundlePath =
|
||||||
mode === "remote" ? "share/quixos/remote/server/module.mjs" : `share/quixos/portable/${target}/module.mjs`;
|
mode === "remote" ? "share/quixos/remote/server/module.mjs" : `share/quixos/portable/${target}/module.mjs`;
|
||||||
await fs.mkdir(path.dirname(path.join(output, bundlePath)), { recursive: true });
|
await fs.mkdir(path.dirname(path.join(output, bundlePath)), { recursive: true });
|
||||||
const restricted = !!config.restricted && target === "server";
|
const coreImport = target === "browser" ? coreURL : coreServer;
|
||||||
const coreImport = target === "browser" ? coreURL : restricted ? config.restricted.sdk : coreServer;
|
|
||||||
const metadata = path.join(work, "bundle-meta.json");
|
const metadata = path.join(work, "bundle-meta.json");
|
||||||
measureBuild("portable-bundle", { package: pkg.revision, target, mode }, () =>
|
measureBuild("portable-bundle", { package: pkg.revision, target, mode }, () =>
|
||||||
execFileSync(
|
execFileSync(
|
||||||
@@ -137,7 +136,7 @@ for (const { target, mode, settings } of builds) {
|
|||||||
[
|
[
|
||||||
entry,
|
entry,
|
||||||
"--bundle",
|
"--bundle",
|
||||||
`--platform=${target === "browser" || restricted ? "browser" : "node"}`,
|
`--platform=${target === "browser" ? "browser" : "node"}`,
|
||||||
"--format=esm",
|
"--format=esm",
|
||||||
"--minify-whitespace",
|
"--minify-whitespace",
|
||||||
"--target=es2024",
|
"--target=es2024",
|
||||||
@@ -159,7 +158,7 @@ for (const { target, mode, settings } of builds) {
|
|||||||
(i) =>
|
(i) =>
|
||||||
i.external &&
|
i.external &&
|
||||||
i.path !== coreImport &&
|
i.path !== coreImport &&
|
||||||
!(target === "server" && !restricted && builtinModules.includes(i.path.replace(/^node:/, ""))),
|
!(target === "server" && builtinModules.includes(i.path.replace(/^node:/, ""))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -182,6 +181,44 @@ for (const { target, mode, settings } of builds) {
|
|||||||
files: [await recordFile(bundlePath, "text/javascript"), ...sharedFiles],
|
files: [await recordFile(bundlePath, "text/javascript"), ...sharedFiles],
|
||||||
};
|
};
|
||||||
implementations.push({ ...body, contentDigest: compiler.implementationDigest(body) });
|
implementations.push({ ...body, contentDigest: compiler.implementationDigest(body) });
|
||||||
|
if (target === "server" && mode !== "remote" && exports.length) {
|
||||||
|
// A closed candidate is not an eligibility claim. The host checks these
|
||||||
|
// bytes and its exact qualified SDK before admitting it to the sandbox.
|
||||||
|
// Keep a normal service implementation for unsupported dependencies.
|
||||||
|
const restrictedPath = "share/quixos/restricted/module.mjs";
|
||||||
|
const sdkPath = "share/quixos/restricted/core.mjs";
|
||||||
|
const sdkName = "quixos:restricted-sdk/v1";
|
||||||
|
await fs.mkdir(path.dirname(path.join(output, restrictedPath)), { recursive: true });
|
||||||
|
await fs.copyFile(config.restrictedSDK, path.join(output, sdkPath));
|
||||||
|
measureBuild("restricted-bundle", { package: pkg.revision }, () =>
|
||||||
|
execFileSync(
|
||||||
|
config.esbuild,
|
||||||
|
[
|
||||||
|
entry,
|
||||||
|
"--bundle",
|
||||||
|
"--platform=node",
|
||||||
|
"--format=esm",
|
||||||
|
"--minify-whitespace",
|
||||||
|
"--target=es2024",
|
||||||
|
`--outfile=${path.join(output, restrictedPath)}`,
|
||||||
|
`--alias:@quixos/camino-replica-core=${sdkName}`,
|
||||||
|
`--external:${sdkName}`,
|
||||||
|
],
|
||||||
|
{ cwd: work, stdio: "inherit" },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const restricted = {
|
||||||
|
id: "restricted-module",
|
||||||
|
target: "server",
|
||||||
|
compatibility: "quixos-restricted-module-v1",
|
||||||
|
path: restrictedPath,
|
||||||
|
ports: "quixos-transaction-ports-v1",
|
||||||
|
registryExport: settings.registryExport,
|
||||||
|
exports,
|
||||||
|
files: [await recordFile(restrictedPath, "text/javascript"), await recordFile(sdkPath, "text/javascript")],
|
||||||
|
};
|
||||||
|
implementations.push({ ...restricted, contentDigest: compiler.implementationDigest(restricted) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const components =
|
const components =
|
||||||
config.group && !config.components
|
config.group && !config.components
|
||||||
|
|||||||
+4
-57
@@ -13,62 +13,6 @@ export async function buildService({
|
|||||||
recordFile,
|
recordFile,
|
||||||
}) {
|
}) {
|
||||||
const relative = compiler.artifactPath;
|
const relative = compiler.artifactPath;
|
||||||
if (config.restricted) {
|
|
||||||
if (config.service.entry || pkg.exports.some((e) => e.execution.kind === "remote"))
|
|
||||||
throw Error(
|
|
||||||
"Restricted services require portable snapshot/transaction exports; remote/custom services keep their VM path",
|
|
||||||
);
|
|
||||||
const modules = implementations.filter((i) => i.target === "server");
|
|
||||||
if (modules.length !== 1 || pkg.exports.some((e) => !modules[0].exports.includes(e.id)))
|
|
||||||
throw Error("Restricted service requires exactly one checked portable registry covering all exports");
|
|
||||||
const limits = config.restricted.limits;
|
|
||||||
if (!limits || typeof limits !== "object" || Array.isArray(limits))
|
|
||||||
throw Error("Restricted limits must be an object");
|
|
||||||
if (Object.keys(limits).some((key) => !["deadlineMs", "maxBytes", "maxPortCalls", "maxQueued"].includes(key)))
|
|
||||||
throw Error("Unknown restricted service limit");
|
|
||||||
for (const [key, min, max] of [
|
|
||||||
["deadlineMs", 1, 60000],
|
|
||||||
["maxBytes", 1024, 4194304],
|
|
||||||
["maxPortCalls", 1, 10000],
|
|
||||||
["maxQueued", 0, 64],
|
|
||||||
])
|
|
||||||
if (limits[key] !== undefined && (!Number.isSafeInteger(limits[key]) || limits[key] < min || limits[key] > max))
|
|
||||||
throw Error("Invalid restricted service limit " + key);
|
|
||||||
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
|
||||||
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
|
||||||
const configFile = "share/quixos/service/restricted.json";
|
|
||||||
const module = modules[0];
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(output, configFile),
|
|
||||||
JSON.stringify({
|
|
||||||
module: path.relative("share/quixos/service", module.path),
|
|
||||||
registryExport: module.registryExport,
|
|
||||||
exports: module.exports,
|
|
||||||
sdk: [{ name: config.restricted.sdk, path: config.restricted.sdk }],
|
|
||||||
...limits,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(output, "bin/service"),
|
|
||||||
`#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.restricted.runtime} "$service_root/${configFile}"\n`,
|
|
||||||
{ mode: 0o755 },
|
|
||||||
);
|
|
||||||
const body = {
|
|
||||||
id: "service",
|
|
||||||
target: "server",
|
|
||||||
compatibility: "quixos-service-v1",
|
|
||||||
path: "bin/service",
|
|
||||||
ports: "quixos-transaction-ports-v1",
|
|
||||||
exports: module.exports,
|
|
||||||
files: [
|
|
||||||
await recordFile("bin/service", "text/x-shellscript"),
|
|
||||||
await recordFile(configFile, "application/json"),
|
|
||||||
...module.files,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
implementations.unshift({ ...body, contentDigest: compiler.implementationDigest(body) });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pkg.exports.length || config.service.entry) {
|
if (pkg.exports.length || config.service.entry) {
|
||||||
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
||||||
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
||||||
@@ -78,7 +22,10 @@ export async function buildService({
|
|||||||
throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging");
|
throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging");
|
||||||
serviceEntry = path.join(config.source, relative(config.service.entry));
|
serviceEntry = path.join(config.source, relative(config.service.entry));
|
||||||
} else {
|
} else {
|
||||||
const modules = implementations.filter((implementation) => implementation.target === "server");
|
const modules = implementations.filter(
|
||||||
|
(implementation) =>
|
||||||
|
implementation.target === "server" && implementation.compatibility === "quixos-js-module-v1",
|
||||||
|
);
|
||||||
if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id))))
|
if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id))))
|
||||||
throw Error("Generated service does not cover every package export; provide a custom service");
|
throw Error("Generated service does not cover every package export; provide a custom service");
|
||||||
if (!modules.length) throw Error("Generated service requires a checked registry");
|
if (!modules.length) throw Error("Generated service requires a checked registry");
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ let
|
|||||||
runtime = nativeProbe;
|
runtime = nativeProbe;
|
||||||
executable = "${nativeProbe}/bin/probe";
|
executable = "${nativeProbe}/bin/probe";
|
||||||
};
|
};
|
||||||
|
identityLauncher =
|
||||||
|
pkgs.runCommand "quixos-restricted-identity-launcher"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = [ pkgs.stdenv.cc ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
mkdir -p "$out/bin"
|
||||||
|
$CC -O2 -Wall -Wextra -Werror ${./identity-launcher.c} \
|
||||||
|
-DSANDBOX_EXECUTABLE='"${sandbox}/bin/quixos-restricted-sandbox"' \
|
||||||
|
-o "$out/bin/quixos-restricted-engine"
|
||||||
|
'';
|
||||||
# Transaction-port/value adapters do not need the browser's CRDT/Wasm engine.
|
# Transaction-port/value adapters do not need the browser's CRDT/Wasm engine.
|
||||||
# Keep this surface explicit; it is bundled into checked restricted handlers.
|
# Keep this surface explicit; it is bundled into checked restricted handlers.
|
||||||
sdk = pkgs.runCommand "quixos-restricted-sdk" { nativeBuildInputs = [ pkgs.esbuild ]; } ''
|
sdk = pkgs.runCommand "quixos-restricted-sdk" { nativeBuildInputs = [ pkgs.esbuild ]; } ''
|
||||||
@@ -61,6 +72,7 @@ in
|
|||||||
inherit
|
inherit
|
||||||
workerd
|
workerd
|
||||||
sandbox
|
sandbox
|
||||||
|
identityLauncher
|
||||||
nativeProbeSandbox
|
nativeProbeSandbox
|
||||||
sdk
|
sdk
|
||||||
runtime
|
runtime
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <grp.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* Installed only as a narrowly executable NixOS wrapper. This trusted helper
|
||||||
|
* accepts no executable, environment, filesystem root or arbitrary UID. The
|
||||||
|
* caller is platform authority, never a workspace guest or authored Worker. */
|
||||||
|
static void fail(const char *message) {
|
||||||
|
fprintf(stderr, "RESTRICTED_IDENTITY: %s\n", message);
|
||||||
|
exit(125);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *root = "/run/quixos-restricted-engines/";
|
||||||
|
uid_t caller = getuid();
|
||||||
|
gid_t group = getgid();
|
||||||
|
struct passwd *owner = getpwnam("quixos-graph");
|
||||||
|
if (geteuid() != 0) fail("privileged wrapper unavailable");
|
||||||
|
if (!owner) fail("authority identity unavailable");
|
||||||
|
if (caller != owner->pw_uid) fail("caller identity mismatch");
|
||||||
|
if (argc != 3) fail("invalid arguments");
|
||||||
|
char *end;
|
||||||
|
unsigned long number = strtoul(argv[2], &end, 10);
|
||||||
|
if (!*argv[2] || *end || number < 610000 || number >= 610064)
|
||||||
|
fail("invalid engine identity");
|
||||||
|
uid_t identity = (uid_t)number;
|
||||||
|
if (strlen(argv[1]) != strlen(root) + 16 || strncmp(argv[1], root, strlen(root)))
|
||||||
|
fail("invalid engine directory");
|
||||||
|
const char *name = argv[1] + strlen(root);
|
||||||
|
if (strncmp(name, "qx-engine-", 10) ||
|
||||||
|
strlen(name) != 16 || strspn(name + 10, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") != 6)
|
||||||
|
fail("invalid engine directory");
|
||||||
|
int parent = open("/run/quixos-restricted-engines", O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||||
|
struct stat info;
|
||||||
|
if (parent < 0 || fstat(parent, &info) || info.st_uid != caller || (info.st_mode & 066))
|
||||||
|
fail("invalid engine root");
|
||||||
|
int directory = openat(parent, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||||
|
if (directory < 0 || fstat(directory, &info) || info.st_uid != caller || (info.st_mode & 077))
|
||||||
|
fail("invalid engine owner");
|
||||||
|
DIR *entries = fdopendir(dup(directory));
|
||||||
|
if (!entries) fail("engine entries");
|
||||||
|
struct dirent *entry;
|
||||||
|
unsigned int files = 0;
|
||||||
|
while ((entry = readdir(entries))) {
|
||||||
|
const char *file = entry->d_name;
|
||||||
|
if (!strcmp(file, ".") || !strcmp(file, "..")) continue;
|
||||||
|
int socket = !strcmp(file, "broker.sock");
|
||||||
|
if ((!socket && strcmp(file, "config.capnp") && strcmp(file, "parent.mjs")) ||
|
||||||
|
fstatat(directory, file, &info, AT_SYMLINK_NOFOLLOW) || info.st_uid != caller ||
|
||||||
|
info.st_nlink != 1 || (socket ? !S_ISSOCK(info.st_mode) : !S_ISREG(info.st_mode)) ||
|
||||||
|
info.st_size > 4 * 1024 * 1024)
|
||||||
|
fail("unexpected engine file");
|
||||||
|
files++;
|
||||||
|
}
|
||||||
|
closedir(entries);
|
||||||
|
if (files != 3) fail("incomplete engine configuration");
|
||||||
|
const char *files_to_transfer[] = {"config.capnp", "parent.mjs", "broker.sock"};
|
||||||
|
for (size_t i = 0; i < 3; i++) {
|
||||||
|
if (fchownat(directory, files_to_transfer[i], identity, group, AT_SYMLINK_NOFOLLOW) ||
|
||||||
|
fchmodat(directory, files_to_transfer[i], 0660, 0))
|
||||||
|
fail("engine file ownership");
|
||||||
|
}
|
||||||
|
if (fchown(directory, identity, group) || fchmod(directory, 0770))
|
||||||
|
fail("engine directory ownership");
|
||||||
|
close(directory);
|
||||||
|
close(parent);
|
||||||
|
struct rlimit core = {0, 0};
|
||||||
|
if (setrlimit(RLIMIT_CORE, &core) || setgroups(0, NULL) ||
|
||||||
|
setresgid(identity, identity, identity) || setresuid(identity, identity, identity) ||
|
||||||
|
prctl(PR_SET_DUMPABLE, 0) || prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
|
||||||
|
fail("identity confinement");
|
||||||
|
/* The containing directory grants only its owner and authority access. A
|
||||||
|
* socket created by workerd must also admit authority's different UID/GID. */
|
||||||
|
umask(0000);
|
||||||
|
char *args[] = {SANDBOX_EXECUTABLE, argv[1], NULL};
|
||||||
|
clearenv();
|
||||||
|
execv(SANDBOX_EXECUTABLE, args);
|
||||||
|
fail("sandbox launch");
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
executor,
|
||||||
|
enable,
|
||||||
|
controlGroup,
|
||||||
|
}:
|
||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
config = lib.mkIf enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = lib.all (user: user.uid == null || user.uid < 610000 || user.uid >= 610064) (
|
||||||
|
builtins.attrValues config.users.users
|
||||||
|
);
|
||||||
|
message = "UIDs 610000–610063 are reserved for restricted engine identities.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /run/quixos-restricted-engines 0711 quixos-graph ${controlGroup} -"
|
||||||
|
];
|
||||||
|
security.wrappers.quixos-restricted-engine = {
|
||||||
|
source = "${executor.identityLauncher}/bin/quixos-restricted-engine";
|
||||||
|
owner = "root";
|
||||||
|
group = controlGroup;
|
||||||
|
permissions = "u+rx,g+rx,o-rwx";
|
||||||
|
setuid = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ int main(int argc, char **argv) {
|
|||||||
struct stat info;
|
struct stat info;
|
||||||
if (argc != 2 || argv[1][0] != '/' || strstr(argv[1], "/../") ||
|
if (argc != 2 || argv[1][0] != '/' || strstr(argv[1], "/../") ||
|
||||||
lstat(argv[1], &info) || !S_ISDIR(info.st_mode) ||
|
lstat(argv[1], &info) || !S_ISDIR(info.st_mode) ||
|
||||||
info.st_uid != getuid() || (info.st_mode & 077))
|
info.st_uid != getuid() || (info.st_mode & 007))
|
||||||
fail("invalid engine directory");
|
fail("invalid engine directory");
|
||||||
char gate;
|
char gate;
|
||||||
if (read(STDIN_FILENO, &gate, 1) != 1 || gate != 1)
|
if (read(STDIN_FILENO, &gate, 1) != 1 || gate != 1)
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export class RestrictedEngineBudget {
|
|||||||
})());
|
})());
|
||||||
release.attach = resource.attach;
|
release.attach = resource.attach;
|
||||||
release.usage = resource.usage;
|
release.usage = resource.usage;
|
||||||
|
release.kill = resource.kill;
|
||||||
|
release.uid = resource.uid;
|
||||||
return release;
|
return release;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#leases.delete(lease);
|
this.#leases.delete(lease);
|
||||||
@@ -82,6 +84,7 @@ export async function serveRestrictedRegistry(config, io = process) {
|
|||||||
!absolute(config.worker) ||
|
!absolute(config.worker) ||
|
||||||
!absolute(config.module) ||
|
!absolute(config.module) ||
|
||||||
(config.sandbox !== undefined && !absolute(config.sandbox)) ||
|
(config.sandbox !== undefined && !absolute(config.sandbox)) ||
|
||||||
|
(config.sandboxIdentity !== undefined && typeof config.sandboxIdentity !== "boolean") ||
|
||||||
!/^[$A-Z_a-z][$\w]*$/.test(config.registryExport) ||
|
!/^[$A-Z_a-z][$\w]*$/.test(config.registryExport) ||
|
||||||
!Array.isArray(config.exports) ||
|
!Array.isArray(config.exports) ||
|
||||||
!config.exports.length ||
|
!config.exports.length ||
|
||||||
@@ -280,10 +283,13 @@ export async function serveRestrictedRegistry(config, io = process) {
|
|||||||
if (index >= 0) idleEngines.splice(index, 1);
|
if (index >= 0) idleEngines.splice(index, 1);
|
||||||
engine.current = null;
|
engine.current = null;
|
||||||
if (engine.child?.pid) {
|
if (engine.child?.pid) {
|
||||||
try {
|
if (engine.release.kill) await engine.release.kill();
|
||||||
process.kill(-engine.child.pid, "SIGKILL");
|
else {
|
||||||
} catch (error) {
|
try {
|
||||||
if (error.code !== "ESRCH") throw error;
|
process.kill(-engine.child.pid, "SIGKILL");
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ESRCH") throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await engine.exited;
|
await engine.exited;
|
||||||
}
|
}
|
||||||
@@ -397,9 +403,13 @@ export async function serveRestrictedRegistry(config, io = process) {
|
|||||||
const file = join(engine.directory, "config.capnp");
|
const file = join(engine.directory, "config.capnp");
|
||||||
await writeFile(file, configuration);
|
await writeFile(file, configuration);
|
||||||
if (!call.open) return;
|
if (!call.open) return;
|
||||||
|
if (config.sandboxIdentity && !Number.isSafeInteger(engine.release.uid))
|
||||||
|
throw fixedError("RESTRICTED_IDENTITY_UNAVAILABLE");
|
||||||
engine.child = spawn(
|
engine.child = spawn(
|
||||||
config.sandbox ?? config.workerd,
|
config.sandbox ?? config.workerd,
|
||||||
config.sandbox ? [engine.directory] : ["serve", "--experimental", file],
|
config.sandbox
|
||||||
|
? [engine.directory, ...(config.sandboxIdentity ? [String(engine.release.uid)] : [])]
|
||||||
|
: ["serve", "--experimental", file],
|
||||||
{
|
{
|
||||||
detached: true,
|
detached: true,
|
||||||
env: { TZ: "UTC", LANG: "C.UTF-8" },
|
env: { TZ: "UTC", LANG: "C.UTF-8" },
|
||||||
@@ -414,16 +424,16 @@ export async function serveRestrictedRegistry(config, io = process) {
|
|||||||
});
|
});
|
||||||
engine.child.stdin?.on("error", () => engine.current?.terminate("RESTRICTED_START_FAILED"));
|
engine.child.stdin?.on("error", () => engine.current?.terminate("RESTRICTED_START_FAILED"));
|
||||||
call.child = engine.child;
|
call.child = engine.child;
|
||||||
await engine.release.attach?.(engine.child.pid);
|
|
||||||
if (config.sandbox && call.open) engine.child.stdin.end(Buffer.from([1]));
|
|
||||||
watchCPU(call);
|
|
||||||
let diagnostics = 0;
|
let diagnostics = 0;
|
||||||
engine.child.stderr.on("data", (chunk) => {
|
engine.child.stderr.on("data", (chunk) => {
|
||||||
if ((diagnostics += chunk.length) > 65536) {
|
if ((diagnostics += chunk.length) > 65536) {
|
||||||
if (engine.current?.open) engine.current.terminate("RESTRICTED_DIAGNOSTIC_LIMIT");
|
if (engine.current?.open) engine.current.terminate("RESTRICTED_DIAGNOSTIC_LIMIT");
|
||||||
else void destroyEngine(engine).catch(fail);
|
else void destroyEngine(engine).catch(fail);
|
||||||
}
|
} else config.diagnostics?.(chunk);
|
||||||
});
|
});
|
||||||
|
await engine.release.attach?.(engine.child.pid);
|
||||||
|
if (config.sandbox && call.open) engine.child.stdin.end(Buffer.from([1]));
|
||||||
|
watchCPU(call);
|
||||||
engine.child.once("error", () => engine.current?.terminate("RESTRICTED_START_FAILED"));
|
engine.child.once("error", () => engine.current?.terminate("RESTRICTED_START_FAILED"));
|
||||||
void engine.exited.then(() => {
|
void engine.exited.then(() => {
|
||||||
if (engine.current?.open) engine.current.terminate("RESTRICTED_ENGINE_EXITED");
|
if (engine.current?.open) engine.current.terminate("RESTRICTED_ENGINE_EXITED");
|
||||||
@@ -535,6 +545,7 @@ export async function serveRestrictedRegistry(config, io = process) {
|
|||||||
await finish(call, fixedError("INVOCATION_ENDED"), reply);
|
await finish(call, fixedError("INVOCATION_ENDED"), reply);
|
||||||
metrics.completed++;
|
metrics.completed++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
config.diagnostics?.(Buffer.from(`RESTRICTED_HOST_ERROR:${error.code ?? error.name}\n`));
|
||||||
if (call.open) {
|
if (call.open) {
|
||||||
const code = /^RESTRICTED_[A-Z_]+$/.test(error.code ?? "") ? error.code : "RESTRICTED_PACKAGE_FAILED";
|
const code = /^RESTRICTED_[A-Z_]+$/.test(error.code ?? "") ? error.code : "RESTRICTED_PACKAGE_FAILED";
|
||||||
await finish(call, error, { invocation: message.invocation, status: "failed", error: { code, message: code } });
|
await finish(call, error, { invocation: message.invocation, status: "failed", error: { code, message: code } });
|
||||||
|
|||||||
@@ -437,15 +437,21 @@ test("bounded FIFO rejects overload without losing active or queued frames", asy
|
|||||||
test("queue wait consumes the deadline and cancelled work never reaches its ports", async () => {
|
test("queue wait consumes the deadline and cancelled work never reaches its ports", async () => {
|
||||||
const f = await fixture(
|
const f = await fixture(
|
||||||
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:null});"),
|
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:null});"),
|
||||||
{ deadlineMs: 350, maxActive: 1 },
|
{ deadlineMs: 4000, maxActive: 1 },
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const first = f.start();
|
const first = f.start();
|
||||||
await f.receive();
|
assert.equal((await f.receive()).kind, "port");
|
||||||
const queued = f.start();
|
// Expire while the first call is deliberately blocked. Two equal relative
|
||||||
const replies = [await f.receive(), await f.receive()];
|
// deadlines let fast startup legitimately begin the queued call first.
|
||||||
assert.deepEqual(new Set(replies.map((x) => x.invocation)), new Set([first.invocation, queued.invocation]));
|
const queued = f.start(null, { root: "queued-deadline", deadlineMs: 50, depth: 1, calls: 10 });
|
||||||
assert.ok(replies.every((x) => x.error.code === "RESTRICTED_DEADLINE"));
|
const expired = await f.receive();
|
||||||
|
assert.equal(expired.invocation, queued.invocation);
|
||||||
|
assert.equal(expired.error.code, "RESTRICTED_DEADLINE");
|
||||||
|
f.send({ kind: "cancel", ...first });
|
||||||
|
const cancelled = await f.receive();
|
||||||
|
assert.equal(cancelled.invocation, first.invocation);
|
||||||
|
assert.equal(cancelled.kind, "cancelled");
|
||||||
const fresh = f.start();
|
const fresh = f.start();
|
||||||
const port = await f.receive();
|
const port = await f.receive();
|
||||||
assert.equal(port.invocation, fresh.invocation);
|
assert.equal(port.invocation, fresh.invocation);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pinned upstream engine. This binary supplies an import/capability boundary;
|
# Pinned upstream engine. This binary supplies an import/capability boundary;
|
||||||
# production execution additionally requires OS limits and platform approval
|
# production execution additionally requires the qualified OS identity,
|
||||||
# for shared placement, or a workspace VM for unreviewed application code.
|
# namespace, syscall, cgroup and invocation-broker boundaries.
|
||||||
{ pkgs }:
|
{ pkgs }:
|
||||||
let
|
let
|
||||||
version = "1.20261005.1";
|
version = "1.20261005.1";
|
||||||
|
|||||||
Reference in New Issue
Block a user