Publish quixos-nix-helpers from 932fe7714761fb8b5db4bd6401357cdff762a428

This commit is contained in:
Quixos Subtree Publisher
2026-10-08 05:08:28 +00:00
15 changed files with 545 additions and 121 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "abc91ab1cbbd61ffd7f0a49dbc5768598ac8745c", "sourceCommit": "932fe7714761fb8b5db4bd6401357cdff762a428",
"sourcePath": "quixos-instance/quixos-nix-helpers", "sourcePath": "quixos-instance/quixos-nix-helpers",
"exportName": "quixos-nix-helpers", "exportName": "quixos-nix-helpers",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git" "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
+12 -7
View File
@@ -113,8 +113,9 @@ service = {
}; };
``` ```
Each invocation gets a fresh engine process. The adapter admits one active frame Each invocation gets a fresh Worker in a reusable engine process. An engine runs
and a bounded FIFO; queue wait consumes the deadline. Overflow is an explicit one active frame; the adapter has bounded concurrency and queueing. Queue wait
consumes the deadline. Overflow is an explicit
`RESTRICTED_QUEUE_FULL` result. Cancellation kills the engine process group and `RESTRICTED_QUEUE_FULL` result. Cancellation kills the engine process group and
removes its sockets before acknowledgement. All graph calls inherit the trusted removes its sockets before acknowledgement. All graph calls inherit the trusted
invocation identity; authored code cannot choose another frame or workspace. invocation identity; authored code cannot choose another frame or workspace.
@@ -127,11 +128,15 @@ parent environment credentials, and unlisted modules are denied. Explicit dates,
available. Host-managed HTTP bridges expose only the current frame's declared available. Host-managed HTTP bridges expose only the current frame's declared
graph ports. Authored serialization is untrusted protocol input. graph ports. Authored serialization is untrusted protocol input.
This is an inner execution restriction, **not a hardened shared-process sandbox**. The packaged runtime uses the platform's Bubblewrap/seccomp launcher. Only the
One workspace owns the surrounding VM. Systemd limits the entire package cgroup, engine's read-only runtime closure and private broker directory are mounted;
including adapter and child engines; memory exhaustion terminates that group. native filesystem/process/network escape probes qualify that boundary. Systemd
No per-isolate memory protection is claimed. A guest user can control its VM, limits the whole package in a workspace VM. Shared authority additionally charges
so authoritative graph grants and credentials must remain outside it. each engine to its own cgroup before launch. No per-isolate memory protection or
Cloudflare hosted side-channel defenses are claimed. See the
[runtime decision](../../docs/RESTRICTED_RUNTIME_DECISION.md#os-process-confinement-implementation)
for the exact boundary and residual risks. A guest user can control its VM, so
authoritative graph grants and credentials must remain outside it.
Qualification commands: Qualification commands:
+1 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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");
+34
View File
@@ -1,6 +1,36 @@
{ pkgs, core }: { pkgs, core }:
let let
workerd = import ./workerd.nix { inherit pkgs; }; workerd = import ./workerd.nix { inherit pkgs; };
sandbox = import ./sandbox.nix {
inherit pkgs;
runtime = workerd;
executable = "${workerd}/bin/workerd";
};
nativeProbe =
pkgs.runCommand "quixos-restricted-native-probe"
{
nativeBuildInputs = [ pkgs.stdenv.cc ];
}
''
mkdir -p "$out/bin"
$CC -O2 -Wall -Wextra -Werror ${./sandbox-probe.c} -o "$out/bin/probe"
'';
nativeProbeSandbox = import ./sandbox.nix {
inherit pkgs;
runtime = nativeProbe;
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 ]; } ''
@@ -23,6 +53,7 @@ let
makeWrapper ${pkgs.nodejs_24}/bin/node "$out/bin/quixos-restricted-registry" \ makeWrapper ${pkgs.nodejs_24}/bin/node "$out/bin/quixos-restricted-registry" \
--add-flags "$out/lib/service.mjs" \ --add-flags "$out/lib/service.mjs" \
--set QUIXOS_RESTRICTED_WORKERD ${workerd}/bin/workerd \ --set QUIXOS_RESTRICTED_WORKERD ${workerd}/bin/workerd \
--set QUIXOS_RESTRICTED_SANDBOX ${sandbox}/bin/quixos-restricted-sandbox \
--set QUIXOS_RESTRICTED_WORKER "$out/lib/worker.mjs" --set QUIXOS_RESTRICTED_WORKER "$out/lib/worker.mjs"
''; '';
qualification = qualification =
@@ -40,6 +71,9 @@ in
{ {
inherit inherit
workerd workerd
sandbox
identityLauncher
nativeProbeSandbox
sdk sdk
runtime runtime
qualification qualification
+88
View File
@@ -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");
}
+28
View File
@@ -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;
};
};
}
+64
View File
@@ -0,0 +1,64 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
static void check(int condition, const char *label) {
if (!condition) {
fprintf(stderr, "ESCAPE: %s (%s)\n", label, strerror(errno));
exit(1);
}
}
int main(void) {
for (int fd = 3; fd < 256; fd++)
check(fcntl(fd, F_GETFD) < 0 && errno == EBADF, "inherited descriptor");
check(!getenv("QUIXOS_VM_SECRET"), "parent environment");
check(open("/etc/test-secret", O_RDONLY) < 0, "authority filesystem");
check(open("/sys/fs/cgroup/cgroup.procs", O_WRONLY) < 0, "resource controls");
check(open("/proc/1/root/etc/test-secret", O_RDONLY) < 0, "proc root escape");
check(socket(AF_INET, SOCK_STREAM, 0) < 0 && errno == EPERM, "IPv4 socket");
check(socket(AF_INET6, SOCK_STREAM, 0) < 0 && errno == EPERM, "IPv6 socket");
check(socket(AF_NETLINK, SOCK_RAW, 0) < 0 && errno == EPERM, "netlink socket");
check(ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0 && errno == EPERM, "ptrace");
check(unshare(CLONE_NEWUSER) < 0, "nested user namespace");
pid_t child = fork();
if (child == 0) _exit(2);
check(child < 0 && errno == EPERM, "subprocess");
check(access("/bin/sh", X_OK) < 0, "ambient executable");
check(getppid() <= 1, "host process visibility");
struct sockaddr_un address = {.sun_family = AF_UNIX};
int outside = socket(AF_UNIX, SOCK_STREAM, 0);
check(outside >= 0, "Unix socket support");
strcpy(address.sun_path, "/run/quixos-native-authority/test.sock");
check(connect(outside, (struct sockaddr *)&address, sizeof(address)) < 0,
"authority Unix socket");
close(outside);
outside = socket(AF_UNIX, SOCK_STREAM, 0);
memset(address.sun_path, 0, sizeof(address.sun_path));
strcpy(address.sun_path + 1, "quixos-native-authority");
check(connect(outside, (struct sockaddr *)&address,
offsetof(struct sockaddr_un, sun_path) + 1 + strlen(address.sun_path + 1)) < 0,
"authority abstract socket");
close(outside);
char directory[4096];
check(getcwd(directory, sizeof(directory)) != NULL, "engine directory");
check(snprintf(address.sun_path, sizeof(address.sun_path), "%s/broker.sock", directory) <
(int)sizeof(address.sun_path), "broker path length");
int broker = socket(AF_UNIX, SOCK_STREAM, 0);
check(broker >= 0 && connect(broker, (struct sockaddr *)&address, sizeof(address)) == 0,
"authorized broker");
check(write(broker, "sandbox-probe", 13) == 13, "broker write");
close(broker);
puts("RESTRICTED_NATIVE_SANDBOX_PASSED");
return 0;
}
+77
View File
@@ -0,0 +1,77 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* Trusted, single-process admission gate. The supervisor charges this PID to
* its engine cgroup before releasing stdin. All subsequently created namespace
* helpers and threads inherit that charge. No authored code runs here. */
static void fail(const char *message) {
fprintf(stderr, "RESTRICTED_SANDBOX: %s\n", message);
exit(125);
}
int main(int argc, char **argv) {
struct stat info;
if (argc != 2 || argv[1][0] != '/' || strstr(argv[1], "/../") ||
lstat(argv[1], &info) || !S_ISDIR(info.st_mode) ||
info.st_uid != getuid() || (info.st_mode & 007))
fail("invalid engine directory");
char gate;
if (read(STDIN_FILENO, &gate, 1) != 1 || gate != 1)
fail("engine admission was not released");
int input = open("/dev/null", O_RDONLY);
if (input < 0 || dup2(input, STDIN_FILENO) < 0)
fail("stdin");
close(input);
int filter = open(SANDBOX_FILTER, O_RDONLY);
if (filter < 0)
fail("seccomp filter");
char filter_fd[32];
snprintf(filter_fd, sizeof(filter_fd), "%d", filter);
char *args[4096];
size_t count = 0;
#define ARG(value) args[count++] = (value)
ARG(SANDBOX_BWRAP);
ARG("--unshare-all");
ARG("--unshare-user");
ARG("--die-with-parent");
ARG("--disable-userns");
ARG("--assert-userns-disabled");
ARG("--cap-drop"); ARG("ALL");
ARG("--hostname"); ARG("quixos-restricted");
ARG("--clearenv");
ARG("--setenv"); ARG("TZ"); ARG("UTC");
ARG("--setenv"); ARG("LANG"); ARG("C.UTF-8");
FILE *closure = fopen(SANDBOX_CLOSURE, "r");
if (!closure)
fail("runtime closure");
char *line = NULL;
size_t capacity = 0;
while (getline(&line, &capacity, closure) >= 0) {
line[strcspn(line, "\n")] = 0;
if (strncmp(line, "/nix/store/", 11) || strchr(line, ' ') || count > 4000)
fail("runtime closure entry");
char *path = strdup(line);
if (!path)
fail("runtime closure allocation");
ARG("--ro-bind"); ARG(path); ARG(path);
}
fclose(closure);
free(line);
ARG("--proc"); ARG("/proc");
ARG("--dev"); ARG("/dev");
ARG("--tmpfs"); ARG("/tmp");
ARG("--bind"); ARG(argv[1]); ARG(argv[1]);
ARG("--chdir"); ARG(argv[1]);
ARG("--seccomp"); ARG(filter_fd);
ARG("--"); ARG(SANDBOX_EXECUTABLE);
ARG("serve"); ARG("--experimental"); ARG("config.capnp");
ARG(NULL);
execv(SANDBOX_BWRAP, args);
fail("bubblewrap failed");
}
+30
View File
@@ -0,0 +1,30 @@
{
pkgs,
runtime,
executable,
}:
let
closure = pkgs.closureInfo { rootPaths = [ runtime ]; };
filter =
pkgs.runCommand "quixos-restricted-seccomp"
{
nativeBuildInputs = [
pkgs.stdenv.cc
pkgs.pkg-config
];
buildInputs = [ pkgs.libseccomp ];
}
''
$CC -O2 -Wall -Wextra -Werror ${./seccomp.c} $(pkg-config --cflags --libs libseccomp) -o filter
./filter > "$out"
'';
in
pkgs.runCommand "quixos-restricted-sandbox" { nativeBuildInputs = [ pkgs.stdenv.cc ]; } ''
mkdir -p "$out/bin"
$CC -O2 -Wall -Wextra -Werror ${./sandbox.c} \
-DSANDBOX_BWRAP='"${pkgs.bubblewrap}/bin/bwrap"' \
-DSANDBOX_EXECUTABLE='"${executable}"' \
-DSANDBOX_FILTER='"${filter}"' \
-DSANDBOX_CLOSURE='"${closure}/store-paths"' \
-o "$out/bin/quixos-restricted-sandbox"
''
+54
View File
@@ -0,0 +1,54 @@
#define _GNU_SOURCE
#include <errno.h>
#include <linux/sched.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
/* Layered on an empty mount tree, fresh PID/user/network namespaces, no caps,
* no inherited descriptors and an immutable runtime closure. Deny kernel and
* process escape surfaces; retain V8's ordinary memory/thread syscalls. */
static void checked(int result) {
if (result < 0) {
fprintf(stderr, "seccomp rule: %d\n", result);
exit(1);
}
}
int main(void) {
scmp_filter_ctx filter = seccomp_init(SCMP_ACT_ALLOW);
if (!filter) return 1;
const char *denied[] = {
"fork", "vfork", "execveat", "ptrace", "process_vm_readv", "process_vm_writev",
"pidfd_getfd", "kcmp", "bpf", "perf_event_open", "userfaultfd", "io_uring_setup",
"mount", "umount2", "pivot_root", "chroot", "move_mount", "open_tree", "fsopen",
"fsconfig", "fsmount", "fspick", "mount_setattr", "open_by_handle_at",
"name_to_handle_at", "setns", "unshare", "keyctl", "add_key", "request_key",
"reboot", "kexec_load", "kexec_file_load", "init_module", "finit_module",
"delete_module", "swapon", "swapoff", "syslog", "acct", "sethostname",
"setdomainname", "iopl", "ioperm", "lookup_dcookie", NULL
};
for (size_t i = 0; denied[i]; i++) {
int syscall = seccomp_syscall_resolve_name(denied[i]);
if (syscall != __NR_SCMP_ERROR)
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), syscall, 0));
}
/* glibc falls back to clone when clone3 is unavailable. Only thread creation
* is admitted; subprocesses and new namespaces cannot be manufactured. */
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(ENOSYS), SCMP_SYS(clone3), 0));
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(clone), 1,
SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_THREAD, 0)));
const unsigned long namespaces[] = {CLONE_NEWUSER, CLONE_NEWNS, CLONE_NEWNET,
CLONE_NEWPID, CLONE_NEWIPC, CLONE_NEWUTS, CLONE_NEWCGROUP};
for (size_t i = 0; i < sizeof(namespaces) / sizeof(namespaces[0]); i++)
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(clone), 1,
SCMP_A0(SCMP_CMP_MASKED_EQ, namespaces[i], namespaces[i])));
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_NE, AF_UNIX)));
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(ioctl), 1,
SCMP_A1(SCMP_CMP_EQ, 0x5412))); /* TIOCSTI */
checked(seccomp_export_bpf(filter, STDOUT_FILENO));
seccomp_release(filter);
return 0;
}
+57 -22
View File
@@ -37,13 +37,13 @@ export class RestrictedEngineBudget {
let releasing; let releasing;
const release = () => const release = () =>
(releasing ??= (async () => { (releasing ??= (async () => {
try {
await resource.release?.(); await resource.release?.();
} finally {
this.#leases.delete(lease); this.#leases.delete(lease);
}
})()); })());
release.attach = resource.attach; release.attach = resource.attach;
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);
@@ -83,6 +83,8 @@ export async function serveRestrictedRegistry(config, io = process) {
!absolute(config.workerd) || !absolute(config.workerd) ||
!absolute(config.worker) || !absolute(config.worker) ||
!absolute(config.module) || !absolute(config.module) ||
(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 ||
@@ -160,19 +162,38 @@ export async function serveRestrictedRegistry(config, io = process) {
throw fixedError("RESTRICTED_BACKPRESSURE"); throw fixedError("RESTRICTED_BACKPRESSURE");
io.stdout.write(line); io.stdout.write(line);
}; };
// Linux USER_HZ is 100 on the supported NixOS architectures. Read process // A sandbox has namespace helpers as well as workerd. Charge the complete
// CPU outside the engine; authored code cannot forge these usage messages. // delegated cgroup when available; the qualification fallback follows the
// bounded process tree, never trusting a worker's self-reported counters.
const engineUsage = async (engine) => {
if (engine.release?.usage) return engine.release.usage();
engine.processTicks ??= new Map();
const pending = [engine.child.pid];
let memoryBytes = 0,
visited = 0;
while (pending.length) {
if (++visited > 64) throw fixedError("RESTRICTED_PROCESS_LIMIT");
const pid = pending.pop();
try {
const stat = await readFile(`/proc/${pid}/stat`, "utf8");
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
engine.processTicks.set(`${pid}:${fields[19]}`, Number(fields[11]) + Number(fields[12]));
const status = await readFile(`/proc/${pid}/status`, "utf8");
memoryBytes += Number(status.match(/^VmRSS:\s+(\d+)/m)?.[1] ?? 0) * 1024;
const children = await readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
pending.push(...children.trim().split(/\s+/).filter(Boolean).map(Number));
} catch (error) {
if (!["ENOENT", "ESRCH"].includes(error.code)) throw error;
}
}
return {
cpuMicroseconds: [...engine.processTicks.values()].reduce((sum, ticks) => sum + ticks * 10000, 0),
memoryBytes,
};
};
const sampleCPU = async (call) => { const sampleCPU = async (call) => {
if (!call.child?.pid) return; if (!call.child?.pid) return;
let text; const { cpuMicroseconds: total } = await engineUsage(call.engine);
try {
text = await readFile(`/proc/${call.child.pid}/stat`, "utf8");
} catch (error) {
if (error.code === "ENOENT" || error.code === "ESRCH") return;
throw error;
}
const fields = text.slice(text.lastIndexOf(")") + 2).split(" ");
const total = (Number(fields[11]) + Number(fields[12])) * 10000;
const cpuMicroseconds = total - (call.cpuBaseline ?? 0); const cpuMicroseconds = total - (call.cpuBaseline ?? 0);
if (call.engine) call.engine.cpuTotal = total; if (call.engine) call.engine.cpuTotal = total;
if (!Number.isSafeInteger(cpuMicroseconds) || cpuMicroseconds < (call.cpuMicroseconds ?? 0)) if (!Number.isSafeInteger(cpuMicroseconds) || cpuMicroseconds < (call.cpuMicroseconds ?? 0))
@@ -217,8 +238,7 @@ export async function serveRestrictedRegistry(config, io = process) {
// A delayed child request can never acquire the next call's authority. // A delayed child request can never acquire the next call's authority.
if (engine.current === call) engine.current = null; if (engine.current === call) engine.current = null;
if (!stopped && reason.code === "INVOCATION_ENDED" && !usageFailure && !engine.destroying) { if (!stopped && reason.code === "INVOCATION_ENDED" && !usageFailure && !engine.destroying) {
const status = await readFile(`/proc/${engine.child.pid}/status`, "utf8").catch(() => ""); const rss = (await engineUsage(engine)).memoryBytes;
const rss = Number(status.match(/^VmRSS:\s+(\d+)/m)?.[1] ?? Infinity) * 1024;
metrics.peakRSSBytes = Math.max(metrics.peakRSSBytes, rss); metrics.peakRSSBytes = Math.max(metrics.peakRSSBytes, rss);
// Bound retained engine heaps as well as lifetime. Hard aggregate // Bound retained engine heaps as well as lifetime. Hard aggregate
// physical memory remains the supervising process/cgroup's contract. // physical memory remains the supervising process/cgroup's contract.
@@ -263,11 +283,14 @@ 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) {
if (engine.release.kill) await engine.release.kill();
else {
try { try {
process.kill(-engine.child.pid, "SIGKILL"); process.kill(-engine.child.pid, "SIGKILL");
} catch (error) { } catch (error) {
if (error.code !== "ESRCH") throw error; if (error.code !== "ESRCH") throw error;
} }
}
await engine.exited; await engine.exited;
} }
engine.server?.closeAllConnections(); engine.server?.closeAllConnections();
@@ -380,27 +403,37 @@ 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;
engine.child = spawn(config.workerd, ["serve", "--experimental", file], { if (config.sandboxIdentity && !Number.isSafeInteger(engine.release.uid))
throw fixedError("RESTRICTED_IDENTITY_UNAVAILABLE");
engine.child = spawn(
config.sandbox ?? config.workerd,
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" },
cwd: engine.directory, cwd: engine.directory,
stdio: ["ignore", "ignore", "pipe"], stdio: [config.sandbox ? "pipe" : "ignore", "ignore", "pipe"],
}); },
);
metrics.starts++; metrics.starts++;
engine.exited = new Promise((resolve) => { engine.exited = new Promise((resolve) => {
engine.child.once("exit", resolve); engine.child.once("exit", resolve);
engine.child.once("error", resolve); engine.child.once("error", resolve);
}); });
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);
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");
@@ -512,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 } });
@@ -663,6 +697,7 @@ if (process.argv[1] === new URL(import.meta.url).pathname) {
...config, ...config,
workerd: process.env.QUIXOS_RESTRICTED_WORKERD, workerd: process.env.QUIXOS_RESTRICTED_WORKERD,
worker: process.env.QUIXOS_RESTRICTED_WORKER, worker: process.env.QUIXOS_RESTRICTED_WORKER,
sandbox: process.env.QUIXOS_RESTRICTED_SANDBOX,
module: join(dirname(process.argv[2]), config.module), module: join(dirname(process.argv[2]), config.module),
}); });
} }
+43 -7
View File
@@ -50,7 +50,16 @@ async function fixture(source, options = {}) {
} }
}; };
const service = await serveRestrictedRegistry( const service = await serveRestrictedRegistry(
{ workerd, worker, module, registryExport: "registry", exports: ["test"], sdk: [], ...options }, {
workerd,
worker,
module,
sandbox: process.env.QUIXOS_TEST_SANDBOX,
registryExport: "registry",
exports: ["test"],
sdk: [],
...options,
},
io, io,
); );
assert.deepEqual(await receive(), { abi, kind: "ready", exports: ["test"] }); assert.deepEqual(await receive(), { abi, kind: "ready", exports: ["test"] });
@@ -86,6 +95,27 @@ async function fixture(source, options = {}) {
} }
const registry = (code) => `export const registry={exports:['test'],async invoke(request,channel){${code}}};`; const registry = (code) => `export const registry={exports:['test'],async invoke(request,channel){${code}}};`;
test("uncertain OS cleanup cannot release engine capacity", async () => {
const budget = new RestrictedEngineBudget(1, async () => ({
async release() {
throw Error("kernel cleanup not confirmed");
},
}));
const release = await budget.acquire(
() => false,
async () => {},
);
await assert.rejects(release(), /cleanup not confirmed/);
assert.equal(budget.stats().engines, 1);
await assert.rejects(
budget.acquire(
() => false,
async () => {},
),
{ code: "RESTRICTED_ENGINE_LIMIT" },
);
});
test("parallel frames route independently and cancellation cannot retire a neighbor", async () => { test("parallel frames route independently and cancellation cannot retire a neighbor", async () => {
const f = await fixture( const f = await fixture(
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:request.input});"), registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:request.input});"),
@@ -407,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);
+2 -2
View File
@@ -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";