Skip to content

Commit a81e55b

Browse files
committed
std.child_process: enable non-standard streams
This PR simplifies the setup code and handling to minimize leaking time for both Windows and Posix and does not touch the list approach of Windows. Portable pipes default to pipe2 with CLOEXEC on Posix and disabled handle inhertence on Windows except shortly before and after the `spawn`. The potential time for leaking can be long due trying to spawn on multiple PATH and PATHEXT entries on Windows. Leaking on Posix 1. CLOEXEC does not take immediately effect with clone(), but after the setup code for the child process was run and a exec* function is executed. External code may at worst be long living and never do exec*. 2. File descriptors without CLOEXEC are designed to "leak to the child", but every spawned process at the same time gets them as well. File leaking on Windows can be fixed with an explicit list approach, which might require runtime probing to not break on Kernel updates for the parameter list.
1 parent a886794 commit a81e55b

File tree

8 files changed

+254
-33
lines changed

8 files changed

+254
-33
lines changed

lib/std/child_process.zig

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ pub const ChildProcess = struct {
5757

5858
expand_arg0: Arg0Expand,
5959

60+
/// Use this for additional posix spawn attributes.
61+
/// Do not set spawn attribute flags and do not modify stdin, stdout, stderr
62+
/// behavior, because those are set in the platform-specific spawn method.
63+
posix_attr: if (builtin.target.isDarwin()) ?os.posix_spawn.Attr else void,
64+
posix_action: if (builtin.target.isDarwin()) ?os.posix_spawn.Actions else void,
65+
6066
/// Darwin-only. Disable ASLR for the child process.
6167
disable_aslr: bool = false,
6268

@@ -100,6 +106,16 @@ pub const ChildProcess = struct {
100106
Close,
101107
};
102108

109+
pub const PipeDirection = enum {
110+
parent_to_child,
111+
child_to_parent,
112+
};
113+
114+
pub const PosixData = union(enum) {
115+
Attribute: os.posix_spawn.Attr,
116+
Actions: os.posix_spawn.Actions,
117+
};
118+
103119
/// First argument in argv is the executable.
104120
pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
105121
return .{
@@ -121,6 +137,8 @@ pub const ChildProcess = struct {
121137
.stdout_behavior = StdIo.Inherit,
122138
.stderr_behavior = StdIo.Inherit,
123139
.expand_arg0 = .no_expand,
140+
.posix_attr = if (comptime builtin.target.isDarwin()) null else undefined,
141+
.posix_action = if (comptime builtin.target.isDarwin()) null else undefined,
124142
};
125143
}
126144

@@ -548,6 +566,10 @@ pub const ChildProcess = struct {
548566
}
549567

550568
fn spawnMacos(self: *ChildProcess) SpawnError!void {
569+
// cleanup user-initialization and initialization in this function
570+
defer if (self.posix_attr != null) self.posix_attr.?.deinit();
571+
defer if (self.posix_actions != null) self.posix_actions.?.deinit();
572+
551573
const pipe_flags = if (io.is_async) os.O.NONBLOCK else 0;
552574
const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined;
553575
errdefer if (self.stdin_behavior == StdIo.Pipe) destroyPipe(stdin_pipe);
@@ -575,28 +597,28 @@ pub const ChildProcess = struct {
575597
undefined;
576598
defer if (any_ignore) os.close(dev_null_fd);
577599

578-
var attr = try os.posix_spawn.Attr.init();
579-
defer attr.deinit();
600+
if (self.posix_attr == null)
601+
self.posix_attr = try os.posix_spawn.Attr.init();
580602
var flags: u16 = os.darwin.POSIX_SPAWN_SETSIGDEF | os.darwin.POSIX_SPAWN_SETSIGMASK;
581603
if (self.disable_aslr) {
582604
flags |= os.darwin._POSIX_SPAWN_DISABLE_ASLR;
583605
}
584606
if (self.start_suspended) {
585607
flags |= os.darwin.POSIX_SPAWN_START_SUSPENDED;
586608
}
587-
try attr.set(flags);
609+
try self.posix_attr.?.set(flags);
588610

589-
var actions = try os.posix_spawn.Actions.init();
590-
defer actions.deinit();
611+
if (self.posix_actions == null)
612+
self.posix_actions = try os.posix_spawn.Actions.init();
591613

592-
try setUpChildIoPosixSpawn(self.stdin_behavior, &actions, stdin_pipe, os.STDIN_FILENO, dev_null_fd);
593-
try setUpChildIoPosixSpawn(self.stdout_behavior, &actions, stdout_pipe, os.STDOUT_FILENO, dev_null_fd);
594-
try setUpChildIoPosixSpawn(self.stderr_behavior, &actions, stderr_pipe, os.STDERR_FILENO, dev_null_fd);
614+
try setUpChildIoPosixSpawn(self.stdin_behavior, &self.posix_actions.?, stdin_pipe, os.STDIN_FILENO, dev_null_fd);
615+
try setUpChildIoPosixSpawn(self.stdout_behavior, &self.posix_actions.?, stdout_pipe, os.STDOUT_FILENO, dev_null_fd);
616+
try setUpChildIoPosixSpawn(self.stderr_behavior, &self.posix_actions.?, stderr_pipe, os.STDERR_FILENO, dev_null_fd);
595617

596618
if (self.cwd_dir) |cwd| {
597-
try actions.fchdir(cwd.fd);
619+
try self.posix_actions.?.fchdir(cwd.fd);
598620
} else if (self.cwd) |cwd| {
599-
try actions.chdir(cwd);
621+
try self.posix_actions.?.chdir(cwd);
600622
}
601623

602624
var arena_allocator = std.heap.ArenaAllocator.init(self.allocator);
@@ -611,7 +633,7 @@ pub const ChildProcess = struct {
611633
break :m envp_buf.ptr;
612634
} else std.c.environ;
613635

614-
const pid = try os.posix_spawn.spawnp(self.argv[0], actions, attr, argv_buf, envp);
636+
const pid = try os.posix_spawn.spawnp(self.argv[0], self.posix_actions.?, self.posix_attr.?, argv_buf, envp);
615637

616638
if (self.stdin_behavior == StdIo.Pipe) {
617639
self.stdin = File{ .handle = stdin_pipe[1] };
@@ -876,7 +898,12 @@ pub const ChildProcess = struct {
876898
var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
877899
switch (self.stdout_behavior) {
878900
StdIo.Pipe => {
879-
try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
901+
try windowsMakeAsyncPipe(
902+
&g_hChildStd_OUT_Rd,
903+
&g_hChildStd_OUT_Wr,
904+
&saAttr,
905+
PipeDirection.child_to_parent,
906+
);
880907
},
881908
StdIo.Ignore => {
882909
g_hChildStd_OUT_Wr = nul_handle;
@@ -896,7 +923,12 @@ pub const ChildProcess = struct {
896923
var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
897924
switch (self.stderr_behavior) {
898925
StdIo.Pipe => {
899-
try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
926+
try windowsMakeAsyncPipe(
927+
&g_hChildStd_ERR_Rd,
928+
&g_hChildStd_ERR_Wr,
929+
&saAttr,
930+
PipeDirection.child_to_parent,
931+
);
900932
},
901933
StdIo.Ignore => {
902934
g_hChildStd_ERR_Wr = nul_handle;
@@ -1324,23 +1356,18 @@ fn windowsCreateProcessPathExt(
13241356
}
13251357

13261358
fn windowsCreateProcess(app_name: [*:0]u16, cmd_line: [*:0]u16, envp_ptr: ?[*]u16, cwd_ptr: ?[*:0]u16, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION) !void {
1327-
// TODO the docs for environment pointer say:
1328-
// > A pointer to the environment block for the new process. If this parameter
1329-
// > is NULL, the new process uses the environment of the calling process.
1330-
// > ...
1331-
// > An environment block can contain either Unicode or ANSI characters. If
1332-
// > the environment block pointed to by lpEnvironment contains Unicode
1333-
// > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
1334-
// > If this parameter is NULL and the environment block of the parent process
1335-
// > contains Unicode characters, you must also ensure that dwCreationFlags
1336-
// > includes CREATE_UNICODE_ENVIRONMENT.
1337-
// This seems to imply that we have to somehow know whether our process parent passed
1338-
// CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
1339-
// Since we do not know this information that would imply that we must not pass NULL
1340-
// for the parameter.
1341-
// However this would imply that programs compiled with -DUNICODE could not pass
1342-
// environment variables to programs that were not, which seems unlikely.
1343-
// More investigation is needed.
1359+
// See https://stackoverflow.com/a/4207169/9306292
1360+
// One can manually write in unicode even if one doesn't compile in unicode
1361+
// (-DUNICODE).
1362+
// Thus CREATE_UNICODE_ENVIRONMENT, according to how one constructed the
1363+
// environment block, is necessary, since CreateProcessA and CreateProcessW may
1364+
// work with either Ansi or Unicode.
1365+
// * The environment variables can still be inherited from parent process,
1366+
// if set to NULL
1367+
// * The OS can for an unspecified environment block not figure out,
1368+
// if it is Unicode or ANSI.
1369+
// * Applications may break without specification of the environment variable
1370+
// due to inability of Windows to check (+translate) the character encodings.
13441371
return windows.CreateProcessW(
13451372
app_name,
13461373
cmd_line,
@@ -1471,7 +1498,16 @@ fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const w
14711498

14721499
var pipe_name_counter = std.atomic.Atomic(u32).init(1);
14731500

1474-
fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1501+
/// Create asynchronous pipe.
1502+
/// To enable inheritance between parent and child process, set bInheritHandle = windows.TRUE
1503+
/// in the security attributes.
1504+
/// Direction defines which handle is updated (to enable inheritance by ChildProcess)
1505+
pub fn windowsMakeAsyncPipe(
1506+
rd: *?windows.HANDLE,
1507+
wr: *?windows.HANDLE,
1508+
sattr: *const windows.SECURITY_ATTRIBUTES,
1509+
direction: ChildProcess.PipeDirection,
1510+
) !void {
14751511
var tmp_bufw: [128]u16 = undefined;
14761512

14771513
// Anonymous pipes are built upon Named pipes.
@@ -1526,8 +1562,10 @@ fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *cons
15261562
}
15271563
errdefer os.close(write_handle);
15281564

1529-
try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
1530-
1565+
switch (direction) {
1566+
.child_to_parent => try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT),
1567+
.parent_to_child => try windows.SetHandleInformation(write_handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT),
1568+
}
15311569
rd.* = read_handle;
15321570
wr.* = write_handle;
15331571
}

lib/std/os.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4843,6 +4843,16 @@ pub fn lseek_CUR_get(fd: fd_t) SeekError!u64 {
48434843
}
48444844
}
48454845

4846+
const UnsetFileInheritanceError = FcntlError || windows.SetHandleInformationError;
4847+
4848+
pub inline fn disableFileInheritance(file_handle: fd_t) UnsetFileInheritanceError!void {
4849+
if (builtin.os.tag == .windows) {
4850+
try windows.SetHandleInformation(file_handle, windows.HANDLE_FLAG_INHERIT, 0);
4851+
} else {
4852+
_ = try fcntl(file_handle, F.SETFD, FD_CLOEXEC);
4853+
}
4854+
}
4855+
48464856
pub const FcntlError = error{
48474857
PermissionDenied,
48484858
FileBusy,

lib/std/os/windows.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,16 @@ pub fn DeviceIoControl(
227227
}
228228
}
229229

230+
pub const GetHandleInformationError = error{Unexpected};
231+
232+
pub fn GetHandleInformation(h: HANDLE, flags: *DWORD) GetHandleInformationError!void {
233+
if (kernel32.GetHandleInformation(h, flags) == 0) {
234+
switch (kernel32.GetLastError()) {
235+
else => |err| return unexpectedError(err),
236+
}
237+
}
238+
}
239+
230240
pub fn GetOverlappedResult(h: HANDLE, overlapped: *OVERLAPPED, wait: bool) !DWORD {
231241
var bytes: DWORD = undefined;
232242
if (kernel32.GetOverlappedResult(h, overlapped, &bytes, @boolToInt(wait)) == 0) {

lib/std/os/windows/kernel32.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ pub extern "kernel32" fn GetFullPathNameW(
227227
lpFilePart: ?*?[*:0]u16,
228228
) callconv(@import("std").os.windows.WINAPI) u32;
229229

230+
pub extern "kernel32" fn GetHandleInformation(hObject: HANDLE, dwFlags: *DWORD) callconv(WINAPI) BOOL;
231+
230232
pub extern "kernel32" fn GetOverlappedResult(hFile: HANDLE, lpOverlapped: *OVERLAPPED, lpNumberOfBytesTransferred: *DWORD, bWait: BOOL) callconv(WINAPI) BOOL;
231233

232234
pub extern "kernel32" fn GetProcessHeap() callconv(WINAPI) ?HANDLE;

test/standalone.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ pub fn addCases(cases: *tests.StandaloneContext) void {
6464
(builtin.os.tag != .windows or builtin.cpu.arch != .aarch64))
6565
{
6666
cases.addBuildFile("test/standalone/load_dynamic_library/build.zig", .{});
67+
cases.addBuildFile("test/standalone/childprocess_extrapipe/build.zig", .{});
6768
}
6869

6970
if (builtin.os.tag == .windows) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const Builder = @import("std").build.Builder;
2+
3+
pub fn build(b: *Builder) void {
4+
const mode = b.standardReleaseOptions();
5+
6+
const child = b.addExecutable("child", "child.zig");
7+
child.setBuildMode(mode);
8+
9+
const parent = b.addExecutable("parent", "parent.zig");
10+
parent.setBuildMode(mode);
11+
const run_cmd = parent.run();
12+
run_cmd.addArtifactArg(child);
13+
14+
const test_step = b.step("test", "Test it");
15+
test_step.dependOn(&run_cmd.step);
16+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const std = @import("std");
2+
const builtin = @import("builtin");
3+
const windows = std.os.windows;
4+
pub fn main() !void {
5+
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
6+
defer if (general_purpose_allocator.deinit()) @panic("found memory leaks");
7+
const gpa = general_purpose_allocator.allocator();
8+
9+
var it = try std.process.argsWithAllocator(gpa);
10+
defer it.deinit();
11+
_ = it.next() orelse unreachable; // skip binary name
12+
const s_handle = it.next() orelse unreachable;
13+
14+
var file_handle: std.os.fd_t = file_handle: {
15+
if (builtin.target.os.tag == .windows) {
16+
var handle_int = try std.fmt.parseInt(usize, s_handle, 10);
17+
break :file_handle @intToPtr(windows.HANDLE, handle_int);
18+
} else {
19+
break :file_handle try std.fmt.parseInt(std.os.fd_t, s_handle, 10);
20+
}
21+
};
22+
if (builtin.target.os.tag == .windows) {
23+
// windows.HANDLE_FLAG_INHERIT is enabled
24+
var handle_flags: windows.DWORD = undefined;
25+
try windows.GetHandleInformation(file_handle, &handle_flags);
26+
try std.testing.expect(handle_flags & windows.HANDLE_FLAG_INHERIT != 0);
27+
} else {
28+
// FD_CLOEXEC is not set
29+
var fcntl_flags = try std.os.fcntl(file_handle, std.os.F.GETFD, 0);
30+
try std.testing.expect((fcntl_flags & std.os.FD_CLOEXEC) == 0);
31+
}
32+
try std.os.disableFileInheritance(file_handle);
33+
var file_in = std.fs.File{ .handle = file_handle }; // read side of pipe
34+
defer file_in.close();
35+
const file_in_reader = file_in.reader();
36+
const message = try file_in_reader.readUntilDelimiterAlloc(gpa, '\x17', 20_000);
37+
defer gpa.free(message);
38+
try std.testing.expectEqualSlices(u8, message, "test123");
39+
}

0 commit comments

Comments
 (0)