Skip to content

Commit d824883

Browse files
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 ziglang#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 693b12f commit d824883

File tree

9 files changed

+285
-28
lines changed

9 files changed

+285
-28
lines changed

lib/std/child_process.zig

+72-26
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ pub const ChildProcess = struct {
821821
fn spawnWindows(self: *ChildProcess) SpawnError!void {
822822
const saAttr = windows.SECURITY_ATTRIBUTES{
823823
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
824-
.bInheritHandle = windows.TRUE,
824+
.bInheritHandle = windows.FALSE,
825825
.lpSecurityDescriptor = null,
826826
};
827827

@@ -872,11 +872,24 @@ pub const ChildProcess = struct {
872872
windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
873873
};
874874

875+
var tmp_hChildStd_Rd: windows.HANDLE = undefined;
876+
var tmp_hChildStd_Wr: windows.HANDLE = undefined;
875877
var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
876878
var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
877879
switch (self.stdout_behavior) {
878880
StdIo.Pipe => {
879-
try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
881+
try windowsMakeAsyncPipe(
882+
&tmp_hChildStd_Rd,
883+
&tmp_hChildStd_Wr,
884+
&saAttr,
885+
);
886+
errdefer {
887+
os.close(tmp_hChildStd_Rd);
888+
os.close(tmp_hChildStd_Wr);
889+
}
890+
try windows.SetHandleInformation(tmp_hChildStd_Wr, windows.HANDLE_FLAG_INHERIT, 1);
891+
g_hChildStd_OUT_Rd = tmp_hChildStd_Rd;
892+
g_hChildStd_OUT_Wr = tmp_hChildStd_Wr;
880893
},
881894
StdIo.Ignore => {
882895
g_hChildStd_OUT_Wr = nul_handle;
@@ -896,7 +909,18 @@ pub const ChildProcess = struct {
896909
var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
897910
switch (self.stderr_behavior) {
898911
StdIo.Pipe => {
899-
try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
912+
try windowsMakeAsyncPipe(
913+
&tmp_hChildStd_Rd,
914+
&tmp_hChildStd_Wr,
915+
&saAttr,
916+
);
917+
errdefer {
918+
os.close(tmp_hChildStd_Rd);
919+
os.close(tmp_hChildStd_Wr);
920+
}
921+
try windows.SetHandleInformation(tmp_hChildStd_Wr, windows.HANDLE_FLAG_INHERIT, 1);
922+
g_hChildStd_ERR_Rd = tmp_hChildStd_Rd;
923+
g_hChildStd_ERR_Wr = tmp_hChildStd_Wr;
900924
},
901925
StdIo.Ignore => {
902926
g_hChildStd_ERR_Wr = nul_handle;
@@ -1094,6 +1118,28 @@ pub const ChildProcess = struct {
10941118
}
10951119
};
10961120

1121+
/// Pipe read side
1122+
pub const pipe_rd = 0;
1123+
/// Pipe write side
1124+
pub const pipe_wr = 1;
1125+
const PortPipeT = if (builtin.os.tag == .windows) [2]windows.HANDLE else [2]os.fd_t;
1126+
1127+
/// Portable pipe creation with disabled inheritance
1128+
pub inline fn portablePipe() !PortPipeT {
1129+
var pipe_new: PortPipeT = undefined;
1130+
if (builtin.os.tag == .windows) {
1131+
const saAttr = windows.SECURITY_ATTRIBUTES{
1132+
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
1133+
.bInheritHandle = windows.FALSE,
1134+
.lpSecurityDescriptor = null,
1135+
};
1136+
try windowsMakeAsyncPipe(&pipe_new[pipe_rd], &pipe_new[pipe_wr], &saAttr);
1137+
} else {
1138+
pipe_new = try os.pipe2(@as(u32, os.O.CLOEXEC));
1139+
}
1140+
return pipe_new;
1141+
}
1142+
10971143
/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
10981144
/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
10991145
/// Note: `app_buf` should not contain any leading path separators.
@@ -1324,30 +1370,25 @@ fn windowsCreateProcessPathExt(
13241370
}
13251371

13261372
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.
1373+
// See https://stackoverflow.com/a/4207169/9306292
1374+
// One can manually write in unicode even if one doesn't compile in unicode
1375+
// (-DUNICODE).
1376+
// Thus CREATE_UNICODE_ENVIRONMENT, according to how one constructed the
1377+
// environment block, is necessary, since CreateProcessA and CreateProcessW may
1378+
// work with either Ansi or Unicode.
1379+
// * The environment variables can still be inherited from parent process,
1380+
// if set to NULL
1381+
// * The OS can for an unspecified environment block not figure out,
1382+
// if it is Unicode or ANSI.
1383+
// * Applications may break without specification of the environment variable
1384+
// due to inability of Windows to check (+translate) the character encodings.
13441385
return windows.CreateProcessW(
13451386
app_name,
13461387
cmd_line,
13471388
null,
13481389
null,
13491390
windows.TRUE,
1350-
windows.CREATE_UNICODE_ENVIRONMENT,
1391+
@enumToInt(windows.PROCESS_CREATION_FLAGS.CREATE_UNICODE_ENVIRONMENT),
13511392
@ptrCast(?*anyopaque, envp_ptr),
13521393
cwd_ptr,
13531394
lpStartupInfo,
@@ -1464,14 +1505,22 @@ fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const w
14641505
var wr_h: windows.HANDLE = undefined;
14651506
try windows.CreatePipe(&rd_h, &wr_h, sattr);
14661507
errdefer windowsDestroyPipe(rd_h, wr_h);
1467-
try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0);
1508+
try windows.SetHandleInformation(rd_h, windows.HANDLE_FLAG_INHERIT, 1);
14681509
rd.* = rd_h;
14691510
wr.* = wr_h;
14701511
}
14711512

14721513
var pipe_name_counter = std.atomic.Atomic(u32).init(1);
14731514

1474-
fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1515+
/// To enable/disable inheritance parent and child process, use
1516+
/// os.enableInheritance() and os.disableInheritance() on the handle.
1517+
/// convention: sattr uses bInheritHandle = windows.FALSE and only used pipe end
1518+
/// is enabled.
1519+
pub fn windowsMakeAsyncPipe(
1520+
rd: *windows.HANDLE,
1521+
wr: *windows.HANDLE,
1522+
sattr: *const windows.SECURITY_ATTRIBUTES,
1523+
) !void {
14751524
var tmp_bufw: [128]u16 = undefined;
14761525

14771526
// Anonymous pipes are built upon Named pipes.
@@ -1524,9 +1573,6 @@ fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *cons
15241573
else => |err| return windows.unexpectedError(err),
15251574
}
15261575
}
1527-
errdefer os.close(write_handle);
1528-
1529-
try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
15301576

15311577
rd.* = read_handle;
15321578
wr.* = write_handle;

lib/std/os.zig

+59
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

+49-2
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

+2
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

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub const base64 = @import("base64.zig");
5252
pub const bit_set = @import("bit_set.zig");
5353
pub const builtin = @import("builtin.zig");
5454
pub const c = @import("c.zig");
55+
pub const child_process = @import("child_process.zig");
5556
pub const coff = @import("coff.zig");
5657
pub const compress = @import("compress.zig");
5758
pub const crypto = @import("crypto.zig");

test/standalone.zig

+1
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) {
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+
}
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)