Skip to contentSkip to conversation

revision: fix memory leak in prepare_show_merge()

PATCH Superseded Lidong Yan via GitGitGadget 4 participants 12 messages Jun 4, 2025 – Jun 10, 2025

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 revision: fix memory leak in prepare_show_merge()

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

---
revision: fix memory leak in prepare_show_merge()

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1989%2Fbrandb97%2Ffix-revision-leak-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1989/brandb97/fix-revision-leak-v1
Pull-Request: https://github.com/git/git/pull/1989

 revision.c | 1 +
 1 file changed, 1 insertion(+)
revision.c +1 -0
index 2c36a9c179e..afee1111961 100644
@@ -2060,6 +2060,7 @@ static void prepare_show_merge(struct rev_info *revs)
 	parse_pathspec(&revs->prune_data, PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
 		       PATHSPEC_PREFER_FULL | PATHSPEC_LITERAL_PATH, "", prune);
 	revs->limited = 1;
+	free(prune);
 }
 
 static int dotdot_missing(const char *arg, char *dotdot,

base-commit: 7014b55638da979331baf8dc31c4e1d697cf2d67

Signature · 1 line
gitgitgadget
#1 lore raw
Quoted 5 lines · Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

That is a rather obvious memory leak indeed. Do you know why we never detected the leak in our CI? Is this code path not exercised at all by our tests?

Patrick

#2 lore raw
Quoted 8 lines · 2025年6月4日 15:48,Patrick Steinhardt <[email protected]> 写道:
Quoted 5 lines · Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

Quoted 6 lines

That is a rather obvious memory leak indeed. Do you know why we never detected the leak in our CI? Is this code path not exercised at all by our tests?

Patrick

I don’t know why CI test doesn’t cover this leak, but I am happy to add a prereq test for this case.

p.s. I also like to ask that it there anyway to run test locally? How do you developers normally run test without open an pull request.

Thanks,
Lidong

#3 lore raw
Quoted 13 lines · lidongyan
Quoted 12 lines · 2025年6月4日 15:48,Patrick Steinhardt <[email protected]> 写道:
Quoted 5 lines · Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

That is a rather obvious memory leak indeed. Do you know why we never detected the leak in our CI? Is this code path not exercised at all by our tests?

Quoted 8 lines
Quoted 2 lines

Patrick

I don’t know why CI test doesn’t cover this leak, but I am happy to add a prereq test for this case.

p.s. I also like to ask that it there anyway to run test locally? How do you developers normally run test without open an pull request.

Do you mean the test suite in general or leak tests in particular? In any case, you can of course run both of these locally. You can do so either by using Make:

# Run tests.
$ make test
# Run tests with the leak checking enabled.
$ make test SANITIZE=leak

Or with Meson:

# Create the build directory and execute tests. $ meson setup build
$ meson test -C build

# Create a second build directory, this time with leak checking # enabled.
$ meson setup build-leaks -Db_sanitize=leak
$ meson test -C build-leaks

Patrick

#4 lore raw
Quoted 39 lines · 2025年6月4日 16:06,Patrick Steinhardt <[email protected]> 写道:
Quoted 13 lines · lidongyan
Quoted 12 lines · 2025年6月4日 15:48,Patrick Steinhardt <[email protected]> 写道:
Quoted 5 lines · Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

That is a rather obvious memory leak indeed. Do you know why we never detected the leak in our CI? Is this code path not exercised at all by our tests?

Quoted 8 lines
Quoted 2 lines

Patrick

I don’t know why CI test doesn’t cover this leak, but I am happy to add a prereq test for this case.

p.s. I also like to ask that it there anyway to run test locally? How do you developers normally run test without open an pull request.

Do you mean the test suite in general or leak tests in particular? In any case, you can of course run both of these locally. You can do so either by using Make:

# Run tests.

   $ make test
   # Run tests with the leak checking enabled.
   $ make test SANITIZE=leak

Or with Meson:

# Create the build directory and execute tests.

   $ meson setup build
   $ meson test -C build
Quoted 6 lines

# Create a second build directory, this time with leak checking

   # enabled.
   $ meson setup build-leaks -Db_sanitize=leak
   $ meson test -C build-leaks

Patrick

Got it, thank you
Lidong

#5 lore raw
In reply to Patrick Steinhardt
Quoted 10 lines · Patrick Steinhardt
Quoted 5 lines · Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

That is a rather obvious memory leak indeed. Do you know why we never detected the leak in our CI? Is this code path not exercised at all by our tests?

I think we have no "show --merge" test. Something like this may be minimally sufficient.

 t/t7007-show.sh | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
t/t7007-show.sh +15 -0
index d6cc69e0f2..99f4d0b963 100755
@@ -167,4 +167,19 @@ test_expect_success 'show --graph is forbidden' '
   test_must_fail git show --graph HEAD
 '
 
+test_expect_success 'unmerged index' '
+	git reset --hard &&
+	git commit --allow-empty -m initial &&
+	git rev-parse HEAD >.git/MERGE_HEAD &&
+	blob1=$(echo hello | git hash-object -w --stdin) &&
+	blob2=$(echo goodbye | git hash-object -w --stdin) &&
+	blob3=$(echo world | git hash-object -w --stdin) &&
+	git update-index --add --index-info <<-EOF &&
+	100644 $blob1 1	conflicting
+	100644 $blob2 2	conflicting
+	100755 $blob3 3	conflicting
+	EOF
+	git show --merge HEAD
+'
+
 test_done
#6 lore raw
Quoted 43 lines · 2025年6月6日 04:56,Junio C Hamano <[email protected]> 写道:
Quoted 10 lines · Patrick Steinhardt
Quoted 5 lines · Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

That is a rather obvious memory leak indeed. Do you know why we never detected the leak in our CI? Is this code path not exercised at all by our tests?

I think we have no "show --merge" test. Something like this may be minimally sufficient.

t/t7007-show.sh | 15 +++++++++++++++

1 file changed, 15 insertions(+)
t/t7007-show.sh +0 -0
index d6cc69e0f2..99f4d0b963 100755
@@ -167,4 +167,19 @@ test_expect_success 'show --graph is forbidden' '
  test_must_fail git show --graph HEAD

'

t/t7007-show.sh +15 -0
+test_expect_success 'unmerged index' '
+ git reset --hard &&
+ git commit --allow-empty -m initial &&
+ git rev-parse HEAD >.git/MERGE_HEAD &&
+ blob1=$(echo hello | git hash-object -w --stdin) &&
+ blob2=$(echo goodbye | git hash-object -w --stdin) &&
+ blob3=$(echo world | git hash-object -w --stdin) &&
+ git update-index --add --index-info <<-EOF &&
+ 100644 $blob1 1 conflicting
+ 100644 $blob2 2 conflicting
+ 100755 $blob3 3 conflicting
+ EOF
+ git show --merge HEAD
+'
+
Quoted 2 lines

test_done

I could add this test case into my patch. Though I don’t understand

Quoted 1 line

+ git rev-parse HEAD >.git/MERGE_HEAD &&

If HEAD is equal to MERGE_HEAD. Would git show —merge still works as usual? How about something like this

t/t7007-show.sh +24 -0
index d6cc69e0f2..f693b6e24b 100755
@@ -167,4 +167,28 @@ test_expect_success 'show --graph is forbidden' '
   test_must_fail git show --graph HEAD
 '
 
+test_expect_success 'unmerged index' '
+       git reset --hard &&
+
+       git switch -C base &&
+       echo "base" > conflicting &&
+       git add conflicting &&
+       git commit -m "base" &&
+
+       git branch hello &&
+       git branch goodbye &&
+
+       git switch hello &&
+       echo "hello" > conflicting &&
+       git commit -am "hello" &&
+
+       git switch goodbye &&
+       echo "goodbye" > conflicting &&
+       git commit -am "goodbye" &&
+
+       git switch hello &&
+       test_must_fail git merge goodbye &&
+       git show --merge HEAD
+'
+
 test_done
#7 lore raw
Quoted 2 lines · lidongyan

I could add this test case into my patch. Though I don’t understand

Quoted 1 line

+ git rev-parse HEAD >.git/MERGE_HEAD &&

That was not about telling "show --merge" to work on any meaningful data and to produce any useful output. I knew the step to prepare for "show --merge" was leaky, so I gave a very minimum that can tickle that codepath. I wasn't of course proud of the direct manipulation of the filesystem (as recent "git update-ref MERGE_HEAD HEAD" would not even allow us to do this, sheesh, not very convenient).

If you came up with a sequence that produces a situation to use the "git show --merge" command in a more realistic way, like below, that is wonderful.

Quoted 36 lines

If HEAD is equal to MERGE_HEAD. Would git show —merge still works as usual? How about something like this

t/t7007-show.sh +24 -0
index d6cc69e0f2..f693b6e24b 100755
@@ -167,4 +167,28 @@ test_expect_success 'show --graph is forbidden' '
   test_must_fail git show --graph HEAD
 '
 
+test_expect_success 'unmerged index' '
+       git reset --hard &&
+
+       git switch -C base &&
+       echo "base" > conflicting &&
+       git add conflicting &&
+       git commit -m "base" &&
+
+       git branch hello &&
+       git branch goodbye &&
+
+       git switch hello &&
+       echo "hello" > conflicting &&
+       git commit -am "hello" &&
+
+       git switch goodbye &&
+       echo "goodbye" > conflicting &&
+       git commit -am "goodbye" &&
+
+       git switch hello &&
+       test_must_fail git merge goodbye &&
+       git show --merge HEAD
+'
+
 test_done
#8 lore raw
Quoted 4 lines · 2025年6月7日 00:47,Junio C Hamano <[email protected]> 写道:
Quoted 1 line · lidongyan

I could add this test case into my patch. Though I don’t understand

Quoted 8 lines
Quoted 1 line
Quoted 1 line

+ git rev-parse HEAD >.git/MERGE_HEAD &&

That was not about telling "show --merge" to work on any meaningful data and to produce any useful output. I knew the step to prepare for "show --merge" was leaky, so I gave a very minimum that can tickle that codepath. I wasn't of course proud of the direct manipulation of the filesystem (as recent "git update-ref MERGE_HEAD HEAD" would not even allow us to do this, sheesh, not very convenient).

I understand. show —merge means the codepath must go through prepare_show_merge() And update-index with index which has stage greater than 0 let prepare_show_merge() go through loop and add index to prune. Thus cause a leak.

Quoted 42 lines

If you came up with a sequence that produces a situation to use the "git show --merge" command in a more realistic way, like below, that is wonderful.

Quoted 36 lines

If HEAD is equal to MERGE_HEAD. Would git show —merge still works as usual? How about something like this

t/t7007-show.sh +0 -0
index d6cc69e0f2..f693b6e24b 100755
@@ -167,4 +167,28 @@ test_expect_success 'show --graph is forbidden' '
  test_must_fail git show --graph HEAD

'

t/t7007-show.sh +24 -0
+test_expect_success 'unmerged index' '
+       git reset --hard &&
+
+       git switch -C base &&
+       echo "base" > conflicting &&
+       git add conflicting &&
+       git commit -m "base" &&
+
+       git branch hello &&
+       git branch goodbye &&
+
+       git switch hello &&
+       echo "hello" > conflicting &&
+       git commit -am "hello" &&
+
+       git switch goodbye &&
+       echo "goodbye" > conflicting &&
+       git commit -am "goodbye" &&
+
+       git switch hello &&
+       test_must_fail git merge goodbye &&
+       git show --merge HEAD
+'
+

test_done

#9 lore raw

PATCH v2 1/1 revision: fix memory leak in prepare_show_merge()

In reply to Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

---
revision: fix memory leak in prepare_show_merge()

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1989%2Fbrandb97%2Ffix-revision-leak-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1989/brandb97/fix-revision-leak-v2
Pull-Request: https://github.com/git/git/pull/1989

Range-diff vs v1:

1: 5d213e2c11c ! 1: 61a2d98dad9 revision: fix memory leak in prepare_show_merge()

     @@ revision.c: static void prepare_show_merge(struct rev_info *revs)
       }
       
       static int dotdot_missing(const char *arg, char *dotdot,
     +
     + ## t/t7007-show.sh ##
     +@@ t/t7007-show.sh: test_expect_success 'show --graph is forbidden' '
     +   test_must_fail git show --graph HEAD
     + '
     + 
     ++test_expect_success 'show unmerged index' '
     ++    git reset --hard &&
     ++
     ++    git switch -C base &&
     ++    echo "base" > conflicting &&
     ++    git add conflicting &&
     ++    git commit -m "base" &&
     ++
     ++    git branch hello &&
     ++    git branch goodbye &&
     ++
     ++    git switch hello &&
     ++    echo "hello" > conflicting &&
     ++    git commit -am "hello" &&
     ++
     ++    git switch goodbye &&
     ++    echo "goodbye" > conflicting &&
     ++    git commit -am "goodbye" &&
     ++
     ++    git switch hello &&
     ++    test_must_fail git merge goodbye &&
     ++    git show --merge HEAD
     ++'
     ++
     + test_done
 revision.c      |  1 +
 t/t7007-show.sh | 24 ++++++++++++++++++++++++
 2 files changed, 25 insertions(+)
revision.c +1 -0
index 2c36a9c179ef..afee11119615 100644
@@ -2060,6 +2060,7 @@ static void prepare_show_merge(struct rev_info *revs)
 	parse_pathspec(&revs->prune_data, PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
 		       PATHSPEC_PREFER_FULL | PATHSPEC_LITERAL_PATH, "", prune);
 	revs->limited = 1;
+	free(prune);
 }
 
 static int dotdot_missing(const char *arg, char *dotdot,
t/t7007-show.sh +24 -0
index d6cc69e0f2cb..4eb824f2e39f 100755
@@ -167,4 +167,28 @@ test_expect_success 'show --graph is forbidden' '
   test_must_fail git show --graph HEAD
 '
 
+test_expect_success 'show unmerged index' '
+    git reset --hard &&
+
+    git switch -C base &&
+    echo "base" > conflicting &&
+    git add conflicting &&
+    git commit -m "base" &&
+
+    git branch hello &&
+    git branch goodbye &&
+
+    git switch hello &&
+    echo "hello" > conflicting &&
+    git commit -am "hello" &&
+
+    git switch goodbye &&
+    echo "goodbye" > conflicting &&
+    git commit -am "goodbye" &&
+
+    git switch hello &&
+    test_must_fail git merge goodbye &&
+    git show --merge HEAD
+'
+
 test_done

base-commit: 7014b55638da979331baf8dc31c4e1d697cf2d67

Signature · 1 line
gitgitgadget
#10 lore raw

t/t7007-show.sh test_expect_success 'show --graph is forbidden' ' · line 171

Quoted 2 lines · Lidong Yan via GitGitGadget
t/t7007-show.sh +2 -0
+test_expect_success 'show unmerged index' '
+    git reset --hard &&

We use tabs for indent.

t/t7007-show.sh test_expect_success 'show --graph is forbidden' ' · line 174

Quoted 3 lines
t/t7007-show.sh +3 -0
+
+    git switch -C base &&
+    echo "base" > conflicting &&

Let's lose the SP between redirection operator ">" and its target "conflicting", i.e.

	echo "base" >conflicting &&
Quoted 22 lines
t/t7007-show.sh +19 -0
+    git add conflicting &&
+    git commit -m "base" &&
+
+    git branch hello &&
+    git branch goodbye &&
+
+    git switch hello &&
+    echo "hello" > conflicting &&
+    git commit -am "hello" &&
+
+    git switch goodbye &&
+    echo "goodbye" > conflicting &&
+    git commit -am "goodbye" &&
+
+    git switch hello &&
+    test_must_fail git merge goodbye &&
+    git show --merge HEAD
+'
+
 test_done

base-commit: 7014b55638da979331baf8dc31c4e1d697cf2d67

#11 lore raw

PATCH v3 1/1 revision: fix memory leak in prepare_show_merge()

In reply to Lidong Yan via GitGitGadget

From: Lidong Yan <[email protected]>

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

---
revision: fix memory leak in prepare_show_merge()

In revision.c:prepare_show_merge(), we allocated an array in prune but forget to free it. Since parse_pathspec is not responsible to free prune, we should add free(prune) in the end of prepare_show_merge().

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1989%2Fbrandb97%2Ffix-revision-leak-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1989/brandb97/fix-revision-leak-v3
Pull-Request: https://github.com/git/git/pull/1989

Range-diff vs v2:

1: 61a2d98dad9 ! 1: 88795b33bbe revision: fix memory leak in prepare_show_merge()

     @@ t/t7007-show.sh: test_expect_success 'show --graph is forbidden' '
       '
       
      +test_expect_success 'show unmerged index' '
     -+    git reset --hard &&
     ++	git reset --hard &&
      +
     -+    git switch -C base &&
     -+    echo "base" > conflicting &&
     -+    git add conflicting &&
     -+    git commit -m "base" &&
     ++	git switch -C base &&
     ++	echo "base" >conflicting &&
     ++	git add conflicting &&
     ++	git commit -m "base" &&
      +
     -+    git branch hello &&
     -+    git branch goodbye &&
     ++	git branch hello &&
     ++	git branch goodbye &&
      +
     -+    git switch hello &&
     -+    echo "hello" > conflicting &&
     -+    git commit -am "hello" &&
     ++	git switch hello &&
     ++	echo "hello" >conflicting &&
     ++	git commit -am "hello" &&
      +
     -+    git switch goodbye &&
     -+    echo "goodbye" > conflicting &&
     -+    git commit -am "goodbye" &&
     ++	git switch goodbye &&
     ++	echo "goodbye" >conflicting &&
     ++	git commit -am "goodbye" &&
      +
     -+    git switch hello &&
     -+    test_must_fail git merge goodbye &&
     -+    git show --merge HEAD
     ++	git switch hello &&
     ++	test_must_fail git merge goodbye &&
     ++	git show --merge HEAD
      +'
      +
       test_done
 revision.c      |  1 +
 t/t7007-show.sh | 24 ++++++++++++++++++++++++
 2 files changed, 25 insertions(+)
revision.c +1 -0
index 2c36a9c179ef..afee11119615 100644
@@ -2060,6 +2060,7 @@ static void prepare_show_merge(struct rev_info *revs)
 	parse_pathspec(&revs->prune_data, PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
 		       PATHSPEC_PREFER_FULL | PATHSPEC_LITERAL_PATH, "", prune);
 	revs->limited = 1;
+	free(prune);
 }
 
 static int dotdot_missing(const char *arg, char *dotdot,
t/t7007-show.sh +24 -0
index d6cc69e0f2cb..2d322b53d16e 100755
@@ -167,4 +167,28 @@ test_expect_success 'show --graph is forbidden' '
   test_must_fail git show --graph HEAD
 '
 
+test_expect_success 'show unmerged index' '
+	git reset --hard &&
+
+	git switch -C base &&
+	echo "base" >conflicting &&
+	git add conflicting &&
+	git commit -m "base" &&
+
+	git branch hello &&
+	git branch goodbye &&
+
+	git switch hello &&
+	echo "hello" >conflicting &&
+	git commit -am "hello" &&
+
+	git switch goodbye &&
+	echo "goodbye" >conflicting &&
+	git commit -am "goodbye" &&
+
+	git switch hello &&
+	test_must_fail git merge goodbye &&
+	git show --merge HEAD
+'
+
 test_done

base-commit: 7014b55638da979331baf8dc31c4e1d697cf2d67

Signature · 1 line
gitgitgadget