Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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 {
Expand All @@ -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');
}
Expand All @@ -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));
}
Expand All @@ -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);
Expand Down
34 changes: 19 additions & 15 deletions lib/minidump.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}

Expand Down
44 changes: 44 additions & 0 deletions test/minidump-test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
});
});
});
});
});

Expand Down