Skip to content

Uncaught TypeError in commit() causes process crash when HEAD content is null #2287

@N0zoM1z0

Description

@N0zoM1z0

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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions