Skip to content

Commit 223fd9f

Browse files
author
Jan Philipp Hafer
committed
std.child_process: enable non-standard streams
This PR simplifies the setup code and handling of non-standard file descriptors and handles for Windows and Posix with main focus on pipes. Portable pipes default to pipe2 with CLOEXEC on Posix and disabled handle inhertance on Windows except shortly before and after the creation of the "child process". Leaking of file desriptors 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. Leaking of handles on Windows 1. File leaking on Windows can be fixed with an explicit list approach as suggested in #14251, which might require runtime probing and allocation of the necessary process setup list. Otherwise, it might break on Kernel updates. 2. The potential time for leaking can be long due trying to spawn on multiple PATH and PATHEXT entries on Windows.
1 parent b52be97 commit 223fd9f

File tree

9 files changed

+285
-28
lines changed

9 files changed

+285
-28
lines changed

lib/std/child_process.zig

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ pub const ChildProcess = struct {
830830
fn spawnWindows(self: *ChildProcess) SpawnError!void {
831831
const saAttr = windows.SECURITY_ATTRIBUTES{
832832
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
833-
.bInheritHandle = windows.TRUE,
833+
.bInheritHandle = windows.FALSE,
834834
.lpSecurityDescriptor = null,
835835
};
836836

@@ -881,11 +881,24 @@ pub const ChildProcess = struct {
881881
windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
882882
};
883883

884+
var tmp_hChildStd_Rd: windows.HANDLE = undefined;
885+
var tmp_hChildStd_Wr: windows.HANDLE = undefined;
884886
var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
885887
var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
886888
switch (self.stdout_behavior) {
887889
StdIo.Pipe => {
888-
try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
890+
try windowsMakeAsyncPipe(
891+
&tmp_hChildStd_Rd,
892+
&tmp_hChildStd_Wr,
893+
&saAttr,
894+
);
895+
errdefer {
896+
os.close(tmp_hChildStd_Rd);
897+
os.close(tmp_hChildStd_Wr);
898+
}
899+
try windows.SetHandleInformation(tmp_hChildStd_Wr, windows.HANDLE_FLAG_INHERIT, 1);
900+
g_hChildStd_OUT_Rd = tmp_hChildStd_Rd;
901+
g_hChildStd_OUT_Wr = tmp_hChildStd_Wr;
889902
},
890903
StdIo.Ignore => {
891904
g_hChildStd_OUT_Wr = nul_handle;
@@ -905,7 +918,18 @@ pub const ChildProcess = struct {
905918
var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
906919
switch (self.stderr_behavior) {
907920
StdIo.Pipe => {
908-
try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
921+
try windowsMakeAsyncPipe(
922+
&tmp_hChildStd_Rd,
923+
&tmp_hChildStd_Wr,
924+
&saAttr,
925+
);
926+
errdefer {
927+
os.close(tmp_hChildStd_Rd);
928+
os.close(tmp_hChildStd_Wr);
929+
}
930+
try windows.SetHandleInformation(tmp_hChildStd_Wr, windows.HANDLE_FLAG_INHERIT, 1);
931+
g_hChildStd_ERR_Rd = tmp_hChildStd_Rd;
932+
g_hChildStd_ERR_Wr = tmp_hChildStd_Wr;
909933
},
910934
StdIo.Ignore => {
911935
g_hChildStd_ERR_Wr = nul_handle;
@@ -1103,6 +1127,28 @@ pub const ChildProcess = struct {
11031127
}
11041128
};
11051129

1130+
/// Pipe read side
1131+
pub const pipe_rd = 0;
1132+
/// Pipe write side
1133+
pub const pipe_wr = 1;
1134+
const PortPipeT = if (builtin.os.tag == .windows) [2]windows.HANDLE else [2]os.fd_t;
1135+
1136+
/// Portable pipe creation with disabled inheritance
1137+
pub inline fn portablePipe() !PortPipeT {
1138+
var pipe_new: PortPipeT = undefined;
1139+
if (builtin.os.tag == .windows) {
1140+
const saAttr = windows.SECURITY_ATTRIBUTES{
1141+
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
1142+
.bInheritHandle = windows.FALSE,
1143+
.lpSecurityDescriptor = null,
1144+
};
1145+
try windowsMakeAsyncPipe(&pipe_new[pipe_rd], &pipe_new[pipe_wr], &saAttr);
1146+
} else {
1147+
pipe_new = try os.pipe2(@as(u32, os.O.CLOEXEC));
1148+
}
1149+
return pipe_new;
1150+
}
1151+
11061152
/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
11071153
/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
11081154
/// Note: `app_buf` should not contain any leading path separators.
@@ -1333,30 +1379,25 @@ fn windowsCreateProcessPathExt(
13331379
}
13341380

13351381
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 {
1336-
// TODO the docs for environment pointer say:
1337-
// > A pointer to the environment block for the new process. If this parameter
1338-
// > is NULL, the new process uses the environment of the calling process.
1339-
// > ...
1340-
// > An environment block can contain either Unicode or ANSI characters. If
1341-
// > the environment block pointed to by lpEnvironment contains Unicode
1342-
// > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
1343-
// > If this parameter is NULL and the environment block of the parent process
1344-
// > contains Unicode characters, you must also ensure that dwCreationFlags
1345-
// > includes CREATE_UNICODE_ENVIRONMENT.
1346-
// This seems to imply that we have to somehow know whether our process parent passed
1347-
// CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
1348-
// Since we do not know this information that would imply that we must not pass NULL
1349-
// for the parameter.
1350-
// However this would imply that programs compiled with -DUNICODE could not pass
1351-
// environment variables to programs that were not, which seems unlikely.
1352-
// More investigation is needed.
1382+
// See https://stackoverflow.com/a/4207169/9306292
1383+
// One can manually write in unicode even if one doesn't compile in unicode
1384+
// (-DUNICODE).
1385+
// Thus CREATE_UNICODE_ENVIRONMENT, according to how one constructed the
1386+
// environment block, is necessary, since CreateProcessA and CreateProcessW may
1387+
// work with either Ansi or Unicode.
1388+
// * The environment variables can still be inherited from parent process,
1389+
// if set to NULL
1390+
// * The OS can for an unspecified environment block not figure out,
1391+
// if it is Unicode or ANSI.
1392+
// * Applications may break without specification of the environment variable
1393+
// due to inability of Windows to check (+translate) the character encodings.
13531394
return windows.CreateProcessW(
13541395
app_name,
13551396
cmd_line,
13561397
null,
13571398
null,
13581399
windows.TRUE,
1359-
windows.CREATE_UNICODE_ENVIRONMENT,
1400+
@enumToInt(windows.PROCESS_CREATION_FLAGS.CREATE_UNICODE_ENVIRONMENT),
13601401
@ptrCast(?*anyopaque, envp_ptr),
13611402
cwd_ptr,
13621403
lpStartupInfo,
@@ -1473,14 +1514,22 @@ fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const w
14731514
var wr_h: windows.HANDLE = undefined;
14741515
try windows.CreatePipe(&rd_h, &wr_h, sattr);
14751516
errdefer windowsDestroyPipe(rd_h, wr_h);
1476-
try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0);
1517+
try windows.SetHandleInformation(rd_h, windows.HANDLE_FLAG_INHERIT, 1);
14771518
rd.* = rd_h;
14781519
wr.* = wr_h;
14791520
}
14801521

14811522
var pipe_name_counter = std.atomic.Atomic(u32).init(1);
14821523

1483-
fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1524+
/// To enable/disable inheritance parent and child process, use
1525+
/// os.enableInheritance() and os.disableInheritance() on the handle.
1526+
/// convention: sattr uses bInheritHandle = windows.FALSE and only used pipe end
1527+
/// is enabled.
1528+
pub fn windowsMakeAsyncPipe(
1529+
rd: *windows.HANDLE,
1530+
wr: *windows.HANDLE,
1531+
sattr: *const windows.SECURITY_ATTRIBUTES,
1532+
) !void {
14841533
var tmp_bufw: [128]u16 = undefined;
14851534

14861535
// Anonymous pipes are built upon Named pipes.
@@ -1533,9 +1582,6 @@ fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *cons
15331582
else => |err| return windows.unexpectedError(err),
15341583
}
15351584
}
1536-
errdefer os.close(write_handle);
1537-
1538-
try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
15391585

15401586
rd.* = read_handle;
15411587
wr.* = write_handle;

lib/std/os.zig

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,27 @@ pub fn close(fd: fd_t) void {
288288
}
289289
}
290290

291+
pub const windowsPtrDigits = 19; // log10(max(usize))
292+
pub const unixoidPtrDigits = 10; // log10(max(u32)) + 1 for sign
293+
pub const handleCharSize = if (builtin.target.os.tag == .windows) windowsPtrDigits else unixoidPtrDigits;
294+
295+
pub fn handleToString(handle: fd_t, buf: []u8) std.fmt.BufPrintError![]u8 {
296+
var s_handle: []u8 = undefined;
297+
const handle_int =
298+
// handle is *anyopaque or an integer on unix-likes Kernels.
299+
if (builtin.target.os.tag == .windows) @ptrToInt(handle) else handle;
300+
s_handle = try std.fmt.bufPrint(buf[0..], "{d}", .{handle_int});
301+
return s_handle;
302+
}
303+
304+
pub fn stringToHandle(s_handle: []const u8) std.fmt.ParseIntError!std.os.fd_t {
305+
var handle: std.os.fd_t = if (builtin.target.os.tag == .windows)
306+
@intToPtr(windows.HANDLE, try std.fmt.parseInt(usize, s_handle, 10))
307+
else
308+
try std.fmt.parseInt(std.os.fd_t, s_handle, 10);
309+
return handle;
310+
}
311+
291312
pub const FChmodError = error{
292313
AccessDenied,
293314
InputOutput,
@@ -4866,6 +4887,44 @@ pub fn lseek_CUR_get(fd: fd_t) SeekError!u64 {
48664887
}
48674888
}
48684889

4890+
const IsInheritableError = FcntlError || windows.GetHandleInformationError;
4891+
4892+
/// Whether inheritence is enabled or CLOEXEC is not set.
4893+
pub inline fn isInheritable(handle: fd_t) IsInheritableError!bool {
4894+
if (builtin.os.tag == .windows) {
4895+
var handle_flags: windows.DWORD = undefined;
4896+
try windows.GetHandleInformation(handle, &handle_flags);
4897+
return handle_flags & windows.HANDLE_FLAG_INHERIT != 0;
4898+
} else {
4899+
const fcntl_flags = try fcntl(handle, F.GETFD, 0);
4900+
return fcntl_flags & FD_CLOEXEC == 0;
4901+
}
4902+
}
4903+
4904+
const EnableInheritanceError = FcntlError || windows.SetHandleInformationError;
4905+
4906+
/// Enables inheritence or sets CLOEXEC.
4907+
pub inline fn enableInheritance(handle: fd_t) EnableInheritanceError!void {
4908+
if (builtin.os.tag == .windows) {
4909+
try windows.SetHandleInformation(handle, windows.HANDLE_FLAG_INHERIT, 1);
4910+
} else {
4911+
var flags = try fcntl(handle, F.GETFD, 0);
4912+
flags &= ~@as(u32, FD_CLOEXEC);
4913+
_ = try fcntl(handle, F.SETFD, flags);
4914+
}
4915+
}
4916+
4917+
const DisableInheritanceError = FcntlError || windows.SetHandleInformationError;
4918+
4919+
/// Disables inheritence or unsets CLOEXEC.
4920+
pub inline fn disableInheritance(handle: fd_t) DisableInheritanceError!void {
4921+
if (builtin.os.tag == .windows) {
4922+
try windows.SetHandleInformation(handle, windows.HANDLE_FLAG_INHERIT, 0);
4923+
} else {
4924+
_ = try fcntl(handle, F.SETFD, FD_CLOEXEC);
4925+
}
4926+
}
4927+
48694928
pub const FcntlError = error{
48704929
PermissionDenied,
48714930
FileBusy,

lib/std/os/windows.zig

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ pub fn DeviceIoControl(
228228
}
229229
}
230230

231+
pub const GetHandleInformationError = error{Unexpected};
232+
233+
pub fn GetHandleInformation(h: HANDLE, flags: *DWORD) GetHandleInformationError!void {
234+
if (kernel32.GetHandleInformation(h, flags) == 0) {
235+
switch (kernel32.GetLastError()) {
236+
else => |err| return unexpectedError(err),
237+
}
238+
}
239+
}
240+
231241
pub fn GetOverlappedResult(h: HANDLE, overlapped: *OVERLAPPED, wait: bool) !DWORD {
232242
var bytes: DWORD = undefined;
233243
if (kernel32.GetOverlappedResult(h, overlapped, &bytes, @boolToInt(wait)) == 0) {
@@ -1590,6 +1600,45 @@ pub fn GetEnvironmentVariableW(lpName: LPWSTR, lpBuffer: [*]u16, nSize: DWORD) G
15901600
return rc;
15911601
}
15921602

1603+
// zig fmt: off
1604+
pub const PROCESS_CREATION_FLAGS = enum(u32) {
1605+
// <- gap here ->
1606+
DEBUG_PROCESS = 0x0000_0001,
1607+
DEBUG_ONLY_THIS_PROCESS = 0x0000_0002,
1608+
CREATE_SUSPENDED = 0x0000_0004,
1609+
DETACHED_PROCESS = 0x0000_0008,
1610+
CREATE_NEW_CONSOLE = 0x0000_0010,
1611+
NORMAL_PRIORITY_CLASS = 0x0000_0020,
1612+
IDLE_PRIORITY_CLASS = 0x0000_0040,
1613+
HIGH_PRIORITY_CLASS = 0x0000_0080,
1614+
REALTIME_PRIORITY_CLASS = 0x0000_0100,
1615+
CREATE_NEW_PROCESS_GROUP = 0x0000_0200,
1616+
CREATE_UNICODE_ENVIRONMENT = 0x0000_0400,
1617+
CREATE_SEPARATE_WOW_VDM = 0x0000_0800,
1618+
CREATE_SHARED_WOW_VDM = 0x0000_1000,
1619+
CREATE_FORCEDOS = 0x0000_2000,
1620+
BELOW_NORMAL_PRIORITY_CLASS = 0x0000_4000,
1621+
ABOVE_NORMAL_PRIORITY_CLASS = 0x0000_8000,
1622+
INHERIT_PARENT_AFFINITY = 0x0001_0000,
1623+
INHERIT_CALLER_PRIORITY = 0x0002_0000,
1624+
CREATE_PROTECTED_PROCESS = 0x0004_0000,
1625+
EXTENDED_STARTUPINFO_PRESENT = 0x0008_0000,
1626+
PROCESS_MODE_BACKGROUND_BEGIN = 0x0010_0000,
1627+
PROCESS_MODE_BACKGROUND_END = 0x0020_0000,
1628+
CREATE_SECURE_PROCESS = 0x0040_0000,
1629+
// <- gap here ->
1630+
CREATE_BREAKAWAY_FROM_JOB = 0x0100_0000,
1631+
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x0200_0000,
1632+
CREATE_DEFAULT_ERROR_MODE = 0x0400_0000,
1633+
CREATE_NO_WINDOW = 0x0800_0000,
1634+
PROFILE_USER = 0x1000_0000,
1635+
PROFILE_KERNEL = 0x2000_0000,
1636+
PROFILE_SERVER = 0x4000_0000,
1637+
CREATE_IGNORE_SYSTEM_DEFAULT = 0x8000_0000,
1638+
_,
1639+
};
1640+
// zig fmt: on
1641+
15931642
pub const CreateProcessError = error{
15941643
FileNotFound,
15951644
AccessDenied,
@@ -2940,8 +2989,6 @@ pub const COORD = extern struct {
29402989
Y: SHORT,
29412990
};
29422991

2943-
pub const CREATE_UNICODE_ENVIRONMENT = 1024;
2944-
29452992
pub const TLS_OUT_OF_INDEXES = 4294967295;
29462993
pub const IMAGE_TLS_DIRECTORY = extern struct {
29472994
StartAddressOfRawData: usize,

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;

lib/std/std.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub const base64 = @import("base64.zig");
5353
pub const bit_set = @import("bit_set.zig");
5454
pub const builtin = @import("builtin.zig");
5555
pub const c = @import("c.zig");
56+
pub const child_process = @import("child_process.zig");
5657
pub const coff = @import("coff.zig");
5758
pub const compress = @import("compress.zig");
5859
pub const crypto = @import("crypto.zig");

test/standalone.zig

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

7071
if (builtin.os.tag == .windows) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const Builder = @import("std").build.Builder;
2+
3+
pub fn build(b: *Builder) void {
4+
const target = b.standardTargetOptions(.{});
5+
const optimize = b.standardOptimizeOption(.{});
6+
7+
const child = b.addExecutable(.{
8+
.name = "child",
9+
.root_source_file = .{ .path = "child.zig" },
10+
.target = target,
11+
.optimize = optimize,
12+
});
13+
14+
const parent = b.addExecutable(.{
15+
.name = "parent",
16+
.root_source_file = .{ .path = "parent.zig" },
17+
.target = target,
18+
.optimize = optimize,
19+
});
20+
const run_cmd = parent.run();
21+
run_cmd.addArtifactArg(child);
22+
23+
const test_step = b.step("test", "Test it");
24+
test_step.dependOn(&run_cmd.step);
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
var file_handle = try std.os.stringToHandle(s_handle);
14+
defer std.os.close(file_handle);
15+
16+
// child inherited the handle, so inheritance must be enabled
17+
const is_inheritable = try std.os.isInheritable(file_handle);
18+
std.debug.assert(is_inheritable);
19+
20+
try std.os.disableInheritance(file_handle);
21+
var file_in = std.fs.File{ .handle = file_handle }; // read side of pipe
22+
const file_in_reader = file_in.reader();
23+
const message = try file_in_reader.readUntilDelimiterAlloc(gpa, '\x17', 20_000);
24+
defer gpa.free(message);
25+
try std.testing.expectEqualSlices(u8, message, "test123");
26+
}

0 commit comments

Comments
 (0)