authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-04-24 13:49:43-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-04-24 13:49:43-07:00
log9d64332a5959b4955fe1a1eac793b48932b4a8a8
tree7849cc351c750a4af890666b4e8d55619d22540d
parenta0f1825c53f5bbc62760b830e8d25499ace5884a
parent422464d54082b6a1fd5e40e4f9c1ec2a96c3f77e
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #19698 from squeek502/windows-batbadbut

std.process.Child: Mitigate arbitrary command execution vulnerability on Windows (BatBadBut)

8 files changed, 709 insertions(+), 18 deletions(-)

lib/std/child_process.zig+276-12
......@@ -136,6 +136,14 @@ pub const ChildProcess = struct {
136136
137137 /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process.
138138 CurrentWorkingDirectoryUnlinked,
139
140 /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
141 /// within arguments when executing a `.bat`/`.cmd` script.
142 /// - NUL/LF signifiies end of arguments, so anything afterwards
143 /// would be lost after execution.
144 /// - CR is stripped by `cmd.exe`, so any CR codepoints
145 /// would be lost after execution.
146 InvalidBatchScriptArg,
139147 } ||
140148 posix.ExecveError ||
141149 posix.SetIdError ||
......@@ -814,17 +822,20 @@ pub const ChildProcess = struct {
814822 const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8);
815823 defer self.allocator.free(app_name_w);
816824
817 const cmd_line_w = argvToCommandLineWindows(self.allocator, self.argv) catch |err| switch (err) {
818 // argv[0] contains unsupported characters that will never resolve to a valid exe.
819 error.InvalidArg0 => return error.FileNotFound,
820 else => |e| return e,
821 };
822 defer self.allocator.free(cmd_line_w);
823
824825 run: {
825826 const PATH: [:0]const u16 = std.process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{};
826827 const PATHEXT: [:0]const u16 = std.process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{};
827828
829 // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules
830 // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously
831 // constructed arguments.
832 //
833 // We'll need to wait until we're actually trying to run the command to know for sure
834 // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually
835 // serializing the command line until we determine how it should be serialized.
836 var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv);
837 defer cmd_line_cache.deinit();
838
828839 var app_buf = std.ArrayListUnmanaged(u16){};
829840 defer app_buf.deinit(self.allocator);
830841
......@@ -846,8 +857,10 @@ pub const ChildProcess = struct {
846857 dir_buf.shrinkRetainingCapacity(normalized_len);
847858 }
848859
849 windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| {
860 windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| {
850861 const original_err = switch (no_path_err) {
862 // argv[0] contains unsupported characters that will never resolve to a valid exe.
863 error.InvalidArg0 => return error.FileNotFound,
851864 error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e,
852865 error.UnrecoverableInvalidExe => return error.InvalidExe,
853866 else => |e| return e,
......@@ -872,9 +885,11 @@ pub const ChildProcess = struct {
872885 const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue;
873886 dir_buf.shrinkRetainingCapacity(normalized_len);
874887
875 if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) {
888 if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) {
876889 break :run;
877890 } else |err| switch (err) {
891 // argv[0] contains unsupported characters that will never resolve to a valid exe.
892 error.InvalidArg0 => return error.FileNotFound,
878893 error.FileNotFound, error.AccessDenied, error.InvalidExe => continue,
879894 error.UnrecoverableInvalidExe => return error.InvalidExe,
880895 else => |e| return e,
......@@ -935,7 +950,7 @@ fn windowsCreateProcessPathExt(
935950 dir_buf: *std.ArrayListUnmanaged(u16),
936951 app_buf: *std.ArrayListUnmanaged(u16),
937952 pathext: [:0]const u16,
938 cmd_line: [*:0]u16,
953 cmd_line_cache: *WindowsCommandLineCache,
939954 envp_ptr: ?[*]u16,
940955 cwd_ptr: ?[*:0]u16,
941956 lpStartupInfo: *windows.STARTUPINFOW,
......@@ -1069,7 +1084,26 @@ fn windowsCreateProcessPathExt(
10691084 try dir_buf.append(allocator, 0);
10701085 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
10711086
1072 if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
1087 const is_bat_or_cmd = bat_or_cmd: {
1088 const app_name = app_buf.items[0..app_name_len];
1089 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false;
1090 const ext = app_name[ext_start..];
1091 const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false;
1092 switch (ext_enum) {
1093 .cmd, .bat => break :bat_or_cmd true,
1094 else => break :bat_or_cmd false,
1095 }
1096 };
1097 const cmd_line_w = if (is_bat_or_cmd)
1098 try cmd_line_cache.scriptCommandLine(full_app_name)
1099 else
1100 try cmd_line_cache.commandLine();
1101 const app_name_w = if (is_bat_or_cmd)
1102 try cmd_line_cache.cmdExePath()
1103 else
1104 full_app_name;
1105
1106 if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
10731107 return;
10741108 } else |err| switch (err) {
10751109 error.FileNotFound,
......@@ -1111,7 +1145,20 @@ fn windowsCreateProcessPathExt(
11111145 try dir_buf.append(allocator, 0);
11121146 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
11131147
1114 if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
1148 const is_bat_or_cmd = switch (ext_enum) {
1149 .cmd, .bat => true,
1150 else => false,
1151 };
1152 const cmd_line_w = if (is_bat_or_cmd)
1153 try cmd_line_cache.scriptCommandLine(full_app_name)
1154 else
1155 try cmd_line_cache.commandLine();
1156 const app_name_w = if (is_bat_or_cmd)
1157 try cmd_line_cache.cmdExePath()
1158 else
1159 full_app_name;
1160
1161 if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
11151162 return;
11161163 } else |err| switch (err) {
11171164 error.FileNotFound => continue,
......@@ -1236,6 +1283,223 @@ test windowsCreateProcessSupportsExtension {
12361283 try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null);
12371284}
12381285
1286/// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW.
1287///
1288/// Serialization is done on-demand and the result is cached in order to allow for:
1289/// - Only serializing the particular type of command line needed (`.bat`/`.cmd`
1290/// command line serialization is different from `.exe`/etc)
1291/// - Reusing the serialized command lines if necessary (i.e. if the execution
1292/// of a command fails and the PATH is going to be continued to be searched
1293/// for more candidates)
1294pub const WindowsCommandLineCache = struct {
1295 cmd_line: ?[:0]u16 = null,
1296 script_cmd_line: ?[:0]u16 = null,
1297 cmd_exe_path: ?[:0]u16 = null,
1298 argv: []const []const u8,
1299 allocator: mem.Allocator,
1300
1301 pub fn init(allocator: mem.Allocator, argv: []const []const u8) WindowsCommandLineCache {
1302 return .{
1303 .allocator = allocator,
1304 .argv = argv,
1305 };
1306 }
1307
1308 pub fn deinit(self: *WindowsCommandLineCache) void {
1309 if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line);
1310 if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line);
1311 if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path);
1312 }
1313
1314 pub fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 {
1315 if (self.cmd_line == null) {
1316 self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv);
1317 }
1318 return self.cmd_line.?;
1319 }
1320
1321 /// Not cached, since the path to the batch script will change during PATH searching.
1322 /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched,
1323 /// then script_path should include both the search path and the script filename
1324 /// (this allows avoiding cmd.exe having to search the PATH again).
1325 pub fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 {
1326 if (self.script_cmd_line) |v| self.allocator.free(v);
1327 self.script_cmd_line = try argvToScriptCommandLineWindows(
1328 self.allocator,
1329 script_path,
1330 self.argv[1..],
1331 );
1332 return self.script_cmd_line.?;
1333 }
1334
1335 pub fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 {
1336 if (self.cmd_exe_path == null) {
1337 self.cmd_exe_path = try windowsCmdExePath(self.allocator);
1338 }
1339 return self.cmd_exe_path.?;
1340 }
1341};
1342
1343pub fn windowsCmdExePath(allocator: mem.Allocator) error{ OutOfMemory, Unexpected }![:0]u16 {
1344 var buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 128);
1345 errdefer buf.deinit(allocator);
1346 while (true) {
1347 const unused_slice = buf.unusedCapacitySlice();
1348 // TODO: Get the system directory from PEB.ReadOnlyStaticServerData
1349 const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len));
1350 if (len == 0) {
1351 switch (windows.kernel32.GetLastError()) {
1352 else => |err| return windows.unexpectedError(err),
1353 }
1354 }
1355 if (len > unused_slice.len) {
1356 try buf.ensureUnusedCapacity(allocator, len);
1357 } else {
1358 buf.items.len = len;
1359 break;
1360 }
1361 }
1362 switch (buf.items[buf.items.len - 1]) {
1363 '/', '\\' => {},
1364 else => try buf.append(allocator, fs.path.sep),
1365 }
1366 try buf.appendSlice(allocator, std.unicode.utf8ToUtf16LeStringLiteral("cmd.exe"));
1367 return try buf.toOwnedSliceSentinel(allocator, 0);
1368}
1369
1370pub const ArgvToScriptCommandLineError = error{
1371 OutOfMemory,
1372 InvalidWtf8,
1373 /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
1374 /// within arguments when executing a `.bat`/`.cmd` script.
1375 /// - NUL/LF signifiies end of arguments, so anything afterwards
1376 /// would be lost after execution.
1377 /// - CR is stripped by `cmd.exe`, so any CR codepoints
1378 /// would be lost after execution.
1379 InvalidBatchScriptArg,
1380};
1381
1382/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific
1383/// escaping rules. The caller owns the returned slice.
1384///
1385/// Escapes `argv` using the suggested mitigation against arbitrary command execution from:
1386/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1387pub fn argvToScriptCommandLineWindows(
1388 allocator: mem.Allocator,
1389 /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD.
1390 /// The script must have been verified to exist at this path before calling this function.
1391 script_path: []const u16,
1392 /// Arguments, not including the script name itself. Expected to be encoded as WTF-8.
1393 script_args: []const []const u8,
1394) ArgvToScriptCommandLineError![:0]u16 {
1395 var buf = try std.ArrayList(u8).initCapacity(allocator, 64);
1396 defer buf.deinit();
1397
1398 // `/d` disables execution of AutoRun commands.
1399 // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation:
1400 // > If delayed expansion is enabled via the registry value DelayedExpansion,
1401 // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option.
1402 // > Escaping for % requires the command extension to be enabled.
1403 // > If it’s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option.
1404 // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1405 buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \"");
1406
1407 // Always quote the path to the script arg
1408 buf.appendAssumeCapacity('"');
1409 // We always want the path to the batch script to include a path separator in order to
1410 // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary
1411 // command execution mitigation, we just know exactly what script we want to execute
1412 // at this point, and potentially making cmd.exe re-find it is unnecessary.
1413 //
1414 // If the script path does not have a path separator, then we know its relative to CWD and
1415 // we can just put `.\` in the front.
1416 if (mem.indexOfAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) {
1417 try buf.appendSlice(".\\");
1418 }
1419 // Note that we don't do any escaping/mitigations for this argument, since the relevant
1420 // characters (", %, etc) are illegal in file paths and this function should only be called
1421 // with script paths that have been verified to exist.
1422 try std.unicode.wtf16LeToWtf8ArrayList(&buf, script_path);
1423 buf.appendAssumeCapacity('"');
1424
1425 for (script_args) |arg| {
1426 // Literal carriage returns get stripped when run through cmd.exe
1427 // and NUL/newlines act as 'end of command.' Because of this, it's basically
1428 // always a mistake to include these characters in argv, so it's
1429 // an error condition in order to ensure that the return of this
1430 // function can always roundtrip through cmd.exe.
1431 if (std.mem.indexOfAny(u8, arg, "\x00\r\n") != null) {
1432 return error.InvalidBatchScriptArg;
1433 }
1434
1435 // Separate args with a space.
1436 try buf.append(' ');
1437
1438 // Need to quote if the argument is empty (otherwise the arg would just be lost)
1439 // or if the last character is a `\`, since then something like "%~2" in a .bat
1440 // script would cause the closing " to be escaped which we don't want.
1441 var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\';
1442 if (!needs_quotes) {
1443 for (arg) |c| {
1444 switch (c) {
1445 // Known good characters that don't need to be quoted
1446 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {},
1447 // When in doubt, quote
1448 else => {
1449 needs_quotes = true;
1450 break;
1451 },
1452 }
1453 }
1454 }
1455 if (needs_quotes) {
1456 try buf.append('"');
1457 }
1458 var backslashes: usize = 0;
1459 for (arg) |c| {
1460 switch (c) {
1461 '\\' => {
1462 backslashes += 1;
1463 },
1464 '"' => {
1465 try buf.appendNTimes('\\', backslashes);
1466 try buf.append('"');
1467 backslashes = 0;
1468 },
1469 // Replace `%` with `%%cd:~,%`.
1470 //
1471 // cmd.exe allows extracting a substring from an environment
1472 // variable with the syntax: `%foo:~<start_index>,<end_index>%`.
1473 // Therefore, `%cd:~,%` will always expand to an empty string
1474 // since both the start and end index are blank, and it is assumed
1475 // that `%cd%` is always available since it is a built-in variable
1476 // that corresponds to the current directory.
1477 //
1478 // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%`
1479 // will stop `%foo%` from being expanded and *after* expansion
1480 // we'll still be left with `%foo%` (the literal string).
1481 '%' => {
1482 // the trailing `%` is appended outside the switch
1483 try buf.appendSlice("%%cd:~,");
1484 backslashes = 0;
1485 },
1486 else => {
1487 backslashes = 0;
1488 },
1489 }
1490 try buf.append(c);
1491 }
1492 if (needs_quotes) {
1493 try buf.appendNTimes('\\', backslashes);
1494 try buf.append('"');
1495 }
1496 }
1497
1498 try buf.append('"');
1499
1500 return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
1501}
1502
12391503pub const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 };
12401504
12411505/// Serializes `argv` to a Windows command-line string suitable for passing to a child process and
lib/std/os/windows/kernel32.zig+2
......@@ -243,6 +243,8 @@ pub extern "kernel32" fn GetSystemInfo(lpSystemInfo: *SYSTEM_INFO) callconv(WINA
243243pub extern "kernel32" fn GetSystemTimeAsFileTime(*FILETIME) callconv(WINAPI) void;
244244pub extern "kernel32" fn IsProcessorFeaturePresent(ProcessorFeature: DWORD) BOOL;
245245
246pub extern "kernel32" fn GetSystemDirectoryW(lpBuffer: LPWSTR, uSize: UINT) callconv(WINAPI) UINT;
247
246248pub extern "kernel32" fn HeapCreate(flOptions: DWORD, dwInitialSize: SIZE_T, dwMaximumSize: SIZE_T) callconv(WINAPI) ?HANDLE;
247249pub extern "kernel32" fn HeapDestroy(hHeap: HANDLE) callconv(WINAPI) BOOL;
248250pub extern "kernel32" fn HeapReAlloc(hHeap: HANDLE, dwFlags: DWORD, lpMem: *anyopaque, dwBytes: SIZE_T) callconv(WINAPI) ?*anyopaque;
lib/std/unicode.zig+64-6
......@@ -934,7 +934,7 @@ fn utf16LeToUtf8ArrayListImpl(
934934 .cannot_encode_surrogate_half => Utf16LeToUtf8AllocError,
935935 .can_encode_surrogate_half => mem.Allocator.Error,
936936})!void {
937 assert(result.capacity >= utf16le.len);
937 assert(result.unusedCapacitySlice().len >= utf16le.len);
938938
939939 var remaining = utf16le;
940940 vectorized: {
......@@ -979,7 +979,7 @@ fn utf16LeToUtf8ArrayListImpl(
979979pub const Utf16LeToUtf8AllocError = mem.Allocator.Error || Utf16LeToUtf8Error;
980980
981981pub fn utf16LeToUtf8ArrayList(result: *std.ArrayList(u8), utf16le: []const u16) Utf16LeToUtf8AllocError!void {
982 try result.ensureTotalCapacityPrecise(utf16le.len);
982 try result.ensureUnusedCapacity(utf16le.len);
983983 return utf16LeToUtf8ArrayListImpl(result, utf16le, .cannot_encode_surrogate_half);
984984}
985985
......@@ -1138,7 +1138,7 @@ test utf16LeToUtf8 {
11381138}
11391139
11401140fn utf8ToUtf16LeArrayListImpl(result: *std.ArrayList(u16), utf8: []const u8, comptime surrogates: Surrogates) !void {
1141 assert(result.capacity >= utf8.len);
1141 assert(result.unusedCapacitySlice().len >= utf8.len);
11421142
11431143 var remaining = utf8;
11441144 vectorized: {
......@@ -1176,7 +1176,7 @@ fn utf8ToUtf16LeArrayListImpl(result: *std.ArrayList(u16), utf8: []const u8, com
11761176}
11771177
11781178pub fn utf8ToUtf16LeArrayList(result: *std.ArrayList(u16), utf8: []const u8) error{ InvalidUtf8, OutOfMemory }!void {
1179 try result.ensureTotalCapacityPrecise(utf8.len);
1179 try result.ensureUnusedCapacity(utf8.len);
11801180 return utf8ToUtf16LeArrayListImpl(result, utf8, .cannot_encode_surrogate_half);
11811181}
11821182
......@@ -1351,6 +1351,64 @@ test utf8ToUtf16LeAllocZ {
13511351 }
13521352}
13531353
1354test "ArrayList functions on a re-used list" {
1355 // utf8ToUtf16LeArrayList
1356 {
1357 var list = std.ArrayList(u16).init(testing.allocator);
1358 defer list.deinit();
1359
1360 const init_slice = utf8ToUtf16LeStringLiteral("abcdefg");
1361 try list.ensureTotalCapacityPrecise(init_slice.len);
1362 list.appendSliceAssumeCapacity(init_slice);
1363
1364 try utf8ToUtf16LeArrayList(&list, "hijklmnopqrstuvwyxz");
1365
1366 try testing.expectEqualSlices(u16, utf8ToUtf16LeStringLiteral("abcdefghijklmnopqrstuvwyxz"), list.items);
1367 }
1368
1369 // utf16LeToUtf8ArrayList
1370 {
1371 var list = std.ArrayList(u8).init(testing.allocator);
1372 defer list.deinit();
1373
1374 const init_slice = "abcdefg";
1375 try list.ensureTotalCapacityPrecise(init_slice.len);
1376 list.appendSliceAssumeCapacity(init_slice);
1377
1378 try utf16LeToUtf8ArrayList(&list, utf8ToUtf16LeStringLiteral("hijklmnopqrstuvwyxz"));
1379
1380 try testing.expectEqualStrings("abcdefghijklmnopqrstuvwyxz", list.items);
1381 }
1382
1383 // wtf8ToWtf16LeArrayList
1384 {
1385 var list = std.ArrayList(u16).init(testing.allocator);
1386 defer list.deinit();
1387
1388 const init_slice = utf8ToUtf16LeStringLiteral("abcdefg");
1389 try list.ensureTotalCapacityPrecise(init_slice.len);
1390 list.appendSliceAssumeCapacity(init_slice);
1391
1392 try wtf8ToWtf16LeArrayList(&list, "hijklmnopqrstuvwyxz");
1393
1394 try testing.expectEqualSlices(u16, utf8ToUtf16LeStringLiteral("abcdefghijklmnopqrstuvwyxz"), list.items);
1395 }
1396
1397 // wtf16LeToWtf8ArrayList
1398 {
1399 var list = std.ArrayList(u8).init(testing.allocator);
1400 defer list.deinit();
1401
1402 const init_slice = "abcdefg";
1403 try list.ensureTotalCapacityPrecise(init_slice.len);
1404 list.appendSliceAssumeCapacity(init_slice);
1405
1406 try wtf16LeToWtf8ArrayList(&list, utf8ToUtf16LeStringLiteral("hijklmnopqrstuvwyxz"));
1407
1408 try testing.expectEqualStrings("abcdefghijklmnopqrstuvwyxz", list.items);
1409 }
1410}
1411
13541412/// Converts a UTF-8 string literal into a UTF-16LE string literal.
13551413pub fn utf8ToUtf16LeStringLiteral(comptime utf8: []const u8) *const [calcUtf16LeLen(utf8) catch |err| @compileError(err):0]u16 {
13561414 return comptime blk: {
......@@ -1685,7 +1743,7 @@ pub const Wtf8Iterator = struct {
16851743};
16861744
16871745pub fn wtf16LeToWtf8ArrayList(result: *std.ArrayList(u8), utf16le: []const u16) mem.Allocator.Error!void {
1688 try result.ensureTotalCapacityPrecise(utf16le.len);
1746 try result.ensureUnusedCapacity(utf16le.len);
16891747 return utf16LeToUtf8ArrayListImpl(result, utf16le, .can_encode_surrogate_half);
16901748}
16911749
......@@ -1714,7 +1772,7 @@ pub fn wtf16LeToWtf8(wtf8: []u8, wtf16le: []const u16) usize {
17141772}
17151773
17161774pub fn wtf8ToWtf16LeArrayList(result: *std.ArrayList(u16), wtf8: []const u8) error{ InvalidWtf8, OutOfMemory }!void {
1717 try result.ensureTotalCapacityPrecise(wtf8.len);
1775 try result.ensureUnusedCapacity(wtf8.len);
17181776 return utf8ToUtf16LeArrayListImpl(result, wtf8, .can_encode_surrogate_half);
17191777}
17201778
test/standalone/build.zig.zon+3
......@@ -107,6 +107,9 @@
107107 .windows_argv = .{
108108 .path = "windows_argv",
109109 },
110 .windows_bat_args = .{
111 .path = "windows_bat_args",
112 },
110113 .self_exe_symlink = .{
111114 .path = "self_exe_symlink",
112115 },
test/standalone/windows_bat_args/build.zig created+58
......@@ -0,0 +1,58 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4pub fn build(b: *std.Build) !void {
5 const test_step = b.step("test", "Test it");
6 b.default_step = test_step;
7
8 const optimize: std.builtin.OptimizeMode = .Debug;
9 const target = b.host;
10
11 if (builtin.os.tag != .windows) return;
12
13 const echo_args = b.addExecutable(.{
14 .name = "echo-args",
15 .root_source_file = b.path("echo-args.zig"),
16 .optimize = optimize,
17 .target = target,
18 });
19
20 const test_exe = b.addExecutable(.{
21 .name = "test",
22 .root_source_file = b.path("test.zig"),
23 .optimize = optimize,
24 .target = target,
25 });
26
27 const run = b.addRunArtifact(test_exe);
28 run.addArtifactArg(echo_args);
29 run.expectExitCode(0);
30 run.skip_foreign_checks = true;
31
32 test_step.dependOn(&run.step);
33
34 const fuzz = b.addExecutable(.{
35 .name = "fuzz",
36 .root_source_file = b.path("fuzz.zig"),
37 .optimize = optimize,
38 .target = target,
39 });
40
41 const fuzz_max_iterations = b.option(u64, "iterations", "The max fuzz iterations (default: 100)") orelse 100;
42 const fuzz_iterations_arg = std.fmt.allocPrint(b.allocator, "{}", .{fuzz_max_iterations}) catch @panic("oom");
43
44 const fuzz_seed = b.option(u64, "seed", "Seed to use for the PRNG (default: random)") orelse seed: {
45 var buf: [8]u8 = undefined;
46 try std.posix.getrandom(&buf);
47 break :seed std.mem.readInt(u64, &buf, builtin.cpu.arch.endian());
48 };
49 const fuzz_seed_arg = std.fmt.allocPrint(b.allocator, "{}", .{fuzz_seed}) catch @panic("oom");
50
51 const fuzz_run = b.addRunArtifact(fuzz);
52 fuzz_run.addArtifactArg(echo_args);
53 fuzz_run.addArgs(&.{ fuzz_iterations_arg, fuzz_seed_arg });
54 fuzz_run.expectExitCode(0);
55 fuzz_run.skip_foreign_checks = true;
56
57 test_step.dependOn(&fuzz_run.step);
58}
test/standalone/windows_bat_args/echo-args.zig created+14
......@@ -0,0 +1,14 @@
1const std = @import("std");
2
3pub fn main() !void {
4 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
5 defer arena_state.deinit();
6 const arena = arena_state.allocator();
7
8 const stdout = std.io.getStdOut().writer();
9 var args = try std.process.argsAlloc(arena);
10 for (args[1..], 1..) |arg, i| {
11 try stdout.writeAll(arg);
12 if (i != args.len - 1) try stdout.writeByte('\x00');
13 }
14}
test/standalone/windows_bat_args/fuzz.zig created+160
......@@ -0,0 +1,160 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4
5pub fn main() anyerror!void {
6 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
7 defer if (gpa.deinit() == .leak) @panic("found memory leaks");
8 const allocator = gpa.allocator();
9
10 var it = try std.process.argsWithAllocator(allocator);
11 defer it.deinit();
12 _ = it.next() orelse unreachable; // skip binary name
13 const child_exe_path = it.next() orelse unreachable;
14
15 const iterations: u64 = iterations: {
16 const arg = it.next() orelse "0";
17 break :iterations try std.fmt.parseUnsigned(u64, arg, 10);
18 };
19
20 var rand_seed = false;
21 const seed: u64 = seed: {
22 const seed_arg = it.next() orelse {
23 rand_seed = true;
24 var buf: [8]u8 = undefined;
25 try std.posix.getrandom(&buf);
26 break :seed std.mem.readInt(u64, &buf, builtin.cpu.arch.endian());
27 };
28 break :seed try std.fmt.parseUnsigned(u64, seed_arg, 10);
29 };
30 var random = std.rand.DefaultPrng.init(seed);
31 const rand = random.random();
32
33 // If the seed was not given via the CLI, then output the
34 // randomly chosen seed so that this run can be reproduced
35 if (rand_seed) {
36 std.debug.print("rand seed: {}\n", .{seed});
37 }
38
39 var tmp = std.testing.tmpDir(.{});
40 defer tmp.cleanup();
41
42 try tmp.dir.setAsCwd();
43 defer tmp.parent_dir.setAsCwd() catch {};
44
45 var buf = try std.ArrayList(u8).initCapacity(allocator, 128);
46 defer buf.deinit();
47 try buf.appendSlice("@echo off\n");
48 try buf.append('"');
49 try buf.appendSlice(child_exe_path);
50 try buf.append('"');
51 const preamble_len = buf.items.len;
52
53 try buf.appendSlice(" %*");
54 try tmp.dir.writeFile("args1.bat", buf.items);
55 buf.shrinkRetainingCapacity(preamble_len);
56
57 try buf.appendSlice(" %1 %2 %3 %4 %5 %6 %7 %8 %9");
58 try tmp.dir.writeFile("args2.bat", buf.items);
59 buf.shrinkRetainingCapacity(preamble_len);
60
61 try buf.appendSlice(" \"%~1\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" \"%~6\" \"%~7\" \"%~8\" \"%~9\"");
62 try tmp.dir.writeFile("args3.bat", buf.items);
63 buf.shrinkRetainingCapacity(preamble_len);
64
65 var i: u64 = 0;
66 while (iterations == 0 or i < iterations) {
67 const rand_arg = try randomArg(allocator, rand);
68 defer allocator.free(rand_arg);
69
70 try testExec(allocator, &.{rand_arg}, null);
71
72 i += 1;
73 }
74}
75
76fn testExec(allocator: std.mem.Allocator, args: []const []const u8, env: ?*std.process.EnvMap) !void {
77 try testExecBat(allocator, "args1.bat", args, env);
78 try testExecBat(allocator, "args2.bat", args, env);
79 try testExecBat(allocator, "args3.bat", args, env);
80}
81
82fn testExecBat(allocator: std.mem.Allocator, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void {
83 var argv = try std.ArrayList([]const u8).initCapacity(allocator, 1 + args.len);
84 defer argv.deinit();
85 argv.appendAssumeCapacity(bat);
86 argv.appendSliceAssumeCapacity(args);
87
88 const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat");
89
90 const result = try std.ChildProcess.run(.{
91 .allocator = allocator,
92 .env_map = env,
93 .argv = argv.items,
94 });
95 defer allocator.free(result.stdout);
96 defer allocator.free(result.stderr);
97
98 try std.testing.expectEqualStrings("", result.stderr);
99 var it = std.mem.splitScalar(u8, result.stdout, '\x00');
100 var i: usize = 0;
101 while (it.next()) |actual_arg| {
102 if (i >= args.len and can_have_trailing_empty_args) {
103 try std.testing.expectEqualStrings("", actual_arg);
104 continue;
105 }
106 const expected_arg = args[i];
107 try std.testing.expectEqualSlices(u8, expected_arg, actual_arg);
108 i += 1;
109 }
110}
111
112fn randomArg(allocator: Allocator, rand: std.rand.Random) ![]const u8 {
113 const Choice = enum {
114 backslash,
115 quote,
116 space,
117 control,
118 printable,
119 surrogate_half,
120 non_ascii,
121 };
122
123 const choices = rand.uintAtMostBiased(u16, 256);
124 var buf = try std.ArrayList(u8).initCapacity(allocator, choices);
125 errdefer buf.deinit();
126
127 var last_codepoint: u21 = 0;
128 for (0..choices) |_| {
129 const choice = rand.enumValue(Choice);
130 const codepoint: u21 = switch (choice) {
131 .backslash => '\\',
132 .quote => '"',
133 .space => ' ',
134 .control => switch (rand.uintAtMostBiased(u8, 0x21)) {
135 // NUL/CR/LF can't roundtrip
136 '\x00', '\r', '\n' => ' ',
137 0x21 => '\x7F',
138 else => |b| b,
139 },
140 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
141 .surrogate_half => rand.intRangeAtMostBiased(u16, 0xD800, 0xDFFF),
142 .non_ascii => rand.intRangeAtMostBiased(u21, 0x80, 0x10FFFF),
143 };
144 // Ensure that we always return well-formed WTF-8.
145 // Instead of concatenating to ensure well-formed WTF-8,
146 // we just skip encoding the low surrogate.
147 if (std.unicode.isSurrogateCodepoint(last_codepoint) and std.unicode.isSurrogateCodepoint(codepoint)) {
148 if (std.unicode.utf16IsHighSurrogate(@intCast(last_codepoint)) and std.unicode.utf16IsLowSurrogate(@intCast(codepoint))) {
149 continue;
150 }
151 }
152 try buf.ensureUnusedCapacity(4);
153 const unused_slice = buf.unusedCapacitySlice();
154 const len = std.unicode.wtf8Encode(codepoint, unused_slice) catch unreachable;
155 buf.items.len += len;
156 last_codepoint = codepoint;
157 }
158
159 return buf.toOwnedSlice();
160}
test/standalone/windows_bat_args/test.zig created+132
......@@ -0,0 +1,132 @@
1const std = @import("std");
2
3pub fn main() anyerror!void {
4 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
5 defer if (gpa.deinit() == .leak) @panic("found memory leaks");
6 const allocator = gpa.allocator();
7
8 var it = try std.process.argsWithAllocator(allocator);
9 defer it.deinit();
10 _ = it.next() orelse unreachable; // skip binary name
11 const child_exe_path = it.next() orelse unreachable;
12
13 var tmp = std.testing.tmpDir(.{});
14 defer tmp.cleanup();
15
16 try tmp.dir.setAsCwd();
17 defer tmp.parent_dir.setAsCwd() catch {};
18
19 var buf = try std.ArrayList(u8).initCapacity(allocator, 128);
20 defer buf.deinit();
21 try buf.appendSlice("@echo off\n");
22 try buf.append('"');
23 try buf.appendSlice(child_exe_path);
24 try buf.append('"');
25 const preamble_len = buf.items.len;
26
27 try buf.appendSlice(" %*");
28 try tmp.dir.writeFile("args1.bat", buf.items);
29 buf.shrinkRetainingCapacity(preamble_len);
30
31 try buf.appendSlice(" %1 %2 %3 %4 %5 %6 %7 %8 %9");
32 try tmp.dir.writeFile("args2.bat", buf.items);
33 buf.shrinkRetainingCapacity(preamble_len);
34
35 try buf.appendSlice(" \"%~1\" \"%~2\" \"%~3\" \"%~4\" \"%~5\" \"%~6\" \"%~7\" \"%~8\" \"%~9\"");
36 try tmp.dir.writeFile("args3.bat", buf.items);
37 buf.shrinkRetainingCapacity(preamble_len);
38
39 // Test cases are from https://github.com/rust-lang/rust/blob/master/tests/ui/std/windows-bat-args.rs
40 try testExecError(error.InvalidBatchScriptArg, allocator, &.{"\x00"});
41 try testExecError(error.InvalidBatchScriptArg, allocator, &.{"\n"});
42 try testExecError(error.InvalidBatchScriptArg, allocator, &.{"\r"});
43 try testExec(allocator, &.{ "a", "b" }, null);
44 try testExec(allocator, &.{ "c is for cat", "d is for dog" }, null);
45 try testExec(allocator, &.{ "\"", " \"" }, null);
46 try testExec(allocator, &.{ "\\", "\\" }, null);
47 try testExec(allocator, &.{">file.txt"}, null);
48 try testExec(allocator, &.{"whoami.exe"}, null);
49 try testExec(allocator, &.{"&a.exe"}, null);
50 try testExec(allocator, &.{"&echo hello "}, null);
51 try testExec(allocator, &.{ "&echo hello", "&whoami", ">file.txt" }, null);
52 try testExec(allocator, &.{"!TMP!"}, null);
53 try testExec(allocator, &.{"key=value"}, null);
54 try testExec(allocator, &.{"\"key=value\""}, null);
55 try testExec(allocator, &.{"key = value"}, null);
56 try testExec(allocator, &.{"key=[\"value\"]"}, null);
57 try testExec(allocator, &.{ "", "a=b" }, null);
58 try testExec(allocator, &.{"key=\"foo bar\""}, null);
59 try testExec(allocator, &.{"key=[\"my_value]"}, null);
60 try testExec(allocator, &.{"key=[\"my_value\",\"other-value\"]"}, null);
61 try testExec(allocator, &.{"key\\=value"}, null);
62 try testExec(allocator, &.{"key=\"&whoami\""}, null);
63 try testExec(allocator, &.{"key=\"value\"=5"}, null);
64 try testExec(allocator, &.{"key=[\">file.txt\"]"}, null);
65 try testExec(allocator, &.{"%hello"}, null);
66 try testExec(allocator, &.{"%PATH%"}, null);
67 try testExec(allocator, &.{"%%cd:~,%"}, null);
68 try testExec(allocator, &.{"%PATH%PATH%"}, null);
69 try testExec(allocator, &.{"\">file.txt"}, null);
70 try testExec(allocator, &.{"abc\"&echo hello"}, null);
71 try testExec(allocator, &.{"123\">file.txt"}, null);
72 try testExec(allocator, &.{"\"&echo hello&whoami.exe"}, null);
73 try testExec(allocator, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null);
74 try testExec(allocator, &.{"&whoami.exe"}, null);
75
76 var env = env: {
77 var env = try std.process.getEnvMap(allocator);
78 errdefer env.deinit();
79 // No escaping
80 try env.put("FOO", "123");
81 // Some possible escaping of %FOO% that could be expanded
82 // when escaping cmd.exe meta characters with ^
83 try env.put("FOO^", "123"); // only escaping %
84 try env.put("^F^O^O^", "123"); // escaping every char
85 break :env env;
86 };
87 defer env.deinit();
88 try testExec(allocator, &.{"%FOO%"}, &env);
89
90 // Ensure that none of the `>file.txt`s have caused file.txt to be created
91 try std.testing.expectError(error.FileNotFound, tmp.dir.access("file.txt", .{}));
92}
93
94fn testExecError(err: anyerror, allocator: std.mem.Allocator, args: []const []const u8) !void {
95 return std.testing.expectError(err, testExec(allocator, args, null));
96}
97
98fn testExec(allocator: std.mem.Allocator, args: []const []const u8, env: ?*std.process.EnvMap) !void {
99 try testExecBat(allocator, "args1.bat", args, env);
100 try testExecBat(allocator, "args2.bat", args, env);
101 try testExecBat(allocator, "args3.bat", args, env);
102}
103
104fn testExecBat(allocator: std.mem.Allocator, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void {
105 var argv = try std.ArrayList([]const u8).initCapacity(allocator, 1 + args.len);
106 defer argv.deinit();
107 argv.appendAssumeCapacity(bat);
108 argv.appendSliceAssumeCapacity(args);
109
110 const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat");
111
112 const result = try std.ChildProcess.run(.{
113 .allocator = allocator,
114 .env_map = env,
115 .argv = argv.items,
116 });
117 defer allocator.free(result.stdout);
118 defer allocator.free(result.stderr);
119
120 try std.testing.expectEqualStrings("", result.stderr);
121 var it = std.mem.splitScalar(u8, result.stdout, '\x00');
122 var i: usize = 0;
123 while (it.next()) |actual_arg| {
124 if (i >= args.len and can_have_trailing_empty_args) {
125 try std.testing.expectEqualStrings("", actual_arg);
126 continue;
127 }
128 const expected_arg = args[i];
129 try std.testing.expectEqualStrings(expected_arg, actual_arg);
130 i += 1;
131 }
132}