From e95fa6bb30edf5a26f62ea9eaebe70189e2a6e37 Mon Sep 17 00:00:00 2001 From: Jeremy Apthorp Date: Tue, 2 Jul 2019 14:42:52 -0700 Subject: [PATCH] feat: add a function for retrieving the module list from a minidump (#33) --- build.js | 4 +- lib/format.js | 177 ++++++++++++++++++++++++++++++++++++++++++ lib/minidump.js | 28 ++++++- test/minidump-test.js | 42 +++++++++- 4 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 lib/format.js diff --git a/build.js b/build.js index b65a49d..ae4f8ae 100644 --- a/build.js +++ b/build.js @@ -1,10 +1,10 @@ const fs = require('fs') const path = require('path') -const {spawnSync} = require('child_process') +const { spawnSync } = require('child_process') const buildDir = path.join(__dirname, 'build') if (!fs.existsSync(buildDir)) { - fs.mkdirSync(buildDir, {recursive: true}) + fs.mkdirSync(buildDir, { recursive: true }) } spawnSync(path.join(__dirname, 'deps', 'breakpad', 'configure'), [], { cwd: buildDir, diff --git a/lib/format.js b/lib/format.js new file mode 100644 index 0000000..8cb2b7e --- /dev/null +++ b/lib/format.js @@ -0,0 +1,177 @@ +// 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)) + } +} + +// 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) { + 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) { + 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) { + 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) { + 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 + const cv_signature = buf.readUInt32LE(rva) + if (cv_signature !== 0x53445352 /* SDSR */) { + const age = buf.readUInt32LE(rva + 4 + 16) + const guid = readGUID(buf.subarray(rva + 4, rva + 4 + 16)) + return { + cv_signature, + 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} + } +} + +// 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 + 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 +const streamTypes = { + MD_MODULE_LIST_STREAM: 4, +} + +const streamTypeProcessors = { + [streamTypes.MD_MODULE_LIST_STREAM]: (stream, buf) => { + 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 + for (let i = 0; i < numModules; i++) { + modules.push(readRawModule(buf, base + i * size)) + } + stream.modules = modules + return stream + } +} + +module.exports.readMinidump = function readMinidump (buf) { + const header = readHeader(buf) + if (header.signature !== headerMagic) { + throw new Error('not a minidump file') + } + + 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 } +} + +module.exports.streamTypes = streamTypes diff --git a/lib/minidump.js b/lib/minidump.js index e2eb3e3..078c4b8 100644 --- a/lib/minidump.js +++ b/lib/minidump.js @@ -1,6 +1,7 @@ -var fs = require('fs') -var path = require('path') -var spawn = require('child_process').spawn +const fs = require('fs') +const path = require('path') +const spawn = require('child_process').spawn +const format = require('./format') const exe = process.platform === 'win32' ? '.exe' : '' const commands = { @@ -36,6 +37,27 @@ function execute (command, args, callback) { var globalSymbolPaths = [] module.exports.addSymbolPath = Array.prototype.push.bind(globalSymbolPaths) +module.exports.moduleList = function (minidump, callback) { + fs.readFile(minidump, (err, data) => { + if (err) return callback(err) + const { streams } = format.readMinidump(data) + const moduleList = streams.find(s => s.type === format.streamTypes.MD_MODULE_LIST_STREAM) + if (!moduleList) return callback(new Error('minidump does not contain module list')) + const modules = moduleList.modules.map(m => { + const mod = { + version: m.version, + name: m.name + } + if (m.cv_record) { + mod.pdb_file_name = m.cv_record.pdb_file_name + mod.debug_identifier = m.cv_record.debug_file_id + } + return mod + }) + callback(null, modules) + }) +} + module.exports.walkStack = function (minidump, symbolPaths, callback, commandArgs) { if (!callback) { callback = symbolPaths diff --git a/test/minidump-test.js b/test/minidump-test.js index 34e2a37..1798e94 100644 --- a/test/minidump-test.js +++ b/test/minidump-test.js @@ -88,6 +88,44 @@ describe('minidump', function () { }) }) }) + + describe('moduleList()', function () { + describe('on a Linux dump', () => { + it('calls back with a module list', function (done) { + const dumpPath = path.join(__dirname, 'fixtures', 'linux.dmp') + minidump.moduleList(dumpPath, (err, modules) => { + if (err) return done(err) + assert.notEqual(modules.length, 0) + assert(modules.some(m => m.name.endsWith('/electron'))) + done() + }) + }) + }) + + describe('on a Windows dump', () => { + it('calls back with a module list', function (done) { + const dumpPath = path.join(__dirname, 'fixtures', 'windows.dmp') + minidump.moduleList(dumpPath, (err, modules) => { + if (err) return done(err) + assert.notEqual(modules.length, 0) + assert(modules.some(m => m.name.endsWith('\\electron.exe'))) + done() + }) + }) + }) + + describe('on a macOS dump', () => { + it('calls back with a module list', function (done) { + const dumpPath = path.join(__dirname, 'fixtures', 'mac.dmp') + minidump.moduleList(dumpPath, (err, modules) => { + if (err) return done(err) + assert.notEqual(modules.length, 0) + assert(modules.some(m => m.name.endsWith('/Electron Helper'))) + done() + }) + }) + }) + }) }) var downloadElectron = function (callback) { @@ -100,7 +138,7 @@ var downloadElectron = function (callback) { if (error) return callback(error) var electronPath = temp.mkdirSync('node-minidump-') - extractZip(zipPath, {dir: electronPath}, function (error) { + extractZip(zipPath, { dir: electronPath }, function (error) { if (error) return callback(error) if (process.platform === 'darwin') { @@ -123,7 +161,7 @@ var downloadElectronSymbols = function (platform, callback) { if (error) return callback(error) var symbolsPath = temp.mkdirSync('node-minidump-') - extractZip(zipPath, {dir: symbolsPath}, function (error) { + extractZip(zipPath, { dir: symbolsPath }, function (error) { if (error) return callback(error) callback(null, path.join(symbolsPath, 'electron.breakpad.syms')) })