authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-30 21:37:08-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 00:27:08-08:00
log32af0f6154edb1c1434d0bb489d2abfea7da1685
tree2afb3763f4f8ba8facd35ebe47525810e69f93a3
parentd6a1e73142396732be80a7e0757cca1c07551d30

std: move child process APIs to std.Io

this gets the build runner compiling again on linux this work is incomplete; it only moves code around so that environment variables can be wrangled properly. a future commit will need to audit the cancelation and error handling of this moved logic.

30 files changed, 2170 insertions(+), 2223 deletions(-)

build.zig+1-1
......@@ -261,7 +261,7 @@ pub fn build(b: *std.Build) !void {
261261 "--git-dir", ".git", // affected by the -C argument
262262 "describe", "--match", "*.*.*", //
263263 "--tags", "--abbrev=9",
264 }, &code, .Ignore) catch {
264 }, &code, .ignore) catch {
265265 break :v version_string;
266266 };
267267 const git_describe = mem.trim(u8, git_describe_untrimmed, " \n\r");
lib/compiler/aro/aro/Driver.zig+6-6
......@@ -1256,9 +1256,9 @@ fn invokeAssembler(d: *Driver, tc: *Toolchain, input_path: []const u8, output_pa
12561256
12571257 var child = std.process.Child.init(&argv, d.comp.gpa);
12581258 // TODO handle better
1259 child.stdin_behavior = .Inherit;
1260 child.stdout_behavior = .Inherit;
1261 child.stderr_behavior = .Inherit;
1259 child.stdin_behavior = .inherit;
1260 child.stdout_behavior = .inherit;
1261 child.stderr_behavior = .inherit;
12621262
12631263 const term = child.spawnAndWait() catch |er| {
12641264 return d.fatal("unable to spawn linker: {s}", .{errorDescription(er)});
......@@ -1508,9 +1508,9 @@ pub fn invokeLinker(d: *Driver, tc: *Toolchain, comptime fast_exit: bool) Compil
15081508 }
15091509 var child = std.process.Child.init(argv.items, d.comp.gpa);
15101510 // TODO handle better
1511 child.stdin_behavior = .Inherit;
1512 child.stdout_behavior = .Inherit;
1513 child.stderr_behavior = .Inherit;
1511 child.stdin_behavior = .inherit;
1512 child.stdout_behavior = .inherit;
1513 child.stderr_behavior = .inherit;
15141514
15151515 const term = child.spawnAndWait() catch |er| {
15161516 return d.fatal("unable to spawn linker: {s}", .{errorDescription(er)});
lib/compiler/reduce.zig+1-1
......@@ -306,7 +306,7 @@ fn termToInteresting(term: std.process.Child.Term) Interestingness {
306306}
307307
308308fn runCheck(arena: Allocator, io: Io, argv: []const []const u8) !Interestingness {
309 const result = try std.process.Child.run(arena, io, .{ .argv = argv });
309 const result = try std.process.run(arena, io, .{ .spawn_options = .{ .argv = argv } });
310310 if (result.stderr.len != 0)
311311 std.debug.print("{s}", .{result.stderr});
312312 return termToInteresting(result.term);
lib/compiler/std-docs.zig+3-3
......@@ -447,9 +447,9 @@ fn openBrowserTabThread(gpa: Allocator, io: Io, url: []const u8) !void {
447447 else => "xdg-open",
448448 };
449449 var child = std.process.Child.init(&.{ main_exe, url }, gpa);
450 child.stdin_behavior = .Ignore;
451 child.stdout_behavior = .Ignore;
452 child.stderr_behavior = .Ignore;
450 child.stdin_behavior = .ignore;
451 child.stdout_behavior = .ignore;
452 child.stderr_behavior = .ignore;
453453 try child.spawn(io);
454454 _ = try child.wait(io);
455455}
lib/std/Build.zig+21-14
......@@ -190,7 +190,7 @@ pub const RunError = error{
190190 ExitCodeFailure,
191191 ProcessTerminated,
192192 ExecNotSupported,
193} || std.process.Child.SpawnError;
193} || std.process.SpawnError;
194194
195195pub const PkgConfigError = error{
196196 PkgConfigCrashed,
......@@ -1755,7 +1755,7 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
17551755}
17561756
17571757fn supportedWindowsProgramExtension(ext: []const u8) bool {
1758 inline for (@typeInfo(std.process.Child.WindowsExtension).@"enum".fields) |field| {
1758 inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| {
17591759 if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true;
17601760 }
17611761 return false;
......@@ -1830,23 +1830,26 @@ pub fn runAllowFail(
18301830 b: *Build,
18311831 argv: []const []const u8,
18321832 out_code: *u8,
1833 stderr_behavior: std.process.Child.StdIo,
1833 stderr_behavior: std.process.SpawnOptions.StdIo,
18341834) RunError![]u8 {
18351835 assert(argv.len != 0);
18361836
18371837 if (!process.can_spawn)
18381838 return error.ExecNotSupported;
18391839
1840 const io = b.graph.io;
1840 const graph = b.graph;
1841 const io = graph.io;
18411842
18421843 const max_output_size = 400 * 1024;
1843 var child = std.process.Child.init(b.allocator, argv, .{ .map = &b.graph.env_map });
1844 child.stdin_behavior = .Ignore;
1845 child.stdout_behavior = .Pipe;
1846 child.stderr_behavior = stderr_behavior;
1847
1848 try Step.handleVerbose2(b, null, child.environ.map, argv);
1849 try child.spawn(io);
1844 try Step.handleVerbose2(b, null, &graph.env_map, argv);
1845
1846 var child = try std.process.spawn(io, .{
1847 .argv = argv,
1848 .env_map = &graph.env_map,
1849 .stdin = .ignore,
1850 .stdout = .pipe,
1851 .stderr = stderr_behavior,
1852 });
18501853
18511854 var stdout_reader = child.stdout.?.readerStreaming(io, &.{});
18521855 const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch {
......@@ -1856,14 +1859,18 @@ pub fn runAllowFail(
18561859
18571860 const term = try child.wait(io);
18581861 switch (term) {
1859 .Exited => |code| {
1862 .exited => |code| {
18601863 if (code != 0) {
18611864 out_code.* = @as(u8, @truncate(code));
18621865 return error.ExitCodeFailure;
18631866 }
18641867 return stdout;
18651868 },
1866 .Signal, .Stopped, .Unknown => |code| {
1869 .signal => |sig| {
1870 out_code.* = @as(u8, @truncate(@intFromEnum(sig)));
1871 return error.ProcessTerminated;
1872 },
1873 .stopped, .unknown => |code| {
18671874 out_code.* = @as(u8, @truncate(code));
18681875 return error.ProcessTerminated;
18691876 },
......@@ -1882,7 +1889,7 @@ pub fn run(b: *Build, argv: []const []const u8) []u8 {
18821889 }
18831890
18841891 var code: u8 = undefined;
1885 return b.runAllowFail(argv, &code, .Inherit) catch |err| {
1892 return b.runAllowFail(argv, &code, .inherit) catch |err| {
18861893 const printed_cmd = Step.allocPrintCmd(b.allocator, null, null, argv) catch @panic("OOM");
18871894 std.debug.print("unable to spawn the following command: {t}\n{s}\n", .{ err, printed_cmd });
18881895 process.exit(1);
lib/std/Build/Step.zig+21-17
......@@ -348,7 +348,7 @@ pub fn captureChildProcess(
348348 gpa: Allocator,
349349 progress_node: std.Progress.Node,
350350 argv: []const []const u8,
351) !std.process.Child.RunResult {
351) !std.process.RunResult {
352352 const graph = s.owner.graph;
353353 const arena = graph.arena;
354354 const io = graph.io;
......@@ -360,11 +360,11 @@ pub fn captureChildProcess(
360360 try handleChildProcUnsupported(s);
361361 try handleVerbose(s.owner, null, argv);
362362
363 const result = std.process.Child.run(arena, io, .{
363 const result = std.process.run(arena, io, .{ .spawn_options = .{
364364 .argv = argv,
365 .environ = .{ .map = &graph.env_map },
365 .env_map = &graph.env_map,
366366 .progress_node = progress_node,
367 }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err });
367 } }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err });
368368
369369 if (result.stderr.len > 0) {
370370 try s.result_error_msgs.append(arena, result.stderr);
......@@ -444,19 +444,20 @@ pub fn evalZigProcess(
444444 return result;
445445 }
446446 assert(argv.len != 0);
447 const arena = b.allocator;
448447
449448 try handleChildProcUnsupported(s);
450449 try handleVerbose(s.owner, null, argv);
451450
452 var child = std.process.Child.init(arena, argv, .{ .map = &b.graph.env_map });
453 child.stdin_behavior = .Pipe;
454 child.stdout_behavior = .Pipe;
455 child.stderr_behavior = .Pipe;
456 child.request_resource_usage_statistics = true;
457 child.progress_node = prog_node;
458
459 child.spawn(io) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
451 var child = std.process.spawn(io, .{
452 .argv = argv,
453 .env_map = &b.graph.env_map,
454 .stdin = .pipe,
455 .stdout = .pipe,
456 .stderr = .pipe,
457 .request_resource_usage_statistics = true,
458 .progress_node = prog_node,
459 }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
460 defer if (!watch) child.kill(io);
460461
461462 const zp = try gpa.create(ZigProcess);
462463 zp.* = .{
......@@ -465,7 +466,7 @@ pub fn evalZigProcess(
465466 .stdout = child.stdout.?,
466467 .stderr = child.stderr.?,
467468 }),
468 .progress_ipc_fd = if (std.Progress.have_ipc) child.progress_node.getIpcFd() else {},
469 .progress_ipc_fd = if (std.Progress.have_ipc) prog_node.getIpcFd() else {},
469470 };
470471 if (watch) s.setZigProcess(zp);
471472 defer if (!watch) {
......@@ -487,7 +488,7 @@ pub fn evalZigProcess(
487488
488489 // Special handling for Compile step that is expecting compile errors.
489490 if (s.cast(Compile)) |compile| switch (term) {
490 .Exited => {
491 .exited => {
491492 // Note that the exit code may be 0 in this case due to the
492493 // compiler server protocol.
493494 if (compile.expect_errors != null) {
......@@ -719,12 +720,15 @@ pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFaile
719720pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void {
720721 assert(s.result_failed_command != null);
721722 switch (term) {
722 .Exited => |code| {
723 .exited => |code| {
723724 if (code != 0) {
724725 return s.fail("process exited with error code {d}", .{code});
725726 }
726727 },
727 .Signal, .Stopped, .Unknown => {
728 .signal => |sig| {
729 return s.fail("process terminated with signal {t}", .{sig});
730 },
731 .stopped, .unknown => {
728732 return s.fail("process terminated unexpectedly", .{});
729733 },
730734 }
lib/std/Build/Step/Compile.zig+2-2
......@@ -747,7 +747,7 @@ fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult {
747747 pkg_name,
748748 "--cflags",
749749 "--libs",
750 }, &code, .Ignore)) |stdout| stdout else |err| switch (err) {
750 }, &code, .ignore)) |stdout| stdout else |err| switch (err) {
751751 error.ProcessTerminated => return error.PkgConfigCrashed,
752752 error.ExecNotSupported => return error.PkgConfigFailed,
753753 error.ExitCodeFailure => return error.PkgConfigFailed,
......@@ -1847,7 +1847,7 @@ pub fn doAtomicSymLinks(
18471847
18481848fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
18491849 const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
1850 const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore);
1850 const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore);
18511851 var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator);
18521852 errdefer list.deinit();
18531853 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
lib/std/Build/Step/Fmt.zig+1-1
......@@ -69,7 +69,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
6969
7070 const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items);
7171 if (fmt.check) switch (run_result.term) {
72 .Exited => |code| if (code != 0 and run_result.stdout.len != 0) {
72 .exited => |code| if (code != 0 and run_result.stdout.len != 0) {
7373 var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n');
7474 while (it.next()) |bad_file_name| {
7575 try step.addError("{s}: non-conforming formatting", .{bad_file_name});
lib/std/Build/Step/Run.zig+67-73
......@@ -149,7 +149,7 @@ pub const StdIo = union(enum) {
149149 expect_stderr_match: []const u8,
150150 expect_stdout_exact: []const u8,
151151 expect_stdout_match: []const u8,
152 expect_term: std.process.Child.Term,
152 expect_term: process.Child.Term,
153153 };
154154};
155155
......@@ -618,7 +618,7 @@ pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void {
618618}
619619
620620pub fn expectExitCode(run: *Run, code: u8) void {
621 const new_check: StdIo.Check = .{ .expect_term = .{ .Exited = code } };
621 const new_check: StdIo.Check = .{ .expect_term = .{ .exited = code } };
622622 run.addCheck(new_check);
623623}
624624
......@@ -1182,40 +1182,40 @@ fn populateGeneratedPaths(
11821182 }
11831183}
11841184
1185fn formatTerm(term: ?std.process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
1185fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
11861186 if (term) |t| switch (t) {
1187 .Exited => |code| try w.print("exited with code {d}", .{code}),
1188 .Signal => |sig| try w.print("terminated with signal {d}", .{sig}),
1189 .Stopped => |sig| try w.print("stopped with signal {d}", .{sig}),
1190 .Unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
1187 .exited => |code| try w.print("exited with code {d}", .{code}),
1188 .signal => |sig| try w.print("terminated with signal {t}", .{sig}),
1189 .stopped => |sig| try w.print("stopped with signal {d}", .{sig}),
1190 .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
11911191 } else {
11921192 try w.writeAll("exited with any code");
11931193 }
11941194}
1195fn fmtTerm(term: ?std.process.Child.Term) std.fmt.Alt(?std.process.Child.Term, formatTerm) {
1195fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) {
11961196 return .{ .data = term };
11971197}
11981198
1199fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term) bool {
1199fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
12001200 return if (expected) |e| switch (e) {
1201 .Exited => |expected_code| switch (actual) {
1202 .Exited => |actual_code| expected_code == actual_code,
1201 .exited => |expected_code| switch (actual) {
1202 .exited => |actual_code| expected_code == actual_code,
12031203 else => false,
12041204 },
1205 .Signal => |expected_sig| switch (actual) {
1206 .Signal => |actual_sig| expected_sig == actual_sig,
1205 .signal => |expected_sig| switch (actual) {
1206 .signal => |actual_sig| expected_sig == actual_sig,
12071207 else => false,
12081208 },
1209 .Stopped => |expected_sig| switch (actual) {
1210 .Stopped => |actual_sig| expected_sig == actual_sig,
1209 .stopped => |expected_sig| switch (actual) {
1210 .stopped => |actual_sig| expected_sig == actual_sig,
12111211 else => false,
12121212 },
1213 .Unknown => |expected_code| switch (actual) {
1214 .Unknown => |actual_code| expected_code == actual_code,
1213 .unknown => |expected_code| switch (actual) {
1214 .unknown => |actual_code| expected_code == actual_code,
12151215 else => false,
12161216 },
12171217 } else switch (actual) {
1218 .Exited => true,
1218 .exited => true,
12191219 else => false,
12201220 };
12211221}
......@@ -1526,8 +1526,8 @@ fn runCommand(
15261526 else => {
15271527 // On failure, report captured stderr like normal standard error output.
15281528 const bad_exit = switch (generic_result.term) {
1529 .Exited => |code| code != 0,
1530 .Signal, .Stopped, .Unknown => true,
1529 .exited => |code| code != 0,
1530 .signal, .stopped, .unknown => true,
15311531 };
15321532 if (bad_exit) {
15331533 if (generic_result.stderr) |bytes| {
......@@ -1541,7 +1541,7 @@ fn runCommand(
15411541}
15421542
15431543const EvalGenericResult = struct {
1544 term: std.process.Child.Term,
1544 term: process.Child.Term,
15451545 stdout: ?[]const u8,
15461546 stderr: ?[]const u8,
15471547};
......@@ -1555,7 +1555,6 @@ fn spawnChildAndCollect(
15551555 fuzz_context: ?FuzzContext,
15561556) !?EvalGenericResult {
15571557 const b = run.step.owner;
1558 const arena = b.allocator;
15591558 const graph = b.graph;
15601559 const io = graph.io;
15611560
......@@ -1564,53 +1563,52 @@ fn spawnChildAndCollect(
15641563 assert(run.stdio == .zig_test);
15651564 }
15661565
1567 var child = std.process.Child.init(arena, argv, .{ .map = env_map });
1568 if (run.cwd) |lazy_cwd| {
1569 child.cwd = lazy_cwd.getPath2(b, &run.step);
1570 }
1571 child.request_resource_usage_statistics = true;
1572
1573 child.stdin_behavior = switch (run.stdio) {
1574 .infer_from_args => if (has_side_effects) .Inherit else .Ignore,
1575 .inherit => .Inherit,
1576 .check => .Ignore,
1577 .zig_test => .Pipe,
1578 };
1579 child.stdout_behavior = switch (run.stdio) {
1580 .infer_from_args => if (has_side_effects) .Inherit else .Ignore,
1581 .inherit => .Inherit,
1582 .check => |checks| if (checksContainStdout(checks.items)) .Pipe else .Ignore,
1583 .zig_test => .Pipe,
1584 };
1585 child.stderr_behavior = switch (run.stdio) {
1586 .infer_from_args => if (has_side_effects) .Inherit else .Pipe,
1587 .inherit => .Inherit,
1588 .check => .Pipe,
1589 .zig_test => .Pipe,
1590 };
1591 if (run.captured_stdout != null) child.stdout_behavior = .Pipe;
1592 if (run.captured_stderr != null) child.stderr_behavior = .Pipe;
1593 if (run.stdin != .none) {
1594 assert(run.stdio != .inherit);
1595 child.stdin_behavior = .Pipe;
1596 }
1566 const child_cwd = if (run.cwd) |lazy_cwd| lazy_cwd.getPath2(b, &run.step) else null;
15971567
15981568 // If an error occurs, it's caused by this command:
15991569 assert(run.step.result_failed_command == null);
1600 run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child.cwd, .{
1570 run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{
16011571 .child = env_map,
16021572 .parent = &graph.env_map,
16031573 }, argv);
16041574
1575 var spawn_options: process.SpawnOptions = .{
1576 .argv = argv,
1577 .cwd = child_cwd,
1578 .env_map = &graph.env_map,
1579 .request_resource_usage_statistics = true,
1580 .stdin = if (run.stdin != .none) s: {
1581 assert(run.stdio != .inherit);
1582 break :s .pipe;
1583 } else switch (run.stdio) {
1584 .infer_from_args => if (has_side_effects) .inherit else .ignore,
1585 .inherit => .inherit,
1586 .check => .ignore,
1587 .zig_test => .pipe,
1588 },
1589 .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) {
1590 .infer_from_args => if (has_side_effects) .inherit else .ignore,
1591 .inherit => .inherit,
1592 .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore,
1593 .zig_test => .pipe,
1594 },
1595 .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) {
1596 .infer_from_args => if (has_side_effects) .inherit else .pipe,
1597 .inherit => .inherit,
1598 .check => .pipe,
1599 .zig_test => .pipe,
1600 },
1601 };
1602
16051603 if (run.stdio == .zig_test) {
16061604 var timer = try std.time.Timer.start();
16071605 defer run.step.result_duration_ns = timer.read();
1608 try evalZigTest(run, &child, options, fuzz_context);
1606 try evalZigTest(run, spawn_options, options, fuzz_context);
16091607 return null;
16101608 } else {
1611 const inherit = child.stdout_behavior == .Inherit or child.stderr_behavior == .Inherit;
1609 const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit;
16121610 if (!run.disable_zig_progress and !inherit) {
1613 child.progress_node = options.progress_node;
1611 spawn_options.progress_node = options.progress_node;
16141612 }
16151613 const terminal_mode: Io.Terminal.Mode = if (inherit) m: {
16161614 const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
......@@ -1619,7 +1617,7 @@ fn spawnChildAndCollect(
16191617 defer if (inherit) io.unlockStderr();
16201618 try setColorEnvironmentVariables(run, env_map, terminal_mode);
16211619 var timer = try std.time.Timer.start();
1622 const res = try evalGeneric(run, &child);
1620 const res = try evalGeneric(run, spawn_options);
16231621 run.step.result_duration_ns = timer.read();
16241622 return .{ .term = res.term, .stdout = res.stdout, .stderr = res.stderr };
16251623 }
......@@ -1658,7 +1656,7 @@ const StdioPollEnum = enum { stdout, stderr };
16581656
16591657fn evalZigTest(
16601658 run: *Run,
1661 child: *std.process.Child,
1659 spawn_options: process.SpawnOptions,
16621660 options: Step.MakeOptions,
16631661 fuzz_context: ?FuzzContext,
16641662) !void {
......@@ -1682,14 +1680,14 @@ fn evalZigTest(
16821680 var test_metadata: ?TestMetadata = null;
16831681
16841682 while (true) {
1685 try child.spawn(io);
1683 var child = try process.spawn(io, spawn_options);
16861684 var poller = std.Io.poll(gpa, StdioPollEnum, .{
16871685 .stdout = child.stdout.?,
16881686 .stderr = child.stderr.?,
16891687 });
16901688 var child_killed = false;
16911689 defer if (!child_killed) {
1692 _ = child.kill(io) catch {};
1690 child.kill(io);
16931691 poller.deinit();
16941692 run.step.result_peak_rss = @max(
16951693 run.step.result_peak_rss,
......@@ -1697,11 +1695,9 @@ fn evalZigTest(
16971695 );
16981696 };
16991697
1700 try child.waitForSpawn();
1701
17021698 switch (try pollZigTest(
17031699 run,
1704 child,
1700 &child,
17051701 options,
17061702 fuzz_context,
17071703 &poller,
......@@ -1763,7 +1759,7 @@ fn evalZigTest(
17631759 // Report an error if the child terminated uncleanly or if we were still trying to run more tests.
17641760 run.step.result_stderr = stderr_owned;
17651761 const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32);
1766 if (!tests_done or !termMatches(.{ .Exited = 0 }, term)) {
1762 if (!tests_done or !termMatches(.{ .exited = 0 }, term)) {
17671763 // The individual unit test results are irrelevant: the test runner itself broke!
17681764 // Fail immediately without populating `s.test_results`.
17691765 return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)});
......@@ -1818,7 +1814,7 @@ fn evalZigTest(
18181814/// * `poll` fails, indicating the child closed stdout and stderr
18191815fn pollZigTest(
18201816 run: *Run,
1821 child: *std.process.Child,
1817 child: *process.Child,
18221818 options: Step.MakeOptions,
18231819 fuzz_context: ?FuzzContext,
18241820 poller: *std.Io.Poller(StdioPollEnum),
......@@ -2176,15 +2172,13 @@ fn sendRunFuzzTestMessage(
21762172 };
21772173}
21782174
2179fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult {
2175fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult {
21802176 const b = run.step.owner;
21812177 const io = b.graph.io;
21822178 const arena = b.allocator;
21832179
2184 try child.spawn(io);
2185 errdefer _ = child.kill(io) catch {};
2186
2187 try child.waitForSpawn();
2180 var child = try process.spawn(io, spawn_options);
2181 defer child.kill(io);
21882182
21892183 switch (run.stdin) {
21902184 .bytes => |bytes| {
......@@ -2334,10 +2328,10 @@ fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void {
23342328 => |s| hh.addBytes(s),
23352329
23362330 .expect_term => |term| {
2337 hh.add(@as(std.meta.Tag(std.process.Child.Term), term));
2331 hh.add(@as(std.meta.Tag(process.Child.Term), term));
23382332 switch (term) {
2339 .Exited => |x| hh.add(x),
2340 .Signal, .Stopped, .Unknown => |x| hh.add(x),
2333 inline .exited, .signal => |x| hh.add(x),
2334 .stopped, .unknown => |x| hh.add(x),
23412335 }
23422336 },
23432337 }
lib/std/Build/WebServer.zig+17-7
......@@ -572,11 +572,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
572572 "--listen=-",
573573 });
574574
575 var child: std.process.Child = .init(gpa, argv.items, .{ .map = &graph.env_map });
576 child.stdin_behavior = .Pipe;
577 child.stdout_behavior = .Pipe;
578 child.stderr_behavior = .Pipe;
579 try child.spawn(io);
575 var child = try std.process.spawn(io, .{
576 .argv = argv.items,
577 .env_map = &graph.env_map,
578 .stdin = .pipe,
579 .stdout = .pipe,
580 .stderr = .pipe,
581 });
582 defer child.kill(io);
580583
581584 var poller = Io.poll(gpa, enum { stdout, stderr }, .{
582585 .stdout = child.stdout.?,
......@@ -636,7 +639,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
636639 child.stdin = null;
637640
638641 switch (try child.wait(io)) {
639 .Exited => |code| {
642 .exited => |code| {
640643 if (code != 0) {
641644 log.err(
642645 "the following command exited with error code {d}:\n{s}",
......@@ -645,7 +648,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
645648 return error.WasmCompilationFailed;
646649 }
647650 },
648 .Signal, .Stopped, .Unknown => {
651 .signal => |sig| {
652 log.err(
653 "the following command terminated with signal {t}:\n{s}",
654 .{ sig, try Build.Step.allocPrintCmd(arena, null, null, argv.items) },
655 );
656 return error.WasmCompilationFailed;
657 },
658 .stopped, .unknown => {
649659 log.err(
650660 "the following command terminated unexpectedly:\n{s}",
651661 .{try Build.Step.allocPrintCmd(arena, null, null, argv.items)},
lib/std/Io.zig+6
......@@ -717,6 +717,12 @@ pub const VTable = struct {
717717 tryLockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!?LockedStderr,
718718 unlockStderr: *const fn (?*anyopaque) void,
719719 processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void,
720 processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError,
721 processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError,
722 processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child,
723 processSpawnPath: *const fn (?*anyopaque, Dir, std.process.SpawnOptions) std.process.SpawnError!std.process.Child,
724 childWait: *const fn (?*anyopaque, *std.process.Child) std.process.Child.WaitError!std.process.Child.Term,
725 childKill: *const fn (?*anyopaque, *std.process.Child) void,
720726
721727 now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp,
722728 sleep: *const fn (?*anyopaque, Timeout) SleepError!void,
lib/std/Io/Threaded.zig+1675-23
......@@ -13,6 +13,7 @@ const File = std.Io.File;
1313const Dir = std.Io.Dir;
1414const HostName = std.Io.net.HostName;
1515const IpAddress = std.Io.net.IpAddress;
16const process = std.process;
1617const Allocator = std.mem.Allocator;
1718const Alignment = std.mem.Alignment;
1819const assert = std.debug.assert;
......@@ -72,7 +73,7 @@ pub const Argv0 = switch (native_os) {
7273
7374pub const Environ = struct {
7475 /// Unmodified data directly from the OS.
75 block: std.process.Environ.Block = &.{},
76 block: process.Environ.Block = &.{},
7677 /// Protected by `mutex`. Determines whether the other fields have been
7778 /// memoized based on `block`.
7879 initialized: bool = false,
......@@ -95,10 +96,10 @@ pub const Environ = struct {
9596 };
9697
9798 pub const String = switch (native_os) {
98 .openbsd, .haiku => struct {
99 .windows, .wasi => struct {},
100 else => struct {
99101 PATH: ?[:0]const u8 = null,
100102 },
101 else => struct {},
102103 };
103104};
104105
......@@ -1400,6 +1401,12 @@ pub fn io(t: *Threaded) Io {
14001401 .tryLockStderr = tryLockStderr,
14011402 .unlockStderr = unlockStderr,
14021403 .processSetCurrentDir = processSetCurrentDir,
1404 .processReplace = processReplace, // TODO audit for cancelation and unreachable
1405 .processReplacePath = processReplacePath, // TODO audit for cancelation and unreachable
1406 .processSpawn = processSpawn, // TODO audit for cancelation and unreachable
1407 .processSpawnPath = processSpawnPath, // TODO audit for cancelation and unreachable
1408 .childWait = childWait, // TODO audit for cancelation and unreachable
1409 .childKill = childKill, // TODO audit for cancelation and unreachable
14031410
14041411 .now = now,
14051412 .sleep = sleep,
......@@ -1538,6 +1545,12 @@ pub fn ioBasic(t: *Threaded) Io {
15381545 .tryLockStderr = tryLockStderr,
15391546 .unlockStderr = unlockStderr,
15401547 .processSetCurrentDir = processSetCurrentDir,
1548 .processReplace = processReplace,
1549 .processReplacePath = processReplacePath,
1550 .processSpawn = processSpawn,
1551 .processSpawnPath = processSpawnPath,
1552 .childWait = childWait,
1553 .childKill = childKill,
15411554
15421555 .now = now,
15431556 .sleep = sleep,
......@@ -1601,6 +1614,11 @@ const have_fchmod = switch (native_os) {
16011614 else => true,
16021615};
16031616
1617const have_wait4 = switch (native_os) {
1618 .dragonfly, .freebsd, .netbsd, .openbsd, .illumos, .linux, .serenity, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => true,
1619 else => false,
1620};
1621
16041622const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;
16051623const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
16061624const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat;
......@@ -2247,7 +2265,7 @@ fn dirCreateDirPath(
22472265) Dir.CreateDirPathError!Dir.CreatePathStatus {
22482266 const t: *Threaded = @ptrCast(@alignCast(userdata));
22492267
2250 var it = std.fs.path.componentIterator(sub_path);
2268 var it = Dir.path.componentIterator(sub_path);
22512269 var status: Dir.CreatePathStatus = .existed;
22522270 var component = it.last() orelse return error.BadPathName;
22532271 while (true) {
......@@ -2307,9 +2325,9 @@ fn dirCreateDirPathOpenWindows(
23072325
23082326 _ = permissions; // TODO apply these permissions
23092327
2310 var it = std.fs.path.componentIterator(sub_path);
2328 var it = Dir.path.componentIterator(sub_path);
23112329 // If there are no components in the path, then create a dummy component with the full path.
2312 var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
2330 var component: Dir.path.NativeComponentIterator.Component = it.last() orelse .{
23132331 .name = "",
23142332 .path = sub_path,
23152333 };
......@@ -2347,7 +2365,7 @@ fn dirCreateDirPathOpenWindows(
23472365 },
23482366 &.{
23492367 .Length = @sizeOf(w.OBJECT_ATTRIBUTES),
2350 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
2368 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
23512369 .Attributes = .{},
23522370 .ObjectName = &nt_name,
23532371 .SecurityDescriptor = null,
......@@ -2986,7 +3004,7 @@ fn dirAccessWindows(
29863004 };
29873005 var attr: windows.OBJECT_ATTRIBUTES = .{
29883006 .Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
2989 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
3007 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
29903008 .Attributes = .{},
29913009 .ObjectName = &nt_name,
29923010 .SecurityDescriptor = null,
......@@ -3535,7 +3553,7 @@ fn dirOpenFileWindows(
35353553 _ = t;
35363554 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
35373555 const sub_path_w = sub_path_w_array.span();
3538 const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
3556 const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
35393557 return dirOpenFileWtf16(dir_handle, sub_path_w, flags);
35403558}
35413559
......@@ -3933,7 +3951,7 @@ pub fn dirOpenDirWindows(
39333951 },
39343952 &.{
39353953 .Length = @sizeOf(w.OBJECT_ATTRIBUTES),
3936 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
3954 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
39373955 .Attributes = .{},
39383956 .ObjectName = &nt_name,
39393957 .SecurityDescriptor = null,
......@@ -5081,7 +5099,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov
50815099 } },
50825100 &.{
50835101 .Length = @sizeOf(w.OBJECT_ATTRIBUTES),
5084 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
5102 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
50855103 .Attributes = .{},
50865104 .ObjectName = &nt_name,
50875105 .SecurityDescriptor = null,
......@@ -5358,7 +5376,7 @@ fn dirRenameWindows(
53585376 .POSIX_SEMANTICS = true,
53595377 .IGNORE_READONLY_ATTRIBUTE = true,
53605378 },
5361 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
5379 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
53625380 .FileName = new_path_w,
53635381 });
53645382 var io_status_block: w.IO_STATUS_BLOCK = undefined;
......@@ -5387,7 +5405,7 @@ fn dirRenameWindows(
53875405 if (need_fallback) {
53885406 const rename_info: w.FILE.RENAME_INFORMATION = .init(.{
53895407 .Flags = .{ .REPLACE_IF_EXISTS = replace_if_exists },
5390 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
5408 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
53915409 .FileName = new_path_w,
53925410 });
53935411 var io_status_block: w.IO_STATUS_BLOCK = undefined;
......@@ -5622,13 +5640,13 @@ fn dirSymLinkWindows(
56225640 // the C:\ drive.
56235641 .rooted => break :target_path target_path_w.span(),
56245642 // Keep relative paths relative, but anything else needs to get NT-prefixed.
5625 else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path_w.span()))
5643 else => if (!Dir.path.isAbsoluteWindowsWtf16(target_path_w.span()))
56265644 break :target_path target_path_w.span(),
56275645 }
56285646 }
56295647 var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span());
56305648 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
5631 is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
5649 is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
56325650 break :target_path prefixed_target_path.span();
56335651 };
56345652
......@@ -5636,8 +5654,8 @@ fn dirSymLinkWindows(
56365654 var buffer: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
56375655 const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
56385656 const header_len = @sizeOf(w.ULONG) + @sizeOf(w.USHORT) * 2;
5639 const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path);
5640 const symlink_data = SYMLINK_DATA{
5657 const target_is_absolute = Dir.path.isAbsoluteWindowsWtf16(final_target_path);
5658 const symlink_data: SYMLINK_DATA = .{
56415659 .ReparseTag = .SYMLINK,
56425660 .ReparseDataLength = @intCast(buf_len - header_len),
56435661 .Reserved = 0,
......@@ -7890,7 +7908,7 @@ fn posixSeekTo(fd: posix.fd_t, offset: u64) File.SeekError!void {
78907908 }
78917909}
78927910
7893fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.process.OpenExecutableError!File {
7911fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.OpenExecutableError!File {
78947912 const t: *Threaded = @ptrCast(@alignCast(userdata));
78957913 switch (native_os) {
78967914 .wasi => return error.OperationUnsupported,
......@@ -7931,7 +7949,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.proce
79317949 }
79327950}
79337951
7934fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.ExecutablePathError!usize {
7952fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.ExecutablePathError!usize {
79357953 const t: *Threaded = @ptrCast(@alignCast(userdata));
79367954
79377955 switch (native_os) {
......@@ -11691,14 +11709,14 @@ fn netLookupFallible(
1169111709fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr {
1169211710 const t: *Threaded = @ptrCast(@alignCast(userdata));
1169311711 // Only global mutex since this is Threaded.
11694 std.process.stderr_thread_mutex.lock();
11712 process.stderr_thread_mutex.lock();
1169511713 return initLockedStderr(t, terminal_mode);
1169611714}
1169711715
1169811716fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr {
1169911717 const t: *Threaded = @ptrCast(@alignCast(userdata));
1170011718 // Only global mutex since this is Threaded.
11701 if (!std.process.stderr_thread_mutex.tryLock()) return null;
11719 if (!process.stderr_thread_mutex.tryLock()) return null;
1170211720 return try initLockedStderr(t, terminal_mode);
1170311721}
1170411722
......@@ -11729,10 +11747,10 @@ fn unlockStderr(userdata: ?*anyopaque) void {
1172911747 };
1173011748 t.stderr_writer.interface.end = 0;
1173111749 t.stderr_writer.interface.buffer = &.{};
11732 std.process.stderr_thread_mutex.unlock();
11750 process.stderr_thread_mutex.unlock();
1173311751}
1173411752
11735fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) std.process.SetCurrentDirError!void {
11753fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirError!void {
1173611754 if (native_os == .wasi) return error.OperationUnsupported;
1173711755 const t: *Threaded = @ptrCast(@alignCast(userdata));
1173811756 _ = t;
......@@ -12690,6 +12708,1640 @@ fn scanEnviron(t: *Threaded) void {
1269012708 }
1269112709}
1269212710
12711fn processReplace(userdata: ?*anyopaque, options: std.process.ReplaceOptions) std.process.ReplaceError {
12712 _ = userdata;
12713 _ = options;
12714 @panic("TODO");
12715}
12716
12717fn processReplacePath(userdata: ?*anyopaque, dir: Dir, options: std.process.ReplaceOptions) std.process.ReplaceError {
12718 _ = userdata;
12719 _ = dir;
12720 _ = options;
12721 @panic("TODO");
12722}
12723
12724fn processSpawnPath(userdata: ?*anyopaque, dir: Dir, options: process.SpawnOptions) process.SpawnError!process.Child {
12725 _ = userdata;
12726 _ = dir;
12727 _ = options;
12728 @panic("TODO");
12729}
12730
12731const processSpawn = switch (native_os) {
12732 .wasi, .ios, .tvos, .visionos, .watchos => processSpawnUnsupported,
12733 .windows => processSpawnWindows,
12734 else => processSpawnPosix,
12735};
12736
12737fn processSpawnUnsupported(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child {
12738 _ = userdata;
12739 _ = options;
12740 return error.OperationUnsupported;
12741}
12742
12743fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child {
12744 const t: *Threaded = @ptrCast(@alignCast(userdata));
12745
12746 // The child process does need to access (one end of) these pipes. However,
12747 // we must initially set CLOEXEC to avoid a race condition. If another thread
12748 // is racing to spawn a different child process, we don't want it to inherit
12749 // these FDs in any scenario; that would mean that, for instance, calls to
12750 // `poll` from the parent would not report the child's stdout as closing when
12751 // expected, since the other child may retain a reference to the write end of
12752 // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we
12753 // need to do something in the new child to make sure we preserve the reference
12754 // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it
12755 // turns out, we `dup2` everything anyway, so there's no need!
12756 const pipe_flags: posix.O = .{ .CLOEXEC = true };
12757
12758 const stdin_pipe = if (options.stdin == .pipe) try posix.pipe2(pipe_flags) else undefined;
12759 errdefer if (options.stdin == .pipe) {
12760 destroyPipe(stdin_pipe);
12761 };
12762
12763 const stdout_pipe = if (options.stdout == .pipe) try posix.pipe2(pipe_flags) else undefined;
12764 errdefer if (options.stdout == .pipe) {
12765 destroyPipe(stdout_pipe);
12766 };
12767
12768 const stderr_pipe = if (options.stderr == .pipe) try posix.pipe2(pipe_flags) else undefined;
12769 errdefer if (options.stderr == .pipe) {
12770 destroyPipe(stderr_pipe);
12771 };
12772
12773 const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore);
12774 const dev_null_fd = if (any_ignore)
12775 posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) {
12776 error.PathAlreadyExists => unreachable,
12777 error.NoSpaceLeft => unreachable,
12778 error.FileTooBig => unreachable,
12779 error.DeviceBusy => unreachable,
12780 error.FileLocksUnsupported => unreachable,
12781 error.BadPathName => unreachable, // Windows-only
12782 error.WouldBlock => unreachable,
12783 error.NetworkNotFound => unreachable, // Windows-only
12784 error.Canceled => unreachable, // temporarily in the posix error set
12785 error.SharingViolation => unreachable, // Windows-only
12786 error.PipeBusy => unreachable, // not a pipe
12787 error.AntivirusInterference => unreachable, // Windows-only
12788 else => |e| return e,
12789 }
12790 else
12791 undefined;
12792 defer {
12793 if (any_ignore) posix.close(dev_null_fd);
12794 }
12795
12796 const prog_pipe: [2]posix.fd_t = p: {
12797 if (options.progress_node.index == .none) {
12798 break :p .{ -1, -1 };
12799 } else {
12800 // We use CLOEXEC for the same reason as in `pipe_flags`.
12801 break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
12802 }
12803 };
12804 errdefer destroyPipe(prog_pipe);
12805
12806 var arena_allocator = std.heap.ArenaAllocator.init(t.allocator);
12807 defer arena_allocator.deinit();
12808 const arena = arena_allocator.allocator();
12809
12810 // The POSIX standard does not allow malloc() between fork() and execve(),
12811 // and this allocator may be a libc allocator.
12812 // I have personally observed the child process deadlocking when it tries
12813 // to call malloc() due to a heap allocation between fork() and execve(),
12814 // in musl v1.1.24.
12815 // Additionally, we want to reduce the number of possible ways things
12816 // can fail between fork() and execve().
12817 // Therefore, we do all the allocation for the execve() before the fork().
12818 // This means we must do the null-termination of argv and env vars here.
12819 const argv_buf = try arena.allocSentinel(?[*:0]const u8, options.argv.len, null);
12820 for (options.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
12821
12822 const prog_fileno = 3;
12823 comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno);
12824
12825 const envp: [*:null]const ?[*:0]const u8 = m: {
12826 const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno;
12827 if (options.env_map) |env_map| {
12828 break :m (try env_map.createBlock(arena, .{
12829 .zig_progress_fd = prog_fd,
12830 })).ptr;
12831 }
12832 break :m (try process.Environ.createBlock(.{ .block = t.environ.block }, arena, .{
12833 .zig_progress_fd = prog_fd,
12834 })).ptr;
12835 };
12836
12837 // This pipe communicates to the parent errors in the child between `fork` and `execvpe`.
12838 // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.
12839 const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true });
12840 errdefer destroyPipe(err_pipe);
12841
12842 t.scanEnviron(); // for PATH
12843 const PATH = t.environ.string.PATH orelse "/usr/local/bin:/bin/:/usr/bin";
12844
12845 const pid_result = try posix.fork();
12846 if (pid_result == 0) {
12847 // we are the child
12848 setUpChildIo(options.stdin, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err);
12849 setUpChildIo(options.stdout, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err);
12850 setUpChildIo(options.stderr, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err);
12851
12852 if (options.cwd_dir) |cwd| {
12853 posix.fchdir(cwd.handle) catch |err| forkBail(err_pipe[1], err);
12854 } else if (options.cwd) |cwd| {
12855 posix.chdir(cwd) catch |err| forkBail(err_pipe[1], err);
12856 }
12857
12858 // Must happen after fchdir above, the cwd file descriptor might be
12859 // equal to prog_fileno and be clobbered by this dup2 call.
12860 if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkBail(err_pipe[1], err);
12861
12862 if (options.gid) |gid| {
12863 posix.setregid(gid, gid) catch |err| forkBail(err_pipe[1], err);
12864 }
12865
12866 if (options.uid) |uid| {
12867 switch (posix.errno(posix.system.setreuid(uid, uid))) {
12868 .SUCCESS => {},
12869 .AGAIN => forkBail(err_pipe[1], error.ResourceLimitReached),
12870 .INVAL => forkBail(err_pipe[1], error.InvalidUserId),
12871 .PERM => forkBail(err_pipe[1], error.PermissionDenied),
12872 else => forkBail(err_pipe[1], error.Unexpected),
12873 }
12874 }
12875
12876 if (options.pgid) |pid| {
12877 switch (posix.errno(posix.system.setpgid(0, pid))) {
12878 .SUCCESS => {},
12879 .ACCES => forkBail(err_pipe[1], error.ProcessAlreadyExec),
12880 .INVAL => forkBail(err_pipe[1], error.InvalidProcessGroupId),
12881 .PERM => forkBail(err_pipe[1], error.PermissionDenied),
12882 else => forkBail(err_pipe[1], error.Unexpected),
12883 }
12884 }
12885
12886 if (options.start_suspended) {
12887 switch (posix.errno(posix.system.kill(posix.system.getpid(), .STOP))) {
12888 .SUCCESS => {},
12889 .PERM => forkBail(err_pipe[1], error.PermissionDenied),
12890 else => forkBail(err_pipe[1], error.Unexpected),
12891 }
12892 }
12893
12894 const err = execvpeZ_expandArg0(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, envp, PATH);
12895 forkBail(err_pipe[1], err);
12896 }
12897
12898 const pid: posix.pid_t = @intCast(pid_result); // We are the parent.
12899
12900 posix.close(err_pipe[1]); // make sure only the child holds the write end open
12901 defer posix.close(err_pipe[0]);
12902
12903 if (options.stdin == .pipe) posix.close(stdin_pipe[0]);
12904 if (options.stdout == .pipe) posix.close(stdout_pipe[1]);
12905 if (options.stderr == .pipe) posix.close(stderr_pipe[1]);
12906
12907 if (prog_pipe[1] != -1) posix.close(prog_pipe[1]);
12908
12909 options.progress_node.setIpcFd(prog_pipe[0]);
12910
12911 // Wait for the child to report any errors in or before `execvpe`.
12912 if (readIntFd(t, err_pipe[0])) |child_err_int| {
12913 const child_err: process.SpawnError = @errorCast(@errorFromInt(child_err_int));
12914 return child_err;
12915 } else |read_err| switch (read_err) {
12916 error.EndOfStream => {
12917 // Write end closed by CLOEXEC at the time of the `execvpe` call,
12918 // indicating success.
12919 },
12920 else => {
12921 // Problem reading the error from the error reporting pipe. We
12922 // don't know if the child is alive or dead. Better to assume it is
12923 // alive so the resource does not risk being leaked.
12924 },
12925 }
12926
12927 return .{
12928 .id = pid,
12929 .stdin = switch (options.stdin) {
12930 .pipe => .{ .handle = stdin_pipe[1] },
12931 else => null,
12932 },
12933 .stdout = switch (options.stdout) {
12934 .pipe => .{ .handle = stdout_pipe[0] },
12935 else => null,
12936 },
12937 .stderr = switch (options.stderr) {
12938 .pipe => .{ .handle = stderr_pipe[0] },
12939 else => null,
12940 },
12941 .request_resource_usage_statistics = options.request_resource_usage_statistics,
12942 };
12943}
12944
12945fn childWait(userdata: ?*anyopaque, child: *std.process.Child) process.Child.WaitError!process.Child.Term {
12946 const t: *Threaded = @ptrCast(@alignCast(userdata));
12947 switch (native_os) {
12948 .windows => return childWaitWindows(t, child),
12949 else => return childWaitPosix(t, child),
12950 }
12951}
12952
12953fn childKill(userdata: ?*anyopaque, child: *std.process.Child) void {
12954 const t: *Threaded = @ptrCast(@alignCast(userdata));
12955 if (is_windows) {
12956 childKillWindows(t, child, 1) catch {
12957 childCleanupStreams(child);
12958 child.id = null;
12959 };
12960 } else {
12961 childKillPosix(t, child) catch {
12962 childCleanupStreams(child);
12963 child.id = null;
12964 };
12965 }
12966}
12967
12968fn childKillWindows(t: *Threaded, child: *process.Child, exit_code: windows.UINT) !void {
12969 windows.TerminateProcess(child.id, exit_code) catch |err| switch (err) {
12970 error.AccessDenied => {
12971 // Usually when TerminateProcess triggers a ACCESS_DENIED error, it
12972 // indicates that the process has already exited, but there may be
12973 // some rare edge cases where our process handle no longer has the
12974 // PROCESS_TERMINATE access right, so let's do another check to make
12975 // sure the process is really no longer running:
12976 windows.WaitForSingleObjectEx(child.id, 0, false) catch return err;
12977 return error.AlreadyTerminated;
12978 },
12979 else => return err,
12980 };
12981 try childWaitWindows(t, child);
12982}
12983
12984fn childWaitWindows(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term {
12985 _ = t; // TODO cancelation
12986 windows.WaitForSingleObjectEx(child.id, windows.INFINITE, false);
12987
12988 const term: process.Child.Term = x: {
12989 var exit_code: windows.DWORD = undefined;
12990 if (windows.kernel32.GetExitCodeProcess(child.id, &exit_code) == 0) {
12991 break :x .{ .unknown = 0 };
12992 } else {
12993 break :x .{ .exited = @as(u8, @truncate(exit_code)) };
12994 }
12995 };
12996
12997 if (child.request_resource_usage_statistics) {
12998 child.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(child.id);
12999 }
13000
13001 posix.close(child.id);
13002 posix.close(child.thread_handle);
13003 childCleanupStreams(child);
13004 child.id = null;
13005 return term;
13006}
13007
13008fn childWaitPosix(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term {
13009 _ = t; // TODO cancelation
13010 const pid = child.id.?;
13011 const res: posix.WaitPidResult = res: {
13012 if (child.request_resource_usage_statistics and have_wait4) {
13013 var ru: posix.rusage = undefined;
13014 const res = posix.wait4(pid, 0, &ru);
13015 child.resource_usage_statistics.rusage = ru;
13016 break :res res;
13017 }
13018 break :res posix.waitpid(pid, 0);
13019 };
13020 const status = res.status;
13021 childCleanupStreams(child);
13022 child.id = null;
13023 return statusToTerm(status);
13024}
13025
13026fn statusToTerm(status: u32) process.Child.Term {
13027 return if (posix.W.IFEXITED(status))
13028 .{ .exited = posix.W.EXITSTATUS(status) }
13029 else if (posix.W.IFSIGNALED(status))
13030 .{ .signal = posix.W.TERMSIG(status) }
13031 else if (posix.W.IFSTOPPED(status))
13032 .{ .stopped = posix.W.STOPSIG(status) }
13033 else
13034 .{ .unknown = status };
13035}
13036
13037fn childKillPosix(t: *Threaded, child: *process.Child) !void {
13038 try posix.kill(child.id.?, posix.SIG.TERM);
13039 _ = try childWaitPosix(t, child);
13040}
13041
13042fn childCleanupStreams(child: *process.Child) void {
13043 if (child.stdin) |*stdin| {
13044 posix.close(stdin.handle);
13045 child.stdin = null;
13046 }
13047 if (child.stdout) |*stdout| {
13048 posix.close(stdout.handle);
13049 child.stdout = null;
13050 }
13051 if (child.stderr) |*stderr| {
13052 posix.close(stderr.handle);
13053 child.stderr = null;
13054 }
13055}
13056
13057/// Errors that can occur between fork() and execv()
13058const ForkBailError = process.SpawnError || process.ReplaceError;
13059
13060/// Child of fork calls this to report an error to the fork parent. Then the
13061/// child exits.
13062fn forkBail(fd: posix.fd_t, err: ForkBailError) noreturn {
13063 writeIntFd(fd, @as(ErrInt, @intFromError(err))) catch {};
13064 // If we're linking libc, some naughty applications may have registered atexit handlers
13065 // which we really do not want to run in the fork child. I caught LLVM doing this and
13066 // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne,
13067 // "Why'd you have to go and make things so complicated?"
13068 if (builtin.link_libc) {
13069 // The _exit(2) function does nothing but make the exit syscall, unlike exit(3)
13070 std.c._exit(1);
13071 }
13072 posix.system.exit(1);
13073}
13074
13075fn writeIntFd(fd: posix.fd_t, value: ErrInt) !void {
13076 var buffer: [8]u8 = undefined;
13077 std.mem.writeInt(u64, &buffer, value, .little);
13078 // Skip the cancel mechanism.
13079 var i: usize = 0;
13080 while (true) {
13081 const rc = posix.system.write(fd, buffer[i..].ptr, buffer.len - i);
13082 switch (posix.errno(rc)) {
13083 .SUCCESS => {
13084 const n: usize = @intCast(rc);
13085 i += n;
13086 if (buffer.len - i == 0) return;
13087 },
13088 .INTR => continue,
13089 else => return error.SystemResources,
13090 }
13091 }
13092}
13093
13094fn readIntFd(t: *Threaded, fd: posix.fd_t) !ErrInt {
13095 _ = t; // TODO cancelation
13096 var buffer: [8]u8 = undefined;
13097 var i: usize = 0;
13098 while (true) {
13099 const rc = posix.system.read(fd, buffer[i..].ptr, buffer.len - i);
13100 switch (posix.errno(rc)) {
13101 .SUCCESS => {
13102 const n: usize = @intCast(rc);
13103 if (n == 0) return error.EndOfStream;
13104 i += n;
13105 continue;
13106 },
13107 .INTR => continue,
13108 else => |err| return posix.unexpectedErrno(err),
13109 }
13110 }
13111 return @intCast(std.mem.readInt(u64, &buffer, .little));
13112}
13113
13114const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8);
13115
13116fn destroyPipe(pipe: [2]posix.fd_t) void {
13117 if (pipe[0] != -1) posix.close(pipe[0]);
13118 if (pipe[0] != pipe[1]) posix.close(pipe[1]);
13119}
13120
13121fn setUpChildIo(stdio: process.SpawnOptions.StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void {
13122 switch (stdio) {
13123 .pipe => try posix.dup2(pipe_fd, std_fileno),
13124 .close => posix.close(std_fileno),
13125 .inherit => {},
13126 .ignore => try posix.dup2(dev_null_fd, std_fileno),
13127 .file => @panic("TODO implement setUpChildIo when file is used"),
13128 }
13129}
13130
13131fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.SpawnError!void {
13132 const t: *Threaded = @ptrCast(@alignCast(userdata));
13133 _ = t;
13134
13135 var saAttr: windows.SECURITY_ATTRIBUTES = .{
13136 .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
13137 .bInheritHandle = windows.TRUE,
13138 .lpSecurityDescriptor = null,
13139 };
13140
13141 const any_ignore =
13142 child.stdin_behavior == .ignore or
13143 child.stdout_behavior == .ignore or
13144 child.stderr_behavior == .ignore;
13145
13146 const nul_handle = if (any_ignore)
13147 // "\Device\Null" or "\??\NUL"
13148 windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{
13149 .access_mask = .{
13150 .STANDARD = .{ .SYNCHRONIZE = true },
13151 .GENERIC = .{ .WRITE = true, .READ = true },
13152 },
13153 .sa = &saAttr,
13154 .creation = .OPEN,
13155 }) catch |err| switch (err) {
13156 error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL"
13157 error.PipeBusy => return error.Unexpected, // not possible for "NUL"
13158 error.NoDevice => return error.Unexpected, // not possible for "NUL"
13159 error.FileNotFound => return error.Unexpected, // not possible for "NUL"
13160 error.AccessDenied => return error.Unexpected, // not possible for "NUL"
13161 error.NameTooLong => return error.Unexpected, // not possible for "NUL"
13162 error.WouldBlock => return error.Unexpected, // not possible for "NUL"
13163 error.NetworkNotFound => return error.Unexpected, // not possible for "NUL"
13164 error.AntivirusInterference => return error.Unexpected, // not possible for "NUL"
13165 error.OperationCanceled => return error.Unexpected, // we're not canceling the operation
13166 else => |e| return e,
13167 }
13168 else
13169 undefined;
13170 defer {
13171 if (any_ignore) posix.close(nul_handle);
13172 }
13173
13174 var g_hChildStd_IN_Rd: ?windows.HANDLE = null;
13175 var g_hChildStd_IN_Wr: ?windows.HANDLE = null;
13176 switch (child.stdin_behavior) {
13177 .pipe => {
13178 try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr);
13179 },
13180 .ignore => {
13181 g_hChildStd_IN_Rd = nul_handle;
13182 },
13183 .inherit => {
13184 g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null;
13185 },
13186 .close => {
13187 g_hChildStd_IN_Rd = null;
13188 },
13189 }
13190 errdefer if (child.stdin_behavior == .pipe) {
13191 windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
13192 };
13193
13194 var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
13195 var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
13196 switch (child.stdout_behavior) {
13197 .pipe => {
13198 try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
13199 },
13200 .ignore => {
13201 g_hChildStd_OUT_Wr = nul_handle;
13202 },
13203 .inherit => {
13204 g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null;
13205 },
13206 .close => {
13207 g_hChildStd_OUT_Wr = null;
13208 },
13209 }
13210 errdefer if (child.stdout_behavior == .pipe) {
13211 windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr);
13212 };
13213
13214 var g_hChildStd_ERR_Rd: ?windows.HANDLE = null;
13215 var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
13216 switch (child.stderr_behavior) {
13217 .pipe => {
13218 try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
13219 },
13220 .ignore => {
13221 g_hChildStd_ERR_Wr = nul_handle;
13222 },
13223 .inherit => {
13224 g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null;
13225 },
13226 .close => {
13227 g_hChildStd_ERR_Wr = null;
13228 },
13229 }
13230 errdefer if (child.stderr_behavior == .pipe) {
13231 windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr);
13232 };
13233
13234 var siStartInfo = windows.STARTUPINFOW{
13235 .cb = @sizeOf(windows.STARTUPINFOW),
13236 .hStdError = g_hChildStd_ERR_Wr,
13237 .hStdOutput = g_hChildStd_OUT_Wr,
13238 .hStdInput = g_hChildStd_IN_Rd,
13239 .dwFlags = windows.STARTF_USESTDHANDLES,
13240
13241 .lpReserved = null,
13242 .lpDesktop = null,
13243 .lpTitle = null,
13244 .dwX = 0,
13245 .dwY = 0,
13246 .dwXSize = 0,
13247 .dwYSize = 0,
13248 .dwXCountChars = 0,
13249 .dwYCountChars = 0,
13250 .dwFillAttribute = 0,
13251 .wShowWindow = 0,
13252 .cbReserved2 = 0,
13253 .lpReserved2 = null,
13254 };
13255 var piProcInfo: windows.PROCESS_INFORMATION = undefined;
13256
13257 const cwd_w = if (child.cwd) |cwd| try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd) else null;
13258 defer if (cwd_w) |cwd| child.allocator.free(cwd);
13259 const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null;
13260
13261 const maybe_envp_buf = if (child.env_map) |env_map| try process.createWindowsEnvBlock(child.allocator, env_map) else null;
13262 defer if (maybe_envp_buf) |envp_buf| child.allocator.free(envp_buf);
13263 const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null;
13264
13265 const app_name_wtf8 = child.argv[0];
13266 const app_name_is_absolute = Dir.path.isAbsolute(app_name_wtf8);
13267
13268 // the cwd set in Child is in effect when choosing the executable path
13269 // to match posix semantics
13270 var cwd_path_w_needs_free = false;
13271 const cwd_path_w = x: {
13272 // If the app name is absolute, then we need to use its dirname as the cwd
13273 if (app_name_is_absolute) {
13274 cwd_path_w_needs_free = true;
13275 const dir = Dir.path.dirname(app_name_wtf8).?;
13276 break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, dir);
13277 } else if (child.cwd) |cwd| {
13278 cwd_path_w_needs_free = true;
13279 break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd);
13280 } else {
13281 break :x &[_:0]u16{}; // empty for cwd
13282 }
13283 };
13284 defer if (cwd_path_w_needs_free) child.allocator.free(cwd_path_w);
13285
13286 // If the app name has more than just a filename, then we need to separate that
13287 // into the basename and dirname and use the dirname as an addition to the cwd
13288 // path. This is because NtQueryDirectoryFile cannot accept FileName params with
13289 // path separators.
13290 const app_basename_wtf8 = Dir.path.basename(app_name_wtf8);
13291 // If the app name is absolute, then the cwd will already have the app's dirname in it,
13292 // so only populate app_dirname if app name is a relative path with > 0 path separators.
13293 const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) Dir.path.dirname(app_name_wtf8) else null;
13294 const app_dirname_w: ?[:0]u16 = x: {
13295 if (maybe_app_dirname_wtf8) |app_dirname_wtf8| {
13296 break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_dirname_wtf8);
13297 }
13298 break :x null;
13299 };
13300 defer if (app_dirname_w != null) child.allocator.free(app_dirname_w.?);
13301
13302 const app_name_w = try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_basename_wtf8);
13303 defer child.allocator.free(app_name_w);
13304
13305 const flags: windows.CreateProcessFlags = .{
13306 .create_suspended = child.start_suspended,
13307 .create_unicode_environment = true,
13308 .create_no_window = child.create_no_window,
13309 };
13310
13311 run: {
13312 const PATH: [:0]const u16 = process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{};
13313 const PATHEXT: [:0]const u16 = process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{};
13314
13315 // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules
13316 // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously
13317 // constructed arguments.
13318 //
13319 // We'll need to wait until we're actually trying to run the command to know for sure
13320 // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually
13321 // serializing the command line until we determine how it should be serialized.
13322 var cmd_line_cache = WindowsCommandLineCache.init(child.allocator, child.argv);
13323 defer cmd_line_cache.deinit();
13324
13325 var app_buf: std.ArrayList(u16) = .empty;
13326 defer app_buf.deinit(child.allocator);
13327
13328 try app_buf.appendSlice(child.allocator, app_name_w);
13329
13330 var dir_buf: std.ArrayList(u16) = .empty;
13331 defer dir_buf.deinit(child.allocator);
13332
13333 if (cwd_path_w.len > 0) {
13334 try dir_buf.appendSlice(child.allocator, cwd_path_w);
13335 }
13336 if (app_dirname_w) |app_dir| {
13337 if (dir_buf.items.len > 0) try dir_buf.append(child.allocator, Dir.path.sep);
13338 try dir_buf.appendSlice(child.allocator, app_dir);
13339 }
13340
13341 windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo) catch |no_path_err| {
13342 const original_err = switch (no_path_err) {
13343 // argv[0] contains unsupported characters that will never resolve to a valid exe.
13344 error.InvalidArg0 => return error.FileNotFound,
13345 error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e,
13346 error.UnrecoverableInvalidExe => return error.InvalidExe,
13347 else => |e| return e,
13348 };
13349
13350 // If the app name had path separators, that disallows PATH searching,
13351 // and there's no need to search the PATH if the app name is absolute.
13352 // We still search the path if the cwd is absolute because of the
13353 // "cwd set in Child is in effect when choosing the executable path
13354 // to match posix semantics" behavior--we don't want to skip searching
13355 // the PATH just because we were trying to set the cwd of the child process.
13356 if (app_dirname_w != null or app_name_is_absolute) {
13357 return original_err;
13358 }
13359
13360 var it = std.mem.tokenizeScalar(u16, PATH, ';');
13361 while (it.next()) |search_path| {
13362 dir_buf.clearRetainingCapacity();
13363 try dir_buf.appendSlice(child.allocator, search_path);
13364
13365 if (windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo)) {
13366 break :run;
13367 } else |err| switch (err) {
13368 // argv[0] contains unsupported characters that will never resolve to a valid exe.
13369 error.InvalidArg0 => return error.FileNotFound,
13370 error.FileNotFound, error.AccessDenied, error.InvalidExe => continue,
13371 error.UnrecoverableInvalidExe => return error.InvalidExe,
13372 else => |e| return e,
13373 }
13374 } else {
13375 return original_err;
13376 }
13377 };
13378 }
13379
13380 if (g_hChildStd_IN_Wr) |h| {
13381 child.stdin = File{ .handle = h };
13382 } else {
13383 child.stdin = null;
13384 }
13385 if (g_hChildStd_OUT_Rd) |h| {
13386 child.stdout = File{ .handle = h };
13387 } else {
13388 child.stdout = null;
13389 }
13390 if (g_hChildStd_ERR_Rd) |h| {
13391 child.stderr = File{ .handle = h };
13392 } else {
13393 child.stderr = null;
13394 }
13395
13396 child.id = piProcInfo.hProcess;
13397 child.thread_handle = piProcInfo.hThread;
13398 child.term = null;
13399
13400 if (child.stdin_behavior == .pipe) {
13401 posix.close(g_hChildStd_IN_Rd.?);
13402 }
13403 if (child.stderr_behavior == .pipe) {
13404 posix.close(g_hChildStd_ERR_Wr.?);
13405 }
13406 if (child.stdout_behavior == .pipe) {
13407 posix.close(g_hChildStd_OUT_Wr.?);
13408 }
13409}
13410
13411/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
13412/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
13413/// Note: `app_buf` should not contain any leading path separators.
13414/// Note: If the dir is the cwd, dir_buf should be empty (len = 0).
13415fn windowsCreateProcessPathExt(
13416 allocator: Allocator,
13417 dir_buf: *std.ArrayList(u16),
13418 app_buf: *std.ArrayList(u16),
13419 pathext: [:0]const u16,
13420 cmd_line_cache: *WindowsCommandLineCache,
13421 envp_ptr: ?[*]u16,
13422 cwd_ptr: ?[*:0]u16,
13423 flags: windows.CreateProcessFlags,
13424 lpStartupInfo: *windows.STARTUPINFOW,
13425 lpProcessInformation: *windows.PROCESS_INFORMATION,
13426) !void {
13427 const app_name_len = app_buf.items.len;
13428 const dir_path_len = dir_buf.items.len;
13429
13430 if (app_name_len == 0) return error.FileNotFound;
13431
13432 defer app_buf.shrinkRetainingCapacity(app_name_len);
13433 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
13434
13435 // The name of the game here is to avoid CreateProcessW calls at all costs,
13436 // and only ever try calling it when we have a real candidate for execution.
13437 // Secondarily, we want to minimize the number of syscalls used when checking
13438 // for each PATHEXT-appended version of the app name.
13439 //
13440 // An overview of the technique used:
13441 // - Open the search directory for iteration (either cwd or a path from PATH)
13442 // - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to
13443 // check if anything that could possibly match either the unappended version
13444 // of the app name or any of the versions with a PATHEXT value appended exists.
13445 // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early
13446 // without needing to use PATHEXT at all.
13447 //
13448 // This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence
13449 // for any directory that doesn't contain any possible matches, instead of having
13450 // to use a separate look up for each individual filename combination (unappended +
13451 // each PATHEXT appended). For directories where the wildcard *does* match something,
13452 // we iterate the matches and take note of any that are either the unappended version,
13453 // or a version with a supported PATHEXT appended. We then try calling CreateProcessW
13454 // with the found versions in the appropriate order.
13455
13456 // In the future, child process execution needs to move to Io implementation.
13457 // Under those conditions, here we will have access to lower level directory
13458 // opening function knowing which implementation we are in. Here, we imitate
13459 // that scenario.
13460 var dir = dir: {
13461 // needs to be null-terminated
13462 try dir_buf.append(allocator, 0);
13463 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
13464 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
13465 const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);
13466 break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{
13467 .iterate = true,
13468 }) catch return error.FileNotFound;
13469 };
13470 defer windows.CloseHandle(dir.handle);
13471
13472 // Add wildcard and null-terminator
13473 try app_buf.append(allocator, '*');
13474 try app_buf.append(allocator, 0);
13475 const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0];
13476
13477 // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries
13478 // returned per NtQueryDirectoryFile call.
13479 var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined;
13480 const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2);
13481 if (file_information_buf.len < file_info_maximum_single_entry_size) {
13482 @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry");
13483 }
13484 var io_status: windows.IO_STATUS_BLOCK = undefined;
13485
13486 const num_supported_pathext = @typeInfo(process.WindowsExtension).@"enum".fields.len;
13487 var pathext_seen = [_]bool{false} ** num_supported_pathext;
13488 var any_pathext_seen = false;
13489 var unappended_exists = false;
13490
13491 // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions
13492 // of the app_name we should try to spawn.
13493 // Note: This is necessary because the order of the files returned is filesystem-dependent:
13494 // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists.
13495 // On FAT32, it's possible for something like `blah.exe.obj` to be returned first.
13496 while (true) {
13497 const app_name_len_bytes = std.math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong;
13498 var app_name_unicode_string = windows.UNICODE_STRING{
13499 .Length = app_name_len_bytes,
13500 .MaximumLength = app_name_len_bytes,
13501 .Buffer = @constCast(app_name_wildcard.ptr),
13502 };
13503 const rc = windows.ntdll.NtQueryDirectoryFile(
13504 dir.handle,
13505 null,
13506 null,
13507 null,
13508 &io_status,
13509 &file_information_buf,
13510 file_information_buf.len,
13511 .Directory,
13512 windows.FALSE, // single result
13513 &app_name_unicode_string,
13514 windows.FALSE, // restart iteration
13515 );
13516
13517 // If we get nothing with the wildcard, then we can just bail out
13518 // as we know appending PATHEXT will not yield anything.
13519 switch (rc) {
13520 .SUCCESS => {},
13521 .NO_SUCH_FILE => return error.FileNotFound,
13522 .NO_MORE_FILES => break,
13523 .ACCESS_DENIED => return error.AccessDenied,
13524 else => return windows.unexpectedStatus(rc),
13525 }
13526
13527 // According to the docs, this can only happen if there is not enough room in the
13528 // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry.
13529 // Therefore, this condition should not be possible to hit with the buffer size we use.
13530 std.debug.assert(io_status.Information != 0);
13531
13532 var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf };
13533 while (it.next()) |info| {
13534 // Skip directories
13535 if (info.FileAttributes.DIRECTORY) continue;
13536 const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2];
13537 // Because all results start with the app_name since we're using the wildcard `app_name*`,
13538 // if the length is equal to app_name then this is an exact match
13539 if (filename.len == app_name_len) {
13540 // Note: We can't break early here because it's possible that the unappended version
13541 // fails to spawn, in which case we still want to try the PATHEXT appended versions.
13542 unappended_exists = true;
13543 } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| {
13544 pathext_seen[@intFromEnum(pathext_ext)] = true;
13545 any_pathext_seen = true;
13546 }
13547 }
13548 }
13549
13550 const unappended_err = unappended: {
13551 if (unappended_exists) {
13552 if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
13553 '/', '\\' => {},
13554 else => try dir_buf.append(allocator, Dir.path.sep),
13555 };
13556 try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
13557 try dir_buf.append(allocator, 0);
13558 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
13559
13560 const is_bat_or_cmd = bat_or_cmd: {
13561 const app_name = app_buf.items[0..app_name_len];
13562 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false;
13563 const ext = app_name[ext_start..];
13564 const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false;
13565 switch (ext_enum) {
13566 .cmd, .bat => break :bat_or_cmd true,
13567 else => break :bat_or_cmd false,
13568 }
13569 };
13570 const cmd_line_w = if (is_bat_or_cmd)
13571 try cmd_line_cache.scriptCommandLine(full_app_name)
13572 else
13573 try cmd_line_cache.commandLine();
13574 const app_name_w = if (is_bat_or_cmd)
13575 try cmd_line_cache.cmdExePath()
13576 else
13577 full_app_name;
13578
13579 if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
13580 return;
13581 } else |err| switch (err) {
13582 error.FileNotFound,
13583 error.AccessDenied,
13584 => break :unappended err,
13585 error.InvalidExe => {
13586 // On InvalidExe, if the extension of the app name is .exe then
13587 // it's treated as an unrecoverable error. Otherwise, it'll be
13588 // skipped as normal.
13589 const app_name = app_buf.items[0..app_name_len];
13590 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
13591 const ext = app_name[ext_start..];
13592 if (windows.eqlIgnoreCaseWtf16(ext, std.unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
13593 return error.UnrecoverableInvalidExe;
13594 }
13595 break :unappended err;
13596 },
13597 else => return err,
13598 }
13599 }
13600 break :unappended error.FileNotFound;
13601 };
13602
13603 if (!any_pathext_seen) return unappended_err;
13604
13605 // Now try any PATHEXT appended versions that we've seen
13606 var ext_it = std.mem.tokenizeScalar(u16, pathext, ';');
13607 while (ext_it.next()) |ext| {
13608 const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue;
13609 if (!pathext_seen[@intFromEnum(ext_enum)]) continue;
13610
13611 dir_buf.shrinkRetainingCapacity(dir_path_len);
13612 if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
13613 '/', '\\' => {},
13614 else => try dir_buf.append(allocator, Dir.path.sep),
13615 };
13616 try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
13617 try dir_buf.appendSlice(allocator, ext);
13618 try dir_buf.append(allocator, 0);
13619 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
13620
13621 const is_bat_or_cmd = switch (ext_enum) {
13622 .cmd, .bat => true,
13623 else => false,
13624 };
13625 const cmd_line_w = if (is_bat_or_cmd)
13626 try cmd_line_cache.scriptCommandLine(full_app_name)
13627 else
13628 try cmd_line_cache.commandLine();
13629 const app_name_w = if (is_bat_or_cmd)
13630 try cmd_line_cache.cmdExePath()
13631 else
13632 full_app_name;
13633
13634 if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
13635 return;
13636 } else |err| switch (err) {
13637 error.FileNotFound => continue,
13638 error.AccessDenied => continue,
13639 error.InvalidExe => {
13640 // On InvalidExe, if the extension of the app name is .exe then
13641 // it's treated as an unrecoverable error. Otherwise, it'll be
13642 // skipped as normal.
13643 if (windows.eqlIgnoreCaseWtf16(ext, std.unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
13644 return error.UnrecoverableInvalidExe;
13645 }
13646 continue;
13647 },
13648 else => return err,
13649 }
13650 }
13651
13652 return unappended_err;
13653}
13654
13655fn windowsCreateProcess(
13656 app_name: [*:0]u16,
13657 cmd_line: [*:0]u16,
13658 envp_ptr: ?[*]u16,
13659 cwd_ptr: ?[*:0]u16,
13660 flags: windows.CreateProcessFlags,
13661 lpStartupInfo: *windows.STARTUPINFOW,
13662 lpProcessInformation: *windows.PROCESS_INFORMATION,
13663) !void {
13664 // TODO the docs for environment pointer say:
13665 // > A pointer to the environment block for the new process. If this parameter
13666 // > is NULL, the new process uses the environment of the calling process.
13667 // > ...
13668 // > An environment block can contain either Unicode or ANSI characters. If
13669 // > the environment block pointed to by lpEnvironment contains Unicode
13670 // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
13671 // > If this parameter is NULL and the environment block of the parent process
13672 // > contains Unicode characters, you must also ensure that dwCreationFlags
13673 // > includes CREATE_UNICODE_ENVIRONMENT.
13674 // This seems to imply that we have to somehow know whether our process parent passed
13675 // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
13676 // Since we do not know this information that would imply that we must not pass NULL
13677 // for the parameter.
13678 // However this would imply that programs compiled with -DUNICODE could not pass
13679 // environment variables to programs that were not, which seems unlikely.
13680 // More investigation is needed.
13681 return windows.CreateProcessW(
13682 app_name,
13683 cmd_line,
13684 null,
13685 null,
13686 windows.TRUE,
13687 flags,
13688 @as(?*anyopaque, @ptrCast(envp_ptr)),
13689 cwd_ptr,
13690 lpStartupInfo,
13691 lpProcessInformation,
13692 );
13693}
13694
13695/// Case-insensitive WTF-16 lookup
13696fn windowsCreateProcessSupportsExtension(ext: []const u16) ?process.WindowsExtension {
13697 comptime {
13698 // Ensures keeping this function in sync with the enum.
13699 const fields = @typeInfo(process.WindowsExtension).@"enum".fields;
13700 assert(fields.len == 4);
13701 assert(@intFromEnum(process.WindowsExtension.bat) == 0);
13702 assert(@intFromEnum(process.WindowsExtension.cmd) == 1);
13703 assert(@intFromEnum(process.WindowsExtension.com) == 2);
13704 assert(@intFromEnum(process.WindowsExtension.exe) == 3);
13705 }
13706
13707 if (ext.len != 4) return null;
13708 const State = enum {
13709 start,
13710 dot,
13711 b,
13712 ba,
13713 c,
13714 cm,
13715 co,
13716 e,
13717 ex,
13718 };
13719 var state: State = .start;
13720 for (ext) |c| switch (state) {
13721 .start => switch (c) {
13722 '.' => state = .dot,
13723 else => return null,
13724 },
13725 .dot => switch (c) {
13726 'b', 'B' => state = .b,
13727 'c', 'C' => state = .c,
13728 'e', 'E' => state = .e,
13729 else => return null,
13730 },
13731 .b => switch (c) {
13732 'a', 'A' => state = .ba,
13733 else => return null,
13734 },
13735 .c => switch (c) {
13736 'm', 'M' => state = .cm,
13737 'o', 'O' => state = .co,
13738 else => return null,
13739 },
13740 .e => switch (c) {
13741 'x', 'X' => state = .ex,
13742 else => return null,
13743 },
13744 .ba => switch (c) {
13745 't', 'T' => return .bat,
13746 else => return null,
13747 },
13748 .cm => switch (c) {
13749 'd', 'D' => return .cmd,
13750 else => return null,
13751 },
13752 .co => switch (c) {
13753 'm', 'M' => return .com,
13754 else => return null,
13755 },
13756 .ex => switch (c) {
13757 'e', 'E' => return .exe,
13758 else => return null,
13759 },
13760 };
13761 return null;
13762}
13763
13764test windowsCreateProcessSupportsExtension {
13765 try std.testing.expectEqual(process.WindowsExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?);
13766 try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null);
13767}
13768
13769/// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW.
13770///
13771/// Serialization is done on-demand and the result is cached in order to allow for:
13772/// - Only serializing the particular type of command line needed (`.bat`/`.cmd`
13773/// command line serialization is different from `.exe`/etc)
13774/// - Reusing the serialized command lines if necessary (i.e. if the execution
13775/// of a command fails and the PATH is going to be continued to be searched
13776/// for more candidates)
13777const WindowsCommandLineCache = struct {
13778 cmd_line: ?[:0]u16 = null,
13779 script_cmd_line: ?[:0]u16 = null,
13780 cmd_exe_path: ?[:0]u16 = null,
13781 argv: []const []const u8,
13782 allocator: Allocator,
13783
13784 fn init(allocator: Allocator, argv: []const []const u8) WindowsCommandLineCache {
13785 return .{
13786 .allocator = allocator,
13787 .argv = argv,
13788 };
13789 }
13790
13791 fn deinit(self: *WindowsCommandLineCache) void {
13792 if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line);
13793 if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line);
13794 if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path);
13795 }
13796
13797 fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 {
13798 if (self.cmd_line == null) {
13799 self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv);
13800 }
13801 return self.cmd_line.?;
13802 }
13803
13804 /// Not cached, since the path to the batch script will change during PATH searching.
13805 /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched,
13806 /// then script_path should include both the search path and the script filename
13807 /// (this allows avoiding cmd.exe having to search the PATH again).
13808 fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 {
13809 if (self.script_cmd_line) |v| self.allocator.free(v);
13810 self.script_cmd_line = try argvToScriptCommandLineWindows(
13811 self.allocator,
13812 script_path,
13813 self.argv[1..],
13814 );
13815 return self.script_cmd_line.?;
13816 }
13817
13818 fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 {
13819 if (self.cmd_exe_path == null) {
13820 self.cmd_exe_path = try windowsCmdExePath(self.allocator);
13821 }
13822 return self.cmd_exe_path.?;
13823 }
13824};
13825
13826/// Returns the absolute path of `cmd.exe` within the Windows system directory.
13827/// The caller owns the returned slice.
13828fn windowsCmdExePath(allocator: Allocator) error{ OutOfMemory, Unexpected }![:0]u16 {
13829 var buf = try std.ArrayList(u16).initCapacity(allocator, 128);
13830 errdefer buf.deinit(allocator);
13831 while (true) {
13832 const unused_slice = buf.unusedCapacitySlice();
13833 // TODO: Get the system directory from PEB.ReadOnlyStaticServerData
13834 const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len));
13835 if (len == 0) {
13836 switch (windows.GetLastError()) {
13837 else => |err| return windows.unexpectedError(err),
13838 }
13839 }
13840 if (len > unused_slice.len) {
13841 try buf.ensureUnusedCapacity(allocator, len);
13842 } else {
13843 buf.items.len = len;
13844 break;
13845 }
13846 }
13847 switch (buf.items[buf.items.len - 1]) {
13848 '/', '\\' => {},
13849 else => try buf.append(allocator, Dir.path.sep),
13850 }
13851 try buf.appendSlice(allocator, std.unicode.utf8ToUtf16LeStringLiteral("cmd.exe"));
13852 return try buf.toOwnedSliceSentinel(allocator, 0);
13853}
13854
13855const ArgvToScriptCommandLineError = error{
13856 OutOfMemory,
13857 InvalidWtf8,
13858 /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
13859 /// within arguments when executing a `.bat`/`.cmd` script.
13860 /// - NUL/LF signifiies end of arguments, so anything afterwards
13861 /// would be lost after execution.
13862 /// - CR is stripped by `cmd.exe`, so any CR codepoints
13863 /// would be lost after execution.
13864 InvalidBatchScriptArg,
13865};
13866
13867/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific
13868/// escaping rules. The caller owns the returned slice.
13869///
13870/// Escapes `argv` using the suggested mitigation against arbitrary command execution from:
13871/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
13872///
13873/// The return of this function will look like
13874/// `cmd.exe /d /e:ON /v:OFF /c "<escaped command line>"`
13875/// and should be used as the `lpCommandLine` of `CreateProcessW`, while the
13876/// return of `windowsCmdExePath` should be used as `lpApplicationName`.
13877///
13878/// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise.
13879/// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem.
13880fn argvToScriptCommandLineWindows(
13881 allocator: Allocator,
13882 /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD.
13883 /// The script must have been verified to exist at this path before calling this function.
13884 script_path: []const u16,
13885 /// Arguments, not including the script name itself. Expected to be encoded as WTF-8.
13886 script_args: []const []const u8,
13887) ArgvToScriptCommandLineError![:0]u16 {
13888 var buf = try std.array_list.Managed(u8).initCapacity(allocator, 64);
13889 defer buf.deinit();
13890
13891 // `/d` disables execution of AutoRun commands.
13892 // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation:
13893 // > If delayed expansion is enabled via the registry value DelayedExpansion,
13894 // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option.
13895 // > Escaping for % requires the command extension to be enabled.
13896 // > If it’s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option.
13897 // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
13898 buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \"");
13899
13900 // Always quote the path to the script arg
13901 buf.appendAssumeCapacity('"');
13902 // We always want the path to the batch script to include a path separator in order to
13903 // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary
13904 // command execution mitigation, we just know exactly what script we want to execute
13905 // at this point, and potentially making cmd.exe re-find it is unnecessary.
13906 //
13907 // If the script path does not have a path separator, then we know its relative to CWD and
13908 // we can just put `.\` in the front.
13909 if (std.mem.findAny(u16, script_path, &[_]u16{
13910 std.mem.nativeToLittle(u16, '\\'), std.mem.nativeToLittle(u16, '/'),
13911 }) == null) {
13912 try buf.appendSlice(".\\");
13913 }
13914 // Note that we don't do any escaping/mitigations for this argument, since the relevant
13915 // characters (", %, etc) are illegal in file paths and this function should only be called
13916 // with script paths that have been verified to exist.
13917 try std.unicode.wtf16LeToWtf8ArrayList(&buf, script_path);
13918 buf.appendAssumeCapacity('"');
13919
13920 for (script_args) |arg| {
13921 // Literal carriage returns get stripped when run through cmd.exe
13922 // and NUL/newlines act as 'end of command.' Because of this, it's basically
13923 // always a mistake to include these characters in argv, so it's
13924 // an error condition in order to ensure that the return of this
13925 // function can always roundtrip through cmd.exe.
13926 if (std.mem.findAny(u8, arg, "\x00\r\n") != null) {
13927 return error.InvalidBatchScriptArg;
13928 }
13929
13930 // Separate args with a space.
13931 try buf.append(' ');
13932
13933 // Need to quote if the argument is empty (otherwise the arg would just be lost)
13934 // or if the last character is a `\`, since then something like "%~2" in a .bat
13935 // script would cause the closing " to be escaped which we don't want.
13936 var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\';
13937 if (!needs_quotes) {
13938 for (arg) |c| {
13939 switch (c) {
13940 // Known good characters that don't need to be quoted
13941 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {},
13942 // When in doubt, quote
13943 else => {
13944 needs_quotes = true;
13945 break;
13946 },
13947 }
13948 }
13949 }
13950 if (needs_quotes) {
13951 try buf.append('"');
13952 }
13953 var backslashes: usize = 0;
13954 for (arg) |c| {
13955 switch (c) {
13956 '\\' => {
13957 backslashes += 1;
13958 },
13959 '"' => {
13960 try buf.appendNTimes('\\', backslashes);
13961 try buf.append('"');
13962 backslashes = 0;
13963 },
13964 // Replace `%` with `%%cd:~,%`.
13965 //
13966 // cmd.exe allows extracting a substring from an environment
13967 // variable with the syntax: `%foo:~<start_index>,<end_index>%`.
13968 // Therefore, `%cd:~,%` will always expand to an empty string
13969 // since both the start and end index are blank, and it is assumed
13970 // that `%cd%` is always available since it is a built-in variable
13971 // that corresponds to the current directory.
13972 //
13973 // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%`
13974 // will stop `%foo%` from being expanded and *after* expansion
13975 // we'll still be left with `%foo%` (the literal string).
13976 '%' => {
13977 // the trailing `%` is appended outside the switch
13978 try buf.appendSlice("%%cd:~,");
13979 backslashes = 0;
13980 },
13981 else => {
13982 backslashes = 0;
13983 },
13984 }
13985 try buf.append(c);
13986 }
13987 if (needs_quotes) {
13988 try buf.appendNTimes('\\', backslashes);
13989 try buf.append('"');
13990 }
13991 }
13992
13993 try buf.append('"');
13994
13995 return try std.unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
13996}
13997
13998const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 };
13999
14000/// Serializes `argv` to a Windows command-line string suitable for passing to a child process and
14001/// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice.
14002///
14003/// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts.
14004/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
14005///
14006/// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead.
14007fn argvToCommandLineWindows(
14008 allocator: Allocator,
14009 argv: []const []const u8,
14010) ArgvToCommandLineError![:0]u16 {
14011 var buf = std.array_list.Managed(u8).init(allocator);
14012 defer buf.deinit();
14013
14014 if (argv.len != 0) {
14015 const arg0 = argv[0];
14016
14017 // The first argument must be quoted if it contains spaces or ASCII control characters
14018 // (excluding DEL). It also follows special quoting rules where backslashes have no special
14019 // interpretation, which makes it impossible to pass certain first arguments containing
14020 // double quotes to a child process without characters from the first argument leaking into
14021 // subsequent ones (which could have security implications).
14022 //
14023 // Empty arguments technically don't need quotes, but we quote them anyway for maximum
14024 // compatibility with different implementations of the 'CommandLineToArgvW' algorithm.
14025 //
14026 // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject
14027 // all first arguments containing double quotes, even ones that we could theoretically
14028 // serialize in unquoted form.
14029 var needs_quotes = arg0.len == 0;
14030 for (arg0) |c| {
14031 if (c <= ' ') {
14032 needs_quotes = true;
14033 } else if (c == '"') {
14034 return error.InvalidArg0;
14035 }
14036 }
14037 if (needs_quotes) {
14038 try buf.append('"');
14039 try buf.appendSlice(arg0);
14040 try buf.append('"');
14041 } else {
14042 try buf.appendSlice(arg0);
14043 }
14044
14045 for (argv[1..]) |arg| {
14046 try buf.append(' ');
14047
14048 // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes,
14049 // or if they are empty. For simplicity and for maximum compatibility with different
14050 // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII
14051 // control characters (again, excluding DEL).
14052 needs_quotes = for (arg) |c| {
14053 if (c <= ' ' or c == '"') {
14054 break true;
14055 }
14056 } else arg.len == 0;
14057 if (!needs_quotes) {
14058 try buf.appendSlice(arg);
14059 continue;
14060 }
14061
14062 try buf.append('"');
14063 var backslash_count: usize = 0;
14064 for (arg) |byte| {
14065 switch (byte) {
14066 '\\' => {
14067 backslash_count += 1;
14068 },
14069 '"' => {
14070 try buf.appendNTimes('\\', backslash_count * 2 + 1);
14071 try buf.append('"');
14072 backslash_count = 0;
14073 },
14074 else => {
14075 try buf.appendNTimes('\\', backslash_count);
14076 try buf.append(byte);
14077 backslash_count = 0;
14078 },
14079 }
14080 }
14081 try buf.appendNTimes('\\', backslash_count * 2);
14082 try buf.append('"');
14083 }
14084 }
14085
14086 return try std.unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
14087}
14088
14089test argvToCommandLineWindows {
14090 const t = testArgvToCommandLineWindows;
14091
14092 try t(&.{
14093 \\C:\Program Files\zig\zig.exe
14094 ,
14095 \\run
14096 ,
14097 \\.\src\main.zig
14098 ,
14099 \\-target
14100 ,
14101 \\x86_64-windows-gnu
14102 ,
14103 \\-O
14104 ,
14105 \\ReleaseSafe
14106 ,
14107 \\--
14108 ,
14109 \\--emoji=🗿
14110 ,
14111 \\--eval=new Regex("Dwayne \"The Rock\" Johnson")
14112 ,
14113 },
14114 \\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=🗿 "--eval=new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")"
14115 );
14116
14117 try t(&.{}, "");
14118 try t(&.{""}, "\"\"");
14119 try t(&.{" "}, "\" \"");
14120 try t(&.{"\t"}, "\"\t\"");
14121 try t(&.{"\x07"}, "\"\x07\"");
14122 try t(&.{"🦎"}, "🦎");
14123
14124 try t(
14125 &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" },
14126 "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee",
14127 );
14128
14129 try t(
14130 &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" },
14131 "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"",
14132 );
14133
14134 try std.testing.expectError(
14135 error.InvalidArg0,
14136 argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}),
14137 );
14138 try std.testing.expectError(
14139 error.InvalidArg0,
14140 argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}),
14141 );
14142 try std.testing.expectError(
14143 error.InvalidArg0,
14144 argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}),
14145 );
14146}
14147
14148fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void {
14149 const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv);
14150 defer std.testing.allocator.free(cmd_line_w);
14151
14152 const cmd_line = try std.unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w);
14153 defer std.testing.allocator.free(cmd_line);
14154
14155 try std.testing.expectEqualStrings(expected_cmd_line, cmd_line);
14156}
14157
14158/// Replaces the current process image with the executed process. If this
14159/// function succeeds, it does not return.
14160///
14161/// This operation is not available on all targets. `can_execv`
14162///
14163/// This function also uses the PATH environment variable to get the full path to the executable.
14164/// If `file` is an absolute path, this is the same as `execveZ`.
14165///
14166/// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable,
14167/// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall.
14168/// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in.
14169fn execvpeZ_expandArg0(
14170 arg0_expand: process.ArgExpansion,
14171 file: [*:0]const u8,
14172 child_argv: [*:null]?[*:0]const u8,
14173 envp: [*:null]const ?[*:0]const u8,
14174 PATH: []const u8,
14175) process.ReplaceError {
14176 const file_slice = std.mem.sliceTo(file, 0);
14177 if (std.mem.findScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp);
14178
14179 // Use of PATH_MAX here is valid as the path_buf will be passed
14180 // directly to the operating system in execveZ.
14181 var path_buf: [posix.PATH_MAX]u8 = undefined;
14182 var it = std.mem.tokenizeScalar(u8, PATH, ':');
14183 var seen_eacces = false;
14184 var err: process.ReplaceError = error.FileNotFound;
14185
14186 // In case of expanding arg0 we must put it back if we return with an error.
14187 const prev_arg0 = child_argv[0];
14188 defer switch (arg0_expand) {
14189 .expand => child_argv[0] = prev_arg0,
14190 .no_expand => {},
14191 };
14192
14193 while (it.next()) |search_path| {
14194 const path_len = search_path.len + file_slice.len + 1;
14195 if (path_buf.len < path_len + 1) return error.NameTooLong;
14196 @memcpy(path_buf[0..search_path.len], search_path);
14197 path_buf[search_path.len] = '/';
14198 @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice);
14199 path_buf[path_len] = 0;
14200 const full_path = path_buf[0..path_len :0].ptr;
14201 switch (arg0_expand) {
14202 .expand => child_argv[0] = full_path,
14203 .no_expand => {},
14204 }
14205 err = execveZ(full_path, child_argv, envp);
14206 switch (err) {
14207 error.AccessDenied => seen_eacces = true,
14208 error.FileNotFound, error.NotDir => {},
14209 else => |e| return e,
14210 }
14211 }
14212 if (seen_eacces) return error.AccessDenied;
14213 return err;
14214}
14215
14216/// This function ignores PATH environment variable. See `execvpeZ` for that.
14217pub fn execveZ(
14218 path: [*:0]const u8,
14219 child_argv: [*:null]const ?[*:0]const u8,
14220 envp: [*:null]const ?[*:0]const u8,
14221) process.ReplaceError {
14222 switch (posix.errno(posix.system.execve(path, child_argv, envp))) {
14223 .SUCCESS => unreachable,
14224 .FAULT => |err| return errnoBug(err), // Bad pointer parameter.
14225 .@"2BIG" => return error.SystemResources,
14226 .MFILE => return error.ProcessFdQuotaExceeded,
14227 .NAMETOOLONG => return error.NameTooLong,
14228 .NFILE => return error.SystemFdQuotaExceeded,
14229 .NOMEM => return error.SystemResources,
14230 .ACCES => return error.AccessDenied,
14231 .PERM => return error.PermissionDenied,
14232 .INVAL => return error.InvalidExe,
14233 .NOEXEC => return error.InvalidExe,
14234 .IO => return error.FileSystem,
14235 .LOOP => return error.FileSystem,
14236 .ISDIR => return error.IsDir,
14237 .NOENT => return error.FileNotFound,
14238 .NOTDIR => return error.NotDir,
14239 .TXTBSY => return error.FileBusy,
14240 else => |err| switch (native_os) {
14241 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => switch (err) {
14242 .BADEXEC => return error.InvalidExe,
14243 .BADARCH => return error.InvalidExe,
14244 else => return posix.unexpectedErrno(err),
14245 },
14246 .linux => switch (err) {
14247 .LIBBAD => return error.InvalidExe,
14248 else => return posix.unexpectedErrno(err),
14249 },
14250 else => return posix.unexpectedErrno(err),
14251 },
14252 }
14253}
14254
14255/// This function also uses the PATH environment variable to get the full path to the executable.
14256/// If `file` is an absolute path, this is the same as `execveZ`.
14257pub fn execvpeZ(
14258 file: [*:0]const u8,
14259 argv_ptr: [*:null]const ?[*:0]const u8,
14260 envp: [*:null]const ?[*:0]const u8,
14261 optional_PATH: ?[]const u8,
14262) process.ReplaceError {
14263 return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp, optional_PATH);
14264}
14265
14266fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
14267 var rd_h: windows.HANDLE = undefined;
14268 var wr_h: windows.HANDLE = undefined;
14269 try windows.CreatePipe(&rd_h, &wr_h, sattr);
14270 errdefer windowsDestroyPipe(rd_h, wr_h);
14271 try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0);
14272 rd.* = rd_h;
14273 wr.* = wr_h;
14274}
14275
14276fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void {
14277 if (rd) |h| posix.close(h);
14278 if (wr) |h| posix.close(h);
14279}
14280
14281fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
14282 var tmp_bufw: [128]u16 = undefined;
14283
14284 // Anonymous pipes are built upon Named pipes.
14285 // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe
14286 // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes.
14287 // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations
14288 const pipe_path = blk: {
14289 var tmp_buf: [128]u8 = undefined;
14290 // Forge a random path for the pipe.
14291 const pipe_path = std.fmt.bufPrintSentinel(
14292 &tmp_buf,
14293 "\\\\.\\pipe\\zig-childprocess-{d}-{d}",
14294 .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) },
14295 0,
14296 ) catch unreachable;
14297 const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable;
14298 tmp_bufw[len] = 0;
14299 break :blk tmp_bufw[0..len :0];
14300 };
14301
14302 // Create the read handle that can be used with overlapped IO ops.
14303 const read_handle = windows.kernel32.CreateNamedPipeW(
14304 pipe_path.ptr,
14305 windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED,
14306 windows.PIPE_TYPE_BYTE,
14307 1,
14308 4096,
14309 4096,
14310 0,
14311 sattr,
14312 );
14313 if (read_handle == windows.INVALID_HANDLE_VALUE) {
14314 switch (windows.GetLastError()) {
14315 else => |err| return windows.unexpectedError(err),
14316 }
14317 }
14318 errdefer posix.close(read_handle);
14319
14320 var sattr_copy = sattr.*;
14321 const write_handle = windows.kernel32.CreateFileW(
14322 pipe_path.ptr,
14323 .{ .GENERIC = .{ .WRITE = true } },
14324 0,
14325 &sattr_copy,
14326 windows.OPEN_EXISTING,
14327 @bitCast(windows.FILE.ATTRIBUTE{ .NORMAL = true }),
14328 null,
14329 );
14330 if (write_handle == windows.INVALID_HANDLE_VALUE) {
14331 switch (windows.GetLastError()) {
14332 else => |err| return windows.unexpectedError(err),
14333 }
14334 }
14335 errdefer posix.close(write_handle);
14336
14337 try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
14338
14339 rd.* = read_handle;
14340 wr.* = write_handle;
14341}
14342
14343var pipe_name_counter = std.atomic.Value(u32).init(1);
14344
1269314345test {
1269414346 _ = @import("Threaded/test.zig");
1269514347}
lib/std/c.zig+22-22
......@@ -3764,8 +3764,8 @@ pub const W = switch (native_os) {
37643764 pub fn EXITSTATUS(x: u32) u8 {
37653765 return @as(u8, @intCast(x >> 8));
37663766 }
3767 pub fn TERMSIG(x: u32) u32 {
3768 return status(x);
3767 pub fn TERMSIG(x: u32) SIG {
3768 return @enumFromInt(status(x));
37693769 }
37703770 pub fn STOPSIG(x: u32) u32 {
37713771 return x >> 8;
......@@ -3797,14 +3797,14 @@ pub const W = switch (native_os) {
37973797 pub fn EXITSTATUS(s: u32) u8 {
37983798 return @as(u8, @intCast((s & 0xff00) >> 8));
37993799 }
3800 pub fn TERMSIG(s: u32) u32 {
3801 return s & 0x7f;
3800 pub fn TERMSIG(s: u32) SIG {
3801 return @enumFromInt(s & 0x7f);
38023802 }
38033803 pub fn STOPSIG(s: u32) u32 {
38043804 return EXITSTATUS(s);
38053805 }
38063806 pub fn IFEXITED(s: u32) bool {
3807 return TERMSIG(s) == 0;
3807 return (s & 0x7f) == 0;
38083808 }
38093809 pub fn IFSTOPPED(s: u32) bool {
38103810 return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00;
......@@ -3825,14 +3825,14 @@ pub const W = switch (native_os) {
38253825 pub fn EXITSTATUS(s: u32) u8 {
38263826 return @as(u8, @intCast((s >> 8) & 0xff));
38273827 }
3828 pub fn TERMSIG(s: u32) u32 {
3829 return s & 0x7f;
3828 pub fn TERMSIG(s: u32) SIG {
3829 return @enumFromInt(s & 0x7f);
38303830 }
38313831 pub fn STOPSIG(s: u32) u32 {
38323832 return EXITSTATUS(s);
38333833 }
38343834 pub fn IFEXITED(s: u32) bool {
3835 return TERMSIG(s) == 0;
3835 return (s & 0x7f) == 0;
38363836 }
38373837
38383838 pub fn IFCONTINUED(s: u32) bool {
......@@ -3859,14 +3859,14 @@ pub const W = switch (native_os) {
38593859 pub fn EXITSTATUS(s: u32) u8 {
38603860 return @as(u8, @intCast((s >> 8) & 0xff));
38613861 }
3862 pub fn TERMSIG(s: u32) u32 {
3863 return s & 0x7f;
3862 pub fn TERMSIG(s: u32) SIG {
3863 return @enumFromInt(s & 0x7f);
38643864 }
38653865 pub fn STOPSIG(s: u32) u32 {
38663866 return EXITSTATUS(s);
38673867 }
38683868 pub fn IFEXITED(s: u32) bool {
3869 return TERMSIG(s) == 0;
3869 return (s & 0x7f) == 0;
38703870 }
38713871
38723872 pub fn IFCONTINUED(s: u32) bool {
......@@ -3893,14 +3893,14 @@ pub const W = switch (native_os) {
38933893 pub fn EXITSTATUS(s: u32) u8 {
38943894 return @as(u8, @intCast((s & 0xff00) >> 8));
38953895 }
3896 pub fn TERMSIG(s: u32) u32 {
3897 return s & 0x7f;
3896 pub fn TERMSIG(s: u32) SIG {
3897 return @enumFromInt(s & 0x7f);
38983898 }
38993899 pub fn STOPSIG(s: u32) u32 {
39003900 return EXITSTATUS(s);
39013901 }
39023902 pub fn IFEXITED(s: u32) bool {
3903 return TERMSIG(s) == 0;
3903 return (s & 0x7f) == 0;
39043904 }
39053905 pub fn IFSTOPPED(s: u32) bool {
39063906 return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00;
......@@ -3921,8 +3921,8 @@ pub const W = switch (native_os) {
39213921 return @as(u8, @intCast(s & 0xff));
39223922 }
39233923
3924 pub fn TERMSIG(s: u32) u32 {
3925 return (s >> 8) & 0xff;
3924 pub fn TERMSIG(s: u32) SIG {
3925 return @enumFromInt((s >> 8) & 0xff);
39263926 }
39273927
39283928 pub fn STOPSIG(s: u32) u32 {
......@@ -3949,14 +3949,14 @@ pub const W = switch (native_os) {
39493949 pub fn EXITSTATUS(s: u32) u8 {
39503950 return @as(u8, @intCast((s >> 8) & 0xff));
39513951 }
3952 pub fn TERMSIG(s: u32) u32 {
3953 return (s & 0x7f);
3952 pub fn TERMSIG(s: u32) SIG {
3953 return @enumFromInt(s & 0x7f);
39543954 }
39553955 pub fn STOPSIG(s: u32) u32 {
39563956 return EXITSTATUS(s);
39573957 }
39583958 pub fn IFEXITED(s: u32) bool {
3959 return TERMSIG(s) == 0;
3959 return (s & 0x7f) == 0;
39603960 }
39613961
39623962 pub fn IFCONTINUED(s: u32) bool {
......@@ -3988,12 +3988,12 @@ pub const W = switch (native_os) {
39883988 return EXITSTATUS(s);
39893989 }
39903990
3991 pub fn TERMSIG(s: u32) u32 {
3992 return s & 0x7f;
3991 pub fn TERMSIG(s: u32) SIG {
3992 return @enumFromInt(s & 0x7f);
39933993 }
39943994
39953995 pub fn IFEXITED(s: u32) bool {
3996 return TERMSIG(s) == 0;
3996 return (s & 0x7f) == 0;
39973997 }
39983998
39993999 pub fn IFSTOPPED(s: u32) bool {
lib/std/os/emscripten.zig+3-3
......@@ -224,14 +224,14 @@ pub const W = struct {
224224 pub fn EXITSTATUS(s: u32) u8 {
225225 return @as(u8, @intCast((s & 0xff00) >> 8));
226226 }
227 pub fn TERMSIG(s: u32) u32 {
228 return s & 0x7f;
227 pub fn TERMSIG(s: u32) SIG {
228 return @enumFromInt(s & 0x7f);
229229 }
230230 pub fn STOPSIG(s: u32) u32 {
231231 return EXITSTATUS(s);
232232 }
233233 pub fn IFEXITED(s: u32) bool {
234 return TERMSIG(s) == 0;
234 return (s & 0x7f) == 0;
235235 }
236236 pub fn IFSTOPPED(s: u32) bool {
237237 return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00;
lib/std/os/linux.zig+3-3
......@@ -3616,14 +3616,14 @@ pub const W = struct {
36163616 pub fn EXITSTATUS(s: u32) u8 {
36173617 return @as(u8, @intCast((s & 0xff00) >> 8));
36183618 }
3619 pub fn TERMSIG(s: u32) u32 {
3620 return s & 0x7f;
3619 pub fn TERMSIG(s: u32) SIG {
3620 return @enumFromInt(s & 0x7f);
36213621 }
36223622 pub fn STOPSIG(s: u32) u32 {
36233623 return EXITSTATUS(s);
36243624 }
36253625 pub fn IFEXITED(s: u32) bool {
3626 return TERMSIG(s) == 0;
3626 return (s & 0x7f) == 0;
36273627 }
36283628 pub fn IFSTOPPED(s: u32) bool {
36293629 return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00;
lib/std/posix.zig-97
......@@ -795,79 +795,10 @@ pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void {
795795 }
796796}
797797
798pub fn getpid() pid_t {
799 return system.getpid();
800}
801
802798pub fn getppid() pid_t {
803799 return system.getppid();
804800}
805801
806pub const ExecveError = error{
807 SystemResources,
808 AccessDenied,
809 PermissionDenied,
810 InvalidExe,
811 FileSystem,
812 IsDir,
813 FileNotFound,
814 NotDir,
815 FileBusy,
816 ProcessFdQuotaExceeded,
817 SystemFdQuotaExceeded,
818 NameTooLong,
819} || UnexpectedError;
820
821/// This function ignores PATH environment variable. See `execvpeZ` for that.
822pub fn execveZ(
823 path: [*:0]const u8,
824 child_argv: [*:null]const ?[*:0]const u8,
825 envp: [*:null]const ?[*:0]const u8,
826) ExecveError {
827 switch (errno(system.execve(path, child_argv, envp))) {
828 .SUCCESS => unreachable,
829 .FAULT => unreachable,
830 .@"2BIG" => return error.SystemResources,
831 .MFILE => return error.ProcessFdQuotaExceeded,
832 .NAMETOOLONG => return error.NameTooLong,
833 .NFILE => return error.SystemFdQuotaExceeded,
834 .NOMEM => return error.SystemResources,
835 .ACCES => return error.AccessDenied,
836 .PERM => return error.PermissionDenied,
837 .INVAL => return error.InvalidExe,
838 .NOEXEC => return error.InvalidExe,
839 .IO => return error.FileSystem,
840 .LOOP => return error.FileSystem,
841 .ISDIR => return error.IsDir,
842 .NOENT => return error.FileNotFound,
843 .NOTDIR => return error.NotDir,
844 .TXTBSY => return error.FileBusy,
845 else => |err| switch (native_os) {
846 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => switch (err) {
847 .BADEXEC => return error.InvalidExe,
848 .BADARCH => return error.InvalidExe,
849 else => return unexpectedErrno(err),
850 },
851 .linux => switch (err) {
852 .LIBBAD => return error.InvalidExe,
853 else => return unexpectedErrno(err),
854 },
855 else => return unexpectedErrno(err),
856 },
857 }
858}
859
860/// This function also uses the PATH environment variable to get the full path to the executable.
861/// If `file` is an absolute path, this is the same as `execveZ`.
862pub fn execvpeZ(
863 file: [*:0]const u8,
864 argv_ptr: [*:null]const ?[*:0]const u8,
865 envp: [*:null]const ?[*:0]const u8,
866 optional_PATH: ?[]const u8,
867) ExecveError {
868 return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp, optional_PATH);
869}
870
871802pub const GetCwdError = error{
872803 NameTooLong,
873804 CurrentWorkingDirectoryUnlinked,
......@@ -1119,16 +1050,6 @@ pub fn seteuid(uid: uid_t) SetEidError!void {
11191050 }
11201051}
11211052
1122pub fn setreuid(ruid: uid_t, euid: uid_t) SetIdError!void {
1123 switch (errno(system.setreuid(ruid, euid))) {
1124 .SUCCESS => return,
1125 .AGAIN => return error.ResourceLimitReached,
1126 .INVAL => return error.InvalidUserId,
1127 .PERM => return error.PermissionDenied,
1128 else => |err| return unexpectedErrno(err),
1129 }
1130}
1131
11321053pub fn setgid(gid: gid_t) SetIdError!void {
11331054 switch (errno(system.setgid(gid))) {
11341055 .SUCCESS => return,
......@@ -1158,24 +1079,6 @@ pub fn setregid(rgid: gid_t, egid: gid_t) SetIdError!void {
11581079 }
11591080}
11601081
1161pub const SetPgidError = error{
1162 ProcessAlreadyExec,
1163 InvalidProcessGroupId,
1164 PermissionDenied,
1165 ProcessNotFound,
1166} || UnexpectedError;
1167
1168pub fn setpgid(pid: pid_t, pgid: pid_t) SetPgidError!void {
1169 switch (errno(system.setpgid(pid, pgid))) {
1170 .SUCCESS => return,
1171 .ACCES => return error.ProcessAlreadyExec,
1172 .INVAL => return error.InvalidProcessGroupId,
1173 .PERM => return error.PermissionDenied,
1174 .SRCH => return error.ProcessNotFound,
1175 else => |err| return unexpectedErrno(err),
1176 }
1177}
1178
11791082pub fn getuid() uid_t {
11801083 return system.getuid();
11811084}
lib/std/process.zig+201-104
......@@ -259,20 +259,44 @@ pub fn getBaseAddress() usize {
259259 }
260260}
261261
262/// Deprecated in favor of `Child.can_spawn`.
263pub const can_spawn = Child.can_spawn;
264/// Deprecated in favor of `can_replace`.
265pub const can_execv = can_replace;
266
267262/// Tells whether the target operating system supports replacing the current
268/// process image. If this is `false` then calling `execv` or `replace`
269/// functions will cause compilation to fail.
263/// process image. If this is `false` then calling `replace` or `replaceFile`
264/// functions will return `error.OperationUnsupported`.
270265pub const can_replace = switch (native_os) {
271266 .windows, .haiku, .wasi => false,
272267 else => true,
273268};
274269
275pub const ReplaceError = std.posix.ExecveError || error{OutOfMemory};
270/// Tells whether spawning child processes is supported.
271pub const can_spawn = switch (native_os) {
272 .wasi, .ios, .tvos, .visionos, .watchos => false,
273 else => true,
274};
275
276pub const ReplaceError = error{
277 /// The target operating system cannot replace the process image with a new
278 /// one.
279 OperationUnsupported,
280 SystemResources,
281 AccessDenied,
282 PermissionDenied,
283 InvalidExe,
284 FileSystem,
285 IsDir,
286 FileNotFound,
287 NotDir,
288 FileBusy,
289 ProcessFdQuotaExceeded,
290 SystemFdQuotaExceeded,
291} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
292
293pub const ReplaceOptions = struct {
294 argv: []const []const u8,
295 arg0_expand: ArgExpansion = .no_expand,
296 /// Replaces the environment when provided. The PATH value from here is
297 /// never used to resolve `argv[0]`.
298 env_map: ?*const Environ.Map = null,
299};
276300
277301/// Replaces the current process image with the executed process. If this
278302/// function succeeds, it does not return.
......@@ -281,25 +305,9 @@ pub const ReplaceError = std.posix.ExecveError || error{OutOfMemory};
281305/// is not already a file path (i.e. it contains '/'), it is resolved into a
282306/// file path based on PATH from the parent environment.
283307///
284/// This operation is not available on targets for which `can_replace` is
285/// `false`.
286///
287/// This function must allocate memory to add a null terminating bytes on path
288/// and each arg.
289///
290/// Due to the heap allocation, it is illegal to call this function in a fork()
291/// child.
292pub fn replace(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Block) ReplaceError {
293 if (!can_replace) @compileError("unsupported operation: replace");
294
295 var arena_allocator = std.heap.ArenaAllocator.init(gpa);
296 defer arena_allocator.deinit();
297 const arena = arena_allocator.allocator();
298
299 const argv_buf = try arena.allocSentinel(?[*:0]const u8, argv.len, null);
300 for (argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
301
302 return posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, env);
308/// It is illegal to call this function in a fork() child.
309pub fn replace(io: Io, options: ReplaceOptions) ReplaceError {
310 return io.vtable.processReplace(io.userdata, options);
303311}
304312
305313/// Replaces the current process image with the executed process. If this
......@@ -309,92 +317,181 @@ pub fn replace(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Bl
309317/// relative to `dir`. It is *always* treated as a file path, even if it does
310318/// not contain '/'.
311319///
312/// This operation is not available on targets for which `can_replace` is
313/// `false`.
320/// It is illegal to call this function in a fork() child.
321pub fn replacePath(io: Io, dir: Io.Dir, options: ReplaceOptions) ReplaceError {
322 return io.vtable.processReplacePath(io.userdata, dir, options);
323}
324
325pub const ArgExpansion = enum { expand, no_expand };
326
327/// File name extensions supported natively by `CreateProcess()` on Windows.
328pub const WindowsExtension = enum { bat, cmd, com, exe };
329
330pub const SpawnError = error{
331 OutOfMemory,
332 /// POSIX-only. `StdIo.ignore` was selected and opening `/dev/null` returned ENODEV.
333 NoDevice,
334 /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8.
335 /// https://wtf-8.codeberg.page/
336 InvalidWtf8,
337 /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process.
338 CurrentWorkingDirectoryUnlinked,
339 /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
340 /// within arguments when executing a `.bat`/`.cmd` script.
341 /// - NUL/LF signifiies end of arguments, so anything afterwards
342 /// would be lost after execution.
343 /// - CR is stripped by `cmd.exe`, so any CR codepoints
344 /// would be lost after execution.
345 InvalidBatchScriptArg,
346 SystemResources,
347 AccessDenied,
348 PermissionDenied,
349 InvalidExe,
350 FileSystem,
351 IsDir,
352 FileNotFound,
353 NotDir,
354 FileBusy,
355 ProcessFdQuotaExceeded,
356 SystemFdQuotaExceeded,
357 ResourceLimitReached,
358 InvalidUserId,
359 InvalidProcessGroupId,
360 SymLinkLoop,
361 InvalidName,
362 /// An attempt was made to change the process group ID of one of the
363 /// children of the calling process and the child had already performed an
364 /// image replacement.
365 ProcessAlreadyExec,
366} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
367
368pub const SpawnOptions = struct {
369 argv: []const []const u8,
370
371 /// Set to change the current working directory when spawning the child process.
372 cwd: ?[]const u8 = null,
373 /// Set to change the current working directory when spawning the child process.
374 /// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190
375 /// Once that is done, `cwd` will be deprecated in favor of this field.
376 cwd_dir: ?Io.Dir = null,
377 /// Replaces the child environment when provided. The PATH value from here
378 /// is not used to resolve `argv[0]`; that resolution always uses parent
379 /// environment.
380 env_map: ?*const Environ.Map = null,
381 expand_arg0: ArgExpansion = .no_expand,
382 /// When populated, a pipe will be created for the child process to
383 /// communicate progress back to the parent. The file descriptor of the
384 /// write end of the pipe will be specified in the `ZIG_PROGRESS`
385 /// environment variable inside the child process. The progress reported by
386 /// the child will be attached to this progress node in the parent process.
387 ///
388 /// The child's progress tree will be grafted into the parent's progress tree,
389 /// by substituting this node with the child's root node.
390 progress_node: std.Progress.Node = std.Progress.Node.none,
391
392 stdin: StdIo = .inherit,
393 stdout: StdIo = .inherit,
394 stderr: StdIo = .inherit,
395
396 /// Set to true to obtain rusage information for the child process.
397 /// Depending on the target platform and implementation status, the
398 /// requested statistics may or may not be available. If they are
399 /// available, then the `resource_usage_statistics` field will be populated
400 /// after calling `wait`.
401 /// On Linux and Darwin, this obtains rusage statistics from wait4().
402 request_resource_usage_statistics: bool = false,
403
404 /// Set to change the user id when spawning the child process.
405 uid: ?posix.uid_t = null,
406 /// Set to change the group id when spawning the child process.
407 gid: ?posix.gid_t = null,
408 /// Set to change the process group id when spawning the child process.
409 pgid: ?posix.pid_t = null,
410
411 /// Start child process in suspended state.
412 /// For Posix systems it's started as if SIGSTOP was sent.
413 start_suspended: bool = false,
414 /// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess.
415 create_no_window: bool = false,
416 /// Darwin-only. Disable ASLR for the child process.
417 disable_aslr: bool = false,
418
419 /// Behavior of the child process's standard input, output, and error streams.
420 pub const StdIo = union(enum) {
421 /// Inherit the corresponding stream from the parent process.
422 inherit,
423 /// Pass an already open file from the parent to the child.
424 file: File,
425 /// Pass a null stream to the child process by opening "/dev/null" on POSIX
426 /// and "NUL" on Windows.
427 ignore,
428 /// Create a new pipe for the stream.
429 ///
430 /// The corresponding field (`stdout`, `stderr`, or `stdin`) will be
431 /// assigned a `File` object that can be used to read from or write to the
432 /// pipe.
433 pipe,
434 /// Spawn the child process with the corresponding stream missing. This
435 /// will likely result in the child encountering EBADF if it tries to use
436 /// stdin, stdout, or stderr, or if only one stream is closed, it will
437 /// result in them getting mixed up. Generally, this option is for advanced
438 /// use cases only.
439 close,
440 };
441};
442
443/// Creates a child process.
314444///
315/// This function must allocate memory to add a null terminating bytes on path
316/// and each arg.
445/// `argv[0]` is the name of the program to execute. If it is not already a
446/// file path (i.e. it contains '/'), it is resolved into a file path based on
447/// PATH from the parent environment.
448pub fn spawn(io: Io, options: SpawnOptions) SpawnError!Child {
449 return io.vtable.processSpawn(io.userdata, options);
450}
451
452/// Creates a child process.
317453///
318/// Due to the heap allocation, it is illegal to call this
319/// function in a fork() child. For that use case, use the `std.posix`
320/// functions directly.
321pub fn replaceFile(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Block) ReplaceError {
322 if (!can_replace) @compileError("unsupported operation: replaceFile");
454/// `argv[0]` is the file path of the program to execute, relative to `dir`. It
455/// is *always* treated as a file path, even if it does not contain '/'.
456pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child {
457 return io.vtable.processSpawnPath(io.userdata, dir, options);
458}
323459
324 var arena_allocator = std.heap.ArenaAllocator.init(gpa);
325 defer arena_allocator.deinit();
326 const arena = arena_allocator.allocator();
460pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{
461 StdoutStreamTooLong,
462 StderrStreamTooLong,
463};
327464
328 const argv_buf = try arena.allocSentinel(?[*:0]const u8, argv.len, null);
329 for (argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
465pub const RunOptions = struct {
466 spawn_options: SpawnOptions,
467 max_output_bytes: usize = 50 * 1024,
468};
330469
331 return posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, env);
332}
470pub const RunResult = struct {
471 term: Child.Term,
472 stdout: []u8,
473 stderr: []u8,
474};
333475
334pub const Arg0Expand = enum { expand, no_expand };
476/// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
477/// If it succeeds, the caller owns result.stdout and result.stderr memory.
478pub fn run(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult {
479 var child = try spawn(io, options.spawn_options);
480 defer child.kill(io);
335481
336/// Replaces the current process image with the executed process. If this
337/// function succeeds, it does not return.
338///
339/// This operation is not available on all targets. `can_execv`
340///
341/// This function also uses the PATH environment variable to get the full path to the executable.
342/// If `file` is an absolute path, this is the same as `execveZ`.
343///
344/// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable,
345/// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall.
346/// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in.
347pub fn replace(
348 comptime arg0_expand: Arg0Expand,
349 file: [*:0]const u8,
350 child_argv: switch (arg0_expand) {
351 .expand => [*:null]?[*:0]const u8,
352 .no_expand => [*:null]const ?[*:0]const u8,
353 },
354 envp: [*:null]const ?[*:0]const u8,
355 optional_PATH: ?[]const u8,
356) ExecveError {
357 const file_slice = mem.sliceTo(file, 0);
358 if (mem.findScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp);
359
360 const PATH = optional_PATH orelse "/usr/local/bin:/bin/:/usr/bin";
361 // Use of PATH_MAX here is valid as the path_buf will be passed
362 // directly to the operating system in execveZ.
363 var path_buf: [PATH_MAX]u8 = undefined;
364 var it = mem.tokenizeScalar(u8, PATH, ':');
365 var seen_eacces = false;
366 var err: ExecveError = error.FileNotFound;
367
368 // In case of expanding arg0 we must put it back if we return with an error.
369 const prev_arg0 = child_argv[0];
370 defer switch (arg0_expand) {
371 .expand => child_argv[0] = prev_arg0,
372 .no_expand => {},
373 };
482 var stdout: std.ArrayList(u8) = .empty;
483 defer stdout.deinit(gpa);
484 var stderr: std.ArrayList(u8) = .empty;
485 defer stderr.deinit(gpa);
374486
375 while (it.next()) |search_path| {
376 const path_len = search_path.len + file_slice.len + 1;
377 if (path_buf.len < path_len + 1) return error.NameTooLong;
378 @memcpy(path_buf[0..search_path.len], search_path);
379 path_buf[search_path.len] = '/';
380 @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice);
381 path_buf[path_len] = 0;
382 const full_path = path_buf[0..path_len :0].ptr;
383 switch (arg0_expand) {
384 .expand => child_argv[0] = full_path,
385 .no_expand => {},
386 }
387 err = execveZ(full_path, child_argv, envp);
388 switch (err) {
389 error.AccessDenied => seen_eacces = true,
390 error.FileNotFound, error.NotDir => {},
391 else => |e| return e,
392 }
393 }
394 if (seen_eacces) return error.AccessDenied;
395 return err;
396}
487 try child.collectOutput(gpa, &stdout, &stderr, options.max_output_bytes);
397488
489 return .{
490 .stdout = try stdout.toOwnedSlice(gpa),
491 .stderr = try stderr.toOwnedSlice(gpa),
492 .term = try child.wait(io),
493 };
494}
398495
399496pub const TotalSystemMemoryError = error{
400497 UnknownTotalSystemMemory,
lib/std/process/Child.zig+39-1760
......@@ -5,120 +5,38 @@ const native_os = builtin.os.tag;
55
66const std = @import("../std.zig");
77const Io = std.Io;
8const unicode = std.unicode;
9const fs = std.fs;
108const process = std.process;
119const File = std.Io.File;
12const windows = std.os.windows;
13const linux = std.os.linux;
14const posix = std.posix;
15const mem = std.mem;
16const maxInt = std.math.maxInt;
1710const assert = std.debug.assert;
1811const Allocator = std.mem.Allocator;
1912const ArrayList = std.ArrayList;
2013
21/// Tells whether spawning child processes is supported.
22pub const can_spawn = switch (native_os) {
23 .wasi, .ios, .tvos, .visionos, .watchos => false,
24 else => true,
25};
26
2714pub const Id = switch (native_os) {
28 .windows => windows.HANDLE,
15 .windows => std.os.windows.HANDLE,
2916 .wasi => void,
30 else => posix.pid_t,
17 else => std.posix.pid_t,
3118};
3219
33/// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`.
20/// After `wait` or `kill` is called, this becomes `null`.
3421/// On Windows this is the hProcess.
3522/// On POSIX this is the pid.
36id: Id,
37thread_handle: if (native_os == .windows) windows.HANDLE else void,
38
39allocator: Allocator,
40
23id: ?Id,
24thread_handle: if (native_os == .windows) std.os.windows.HANDLE else void = {},
4125/// The writing end of the child process's standard input pipe.
42/// Usage requires `stdin_behavior == StdIo.Pipe`.
43/// Available after calling `spawn()`.
26/// Usage requires `process.SpawnOptions.StdIo.pipe`.
4427stdin: ?File,
45
4628/// The reading end of the child process's standard output pipe.
47/// Usage requires `stdout_behavior == StdIo.Pipe`.
48/// Available after calling `spawn()`.
29/// Usage requires `process.SpawnOptions.StdIo.pipe`.
4930stdout: ?File,
50
5131/// The reading end of the child process's standard error pipe.
52/// Usage requires `stderr_behavior == StdIo.Pipe`.
53/// Available after calling `spawn()`.
32/// Usage requires `process.SpawnOptions.StdIo.pipe`.
5433stderr: ?File,
55
56/// Terminated state of the child process.
57/// Available after calling `wait()`.
58term: ?(SpawnError!Term),
59
60argv: []const []const u8,
61
62parent_environ: process.Environ,
63/// `null` means to use `parent_environ` also for the spawned process.
64env_map: ?*const EnvMap,
65
66stdin_behavior: StdIo,
67stdout_behavior: StdIo,
68stderr_behavior: StdIo,
69
70/// Set to change the user id when spawning the child process.
71uid: if (native_os == .windows or native_os == .wasi) void else ?posix.uid_t,
72
73/// Set to change the group id when spawning the child process.
74gid: if (native_os == .windows or native_os == .wasi) void else ?posix.gid_t,
75
76/// Set to change the process group id when spawning the child process.
77pgid: if (native_os == .windows or native_os == .wasi) void else ?posix.pid_t,
78
79/// Set to change the current working directory when spawning the child process.
80cwd: ?[]const u8,
81/// Set to change the current working directory when spawning the child process.
82/// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190
83/// Once that is done, `cwd` will be deprecated in favor of this field.
84cwd_dir: ?Io.Dir = null,
85
86err_pipe: if (native_os == .windows) void else ?posix.fd_t,
87
88expand_arg0: Arg0Expand,
89
90/// Darwin-only. Disable ASLR for the child process.
91disable_aslr: bool = false,
92
93/// Start child process in suspended state.
94/// For Posix systems it's started as if SIGSTOP was sent.
95start_suspended: bool = false,
96
97/// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess.
98create_no_window: bool = false,
99
100/// Set to true to obtain rusage information for the child process.
101/// Depending on the target platform and implementation status, the
102/// requested statistics may or may not be available. If they are
103/// available, then the `resource_usage_statistics` field will be populated
104/// after calling `wait`.
105/// On Linux and Darwin, this obtains rusage statistics from wait4().
106request_resource_usage_statistics: bool = false,
107
10834/// This is available after calling wait if
10935/// `request_resource_usage_statistics` was set to `true` before calling
11036/// `spawn`.
37/// TODO move this data into `Term`
11138resource_usage_statistics: ResourceUsageStatistics = .{},
112
113/// When populated, a pipe will be created for the child process to
114/// communicate progress back to the parent. The file descriptor of the
115/// write end of the pipe will be specified in the `ZIG_PROGRESS`
116/// environment variable inside the child process. The progress reported by
117/// the child will be attached to this progress node in the parent process.
118///
119/// The child's progress tree will be grafted into the parent's progress tree,
120/// by substituting this node with the child's root node.
121progress_node: std.Progress.Node = std.Progress.Node.none,
39request_resource_usage_statistics: bool,
12240
12341pub const ResourceUsageStatistics = struct {
12442 rusage: @TypeOf(rusage_init) = rusage_init,
......@@ -168,233 +86,60 @@ pub const ResourceUsageStatistics = struct {
16886 .tvos,
16987 .visionos,
17088 .watchos,
171 => @as(?posix.rusage, null),
172 .windows => @as(?windows.VM_COUNTERS, null),
89 => @as(?std.posix.rusage, null),
90 .windows => @as(?std.os.windows.VM_COUNTERS, null),
17391 else => {},
17492 };
17593};
17694
177pub const Arg0Expand = posix.Arg0Expand;
178
179pub const SpawnError = error{
180 OutOfMemory,
181
182 /// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV.
183 NoDevice,
184
185 /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8.
186 /// https://wtf-8.codeberg.page/
187 InvalidWtf8,
188
189 /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process.
190 CurrentWorkingDirectoryUnlinked,
191
192 /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
193 /// within arguments when executing a `.bat`/`.cmd` script.
194 /// - NUL/LF signifiies end of arguments, so anything afterwards
195 /// would be lost after execution.
196 /// - CR is stripped by `cmd.exe`, so any CR codepoints
197 /// would be lost after execution.
198 InvalidBatchScriptArg,
199} ||
200 posix.ExecveError ||
201 posix.SetIdError ||
202 posix.SetPgidError ||
203 posix.ChangeCurDirError ||
204 windows.CreateProcessError ||
205 windows.GetProcessMemoryInfoError ||
206 windows.WaitForSingleObjectError;
207
20895pub const Term = union(enum) {
209 Exited: u8,
210 Signal: u32,
211 Stopped: u32,
212 Unknown: u32,
213};
214
215/// Behavior of the child process's standard input, output, and error
216/// streams.
217pub const StdIo = enum {
218 /// Inherit the stream from the parent process.
219 Inherit,
220
221 /// Pass a null stream to the child process.
222 /// This is /dev/null on POSIX and NUL on Windows.
223 Ignore,
224
225 /// Create a pipe for the stream.
226 /// The corresponding field (`stdout`, `stderr`, or `stdin`)
227 /// will be assigned a `File` object that can be used
228 /// to read from or write to the pipe.
229 Pipe,
230
231 /// Close the stream after the child process spawns.
232 Close,
96 exited: u8,
97 signal: std.posix.SIG,
98 stopped: u32,
99 unknown: u32,
233100};
234101
235/// First argument in argv is the executable.
236pub fn init(gpa: Allocator, argv: []const []const u8, environ: Environ) Child {
237 return .{
238 .allocator = gpa,
239 .argv = argv,
240 .environ = environ,
241 .id = undefined,
242 .thread_handle = undefined,
243 .err_pipe = if (native_os == .windows) {} else null,
244 .term = null,
245 .cwd = null,
246 .uid = if (native_os == .windows or native_os == .wasi) {} else null,
247 .gid = if (native_os == .windows or native_os == .wasi) {} else null,
248 .pgid = if (native_os == .windows or native_os == .wasi) {} else null,
249 .stdin = null,
250 .stdout = null,
251 .stderr = null,
252 .stdin_behavior = .Inherit,
253 .stdout_behavior = .Inherit,
254 .stderr_behavior = .Inherit,
255 .expand_arg0 = .no_expand,
256 };
257}
258
259pub fn setUserName(self: *Child, name: []const u8) !void {
260 const user_info = try process.getUserInfo(name);
261 self.uid = user_info.uid;
262 self.gid = user_info.gid;
263}
264
265/// On success must call `kill` or `wait`.
266/// After spawning the `id` is available.
267pub fn spawn(self: *Child, io: Io) SpawnError!void {
268 if (!process.can_spawn) {
269 @compileError("the target operating system cannot spawn processes");
270 }
271
272 if (native_os == .windows) {
273 return self.spawnWindows(io);
274 } else {
275 return self.spawnPosix(io);
276 }
277}
278
279pub fn spawnAndWait(child: *Child, io: Io) SpawnError!Term {
280 try child.spawn(io);
281 return child.wait(io);
282}
283
284/// Forcibly terminates child process and then cleans up all resources.
285pub fn kill(self: *Child, io: Io) !Term {
286 if (native_os == .windows) {
287 return self.killWindows(io, 1);
288 } else {
289 return self.killPosix(io);
290 }
291}
292
293pub fn killWindows(self: *Child, io: Io, exit_code: windows.UINT) !Term {
294 if (self.term) |term| {
295 self.cleanupStreams(io);
296 return term;
297 }
298
299 windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) {
300 error.AccessDenied => {
301 // Usually when TerminateProcess triggers a ACCESS_DENIED error, it
302 // indicates that the process has already exited, but there may be
303 // some rare edge cases where our process handle no longer has the
304 // PROCESS_TERMINATE access right, so let's do another check to make
305 // sure the process is really no longer running:
306 windows.WaitForSingleObjectEx(self.id, 0, false) catch return err;
307 return error.AlreadyTerminated;
308 },
309 else => return err,
310 };
311 try self.waitUnwrappedWindows(io);
312 return self.term.?;
313}
314
315pub fn killPosix(self: *Child, io: Io) !Term {
316 if (self.term) |term| {
317 self.cleanupStreams(io);
318 return term;
319 }
320 posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) {
321 error.ProcessNotFound => return error.AlreadyTerminated,
322 else => return err,
323 };
324 self.waitUnwrappedPosix(io);
325 return self.term.?;
326}
327
328pub const WaitError = SpawnError || std.os.windows.GetProcessMemoryInfoError;
329
330/// On some targets, `spawn` may not report all spawn errors, such as `error.InvalidExe`.
331/// This function will block until any spawn errors can be reported, and return them.
332pub fn waitForSpawn(self: *Child) SpawnError!void {
333 if (native_os == .windows) return; // `spawn` reports everything
334 if (self.term) |term| {
335 _ = term catch |spawn_err| return spawn_err;
102/// Requests for the operating system to forcibly terminate the child process,
103/// then blocks until it terminates, then cleans up all resources.
104///
105/// Idempotent and does nothing after `wait` returns.
106///
107/// Uncancelable. Ignores unexpected errors from the operating system.
108pub fn kill(child: *Child, io: Io) void {
109 if (child.id != null) {
110 assert(child.stdin == null);
111 assert(child.stdout == null);
112 assert(child.stderr == null);
336113 return;
337114 }
338
339 const err_pipe = self.err_pipe orelse return;
340 self.err_pipe = null;
341 // Wait for the child to report any errors in or before `execvpe`.
342 const report = readIntFd(err_pipe);
343 posix.close(err_pipe);
344 if (report) |child_err_int| {
345 const child_err: SpawnError = @errorCast(@errorFromInt(child_err_int));
346 self.term = child_err;
347 return child_err;
348 } else |read_err| switch (read_err) {
349 error.EndOfStream => {
350 // Write end closed by CLOEXEC at the time of the `execvpe` call,
351 // indicating success.
352 },
353 else => {
354 // Problem reading the error from the error reporting pipe. We
355 // don't know if the child is alive or dead. Better to assume it is
356 // alive so the resource does not risk being leaked.
357 },
358 }
115 io.vtable.childKill(io.userdata, child);
116 assert(child.id == null);
359117}
360118
119pub const WaitError = error{
120 AccessDenied,
121} || Io.Cancelable || Io.UnexpectedError;
122
361123/// Blocks until child process terminates and then cleans up all resources.
362pub fn wait(self: *Child, io: Io) WaitError!Term {
363 try self.waitForSpawn(); // report spawn errors
364 if (self.term) |term| {
365 self.cleanupStreams(io);
366 return term;
367 }
368 switch (native_os) {
369 .windows => try self.waitUnwrappedWindows(io),
370 else => self.waitUnwrappedPosix(io),
371 }
372 self.id = undefined;
373 return self.term.?;
124pub fn wait(child: *Child, io: Io) WaitError!Term {
125 assert(child.id != null);
126 return io.vtable.childWait(io.userdata, child);
374127}
375128
376pub const RunResult = struct {
377 term: Term,
378 stdout: []u8,
379 stderr: []u8,
380};
381
382129/// Collect the output from the process's stdout and stderr. Will return once all output
383130/// has been collected. This does not mean that the process has ended. `wait` should still
384131/// be called to wait for and clean up the process.
385132///
386/// The process must be started with stdout_behavior and stderr_behavior == .Pipe
133/// The process must have been started with stdout and stderr set to
134/// `process.SpawnOptions.StdIo.pipe`.
387135pub fn collectOutput(
388 child: Child,
136 child: *const Child,
389137 /// Used for `stdout` and `stderr`.
390138 allocator: Allocator,
391139 stdout: *ArrayList(u8),
392140 stderr: *ArrayList(u8),
393141 max_output_bytes: usize,
394142) !void {
395 assert(child.stdout_behavior == .Pipe);
396 assert(child.stderr_behavior == .Pipe);
397
398143 var poller = std.Io.poll(allocator, enum { stdout, stderr }, .{
399144 .stdout = child.stdout.?,
400145 .stderr = child.stderr.?,
......@@ -431,1469 +176,3 @@ pub fn collectOutput(
431176 return error.StderrStreamTooLong;
432177 }
433178}
434
435pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{
436 StdoutStreamTooLong,
437 StderrStreamTooLong,
438};
439
440/// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
441/// If it succeeds, the caller owns result.stdout and result.stderr memory.
442pub fn run(gpa: Allocator, io: Io, args: struct {
443 argv: []const []const u8,
444 environ: Environ,
445 cwd: ?[]const u8 = null,
446 cwd_dir: ?Io.Dir = null,
447 max_output_bytes: usize = 50 * 1024,
448 expand_arg0: Arg0Expand = .no_expand,
449 progress_node: std.Progress.Node = std.Progress.Node.none,
450}) RunError!RunResult {
451 var child = Child.init(gpa, args.argv, args.environ);
452 child.stdin_behavior = .Ignore;
453 child.stdout_behavior = .Pipe;
454 child.stderr_behavior = .Pipe;
455 child.cwd = args.cwd;
456 child.cwd_dir = args.cwd_dir;
457 child.expand_arg0 = args.expand_arg0;
458 child.progress_node = args.progress_node;
459
460 var stdout: ArrayList(u8) = .empty;
461 defer stdout.deinit(gpa);
462 var stderr: ArrayList(u8) = .empty;
463 defer stderr.deinit(gpa);
464
465 try child.spawn(io);
466 errdefer {
467 _ = child.kill(io) catch {};
468 }
469 try child.collectOutput(gpa, &stdout, &stderr, args.max_output_bytes);
470
471 return .{
472 .stdout = try stdout.toOwnedSlice(gpa),
473 .stderr = try stderr.toOwnedSlice(gpa),
474 .term = try child.wait(io),
475 };
476}
477
478fn waitUnwrappedWindows(self: *Child, io: Io) WaitError!void {
479 const result = windows.WaitForSingleObjectEx(self.id, windows.INFINITE, false);
480
481 self.term = @as(SpawnError!Term, x: {
482 var exit_code: windows.DWORD = undefined;
483 if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) {
484 break :x Term{ .Unknown = 0 };
485 } else {
486 break :x Term{ .Exited = @as(u8, @truncate(exit_code)) };
487 }
488 });
489
490 if (self.request_resource_usage_statistics) {
491 self.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(self.id);
492 }
493
494 posix.close(self.id);
495 posix.close(self.thread_handle);
496 self.cleanupStreams(io);
497 return result;
498}
499
500fn waitUnwrappedPosix(self: *Child, io: Io) void {
501 const res: posix.WaitPidResult = res: {
502 if (self.request_resource_usage_statistics) {
503 switch (native_os) {
504 .dragonfly,
505 .freebsd,
506 .netbsd,
507 .openbsd,
508 .illumos,
509 .linux,
510 .serenity,
511 .driverkit,
512 .ios,
513 .maccatalyst,
514 .macos,
515 .tvos,
516 .visionos,
517 .watchos,
518 => {
519 var ru: posix.rusage = undefined;
520 const res = posix.wait4(self.id, 0, &ru);
521 self.resource_usage_statistics.rusage = ru;
522 break :res res;
523 },
524 else => {},
525 }
526 }
527
528 break :res posix.waitpid(self.id, 0);
529 };
530 const status = res.status;
531 self.cleanupStreams(io);
532 self.handleWaitResult(status);
533}
534
535fn handleWaitResult(self: *Child, status: u32) void {
536 self.term = statusToTerm(status);
537}
538
539fn cleanupStreams(self: *Child, io: Io) void {
540 if (self.stdin) |*stdin| {
541 stdin.close(io);
542 self.stdin = null;
543 }
544 if (self.stdout) |*stdout| {
545 stdout.close(io);
546 self.stdout = null;
547 }
548 if (self.stderr) |*stderr| {
549 stderr.close(io);
550 self.stderr = null;
551 }
552}
553
554fn statusToTerm(status: u32) Term {
555 return if (posix.W.IFEXITED(status))
556 Term{ .Exited = posix.W.EXITSTATUS(status) }
557 else if (posix.W.IFSIGNALED(status))
558 Term{ .Signal = posix.W.TERMSIG(status) }
559 else if (posix.W.IFSTOPPED(status))
560 Term{ .Stopped = posix.W.STOPSIG(status) }
561 else
562 Term{ .Unknown = status };
563}
564
565fn spawnPosix(self: *Child, io: Io) SpawnError!void {
566 // The child process does need to access (one end of) these pipes. However,
567 // we must initially set CLOEXEC to avoid a race condition. If another thread
568 // is racing to spawn a different child process, we don't want it to inherit
569 // these FDs in any scenario; that would mean that, for instance, calls to
570 // `poll` from the parent would not report the child's stdout as closing when
571 // expected, since the other child may retain a reference to the write end of
572 // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we
573 // need to do something in the new child to make sure we preserve the reference
574 // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it
575 // turns out, we `dup2` everything anyway, so there's no need!
576 const pipe_flags: posix.O = .{ .CLOEXEC = true };
577
578 const stdin_pipe = if (self.stdin_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined;
579 errdefer if (self.stdin_behavior == .Pipe) {
580 destroyPipe(stdin_pipe);
581 };
582
583 const stdout_pipe = if (self.stdout_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined;
584 errdefer if (self.stdout_behavior == .Pipe) {
585 destroyPipe(stdout_pipe);
586 };
587
588 const stderr_pipe = if (self.stderr_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined;
589 errdefer if (self.stderr_behavior == .Pipe) {
590 destroyPipe(stderr_pipe);
591 };
592
593 const any_ignore = (self.stdin_behavior == .Ignore or self.stdout_behavior == .Ignore or self.stderr_behavior == .Ignore);
594 const dev_null_fd = if (any_ignore)
595 posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) {
596 error.PathAlreadyExists => unreachable,
597 error.NoSpaceLeft => unreachable,
598 error.FileTooBig => unreachable,
599 error.DeviceBusy => unreachable,
600 error.FileLocksUnsupported => unreachable,
601 error.BadPathName => unreachable, // Windows-only
602 error.WouldBlock => unreachable,
603 error.NetworkNotFound => unreachable, // Windows-only
604 error.Canceled => unreachable, // temporarily in the posix error set
605 error.SharingViolation => unreachable, // Windows-only
606 error.PipeBusy => unreachable, // not a pipe
607 error.AntivirusInterference => unreachable, // Windows-only
608 else => |e| return e,
609 }
610 else
611 undefined;
612 defer {
613 if (any_ignore) posix.close(dev_null_fd);
614 }
615
616 const prog_pipe: [2]posix.fd_t = p: {
617 if (self.progress_node.index == .none) {
618 break :p .{ -1, -1 };
619 } else {
620 // We use CLOEXEC for the same reason as in `pipe_flags`.
621 break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
622 }
623 };
624 errdefer destroyPipe(prog_pipe);
625
626 var arena_allocator = std.heap.ArenaAllocator.init(self.allocator);
627 defer arena_allocator.deinit();
628 const arena = arena_allocator.allocator();
629
630 // The POSIX standard does not allow malloc() between fork() and execve(),
631 // and `self.allocator` may be a libc allocator.
632 // I have personally observed the child process deadlocking when it tries
633 // to call malloc() due to a heap allocation between fork() and execve(),
634 // in musl v1.1.24.
635 // Additionally, we want to reduce the number of possible ways things
636 // can fail between fork() and execve().
637 // Therefore, we do all the allocation for the execve() before the fork().
638 // This means we must do the null-termination of argv and env vars here.
639 const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null);
640 for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
641
642 const prog_fileno = 3;
643 comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno);
644
645 const envp: [*:null]const ?[*:0]const u8 = m: {
646 const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno;
647 switch (self.environ) {
648 .empty => break :m (try process.Environ.createBlock(.{ .block = &.{} }, arena, .{
649 .zig_progress_fd = prog_fd,
650 })).ptr,
651 .inherit => |b| break :m (try b.createBlock(arena, .{
652 .zig_progress_fd = prog_fd,
653 })).ptr,
654 .map => |m| break :m (try m.createBlock(arena, .{
655 .zig_progress_fd = prog_fd,
656 })).ptr,
657 }
658 };
659
660 // This pipe communicates to the parent errors in the child between `fork` and `execvpe`.
661 // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.
662 const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true });
663 errdefer destroyPipe(err_pipe);
664
665 const pid_result = try posix.fork();
666 if (pid_result == 0) {
667 // we are the child
668 setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err);
669 setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err);
670 setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err);
671
672 if (self.cwd_dir) |cwd| {
673 posix.fchdir(cwd.handle) catch |err| forkChildErrReport(io, err_pipe[1], err);
674 } else if (self.cwd) |cwd| {
675 posix.chdir(cwd) catch |err| forkChildErrReport(io, err_pipe[1], err);
676 }
677
678 // Must happen after fchdir above, the cwd file descriptor might be
679 // equal to prog_fileno and be clobbered by this dup2 call.
680 if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkChildErrReport(io, err_pipe[1], err);
681
682 if (self.gid) |gid| {
683 posix.setregid(gid, gid) catch |err| forkChildErrReport(io, err_pipe[1], err);
684 }
685
686 if (self.uid) |uid| {
687 posix.setreuid(uid, uid) catch |err| forkChildErrReport(io, err_pipe[1], err);
688 }
689
690 if (self.pgid) |pid| {
691 posix.setpgid(0, pid) catch |err| forkChildErrReport(io, err_pipe[1], err);
692 }
693
694 if (self.start_suspended) {
695 posix.kill(posix.getpid(), .STOP) catch |err| forkChildErrReport(io, err_pipe[1], err);
696 }
697
698 const parent_PATH: ?[]const u8 = switch(self.environ) {
699 .empty => null,
700 .inherit =>
701 .map => |m| m.get("PATH"),
702 };
703
704 const err = switch (self.expand_arg0) {
705 .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp, parent_PATH),
706 .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp, parent_PATH),
707 };
708 forkChildErrReport(io, err_pipe[1], err);
709 }
710
711 // we are the parent
712 errdefer comptime unreachable; // The child is forked; we must not error from now on
713
714 posix.close(err_pipe[1]); // make sure only the child holds the write end open
715 self.err_pipe = err_pipe[0];
716
717 const pid: i32 = @intCast(pid_result);
718 if (self.stdin_behavior == .Pipe) {
719 self.stdin = .{ .handle = stdin_pipe[1] };
720 } else {
721 self.stdin = null;
722 }
723 if (self.stdout_behavior == .Pipe) {
724 self.stdout = .{ .handle = stdout_pipe[0] };
725 } else {
726 self.stdout = null;
727 }
728 if (self.stderr_behavior == .Pipe) {
729 self.stderr = .{ .handle = stderr_pipe[0] };
730 } else {
731 self.stderr = null;
732 }
733
734 self.id = pid;
735 self.term = null;
736
737 if (self.stdin_behavior == .Pipe) {
738 posix.close(stdin_pipe[0]);
739 }
740 if (self.stdout_behavior == .Pipe) {
741 posix.close(stdout_pipe[1]);
742 }
743 if (self.stderr_behavior == .Pipe) {
744 posix.close(stderr_pipe[1]);
745 }
746
747 if (prog_pipe[1] != -1) {
748 posix.close(prog_pipe[1]);
749 }
750 self.progress_node.setIpcFd(prog_pipe[0]);
751}
752
753fn spawnWindows(self: *Child, io: Io) SpawnError!void {
754 var saAttr = windows.SECURITY_ATTRIBUTES{
755 .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
756 .bInheritHandle = windows.TRUE,
757 .lpSecurityDescriptor = null,
758 };
759
760 const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore);
761
762 const nul_handle = if (any_ignore)
763 // "\Device\Null" or "\??\NUL"
764 windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{
765 .access_mask = .{
766 .STANDARD = .{ .SYNCHRONIZE = true },
767 .GENERIC = .{ .WRITE = true, .READ = true },
768 },
769 .sa = &saAttr,
770 .creation = .OPEN,
771 }) catch |err| switch (err) {
772 error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL"
773 error.PipeBusy => return error.Unexpected, // not possible for "NUL"
774 error.NoDevice => return error.Unexpected, // not possible for "NUL"
775 error.FileNotFound => return error.Unexpected, // not possible for "NUL"
776 error.AccessDenied => return error.Unexpected, // not possible for "NUL"
777 error.NameTooLong => return error.Unexpected, // not possible for "NUL"
778 error.WouldBlock => return error.Unexpected, // not possible for "NUL"
779 error.NetworkNotFound => return error.Unexpected, // not possible for "NUL"
780 error.AntivirusInterference => return error.Unexpected, // not possible for "NUL"
781 error.OperationCanceled => return error.Unexpected, // we're not canceling the operation
782 else => |e| return e,
783 }
784 else
785 undefined;
786 defer {
787 if (any_ignore) posix.close(nul_handle);
788 }
789
790 var g_hChildStd_IN_Rd: ?windows.HANDLE = null;
791 var g_hChildStd_IN_Wr: ?windows.HANDLE = null;
792 switch (self.stdin_behavior) {
793 StdIo.Pipe => {
794 try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr);
795 },
796 StdIo.Ignore => {
797 g_hChildStd_IN_Rd = nul_handle;
798 },
799 StdIo.Inherit => {
800 g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null;
801 },
802 StdIo.Close => {
803 g_hChildStd_IN_Rd = null;
804 },
805 }
806 errdefer if (self.stdin_behavior == StdIo.Pipe) {
807 windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
808 };
809
810 var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
811 var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
812 switch (self.stdout_behavior) {
813 StdIo.Pipe => {
814 try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
815 },
816 StdIo.Ignore => {
817 g_hChildStd_OUT_Wr = nul_handle;
818 },
819 StdIo.Inherit => {
820 g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null;
821 },
822 StdIo.Close => {
823 g_hChildStd_OUT_Wr = null;
824 },
825 }
826 errdefer if (self.stdout_behavior == StdIo.Pipe) {
827 windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr);
828 };
829
830 var g_hChildStd_ERR_Rd: ?windows.HANDLE = null;
831 var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
832 switch (self.stderr_behavior) {
833 StdIo.Pipe => {
834 try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
835 },
836 StdIo.Ignore => {
837 g_hChildStd_ERR_Wr = nul_handle;
838 },
839 StdIo.Inherit => {
840 g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null;
841 },
842 StdIo.Close => {
843 g_hChildStd_ERR_Wr = null;
844 },
845 }
846 errdefer if (self.stderr_behavior == StdIo.Pipe) {
847 windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr);
848 };
849
850 var siStartInfo = windows.STARTUPINFOW{
851 .cb = @sizeOf(windows.STARTUPINFOW),
852 .hStdError = g_hChildStd_ERR_Wr,
853 .hStdOutput = g_hChildStd_OUT_Wr,
854 .hStdInput = g_hChildStd_IN_Rd,
855 .dwFlags = windows.STARTF_USESTDHANDLES,
856
857 .lpReserved = null,
858 .lpDesktop = null,
859 .lpTitle = null,
860 .dwX = 0,
861 .dwY = 0,
862 .dwXSize = 0,
863 .dwYSize = 0,
864 .dwXCountChars = 0,
865 .dwYCountChars = 0,
866 .dwFillAttribute = 0,
867 .wShowWindow = 0,
868 .cbReserved2 = 0,
869 .lpReserved2 = null,
870 };
871 var piProcInfo: windows.PROCESS_INFORMATION = undefined;
872
873 const cwd_w = if (self.cwd) |cwd| try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd) else null;
874 defer if (cwd_w) |cwd| self.allocator.free(cwd);
875 const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null;
876
877 const maybe_envp_buf = if (self.env_map) |env_map| try process.createWindowsEnvBlock(self.allocator, env_map) else null;
878 defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf);
879 const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null;
880
881 const app_name_wtf8 = self.argv[0];
882 const app_name_is_absolute = fs.path.isAbsolute(app_name_wtf8);
883
884 // the cwd set in Child is in effect when choosing the executable path
885 // to match posix semantics
886 var cwd_path_w_needs_free = false;
887 const cwd_path_w = x: {
888 // If the app name is absolute, then we need to use its dirname as the cwd
889 if (app_name_is_absolute) {
890 cwd_path_w_needs_free = true;
891 const dir = fs.path.dirname(app_name_wtf8).?;
892 break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, dir);
893 } else if (self.cwd) |cwd| {
894 cwd_path_w_needs_free = true;
895 break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd);
896 } else {
897 break :x &[_:0]u16{}; // empty for cwd
898 }
899 };
900 defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w);
901
902 // If the app name has more than just a filename, then we need to separate that
903 // into the basename and dirname and use the dirname as an addition to the cwd
904 // path. This is because NtQueryDirectoryFile cannot accept FileName params with
905 // path separators.
906 const app_basename_wtf8 = fs.path.basename(app_name_wtf8);
907 // If the app name is absolute, then the cwd will already have the app's dirname in it,
908 // so only populate app_dirname if app name is a relative path with > 0 path separators.
909 const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_wtf8) else null;
910 const app_dirname_w: ?[:0]u16 = x: {
911 if (maybe_app_dirname_wtf8) |app_dirname_wtf8| {
912 break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_dirname_wtf8);
913 }
914 break :x null;
915 };
916 defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?);
917
918 const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8);
919 defer self.allocator.free(app_name_w);
920
921 const flags: windows.CreateProcessFlags = .{
922 .create_suspended = self.start_suspended,
923 .create_unicode_environment = true,
924 .create_no_window = self.create_no_window,
925 };
926
927 run: {
928 const PATH: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{};
929 const PATHEXT: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{};
930
931 // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules
932 // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously
933 // constructed arguments.
934 //
935 // We'll need to wait until we're actually trying to run the command to know for sure
936 // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually
937 // serializing the command line until we determine how it should be serialized.
938 var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv);
939 defer cmd_line_cache.deinit();
940
941 var app_buf: ArrayList(u16) = .empty;
942 defer app_buf.deinit(self.allocator);
943
944 try app_buf.appendSlice(self.allocator, app_name_w);
945
946 var dir_buf: ArrayList(u16) = .empty;
947 defer dir_buf.deinit(self.allocator);
948
949 if (cwd_path_w.len > 0) {
950 try dir_buf.appendSlice(self.allocator, cwd_path_w);
951 }
952 if (app_dirname_w) |app_dir| {
953 if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep);
954 try dir_buf.appendSlice(self.allocator, app_dir);
955 }
956
957 windowsCreateProcessPathExt(self.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo) catch |no_path_err| {
958 const original_err = switch (no_path_err) {
959 // argv[0] contains unsupported characters that will never resolve to a valid exe.
960 error.InvalidArg0 => return error.FileNotFound,
961 error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e,
962 error.UnrecoverableInvalidExe => return error.InvalidExe,
963 else => |e| return e,
964 };
965
966 // If the app name had path separators, that disallows PATH searching,
967 // and there's no need to search the PATH if the app name is absolute.
968 // We still search the path if the cwd is absolute because of the
969 // "cwd set in Child is in effect when choosing the executable path
970 // to match posix semantics" behavior--we don't want to skip searching
971 // the PATH just because we were trying to set the cwd of the child process.
972 if (app_dirname_w != null or app_name_is_absolute) {
973 return original_err;
974 }
975
976 var it = mem.tokenizeScalar(u16, PATH, ';');
977 while (it.next()) |search_path| {
978 dir_buf.clearRetainingCapacity();
979 try dir_buf.appendSlice(self.allocator, search_path);
980
981 if (windowsCreateProcessPathExt(self.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo)) {
982 break :run;
983 } else |err| switch (err) {
984 // argv[0] contains unsupported characters that will never resolve to a valid exe.
985 error.InvalidArg0 => return error.FileNotFound,
986 error.FileNotFound, error.AccessDenied, error.InvalidExe => continue,
987 error.UnrecoverableInvalidExe => return error.InvalidExe,
988 else => |e| return e,
989 }
990 } else {
991 return original_err;
992 }
993 };
994 }
995
996 if (g_hChildStd_IN_Wr) |h| {
997 self.stdin = File{ .handle = h };
998 } else {
999 self.stdin = null;
1000 }
1001 if (g_hChildStd_OUT_Rd) |h| {
1002 self.stdout = File{ .handle = h };
1003 } else {
1004 self.stdout = null;
1005 }
1006 if (g_hChildStd_ERR_Rd) |h| {
1007 self.stderr = File{ .handle = h };
1008 } else {
1009 self.stderr = null;
1010 }
1011
1012 self.id = piProcInfo.hProcess;
1013 self.thread_handle = piProcInfo.hThread;
1014 self.term = null;
1015
1016 if (self.stdin_behavior == StdIo.Pipe) {
1017 posix.close(g_hChildStd_IN_Rd.?);
1018 }
1019 if (self.stderr_behavior == StdIo.Pipe) {
1020 posix.close(g_hChildStd_ERR_Wr.?);
1021 }
1022 if (self.stdout_behavior == StdIo.Pipe) {
1023 posix.close(g_hChildStd_OUT_Wr.?);
1024 }
1025}
1026
1027fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void {
1028 switch (stdio) {
1029 .Pipe => try posix.dup2(pipe_fd, std_fileno),
1030 .Close => posix.close(std_fileno),
1031 .Inherit => {},
1032 .Ignore => try posix.dup2(dev_null_fd, std_fileno),
1033 }
1034}
1035
1036fn destroyPipe(pipe: [2]posix.fd_t) void {
1037 if (pipe[0] != -1) posix.close(pipe[0]);
1038 if (pipe[0] != pipe[1]) posix.close(pipe[1]);
1039}
1040
1041// Child of fork calls this to report an error to the fork parent.
1042// Then the child exits.
1043fn forkChildErrReport(io: Io, fd: i32, err: Child.SpawnError) noreturn {
1044 writeIntFd(io, fd, @as(ErrInt, @intFromError(err))) catch {};
1045 // If we're linking libc, some naughty applications may have registered atexit handlers
1046 // which we really do not want to run in the fork child. I caught LLVM doing this and
1047 // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne,
1048 // "Why'd you have to go and make things so complicated?"
1049 if (builtin.link_libc) {
1050 // The _exit(2) function does nothing but make the exit syscall, unlike exit(3)
1051 std.c._exit(1);
1052 }
1053 posix.system.exit(1);
1054}
1055
1056fn writeIntFd(io: Io, fd: i32, value: ErrInt) !void {
1057 var buffer: [8]u8 = undefined;
1058 var fw: File.Writer = .initStreaming(.{ .handle = fd }, io, &buffer);
1059 fw.interface.writeInt(u64, value, .little) catch unreachable;
1060 fw.interface.flush() catch return error.SystemResources;
1061}
1062
1063fn readIntFd(fd: i32) !ErrInt {
1064 var buffer: [8]u8 = undefined;
1065 var i: usize = 0;
1066 while (i < buffer.len) {
1067 const n = try std.posix.read(fd, buffer[i..]);
1068 if (n == 0) return error.EndOfStream;
1069 i += n;
1070 }
1071 const int = mem.readInt(u64, &buffer, .little);
1072 return @intCast(int);
1073}
1074
1075const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8);
1076
1077/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
1078/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
1079/// Note: `app_buf` should not contain any leading path separators.
1080/// Note: If the dir is the cwd, dir_buf should be empty (len = 0).
1081fn windowsCreateProcessPathExt(
1082 allocator: Allocator,
1083 io: Io,
1084 dir_buf: *ArrayList(u16),
1085 app_buf: *ArrayList(u16),
1086 pathext: [:0]const u16,
1087 cmd_line_cache: *WindowsCommandLineCache,
1088 envp_ptr: ?[*]u16,
1089 cwd_ptr: ?[*:0]u16,
1090 flags: windows.CreateProcessFlags,
1091 lpStartupInfo: *windows.STARTUPINFOW,
1092 lpProcessInformation: *windows.PROCESS_INFORMATION,
1093) !void {
1094 const app_name_len = app_buf.items.len;
1095 const dir_path_len = dir_buf.items.len;
1096
1097 if (app_name_len == 0) return error.FileNotFound;
1098
1099 defer app_buf.shrinkRetainingCapacity(app_name_len);
1100 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
1101
1102 // The name of the game here is to avoid CreateProcessW calls at all costs,
1103 // and only ever try calling it when we have a real candidate for execution.
1104 // Secondarily, we want to minimize the number of syscalls used when checking
1105 // for each PATHEXT-appended version of the app name.
1106 //
1107 // An overview of the technique used:
1108 // - Open the search directory for iteration (either cwd or a path from PATH)
1109 // - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to
1110 // check if anything that could possibly match either the unappended version
1111 // of the app name or any of the versions with a PATHEXT value appended exists.
1112 // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early
1113 // without needing to use PATHEXT at all.
1114 //
1115 // This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence
1116 // for any directory that doesn't contain any possible matches, instead of having
1117 // to use a separate look up for each individual filename combination (unappended +
1118 // each PATHEXT appended). For directories where the wildcard *does* match something,
1119 // we iterate the matches and take note of any that are either the unappended version,
1120 // or a version with a supported PATHEXT appended. We then try calling CreateProcessW
1121 // with the found versions in the appropriate order.
1122
1123 // In the future, child process execution needs to move to Io implementation.
1124 // Under those conditions, here we will have access to lower level directory
1125 // opening function knowing which implementation we are in. Here, we imitate
1126 // that scenario.
1127 var dir = dir: {
1128 // needs to be null-terminated
1129 try dir_buf.append(allocator, 0);
1130 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
1131 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1132 const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);
1133 break :dir Io.Threaded.dirOpenDirWindows(.cwd(), prefixed_path.span(), .{
1134 .iterate = true,
1135 }) catch return error.FileNotFound;
1136 };
1137 defer dir.close(io);
1138
1139 // Add wildcard and null-terminator
1140 try app_buf.append(allocator, '*');
1141 try app_buf.append(allocator, 0);
1142 const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0];
1143
1144 // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries
1145 // returned per NtQueryDirectoryFile call.
1146 var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined;
1147 const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2);
1148 if (file_information_buf.len < file_info_maximum_single_entry_size) {
1149 @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry");
1150 }
1151 var io_status: windows.IO_STATUS_BLOCK = undefined;
1152
1153 const num_supported_pathext = @typeInfo(WindowsExtension).@"enum".fields.len;
1154 var pathext_seen = [_]bool{false} ** num_supported_pathext;
1155 var any_pathext_seen = false;
1156 var unappended_exists = false;
1157
1158 // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions
1159 // of the app_name we should try to spawn.
1160 // Note: This is necessary because the order of the files returned is filesystem-dependent:
1161 // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists.
1162 // On FAT32, it's possible for something like `blah.exe.obj` to be returned first.
1163 while (true) {
1164 const app_name_len_bytes = std.math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong;
1165 var app_name_unicode_string = windows.UNICODE_STRING{
1166 .Length = app_name_len_bytes,
1167 .MaximumLength = app_name_len_bytes,
1168 .Buffer = @constCast(app_name_wildcard.ptr),
1169 };
1170 const rc = windows.ntdll.NtQueryDirectoryFile(
1171 dir.handle,
1172 null,
1173 null,
1174 null,
1175 &io_status,
1176 &file_information_buf,
1177 file_information_buf.len,
1178 .Directory,
1179 windows.FALSE, // single result
1180 &app_name_unicode_string,
1181 windows.FALSE, // restart iteration
1182 );
1183
1184 // If we get nothing with the wildcard, then we can just bail out
1185 // as we know appending PATHEXT will not yield anything.
1186 switch (rc) {
1187 .SUCCESS => {},
1188 .NO_SUCH_FILE => return error.FileNotFound,
1189 .NO_MORE_FILES => break,
1190 .ACCESS_DENIED => return error.AccessDenied,
1191 else => return windows.unexpectedStatus(rc),
1192 }
1193
1194 // According to the docs, this can only happen if there is not enough room in the
1195 // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry.
1196 // Therefore, this condition should not be possible to hit with the buffer size we use.
1197 std.debug.assert(io_status.Information != 0);
1198
1199 var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf };
1200 while (it.next()) |info| {
1201 // Skip directories
1202 if (info.FileAttributes.DIRECTORY) continue;
1203 const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2];
1204 // Because all results start with the app_name since we're using the wildcard `app_name*`,
1205 // if the length is equal to app_name then this is an exact match
1206 if (filename.len == app_name_len) {
1207 // Note: We can't break early here because it's possible that the unappended version
1208 // fails to spawn, in which case we still want to try the PATHEXT appended versions.
1209 unappended_exists = true;
1210 } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| {
1211 pathext_seen[@intFromEnum(pathext_ext)] = true;
1212 any_pathext_seen = true;
1213 }
1214 }
1215 }
1216
1217 const unappended_err = unappended: {
1218 if (unappended_exists) {
1219 if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
1220 '/', '\\' => {},
1221 else => try dir_buf.append(allocator, fs.path.sep),
1222 };
1223 try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
1224 try dir_buf.append(allocator, 0);
1225 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1226
1227 const is_bat_or_cmd = bat_or_cmd: {
1228 const app_name = app_buf.items[0..app_name_len];
1229 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false;
1230 const ext = app_name[ext_start..];
1231 const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false;
1232 switch (ext_enum) {
1233 .cmd, .bat => break :bat_or_cmd true,
1234 else => break :bat_or_cmd false,
1235 }
1236 };
1237 const cmd_line_w = if (is_bat_or_cmd)
1238 try cmd_line_cache.scriptCommandLine(full_app_name)
1239 else
1240 try cmd_line_cache.commandLine();
1241 const app_name_w = if (is_bat_or_cmd)
1242 try cmd_line_cache.cmdExePath()
1243 else
1244 full_app_name;
1245
1246 if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
1247 return;
1248 } else |err| switch (err) {
1249 error.FileNotFound,
1250 error.AccessDenied,
1251 => break :unappended err,
1252 error.InvalidExe => {
1253 // On InvalidExe, if the extension of the app name is .exe then
1254 // it's treated as an unrecoverable error. Otherwise, it'll be
1255 // skipped as normal.
1256 const app_name = app_buf.items[0..app_name_len];
1257 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
1258 const ext = app_name[ext_start..];
1259 if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1260 return error.UnrecoverableInvalidExe;
1261 }
1262 break :unappended err;
1263 },
1264 else => return err,
1265 }
1266 }
1267 break :unappended error.FileNotFound;
1268 };
1269
1270 if (!any_pathext_seen) return unappended_err;
1271
1272 // Now try any PATHEXT appended versions that we've seen
1273 var ext_it = mem.tokenizeScalar(u16, pathext, ';');
1274 while (ext_it.next()) |ext| {
1275 const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue;
1276 if (!pathext_seen[@intFromEnum(ext_enum)]) continue;
1277
1278 dir_buf.shrinkRetainingCapacity(dir_path_len);
1279 if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
1280 '/', '\\' => {},
1281 else => try dir_buf.append(allocator, fs.path.sep),
1282 };
1283 try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
1284 try dir_buf.appendSlice(allocator, ext);
1285 try dir_buf.append(allocator, 0);
1286 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1287
1288 const is_bat_or_cmd = switch (ext_enum) {
1289 .cmd, .bat => true,
1290 else => false,
1291 };
1292 const cmd_line_w = if (is_bat_or_cmd)
1293 try cmd_line_cache.scriptCommandLine(full_app_name)
1294 else
1295 try cmd_line_cache.commandLine();
1296 const app_name_w = if (is_bat_or_cmd)
1297 try cmd_line_cache.cmdExePath()
1298 else
1299 full_app_name;
1300
1301 if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
1302 return;
1303 } else |err| switch (err) {
1304 error.FileNotFound => continue,
1305 error.AccessDenied => continue,
1306 error.InvalidExe => {
1307 // On InvalidExe, if the extension of the app name is .exe then
1308 // it's treated as an unrecoverable error. Otherwise, it'll be
1309 // skipped as normal.
1310 if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1311 return error.UnrecoverableInvalidExe;
1312 }
1313 continue;
1314 },
1315 else => return err,
1316 }
1317 }
1318
1319 return unappended_err;
1320}
1321
1322fn windowsCreateProcess(
1323 app_name: [*:0]u16,
1324 cmd_line: [*:0]u16,
1325 envp_ptr: ?[*]u16,
1326 cwd_ptr: ?[*:0]u16,
1327 flags: windows.CreateProcessFlags,
1328 lpStartupInfo: *windows.STARTUPINFOW,
1329 lpProcessInformation: *windows.PROCESS_INFORMATION,
1330) !void {
1331 // TODO the docs for environment pointer say:
1332 // > A pointer to the environment block for the new process. If this parameter
1333 // > is NULL, the new process uses the environment of the calling process.
1334 // > ...
1335 // > An environment block can contain either Unicode or ANSI characters. If
1336 // > the environment block pointed to by lpEnvironment contains Unicode
1337 // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
1338 // > If this parameter is NULL and the environment block of the parent process
1339 // > contains Unicode characters, you must also ensure that dwCreationFlags
1340 // > includes CREATE_UNICODE_ENVIRONMENT.
1341 // This seems to imply that we have to somehow know whether our process parent passed
1342 // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
1343 // Since we do not know this information that would imply that we must not pass NULL
1344 // for the parameter.
1345 // However this would imply that programs compiled with -DUNICODE could not pass
1346 // environment variables to programs that were not, which seems unlikely.
1347 // More investigation is needed.
1348 return windows.CreateProcessW(
1349 app_name,
1350 cmd_line,
1351 null,
1352 null,
1353 windows.TRUE,
1354 flags,
1355 @as(?*anyopaque, @ptrCast(envp_ptr)),
1356 cwd_ptr,
1357 lpStartupInfo,
1358 lpProcessInformation,
1359 );
1360}
1361
1362fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1363 var rd_h: windows.HANDLE = undefined;
1364 var wr_h: windows.HANDLE = undefined;
1365 try windows.CreatePipe(&rd_h, &wr_h, sattr);
1366 errdefer windowsDestroyPipe(rd_h, wr_h);
1367 try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0);
1368 rd.* = rd_h;
1369 wr.* = wr_h;
1370}
1371
1372fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void {
1373 if (rd) |h| posix.close(h);
1374 if (wr) |h| posix.close(h);
1375}
1376
1377fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1378 var tmp_bufw: [128]u16 = undefined;
1379
1380 // Anonymous pipes are built upon Named pipes.
1381 // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe
1382 // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes.
1383 // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations
1384 const pipe_path = blk: {
1385 var tmp_buf: [128]u8 = undefined;
1386 // Forge a random path for the pipe.
1387 const pipe_path = std.fmt.bufPrintSentinel(
1388 &tmp_buf,
1389 "\\\\.\\pipe\\zig-childprocess-{d}-{d}",
1390 .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) },
1391 0,
1392 ) catch unreachable;
1393 const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable;
1394 tmp_bufw[len] = 0;
1395 break :blk tmp_bufw[0..len :0];
1396 };
1397
1398 // Create the read handle that can be used with overlapped IO ops.
1399 const read_handle = windows.kernel32.CreateNamedPipeW(
1400 pipe_path.ptr,
1401 windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED,
1402 windows.PIPE_TYPE_BYTE,
1403 1,
1404 4096,
1405 4096,
1406 0,
1407 sattr,
1408 );
1409 if (read_handle == windows.INVALID_HANDLE_VALUE) {
1410 switch (windows.GetLastError()) {
1411 else => |err| return windows.unexpectedError(err),
1412 }
1413 }
1414 errdefer posix.close(read_handle);
1415
1416 var sattr_copy = sattr.*;
1417 const write_handle = windows.kernel32.CreateFileW(
1418 pipe_path.ptr,
1419 .{ .GENERIC = .{ .WRITE = true } },
1420 0,
1421 &sattr_copy,
1422 windows.OPEN_EXISTING,
1423 @bitCast(windows.FILE.ATTRIBUTE{ .NORMAL = true }),
1424 null,
1425 );
1426 if (write_handle == windows.INVALID_HANDLE_VALUE) {
1427 switch (windows.GetLastError()) {
1428 else => |err| return windows.unexpectedError(err),
1429 }
1430 }
1431 errdefer posix.close(write_handle);
1432
1433 try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
1434
1435 rd.* = read_handle;
1436 wr.* = write_handle;
1437}
1438
1439var pipe_name_counter = std.atomic.Value(u32).init(1);
1440
1441/// File name extensions supported natively by `CreateProcess()` on Windows.
1442// Should be kept in sync with `windowsCreateProcessSupportsExtension`.
1443pub const WindowsExtension = enum {
1444 bat,
1445 cmd,
1446 com,
1447 exe,
1448};
1449
1450/// Case-insensitive WTF-16 lookup
1451fn windowsCreateProcessSupportsExtension(ext: []const u16) ?WindowsExtension {
1452 if (ext.len != 4) return null;
1453 const State = enum {
1454 start,
1455 dot,
1456 b,
1457 ba,
1458 c,
1459 cm,
1460 co,
1461 e,
1462 ex,
1463 };
1464 var state: State = .start;
1465 for (ext) |c| switch (state) {
1466 .start => switch (c) {
1467 '.' => state = .dot,
1468 else => return null,
1469 },
1470 .dot => switch (c) {
1471 'b', 'B' => state = .b,
1472 'c', 'C' => state = .c,
1473 'e', 'E' => state = .e,
1474 else => return null,
1475 },
1476 .b => switch (c) {
1477 'a', 'A' => state = .ba,
1478 else => return null,
1479 },
1480 .c => switch (c) {
1481 'm', 'M' => state = .cm,
1482 'o', 'O' => state = .co,
1483 else => return null,
1484 },
1485 .e => switch (c) {
1486 'x', 'X' => state = .ex,
1487 else => return null,
1488 },
1489 .ba => switch (c) {
1490 't', 'T' => return .bat,
1491 else => return null,
1492 },
1493 .cm => switch (c) {
1494 'd', 'D' => return .cmd,
1495 else => return null,
1496 },
1497 .co => switch (c) {
1498 'm', 'M' => return .com,
1499 else => return null,
1500 },
1501 .ex => switch (c) {
1502 'e', 'E' => return .exe,
1503 else => return null,
1504 },
1505 };
1506 return null;
1507}
1508
1509test windowsCreateProcessSupportsExtension {
1510 try std.testing.expectEqual(WindowsExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?);
1511 try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null);
1512}
1513
1514/// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW.
1515///
1516/// Serialization is done on-demand and the result is cached in order to allow for:
1517/// - Only serializing the particular type of command line needed (`.bat`/`.cmd`
1518/// command line serialization is different from `.exe`/etc)
1519/// - Reusing the serialized command lines if necessary (i.e. if the execution
1520/// of a command fails and the PATH is going to be continued to be searched
1521/// for more candidates)
1522const WindowsCommandLineCache = struct {
1523 cmd_line: ?[:0]u16 = null,
1524 script_cmd_line: ?[:0]u16 = null,
1525 cmd_exe_path: ?[:0]u16 = null,
1526 argv: []const []const u8,
1527 allocator: Allocator,
1528
1529 fn init(allocator: Allocator, argv: []const []const u8) WindowsCommandLineCache {
1530 return .{
1531 .allocator = allocator,
1532 .argv = argv,
1533 };
1534 }
1535
1536 fn deinit(self: *WindowsCommandLineCache) void {
1537 if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line);
1538 if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line);
1539 if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path);
1540 }
1541
1542 fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 {
1543 if (self.cmd_line == null) {
1544 self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv);
1545 }
1546 return self.cmd_line.?;
1547 }
1548
1549 /// Not cached, since the path to the batch script will change during PATH searching.
1550 /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched,
1551 /// then script_path should include both the search path and the script filename
1552 /// (this allows avoiding cmd.exe having to search the PATH again).
1553 fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 {
1554 if (self.script_cmd_line) |v| self.allocator.free(v);
1555 self.script_cmd_line = try argvToScriptCommandLineWindows(
1556 self.allocator,
1557 script_path,
1558 self.argv[1..],
1559 );
1560 return self.script_cmd_line.?;
1561 }
1562
1563 fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 {
1564 if (self.cmd_exe_path == null) {
1565 self.cmd_exe_path = try windowsCmdExePath(self.allocator);
1566 }
1567 return self.cmd_exe_path.?;
1568 }
1569};
1570
1571/// Returns the absolute path of `cmd.exe` within the Windows system directory.
1572/// The caller owns the returned slice.
1573fn windowsCmdExePath(allocator: Allocator) error{ OutOfMemory, Unexpected }![:0]u16 {
1574 var buf = try ArrayList(u16).initCapacity(allocator, 128);
1575 errdefer buf.deinit(allocator);
1576 while (true) {
1577 const unused_slice = buf.unusedCapacitySlice();
1578 // TODO: Get the system directory from PEB.ReadOnlyStaticServerData
1579 const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len));
1580 if (len == 0) {
1581 switch (windows.GetLastError()) {
1582 else => |err| return windows.unexpectedError(err),
1583 }
1584 }
1585 if (len > unused_slice.len) {
1586 try buf.ensureUnusedCapacity(allocator, len);
1587 } else {
1588 buf.items.len = len;
1589 break;
1590 }
1591 }
1592 switch (buf.items[buf.items.len - 1]) {
1593 '/', '\\' => {},
1594 else => try buf.append(allocator, fs.path.sep),
1595 }
1596 try buf.appendSlice(allocator, unicode.utf8ToUtf16LeStringLiteral("cmd.exe"));
1597 return try buf.toOwnedSliceSentinel(allocator, 0);
1598}
1599
1600const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 };
1601
1602/// Serializes `argv` to a Windows command-line string suitable for passing to a child process and
1603/// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice.
1604///
1605/// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts.
1606/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1607///
1608/// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead.
1609fn argvToCommandLineWindows(
1610 allocator: Allocator,
1611 argv: []const []const u8,
1612) ArgvToCommandLineError![:0]u16 {
1613 var buf = std.array_list.Managed(u8).init(allocator);
1614 defer buf.deinit();
1615
1616 if (argv.len != 0) {
1617 const arg0 = argv[0];
1618
1619 // The first argument must be quoted if it contains spaces or ASCII control characters
1620 // (excluding DEL). It also follows special quoting rules where backslashes have no special
1621 // interpretation, which makes it impossible to pass certain first arguments containing
1622 // double quotes to a child process without characters from the first argument leaking into
1623 // subsequent ones (which could have security implications).
1624 //
1625 // Empty arguments technically don't need quotes, but we quote them anyway for maximum
1626 // compatibility with different implementations of the 'CommandLineToArgvW' algorithm.
1627 //
1628 // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject
1629 // all first arguments containing double quotes, even ones that we could theoretically
1630 // serialize in unquoted form.
1631 var needs_quotes = arg0.len == 0;
1632 for (arg0) |c| {
1633 if (c <= ' ') {
1634 needs_quotes = true;
1635 } else if (c == '"') {
1636 return error.InvalidArg0;
1637 }
1638 }
1639 if (needs_quotes) {
1640 try buf.append('"');
1641 try buf.appendSlice(arg0);
1642 try buf.append('"');
1643 } else {
1644 try buf.appendSlice(arg0);
1645 }
1646
1647 for (argv[1..]) |arg| {
1648 try buf.append(' ');
1649
1650 // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes,
1651 // or if they are empty. For simplicity and for maximum compatibility with different
1652 // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII
1653 // control characters (again, excluding DEL).
1654 needs_quotes = for (arg) |c| {
1655 if (c <= ' ' or c == '"') {
1656 break true;
1657 }
1658 } else arg.len == 0;
1659 if (!needs_quotes) {
1660 try buf.appendSlice(arg);
1661 continue;
1662 }
1663
1664 try buf.append('"');
1665 var backslash_count: usize = 0;
1666 for (arg) |byte| {
1667 switch (byte) {
1668 '\\' => {
1669 backslash_count += 1;
1670 },
1671 '"' => {
1672 try buf.appendNTimes('\\', backslash_count * 2 + 1);
1673 try buf.append('"');
1674 backslash_count = 0;
1675 },
1676 else => {
1677 try buf.appendNTimes('\\', backslash_count);
1678 try buf.append(byte);
1679 backslash_count = 0;
1680 },
1681 }
1682 }
1683 try buf.appendNTimes('\\', backslash_count * 2);
1684 try buf.append('"');
1685 }
1686 }
1687
1688 return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
1689}
1690
1691test argvToCommandLineWindows {
1692 const t = testArgvToCommandLineWindows;
1693
1694 try t(&.{
1695 \\C:\Program Files\zig\zig.exe
1696 ,
1697 \\run
1698 ,
1699 \\.\src\main.zig
1700 ,
1701 \\-target
1702 ,
1703 \\x86_64-windows-gnu
1704 ,
1705 \\-O
1706 ,
1707 \\ReleaseSafe
1708 ,
1709 \\--
1710 ,
1711 \\--emoji=🗿
1712 ,
1713 \\--eval=new Regex("Dwayne \"The Rock\" Johnson")
1714 ,
1715 },
1716 \\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=🗿 "--eval=new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")"
1717 );
1718
1719 try t(&.{}, "");
1720 try t(&.{""}, "\"\"");
1721 try t(&.{" "}, "\" \"");
1722 try t(&.{"\t"}, "\"\t\"");
1723 try t(&.{"\x07"}, "\"\x07\"");
1724 try t(&.{"🦎"}, "🦎");
1725
1726 try t(
1727 &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" },
1728 "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee",
1729 );
1730
1731 try t(
1732 &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" },
1733 "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"",
1734 );
1735
1736 try std.testing.expectError(
1737 error.InvalidArg0,
1738 argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}),
1739 );
1740 try std.testing.expectError(
1741 error.InvalidArg0,
1742 argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}),
1743 );
1744 try std.testing.expectError(
1745 error.InvalidArg0,
1746 argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}),
1747 );
1748}
1749
1750fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void {
1751 const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv);
1752 defer std.testing.allocator.free(cmd_line_w);
1753
1754 const cmd_line = try unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w);
1755 defer std.testing.allocator.free(cmd_line);
1756
1757 try std.testing.expectEqualStrings(expected_cmd_line, cmd_line);
1758}
1759
1760const ArgvToScriptCommandLineError = error{
1761 OutOfMemory,
1762 InvalidWtf8,
1763 /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
1764 /// within arguments when executing a `.bat`/`.cmd` script.
1765 /// - NUL/LF signifiies end of arguments, so anything afterwards
1766 /// would be lost after execution.
1767 /// - CR is stripped by `cmd.exe`, so any CR codepoints
1768 /// would be lost after execution.
1769 InvalidBatchScriptArg,
1770};
1771
1772/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific
1773/// escaping rules. The caller owns the returned slice.
1774///
1775/// Escapes `argv` using the suggested mitigation against arbitrary command execution from:
1776/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1777///
1778/// The return of this function will look like
1779/// `cmd.exe /d /e:ON /v:OFF /c "<escaped command line>"`
1780/// and should be used as the `lpCommandLine` of `CreateProcessW`, while the
1781/// return of `windowsCmdExePath` should be used as `lpApplicationName`.
1782///
1783/// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise.
1784/// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem.
1785fn argvToScriptCommandLineWindows(
1786 allocator: Allocator,
1787 /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD.
1788 /// The script must have been verified to exist at this path before calling this function.
1789 script_path: []const u16,
1790 /// Arguments, not including the script name itself. Expected to be encoded as WTF-8.
1791 script_args: []const []const u8,
1792) ArgvToScriptCommandLineError![:0]u16 {
1793 var buf = try std.array_list.Managed(u8).initCapacity(allocator, 64);
1794 defer buf.deinit();
1795
1796 // `/d` disables execution of AutoRun commands.
1797 // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation:
1798 // > If delayed expansion is enabled via the registry value DelayedExpansion,
1799 // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option.
1800 // > Escaping for % requires the command extension to be enabled.
1801 // > If it’s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option.
1802 // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1803 buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \"");
1804
1805 // Always quote the path to the script arg
1806 buf.appendAssumeCapacity('"');
1807 // We always want the path to the batch script to include a path separator in order to
1808 // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary
1809 // command execution mitigation, we just know exactly what script we want to execute
1810 // at this point, and potentially making cmd.exe re-find it is unnecessary.
1811 //
1812 // If the script path does not have a path separator, then we know its relative to CWD and
1813 // we can just put `.\` in the front.
1814 if (mem.findAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) {
1815 try buf.appendSlice(".\\");
1816 }
1817 // Note that we don't do any escaping/mitigations for this argument, since the relevant
1818 // characters (", %, etc) are illegal in file paths and this function should only be called
1819 // with script paths that have been verified to exist.
1820 try unicode.wtf16LeToWtf8ArrayList(&buf, script_path);
1821 buf.appendAssumeCapacity('"');
1822
1823 for (script_args) |arg| {
1824 // Literal carriage returns get stripped when run through cmd.exe
1825 // and NUL/newlines act as 'end of command.' Because of this, it's basically
1826 // always a mistake to include these characters in argv, so it's
1827 // an error condition in order to ensure that the return of this
1828 // function can always roundtrip through cmd.exe.
1829 if (std.mem.findAny(u8, arg, "\x00\r\n") != null) {
1830 return error.InvalidBatchScriptArg;
1831 }
1832
1833 // Separate args with a space.
1834 try buf.append(' ');
1835
1836 // Need to quote if the argument is empty (otherwise the arg would just be lost)
1837 // or if the last character is a `\`, since then something like "%~2" in a .bat
1838 // script would cause the closing " to be escaped which we don't want.
1839 var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\';
1840 if (!needs_quotes) {
1841 for (arg) |c| {
1842 switch (c) {
1843 // Known good characters that don't need to be quoted
1844 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {},
1845 // When in doubt, quote
1846 else => {
1847 needs_quotes = true;
1848 break;
1849 },
1850 }
1851 }
1852 }
1853 if (needs_quotes) {
1854 try buf.append('"');
1855 }
1856 var backslashes: usize = 0;
1857 for (arg) |c| {
1858 switch (c) {
1859 '\\' => {
1860 backslashes += 1;
1861 },
1862 '"' => {
1863 try buf.appendNTimes('\\', backslashes);
1864 try buf.append('"');
1865 backslashes = 0;
1866 },
1867 // Replace `%` with `%%cd:~,%`.
1868 //
1869 // cmd.exe allows extracting a substring from an environment
1870 // variable with the syntax: `%foo:~<start_index>,<end_index>%`.
1871 // Therefore, `%cd:~,%` will always expand to an empty string
1872 // since both the start and end index are blank, and it is assumed
1873 // that `%cd%` is always available since it is a built-in variable
1874 // that corresponds to the current directory.
1875 //
1876 // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%`
1877 // will stop `%foo%` from being expanded and *after* expansion
1878 // we'll still be left with `%foo%` (the literal string).
1879 '%' => {
1880 // the trailing `%` is appended outside the switch
1881 try buf.appendSlice("%%cd:~,");
1882 backslashes = 0;
1883 },
1884 else => {
1885 backslashes = 0;
1886 },
1887 }
1888 try buf.append(c);
1889 }
1890 if (needs_quotes) {
1891 try buf.appendNTimes('\\', backslashes);
1892 try buf.append('"');
1893 }
1894 }
1895
1896 try buf.append('"');
1897
1898 return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
1899}
lib/std/zig/LibCInstallation.zig+22-18
......@@ -268,15 +268,17 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar
268268 dev_null,
269269 });
270270
271 const run_res = std.process.Child.run(gpa, io, .{
272 .argv = argv.items,
271 const run_res = std.process.run(gpa, io, .{
273272 .max_output_bytes = 1024 * 1024,
274 .env_map = &env_map,
275 // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
276 // to their own executable, without even bothering to resolve PATH. This results in the message:
277 // error: unable to execute command: Executable "" doesn't exist!
278 // So we use the expandArg0 variant of ChildProcess to give them a helping hand.
279 .expand_arg0 = .expand,
273 .spawn_options = .{
274 .argv = argv.items,
275 .env_map = &env_map,
276 // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
277 // to their own executable, without even bothering to resolve PATH. This results in the message:
278 // error: unable to execute command: Executable "" doesn't exist!
279 // So we use the expandArg0 variant of ChildProcess to give them a helping hand.
280 .expand_arg0 = .expand,
281 },
280282 }) catch |err| switch (err) {
281283 error.OutOfMemory => return error.OutOfMemory,
282284 else => {
......@@ -289,7 +291,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar
289291 gpa.free(run_res.stderr);
290292 }
291293 switch (run_res.term) {
292 .Exited => |code| if (code != 0) {
294 .exited => |code| if (code != 0) {
293295 printVerboseInvocation(argv.items, null, args.verbose, run_res.stderr);
294296 return error.CCompilerExitCode;
295297 },
......@@ -585,15 +587,17 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8
585587 try appendCcExe(&argv, skip_cc_env_var);
586588 try argv.append(arg1);
587589
588 const run_res = std.process.Child.run(gpa, io, .{
589 .argv = argv.items,
590 const run_res = std.process.run(gpa, io, .{
590591 .max_output_bytes = 1024 * 1024,
591 .env_map = &env_map,
592 // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
593 // to their own executable, without even bothering to resolve PATH. This results in the message:
594 // error: unable to execute command: Executable "" doesn't exist!
595 // So we use the expandArg0 variant of ChildProcess to give them a helping hand.
596 .expand_arg0 = .expand,
592 .spawn_options = .{
593 .argv = argv.items,
594 .env_map = &env_map,
595 // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
596 // to their own executable, without even bothering to resolve PATH. This results in the message:
597 // error: unable to execute command: Executable "" doesn't exist!
598 // So we use the expandArg0 variant of ChildProcess to give them a helping hand.
599 .expand_arg0 = .expand,
600 },
597601 }) catch |err| switch (err) {
598602 error.OutOfMemory => return error.OutOfMemory,
599603 else => return error.UnableToSpawnCCompiler,
......@@ -603,7 +607,7 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8
603607 gpa.free(run_res.stderr);
604608 }
605609 switch (run_res.term) {
606 .Exited => |code| if (code != 0) {
610 .exited => |code| if (code != 0) {
607611 printVerboseInvocation(argv.items, args.search_basename, args.verbose, run_res.stderr);
608612 return error.CCompilerExitCode;
609613 },
lib/std/zig/system/darwin.zig+6-9
......@@ -17,15 +17,15 @@ pub const macos = @import("darwin/macos.zig");
1717///
1818/// If error.OutOfMemory occurs in Allocator, this function returns null.
1919pub fn isSdkInstalled(gpa: Allocator, io: Io) bool {
20 const result = std.process.Child.run(gpa, io, .{
20 const result = std.process.run(gpa, io, .{ .spawn_options = .{
2121 .argv = &.{ "xcode-select", "--print-path" },
22 }) catch return false;
22 } }) catch return false;
2323 defer {
2424 gpa.free(result.stderr);
2525 gpa.free(result.stdout);
2626 }
2727 return switch (result.term) {
28 .Exited => |code| if (code == 0) result.stdout.len > 0 else false,
28 .exited => |code| if (code == 0) result.stdout.len > 0 else false,
2929 else => false,
3030 };
3131}
......@@ -35,7 +35,7 @@ pub fn isSdkInstalled(gpa: Allocator, io: Io) bool {
3535/// Caller owns the memory.
3636/// stderr from xcrun is ignored.
3737/// If error.OutOfMemory occurs in Allocator, this function returns null.
38pub fn getSdk(gpa: Allocator, io: Io, environ: std.process.Child.Environ, target: *const Target) ?[]const u8 {
38pub fn getSdk(gpa: Allocator, io: Io, target: *const Target) ?[]const u8 {
3939 const is_simulator_abi = target.abi == .simulator;
4040 const sdk = switch (target.os.tag) {
4141 .driverkit => "driverkit",
......@@ -47,16 +47,13 @@ pub fn getSdk(gpa: Allocator, io: Io, environ: std.process.Child.Environ, target
4747 else => return null,
4848 };
4949 const argv = &[_][]const u8{ "xcrun", "--sdk", sdk, "--show-sdk-path" };
50 const result = std.process.Child.run(gpa, io, .{
51 .argv = argv,
52 .environ = environ,
53 }) catch return null;
50 const result = std.process.run(gpa, io, .{ .spawn_options = .{ .argv = argv } }) catch return null;
5451 defer {
5552 gpa.free(result.stderr);
5653 gpa.free(result.stdout);
5754 }
5855 switch (result.term) {
59 .Exited => |code| if (code != 0) return null,
56 .exited => |code| if (code != 0) return null,
6057 else => return null,
6158 }
6259 return gpa.dupe(u8, mem.trimEnd(u8, result.stdout, "\r\n")) catch null;
src/Compilation.zig+16-12
......@@ -6339,15 +6339,15 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
63396339 if (std.process.can_spawn) {
63406340 var child = std.process.Child.init(argv.items, arena);
63416341 if (comp.clang_passthrough_mode) {
6342 child.stdin_behavior = .Inherit;
6343 child.stdout_behavior = .Inherit;
6344 child.stderr_behavior = .Inherit;
6342 child.stdin_behavior = .inherit;
6343 child.stdout_behavior = .inherit;
6344 child.stderr_behavior = .inherit;
63456345
63466346 const term = child.spawnAndWait(io) catch |err| {
63476347 return comp.failCObj(c_object, "failed to spawn zig clang (passthrough mode) {s}: {s}", .{ argv.items[0], @errorName(err) });
63486348 };
63496349 switch (term) {
6350 .Exited => |code| {
6350 .exited => |code| {
63516351 if (code != 0) {
63526352 std.process.exit(code);
63536353 }
......@@ -6357,9 +6357,9 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
63576357 else => std.process.abort(),
63586358 }
63596359 } else {
6360 child.stdin_behavior = .Ignore;
6361 child.stdout_behavior = .Ignore;
6362 child.stderr_behavior = .Pipe;
6360 child.stdin_behavior = .ignore;
6361 child.stdout_behavior = .ignore;
6362 child.stderr_behavior = .pipe;
63636363
63646364 try child.spawn(io);
63656365
......@@ -6371,7 +6371,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
63716371 };
63726372
63736373 switch (term) {
6374 .Exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| {
6374 .exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| {
63756375 const bundle = CObject.Diag.Bundle.parse(gpa, io, diag_file_path) catch |err| {
63766376 log.err("{}: failed to parse clang diagnostics: {s}", .{ err, stderr });
63776377 return comp.failCObj(c_object, "clang exited with code {d}", .{code});
......@@ -6742,9 +6742,9 @@ fn spawnZigRc(
67426742 defer node_name.deinit(arena);
67436743
67446744 var child = std.process.Child.init(argv, arena);
6745 child.stdin_behavior = .Ignore;
6746 child.stdout_behavior = .Pipe;
6747 child.stderr_behavior = .Pipe;
6745 child.stdin_behavior = .ignore;
6746 child.stdout_behavior = .pipe;
6747 child.stderr_behavior = .pipe;
67486748 child.progress_node = child_progress_node;
67496749
67506750 child.spawn(io) catch |err| {
......@@ -6785,12 +6785,16 @@ fn spawnZigRc(
67856785 };
67866786
67876787 switch (term) {
6788 .Exited => |code| {
6788 .exited => |code| {
67896789 if (code != 0) {
67906790 log.err("zig rc failed with stderr:\n{s}", .{stderr.buffered()});
67916791 return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
67926792 }
67936793 },
6794 .signal => |sig| {
6795 log.err("zig rc signaled {t} with stderr:\n{s}", .{ sig, stderr.buffered() });
6796 return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
6797 },
67946798 else => {
67956799 log.err("zig rc terminated with stderr:\n{s}", .{stderr.buffered()});
67966800 return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
src/link/Lld.zig+13-13
......@@ -1606,15 +1606,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi
16061606
16071607 var child = std.process.Child.init(argv, arena);
16081608 const term = (if (comp.clang_passthrough_mode) term: {
1609 child.stdin_behavior = .Inherit;
1610 child.stdout_behavior = .Inherit;
1611 child.stderr_behavior = .Inherit;
1609 child.stdin_behavior = .inherit;
1610 child.stdout_behavior = .inherit;
1611 child.stderr_behavior = .inherit;
16121612
16131613 break :term child.spawnAndWait(io);
16141614 } else term: {
1615 child.stdin_behavior = .Ignore;
1616 child.stdout_behavior = .Ignore;
1617 child.stderr_behavior = .Pipe;
1615 child.stdin_behavior = .ignore;
1616 child.stdout_behavior = .ignore;
1617 child.stderr_behavior = .pipe;
16181618
16191619 child.spawn(io) catch |err| break :term err;
16201620 var stderr_reader = child.stderr.?.readerStreaming(io, &.{});
......@@ -1656,15 +1656,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi
16561656 .{try comp.dirs.local_cache.join(arena, &.{rsp_path})},
16571657 ) }, arena);
16581658 if (comp.clang_passthrough_mode) {
1659 rsp_child.stdin_behavior = .Inherit;
1660 rsp_child.stdout_behavior = .Inherit;
1661 rsp_child.stderr_behavior = .Inherit;
1659 rsp_child.stdin_behavior = .inherit;
1660 rsp_child.stdout_behavior = .inherit;
1661 rsp_child.stderr_behavior = .inherit;
16621662
16631663 break :term rsp_child.spawnAndWait(io) catch |err| break :err err;
16641664 } else {
1665 rsp_child.stdin_behavior = .Ignore;
1666 rsp_child.stdout_behavior = .Ignore;
1667 rsp_child.stderr_behavior = .Pipe;
1665 rsp_child.stdin_behavior = .ignore;
1666 rsp_child.stdout_behavior = .ignore;
1667 rsp_child.stderr_behavior = .pipe;
16681668
16691669 rsp_child.spawn(io) catch |err| break :err err;
16701670 var stderr_reader = rsp_child.stderr.?.readerStreaming(io, &.{});
......@@ -1680,7 +1680,7 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi
16801680
16811681 const diags = &comp.link_diags;
16821682 switch (term) {
1683 .Exited => |code| if (code != 0) {
1683 .exited => |code| if (code != 0) {
16841684 if (comp.clang_passthrough_mode) std.process.exit(code);
16851685 diags.lockAndParseLldStderr(argv[1], stderr);
16861686 return error.LinkFailure;
src/main.zig+12-12
......@@ -4442,9 +4442,9 @@ fn runOrTest(
44424442 } else if (process.can_spawn) {
44434443 var child = std.process.Child.init(argv.items, gpa);
44444444 child.env_map = &env_map;
4445 child.stdin_behavior = .Inherit;
4446 child.stdout_behavior = .Inherit;
4447 child.stderr_behavior = .Inherit;
4445 child.stdin_behavior = .inherit;
4446 child.stdout_behavior = .inherit;
4447 child.stderr_behavior = .inherit;
44484448
44494449 // Here we release all the locks associated with the Compilation so
44504450 // that whatever this child process wants to do won't deadlock.
......@@ -4587,9 +4587,9 @@ fn runOrTestHotSwap(
45874587 else => {
45884588 var child = std.process.Child.init(argv.items, gpa);
45894589
4590 child.stdin_behavior = .Inherit;
4591 child.stdout_behavior = .Inherit;
4592 child.stderr_behavior = .Inherit;
4590 child.stdin_behavior = .inherit;
4591 child.stdout_behavior = .inherit;
4592 child.stderr_behavior = .inherit;
45934593
45944594 try child.spawn(io);
45954595
......@@ -5417,9 +5417,9 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8)
54175417
54185418 if (process.can_spawn) {
54195419 var child = std.process.Child.init(child_argv.items, gpa);
5420 child.stdin_behavior = .Inherit;
5421 child.stdout_behavior = .Inherit;
5422 child.stderr_behavior = .Inherit;
5420 child.stdin_behavior = .inherit;
5421 child.stdout_behavior = .inherit;
5422 child.stderr_behavior = .inherit;
54235423
54245424 const term = t: {
54255425 _ = try io.lockStderr(&.{}, .no_color);
......@@ -5686,9 +5686,9 @@ fn jitCmd(
56865686 }
56875687
56885688 var child = std.process.Child.init(child_argv.items, gpa);
5689 child.stdin_behavior = .Inherit;
5690 child.stdout_behavior = if (options.capture == null) .Inherit else .Pipe;
5691 child.stderr_behavior = .Inherit;
5689 child.stdin_behavior = .inherit;
5690 child.stdout_behavior = if (options.capture == null) .inherit else .pipe;
5691 child.stderr_behavior = .inherit;
56925692
56935693 const term = t: {
56945694 _ = try io.lockStderr(&.{}, .no_color);
test/link/macho.zig+1-1
......@@ -871,7 +871,7 @@ fn testLinkDirectlyCppTbd(b: *Build, opts: Options) *Step {
871871 const io = b.graph.io;
872872 const test_step = addTestStep(b, "link-directly-cpp-tbd", opts);
873873
874 const sdk = std.zig.system.darwin.getSdk(b.allocator, io, .{ .map = &b.graph.env_map }, &opts.target.result) orelse
874 const sdk = std.zig.system.darwin.getSdk(b.allocator, io, &opts.target.result) orelse
875875 @panic("macOS SDK is required to run the test");
876876
877877 const exe = addExecutable(b, opts, .{
test/src/Debugger.zig+1-1
......@@ -2306,7 +2306,7 @@ fn addTest(
23062306 run.addArgs(db_argv2);
23072307 run.addArtifactArg(exe);
23082308 for (expected_output) |expected| run.addCheck(.{ .expect_stdout_match = db.b.fmt("{s}\n", .{expected}) });
2309 run.addCheck(.{ .expect_term = .{ .Exited = success } });
2309 run.addCheck(.{ .expect_term = .{ .exited = success } });
23102310 run.setStdIn(.{ .bytes = "" });
23112311 db.root_step.dependOn(&run.step);
23122312}
test/src/StackTrace.zig+3-3
......@@ -193,9 +193,9 @@ fn addCaseInstance(
193193 run.removeEnvironmentVariable("CLICOLOR_FORCE");
194194 run.setEnvironmentVariable("NO_COLOR", "1");
195195 run.addCheck(.{ .expect_term = term: {
196 if (!expect_panic) break :term .{ .Exited = 0 };
197 if (target.result.os.tag == .windows) break :term .{ .Exited = 3 };
198 break :term .{ .Signal = 6 };
196 if (!expect_panic) break :term .{ .exited = 0 };
197 if (target.result.os.tag == .windows) break :term .{ .exited = 3 };
198 break :term .{ .signal = @enumFromInt(6) };
199199 } });
200200 run.expectStdOutEqual("");
201201
test/standalone/child_process/main.zig+3-3
......@@ -26,9 +26,9 @@ pub fn main() !void {
2626 const io = threaded.io();
2727
2828 var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa);
29 child.stdin_behavior = .Pipe;
30 child.stdout_behavior = .Pipe;
31 child.stderr_behavior = .Inherit;
29 child.stdin_behavior = .pipe;
30 child.stdout_behavior = .pipe;
31 child.stderr_behavior = .inherit;
3232 try child.spawn(io);
3333 const child_stdin = child.stdin.?;
3434 try child_stdin.writeStreamingAll(io, "hello from stdin"); // verified in child
test/standalone/ios/build.zig+1-1
......@@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void {
2525
2626 const io = b.graph.io;
2727
28 if (std.zig.system.darwin.getSdk(b.allocator, io, .{ .map = &b.graph.env_map }, &target.result)) |sdk| {
28 if (std.zig.system.darwin.getSdk(b.allocator, io, &target.result)) |sdk| {
2929 b.sysroot = sdk;
3030 exe.root_module.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/include" }) });
3131 exe.root_module.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/System/Library/Frameworks" }) });
test/standalone/simple/hello_world/hello.zig+2-12
......@@ -1,15 +1,5 @@
11const std = @import("std");
22
3// See https://github.com/ziglang/zig/issues/24510
4// for the plan to simplify this code.
5pub fn main() !void {
6 var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
7 defer _ = debug_allocator.deinit();
8 const gpa = debug_allocator.allocator();
9
10 var threaded: std.Io.Threaded = .init(gpa, .{});
11 defer threaded.deinit();
12 const io = threaded.io();
13
14 try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n");
3pub fn main(init: std.process.Init) !void {
4 try std.Io.File.stdout().writeStreamingAll(init.io, "Hello, World!\n");
155}
test/tests.zig+1-1
......@@ -2718,7 +2718,7 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons
27182718 if (b.enable_wasmtime) run.addArg("-fwasmtime");
27192719 if (b.enable_darling) run.addArg("-fdarling");
27202720
2721 run.addCheck(.{ .expect_term = .{ .Exited = 0 } });
2721 run.addCheck(.{ .expect_term = .{ .exited = 0 } });
27222722
27232723 test_step.dependOn(&run.step);
27242724 }