diff --git a/examples/README.md b/examples/README.md index 7bb2c7160c..840aa47088 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,6 +18,11 @@ wasm32posix-cc examples/hello.c -o hello.wasm npx tsx examples/run-example.ts hello ``` +`run-example.ts` starts guests as root by default. Set `KERNEL_UID` and +`KERNEL_GID` to decimal values from 0 through 4294967294 when a test needs a +different initial user or group. The maximum unsigned 32-bit value is reserved +by the host protocol and is rejected rather than being mistaken for an ID. + See [docs/sdk-guide.md](../docs/sdk-guide.md) for full SDK documentation. ## Programs diff --git a/examples/initial-credentials-test.c b/examples/initial-credentials-test.c new file mode 100644 index 0000000000..dd5469fea0 --- /dev/null +++ b/examples/initial-credentials-test.c @@ -0,0 +1,12 @@ +/* Report the initial process credentials supplied by the host. */ +#include +#include + +int main(void) { + printf("uid=%lu euid=%lu gid=%lu egid=%lu\n", + (unsigned long) getuid(), + (unsigned long) geteuid(), + (unsigned long) getgid(), + (unsigned long) getegid()); + return 0; +} diff --git a/examples/run-example.ts b/examples/run-example.ts index 93fbbcdbf4..36bb5a49cb 100644 --- a/examples/run-example.ts +++ b/examples/run-example.ts @@ -10,6 +10,7 @@ * Example: * npx tsx examples/run-example.ts hello * npx tsx examples/run-example.ts /path/to/test.wasm + * KERNEL_UID=1000 KERNEL_GID=1000 npx tsx examples/run-example.ts hello */ import { closeSync, existsSync, openSync, readFileSync, statSync, writeSync } from "fs"; @@ -20,6 +21,29 @@ import { isWithinRealDirectory } from "./run-example-paths"; const repoRoot = resolve(dirname(new URL(import.meta.url).pathname), ".."); +const MAX_CONFIGURABLE_CREDENTIAL = 0xfffffffe; + +function parseKernelCredential(name: "KERNEL_UID" | "KERNEL_GID"): number | undefined { + const raw = process.env[name]; + if (raw === undefined || raw === "") return undefined; + + // u32::MAX is the host/kernel protocol's "leave unchanged" sentinel. If + // it were accepted here, a request for that ID would silently leave the + // new process running as root. + if (!/^[0-9]+$/.test(raw)) { + throw new Error( + `${name} must be a decimal integer from 0 to ${MAX_CONFIGURABLE_CREDENTIAL}`, + ); + } + const value = Number(raw); + if (!Number.isSafeInteger(value) || value > MAX_CONFIGURABLE_CREDENTIAL) { + throw new Error( + `${name} must be a decimal integer from 0 to ${MAX_CONFIGURABLE_CREDENTIAL}`, + ); + } + return value; +} + // Built-in program resolution via the binary-resolver. Resolver returns // null for programs that aren't fetched or locally built; callers that // need the path must handle null explicitly. @@ -312,6 +336,8 @@ async function main() { console.error("Usage: npx tsx examples/run-example.ts "); process.exit(1); } + const uid = parseKernelCredential("KERNEL_UID"); + const gid = parseKernelCredential("KERNEL_GID"); let programPath: string; if (name.endsWith(".wasm")) { @@ -386,6 +412,8 @@ async function main() { ...gitEnv, ], cwd: process.env.KERNEL_CWD || process.cwd(), + uid, + gid, stdin: stdinData, }); const timeoutPromise = new Promise((_, reject) => { diff --git a/host/test/global-setup.ts b/host/test/global-setup.ts index 300adea434..2604ccd70d 100644 --- a/host/test/global-setup.ts +++ b/host/test/global-setup.ts @@ -51,6 +51,7 @@ const TEST_PROGRAMS = [ "spawn-pause.c", "mount_probe_test.c", "getpwent_smoke.c", + "initial-credentials-test.c", "thread-exit-group.c", ]; diff --git a/host/test/run-example-credentials.test.ts b/host/test/run-example-credentials.test.ts new file mode 100644 index 0000000000..b7f826a93c --- /dev/null +++ b/host/test/run-example-credentials.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(__dirname, "..", ".."); +const runExample = join(repoRoot, "examples", "run-example.ts"); +const credentialProbe = join(repoRoot, "examples", "initial-credentials-test.wasm"); + +function runCredentialProbe(overrides: Record) { + const env = { ...process.env }; + delete env.KERNEL_UID; + delete env.KERNEL_GID; + for (const [name, value] of Object.entries(overrides)) { + if (value === undefined) delete env[name]; + else env[name] = value; + } + + return spawnSync( + process.execPath, + [ + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + runExample, + credentialProbe, + ], + { + cwd: repoRoot, + // The probe only inspects credentials. Keep its guest cwd independent + // of checkout ownership and group modes on the CI host. + env: { ...env, KERNEL_CWD: "/tmp", TIMEOUT: "30000" }, + encoding: "utf8", + timeout: 45_000, + }, + ); +} + +describe("run-example initial credentials", () => { + it("starts the guest with the requested real and effective IDs", () => { + const result = runCredentialProbe({ KERNEL_UID: "1000", KERNEL_GID: "1001" }); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("uid=1000 euid=1000 gid=1001 egid=1001"); + }); + + it("leaves an omitted credential at the kernel default", () => { + const result = runCredentialProbe({ KERNEL_UID: "2000" }); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("uid=2000 euid=2000 gid=0 egid=0"); + }); + + it("accepts the largest ID that is not the unchanged sentinel", () => { + const result = runCredentialProbe({ + KERNEL_UID: "4294967294", + KERNEL_GID: "4294967294", + }); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain( + "uid=4294967294 euid=4294967294 gid=4294967294 egid=4294967294", + ); + }); + + it.each(["-1", "1.5", "0x10", " 1000 ", "4294967295", "4294967296"])( + "rejects an invalid KERNEL_UID value (%s)", + (value) => { + const result = runCredentialProbe({ KERNEL_UID: value }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain( + "KERNEL_UID must be a decimal integer from 0 to 4294967294", + ); + }, + ); +});