Compare commits

..
8 changed files with 115 additions and 24 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "286ada5419c4fc2184b387cea4715fae1a74bde5",
"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"
+19
View File
@@ -61,6 +61,25 @@ with their own versions. Authored source symlinks and non-bundled portable impor
other than the SDK and server Node builtins are rejected. This is build closure
checking, not a purity proof or security sandbox.
`mkCaminoYarnDependencies` installs the locked dependency recipe and exports its
installed Node dependency tree, including native package results and executable
links. Generated Yarn install/linker state stays outside runtime outputs. Yarn
describes its install-state file as an optimization cache in its
[configuration reference](https://yarnpkg.com/configuration/yarnrc#installStatePath).
This helper output is a dependency directory for a build, not a standalone
Package or a runnable Yarn project. Its `devShell` supplies freestanding Yarn for
editable projects; do not run Yarn against the readonly dependency output.
Use the shared `installDependencies` assembly for consumers: authored libraries
are copied into the consumer's `node_modules`, where Node can find their
transitive dependencies, while selected SDK links retain their canonical type
identity. Reserved SDK substitutions fail checks. Web Studio uses this same
assembly rather than retaining a full Yarn project just to resolve UI libraries.
Its dependency inputs contain manifests, lock and Yarn configuration/plugin
recipe; ordinary Studio source edits do not enter that dependency derivation.
Recipes with local/workspace dependencies must still include their needed source.
This is language-specific build machinery; it adds no core Package file contract.
The core/Automerge bundle and Replicache bundle are built once. Browser modules
import the core through a content-addressed gateway URL, independent of package
identity. Each manifest covers the shared bytes it uses. The gateway retains its
+4 -11
View File
@@ -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
+4 -4
View File
@@ -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");
@@ -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"
''
+29 -7
View File
@@ -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";
+26
View File
@@ -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) =>
+3
View File
@@ -13,6 +13,9 @@ let
test -d node_modules
mkdir -p "$out"
cp -a node_modules/. "$out/"
# Node resolves installed packages directly. This generated index is for
# Yarn's linker, not a runtime input; do not export build-session state.
rm -f "$out/.yarn-state.yml"
runHook postInstall
'';
};