mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
7 Commits
claude/fix
...
pfg/child-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a6a96bd5 | ||
|
|
4c4854170f | ||
|
|
4db7aa5441 | ||
|
|
d5d3fc2afc | ||
|
|
09b2b2d68f | ||
|
|
2d4c5cab0e | ||
|
|
920947c801 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
189
src/bun.js/bindings/bun-spawn-darwin.cpp
Normal file
189
src/bun.js/bindings/bun-spawn-darwin.cpp
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
39
test/js/node/child_process/child_process_uid_gid.test.ts
Normal file
39
test/js/node/child_process/child_process_uid_gid.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
20
test/js/node/test/parallel/test-child-process-uid-gid.js
Normal file
20
test/js/node/test/parallel/test-child-process-uid-gid.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user