diff --git a/restricted-execution/default.nix b/restricted-execution/default.nix index c77c52d..a909585 100644 --- a/restricted-execution/default.nix +++ b/restricted-execution/default.nix @@ -41,6 +41,8 @@ let '' mkdir -p "$out/lib" "$out/bin" cp ${./service.mjs} "$out/lib/service.mjs" + cp ${../socket-address.mjs} "$out/lib/socket-address.mjs" + substituteInPlace "$out/lib/service.mjs" --replace-fail '../socket-address.mjs' './socket-address.mjs' cp ${./worker.mjs} "$out/lib/worker.mjs" makeWrapper ${pkgs.nodejs_24}/bin/node "$out/bin/quixos-restricted-registry" \ --add-flags "$out/lib/service.mjs" \ @@ -48,6 +50,13 @@ let --set QUIXOS_RESTRICTED_SANDBOX ${sandbox}/bin/quixos-restricted-sandbox \ --set QUIXOS_RESTRICTED_WORKER "$out/lib/worker.mjs" ''; + qualificationSource = pkgs.lib.fileset.toSource { + root = ../.; + fileset = pkgs.lib.fileset.unions [ + (pkgs.lib.fileset.fileFilter (file: file.hasExt "mjs") ./.) + ../socket-address.mjs + ]; + }; qualification = pkgs.runCommand "quixos-restricted-execution-qualification" { @@ -56,7 +65,7 @@ let QUIXOS_TEST_SDK = "${sdk}/core.mjs"; } '' - node --test ${./.}/service.test.mjs + node --test ${qualificationSource}/restricted-execution/service.test.mjs touch "$out" ''; in @@ -69,6 +78,7 @@ in nativeProbeSandbox sdk runtime + qualificationSource qualification ; } diff --git a/restricted-execution/identity-launcher.nix b/restricted-execution/identity-launcher.nix index 7cfdca1..f277cfc 100644 --- a/restricted-execution/identity-launcher.nix +++ b/restricted-execution/identity-launcher.nix @@ -12,17 +12,31 @@ 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" -'' +pkgs.runCommand "quixos-restricted-identity-launcher" + { + nativeBuildInputs = [ + pkgs.stdenv.cc + pkgs.nodejs_24 + ]; + QUIXOS_RESTRICTED_ENGINE_DIRECTORY = engineDirectory; + } + '' + # Validate the complete generated filename with the shared UTF-8 policy, + # before compiling or installing a launcher for this configured /run root. + node --input-type=module <<'JS' + import { checkedSocketPath } from '${../socket-address.mjs}'; + for (const filename of ["broker.sock", "engine.sock"]) + checkedSocketPath(process.env.QUIXOS_RESTRICTED_ENGINE_DIRECTORY + "/qx-engine-XXXXXX/" + filename); + JS + 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/service.mjs b/restricted-execution/service.mjs index 091b79e..a4140b9 100644 --- a/restricted-execution/service.mjs +++ b/restricted-execution/service.mjs @@ -6,6 +6,8 @@ import { constants } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; +import { checkedSocketPath } from "../socket-address.mjs"; + 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 @@ -359,10 +361,16 @@ export async function serveRestrictedRegistry(config, io = process) { () => destroyEngine(engine), ); if (!call.open) return; + if (config.sandboxIdentity) { + // The privileged helper owns a fixed explicit /run root and requires + // this exact basename. Keep its cross-UID ancestry and identity rules. + for (const filename of ["broker.sock", "engine.sock"]) + checkedSocketPath(tmpdir() + "/qx-engine-XXXXXX/" + filename); + } engine.directory = await mkdtemp(join(tmpdir(), "qx-engine-")); if (!call.open) return; - const broker = join(engine.directory, "broker.sock"); - engine.socket = join(engine.directory, "engine.sock"); + const broker = checkedSocketPath(join(engine.directory, "broker.sock")); + engine.socket = checkedSocketPath(join(engine.directory, "engine.sock")); engine.server = createServer(async (request, response) => { const call = engine.current; const message = call?.message; diff --git a/socket-address.mjs b/socket-address.mjs index e75695b..57d866f 100644 --- a/socket-address.mjs +++ b/socket-address.mjs @@ -68,7 +68,8 @@ export function socketDirectory(identity, options) { } export function socketAddress(identity, role, options) { - if (typeof role !== "string" || !/^[a-z][a-z0-9-]{0,10}$/.test(role)) throw Error("Invalid bounded socket role"); + if (typeof role !== "string" || role !== role.trim() || !/^[a-z][a-z0-9-]{0,10}$/.test(role)) + throw Error("Invalid bounded socket role"); return checkedSocketPath(path.join(socketDirectory(identity, options), role + ".sock")); }