Skip to content

Commit 668d606

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 5b9e528 commit 668d606

File tree

9 files changed

+288
-28
lines changed

9 files changed

+288
-28
lines changed

lib/std/child_process.zig

+72-26
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ pub const ChildProcess = struct {
640640
fn spawnWindows(self: *ChildProcess) SpawnError!void {
641641
const saAttr = windows.SECURITY_ATTRIBUTES{
642642
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
643-
.bInheritHandle = windows.TRUE,
643+
.bInheritHandle = windows.FALSE,
644644
.lpSecurityDescriptor = null,
645645
};
646646

@@ -691,11 +691,24 @@ pub const ChildProcess = struct {
691691
windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
692692
};
693693

694+
var tmp_hChildStd_Rd: windows.HANDLE = undefined;
695+
var tmp_hChildStd_Wr: windows.HANDLE = undefined;
694696
var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
695697
var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
696698
switch (self.stdout_behavior) {
697699
StdIo.Pipe => {
698-
try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
700+
try windowsMakeAsyncPipe(
701+
&tmp_hChildStd_Rd,
702+
&tmp_hChildStd_Wr,
703+
&saAttr,
704+
);
705+
errdefer {
706+
os.close(tmp_hChildStd_Rd);
707+
os.close(tmp_hChildStd_Wr);
708+
}
709+
try windows.SetHandleInformation(tmp_hChildStd_Wr, windows.HANDLE_FLAG_INHERIT, 1);
710+
g_hChildStd_OUT_Rd = tmp_hChildStd_Rd;
711+
g_hChildStd_OUT_Wr = tmp_hChildStd_Wr;
699712
},
700713
StdIo.Ignore => {
701714
g_hChildStd_OUT_Wr = nul_handle;
@@ -715,7 +728,18 @@ pub const ChildProcess = struct {
715728
var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
716729
switch (self.stderr_behavior) {
717730
StdIo.Pipe => {
718-
try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
731+
try windowsMakeAsyncPipe(
732+
&tmp_hChildStd_Rd,
733+
&tmp_hChildStd_Wr,
734+
&saAttr,
735+
);
736+
errdefer {
737+
os.close(tmp_hChildStd_Rd);
738+
os.close(tmp_hChildStd_Wr);
739+
}
740+
try windows.SetHandleInformation(tmp_hChildStd_Wr, windows.HANDLE_FLAG_INHERIT, 1);
741+
g_hChildStd_ERR_Rd = tmp_hChildStd_Rd;
742+
g_hChildStd_ERR_Wr = tmp_hChildStd_Wr;
719743
},
720744
StdIo.Ignore => {
721745
g_hChildStd_ERR_Wr = nul_handle;
@@ -913,6 +937,28 @@ pub const ChildProcess = struct {
913937
}
914938
};
915939

940+
/// Pipe read side
941+
pub const pipe_rd = 0;
942+
/// Pipe write side
943+
pub const pipe_wr = 1;
944+
const PortPipeT = if (builtin.os.tag == .windows) [2]windows.HANDLE else [2]os.fd_t;
945+
946+
/// Portable pipe creation with disabled inheritance
947+
pub inline fn portablePipe() !PortPipeT {
948+
var pipe_new: PortPipeT = undefined;
949+
if (builtin.os.tag == .windows) {
950+
const saAttr = windows.SECURITY_ATTRIBUTES{
951+
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
952+
.bInheritHandle = windows.FALSE,
953+
.lpSecurityDescriptor = null,
954+
};
955+
try windowsMakeAsyncPipe(&pipe_new[pipe_rd], &pipe_new[pipe_wr], &saAttr);
956+
} else {
957+
pipe_new = try os.pipe2(@as(u32, os.O.CLOEXEC));
958+
}
959+
return pipe_new;
960+
}
961+
916962
/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
917963
/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
918964
/// Note: `app_buf` should not contain any leading path separators.
@@ -1143,30 +1189,25 @@ fn windowsCreateProcessPathExt(
11431189
}
11441190

11451191
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 {
1146-
// TODO the docs for environment pointer say:
1147-
// > A pointer to the environment block for the new process. If this parameter
1148-
// > is NULL, the new process uses the environment of the calling process.
1149-
// > ...
1150-
// > An environment block can contain either Unicode or ANSI characters. If
1151-
// > the environment block pointed to by lpEnvironment contains Unicode
1152-
// > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
1153-
// > If this parameter is NULL and the environment block of the parent process
1154-
// > contains Unicode characters, you must also ensure that dwCreationFlags
1155-
// > includes CREATE_UNICODE_ENVIRONMENT.
1156-
// This seems to imply that we have to somehow know whether our process parent passed
1157-
// CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
1158-
// Since we do not know this information that would imply that we must not pass NULL
1159-
// for the parameter.
1160-
// However this would imply that programs compiled with -DUNICODE could not pass
1161-
// environment variables to programs that were not, which seems unlikely.
1162-
// More investigation is needed.
1192+
// See https://stackoverflow.com/a/4207169/9306292
1193+
// One can manually write in unicode even if one doesn't compile in unicode
1194+
// (-DUNICODE).
1195+
// Thus CREATE_UNICODE_ENVIRONMENT, according to how one constructed the
1196+
// environment block, is necessary, since CreateProcessA and CreateProcessW may
1197+
// work with either Ansi or Unicode.
1198+
// * The environment variables can still be inherited from parent process,
1199+
// if set to NULL
1200+
// * The OS can for an unspecified environment block not figure out,
1201+
// if it is Unicode or ANSI.
1202+
// * Applications may break without specification of the environment variable
1203+
// due to inability of Windows to check (+translate) the character encodings.
11631204
return windows.CreateProcessW(
11641205
app_name,
11651206
cmd_line,
11661207
null,
11671208
null,
11681209
windows.TRUE,
1169-
windows.CREATE_UNICODE_ENVIRONMENT,
1210+
@enumToInt(windows.PROCESS_CREATION_FLAGS.CREATE_UNICODE_ENVIRONMENT),
11701211
@ptrCast(?*anyopaque, envp_ptr),
11711212
cwd_ptr,
11721213
lpStartupInfo,
@@ -1283,14 +1324,22 @@ fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const w
12831324
var wr_h: windows.HANDLE = undefined;
12841325
try windows.CreatePipe(&rd_h, &wr_h, sattr);
12851326
errdefer windowsDestroyPipe(rd_h, wr_h);
1286-
try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0);
1327+
try windows.SetHandleInformation(rd_h, windows.HANDLE_FLAG_INHERIT, 1);
12871328
rd.* = rd_h;
12881329
wr.* = wr_h;
12891330
}
12901331

12911332
var pipe_name_counter = std.atomic.Atomic(u32).init(1);
12921333

1293-
fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1334+
/// To enable/disable inheritance parent and child process, use
1335+
/// os.enableInheritance() and os.disableInheritance() on the handle.
1336+
/// convention: sattr uses bInheritHandle = windows.FALSE and only used pipe end
1337+
/// is enabled.
1338+
pub fn windowsMakeAsyncPipe(
1339+
rd: *windows.HANDLE,
1340+
wr: *windows.HANDLE,
1341+
sattr: *const windows.SECURITY_ATTRIBUTES,
1342+
) !void {
12941343
var tmp_bufw: [128]u16 = undefined;
12951344

12961345
// Anonymous pipes are built upon Named pipes.
@@ -1343,9 +1392,6 @@ fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *cons
13431392
else => |err| return windows.unexpectedError(err),
13441393
}
13451394
}
1346-
errdefer os.close(write_handle);
1347-
1348-
try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
13491395

13501396
rd.* = read_handle;
13511397
wr.* = write_handle;

lib/std/os.zig

+59
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,27 @@ pub fn close(fd: fd_t) void {
309309
}
310310
}
311311

312+
pub const windowsPtrDigits = 19; // log10(max(usize))
313+
pub const unixoidPtrDigits = 10; // log10(max(u32)) + 1 for sign
314+
pub const handleCharSize = if (builtin.target.os.tag == .windows) windowsPtrDigits else unixoidPtrDigits;
315+
316+
pub fn handleToString(handle: fd_t, buf: []u8) std.fmt.BufPrintError![]u8 {
317+
var s_handle: []u8 = undefined;
318+
const handle_int =
319+
// handle is *anyopaque or an integer on unix-likes Kernels.
320+
if (builtin.target.os.tag == .windows) @ptrToInt(handle) else handle;
321+
s_handle = try std.fmt.bufPrint(buf[0..], "{d}", .{handle_int});
322+
return s_handle;
323+
}
324+
325+
pub fn stringToHandle(s_handle: []const u8) std.fmt.ParseIntError!std.os.fd_t {
326+
var handle: std.os.fd_t = if (builtin.target.os.tag == .windows)
327+
@intToPtr(windows.HANDLE, try std.fmt.parseInt(usize, s_handle, 10))
328+
else
329+
try std.fmt.parseInt(std.os.fd_t, s_handle, 10);
330+
return handle;
331+
}
332+
312333
pub const FChmodError = error{
313334
AccessDenied,
314335
InputOutput,
@@ -4963,6 +4984,44 @@ pub fn lseek_CUR_get(fd: fd_t) SeekError!u64 {
49634984
}
49644985
}
49654986

4987+
const IsInheritableError = FcntlError || windows.GetHandleInformationError;
4988+
4989+
/// Whether inheritence is enabled or CLOEXEC is not set.
4990+
pub inline fn isInheritable(handle: fd_t) IsInheritableError!bool {
4991+
if (builtin.os.tag == .windows) {
4992+
var handle_flags: windows.DWORD = undefined;
4993+
try windows.GetHandleInformation(handle, &handle_flags);
4994+
return handle_flags & windows.HANDLE_FLAG_INHERIT != 0;
4995+
} else {
4996+
const fcntl_flags = try fcntl(handle, F.GETFD, 0);
4997+
return fcntl_flags & FD_CLOEXEC == 0;
4998+
}
4999+
}
5000+
5001+
const EnableInheritanceError = FcntlError || windows.SetHandleInformationError;
5002+
5003+
/// Enables inheritence or sets CLOEXEC.
5004+
pub inline fn enableInheritance(handle: fd_t) EnableInheritanceError!void {
5005+
if (builtin.os.tag == .windows) {
5006+
try windows.SetHandleInformation(handle, windows.HANDLE_FLAG_INHERIT, 1);
5007+
} else {
5008+
var flags = try fcntl(handle, F.GETFD, 0);
5009+
flags &= ~@as(u32, FD_CLOEXEC);
5010+
_ = try fcntl(handle, F.SETFD, flags);
5011+
}
5012+
}
5013+
5014+
const DisableInheritanceError = FcntlError || windows.SetHandleInformationError;
5015+
5016+
/// Disables inheritence or unsets CLOEXEC.
5017+
pub inline fn disableInheritance(handle: fd_t) DisableInheritanceError!void {
5018+
if (builtin.os.tag == .windows) {
5019+
try windows.SetHandleInformation(handle, windows.HANDLE_FLAG_INHERIT, 0);
5020+
} else {
5021+
_ = try fcntl(handle, F.SETFD, FD_CLOEXEC);
5022+
}
5023+
}
5024+
49665025
pub const FcntlError = error{
49675026
PermissionDenied,
49685027
FileBusy,

lib/std/os/windows.zig

+49-2
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,16 @@ pub fn DeviceIoControl(
240240
}
241241
}
242242

243+
pub const GetHandleInformationError = error{Unexpected};
244+
245+
pub fn GetHandleInformation(h: HANDLE, flags: *DWORD) GetHandleInformationError!void {
246+
if (kernel32.GetHandleInformation(h, flags) == 0) {
247+
switch (kernel32.GetLastError()) {
248+
else => |err| return unexpectedError(err),
249+
}
250+
}
251+
}
252+
243253
pub fn GetOverlappedResult(h: HANDLE, overlapped: *OVERLAPPED, wait: bool) !DWORD {
244254
var bytes: DWORD = undefined;
245255
if (kernel32.GetOverlappedResult(h, overlapped, &bytes, @boolToInt(wait)) == 0) {
@@ -1625,6 +1635,45 @@ pub fn GetEnvironmentVariableW(lpName: LPWSTR, lpBuffer: [*]u16, nSize: DWORD) G
16251635
return rc;
16261636
}
16271637

1638+
// zig fmt: off
1639+
pub const PROCESS_CREATION_FLAGS = enum(u32) {
1640+
// <- gap here ->
1641+
DEBUG_PROCESS = 0x0000_0001,
1642+
DEBUG_ONLY_THIS_PROCESS = 0x0000_0002,
1643+
CREATE_SUSPENDED = 0x0000_0004,
1644+
DETACHED_PROCESS = 0x0000_0008,
1645+
CREATE_NEW_CONSOLE = 0x0000_0010,
1646+
NORMAL_PRIORITY_CLASS = 0x0000_0020,
1647+
IDLE_PRIORITY_CLASS = 0x0000_0040,
1648+
HIGH_PRIORITY_CLASS = 0x0000_0080,
1649+
REALTIME_PRIORITY_CLASS = 0x0000_0100,
1650+
CREATE_NEW_PROCESS_GROUP = 0x0000_0200,
1651+
CREATE_UNICODE_ENVIRONMENT = 0x0000_0400,
1652+
CREATE_SEPARATE_WOW_VDM = 0x0000_0800,
1653+
CREATE_SHARED_WOW_VDM = 0x0000_1000,
1654+
CREATE_FORCEDOS = 0x0000_2000,
1655+
BELOW_NORMAL_PRIORITY_CLASS = 0x0000_4000,
1656+
ABOVE_NORMAL_PRIORITY_CLASS = 0x0000_8000,
1657+
INHERIT_PARENT_AFFINITY = 0x0001_0000,
1658+
INHERIT_CALLER_PRIORITY = 0x0002_0000,
1659+
CREATE_PROTECTED_PROCESS = 0x0004_0000,
1660+
EXTENDED_STARTUPINFO_PRESENT = 0x0008_0000,
1661+
PROCESS_MODE_BACKGROUND_BEGIN = 0x0010_0000,
1662+
PROCESS_MODE_BACKGROUND_END = 0x0020_0000,
1663+
CREATE_SECURE_PROCESS = 0x0040_0000,
1664+
// <- gap here ->
1665+
CREATE_BREAKAWAY_FROM_JOB = 0x0100_0000,
1666+
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x0200_0000,
1667+
CREATE_DEFAULT_ERROR_MODE = 0x0400_0000,
1668+
CREATE_NO_WINDOW = 0x0800_0000,
1669+
PROFILE_USER = 0x1000_0000,
1670+
PROFILE_KERNEL = 0x2000_0000,
1671+
PROFILE_SERVER = 0x4000_0000,
1672+
CREATE_IGNORE_SYSTEM_DEFAULT = 0x8000_0000,
1673+
_,
1674+
};
1675+
// zig fmt: on
1676+
16281677
pub const CreateProcessError = error{
16291678
FileNotFound,
16301679
AccessDenied,
@@ -2987,8 +3036,6 @@ pub const COORD = extern struct {
29873036
Y: SHORT,
29883037
};
29893038

2990-
pub const CREATE_UNICODE_ENVIRONMENT = 1024;
2991-
29923039
pub const TLS_OUT_OF_INDEXES = 4294967295;
29933040
pub const IMAGE_TLS_DIRECTORY = extern struct {
29943041
StartAddressOfRawData: usize,

lib/std/os/windows/kernel32.zig

+2
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ pub extern "kernel32" fn GetFullPathNameW(
223223
lpFilePart: ?*?[*:0]u16,
224224
) callconv(@import("std").os.windows.WINAPI) u32;
225225

226+
pub extern "kernel32" fn GetHandleInformation(hObject: HANDLE, dwFlags: *DWORD) callconv(WINAPI) BOOL;
227+
226228
pub extern "kernel32" fn GetOverlappedResult(hFile: HANDLE, lpOverlapped: *OVERLAPPED, lpNumberOfBytesTransferred: *DWORD, bWait: BOOL) callconv(WINAPI) BOOL;
227229

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

lib/std/std.zig

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub const base64 = @import("base64.zig");
5555
pub const bit_set = @import("bit_set.zig");
5656
pub const builtin = @import("builtin.zig");
5757
pub const c = @import("c.zig");
58+
pub const child_process = @import("child_process.zig");
5859
pub const coff = @import("coff.zig");
5960
pub const compress = @import("compress.zig");
6061
pub const crypto = @import("crypto.zig");

test/standalone.zig

+4
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ pub const build_cases = [_]BuildCase{
137137
.build_root = "test/standalone/install_raw_hex",
138138
.import = @import("standalone/install_raw_hex/build.zig"),
139139
},
140+
.{
141+
.build_root = "test/standalone/childprocess_extrapipe",
142+
.import = @import("standalone/childprocess_extrapipe/build.zig"),
143+
},
140144
// TODO take away EmitOption.emit_to option and make it give a FileSource
141145
//.{
142146
// .build_root = "test/standalone/emit_asm_and_bin",
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)