Skip to content
Open
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
4 changes: 2 additions & 2 deletions test/cli/install/bun-add-filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { file, write } from "bun";
import { afterAll, beforeAll, expect, test } from "bun:test";
import { chmod, exists, mkdir, rm } from "fs/promises";
import { VerdaccioRegistry, bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness";
import { VerdaccioRegistry, bunEnv, bunExe, isRoot, isWindows, normalizeBunSnapshot } from "harness";
import { join } from "path";

const registry = new VerdaccioRegistry();
Expand Down Expand Up @@ -2141,7 +2141,7 @@ test.concurrent("remove --filter --dry-run touches nothing", async () => {
expect(await exists(join(dir, "node_modules", "no-deps", "package.json"))).toBeTrue();
});

test.concurrent.skipIf(isWindows || process.getuid?.() === 0)(
test.concurrent.skipIf(isWindows || isRoot)(
"an unwritable target is reported by name, the rest is still written, exit code 1",
async () => {
const dir = await makeMonorepo();
Expand Down
13 changes: 11 additions & 2 deletions test/cli/install/bun-prune.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { file, write } from "bun";
import { afterAll, beforeAll, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows, normalizeBunSnapshot, runBunInstall, tempDir, VerdaccioRegistry } from "harness";
import {
bunEnv,
bunExe,
isRoot,
isWindows,
normalizeBunSnapshot,
runBunInstall,
tempDir,
VerdaccioRegistry,
} from "harness";
import {
chmodSync,
closeSync,
Expand Down Expand Up @@ -1974,7 +1983,7 @@ test.concurrent("isolated + publicHoistPattern: hoisted links follow their store
expect(await file(join(nm, "no-deps", "package.json")).json()).toMatchObject({ version: "1.0.0" });
});

test.concurrent.skipIf(isWindows || process.getuid?.() === 0)(
test.concurrent.skipIf(isWindows || isRoot)(
"a failed deletion is reported, the rest is removed, exit code 1",
async () => {
const dir = await setup({ name: "foo", dependencies: { "no-deps": "1.0.0" } });
Expand Down
8 changes: 2 additions & 6 deletions test/cli/run/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isASAN,
isDebug,
isLinux,
isRoot,
isWindows,
tempDir,
tempDirWithFiles,
Expand Down Expand Up @@ -1070,12 +1071,7 @@ function hasNobodyUser(): boolean {
}
}

const canUseRunuser =
isLinux &&
typeof process.getuid === "function" &&
process.getuid() === 0 &&
!!Bun.which("runuser") &&
hasNobodyUser();
const canUseRunuser = isLinux && isRoot && !!Bun.which("runuser") && hasNobodyUser();

test.skipIf(!canUseRunuser)("process.env is preserved when cwd lacks read permission", () => {
using dir = tempDir("env-eacces", {
Expand Down
4 changes: 2 additions & 2 deletions test/cli/run/no-orphans.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dlopen, FFIType } from "bun:ffi";
import { describe, expect, setDefaultTimeout, test } from "bun:test";
import { bunEnv, bunExe, isLinux, isMusl, isWindows, tempDir } from "harness";
import { bunEnv, bunExe, isLinux, isMusl, isRoot, isWindows, tempDir } from "harness";
import { chmodSync, readFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";

Expand Down Expand Up @@ -219,7 +219,7 @@ test.concurrent.skipIf(!isPosix)(
// Same as above but the grandchild is spawned with uid/gid. The credential
// change clears the pdeathsig the spawn armed (prctl(2)), so this proves it
// gets re-armed after setgid/setuid. Needs root to setuid.
test.concurrent.skipIf(!isPosix || process.getuid?.() !== 0)(
test.concurrent.skipIf(!isPosix || !isRoot)(
"BUN_FEATURE_FLAG_NO_ORPHANS=1: a non-Bun grandchild spawned with uid/gid is reaped",
async () => {
const { sh, bunPid, grandchildPid } = await spawnTree("1", "child-nonbun-uid.js");
Expand Down
7 changes: 7 additions & 0 deletions test/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export const isFreeBSD = process.platform === "freebsd";
export const isAndroid = process.platform === "android";
export const isPosix = isMacOS || isLinux || isFreeBSD || isAndroid;
export const isWindows = process.platform === "win32";
/**
* Whether the tests are running as uid 0. Root bypasses file and directory
* mode bits, so tests that expect EACCES from a `chmod 000` fixture skip on it,
* while tests that need to setuid/setgid a child only run on it. Always false
* on Windows, where `process.getuid` does not exist.
*/
export const isRoot = process.getuid?.() === 0;
Comment thread
claude[bot] marked this conversation as resolved.
export const isIntelMacOS = isMacOS && process.arch === "x64";
export const isArm64 = process.arch === "arm64";
export const isDebug = Bun.version.includes("debug");
Expand Down
3 changes: 1 addition & 2 deletions test/js/bun/resolve/resolve.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { pathToFileURL } from "bun";
import { describe, expect, it, test } from "bun:test";
import { chmodSync, chownSync, mkdirSync, readFileSync, realpathSync, symlinkSync, writeFileSync } from "fs";
import { bunEnv, bunExe, bunRun, isLinux, isMacOS, isWindows, joinP, tempDir, tempDirWithFiles } from "harness";
import { bunEnv, bunExe, bunRun, isLinux, isMacOS, isRoot, isWindows, joinP, tempDir, tempDirWithFiles } from "harness";
import { join, resolve, sep } from "path";

const fixture = (...segs: string[]) => resolve(import.meta.dir, "fixtures", ...segs);
Expand Down Expand Up @@ -908,7 +908,6 @@ describe.if(isWindows)("#30839 - imports entry pointing at a scoped package", ()
// Root bypasses DAC, so chmod 0 won't yield EACCES. When running as root on
// Linux we drop to `nobody` via runuser (and chown the temp dir so the
// fixture can chmod it back). Otherwise we run the fixture directly.
const isRoot = !isWindows && process.getuid?.() === 0;
const nobody = (() => {
try {
// /etc/passwd format: name:x:uid:gid:gecos:home:shell
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { describe, expect, test } from "bun:test";
import { chmodSync, rmSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import { bunEnv, bunExe, isRoot, isWindows, tempDir } from "harness";
import { join } from "path";

// An ancestor directory the process may traverse but not read (mode 0o111 —
// common on shared hosts and in sandboxes) must not abort module resolution
// for readable subtrees: the resolver treats it as an opaque, empty
// directory. Previously the whole walk failed with "error loading current
// directory". Root bypasses permission checks, so skip there.
describe.skipIf(isWindows || process.getuid?.() === 0)("resolver with unreadable ancestor", () => {
describe.skipIf(isWindows || isRoot)("resolver with unreadable ancestor", () => {
test("bun run works under an execute-only ancestor", () => {
using dir = tempDir("xonly-ancestor", {
"outer/project/package.json": JSON.stringify({
Expand Down
13 changes: 11 additions & 2 deletions test/js/bun/shell/bunshell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ import { $ } from "bun";
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
import { chmodSync, mkdirSync } from "fs";
import { mkdir, rm, stat } from "fs/promises";
import { bunExe, isPosix, isWindows, rss, runWithErrorPromise, tempDir, tempDirWithFiles, tmpdirSync } from "harness";
import {
bunExe,
isPosix,
isRoot,
isWindows,
rss,
runWithErrorPromise,
tempDir,
tempDirWithFiles,
tmpdirSync,
} from "harness";
import { join, sep } from "path";
import { createTestBuilder, sortedShellOutput } from "./util";
const TestBuilder = createTestBuilder(import.meta.path);
Expand Down Expand Up @@ -54,7 +64,6 @@ afterAll(async () => {
});

const BUN = bunExe();
const isRoot = process.getuid?.() === 0;

describe("bunshell", () => {
describe("exit codes", async () => {
Expand Down
6 changes: 3 additions & 3 deletions test/js/bun/shell/commands/ls.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { $ } from "bun";
import { beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { isPosix, tempDir, tempDirWithFiles } from "harness";
import { isPosix, isRoot, tempDir, tempDirWithFiles } from "harness";
import { createTestBuilder } from "../util";
const TestBuilder = createTestBuilder(import.meta.path);

Expand Down Expand Up @@ -301,7 +301,7 @@ describe.concurrent("bunshell ls", () => {
.run();
});

test.if(isPosix)("permission denied directory", async () => {
test.if(isPosix && !isRoot)("permission denied directory", async () => {
await using tempdir = tempDir("ls-permission", {});
await $`mkdir restricted; chmod 000 restricted`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls restricted`
Expand All @@ -312,7 +312,7 @@ describe.concurrent("bunshell ls", () => {
await $`chmod 755 restricted`.quiet().throws(true).cwd(tempdir); // cleanup
});

test.if(isPosix)("permission denied directory recursive", async () => {
test.if(isPosix && !isRoot)("permission denied directory recursive", async () => {
await using tempdir = tempDir("ls-permission-recursive", {});
// Create 3-level deep directory structure with 3+ items per level
await $`mkdir -p level1/level2/level3;
Expand Down
3 changes: 1 addition & 2 deletions test/js/bun/shell/commands/mv.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { $ } from "bun";
import { describe, expect, test } from "bun:test";
import { isPosix } from "harness";
import { isPosix, isRoot } from "harness";
import {
accessSync,
chmodSync,
Expand All @@ -20,7 +20,6 @@ import { join } from "path";
import { createTestBuilder } from "../test_builder";
import { sortedShellOutput } from "../util";
const TestBuilder = createTestBuilder(import.meta.path);
const isRoot = process.getuid?.() === 0;

$.nothrow();

Expand Down
4 changes: 2 additions & 2 deletions test/js/bun/spawn/spawn-cgroup.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isLinux, tempDir } from "harness";
import { bunEnv, bunExe, isLinux, isRoot, tempDir } from "harness";
import { spawn as cpSpawn, spawnSync as cpSpawnSync, execFile, fork } from "node:child_process";
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, rmdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
Expand All @@ -9,7 +9,7 @@ import { dirname, join } from "node:path";
// controller). v2: a sibling of our own cgroup (children of a populated cgroup
// can't get controllers). v1: directly under the memory hierarchy root.
function setupCgroup(): { dir: string; version: 1 | 2; relative: string; canOOM: boolean } | null {
if (!isLinux || process.getuid?.() !== 0) return null;
if (!isLinux || !isRoot) return null;
const name = `bun-spawn-test-${process.pid}`;
const candidates: { dir: string; version: 1 | 2; relative: string }[] = [];

Expand Down
3 changes: 1 addition & 2 deletions test/js/bun/spawn/spawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isDebug,
isLinux,
isPosix,
isRoot,
isWindows,
shellExe,
tempDir,
Expand Down Expand Up @@ -1593,8 +1594,6 @@ describe("option combinations", () => {
});

describe("uid/gid", () => {
const isRoot = process.getuid?.() === 0;

it.if(isPosix && isRoot)("applies uid and gid to the child", async () => {
await using proc = spawn({ cmd: ["id", "-u"], uid: 65534, gid: 65534, stdout: "pipe" });
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
Expand Down
4 changes: 1 addition & 3 deletions test/js/bun/spawn/spawnSync.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, bunRun, isLinux, isMusl, isPosix, isWindows } from "harness";
import { bunEnv, bunExe, bunRun, isLinux, isMusl, isPosix, isRoot, isWindows } from "harness";
import { join } from "path";
describe("spawnSync", () => {
it("should throw a RangeError if timeout is less than 0", () => {
Expand Down Expand Up @@ -100,8 +100,6 @@ describe("spawnSync", () => {
});

describe("uid/gid", () => {
const isRoot = process.getuid?.() === 0;

it("rejects a non-integer uid", () => {
expect(() => Bun.spawnSync({ cmd: [bunExe()], env: bunEnv, uid: 1.5 })).toThrow();
expect(() => Bun.spawnSync({ cmd: [bunExe()], env: bunEnv, gid: 1.5 })).toThrow();
Expand Down
2 changes: 1 addition & 1 deletion test/js/node/child_process/child_process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
bunExe,
isLinux,
isPosix,
isRoot,
isWindows,
nodeExe,
runBunInstall,
Expand Down Expand Up @@ -1008,7 +1009,6 @@ done
});

describe("uid/gid options", () => {
const isRoot = process.getuid?.() === 0;
// 65534 is "nobody" on every Linux distro and on macOS.
const NOBODY = 65534;

Expand Down
31 changes: 14 additions & 17 deletions test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawnSync, which } from "bun";
import { describe, expect, it } from "bun:test";
import { familySync } from "detect-libc";
import { bunEnv, bunExe, isMacOS, isWindows, tempDir, tmpdirSync } from "harness";
import { bunEnv, bunExe, isMacOS, isRoot, isWindows, tempDir, tmpdirSync } from "harness";
import { basename, join, resolve } from "path";

const process_sleep = resolve(import.meta.dir, "process-sleep.js");
Expand Down Expand Up @@ -1111,7 +1111,7 @@ describe.concurrent(() => {
// bmalloc scavenger) had signal-suspended a thread, it could never ack the barrier
// and the whole process wedged at 0% CPU. The race is probabilistic, so hammer
// seteuid under GC pressure from many processes at once and require each to exit.
it.skipIf(process.platform !== "linux" || process.getuid() !== 0)(
it.skipIf(process.platform !== "linux" || !isRoot)(
"seteuid under GC pressure does not deadlock",
async () => {
using dir = tempDir("seteuid-deadlock", {
Expand Down Expand Up @@ -2335,21 +2335,18 @@ it("_rawDebug never throws when fd 2 is closed", async () => {
expect(exitCode).toBe(0);
});

it.skipIf(isWindows || process.getuid?.() === 0)(
"process.initgroups passes an unknown string user straight to initgroups(3)",
() => {
// Node hands string users to initgroups(3) as-is (no getpwnam pre-resolve),
// so as non-root we see the syscall's EPERM, not ERR_UNKNOWN_CREDENTIAL.
let err;
try {
process.initgroups("zz_no_user_zz", 0);
} catch (e) {
err = e;
}
expect(err?.code).toBe("EPERM");
expect(err?.syscall).toBe("initgroups");
},
);
it.skipIf(isWindows || isRoot)("process.initgroups passes an unknown string user straight to initgroups(3)", () => {
// Node hands string users to initgroups(3) as-is (no getpwnam pre-resolve),
// so as non-root we see the syscall's EPERM, not ERR_UNKNOWN_CREDENTIAL.
let err;
try {
process.initgroups("zz_no_user_zz", 0);
} catch (e) {
err = e;
}
expect(err?.code).toBe("EPERM");
expect(err?.syscall).toBe("initgroups");
});

it.skipIf(isWindows)("process.initgroups pre-resolves a numeric uid through passwd", () => {
// Numeric users go through getpwuid_r first, so an unknown uid surfaces
Expand Down
5 changes: 2 additions & 3 deletions test/js/node/watch/fs.watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
bunRunAsScript,
isLinux,
isMacOS,
isRoot,
isWindows,
tempDir,
tempDirWithFiles,
Expand Down Expand Up @@ -532,8 +533,6 @@ describe("fs.watch", () => {
// on windows 0o200 will be readable (match nodejs behavior)
// Root has CAP_DAC_OVERRIDE, so the chmod 0o200 below never yields the
// EACCES these two tests expect; they only make sense as a non-root user.
const isRoot = process.getuid?.() === 0;

test.skipIf(isWindows || isRoot)("should throw if no permission to watch the directory", async () => {
const filepath = path.join(testDir, "permission-dir");
fs.mkdirSync(filepath, { recursive: true });
Expand Down Expand Up @@ -708,7 +707,7 @@ describe("fs.watch", () => {
// seccomp. The same `add_one` error branch is reachable with EACCES by
// watching a tree that contains one unreadable subdirectory as an
// unprivileged user, so this test drops privileges to trigger it.
test.skipIf(!isLinux || process.getuid?.() !== 0)(
test.skipIf(!isLinux || !isRoot)(
"recursive watch surfaces inotify_add_watch failure on a subdirectory as an 'error' event",
async () => {
const NOBODY = 65534;
Expand Down
6 changes: 4 additions & 2 deletions test/js/web/fetch/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isDebug,
isFlaky,
isMacOS,
isRoot,
isWindows,
rss,
runFixtureMaxRSS,
Expand Down Expand Up @@ -1111,8 +1112,9 @@ describe.concurrent("Bun.file", () => {
}
}

// on Windows the creator of the file will be able to read from it so this test is disabled on it
describe.skipIf(isWindows)("bad permissions throws", () => {
// on Windows the creator of the file will be able to read from it so this test is disabled on it;
// root can read it anywhere, so it is disabled there as well
describe.skipIf(isWindows || isRoot)("bad permissions throws", () => {
const path = join(tmp_dir, "my-new-file");
beforeAll(async () => {
await Bun.write(path, "hey");
Expand Down