Skip to content

Crash in GCDiff _cacheDeltasIfNeeded when git index is locked (GIT_ELOCKED) #2768

@macdrevx

Description

@macdrevx

(via claude)

Crash Report

GitUp crashes with NSInvalidArgumentException: capacity (18446744073709551615) is ridiculous in -[GCDiff _cacheDeltasIfNeeded] when another process holds the git index lock.

Exception

Exception Type:    EXC_CRASH (SIGABRT)
Exception Reason:  *** -[__NSPlaceholderArray initWithCapacity:]: capacity (18446744073709551615) is ridiculous

Crash Backtrace

0  CoreFoundation  -[__NSPlaceholderArray initWithCapacity:] + 376
1  GitUpKit        -[GCDiff _cacheDeltasIfNeeded] + 92
2  GitUpKit        -[GCDiff deltas] + 20
3  GitUpKit        -[GIAdvancedCommitViewController _reloadContents] + 132
4  GitUpKit        -[GIAdvancedCommitViewController repositoryStatusDidUpdate] + 68
5  ...
6  GitUpKit        -[GCLiveRepository _updateStatus:] + 1068
7  GitUpKit        -[GCLiveRepository _notifyWorkingDirectoryChanged:gitDirectoryChanged:] + 344

Root Cause Analysis

The crash is caused by a chain of events when the git index file is locked by another process:

  1. git_diff_index_to_workdir() is called with the GIT_DIFF_UPDATE_INDEX flag (in GCDiff.m, both diffWorkingDirectoryWithIndex: and diffWorkingDirectoryWithCommit:usingIndex:)
  2. The diff computation succeeds, but git_index_write() fails with GIT_ELOCKED because another process holds the lock
  3. In the bundled libgit2 (diff_generate.c:1513-1518), on GIT_ELOCKED after diff computation, the function jumps to its cleanup label which frees the computed diff and leaves *out = NULL
  4. GitUp's code overrides GIT_ELOCKEDGIT_OK, assuming the diff pointer is still valid — but it is now NULL
  5. A GCDiff object is created wrapping a NULL git_diff* pointer
  6. Later, git_diff_num_deltas(NULL) triggers GIT_ASSERT_ARG(diff) in assert_safe.h. In release builds, this macro returns -1 rather than crashing
  7. Since git_diff_num_deltas returns size_t (unsigned), -1 becomes 18446744073709551615 (0xFFFFFFFFFFFFFFFF)
  8. [[NSMutableArray alloc] initWithCapacity:0xFFFFFFFFFFFFFFFF] throws the "capacity is ridiculous" exception

Affected Code

GCDiff.mdiffWorkingDirectoryWithIndex: (line ~600):

int status = git_diff_index_to_workdir(outDiff, self.private, index.private, diffOptions);
if (status == GIT_ELOCKED) {
    status = GIT_OK;  // BUG: *outDiff is NULL here, diff was freed by libgit2
}

GCDiff.mdiffWorkingDirectoryWithCommit:usingIndex: (line ~562):
Same pattern — also has a secondary bug where git_diff_merge(*outDiff, diff2) is called with a NULL diff2, then *outDiff is freed but the pointer is not NULLed out, causing a double-free in _diffWithType:'s cleanup.

Proposed Fix

Primary fix: When git_diff_index_to_workdir returns GIT_ELOCKED, retry without the GIT_DIFF_UPDATE_INDEX flag to obtain a valid diff:

int status = git_diff_index_to_workdir(outDiff, self.private, index.private, diffOptions);
if (status == GIT_ELOCKED) {
    // On GIT_ELOCKED, libgit2 frees the diff and sets *outDiff to NULL.
    // Retry without the flag to get a valid diff.
    diffOptions->flags &= ~GIT_DIFF_UPDATE_INDEX;
    status = git_diff_index_to_workdir(outDiff, self.private, index.private, diffOptions);
}

Secondary fix (defense-in-depth): Add a NULL check in _cacheDeltasIfNeeded:

- (void)_cacheDeltasIfNeeded {
  if (_deltas == nil) {
    if (_private == NULL) {
      XLOG_DEBUG_UNREACHABLE();
      _deltas = [[NSMutableArray alloc] init];
      return;
    }
    size_t count = git_diff_num_deltas(_private);
    // ...

Tertiary fix: In the diffWorkingDirectoryWithCommit: block, NULL out *outDiff after freeing it to prevent double-free:

if (status != GIT_OK) {
    git_diff_free(*outDiff);
    *outDiff = NULL;  // Prevent double-free in _diffWithType: cleanup
}

Environment

  • GitUp 1.4.3 (build 1052)
  • macOS 26.4.1 (25E253), ARM64

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