Admit checked restricted artifacts without individual code approvals
This commit is contained in:
@@ -20,6 +20,17 @@ let
|
||||
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.
|
||||
# Keep this surface explicit; it is bundled into checked restricted handlers.
|
||||
sdk = pkgs.runCommand "quixos-restricted-sdk" { nativeBuildInputs = [ pkgs.esbuild ]; } ''
|
||||
@@ -61,6 +72,7 @@ in
|
||||
inherit
|
||||
workerd
|
||||
sandbox
|
||||
identityLauncher
|
||||
nativeProbeSandbox
|
||||
sdk
|
||||
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;
|
||||
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 & 077))
|
||||
info.st_uid != getuid() || (info.st_mode & 007))
|
||||
fail("invalid engine directory");
|
||||
char gate;
|
||||
if (read(STDIN_FILENO, &gate, 1) != 1 || gate != 1)
|
||||
|
||||
@@ -42,6 +42,8 @@ export class RestrictedEngineBudget {
|
||||
})());
|
||||
release.attach = resource.attach;
|
||||
release.usage = resource.usage;
|
||||
release.kill = resource.kill;
|
||||
release.uid = resource.uid;
|
||||
return release;
|
||||
} catch (error) {
|
||||
this.#leases.delete(lease);
|
||||
@@ -82,6 +84,7 @@ export async function serveRestrictedRegistry(config, io = process) {
|
||||
!absolute(config.worker) ||
|
||||
!absolute(config.module) ||
|
||||
(config.sandbox !== undefined && !absolute(config.sandbox)) ||
|
||||
(config.sandboxIdentity !== undefined && typeof config.sandboxIdentity !== "boolean") ||
|
||||
!/^[$A-Z_a-z][$\w]*$/.test(config.registryExport) ||
|
||||
!Array.isArray(config.exports) ||
|
||||
!config.exports.length ||
|
||||
@@ -280,10 +283,13 @@ export async function serveRestrictedRegistry(config, io = process) {
|
||||
if (index >= 0) idleEngines.splice(index, 1);
|
||||
engine.current = null;
|
||||
if (engine.child?.pid) {
|
||||
try {
|
||||
process.kill(-engine.child.pid, "SIGKILL");
|
||||
} catch (error) {
|
||||
if (error.code !== "ESRCH") throw error;
|
||||
if (engine.release.kill) await engine.release.kill();
|
||||
else {
|
||||
try {
|
||||
process.kill(-engine.child.pid, "SIGKILL");
|
||||
} catch (error) {
|
||||
if (error.code !== "ESRCH") throw error;
|
||||
}
|
||||
}
|
||||
await engine.exited;
|
||||
}
|
||||
@@ -397,9 +403,13 @@ export async function serveRestrictedRegistry(config, io = process) {
|
||||
const file = join(engine.directory, "config.capnp");
|
||||
await writeFile(file, configuration);
|
||||
if (!call.open) return;
|
||||
if (config.sandboxIdentity && !Number.isSafeInteger(engine.release.uid))
|
||||
throw fixedError("RESTRICTED_IDENTITY_UNAVAILABLE");
|
||||
engine.child = spawn(
|
||||
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,
|
||||
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"));
|
||||
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;
|
||||
engine.child.stderr.on("data", (chunk) => {
|
||||
if ((diagnostics += chunk.length) > 65536) {
|
||||
if (engine.current?.open) engine.current.terminate("RESTRICTED_DIAGNOSTIC_LIMIT");
|
||||
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"));
|
||||
void engine.exited.then(() => {
|
||||
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);
|
||||
metrics.completed++;
|
||||
} catch (error) {
|
||||
config.diagnostics?.(Buffer.from(`RESTRICTED_HOST_ERROR:${error.code ?? error.name}\n`));
|
||||
if (call.open) {
|
||||
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 } });
|
||||
|
||||
@@ -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 () => {
|
||||
const f = await fixture(
|
||||
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:null});"),
|
||||
{ deadlineMs: 350, maxActive: 1 },
|
||||
{ deadlineMs: 4000, maxActive: 1 },
|
||||
);
|
||||
try {
|
||||
const first = f.start();
|
||||
await f.receive();
|
||||
const queued = f.start();
|
||||
const replies = [await f.receive(), await f.receive()];
|
||||
assert.deepEqual(new Set(replies.map((x) => x.invocation)), new Set([first.invocation, queued.invocation]));
|
||||
assert.ok(replies.every((x) => x.error.code === "RESTRICTED_DEADLINE"));
|
||||
assert.equal((await f.receive()).kind, "port");
|
||||
// Expire while the first call is deliberately blocked. Two equal relative
|
||||
// deadlines let fast startup legitimately begin the queued call first.
|
||||
const queued = f.start(null, { root: "queued-deadline", deadlineMs: 50, depth: 1, calls: 10 });
|
||||
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 port = await f.receive();
|
||||
assert.equal(port.invocation, fresh.invocation);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Pinned upstream engine. This binary supplies an import/capability boundary;
|
||||
# production execution additionally requires OS limits and platform approval
|
||||
# for shared placement, or a workspace VM for unreviewed application code.
|
||||
# production execution additionally requires the qualified OS identity,
|
||||
# namespace, syscall, cgroup and invocation-broker boundaries.
|
||||
{ pkgs }:
|
||||
let
|
||||
version = "1.20261005.1";
|
||||
|
||||
Reference in New Issue
Block a user