diff --git a/.quixos-subtree-source.json b/.quixos-subtree-source.json index 2c2a36b..6ba40e1 100644 --- a/.quixos-subtree-source.json +++ b/.quixos-subtree-source.json @@ -1,7 +1,7 @@ { "version": 1, - "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", - "sourceCommit": "358e649a7d70828db2ef96d5fed18931e48bc498", + "sourceRepo": "https://gitea.internal.quixos.org/quixos/quixos.git", + "sourceCommit": "e4393946ba63cfef75580f6aa6ba840035e7d43c", "sourcePath": "quixos-instance/quixos-nix-helpers", "exportName": "quixos-nix-helpers", "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git" diff --git a/restricted-execution/default.nix b/restricted-execution/default.nix index deca898..c77c52d 100644 --- a/restricted-execution/default.nix +++ b/restricted-execution/default.nix @@ -20,17 +20,9 @@ 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" - ''; + mkIdentityLauncher = + settings: import ./identity-launcher.nix ({ inherit pkgs sandbox; } // settings); + identityLauncher = mkIdentityLauncher { }; # 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 ]; } '' @@ -73,6 +65,7 @@ in workerd sandbox identityLauncher + mkIdentityLauncher nativeProbeSandbox sdk runtime diff --git a/restricted-execution/identity-launcher.c b/restricted-execution/identity-launcher.c index 296a343..e5daa83 100644 --- a/restricted-execution/identity-launcher.c +++ b/restricted-execution/identity-launcher.c @@ -20,17 +20,17 @@ static void fail(const char *message) { } int main(int argc, char **argv) { - const char *root = "/run/quixos-restricted-engines/"; + const char *root = ENGINE_DIRECTORY "/"; uid_t caller = getuid(); gid_t group = getgid(); - struct passwd *owner = getpwnam("quixos-graph"); + struct passwd *owner = getpwnam(AUTHORITY_USER); 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) + if (!*argv[2] || *end || number < ENGINE_FIRST_UID || number >= ENGINE_FIRST_UID + ENGINE_IDENTITY_COUNT) fail("invalid engine identity"); uid_t identity = (uid_t)number; if (strlen(argv[1]) != strlen(root) + 16 || strncmp(argv[1], root, strlen(root))) @@ -39,7 +39,7 @@ int main(int argc, char **argv) { 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); + int parent = open(ENGINE_DIRECTORY, 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"); diff --git a/restricted-execution/identity-launcher.nix b/restricted-execution/identity-launcher.nix new file mode 100644 index 0000000..7cfdca1 --- /dev/null +++ b/restricted-execution/identity-launcher.nix @@ -0,0 +1,28 @@ +# Trusted host construction only. Authored Workers cannot choose the caller, +# reserved identities, engine directory or sandbox executable. +{ + pkgs, + sandbox, + authorityUser ? "quixos-graph", + engineDirectory ? "/run/quixos-restricted-engines", + firstUid ? 610000, + identityCount ? 64, +}: +assert builtins.match "[a-z][a-z0-9_-]{0,63}" authorityUser != null; +assert builtins.match "/run/[a-zA-Z0-9_/-]+" engineDirectory != null; +assert !(pkgs.lib.hasInfix ".." engineDirectory); +assert !(pkgs.lib.hasSuffix "/" engineDirectory); +assert builtins.stringLength engineDirectory < 80; +assert builtins.isInt firstUid && builtins.isInt identityCount; +assert firstUid >= 610000 && firstUid + identityCount <= 2147483647; +assert identityCount >= 1 && identityCount <= 64; +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"' \ + -DAUTHORITY_USER='"${authorityUser}"' \ + -DENGINE_DIRECTORY='"${engineDirectory}"' \ + -DENGINE_FIRST_UID=${toString firstUid} \ + -DENGINE_IDENTITY_COUNT=${toString identityCount} \ + -o "$out/bin/quixos-restricted-engine" +'' diff --git a/restricted-execution/identity-module.nix b/restricted-execution/identity-module.nix index 3aa0ba7..71544cd 100644 --- a/restricted-execution/identity-module.nix +++ b/restricted-execution/identity-module.nix @@ -2,23 +2,45 @@ executor, enable, controlGroup, + authorityUser ? "quixos-graph", + engineDirectory ? "/run/quixos-restricted-engines", + firstUid ? 610000, + identityCount ? 64, + wrapperName ? "quixos-restricted-engine", }: { 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."; + assertion = lib.all ( + user: user.uid == null || user.uid < firstUid || user.uid >= firstUid + identityCount + ) (builtins.attrValues config.users.users); + message = "UIDs ${toString firstUid}–${ + toString (firstUid + identityCount - 1) + } are reserved for restricted engine identities."; + } + { + assertion = lib.all ( + group: group.gid == null || group.gid < firstUid || group.gid >= firstUid + identityCount + ) (builtins.attrValues config.users.groups); + message = "Restricted engine GIDs must not overlap host groups."; } ]; systemd.tmpfiles.rules = [ - "d /run/quixos-restricted-engines 0711 quixos-graph ${controlGroup} -" + "d ${engineDirectory} 0711 ${authorityUser} ${controlGroup} -" ]; - security.wrappers.quixos-restricted-engine = { - source = "${executor.identityLauncher}/bin/quixos-restricted-engine"; + security.wrappers.${wrapperName} = { + source = "${ + executor.mkIdentityLauncher { + inherit + authorityUser + engineDirectory + firstUid + identityCount + ; + } + }/bin/quixos-restricted-engine"; owner = "root"; group = controlGroup; permissions = "u+rx,g+rx,o-rwx"; diff --git a/restricted-execution/service.mjs b/restricted-execution/service.mjs index a30d290..091b79e 100644 --- a/restricted-execution/service.mjs +++ b/restricted-execution/service.mjs @@ -7,6 +7,32 @@ import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; const abi = "quixos-transaction-ports-v1"; +/** Reserved supervisor faults are not authored domain rejections. A graph + * transaction must roll back without recording a permanent rejected outcome + * when its executor dies, exceeds its limits or cannot start. */ +export const restrictedExecutionFailureCodes = Object.freeze([ + "RESTRICTED_ABI_MISMATCH", + "RESTRICTED_BACKPRESSURE", + "RESTRICTED_CPU_ACCOUNTING", + "RESTRICTED_DEADLINE", + "RESTRICTED_DIAGNOSTIC_LIMIT", + "RESTRICTED_DUPLICATE_INVOCATION", + "RESTRICTED_ENGINE_EXITED", + "RESTRICTED_ENGINE_LIMIT", + "RESTRICTED_ENTRY_LIMIT", + "RESTRICTED_IDENTITY_UNAVAILABLE", + "RESTRICTED_MESSAGE_LIMIT", + "RESTRICTED_PACKAGE_FAILED", + "RESTRICTED_PORT_FAILED", + "RESTRICTED_PORT_LIMIT", + "RESTRICTED_PROCESS_LIMIT", + "RESTRICTED_QUEUE_FULL", + "RESTRICTED_SDK_LIMIT", + "RESTRICTED_SOURCE_LIMIT", + "RESTRICTED_START_FAILED", + "RESTRICTED_UNKNOWN_INVOCATION", + "RESTRICTED_UNKNOWN_PORT", +]); const fixedError = (code) => Object.assign(Error(code), { code }); const object = (v) => !!v && typeof v === "object" && !Array.isArray(v); const absolute = (value) =>