From 5a2bbe9a08e51ed38456929f9746a3caead7c713 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Sep 2024 21:09:14 -0700 Subject: [PATCH 01/10] crash/minidump: header parsing --- src/crash/main.zig | 1 + src/crash/minidump.zig | 75 +++++++++++++++++++++++++++++++++++ src/crash/testdata/macos.dmp | Bin 0 -> 447456 bytes 3 files changed, 76 insertions(+) create mode 100644 src/crash/minidump.zig create mode 100644 src/crash/testdata/macos.dmp diff --git a/src/crash/main.zig b/src/crash/main.zig index 1ac971851..5f9aa96c5 100644 --- a/src/crash/main.zig +++ b/src/crash/main.zig @@ -5,6 +5,7 @@ const dir = @import("dir.zig"); const sentry_envelope = @import("sentry_envelope.zig"); +pub const minidump = @import("minidump.zig"); pub const sentry = @import("sentry.zig"); pub const Envelope = sentry_envelope.Envelope; pub const defaultDir = dir.defaultDir; diff --git a/src/crash/minidump.zig b/src/crash/minidump.zig new file mode 100644 index 000000000..caec0f1ea --- /dev/null +++ b/src/crash/minidump.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const log = std.log.scoped(.minidump); + +/// Minidump parser. +pub const Minidump = struct { + header: Header, + + /// Read the minidump file for the given source. + /// + /// The source must have a reader() and seekableStream() method. + /// For example, both File and std.io.FixedBufferStream implement these. + pub fn read(alloc: Allocator, source: anytype) !Minidump { + _ = alloc; + + // Read the header which also determines the endianness of the file. + const header, const endian = try readHeader(source); + log.warn("header={} endian={}", .{ header, endian }); + + return .{ + .header = header, + }; + } + + /// Reads the header for the minidump file and returns endianness of + /// the file. + fn readHeader(source: anytype) !struct { Header, std.builtin.Endian } { + // Start by trying LE. + var endian: std.builtin.Endian = .little; + var header = try source.reader().readStructEndian(Header, endian); + + // If the signature doesn't match, we assume its BE. + if (header.signature != signature) { + // Seek back to the start of the file so we can reread. + try source.seekableStream().seekTo(0); + + // Try BE, if the signature doesn't match, return an error. + endian = .big; + header = try source.reader().readStructEndian(Header, endian); + if (header.signature != signature) return error.InvalidHeader; + } + + // "The low-order word is MINIDUMP_VERSION. The high-order word is an + // internal value that is implementation specific." + if (header.version.low != version) return error.InvalidVersion; + + return .{ header, endian }; + } +}; + +/// "MDMP" in little-endian. +pub const signature = 0x504D444D; + +/// The version of the minidump format. +pub const version = 0xA793; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header +pub const Header = extern struct { + signature: u32, + version: packed struct(u32) { low: u16, high: u16 }, + stream_count: u32, + stream_directory_rva: u32, + checksum: u32, + time_date_stamp: u32, + flags: u64, +}; + +test "Minidump read" { + const testing = std.testing; + const alloc = testing.allocator; + var fbs = std.io.fixedBufferStream(@embedFile("testdata/macos.dmp")); + _ = try Minidump.read(alloc, &fbs); +} diff --git a/src/crash/testdata/macos.dmp b/src/crash/testdata/macos.dmp new file mode 100644 index 0000000000000000000000000000000000000000..5931c13a06284043c286fcda9a94aa93f2762909 GIT binary patch literal 447456 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
<8}G+das4`uDlUM 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%0A}{b*TTyo2jXplkIV$)2yqB#=6B62 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 literal 0 HcmV?d00001 From ae8859bc7bcf0267c00dfb0c136c40be4c1fc0f4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Sep 2024 21:50:47 -0700 Subject: [PATCH 02/10] crash/minidump: read the streams from the minidump file --- src/crash/minidump.zig | 118 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/src/crash/minidump.zig b/src/crash/minidump.zig index caec0f1ea..fbd4ec809 100644 --- a/src/crash/minidump.zig +++ b/src/crash/minidump.zig @@ -4,23 +4,108 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.minidump); -/// Minidump parser. +/// Minidump file format. pub const Minidump = struct { + /// The arena that all streams are allocated within when reading the + /// minidump file. This is freed on deinit. + arena: std.heap.ArenaAllocator, + + /// The header of the minidump file. On serialization, the stream count + /// and rva will be updated to match the streams. On deserialization, + /// this is read directly from the file. header: Header, + /// The streams within the minidump file in the order they're serialized. + streams: std.ArrayListUnmanaged(Stream), + + pub const Stream = struct { + type: u32, + data: []const u8, + }; + /// Read the minidump file for the given source. /// /// The source must have a reader() and seekableStream() method. /// For example, both File and std.io.FixedBufferStream implement these. - pub fn read(alloc: Allocator, source: anytype) !Minidump { - _ = alloc; + /// + /// The reader will read the full minidump data into memory. This makes + /// it easy to serialize the data back out. This is acceptable for our + /// use case which doesn't rely too much on being memory efficient or + /// high load. We also expect the minidump files to be relatively small + /// (dozens of MB at most, hundreds of KB typically). + /// + /// NOTE(mitchellh): If we ever want to make this more memory efficient, + /// I would create a new type that is a "lazy reader" that stores the + /// source type and reads the data as needed. Then this type should use + /// that type. + pub fn read(alloc_gpa: Allocator, source: anytype) !Minidump { + var arena = std.heap.ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); // Read the header which also determines the endianness of the file. const header, const endian = try readHeader(source); - log.warn("header={} endian={}", .{ header, endian }); + + var streams = try std.ArrayListUnmanaged(Stream).initCapacity( + alloc, + header.stream_count, + ); + errdefer streams.deinit(alloc); + + // Read the streams. All the streams are first described in a + // "directory" structure which tells us the type of stream and + // where it is located in the file. The directory structures are + // stored in a contiguous block at the stream_directory_rva. + // + // Due to how we use this structure, we read directories one by one, + // then read all the data for that directory, then move on to the + // next directory. This is because we copy all the minidump data + // into memory. + const seeker = source.seekableStream(); + try seeker.seekTo(header.stream_directory_rva); + for (0..header.stream_count) |_| { + // Read the current directory + const directory = try source.reader().readStructEndian(Directory, endian); + + // Seek to the location of the data. We have to store our current + // position because we need to seek back to it after reading the + // data in order to read the next directory. + const pos = try seeker.getPos(); + try seeker.seekTo(directory.location.rva); + + // Read the data. The data length is defined by the directory. + // If we can't read exactly that amount of data, we return an error. + var data = std.ArrayList(u8).init(alloc); + defer data.deinit(); + source.reader().readAllArrayList( + &data, + directory.location.data_size, + ) catch |err| switch (err) { + // This means there was more data in the reader than what + // we asked for this. This is okay and expected because + // all streams except the last one will have this error. + error.StreamTooLong => {}, + else => return err, + }; + + // Basic check. + if (data.items.len != directory.location.data_size) return error.DataSizeMismatch; + + // Store our stream + try streams.append(alloc, .{ + .type = directory.stream_type, + .data = try data.toOwnedSlice(), + }); + + // Seek back to where we were after reading this directory + // entry so we can read the next one. + try seeker.seekTo(pos); + } return .{ + .arena = arena, .header = header, + .streams = streams, }; } @@ -48,8 +133,16 @@ pub const Minidump = struct { return .{ header, endian }; } -}; + pub fn deinit(self: *Minidump) void { + self.arena.deinit(); + } + + /// The arena allocator associated with this envelope + pub fn allocator(self: *Minidump) Allocator { + return self.arena.allocator(); + } +}; /// "MDMP" in little-endian. pub const signature = 0x504D444D; @@ -67,9 +160,22 @@ pub const Header = extern struct { flags: u64, }; +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory +pub const Directory = extern struct { + stream_type: u32, + location: LocationDescriptor, +}; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor +pub const LocationDescriptor = extern struct { + data_size: u32, + rva: u32, +}; + test "Minidump read" { const testing = std.testing; const alloc = testing.allocator; var fbs = std.io.fixedBufferStream(@embedFile("testdata/macos.dmp")); - _ = try Minidump.read(alloc, &fbs); + var md = try Minidump.read(alloc, &fbs); + defer md.deinit(); } From 3cc18b62e7ae7b82009c7dd80bc6de94cc4df8e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Sep 2024 09:35:43 -0700 Subject: [PATCH 03/10] crash/minidump: split out into multiple files --- src/crash/minidump.zig | 183 +------------------------------- src/crash/minidump/external.zig | 36 +++++++ src/crash/minidump/minidump.zig | 154 +++++++++++++++++++++++++++ src/crash/minidump/stream.zig | 21 ++++ 4 files changed, 216 insertions(+), 178 deletions(-) create mode 100644 src/crash/minidump/external.zig create mode 100644 src/crash/minidump/minidump.zig create mode 100644 src/crash/minidump/stream.zig diff --git a/src/crash/minidump.zig b/src/crash/minidump.zig index fbd4ec809..0cf641114 100644 --- a/src/crash/minidump.zig +++ b/src/crash/minidump.zig @@ -1,181 +1,8 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; +const minidump = @import("minidump/minidump.zig"); -const log = std.log.scoped(.minidump); +pub const stream = @import("minidump/stream.zig"); +pub const Minidump = minidump.Minidump; -/// Minidump file format. -pub const Minidump = struct { - /// The arena that all streams are allocated within when reading the - /// minidump file. This is freed on deinit. - arena: std.heap.ArenaAllocator, - - /// The header of the minidump file. On serialization, the stream count - /// and rva will be updated to match the streams. On deserialization, - /// this is read directly from the file. - header: Header, - - /// The streams within the minidump file in the order they're serialized. - streams: std.ArrayListUnmanaged(Stream), - - pub const Stream = struct { - type: u32, - data: []const u8, - }; - - /// Read the minidump file for the given source. - /// - /// The source must have a reader() and seekableStream() method. - /// For example, both File and std.io.FixedBufferStream implement these. - /// - /// The reader will read the full minidump data into memory. This makes - /// it easy to serialize the data back out. This is acceptable for our - /// use case which doesn't rely too much on being memory efficient or - /// high load. We also expect the minidump files to be relatively small - /// (dozens of MB at most, hundreds of KB typically). - /// - /// NOTE(mitchellh): If we ever want to make this more memory efficient, - /// I would create a new type that is a "lazy reader" that stores the - /// source type and reads the data as needed. Then this type should use - /// that type. - pub fn read(alloc_gpa: Allocator, source: anytype) !Minidump { - var arena = std.heap.ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - // Read the header which also determines the endianness of the file. - const header, const endian = try readHeader(source); - - var streams = try std.ArrayListUnmanaged(Stream).initCapacity( - alloc, - header.stream_count, - ); - errdefer streams.deinit(alloc); - - // Read the streams. All the streams are first described in a - // "directory" structure which tells us the type of stream and - // where it is located in the file. The directory structures are - // stored in a contiguous block at the stream_directory_rva. - // - // Due to how we use this structure, we read directories one by one, - // then read all the data for that directory, then move on to the - // next directory. This is because we copy all the minidump data - // into memory. - const seeker = source.seekableStream(); - try seeker.seekTo(header.stream_directory_rva); - for (0..header.stream_count) |_| { - // Read the current directory - const directory = try source.reader().readStructEndian(Directory, endian); - - // Seek to the location of the data. We have to store our current - // position because we need to seek back to it after reading the - // data in order to read the next directory. - const pos = try seeker.getPos(); - try seeker.seekTo(directory.location.rva); - - // Read the data. The data length is defined by the directory. - // If we can't read exactly that amount of data, we return an error. - var data = std.ArrayList(u8).init(alloc); - defer data.deinit(); - source.reader().readAllArrayList( - &data, - directory.location.data_size, - ) catch |err| switch (err) { - // This means there was more data in the reader than what - // we asked for this. This is okay and expected because - // all streams except the last one will have this error. - error.StreamTooLong => {}, - else => return err, - }; - - // Basic check. - if (data.items.len != directory.location.data_size) return error.DataSizeMismatch; - - // Store our stream - try streams.append(alloc, .{ - .type = directory.stream_type, - .data = try data.toOwnedSlice(), - }); - - // Seek back to where we were after reading this directory - // entry so we can read the next one. - try seeker.seekTo(pos); - } - - return .{ - .arena = arena, - .header = header, - .streams = streams, - }; - } - - /// Reads the header for the minidump file and returns endianness of - /// the file. - fn readHeader(source: anytype) !struct { Header, std.builtin.Endian } { - // Start by trying LE. - var endian: std.builtin.Endian = .little; - var header = try source.reader().readStructEndian(Header, endian); - - // If the signature doesn't match, we assume its BE. - if (header.signature != signature) { - // Seek back to the start of the file so we can reread. - try source.seekableStream().seekTo(0); - - // Try BE, if the signature doesn't match, return an error. - endian = .big; - header = try source.reader().readStructEndian(Header, endian); - if (header.signature != signature) return error.InvalidHeader; - } - - // "The low-order word is MINIDUMP_VERSION. The high-order word is an - // internal value that is implementation specific." - if (header.version.low != version) return error.InvalidVersion; - - return .{ header, endian }; - } - - pub fn deinit(self: *Minidump) void { - self.arena.deinit(); - } - - /// The arena allocator associated with this envelope - pub fn allocator(self: *Minidump) Allocator { - return self.arena.allocator(); - } -}; -/// "MDMP" in little-endian. -pub const signature = 0x504D444D; - -/// The version of the minidump format. -pub const version = 0xA793; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header -pub const Header = extern struct { - signature: u32, - version: packed struct(u32) { low: u16, high: u16 }, - stream_count: u32, - stream_directory_rva: u32, - checksum: u32, - time_date_stamp: u32, - flags: u64, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory -pub const Directory = extern struct { - stream_type: u32, - location: LocationDescriptor, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor -pub const LocationDescriptor = extern struct { - data_size: u32, - rva: u32, -}; - -test "Minidump read" { - const testing = std.testing; - const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream(@embedFile("testdata/macos.dmp")); - var md = try Minidump.read(alloc, &fbs); - defer md.deinit(); +test { + @import("std").testing.refAllDecls(@This()); } diff --git a/src/crash/minidump/external.zig b/src/crash/minidump/external.zig new file mode 100644 index 000000000..9356a6cb3 --- /dev/null +++ b/src/crash/minidump/external.zig @@ -0,0 +1,36 @@ +//! This file contains the external structs and constants for the minidump +//! format. Most are from the Microsoft documentation on the minidump format: +//! https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ +//! +//! Wherever possible, we also compare our definitions to other projects +//! such as rust-minidump, libmdmp, breakpad, etc. to ensure we're doing +//! the right thing. + +/// "MDMP" in little-endian. +pub const signature = 0x504D444D; + +/// The version of the minidump format. +pub const version = 0xA793; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header +pub const Header = extern struct { + signature: u32, + version: packed struct(u32) { low: u16, high: u16 }, + stream_count: u32, + stream_directory_rva: u32, + checksum: u32, + time_date_stamp: u32, + flags: u64, +}; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory +pub const Directory = extern struct { + stream_type: u32, + location: LocationDescriptor, +}; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor +pub const LocationDescriptor = extern struct { + data_size: u32, + rva: u32, +}; diff --git a/src/crash/minidump/minidump.zig b/src/crash/minidump/minidump.zig new file mode 100644 index 000000000..2056212a7 --- /dev/null +++ b/src/crash/minidump/minidump.zig @@ -0,0 +1,154 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const external = @import("external.zig"); +const stream = @import("stream.zig"); +const Stream = stream.Stream; + +const log = std.log.scoped(.minidump); + +/// Minidump file format. +pub const Minidump = struct { + /// The arena that all streams are allocated within when reading the + /// minidump file. This is freed on deinit. + arena: std.heap.ArenaAllocator, + + /// The header of the minidump file. On serialization, the stream count + /// and rva will be updated to match the streams. On deserialization, + /// this is read directly from the file. + header: external.Header, + + /// The streams within the minidump file in the order they're serialized. + streams: std.ArrayListUnmanaged(Stream), + + /// Read the minidump file for the given source. + /// + /// The source must have a reader() and seekableStream() method. + /// For example, both File and std.io.FixedBufferStream implement these. + /// + /// The reader will read the full minidump data into memory. This makes + /// it easy to serialize the data back out. This is acceptable for our + /// use case which doesn't rely too much on being memory efficient or + /// high load. We also expect the minidump files to be relatively small + /// (dozens of MB at most, hundreds of KB typically). + /// + /// NOTE(mitchellh): If we ever want to make this more memory efficient, + /// I would create a new type that is a "lazy reader" that stores the + /// source type and reads the data as needed. Then this type should use + /// that type. + pub fn read(alloc_gpa: Allocator, source: anytype) !Minidump { + var arena = std.heap.ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Read the header which also determines the endianness of the file. + const header, const endian = try readHeader(source); + //log.warn("header={} endian={}", .{ header, endian }); + + var streams = try std.ArrayListUnmanaged(Stream).initCapacity( + alloc, + header.stream_count, + ); + errdefer streams.deinit(alloc); + + // Read the streams. All the streams are first described in a + // "directory" structure which tells us the type of stream and + // where it is located in the file. The directory structures are + // stored in a contiguous block at the stream_directory_rva. + // + // Due to how we use this structure, we read directories one by one, + // then read all the data for that directory, then move on to the + // next directory. This is because we copy all the minidump data + // into memory. + const seeker = source.seekableStream(); + try seeker.seekTo(header.stream_directory_rva); + for (0..header.stream_count) |_| { + // Read the current directory + const directory = try source.reader().readStructEndian(external.Directory, endian); + log.warn("directory={}", .{directory}); + + // Seek to the location of the data. We have to store our current + // position because we need to seek back to it after reading the + // data in order to read the next directory. + const pos = try seeker.getPos(); + + try seeker.seekTo(directory.location.rva); + + // Read the data. The data length is defined by the directory. + // If we can't read exactly that amount of data, we return an error. + var data = std.ArrayList(u8).init(alloc); + defer data.deinit(); + source.reader().readAllArrayList( + &data, + directory.location.data_size, + ) catch |err| switch (err) { + // This means there was more data in the reader than what + // we asked for this. This is okay and expected because + // all streams except the last one will have this error. + error.StreamTooLong => {}, + else => return err, + }; + + // Basic check. + if (data.items.len != directory.location.data_size) return error.DataSizeMismatch; + + // Store our stream + try streams.append(alloc, .{ .encoded = .{ + .type = directory.stream_type, + .data = try data.toOwnedSlice(), + } }); + + // Seek back to where we were after reading this directory + // entry so we can read the next one. + try seeker.seekTo(pos); + } + + return .{ + .arena = arena, + .header = header, + .streams = streams, + }; + } + + /// Reads the header for the minidump file and returns endianness of + /// the file. + fn readHeader(source: anytype) !struct { external.Header, std.builtin.Endian } { + // Start by trying LE. + var endian: std.builtin.Endian = .little; + var header = try source.reader().readStructEndian(external.Header, endian); + + // If the signature doesn't match, we assume its BE. + if (header.signature != external.signature) { + // Seek back to the start of the file so we can reread. + try source.seekableStream().seekTo(0); + + // Try BE, if the signature doesn't match, return an error. + endian = .big; + header = try source.reader().readStructEndian(external.Header, endian); + if (header.signature != external.signature) return error.InvalidHeader; + } + + // "The low-order word is MINIDUMP_VERSION. The high-order word is an + // internal value that is implementation specific." + if (header.version.low != external.version) return error.InvalidVersion; + + return .{ header, endian }; + } + + pub fn deinit(self: *Minidump) void { + self.arena.deinit(); + } + + /// The arena allocator associated with this envelope + pub fn allocator(self: *Minidump) Allocator { + return self.arena.allocator(); + } +}; + +test "Minidump read" { + const testing = std.testing; + const alloc = testing.allocator; + var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); + var md = try Minidump.read(alloc, &fbs); + defer md.deinit(); +} diff --git a/src/crash/minidump/stream.zig b/src/crash/minidump/stream.zig new file mode 100644 index 000000000..d607ed82b --- /dev/null +++ b/src/crash/minidump/stream.zig @@ -0,0 +1,21 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +/// A stream within the minidump file. A stream can be either in an encoded +/// form or decoded form. The encoded form are raw bytes and aren't validated +/// until they're decoded. The decoded form is a structured form of the stream. +/// +/// The decoded form is more ergonomic to work with but the encoded form is +/// more efficient to read/write. +pub const Stream = union(enum) { + encoded: EncodedStream, +}; + +/// An encoded stream value. It is "encoded" in the sense that it is raw bytes +/// with a type associated. The raw bytes are not validated to be correct for +/// the type. +pub const EncodedStream = struct { + type: u32, + data: []const u8, +}; From b8ec91242f96cf74f7545de7f70e2c80deb37829 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Sep 2024 10:07:24 -0700 Subject: [PATCH 04/10] crash/minidump: reader that streams data from a source --- src/crash/minidump.zig | 4 +- src/crash/minidump/minidump.zig | 154 ----------------------------- src/crash/minidump/reader.zig | 167 ++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 156 deletions(-) delete mode 100644 src/crash/minidump/minidump.zig create mode 100644 src/crash/minidump/reader.zig diff --git a/src/crash/minidump.zig b/src/crash/minidump.zig index 0cf641114..1e103283f 100644 --- a/src/crash/minidump.zig +++ b/src/crash/minidump.zig @@ -1,7 +1,7 @@ -const minidump = @import("minidump/minidump.zig"); +const reader = @import("minidump/reader.zig"); pub const stream = @import("minidump/stream.zig"); -pub const Minidump = minidump.Minidump; +pub const Reader = reader.Reader; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/crash/minidump/minidump.zig b/src/crash/minidump/minidump.zig deleted file mode 100644 index 2056212a7..000000000 --- a/src/crash/minidump/minidump.zig +++ /dev/null @@ -1,154 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const external = @import("external.zig"); -const stream = @import("stream.zig"); -const Stream = stream.Stream; - -const log = std.log.scoped(.minidump); - -/// Minidump file format. -pub const Minidump = struct { - /// The arena that all streams are allocated within when reading the - /// minidump file. This is freed on deinit. - arena: std.heap.ArenaAllocator, - - /// The header of the minidump file. On serialization, the stream count - /// and rva will be updated to match the streams. On deserialization, - /// this is read directly from the file. - header: external.Header, - - /// The streams within the minidump file in the order they're serialized. - streams: std.ArrayListUnmanaged(Stream), - - /// Read the minidump file for the given source. - /// - /// The source must have a reader() and seekableStream() method. - /// For example, both File and std.io.FixedBufferStream implement these. - /// - /// The reader will read the full minidump data into memory. This makes - /// it easy to serialize the data back out. This is acceptable for our - /// use case which doesn't rely too much on being memory efficient or - /// high load. We also expect the minidump files to be relatively small - /// (dozens of MB at most, hundreds of KB typically). - /// - /// NOTE(mitchellh): If we ever want to make this more memory efficient, - /// I would create a new type that is a "lazy reader" that stores the - /// source type and reads the data as needed. Then this type should use - /// that type. - pub fn read(alloc_gpa: Allocator, source: anytype) !Minidump { - var arena = std.heap.ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - // Read the header which also determines the endianness of the file. - const header, const endian = try readHeader(source); - //log.warn("header={} endian={}", .{ header, endian }); - - var streams = try std.ArrayListUnmanaged(Stream).initCapacity( - alloc, - header.stream_count, - ); - errdefer streams.deinit(alloc); - - // Read the streams. All the streams are first described in a - // "directory" structure which tells us the type of stream and - // where it is located in the file. The directory structures are - // stored in a contiguous block at the stream_directory_rva. - // - // Due to how we use this structure, we read directories one by one, - // then read all the data for that directory, then move on to the - // next directory. This is because we copy all the minidump data - // into memory. - const seeker = source.seekableStream(); - try seeker.seekTo(header.stream_directory_rva); - for (0..header.stream_count) |_| { - // Read the current directory - const directory = try source.reader().readStructEndian(external.Directory, endian); - log.warn("directory={}", .{directory}); - - // Seek to the location of the data. We have to store our current - // position because we need to seek back to it after reading the - // data in order to read the next directory. - const pos = try seeker.getPos(); - - try seeker.seekTo(directory.location.rva); - - // Read the data. The data length is defined by the directory. - // If we can't read exactly that amount of data, we return an error. - var data = std.ArrayList(u8).init(alloc); - defer data.deinit(); - source.reader().readAllArrayList( - &data, - directory.location.data_size, - ) catch |err| switch (err) { - // This means there was more data in the reader than what - // we asked for this. This is okay and expected because - // all streams except the last one will have this error. - error.StreamTooLong => {}, - else => return err, - }; - - // Basic check. - if (data.items.len != directory.location.data_size) return error.DataSizeMismatch; - - // Store our stream - try streams.append(alloc, .{ .encoded = .{ - .type = directory.stream_type, - .data = try data.toOwnedSlice(), - } }); - - // Seek back to where we were after reading this directory - // entry so we can read the next one. - try seeker.seekTo(pos); - } - - return .{ - .arena = arena, - .header = header, - .streams = streams, - }; - } - - /// Reads the header for the minidump file and returns endianness of - /// the file. - fn readHeader(source: anytype) !struct { external.Header, std.builtin.Endian } { - // Start by trying LE. - var endian: std.builtin.Endian = .little; - var header = try source.reader().readStructEndian(external.Header, endian); - - // If the signature doesn't match, we assume its BE. - if (header.signature != external.signature) { - // Seek back to the start of the file so we can reread. - try source.seekableStream().seekTo(0); - - // Try BE, if the signature doesn't match, return an error. - endian = .big; - header = try source.reader().readStructEndian(external.Header, endian); - if (header.signature != external.signature) return error.InvalidHeader; - } - - // "The low-order word is MINIDUMP_VERSION. The high-order word is an - // internal value that is implementation specific." - if (header.version.low != external.version) return error.InvalidVersion; - - return .{ header, endian }; - } - - pub fn deinit(self: *Minidump) void { - self.arena.deinit(); - } - - /// The arena allocator associated with this envelope - pub fn allocator(self: *Minidump) Allocator { - return self.arena.allocator(); - } -}; - -test "Minidump read" { - const testing = std.testing; - const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - var md = try Minidump.read(alloc, &fbs); - defer md.deinit(); -} diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig new file mode 100644 index 000000000..0735de048 --- /dev/null +++ b/src/crash/minidump/reader.zig @@ -0,0 +1,167 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const external = @import("external.zig"); +const stream = @import("stream.zig"); +const EncodedStream = stream.EncodedStream; + +const log = std.log.scoped(.minidump_reader); + +/// Possible minidump-specific errors that can occur when reading a minidump. +/// This isn't the full error set since IO errors can also occur depending +/// on the Source type. +pub const ReadError = error{ + InvalidHeader, + InvalidVersion, +}; + +/// Reader creates a new minidump reader for the given source type. The +/// source must have both a "reader()" and "seekableStream()" function. +/// +/// Given the format of a minidump file, we must keep the source open and +/// continually access it because the format of the minidump is full of +/// pointers and offsets that we must follow depending on the stream types. +/// Also, since we're not aware of all stream types (in fact its impossible +/// to be aware since custom stream types are allowed), its possible any stream +/// type can define their own pointers and offsets. So, the source must always +/// be available so callers can decode the streams as needed. +pub fn Reader(comptime Source: type) type { + return struct { + const Self = @This(); + + /// The source data. + source: Source, + + /// The endianness of the minidump file. This is detected by reading + /// the byte order of the header. + endian: std.builtin.Endian, + + /// The number of streams within the minidump file. This is read from + /// the header and stored here so we can quickly access them. Note + /// the stream types require reading the source; this is an optimization + /// to avoid any allocations on the reader and the caller can choose + /// to store them if they want. + stream_count: u32, + stream_directory_rva: u32, + + const SourceCallable = switch (@typeInfo(Source)) { + .Pointer => |v| v.child, + .Struct => Source, + else => @compileError("Source type must be a pointer or struct"), + }; + + const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).Fn.return_type.?; + const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).Fn.return_type.?; + + /// The reader type for stream reading. This is a LimitedReader so + /// you must still call reader() on the result to get the actual + /// reader to read the data. + pub const StreamReader = std.io.LimitedReader(SourceReader); + + /// Initialize a reader. The source must remain available for the entire + /// lifetime of the reader. The reader does not take ownership of the + /// source so if it has resources that need to be cleaned up, the caller + /// must do so once the reader is no longer needed. + pub fn init(source: Source) !Self { + const header, const endian = try readHeader(Source, source); + return .{ + .source = source, + .endian = endian, + .stream_count = header.stream_count, + .stream_directory_rva = header.stream_directory_rva, + }; + } + + /// Return a StreamReader for the given directory type. This streams + /// from the underlying source so the returned reader is only valid + /// as long as the source is unmodified (i.e. the source is not + /// closed, the source is not seeked, etc.). + pub fn streamReader( + self: *const Self, + dir: external.Directory, + ) SourceSeeker.SeekError!StreamReader { + try self.source.seekableStream().seekTo(dir.location.rva); + return .{ + .inner_reader = self.source.reader(), + .bytes_left = dir.location.data_size, + }; + } + + /// Get the directory entry with the given index. + /// + /// Asserts the index is valid (idx < stream_count). + pub fn directory(self: *const Self, idx: usize) !external.Directory { + assert(idx < self.stream_count); + + // Seek to the directory. + const offset: u32 = @intCast(@sizeOf(external.Directory) * idx); + const rva: u32 = self.stream_directory_rva + offset; + try self.source.seekableStream().seekTo(rva); + + // Read the directory. + return try self.source.reader().readStructEndian( + external.Directory, + self.endian, + ); + } + }; +} + +/// Reads the header for the minidump file and returns endianness of +/// the file. +fn readHeader(comptime T: type, source: T) !struct { + external.Header, + std.builtin.Endian, +} { + // Start by trying LE. + var endian: std.builtin.Endian = .little; + var header = try source.reader().readStructEndian(external.Header, endian); + + // If the signature doesn't match, we assume its BE. + if (header.signature != external.signature) { + // Seek back to the start of the file so we can reread. + try source.seekableStream().seekTo(0); + + // Try BE, if the signature doesn't match, return an error. + endian = .big; + header = try source.reader().readStructEndian(external.Header, endian); + if (header.signature != external.signature) return ReadError.InvalidHeader; + } + + // "The low-order word is MINIDUMP_VERSION. The high-order word is an + // internal value that is implementation specific." + if (header.version.low != external.version) return ReadError.InvalidVersion; + + return .{ header, endian }; +} + +// Uncomment to dump some debug information for a minidump file. +test "Minidump debug" { + var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); + const r = try Reader(*@TypeOf(fbs)).init(&fbs); + for (0..r.stream_count) |i| { + const dir = try r.directory(i); + log.warn("directory i={} dir={}", .{ i, dir }); + } +} + +test "Minidump read" { + const testing = std.testing; + const alloc = testing.allocator; + + var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); + const r = try Reader(*@TypeOf(fbs)).init(&fbs); + try testing.expectEqual(std.builtin.Endian.little, r.endian); + try testing.expectEqual(7, r.stream_count); + { + const dir = try r.directory(0); + try testing.expectEqual(3, dir.stream_type); + try testing.expectEqual(584, dir.location.data_size); + + var bytes = std.ArrayList(u8).init(alloc); + defer bytes.deinit(); + var sr = try r.streamReader(dir); + try sr.reader().readAllArrayList(&bytes, std.math.maxInt(usize)); + try testing.expectEqual(584, bytes.items.len); + } +} From facbabfd2c2eb3af182328c7034f17a8e6971ab8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Sep 2024 10:15:36 -0700 Subject: [PATCH 05/10] crash/minidump: StreamReader --- src/crash/minidump/reader.zig | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig index 0735de048..8f795960f 100644 --- a/src/crash/minidump/reader.zig +++ b/src/crash/minidump/reader.zig @@ -53,10 +53,36 @@ pub fn Reader(comptime Source: type) type { const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).Fn.return_type.?; const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).Fn.return_type.?; - /// The reader type for stream reading. This is a LimitedReader so + /// The reader type for stream reading. This has some other methods so /// you must still call reader() on the result to get the actual /// reader to read the data. - pub const StreamReader = std.io.LimitedReader(SourceReader); + pub const StreamReader = struct { + source: Source, + directory: external.Directory, + + /// Should not be accessed directly. This is setup whenever + /// reader() is called. + limit_reader: LimitedReader = undefined, + + const LimitedReader = std.io.LimitedReader(SourceReader); + pub const Reader = LimitedReader.Reader; + + /// Returns a Reader implementation that reads the bytes of the + /// stream. + /// + /// The reader is dependent on the state of Source so any + /// state-changing operations on Source will invalidate the + /// reader. For example, making another reader, reading another + /// stream directory, closing the source, etc. + pub fn reader(self: *StreamReader) LimitedReader.Reader { + try self.source.seekableStream().seekTo(self.directory.location.rva); + self.limit_reader = .{ + .inner_reader = self.source.reader(), + .bytes_left = self.directory.location.data_size, + }; + return self.limit_reader.reader(); + } + }; /// Initialize a reader. The source must remain available for the entire /// lifetime of the reader. The reader does not take ownership of the @@ -80,10 +106,9 @@ pub fn Reader(comptime Source: type) type { self: *const Self, dir: external.Directory, ) SourceSeeker.SeekError!StreamReader { - try self.source.seekableStream().seekTo(dir.location.rva); return .{ - .inner_reader = self.source.reader(), - .bytes_left = dir.location.data_size, + .source = self.source, + .directory = dir, }; } From ca1ab7bcdc61b0ecb213e1940aa196818a683527 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Sep 2024 10:32:31 -0700 Subject: [PATCH 06/10] crash/minidump: streamIterator --- src/crash/minidump/reader.zig | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig index 8f795960f..efdd76a6a 100644 --- a/src/crash/minidump/reader.zig +++ b/src/crash/minidump/reader.zig @@ -84,6 +84,19 @@ pub fn Reader(comptime Source: type) type { } }; + /// Iterator type to read over the streams in the minidump file. + pub const StreamIterator = struct { + reader: *const Self, + i: u32 = 0, + + pub fn next(self: *StreamIterator) !?StreamReader { + if (self.i >= self.reader.stream_count) return null; + const dir = try self.reader.directory(self.i); + self.i += 1; + return try self.reader.streamReader(dir); + } + }; + /// Initialize a reader. The source must remain available for the entire /// lifetime of the reader. The reader does not take ownership of the /// source so if it has resources that need to be cleaned up, the caller @@ -98,6 +111,14 @@ pub fn Reader(comptime Source: type) type { }; } + /// Return an interator to read over the streams in the minidump file. + /// This is very similar to using a simple for loop to stream_count + /// and calling directory() on each index, but is more idiomatic + /// Zig. + pub fn streamIterator(self: *const Self) StreamIterator { + return .{ .reader = self }; + } + /// Return a StreamReader for the given directory type. This streams /// from the underlying source so the returned reader is only valid /// as long as the source is unmodified (i.e. the source is not @@ -164,9 +185,9 @@ fn readHeader(comptime T: type, source: T) !struct { test "Minidump debug" { var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); const r = try Reader(*@TypeOf(fbs)).init(&fbs); - for (0..r.stream_count) |i| { - const dir = try r.directory(i); - log.warn("directory i={} dir={}", .{ i, dir }); + var it = r.streamIterator(); + while (try it.next()) |s| { + log.warn("directory i={} dir={}", .{ it.i - 1, s.directory }); } } From c0719fceef05870b6992fe54c24dd0db4a919cd7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Sep 2024 10:37:59 -0700 Subject: [PATCH 07/10] crash/minidump: typos --- src/crash/minidump/reader.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig index efdd76a6a..a88e6ed08 100644 --- a/src/crash/minidump/reader.zig +++ b/src/crash/minidump/reader.zig @@ -111,7 +111,7 @@ pub fn Reader(comptime Source: type) type { }; } - /// Return an interator to read over the streams in the minidump file. + /// Return an iterator to read over the streams in the minidump file. /// This is very similar to using a simple for loop to stream_count /// and calling directory() on each index, but is more idiomatic /// Zig. @@ -122,7 +122,7 @@ pub fn Reader(comptime Source: type) type { /// Return a StreamReader for the given directory type. This streams /// from the underlying source so the returned reader is only valid /// as long as the source is unmodified (i.e. the source is not - /// closed, the source is not seeked, etc.). + /// closed, the source seek position is not moved, etc.). pub fn streamReader( self: *const Self, dir: external.Directory, From df629044fad2648e13d686bb1245bd9d5d4a4e8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Sep 2024 10:58:38 -0700 Subject: [PATCH 08/10] crash/minidump: working on rich stream type decoding, ThreadList --- src/crash/minidump/external.zig | 24 ++++++++++++++ src/crash/minidump/reader.zig | 16 ++++++++-- src/crash/minidump/stream.zig | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/crash/minidump/external.zig b/src/crash/minidump/external.zig index 9356a6cb3..a6f89d3e9 100644 --- a/src/crash/minidump/external.zig +++ b/src/crash/minidump/external.zig @@ -34,3 +34,27 @@ pub const LocationDescriptor = extern struct { data_size: u32, rva: u32, }; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory_descriptor +pub const MemoryDescriptor = extern struct { + start_of_memory_range: u64, + memory: LocationDescriptor, +}; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_list +pub const ThreadList = extern struct { + number_of_threads: u32, + + // This struct has a trailing array of `Thread` structs. +}; + +/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread +pub const Thread = extern struct { + thread_id: u32, + suspend_count: u32, + priority_class: u32, + priority: u32, + teb: u64, + stack: MemoryDescriptor, + thread_context: LocationDescriptor, +}; diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig index a88e6ed08..582044879 100644 --- a/src/crash/minidump/reader.zig +++ b/src/crash/minidump/reader.zig @@ -25,7 +25,7 @@ pub const ReadError = error{ /// to be aware since custom stream types are allowed), its possible any stream /// type can define their own pointers and offsets. So, the source must always /// be available so callers can decode the streams as needed. -pub fn Reader(comptime Source: type) type { +pub fn Reader(comptime S: type) type { return struct { const Self = @This(); @@ -53,11 +53,15 @@ pub fn Reader(comptime Source: type) type { const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).Fn.return_type.?; const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).Fn.return_type.?; + /// The source type for the reader. + pub const Source = S; + /// The reader type for stream reading. This has some other methods so /// you must still call reader() on the result to get the actual /// reader to read the data. pub const StreamReader = struct { source: Source, + endian: std.builtin.Endian, directory: external.Directory, /// Should not be accessed directly. This is setup whenever @@ -82,6 +86,11 @@ pub fn Reader(comptime Source: type) type { }; return self.limit_reader.reader(); } + + /// Seeks the source to the location of the directory. + pub fn seekToPayload(self: *StreamReader) !void { + try self.source.seekableStream().seekTo(self.directory.location.rva); + } }; /// Iterator type to read over the streams in the minidump file. @@ -129,6 +138,7 @@ pub fn Reader(comptime Source: type) type { ) SourceSeeker.SeekError!StreamReader { return .{ .source = self.source, + .endian = self.endian, .directory = dir, }; } @@ -182,7 +192,7 @@ fn readHeader(comptime T: type, source: T) !struct { } // Uncomment to dump some debug information for a minidump file. -test "Minidump debug" { +test "minidump debug" { var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); const r = try Reader(*@TypeOf(fbs)).init(&fbs); var it = r.streamIterator(); @@ -191,7 +201,7 @@ test "Minidump debug" { } } -test "Minidump read" { +test "minidump read" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/crash/minidump/stream.zig b/src/crash/minidump/stream.zig index d607ed82b..bb383cce0 100644 --- a/src/crash/minidump/stream.zig +++ b/src/crash/minidump/stream.zig @@ -1,6 +1,9 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const Reader = @import("reader.zig").Reader; + +const log = std.log.scoped(.minidump_stream); /// A stream within the minidump file. A stream can be either in an encoded /// form or decoded form. The encoded form are raw bytes and aren't validated @@ -19,3 +22,56 @@ pub const EncodedStream = struct { type: u32, data: []const u8, }; + +/// This is the list of threads from the process. +/// +/// ThreadList is stream type 0x3. +/// StreamReader is the Reader(T).StreamReader type. +pub fn ThreadList(comptime R: type) type { + return struct { + const Self = @This(); + + /// The number of threads in the list. + count: u32, + + /// The rva to the first thread in the list. + rva: u32, + + /// The source data and endianness so we can continue reading. + source: R.Source, + endian: std.builtin.Endian, + + pub fn init(r: *R.StreamReader) !Self { + assert(r.directory.stream_type == 0x3); + try r.seekToPayload(); + + const reader = r.source.reader(); + const count = try reader.readInt(u32, r.endian); + const rva = r.directory.location.rva + @as(u32, @intCast(@sizeOf(u32))); + + return .{ + .count = count, + .rva = rva, + .source = r.source, + .endian = r.endian, + }; + } + }; +} + +test "minidump: threadlist" { + const testing = std.testing; + + var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); + const R = Reader(*@TypeOf(fbs)); + const r = try R.init(&fbs); + + // Get our thread list stream + const dir = try r.directory(0); + try testing.expectEqual(3, dir.stream_type); + var sr = try r.streamReader(dir); + + // Get our rich structure + const v = try ThreadList(R).init(&sr); + log.warn("threadlist count={} rva={}", .{ v.count, v.rva }); +} From 5b1d729748f10f1793401a0d8a68f877ceef4bcd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Sep 2024 15:26:21 -0700 Subject: [PATCH 09/10] crash/minidump: handle padding in the ThreadList stream --- src/crash/minidump.zig | 3 +- src/crash/minidump/external.zig | 3 +- src/crash/minidump/reader.zig | 4 + src/crash/minidump/stream.zig | 57 +----------- src/crash/minidump/stream_threadlist.zig | 111 +++++++++++++++++++++++ src/crash/testdata/macos.dmp | Bin 447456 -> 447584 bytes 6 files changed, 122 insertions(+), 56 deletions(-) create mode 100644 src/crash/minidump/stream_threadlist.zig diff --git a/src/crash/minidump.zig b/src/crash/minidump.zig index 1e103283f..0abd67eae 100644 --- a/src/crash/minidump.zig +++ b/src/crash/minidump.zig @@ -1,5 +1,4 @@ -const reader = @import("minidump/reader.zig"); - +pub const reader = @import("minidump/reader.zig"); pub const stream = @import("minidump/stream.zig"); pub const Reader = reader.Reader; diff --git a/src/crash/minidump/external.zig b/src/crash/minidump/external.zig index a6f89d3e9..451810883 100644 --- a/src/crash/minidump/external.zig +++ b/src/crash/minidump/external.zig @@ -44,8 +44,7 @@ pub const MemoryDescriptor = extern struct { /// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_list pub const ThreadList = extern struct { number_of_threads: u32, - - // This struct has a trailing array of `Thread` structs. + threads: [1]Thread, }; /// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig index 582044879..f316e63b0 100644 --- a/src/crash/minidump/reader.zig +++ b/src/crash/minidump/reader.zig @@ -13,6 +13,7 @@ const log = std.log.scoped(.minidump_reader); pub const ReadError = error{ InvalidHeader, InvalidVersion, + StreamSizeMismatch, }; /// Reader creates a new minidump reader for the given source type. The @@ -56,6 +57,9 @@ pub fn Reader(comptime S: type) type { /// The source type for the reader. pub const Source = S; + /// The stream types for reading + pub const ThreadList = stream.thread_list.ThreadListReader(Self); + /// The reader type for stream reading. This has some other methods so /// you must still call reader() on the result to get the actual /// reader to read the data. diff --git a/src/crash/minidump/stream.zig b/src/crash/minidump/stream.zig index bb383cce0..00ec6b042 100644 --- a/src/crash/minidump/stream.zig +++ b/src/crash/minidump/stream.zig @@ -1,10 +1,12 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const Reader = @import("reader.zig").Reader; const log = std.log.scoped(.minidump_stream); +/// The known stream types. +pub const thread_list = @import("stream_threadlist.zig"); + /// A stream within the minidump file. A stream can be either in an encoded /// form or decoded form. The encoded form are raw bytes and aren't validated /// until they're decoded. The decoded form is a structured form of the stream. @@ -23,55 +25,6 @@ pub const EncodedStream = struct { data: []const u8, }; -/// This is the list of threads from the process. -/// -/// ThreadList is stream type 0x3. -/// StreamReader is the Reader(T).StreamReader type. -pub fn ThreadList(comptime R: type) type { - return struct { - const Self = @This(); - - /// The number of threads in the list. - count: u32, - - /// The rva to the first thread in the list. - rva: u32, - - /// The source data and endianness so we can continue reading. - source: R.Source, - endian: std.builtin.Endian, - - pub fn init(r: *R.StreamReader) !Self { - assert(r.directory.stream_type == 0x3); - try r.seekToPayload(); - - const reader = r.source.reader(); - const count = try reader.readInt(u32, r.endian); - const rva = r.directory.location.rva + @as(u32, @intCast(@sizeOf(u32))); - - return .{ - .count = count, - .rva = rva, - .source = r.source, - .endian = r.endian, - }; - } - }; -} - -test "minidump: threadlist" { - const testing = std.testing; - - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - const R = Reader(*@TypeOf(fbs)); - const r = try R.init(&fbs); - - // Get our thread list stream - const dir = try r.directory(0); - try testing.expectEqual(3, dir.stream_type); - var sr = try r.streamReader(dir); - - // Get our rich structure - const v = try ThreadList(R).init(&sr); - log.warn("threadlist count={} rva={}", .{ v.count, v.rva }); +test { + @import("std").testing.refAllDecls(@This()); } diff --git a/src/crash/minidump/stream_threadlist.zig b/src/crash/minidump/stream_threadlist.zig new file mode 100644 index 000000000..e74d11e3e --- /dev/null +++ b/src/crash/minidump/stream_threadlist.zig @@ -0,0 +1,111 @@ +const std = @import("std"); +const assert = std.debug.assert; +const external = @import("external.zig"); +const readerpkg = @import("reader.zig"); +const Reader = readerpkg.Reader; +const ReadError = readerpkg.ReadError; + +const log = std.log.scoped(.minidump_stream); + +/// This is the list of threads from the process. +/// +/// This is the Reader implementation. You usually do not use this directly. +/// Instead, use Reader(T).ThreadList which will get you the same thing. +/// +/// ThreadList is stream type 0x3. +/// StreamReader is the Reader(T).StreamReader type. +pub fn ThreadListReader(comptime R: type) type { + return struct { + const Self = @This(); + + /// The number of threads in the list. + count: u32, + + /// The rva to the first thread in the list. + rva: u32, + + /// Source data and endianness so we can read. + source: R.Source, + endian: std.builtin.Endian, + + pub fn init(r: *R.StreamReader) !Self { + assert(r.directory.stream_type == 0x3); + try r.seekToPayload(); + const reader = r.source.reader(); + + // Our count is always a u32 in the header. + const count = try reader.readInt(u32, r.endian); + + // Determine if we have padding in our header. It is possible + // for there to be padding if the list header was written by + // a 32-bit process but is being read on a 64-bit process. + const padding = padding: { + const maybe_size = @sizeOf(u32) + (@sizeOf(external.Thread) * count); + switch (std.math.order(maybe_size, r.directory.location.data_size)) { + // It should never be larger than what the directory says. + .gt => return ReadError.StreamSizeMismatch, + + // If the sizes match exactly we're good. + .eq => break :padding 0, + + .lt => { + const padding = r.directory.location.data_size - maybe_size; + if (padding != 4) return ReadError.StreamSizeMismatch; + break :padding padding; + }, + } + }; + + // Rva is the location of the first thread in the list. + const rva = r.directory.location.rva + @as(u32, @sizeOf(u32)) + padding; + + return .{ + .count = count, + .rva = rva, + .source = r.source, + .endian = r.endian, + }; + } + + /// Get the thread entry for the given index. + /// + /// Index is asserted to be less than count. + pub fn thread(self: *const Self, i: usize) !external.Thread { + assert(i < self.count); + + // Seek to the thread + const offset: u32 = @intCast(@sizeOf(external.Thread) * i); + const rva: u32 = self.rva + offset; + try self.source.seekableStream().seekTo(rva); + + // Read the thread + return try self.source.reader().readStructEndian( + external.Thread, + self.endian, + ); + } + }; +} + +test "minidump: threadlist" { + const testing = std.testing; + + var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); + const R = Reader(*@TypeOf(fbs)); + const r = try R.init(&fbs); + + // Get our thread list stream + const dir = try r.directory(0); + try testing.expectEqual(3, dir.stream_type); + var sr = try r.streamReader(dir); + + // Get our rich structure + const v = try R.ThreadList.init(&sr); + log.warn("threadlist count={} rva={}", .{ v.count, v.rva }); + + try testing.expectEqual(12, v.count); + for (0..v.count) |i| { + const t = try v.thread(i); + log.warn("thread i={} thread={}", .{ i, t }); + } +} diff --git a/src/crash/testdata/macos.dmp b/src/crash/testdata/macos.dmp index 5931c13a06284043c286fcda9a94aa93f2762909..212cc7e624a1a670687dc20eca7f81e4db53ba5f 100644 GIT binary patch delta 31642 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 delta 31643 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 Date: Sun, 8 Sep 2024 21:25:34 -0700 Subject: [PATCH 10/10] crash/minidump: locationReader to read locations in a minidump --- src/crash/minidump/reader.zig | 17 ++++++++++++++++- src/crash/minidump/stream_threadlist.zig | 6 ++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig index f316e63b0..f792d6670 100644 --- a/src/crash/minidump/reader.zig +++ b/src/crash/minidump/reader.zig @@ -54,6 +54,9 @@ pub fn Reader(comptime S: type) type { const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).Fn.return_type.?; const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).Fn.return_type.?; + /// A limited reader for reading data from the source. + pub const LimitedReader = std.io.LimitedReader(SourceReader); + /// The source type for the reader. pub const Source = S; @@ -72,7 +75,6 @@ pub fn Reader(comptime S: type) type { /// reader() is called. limit_reader: LimitedReader = undefined, - const LimitedReader = std.io.LimitedReader(SourceReader); pub const Reader = LimitedReader.Reader; /// Returns a Reader implementation that reads the bytes of the @@ -164,6 +166,19 @@ pub fn Reader(comptime S: type) type { self.endian, ); } + + /// Return a reader for the given location descriptor. This is only + /// valid until the reader source is modified in some way. + pub fn locationReader( + self: *const Self, + loc: external.LocationDescriptor, + ) !LimitedReader { + try self.source.seekableStream().seekTo(loc.rva); + return .{ + .inner_reader = self.source.reader(), + .bytes_left = loc.data_size, + }; + } }; } diff --git a/src/crash/minidump/stream_threadlist.zig b/src/crash/minidump/stream_threadlist.zig index e74d11e3e..51f3f9d4c 100644 --- a/src/crash/minidump/stream_threadlist.zig +++ b/src/crash/minidump/stream_threadlist.zig @@ -89,6 +89,7 @@ pub fn ThreadListReader(comptime R: type) type { test "minidump: threadlist" { const testing = std.testing; + const alloc = testing.allocator; var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); const R = Reader(*@TypeOf(fbs)); @@ -107,5 +108,10 @@ test "minidump: threadlist" { for (0..v.count) |i| { const t = try v.thread(i); log.warn("thread i={} thread={}", .{ i, t }); + + // Read our stack memory + var stack_reader = try r.locationReader(t.stack.memory); + const bytes = try stack_reader.reader().readAllAlloc(alloc, t.stack.memory.data_size); + defer alloc.free(bytes); } }