Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions examples/initial-credentials-test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* Report the initial process credentials supplied by the host. */
#include <stdio.h>
#include <unistd.h>

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;
}
28 changes: 28 additions & 0 deletions examples/run-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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.
Expand Down Expand Up @@ -312,6 +336,8 @@ async function main() {
console.error("Usage: npx tsx examples/run-example.ts <name>");
process.exit(1);
}
const uid = parseKernelCredential("KERNEL_UID");
const gid = parseKernelCredential("KERNEL_GID");

let programPath: string;
if (name.endsWith(".wasm")) {
Expand Down Expand Up @@ -386,6 +412,8 @@ async function main() {
...gitEnv,
],
cwd: process.env.KERNEL_CWD || process.cwd(),
uid,
gid,
stdin: stdinData,
});
const timeoutPromise = new Promise<number>((_, reject) => {
Expand Down
1 change: 1 addition & 0 deletions host/test/global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];

Expand Down
78 changes: 78 additions & 0 deletions host/test/run-example-credentials.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined>) {
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",
);
},
);
});
Loading