-
-
Notifications
You must be signed in to change notification settings - Fork 458
Open
Description
Summary
I discovered an uncaught TypeError in the commit() function that causes the Node.js process to crash. This occurs when the .git/HEAD file exists but its content is read as null (e.g., file is empty or corrupted).
Location
The issue is located in src/commands/commit.js. The code attempts to call .startsWith() on headContent without checking if it is null.
isomorphic-git/src/commands/commit.js
Line 67 in e96e940
| detachedHead = !headContent.startsWith('ref:') |
Stack Trace
TypeError: Cannot read properties of null (reading 'startsWith')
at _commit (node_modules/isomorphic-git/index.cjs:5857:33)
at Object.commit (node_modules/isomorphic-git/index.cjs:10300:12)
Reproduction
reproduce.js:
#!/usr/bin/env node
// Standalone reproduce script for isomorphic-git fuzzing crashes
// Usage: node reproduce.js <crash-file>
const git = require('isomorphic-git');
const fs = require('fs');
const path = require('path');
const os = require('os');
function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.error('Usage: node reproduce.js <crash-file>');
console.error('');
console.error('Reproduces a crash from a fuzzer-generated crash file.');
console.error('');
console.error('Examples:');
console.error(' node reproduce.js crash-6211a363adb6f9c3b44181785257b6428b03f838');
console.error(' node reproduce.js corpus/seed1_init');
process.exit(1);
}
const crashFile = args[0];
// Check if file exists
if (!fs.existsSync(crashFile)) {
console.error(`Error: File not found: ${crashFile}`);
process.exit(1);
}
// Read the crash file
let data;
try {
data = fs.readFileSync(crashFile);
} catch (err) {
console.error(`Error reading file: ${err.message}`);
process.exit(1);
}
console.log(`Reproducing crash from: ${crashFile}`);
console.log(`File size: ${data.length} bytes`);
if (data.length < 2) {
console.log('File too small (< 2 bytes), nothing to reproduce');
process.exit(0);
}
console.log(`First byte (fuzzer type): ${data[0]} (${getFuzzerTypeName(data[0] % 3)})`);
console.log('');
// Run the fuzzer with the crash input
try {
runFuzzTarget(data);
console.log('Execution completed without crash');
} catch (err) {
console.log('=================================');
console.log('CRASH REPRODUCED!');
console.log('=================================');
console.log('');
console.log('Error:', err.message);
console.log('');
console.log('Stack trace:');
console.log(err.stack);
process.exit(1);
}
}
function runFuzzTarget(data) {
const fuzzerType = data[0] % 3;
const remainingData = data.slice(1);
switch (fuzzerType) {
case 0:
fuzzGitOperations(remainingData);
break;
case 1:
fuzzObjectParsing(remainingData);
break;
case 2:
fuzzRefParsing(remainingData);
break;
}
}
function fuzzGitOperations(data) {
const opsStr = data.toString('utf-8');
const ops = opsStr.split('|').map(s => s.trim()).filter(s => s);
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-fuzz-'));
try {
const gitDir = path.join(tmpDir, '.git');
fs.mkdirSync(gitDir, { recursive: true });
fs.mkdirSync(path.join(gitDir, 'objects'), { recursive: true });
fs.mkdirSync(path.join(gitDir, 'refs', 'heads'), { recursive: true });
fs.writeFileSync(path.join(gitDir, 'HEAD'), 'ref: refs/heads/master\n');
fs.writeFileSync(path.join(tmpDir, 'test.txt'), 'initial content');
for (const op of ops) {
try {
switch (op) {
case 'init':
break;
case 'add':
git.add({ fs, dir: tmpDir, filepath: 'test.txt' });
break;
case 'remove':
git.remove({ fs, dir: tmpDir, filepath: 'test.txt' });
break;
case 'commit':
git.commit({
fs,
dir: tmpDir,
message: 'test commit',
author: { name: 'Fuzz', email: '[email protected]' }
});
break;
case 'branch':
git.branch({ fs, dir: tmpDir, ref: 'test-branch', checkout: false });
break;
case 'checkout':
git.checkout({ fs, dir: tmpDir, ref: 'test-branch' });
break;
case 'log':
git.log({ fs, dir: tmpDir });
break;
case 'status':
git.status({ fs, dir: tmpDir, filepath: 'test.txt' });
break;
}
} catch (e) {
// Individual operation errors are expected
}
}
} finally {
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch (e) {}
}
}
function fuzzObjectParsing(data) {
const content = data.toString('utf-8');
try {
if (content.startsWith('tree ') || content.includes('author ')) {
// Commit object format check
}
} catch (e) {}
try {
if (content.includes('\x00') && content.length >= 21) {
// Tree format check
}
} catch (e) {}
try {
const hexStr = content.slice(0, 40);
if (/^[0-9a-fA-F]{40}$/.test(hexStr)) {
// Valid SHA-1 format
}
} catch (e) {}
}
function fuzzRefParsing(data) {
const refStr = data.toString('utf-8').trim();
const validRefPatterns = [
/^refs\/heads\/.+/,
/^refs\/tags\/.+/,
/^refs\/remotes\/.+/,
/^[0-9a-fA-F]{40}$/,
/^[0-9a-fA-F]{7,16}$/
];
for (const pattern of validRefPatterns) {
if (pattern.test(refStr)) {
break;
}
}
}
function getFuzzerTypeName(type) {
switch (type) {
case 0:
return 'Git Operations';
case 1:
return 'Object Parsing';
case 2:
return 'Ref Parsing';
default:
return 'Unknown';
}
}
if (require.main === module) {
main();
}base64ed poc file crash-6211a363adb6f9c3b44181785257b6428b03f838 :
AGluaXR8YWRkfGNvbW1pdHxicmFuY2h8Y2hlY2tvdXQ=use
echo "AGluaXR8YWRkfGNvbW1pdHxicmFuY2h8Y2hlY2tvdXQ=" | base64 -d > crash-6211a363adb6f9c3b44181785257b6428b03f838 then run
node reproduce.js crash-6211a363adb6f9c3b44181785257b6428b03f838 Suggested Fix
Add a null check for headContent before accessing its properties:
// Current:
detachedHead = !headContent.startsWith('ref:');
// Suggested:
detachedHead = !headContent || !headContent.startsWith('ref:');Environment
- isomorphic-git: v1.37.1
- Node.js: v22.18.0
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels