[BUG] NULL deref in `git_config_list_append` on allocation failure · Issue #7416 · libgit2/libgit2 · GitHub
Skip to content

[BUG] NULL deref in git_config_list_append on allocation failure #7416

Open

Description

@bunch-suffice-ivy

libgit2 commit 07b89d1 src/libgit2/config_list.c:174-178.

Bug

The git__calloc for a new map_head is unchecked. On OOM, NULL is stored in the hashmap and then written through, so the process gets SIGSEGV where it should get -1:

map_head = git__calloc(1, sizeof(*map_head));                 /* 174: may be NULL */
if ((git_config_list_headmap_put(&config_list->map,
         entry->base.entry.name, map_head)) < 0)
    return -1;
...
map_head->entry = entry;                                      /* 178: NULL deref */

Every other allocation in config_list.c uses GIT_ERROR_CHECK_ALLOC.

Reproduce

00.c installs a counting allocator through GIT_OPT_SET_ALLOCATOR that returns NULL for the Nth allocation, which looks exactly like a real OOM to libgit2. It then runs open → snapshot → get on a small config for N = 1..200. Failures at checked sites return clean errors. The unchecked site crashes. The crashing N depends on the build, which is why the program scans instead of hard-coding it.

gcc -g -O0 00.c -I"$LIBGIT2_INCLUDE" "$LIBGIT2_LIBDIR/libgit2.a" -lz -lpthread -ldl -o 00_c
./00_c                                        # scan
gdb -ex run -ex bt --args ./00_c <cfg> <N>    # single index
  alloc #1: git_config_open_ondisk -> clean error -1 (handled)
  ...
  alloc #21: git_config_open_ondisk -> clean error -1 (handled)
Segmentation fault                            # N = 22, exit 139

Program received signal SIGSEGV, Segmentation fault.
git_config_list_append (...) at src/libgit2/config_list.c:178
178     map_head->entry = entry;
00.c
#include <git2.h>
#include <git2/sys/alloc.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MAXN 200

static long g_fail_at; /* fail this (1-based) allocation; 0 = pass-through */
static long g_count;

static void *counting_malloc(size_t n, const char *file, int line)
{
	(void)file; (void)line;
	if (g_fail_at && ++g_count == g_fail_at)
		return NULL;
	return malloc(n ? n : 1);
}

static void *counting_realloc(void *p, size_t n, const char *file, int line)
{
	(void)file; (void)line;
	if (g_fail_at && ++g_count == g_fail_at)
		return NULL;
	return realloc(p, n ? n : 1);
}

static void counting_free(void *p)
{
	free(p);
}

/* One run: open config, snapshot, read a value, with the Nth libgit2
 * allocation failed. Returns 0 ok, 2 clean error (handled), or dies by
 * SIGSEGV when the failed allocation is the unchecked site. */
static int run_once(const char *path, long fail_at)
{
	git_config *cfg = NULL, *snap = NULL;
	const char *val = NULL;
	int rc;

	g_fail_at = fail_at; g_count = 0;

	rc = git_config_open_ondisk(&cfg, path);
	if (rc < 0) {
		printf("  alloc #%ld: git_config_open_ondisk -> clean error %d (handled)\n", fail_at, rc);
		return 2;
	}
	rc = git_config_snapshot(&snap, cfg);
	git_config_free(cfg);
	if (rc < 0) {
		printf("  alloc #%ld: git_config_snapshot -> clean error %d (handled)\n", fail_at, rc);
		return 2;
	}
	rc = git_config_get_string(&val, snap, "core.filemode");
	git_config_free(snap);
	printf("  alloc #%ld: open+snapshot+get -> %s\n", fail_at,
		(rc == 0 && val && strcmp(val, "true") == 0) ? "OK" : "WRONG RESULT");
	return 0;
}

int main(int argc, char **argv)
{
	const char *cfg_path = argc > 1 ? argv[1] : "/tmp/00_c.cfg";
	long only_n = argc > 2 ? atol(argv[2]) : 0;
	git_allocator alloc = { counting_malloc, counting_realloc, counting_free };
	FILE *f;
	long n;

	setvbuf(stdout, NULL, _IONBF, 0);

	f = fopen(cfg_path, "w");
	if (!f) { perror("config file"); return 1; }
	fputs("[core]\n\tfilemode = true\n\trepositoryformatversion = 0\n", f);
	fclose(f);

	if (git_libgit2_init() < 0 ||
	    git_libgit2_opts(GIT_OPT_SET_ALLOCATOR, &alloc) != 0) {
		fprintf(stderr, "00_c: libgit2 init / allocator setup failed\n");
		return 1;
	}

	printf("00_c: libgit2 %d.%d.%d, pure C (no git2-rs), config = %s\n",
		LIBGIT2_VERSION_MAJOR, LIBGIT2_VERSION_MINOR, LIBGIT2_VERSION_REVISION,
		cfg_path);

	if (run_once(cfg_path, 0) != 0) {          /* sanity: no-failure run */
		fprintf(stderr, "00_c: no-failure run failed -- setup broken\n");
		return 1;
	}

	/* Single-index mode: for gdb-based crash attribution. */
	if (only_n > 0) {
		printf("00_c: testing only alloc #%ld\n", only_n);
		return run_once(cfg_path, only_n) == 2 ? 2 : 0;
	}

	printf("00_c: scanning: fail the Nth libgit2 allocation, N = 1..%d;\n"
	       "  the process stops (SIGSEGV) at the first crashing index\n", MAXN);
	for (n = 1; n <= MAXN; n++) {
		if (run_once(cfg_path, n) == 0)
			continue;
		g_fail_at = 0;   /* handled: reset and try the next N */
	}
	printf("00_c: reached N=%ld without a crash -- bug did NOT reproduce\n", n);
	return 1;
}

Activity

  1. changed the title [-]NULL deref in `git_config_list_append` on allocation failure[/-] [+][BUG] NULL deref in `git_config_list_append` on allocation failure[/+] on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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