merge: Fix double free issue after merging the diffs by vincenzopalazzo · Pull Request #6589 · libgit2/libgit2 · GitHub
Skip to content

merge: Fix double free issue after merging the diffs#6589

Open
vincenzopalazzo wants to merge 2 commits into
libgit2:mainfrom
vincenzopalazzo:macros/double-free-patch
Open

merge: Fix double free issue after merging the diffs#6589
vincenzopalazzo wants to merge 2 commits into
libgit2:mainfrom
vincenzopalazzo:macros/double-free-patch

Conversation

@vincenzopalazzo

Copy link
Copy Markdown
Contributor

This commit addresses a double free issue that occurs when merging two diffs and free the diff objects.

The problem was caused from copying the pointer using a vector swap git function and then make a deep free of the original vector.

This operation made on a vector of pointers deallocated all the pointers inside the new vector where we swap the content to.

As a result, an invalid object was returned to the user.

The valgrid report.

==256010== HEAP SUMMARY:
==256010==     in use at exit: 696,247 bytes in 11,244 blocks
==256010==   total heap usage: 50,123 allocs, 38,880 frees, 4,419,142 bytes allocated
==256010==
==256010== Searching for pointers to 11,244 not-freed blocks
==256010== Checked 850,720 bytes
==256010==
==256010== 112 bytes in 1 blocks are definitely lost in loss record 69 of 163
==256010==    at 0x4841848: malloc (vg_replace_malloc.c:431)
==256010==    by 0x48964FD: stdalloc__malloc.lto_priv.0 (stdalloc.c:22)
==256010==    by 0x48D4C34: git_diff__delta_dup (diff_tform.c:23)
==256010==    by 0x48DAA22: git_diff__merge_like_cgit (diff_tform.c:81)
==256010==    by 0x48D5C14: git_diff__merge (diff_tform.c:160)
==256010==    by 0x1092A6: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==
==256010== LEAK SUMMARY:
==256010==    definitely lost: 112 bytes in 1 blocks
==256010==    indirectly lost: 0 bytes in 0 blocks
==256010==      possibly lost: 0 bytes in 0 blocks
==256010==    still reachable: 696,135 bytes in 11,243 blocks
==256010==         suppressed: 0 bytes in 0 blocks
==256010== Reachable blocks (those to which a pointer was found) are not shown.
==256010== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==256010==
==256010== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
==256010==
==256010== 1 errors in context 1 of 2:
==256010== Invalid free() / delete / delete[] / realloc()
==256010==    at 0x484412F: free (vg_replace_malloc.c:974)
==256010==    by 0x490D5B0: patch_parsed__free.part.0.lto_priv.0 (patch_parse.c:1150)
==256010==    by 0x48CC454: diff_parsed_free (diff_parse.c:21)
==256010==    by 0x1092EA: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==  Address 0x59e55b0 is 0 bytes inside a block of size 112 free'd
==256010==    at 0x484412F: free (vg_replace_malloc.c:974)
==256010==    by 0x48A17CD: git_vector_free_deep.part.0 (vector.c:99)
==256010==    by 0x48D5A2B: UnknownInlinedFun (vector.c:95)
==256010==    by 0x48D5A2B: git_diff__merge (diff_tform.c:193)
==256010==    by 0x1092A6: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==  Block was alloc'd at
==256010==    at 0x48469B3: calloc (vg_replace_malloc.c:1554)
==256010==    by 0x489652D: stdalloc__calloc.lto_priv.0 (stdalloc.c:42)
==256010==    by 0x48CCDE9: UnknownInlinedFun (patch_parse.c:1184)
==256010==    by 0x48CCDE9: git_diff_from_buffer (diff_parse.c:86)
==256010==    by 0x109219: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==
==256010== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Fixes: #6588
Reported-by: Eric Huss

@vincenzopalazzo vincenzopalazzo force-pushed the macros/double-free-patch branch 2 times, most recently from 9f725fd to 99da38d Compare July 9, 2023 15:09
@vincenzopalazzo

This comment was marked as off-topic.

@vincenzopalazzo vincenzopalazzo force-pushed the macros/double-free-patch branch from 99da38d to e305ab4 Compare July 12, 2023 16:04
@ethomson

Copy link
Copy Markdown
Member

@ethomson

Copy link
Copy Markdown
Member

Here's more concretely what I'm thinking -- this builds on your PR:

commit c852d166c02ca93e1753f67ee56b6dd3ba296ab7
Author: Edward Thomson <ethomson@edwardthomson.com>
Date:   Fri Jul 14 17:05:21 2023 +0100

    diff: provide a delta free function, use it in merge
    
    `git_diff_merge` needs to free the old deltas, which are created and
    managed differently for parsed vs generated deltas. Provide a function
    to free them and use it in the diff merge functionality.

diff --git a/src/libgit2/diff.h b/src/libgit2/diff.h
index f21b2764505e..2cd4feccb816 100644
--- a/src/libgit2/diff.h
+++ b/src/libgit2/diff.h
@@ -47,6 +47,7 @@ struct git_diff {
 	int (*entrycomp)(const void *a, const void *b);
 
 	int (*patch_fn)(git_patch **out, git_diff *diff, size_t idx);
+	void (*free_deltas_fn)(git_diff *diff);
 	void (*free_fn)(git_diff *diff);
 };
 
diff --git a/src/libgit2/diff_generate.c b/src/libgit2/diff_generate.c
index 78fe510e7482..6d52f34c24ae 100644
--- a/src/libgit2/diff_generate.c
+++ b/src/libgit2/diff_generate.c
@@ -424,15 +424,23 @@ static void diff_set_ignore_case(git_diff *diff, bool ignore_case)
 	git_vector_sort(&diff->deltas);
 }
 
+static void diff_generated_free_deltas(git_diff *d)
+{
+	git_diff_generated *diff = (git_diff_generated *)d;
+
+	git_vector_free_deep(&diff->base.deltas);
+	git_pool_clear(&diff->base.pool);
+}
+
 static void diff_generated_free(git_diff *d)
 {
 	git_diff_generated *diff = (git_diff_generated *)d;
 
+	diff_generated_free_deltas(d);
+
 	git_attr_session__free(&diff->base.attrsession);
-	git_vector_free_deep(&diff->base.deltas);
 
 	git_pathspec__vfree(&diff->pathspec);
-	git_pool_clear(&diff->base.pool);
 
 	git__memzero(diff, sizeof(*diff));
 	git__free(diff);
@@ -459,6 +467,7 @@ static git_diff_generated *diff_generated_alloc(
 	diff->base.old_src = old_iter->type;
 	diff->base.new_src = new_iter->type;
 	diff->base.patch_fn = git_patch_generated_from_diff;
+	diff->base.free_deltas_fn = diff_generated_free_deltas;
 	diff->base.free_fn = diff_generated_free;
 	git_attr_session__init(&diff->base.attrsession, repo);
 	memcpy(&diff->base.opts, &dflt, sizeof(git_diff_options));
diff --git a/src/libgit2/diff_parse.c b/src/libgit2/diff_parse.c
index 04603969e408..c95b4c51dbe1 100644
--- a/src/libgit2/diff_parse.c
+++ b/src/libgit2/diff_parse.c
@@ -11,7 +11,7 @@
 #include "patch.h"
 #include "patch_parse.h"
 
-static void diff_parsed_free(git_diff *d)
+static void diff_parsed_free_deltas(git_diff *d)
 {
 	git_diff_parsed *diff = (git_diff_parsed *)d;
 	git_patch *patch;
@@ -24,6 +24,13 @@ static void diff_parsed_free(git_diff *d)
 
 	git_vector_free(&diff->base.deltas);
 	git_pool_clear(&diff->base.pool);
+}
+
+static void diff_parsed_free(git_diff *d)
+{
+	git_diff_parsed *diff = (git_diff_parsed *)d;
+
+	diff_parsed_free_deltas(d);
 
 	git__memzero(diff, sizeof(*diff));
 	git__free(diff);
@@ -43,6 +50,7 @@ static git_diff_parsed *diff_parsed_alloc(git_oid_t oid_type)
 	diff->base.pfxcomp = git__prefixcmp;
 	diff->base.entrycomp = git_diff__entry_cmp;
 	diff->base.patch_fn = git_patch_parsed_from_diff;
+	diff->base.free_deltas_fn = diff_parsed_free_deltas;
 	diff->base.free_fn = diff_parsed_free;
 
 	if (git_diff_options_init(&diff->base.opts, GIT_DIFF_OPTIONS_VERSION) < 0) {
diff --git a/src/libgit2/diff_tform.c b/src/libgit2/diff_tform.c
index a651d47b861d..65181b13d40b 100644
--- a/src/libgit2/diff_tform.c
+++ b/src/libgit2/diff_tform.c
@@ -174,27 +174,30 @@ int git_diff__merge(
 			break;
 	}
 
-	if (!error) {
-		onto->deltas = onto_new;
-		onto->pool = onto_pool;
-
-		if ((onto->opts.flags & GIT_DIFF_REVERSE) != 0)
-			onto->old_src = from->old_src;
-		else
-			onto->new_src = from->new_src;
-
-		/* prefix strings also come from old pool, so recreate those.*/
-		onto->opts.old_prefix =
-			git_pool_strdup_safe(&onto->pool, onto->opts.old_prefix);
-		onto->opts.new_prefix =
-			git_pool_strdup_safe(&onto->pool, onto->opts.new_prefix);
-	} else {
-		/* In the case of an error, it is safe to perform a free the objects of the vector. */
+	if (error) {
 		git_vector_free_deep(&onto_new);
 		git_pool_clear(&onto_pool);
+
+		return error;
 	}
 
-	return error;
+	onto->free_deltas_fn(onto);
+
+	onto->deltas = onto_new;
+	onto->pool = onto_pool;
+
+	if ((onto->opts.flags & GIT_DIFF_REVERSE) != 0)
+		onto->old_src = from->old_src;
+	else
+		onto->new_src = from->new_src;
+
+	/* prefix strings also come from old pool, so recreate those.*/
+	onto->opts.old_prefix =
+		git_pool_strdup_safe(&onto->pool, onto->opts.old_prefix);
+	onto->opts.new_prefix =
+		git_pool_strdup_safe(&onto->pool, onto->opts.new_prefix);
+
+	return 0;
 }
 
 int git_diff_merge(git_diff *onto, const git_diff *from)

A review of this ☝️ would be super helpful. Does this make sense to you? Does this work? What else am I missing, if anything?

@vincenzopalazzo

Copy link
Copy Markdown
Contributor Author

The git_vector_swap followed by a free should be safe and correct. This will, effectively, move the newly generated data to the diff, swapping it with the old data. The old data is then freed.

Yes it is correct, the swap does not free any data

I think that the problem is not with the swap. It's with the git_vector_free_deep. This is an old bug that's been lurking here for a while.

Correct, if we swap the array A in the array B and then we git_vector_free_deep, we delete the pointer in the array A and B. This looked a little odd to me, but now I understand that it has a historical reason.

A review of this point_up would be super helpful. Does this make sense to you? Does this work? What else am I missing, if anything?

Mh I ready it and tested, but I need to take a look with a fresh mind tomorrow. I will report my though in one day or so

This commit addresses a double free issue that occurs when
merging two diffs and free the diff objects.

The problem was caused from copying the pointer using a vector swap
git function and then make a deep free of the original vector.

This operation made on a vector of pointers deallocated all the
pointers inside the new vector where we swap the content to.

As a result, an invalid object was returned to the user.

The valgrid report.

```
==256010== HEAP SUMMARY:
==256010==     in use at exit: 696,247 bytes in 11,244 blocks
==256010==   total heap usage: 50,123 allocs, 38,880 frees, 4,419,142 bytes allocated
==256010==
==256010== Searching for pointers to 11,244 not-freed blocks
==256010== Checked 850,720 bytes
==256010==
==256010== 112 bytes in 1 blocks are definitely lost in loss record 69 of 163
==256010==    at 0x4841848: malloc (vg_replace_malloc.c:431)
==256010==    by 0x48964FD: stdalloc__malloc.lto_priv.0 (stdalloc.c:22)
==256010==    by 0x48D4C34: git_diff__delta_dup (diff_tform.c:23)
==256010==    by 0x48DAA22: git_diff__merge_like_cgit (diff_tform.c:81)
==256010==    by 0x48D5C14: git_diff__merge (diff_tform.c:160)
==256010==    by 0x1092A6: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==
==256010== LEAK SUMMARY:
==256010==    definitely lost: 112 bytes in 1 blocks
==256010==    indirectly lost: 0 bytes in 0 blocks
==256010==      possibly lost: 0 bytes in 0 blocks
==256010==    still reachable: 696,135 bytes in 11,243 blocks
==256010==         suppressed: 0 bytes in 0 blocks
==256010== Reachable blocks (those to which a pointer was found) are not shown.
==256010== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==256010==
==256010== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
==256010==
==256010== 1 errors in context 1 of 2:
==256010== Invalid free() / delete / delete[] / realloc()
==256010==    at 0x484412F: free (vg_replace_malloc.c:974)
==256010==    by 0x490D5B0: patch_parsed__free.part.0.lto_priv.0 (patch_parse.c:1150)
==256010==    by 0x48CC454: diff_parsed_free (diff_parse.c:21)
==256010==    by 0x1092EA: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==  Address 0x59e55b0 is 0 bytes inside a block of size 112 free'd
==256010==    at 0x484412F: free (vg_replace_malloc.c:974)
==256010==    by 0x48A17CD: git_vector_free_deep.part.0 (vector.c:99)
==256010==    by 0x48D5A2B: UnknownInlinedFun (vector.c:95)
==256010==    by 0x48D5A2B: git_diff__merge (diff_tform.c:193)
==256010==    by 0x1092A6: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==  Block was alloc'd at
==256010==    at 0x48469B3: calloc (vg_replace_malloc.c:1554)
==256010==    by 0x489652D: stdalloc__calloc.lto_priv.0 (stdalloc.c:42)
==256010==    by 0x48CCDE9: UnknownInlinedFun (patch_parse.c:1184)
==256010==    by 0x48CCDE9: git_diff_from_buffer (diff_parse.c:86)
==256010==    by 0x109219: main (in /home/vincent/Github/libgit2/build/a.out)
==256010==
==256010== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
```

Link: libgit2#6588
Reported-by: Eric Huss
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
@vincenzopalazzo vincenzopalazzo force-pushed the macros/double-free-patch branch from e305ab4 to 63dd690 Compare July 21, 2023 15:08
@vincenzopalazzo

vincenzopalazzo commented Jul 22, 2023

Copy link
Copy Markdown
Contributor Author

@vincenzopalazzo vincenzopalazzo force-pushed the macros/double-free-patch branch from 5d7c1a1 to 48423fb Compare July 22, 2023 13:31
@ethomson ethomson added the p1 label Oct 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Double free after calling git_diff_merge

2 participants