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";