diff --git a/lib/format.js b/lib/format.js index 942ba79..d04e68b 100644 --- a/lib/format.js +++ b/lib/format.js @@ -13,6 +13,14 @@ if (!Buffer.prototype.readBigUInt64LE) { }; } +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) { @@ -30,6 +38,7 @@ function readHeader(buf) { // 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), @@ -39,6 +48,7 @@ function readDirectory(buf, rva) { // 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), @@ -63,6 +73,7 @@ function readRawModule(buf, rva) { // 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), @@ -83,6 +94,7 @@ function readVersionInfo(buf, base) { // 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), @@ -118,8 +130,10 @@ function debugIdFromGuidAndAge(guid, age) { // 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 { @@ -138,6 +152,7 @@ function readCVRecord(buf, { rva, data_size: dataSize }) { // 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'); } @@ -150,10 +165,12 @@ export const streamTypes = { 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)); } @@ -163,11 +180,16 @@ const streamTypeProcessors = { }; 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); diff --git a/lib/minidump.js b/lib/minidump.js index c0fac7a..b5d113b 100644 --- a/lib/minidump.js +++ b/lib/minidump.js @@ -51,21 +51,25 @@ export const addSymbolPath = Array.prototype.push.bind(globalSymbolPaths); export function moduleList(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); + try { + 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); + } catch (e) { + callback(e); + } }); } diff --git a/test/minidump-test.js b/test/minidump-test.js index 5b2007d..7072eb8 100644 --- a/test/minidump-test.js +++ b/test/minidump-test.js @@ -1,4 +1,5 @@ import assert from 'node:assert'; +import fs from 'node:fs'; import path from 'node:path'; import * as minidump from '../lib/minidump.js'; @@ -150,6 +151,49 @@ describe('minidump', function () { }); }); }); + + describe('on a malformed dump', () => { + function writeDump(buf) { + const dumpPath = temp.path({ suffix: '.dmp' }); + fs.writeFileSync(dumpPath, buf); + return dumpPath; + } + + it('returns an error for a truncated header', function (done) { + const dumpPath = writeDump(Buffer.alloc(16)); + minidump.moduleList(dumpPath, (err, modules) => { + assert(err instanceof Error, 'expected an Error'); + assert.match(err.message, /too small/); + assert.equal(modules, undefined); + done(); + }); + }); + + it('returns an error for an invalid magic signature', function (done) { + const dumpPath = writeDump(Buffer.alloc(32)); + minidump.moduleList(dumpPath, (err, modules) => { + assert(err instanceof Error, 'expected an Error'); + assert.equal(err.message, 'not a minidump file'); + assert.equal(modules, undefined); + done(); + }); + }); + + it('returns an error for an out-of-bounds stream_directory_rva', function (done) { + const buf = Buffer.alloc(32); + buf.writeUInt32LE(0x504d444d, 0); // 'MDMP' + buf.writeUInt32LE(0xa793, 4); // version + buf.writeUInt32LE(1, 8); // stream_count + buf.writeUInt32LE(0x1000, 12); // stream_directory_rva — far past EOF + const dumpPath = writeDump(buf); + minidump.moduleList(dumpPath, (err, modules) => { + assert(err instanceof Error, 'expected an Error'); + assert.match(err.message, /out of bounds/); + assert.equal(modules, undefined); + done(); + }); + }); + }); }); });