]> git.ipfire.org Git - thirdparty/git.git/commitdiff
apply: avoid leaking abandoned git-header state
authorZephyr Yao <zot.zot.yao@gmail.com>
Thu, 2 Jul 2026 04:17:59 +0000 (00:17 -0400)
committerJunio C Hamano <gitster@pobox.com>
Thu, 2 Jul 2026 17:32:21 +0000 (10:32 -0700)
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>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
apply.c
t/t4100-apply-stat.sh

diff --git a/apply.c b/apply.c
index 4aa1694cfaa2f03881579078fb76d83666eecb24..e01850255808fdf208120def1a6a37affaacff66 100644 (file)
--- 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;
                }
index 8393076469e0835ddf0c1d924cf4cc6dd50b34d9..d3406edaddbe11a8275ee67149883a318463a787 100755 (executable)
@@ -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