Invalid memory access on Windows when handling long names
const std = @import("std");
const clap = @import("clap");
const Allocator = std.mem.Allocator;
const DebugAllocator = std.heap.DebugAllocator(.{});
pub fn main() !void {
var da = DebugAllocator.init;
defer _ = da.deinit();
const allocator = da.allocator();
const params = comptime clap.parseParamsComptime(
\\-h, --help Display help message and exit.
\\-v, --verbose Optional. Use verbose printing.
\\-p, --path <str> The path.
\\<str>...
\\
);
var diag = clap.Diagnostic{};
const res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{
.diagnostic = &diag,
.allocator = allocator,
.assignment_separators = "=",
}) catch |err| {
diag.report(std.io.getStdErr().writer(), err) catch {};
return;
};
defer res.deinit();
}
Running the program with any invalid long flag will raise a panic in debug mode:
> zig build run -- --pack
Invalid argument '--error.Unexpected: GetLastError(998): Invalid access to memory location.
C:\Program Files\Zig\lib\std\os\windows.zig:690:49: 0x7ff7b6026bd1 in WriteFile (zealupd.exe.obj)
else => |err| return unexpectedError(err),
^
C:\Program Files\Zig\lib\std\fs\File.zig:1324:33: 0x7ff7b60024c8 in write (zealupd.exe.obj)
return windows.WriteFile(self.handle, bytes, null);
^
C:\Program Files\Zig\lib\std\io.zig:348:27: 0x7ff7b5ffb97c in typeErasedWriteFn (zealupd.exe.obj)
return writeFn(ptr.*, bytes);
^
C:\Program Files\Zig\lib\std\io\Writer.zig:13:24: 0x7ff7b6001ca2 in write (zealupd.exe.obj)
return self.writeFn(self.context, bytes);
^
C:\Program Files\Zig\lib\std\io\Writer.zig:19:32: 0x7ff7b5ffb3e3 in writeAll (zealupd.exe.obj)
index += try self.write(bytes[index..]);
^
C:\Program Files\Zig\lib\std\fmt.zig:1068:28: 0x7ff7b60015e6 in formatBuf__anon_6765 (zealupd.exe.obj)
try writer.writeAll(buf);
^
C:\Program Files\Zig\lib\std\fmt.zig:658:37: 0x7ff7b5fffb7f in formatType__anon_6168 (zealupd.exe.obj)
return formatBuf(value, options, writer);
^
C:\Program Files\Zig\lib\std\fmt.zig:193:23: 0x7ff7b608c2dd in format__anon_28387 (zealupd.exe.obj)
try formatType(
^
C:\Program Files\Zig\lib\std\io\Writer.zig:24:26: 0x7ff7b60880a2 in print__anon_28244 (zealupd.exe.obj)
return std.fmt.format(self, format, args);
^
C:\Users\Me\AppData\Local\zig\p\clap-0.10.0-oBajB8fkAQB0JvsrWLar4YZrseSZ9irFxHB7Hvy_bvxb\clap.zig:559:64: 0x7ff7b6087de1 in report__anon_28216 (zealupd.exe.obj)
streaming.Error.InvalidArgument => try stream.print(
^
F:\zealupd\src\main.zig:31:20: 0x7ff7b6088318 in main (zealupd.exe.obj)
diag.report(std.io.getStdErr().writer(), err) catch {};
^
C:\Program Files\Zig\lib\std\start.zig:631:28: 0x7ff7b6088779 in main (zealupd.exe.obj)
return callMainWithArgs(@as(usize, @intCast(c_argc)), @as([*][*:0]u8, @ptrCast(c_argv)), envp);
^
C:\Program Files\Zig\lib\libc\mingw\crt\crtexe.c:266:0: 0x7ff7b609bb6b in __tmainCRTStartup (crt2.obj)
mainret = _tmain (argc, argv, envp);
C:\Program Files\Zig\lib\libc\mingw\crt\crtexe.c:186:0: 0x7ff7b609bbcb in mainCRTStartup (crt2.obj)
ret = __tmainCRTStartup ();
???:?:?: 0x7ffb5b3ce8d6 in ??? (KERNEL32.DLL)
???:?:?: 0x7ffb5be314fb in ??? (ntdll.dll)
The intended output breaks after --, so the access likely happens when reading the argument name. Using LLDB gives:
* thread #1, stop reason = breakpoint 2.1
frame #0: 0x00007ff7cea1b823 zealupd.exe`report__anon_27860(diag=0x0000005df5dff710, stream=<unavailable>, err=116, longest=clap.Names.Longest @ 0x0000005df5dff460) at clap.zig:555
552 "The argument '{s}{s}' does not take a value\n",
553 .{ longest.kind.prefix(), longest.name },
554 ),
-> 555 streaming.Error.MissingValue => try stream.print(
556 "The argument '{s}{s}' requires a value but none was supplied\n",
557 .{ longest.kind.prefix(), longest.name },
558 ),
(lldb) p diag
(clap.Diagnostic &) diag = 0x0000005df5dff710: {
arg = (ptr = "", len = 7)
name = {
long = (ptr = "", len = 7)
short = (data = '\0', some = '\0')
}
}
It seems like diag.arg (and diag.name.long that derives from it) contains invalid pointer.
I'm using the latest Zig master build, but I've also tried 0.14.0 to find the same problem. Meanwhile, this doesn't happen to Linux builds from what my test on Ubuntu shows.
I guess that's because of https://github.com/Hejsil/zig-clap/blob/a4e784da8399c51d5eeb5783e6a485b960d5c1f9/clap.zig#L651
The arena memory is destroyed after parse returns error, making argument names taken from ArgIterator invalid. I guess it doesn't happen on Linux because posix ArgIterator doesn't allocate memory.
EDIT: I don't know if this is a bug or intended behavior, because you can use parseEx and supply an argument iterator whose memory is owned by you. It is strange, however, to return freed memory.
Aah yikes. That is not intended. The question then becomes how to fix this. I don't really want to have diagnostics own memory if possible, but that might be what needs to be done
EDIT: I don't know if this is a bug or intended behavior, because you can use
parseExand supply an argument iterator whose memory is owned by you. It is strange, however, to return freed memory.
This works indeed. Then I think the issue happens with handling iterator memory.
var iter = try std.process.ArgIterator.initWithAllocator(allocator);
defer iter.deinit();
_ = iter.next();
var diag = clap.Diagnostic{};
var res = clap.parseEx(
clap.Help,
¶ms,
clap.parsers.default,
&iter,
.{
.diagnostic = &diag,
.allocator = allocator,
.assignment_separators = "=",
},
) catch |err| {
diag.report(std.io.getStdErr().writer(), err) catch {};
return;
};
defer res.deinit();
The question then becomes how to fix this. I don't really want to have diagnostics own memory if possible, but that might be what needs to be done
I don't think allocation could be avoided with current diagnostics interface, maybe with callbacks but that's ugly.
Then I think the issue happens with handling iterator memory.
Use an arena, just like parse does