minidump
Version:
Read and process minidump file
202 lines (184 loc) • 7.42 kB
JavaScript
// Just enough of the minidump format to extract module names + debug
// identifiers so we can download pdbs
const headerMagic = Buffer.from('MDMP').readUInt32LE(0);
if (!Buffer.prototype.readBigUInt64LE) {
Buffer.prototype.readBigUInt64LE = function (offset) {
// ESLint doesn't support BigInt yet
// eslint-disable-next-line
return (
BigInt(this.readUInt32LE(offset)) + (BigInt(this.readUInt32LE(offset + 4)) << BigInt(32))
);
};
}
function assertRange(buf, offset, length, label) {
if (!Number.isInteger(offset) || offset < 0 || offset + length > buf.length) {
throw new Error(
`malformed minidump: ${label} at offset ${offset} (len ${length}) is out of bounds`,
);
}
}
// MDRawHeader
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#252
function readHeader(buf) {
return {
signature: buf.readUInt32LE(0),
version: buf.readUInt32LE(4),
stream_count: buf.readUInt32LE(8),
stream_directory_rva: buf.readUInt32LE(12),
checksum: buf.readUInt32LE(16),
time_date_stamp: buf.readUInt32LE(20),
flags: buf.readBigUInt64LE(24),
};
}
// MDRawDirectory
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#305
function readDirectory(buf, rva) {
assertRange(buf, rva, 12, 'stream directory entry');
return {
type: buf.readUInt32LE(rva),
location: readLocationDescriptor(buf, rva + 4),
};
}
// MDRawModule
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#386
function readRawModule(buf, rva) {
assertRange(buf, rva, 24 + 13 * 4 + 8 + 8, 'module record');
const module = {
base_of_image: buf.readBigUInt64LE(rva),
size_of_image: buf.readUInt32LE(rva + 8),
checksum: buf.readUInt32LE(rva + 12),
time_date_stamp: buf.readUInt32LE(rva + 16),
module_name_rva: buf.readUInt32LE(rva + 20),
version_info: readVersionInfo(buf, rva + 24),
cv_record: readCVRecord(buf, readLocationDescriptor(buf, rva + 24 + 13 * 4)),
misc_record: readLocationDescriptor(buf, rva + 24 + 13 * 4 + 8),
};
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2255
module.version = [
module.version_info.file_version_hi >> 16,
module.version_info.file_version_hi & 0xffff,
module.version_info.file_version_lo >> 16,
module.version_info.file_version_lo & 0xffff,
].join('.');
module.name = readString(buf, module.module_name_rva);
return module;
}
// MDVSFixedFileInfo
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#129
function readVersionInfo(buf, base) {
assertRange(buf, base, 40, 'version info');
return {
signature: buf.readUInt32LE(base),
struct_version: buf.readUInt32LE(base + 4),
file_version_hi: buf.readUInt32LE(base + 8),
file_version_lo: buf.readUInt32LE(base + 12),
product_version_hi: buf.readUInt32LE(base + 16),
product_version_lo: buf.readUInt32LE(base + 20),
file_flags_mask: buf.readUInt32LE(base + 24),
file_flags: buf.readUInt32LE(base + 28),
file_os: buf.readUInt32LE(base + 32),
file_type: buf.readUInt32LE(base + 24),
file_subtype: buf.readUInt32LE(base + 28),
file_date_hi: buf.readUInt32LE(base + 32),
file_date_lo: buf.readUInt32LE(base + 36),
};
}
// MDLocationDescriptor
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#237
function readLocationDescriptor(buf, base) {
assertRange(buf, base, 8, 'location descriptor');
return {
data_size: buf.readUInt32LE(base),
rva: buf.readUInt32LE(base + 4),
};
}
// MDGUID
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#81
function readGUID(buf) {
return {
data1: buf.readUInt32LE(0),
data2: buf.readUInt16LE(4),
data3: buf.readUInt16LE(6),
data4: [...buf.subarray(8)],
};
}
// guid_and_age_to_debug_id
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2153
function debugIdFromGuidAndAge(guid, age) {
return [
guid.data1.toString(16).padStart(8, '0'),
guid.data2.toString(16).padStart(4, '0'),
guid.data3.toString(16).padStart(4, '0'),
...guid.data4.map((x) => x.toString(16).padStart(2, '0')),
age.toString(16),
]
.join('')
.toUpperCase();
}
// MDCVInfo{PDB70,ELF}
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#426
function readCVRecord(buf, { rva, data_size: dataSize }) {
if (rva === 0) return;
assertRange(buf, rva, 4, 'cv record signature');
const cvSignature = buf.readUInt32LE(rva);
if (cvSignature !== 0x53445352 /* SDSR */) {
assertRange(buf, rva + 4, 16 + 4, 'cv record guid/age');
const age = buf.readUInt32LE(rva + 4 + 16);
const guid = readGUID(buf.subarray(rva + 4, rva + 4 + 16));
return {
cv_signature: cvSignature,
guid,
age,
pdb_file_name: buf.subarray(rva + 4 + 16 + 4, rva + dataSize - 1).toString('utf8'),
debug_file_id: debugIdFromGuidAndAge(guid, age),
};
} else {
return { cv_signature: cvSignature };
}
}
// MDString
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#357
function readString(buf, rva) {
if (rva === 0) return null;
assertRange(buf, rva, 4, 'string length');
const bytes = buf.readUInt32LE(rva);
return buf.subarray(rva + 4, rva + 4 + bytes).toString('utf16le');
}
// MDStreamType
// https://chromium.googlesource.com/breakpad/breakpad/+/refs/heads/master/src/google_breakpad/common/minidump_format.h#310
export const streamTypes = {
MD_MODULE_LIST_STREAM: 4,
};
const streamTypeProcessors = {
[streamTypes.MD_MODULE_LIST_STREAM]: (stream, buf) => {
assertRange(buf, stream.location.rva, 4, 'module list count');
const numModules = buf.readUInt32LE(stream.location.rva);
const modules = [];
const size = 8 + 4 + 4 + 4 + 4 + 13 * 4 + 8 + 8 + 8 + 8;
const base = stream.location.rva + 4;
assertRange(buf, base, numModules * size, 'module list');
for (let i = 0; i < numModules; i++) {
modules.push(readRawModule(buf, base + i * size));
}
stream.modules = modules;
return stream;
},
};
export function readMinidump(buf) {
if (!Buffer.isBuffer(buf) || buf.length < 32) {
throw new Error('minidump too small');
}
const header = readHeader(buf);
if (header.signature !== headerMagic) {
throw new Error('not a minidump file');
}
assertRange(buf, header.stream_directory_rva, header.stream_count * 12, 'stream directory');
const streams = [];
for (let i = 0; i < header.stream_count; i++) {
const stream = readDirectory(buf, header.stream_directory_rva + i * 12);
if (stream.type !== 0) {
streams.push((streamTypeProcessors[stream.type] || ((s) => s))(stream, buf));
}
}
return { header, streams };
}