Git development
 help / color / mirror / Atom feed
* [PATCH] apply: avoid leaking abandoned git-header state
@ 2026-07-02  4:17 Zephyr Yao
  2026-08-26 20:31 ` Junio C Hamano
  0 siblings, 1 reply; 2+ messages in thread
From: Zephyr Yao @ 2026-07-02  4:17 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Zephyr Yao, Mahya SamDaliri, Haotian Zhang,
	Martin Kellogg

When find_header() sees a "diff --git" line, it calls
parse_git_diff_header() to parse the git-style extended header. That parser
updates the caller's struct patch as it goes, filling in the default name,
old/new names, and new/delete state.

But not every "diff --git" line found while scanning is ultimately accepted
as the patch header. If parse_git_diff_header() returns a length that covers
only the "diff --git" line, find_header() continues scanning for another
header. In that case the partially parsed git-header state must not interfere
with the later traditional "---" / "+++" header.

Leaving that state behind can combine incompatible metadata from the
abandoned git header and the later traditional header. For example, after:

	diff --git a/foo b/foo

	--- /dev/null
	+++ b/foo
	@@ -0,0 +1 @@
	+x

the abandoned git header can leave an old name in the patch, while the
traditional header marks the patch as creating a new file. That impossible
state later trips the check_preimage() assertion that a creation patch should
not have a preimage.

Parse a candidate git header into a temporary patch and line number. Commit
that temporary state to the real patch only when the git header is actually
accepted; otherwise release it and keep scanning with the original patch
state unchanged.

Also reject an empty parsed default name from the "diff --git" line.
An empty patch->def_name is not a valid pathname, and should not be
used later as a fallback when old_name and new_name are missing.

Add regression tests for both the empty default-name case and the non-empty
abandoned-header case above.

Co-authored-by: Mahya SamDaliri <ms3539@njit.edu>
Signed-off-by: Mahya SamDaliri <ms3539@njit.edu>
Co-authored-by: Haotian Zhang <haotian.zhang@njit.edu>
Signed-off-by: Haotian Zhang <haotian.zhang@njit.edu>
Co-authored-by: Martin Kellogg <martin.kellogg@njit.edu>
Signed-off-by: Martin Kellogg <martin.kellogg@njit.edu>
Signed-off-by: Zephyr Yao <zhihao.yao@njit.edu>
---
 apply.c               | 29 ++++++++++++++++++++++-------
 t/t4100-apply-stat.sh | 25 +++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/apply.c b/apply.c
index 5e54453..2ce9b6a 100644
--- a/apply.c
+++ b/apply.c
@@ -1362,6 +1362,9 @@ int parse_git_diff_header(struct strbuf *root,
 	 * the default name from the header.
 	 */
 	patch->def_name = git_header_name(p_value, line, len);
+	if (patch->def_name && !*patch->def_name)
+		FREE_AND_NULL(patch->def_name);
+
 	if (patch->def_name && root->len) {
 		char *s = xstrfmt("%s%s", root->buf, patch->def_name);
 		free(patch->def_name);
@@ -1632,15 +1635,27 @@ static int find_header(struct apply_state *state,
 		 * or mode change, so we handle that specially
 		 */
 		if (!memcmp("diff --git ", line, 11)) {
-			int git_hdr_len = parse_git_diff_header(&state->root,
-								state->patch_input_file,
-								&state->linenr,
-								state->p_value, line, len,
-								size, patch);
-			if (git_hdr_len < 0)
+			struct patch git_patch = { 0 };
+			int git_linenr = state->linenr;
+			int git_hdr_len;
+
+			git_patch.inaccurate_eof = patch->inaccurate_eof;
+			git_patch.recount = patch->recount;
+			git_hdr_len = parse_git_diff_header(&state->root,
+							    state->patch_input_file,
+							    &git_linenr,
+							    state->p_value, line, len,
+							    size, &git_patch);
+			if (git_hdr_len < 0) {
+				release_patch(&git_patch);
 				return -128;
-			if (git_hdr_len <= len)
+			}
+			if (git_hdr_len <= len) {
+				release_patch(&git_patch);
 				continue;
+			}
+			*patch = git_patch;
+			state->linenr = git_linenr;
 			*hdrsize = git_hdr_len;
 			return offset;
 		}
diff --git a/t/t4100-apply-stat.sh b/t/t4100-apply-stat.sh
index 8393076..d3406ed 100755
--- a/t/t4100-apply-stat.sh
+++ b/t/t4100-apply-stat.sh
@@ -113,6 +113,31 @@ test_expect_success 'applying a patch with a missing filename reports the input'
 	test_cmp expect err
 '
 
+test_expect_success 'empty default filename reports the input' '
+	cat >empty-name.patch <<-\EOF &&
+	diff --git "a/""b/"
+
+	--- /dev/null
+	+++ "
+	@@ -0,0 +1 @@
+	+
+	EOF
+	test_must_fail git apply empty-name.patch 2>err &&
+	test_grep "git diff header lacks filename information" err
+'
+
+test_expect_success 'abandoned git header does not reuse names' '
+	cat >abandoned-git-header.patch <<-\EOF &&
+	diff --git a/foo b/foo
+
+	--- /dev/null
+	+++ b/foo
+	@@ -0,0 +1 @@
+	+x
+	EOF
+	git apply --check abandoned-git-header.patch
+'
+
 test_expect_success 'applying a patch with an invalid mode reports the input' '
 	cat >mode.patch <<-\EOF &&
 	diff --git a/f b/f
-- 
2.47.0

^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH] apply: avoid leaking abandoned git-header state
  2026-07-02  4:17 [PATCH] apply: avoid leaking abandoned git-header state Zephyr Yao
@ 2026-08-26 20:31 ` Junio C Hamano
  0 siblings, 0 replies; 2+ messages in thread
From: Junio C Hamano @ 2026-08-26 20:31 UTC (permalink / raw)
  To: Zephyr Yao
  Cc: git, Zephyr Yao, Mahya SamDaliri, Haotian Zhang, Martin Kellogg

Zephyr Yao <zot.zot.yao@gmail.com> writes:

> When find_header() sees a "diff --git" line, it calls
> parse_git_diff_header() to parse the git-style extended header. That parser
> updates the caller's struct patch as it goes, filling in the default name,
> old/new names, and new/delete state.
>
> But not every "diff --git" line found while scanning is ultimately accepted
> as the patch header. If parse_git_diff_header() returns a length that covers
> only the "diff --git" line, find_header() continues scanning for another
> header. In that case the partially parsed git-header state must not interfere
> with the later traditional "---" / "+++" header.

This patch has gathered no response.  Perhaps the e-mail received no
reply because it was sent in early July around the holiday, or
perhaps nobody was interested in the topic.  In any case, I am
cleaning up the "What's cooking" report and noticed that this has
been in the "Needs review" state for a long time.

So I took a look.

These cross checks are primarily sanity checks.  Having the parser
notice a discrepancy and abort is a good thing.  The user is
supposed to inspect the situation and fix a malformed patch (such
as one containing a stray 'diff --git' header unrelated to the
actual patch).

So I do not think we want to apply this patch.

Thanks.

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-08-26 20:31 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-02  4:17 [PATCH] apply: avoid leaking abandoned git-header state Zephyr Yao
2026-08-26 20:31 ` Junio C Hamano

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox