Subject: Re: [PATCH 0/4] plugging some mmap() leaks

From: Ramsay Jones <[email-masked]>
Date: Fri, 6 Mar 2026 04:37:49 +0000

On 05/03/2026 11:03 pm, Jeff King wrote:
> On Thu, Mar 05, 2026 at 05:02:14PM -0500, Jeff King wrote:
> 
>> Anyway, I think the solution is probably something like the patch above,
>> though probably it needs to cover the case where new_pack is NULL.
> 
> So here is a more polished version. I decided to try running the whole
> test suite with leak-checking and NO_MMAP, and it turned up one other
> case. This series fixes that, too, and then turns on the flag for all
> leak-checking builds.
> 

Hmm, this gives me flash-backs. ;)

Many moons ago, when the cygwin build routinely set NO_MMAP I had an
valgrind build of git fail with a 'double free' caused by a call to
git_munmap() for a pointer that had already been git_munmap-ed!

In addition, the failure was not reproducible (or at least I could not
find such a test). This was at a time when the testsuite took 4+ hours
to run for a regular build, let alone a valgrind build. So, to try and
pin down the failure, I created a debug version of the mmap compat
functions, which I ran with for several weeks, without failing ... :(

It just so happens that about this time I was also testing running the
cygwin build without NO_MMAP set. This was a success, so I dropped
the NO_MMAP investigation, never having found the cause of the failure!

I have had the 'mmap' branch, with a version of the debug patch, in my
cygwin repo for ever (well, the 'author date' says sep 9th 2012, but I
know it was somewhat before then). This version of the patch removed the
'debug' output and was only concerned with the error return behaviour of
the 'emulated' syscalls. (it was also somewhat non-performant if you had
many mmap's; luckily, that wasn't the case then, and I tended to git-gc
very often - which I still do to this day!)

Anyway, just some food for thought. I have nearly deleted that branch
many times. I should probably do that now! (Hmm, patch given below just
FYI).

Thanks.

ATB,
Ramsay Jones

-------- >8 --------
>From 40442aa06901720ec55005144438c8c733025cbb Mon Sep 17 00:00:00 2001
From: Ramsay Jones <[email-masked]>
Date: Sun, 9 Sep 2012 20:50:32 +0100
Subject: [PATCH] mmap.c: log mmap() blocks to avoid double-delete bug

When compiling with the NO_MMAP build variable set, the built-in
'git_mmap()' and 'git_munmap()' compatability routines use simple
memory allocation and file I/O to emulate the required behaviour.
The current implementation is vunerable to the "double-delete" bug
(where the pointer returned by malloc() is passed to free() two or
more times), should the mapped memory block address be passed to
munmap() multiple times.

In order to guard the implementation from such a calling sequence,
we keep a list of mmap-block descriptors, which we then consult to
determine the validity of the input pointer to munmap(). This then
allows 'git_munmap()' to return -1 on error, as required, with
errno set to EINVAL.

Using a list in the log of mmap-ed blocks, along with the resulting
linear search, means that the performance of the code is directly
proportional to the number of concurrently active memory mapped
file regions. The number of such regions is not expected to be
excessive.

Signed-off-by: Ramsay Jones <[email-masked]>
---
 compat/mmap.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 56 insertions(+), 1 deletion(-)

diff --git a/compat/mmap.c b/compat/mmap.c
index 7f662fef7b..137c6dc005 100644
--- a/compat/mmap.c
+++ b/compat/mmap.c
@@ -1,14 +1,61 @@
 #include "../git-compat-util.h"
 
+struct mmbd {  /* memory mapped block descriptor */
+	struct mmbd *next;  /* next in list */
+	void   *start;      /* pointer to memory mapped block */
+	size_t length;      /* length of memory mapped block */
+};
+
+static struct mmbd *head;  /* head of mmb descriptor list */
+
+
+static void add_desc(struct mmbd *desc, void *start, size_t length)
+{
+	desc->start = start;
+	desc->length = length;
+	desc->next = head;
+	head = desc;
+}
+
+static void free_desc(struct mmbd *desc)
+{
+	if (head == desc)
+		head = head->next;
+	else {
+		struct mmbd *d = head;
+		for (; d; d = d->next) {
+			if (d->next == desc) {
+				d->next = desc->next;
+				break;
+			}
+		}
+	}
+	free(desc);
+}
+
+static struct mmbd *find_desc(void *start)
+{
+	struct mmbd *d = head;
+	for (; d; d = d->next) {
+		if (d->start == start)
+			return d;
+	}
+	return NULL;
+}
+
 void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
 {
 	size_t n = 0;
+	struct mmbd *desc = NULL;
 
 	if (start != NULL || !(flags & MAP_PRIVATE))
 		die("Invalid usage of mmap when built with NO_MMAP");
 
 	start = xmalloc(length);
-	if (start == NULL) {
+	desc = xmalloc(sizeof(*desc));
+	if (!start || !desc) {
+		free(start);
+		free(desc);
 		errno = ENOMEM;
 		return MAP_FAILED;
 	}
@@ -23,18 +70,26 @@ void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t of
 
 		if (count < 0) {
 			free(start);
+			free(desc);
 			errno = EACCES;
 			return MAP_FAILED;
 		}
 
 		n += count;
 	}
+	add_desc(desc, start, length);
 
 	return start;
 }
 
 int git_munmap(void *start, size_t length)
 {
+	struct mmbd *d = find_desc(start);
+	if (!d) {
+		errno = EINVAL;
+		return -1;
+	}
+	free_desc(d);
 	free(start);
 	return 0;
 }
-- 
2.53.0