Compare commits

...

7 Commits

Author SHA1 Message Date
pfgithub
27a6a96bd5 bun run clang-format 2025-07-12 04:14:58 +00:00
pfgithub
4c4854170f bun run prettier 2025-07-12 04:14:04 +00:00
pfgithub
4db7aa5441 bun run zig-format 2025-07-12 04:12:35 +00:00
pfgithub
d5d3fc2afc bun scripts/glob-sources.mjs 2025-07-12 04:11:49 +00:00
pfg
09b2b2d68f another test 2025-07-11 21:00:52 -07:00
pfg
2d4c5cab0e macOS 2025-07-11 20:44:36 -07:00
pfg
920947c801 WIP: uid/gid 2025-07-11 20:23:05 -07:00
9 changed files with 384 additions and 10 deletions

View File

@@ -10,6 +10,7 @@ src/bun.js/bindings/Base64Helpers.cpp
src/bun.js/bindings/bindings.cpp
src/bun.js/bindings/blob.cpp
src/bun.js/bindings/bun-simdutf.cpp
src/bun.js/bindings/bun-spawn-darwin.cpp
src/bun.js/bindings/bun-spawn.cpp
src/bun.js/bindings/BunClientData.cpp
src/bun.js/bindings/BunCommonStrings.cpp

View File

@@ -986,6 +986,8 @@ pub const PosixSpawnOptions = struct {
/// for stdout. This is used to preserve
/// consistent shell semantics.
no_sigpipe: bool = true,
uid: ?u32 = null,
gid: ?u32 = null,
pub const Stdio = union(enum) {
path: []const u8,
@@ -1054,6 +1056,8 @@ pub const WindowsSpawnOptions = struct {
stream: bool = true,
use_execve_on_macos: bool = false,
can_block_entire_thread_to_reduce_cpu_usage_in_fast_path: bool = false,
uid: ?u32 = null,
gid: ?u32 = null,
pub const WindowsOptions = struct {
verbatim_arguments: bool = false,
hide_window: bool = true,
@@ -1230,6 +1234,18 @@ pub fn spawnProcessPosix(
var attr = try PosixSpawn.Attr.init();
defer attr.deinit();
if (comptime Environment.isLinux) {
attr.uid = options.uid;
attr.gid = options.gid;
} else if (comptime Environment.isMac) {
// On macOS, uid/gid are handled separately in the custom spawn implementation
// We don't set them on the attr here
} else {
// On other platforms, throw an error if uid/gid are specified
if (options.uid != null or options.gid != null) {
return .{ .err = bun.sys.Error.fromCode(.PERM, .posix_spawn) };
}
}
var flags: i32 = bun.c.POSIX_SPAWN_SETSIGDEF | bun.c.POSIX_SPAWN_SETSIGMASK;
@@ -1459,6 +1475,66 @@ pub fn spawnProcessPosix(
}
const argv0 = options.argv0 orelse argv[0].?;
// On macOS, if uid/gid are specified, we need to use a custom spawn implementation
// because posix_spawn doesn't support uid/gid changes
if (comptime Environment.isMac) {
if (options.uid != null or options.gid != null) {
// We need to use our custom fork+exec implementation
var chdir_buf: ?[:0]u8 = null;
defer if (chdir_buf) |buf| bun.default_allocator.free(buf);
if (options.cwd.len > 0) {
chdir_buf = try bun.default_allocator.dupeZ(u8, options.cwd);
}
const spawn_request = PosixSpawn.BunSpawnRequest{
.chdir_buf = if (chdir_buf) |buf| buf.ptr else null,
.detached = options.detached,
.actions = .{
.ptr = null,
.len = 0,
},
.uid = options.uid orelse 0,
.gid = options.gid orelse 0,
.has_uid = options.uid != null,
.has_gid = options.gid != null,
};
const spawn_result = PosixSpawn.BunSpawnRequest.spawn(
argv0,
spawn_request,
argv,
envp,
);
// Continue with the rest of the function
var failed_after_spawn = false;
defer {
if (failed_after_spawn) {
for (to_close_on_error.items) |fd| {
fd.close();
}
to_close_on_error.clearAndFree();
}
}
switch (spawn_result) {
.err => {
failed_after_spawn = true;
return .{ .err = spawn_result.err };
},
.result => |pid| {
spawned.pid = pid;
spawned.extra_pipes = extra_fds;
extra_fds = std.ArrayList(bun.FileDescriptor).init(bun.default_allocator);
return .{ .result = spawned };
},
}
}
}
const spawn_result = PosixSpawn.spawnZ(
argv0,
actions,
@@ -1528,6 +1604,11 @@ pub fn spawnProcessWindows(
bun.markWindowsOnly();
bun.Analytics.Features.spawn += 1;
// Windows doesn't support uid/gid
if (options.uid != null or options.gid != null) {
return .{ .err = bun.sys.Error.fromCode(.NOTSUP, .posix_spawn) };
}
var uv_process_options = std.mem.zeroes(uv.uv_process_options_t);
uv_process_options.args = argv;

View File

@@ -108,6 +108,8 @@ pub const BunSpawn = struct {
pub const Attr = struct {
detached: bool = false,
uid: ?u32 = null,
gid: ?u32 = null,
pub fn init() !Attr {
return Attr{};
@@ -278,10 +280,14 @@ pub const PosixSpawn = struct {
pub const Actions = if (Environment.isLinux) BunSpawn.Actions else PosixSpawnActions;
pub const Attr = if (Environment.isLinux) BunSpawn.Attr else PosixSpawnAttr;
const BunSpawnRequest = extern struct {
pub const BunSpawnRequest = extern struct {
chdir_buf: ?[*:0]u8 = null,
detached: bool = false,
actions: ActionsList = .{},
uid: u32 = 0,
gid: u32 = 0,
has_uid: bool = false,
has_gid: bool = false,
const ActionsList = extern struct {
ptr: ?[*]const BunSpawn.Action = null,
@@ -289,9 +295,8 @@ pub const PosixSpawn = struct {
};
extern fn posix_spawn_bun(
pid: *c_int,
path: [*:0]const u8,
request: *const BunSpawnRequest,
path: [*:0]const u8,
argv: [*:null]?[*:0]const u8,
envp: [*:null]?[*:0]const u8,
) isize;
@@ -303,23 +308,21 @@ pub const PosixSpawn = struct {
envp: [*:null]?[*:0]const u8,
) Maybe(pid_t) {
var req = req_;
var pid: c_int = 0;
const rc = posix_spawn_bun(&pid, path, &req, argv, envp);
const rc = posix_spawn_bun(&req, path, argv, envp);
if (comptime bun.Environment.allow_assert)
bun.sys.syslog("posix_spawn_bun({s}) = {d} ({d})", .{
bun.sys.syslog("posix_spawn_bun({s}) = {d}", .{
bun.span(argv[0] orelse ""),
rc,
pid,
});
if (rc == 0) {
return Maybe(pid_t){ .result = @intCast(pid) };
if (rc > 0) {
return Maybe(pid_t){ .result = @intCast(rc) };
}
return Maybe(pid_t){
.err = .{
.errno = @as(bun.sys.Error.Int, @truncate(@intFromEnum(@as(std.c.E, @enumFromInt(rc))))),
.errno = @as(bun.sys.Error.Int, @truncate(@intFromEnum(@as(std.c.E, @enumFromInt(-rc))))),
.syscall = .posix_spawn,
.path = bun.span(argv[0] orelse ""),
},
@@ -347,6 +350,10 @@ pub const PosixSpawn = struct {
},
.chdir_buf = if (actions) |a| a.chdir_buf else null,
.detached = if (attr) |a| a.detached else false,
.uid = if (attr) |a| a.uid orelse 0 else 0,
.gid = if (attr) |a| a.gid orelse 0 else 0,
.has_uid = if (attr) |a| a.uid != null else false,
.has_gid = if (attr) |a| a.gid != null else false,
},
argv,
envp,

View File

@@ -1964,6 +1964,8 @@ pub fn spawnMaybeSync(
var timeout: ?i32 = null;
var killSignal: SignalCode = SignalCode.default;
var maxBuffer: ?i64 = null;
var uid: ?u32 = null;
var gid: ?u32 = null;
var windows_hide: bool = false;
var windows_verbatim_arguments: bool = false;
@@ -2179,6 +2181,18 @@ pub fn spawnMaybeSync(
}
}
}
if (try args.get(globalThis, "uid")) |val| {
if (!val.isUndefinedOrNull()) {
uid = try globalThis.validateIntegerRange(val, u32, 0, .{ .min = 0, .field_name = "uid" });
}
}
if (try args.get(globalThis, "gid")) |val| {
if (!val.isUndefinedOrNull()) {
gid = try globalThis.validateIntegerRange(val, u32, 0, .{ .min = 0, .field_name = "gid" });
}
}
} else {
try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv);
}
@@ -2311,6 +2325,8 @@ pub fn spawnMaybeSync(
.extra_fds = extra_fds.items,
.argv0 = argv0,
.can_block_entire_thread_to_reduce_cpu_usage_in_fast_path = can_block_entire_thread_to_reduce_cpu_usage_in_fast_path,
.uid = uid,
.gid = gid,
.windows = if (Environment.isWindows) .{
.hide_window = windows_hide,

View File

@@ -0,0 +1,189 @@
#include "root.h"
#if OS(DARWIN)
#include <fcntl.h>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <errno.h>
#include <stdlib.h>
extern char** environ;
enum FileActionType : uint8_t {
None,
Close,
Dup2,
Open,
};
typedef struct bun_spawn_request_file_action_t {
FileActionType type;
const char* path;
int fds[2];
int flags;
int mode;
} bun_spawn_request_file_action_t;
typedef struct bun_spawn_file_action_list_t {
const bun_spawn_request_file_action_t* ptr;
size_t len;
} bun_spawn_file_action_list_t;
typedef struct bun_spawn_request_t {
const char* chdir;
bool detached;
bun_spawn_file_action_list_t actions;
uint32_t uid;
uint32_t gid;
bool has_uid;
bool has_gid;
} bun_spawn_request_t;
extern "C" ssize_t posix_spawn_bun(
const bun_spawn_request_t* request,
const char* path,
char* const argv[],
char* const envp[])
{
// Check permissions before forking
if (request->has_uid && request->uid != geteuid()) {
if (geteuid() != 0) {
errno = EPERM;
return -EPERM;
}
}
if (request->has_gid && request->gid != getegid()) {
if (geteuid() != 0) {
errno = EPERM;
return -EPERM;
}
}
pid_t pid;
int saved_errno;
sigset_t oldmask;
sigset_t newmask;
// Block all signals during fork to prevent signal handlers from running
sigfillset(&newmask);
sigprocmask(SIG_SETMASK, &newmask, &oldmask);
pid = fork();
saved_errno = errno;
if (pid == 0) {
// Child process
// Restore signal mask in child
sigprocmask(SIG_SETMASK, &oldmask, NULL);
// Reset signal handlers to default
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
for (int i = 1; i < NSIG; i++) {
// Skip SIGKILL and SIGSTOP as they can't be changed
if (i == SIGKILL || i == SIGSTOP) continue;
sigaction(i, &sa, NULL);
}
// Set up process session if detached
if (request->detached) {
setsid();
}
// Change directory if requested
if (request->chdir) {
if (chdir(request->chdir) != 0) {
_exit(127);
}
}
// Apply file actions
for (size_t i = 0; i < request->actions.len; i++) {
const bun_spawn_request_file_action_t* action = &request->actions.ptr[i];
switch (action->type) {
case Close:
close(action->fds[0]);
break;
case Dup2:
if (dup2(action->fds[0], action->fds[1]) < 0) {
_exit(127);
}
break;
case Open: {
int fd = open(action->path, action->flags, action->mode);
if (fd < 0) {
_exit(127);
}
if (fd != action->fds[0]) {
if (dup2(fd, action->fds[0]) < 0) {
_exit(127);
}
close(fd);
}
break;
}
default:
break;
}
}
// Close all file descriptors above stderr except those we just set up
int max_fd = getdtablesize();
for (int fd = 3; fd < max_fd; fd++) {
int flags = fcntl(fd, F_GETFD);
if (flags >= 0 && (flags & FD_CLOEXEC)) {
close(fd);
}
}
// Set group id before user id (required order)
if (request->has_gid) {
if (setgid(request->gid) != 0) {
_exit(127);
}
}
if (request->has_uid) {
if (setuid(request->uid) != 0) {
_exit(127);
}
}
// Execute the program
if (!envp) {
envp = environ;
}
execve(path, argv, envp);
// If we get here, execve failed
_exit(127);
}
// Parent process
sigprocmask(SIG_SETMASK, &oldmask, NULL);
if (pid < 0) {
// Fork failed
errno = saved_errno;
return -1;
}
return pid;
}
#endif

View File

@@ -45,6 +45,10 @@ typedef struct bun_spawn_request_t {
const char* chdir;
bool detached;
bun_spawn_file_action_list_t actions;
uint32_t uid;
uint32_t gid;
bool has_uid;
bool has_gid;
} bun_spawn_request_t;
extern "C" ssize_t posix_spawn_bun(
@@ -165,6 +169,19 @@ extern "C" ssize_t posix_spawn_bun(
if (!envp)
envp = environ;
// Set group id before user id (required order)
if (request->has_gid) {
if (setgid(request->gid) != 0) {
return childFailed();
}
}
if (request->has_uid) {
if (setuid(request->uid) != 0) {
return childFailed();
}
}
if (bun_close_range(current_max_fd + 1, ~0U, CLOSE_RANGE_CLOEXEC) != 0) {
bun_close_range(current_max_fd + 1, ~0U, 0);
}

View File

@@ -543,6 +543,8 @@ function spawnSync(file, args, options) {
timeout: options.timeout,
killSignal: options.killSignal,
maxBuffer: options.maxBuffer,
uid: options.uid,
gid: options.gid,
});
} catch (err) {
error = err;
@@ -1321,6 +1323,8 @@ class ChildProcess extends EventEmitter {
argv0: spawnargs[0],
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
uid: options.uid,
gid: options.gid,
});
this.pid = this.#handle.pid;

View File

@@ -0,0 +1,39 @@
import { spawn, spawnSync } from "child_process";
import { bunExe, isLinux, isMacOS } from "harness";
describe.if(isMacOS || isLinux)("uid/gid", () => {
test("cannot spawn root process", async () => {
expect(() => spawn("echo", ["test"], { uid: 0 })).toThrow(
expect.objectContaining({
code: "EPERM",
}),
);
});
test("cannot spawn root process (spawnSync)", async () => {
const result = spawnSync("echo", ["test"], { uid: 0 });
expect(result.error).toEqual(
expect.objectContaining({
code: "EPERM",
}),
);
});
test("can spawn user process with uid/gid", async () => {
const child3 = Bun.spawn({
cmd: [bunExe(), "-p", `JSON.stringify({uid: process.getuid?.(), gid: process.getgid?.()})`],
uid: process.getuid?.(),
gid: process.getgid?.(),
stdio: ["ignore", "pipe", "pipe"],
});
await child3.exited;
const output = await child3.stdout.json();
const stderr = await child3.stderr.text();
expect(output).toEqual({
uid: process.getuid?.(),
gid: process.getgid?.(),
});
expect(stderr).toBe("");
expect(child3.exitCode).toBe(0);
});
});

View File

@@ -0,0 +1,20 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const spawn = require('child_process').spawn;
const expectedError = common.isWindows ? /\bENOTSUP\b/ : /\bEPERM\b/;
if (common.isIBMi)
common.skip('IBMi has a different behavior');
if (common.isWindows || process.getuid() !== 0) {
assert.throws(() => {
spawn('echo', ['fhqwhgads'], { uid: 0 });
}, expectedError);
}
if (common.isWindows || !process.getgroups().some((gid) => gid === 0)) {
assert.throws(() => {
spawn('echo', ['fhqwhgads'], { gid: 0 });
}, expectedError);
}