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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions