Skip to content

Commit 416f7cb

Browse files
authored
Merge pull request libgit2#6208 from jorio/fix-stale-filesize-crash
diff_file: fix crash if size of diffed file changes in workdir
2 parents 4467bd6 + d2ce981 commit 416f7cb

File tree

9 files changed

+188
-10
lines changed

9 files changed

+188
-10
lines changed

include/git2/diff.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ typedef enum {
207207
GIT_DIFF_FLAG_BINARY = (1u << 0), /**< file(s) treated as binary data */
208208
GIT_DIFF_FLAG_NOT_BINARY = (1u << 1), /**< file(s) treated as text data */
209209
GIT_DIFF_FLAG_VALID_ID = (1u << 2), /**< `id` value is known correct */
210-
GIT_DIFF_FLAG_EXISTS = (1u << 3) /**< file exists at this side of the delta */
210+
GIT_DIFF_FLAG_EXISTS = (1u << 3), /**< file exists at this side of the delta */
211+
GIT_DIFF_FLAG_VALID_SIZE = (1u << 4) /**< file size value is known correct */
211212
} git_diff_flag_t;
212213

213214
/**

src/diff_file.c

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,16 +328,25 @@ static int diff_file_content_load_workdir_file(
328328
git_filter_list *fl = NULL;
329329
git_file fd = git_futils_open_ro(git_str_cstr(path));
330330
git_str raw = GIT_STR_INIT;
331+
git_object_size_t new_file_size = 0;
331332

332333
if (fd < 0)
333334
return fd;
334335

335-
if (!fc->file->size)
336-
error = git_futils_filesize(&fc->file->size, fd);
336+
error = git_futils_filesize(&new_file_size, fd);
337337

338-
if (error < 0 || !fc->file->size)
338+
if (error < 0)
339339
goto cleanup;
340340

341+
if (!(fc->file->flags & GIT_DIFF_FLAG_VALID_SIZE)) {
342+
fc->file->size = new_file_size;
343+
fc->file->flags |= GIT_DIFF_FLAG_VALID_SIZE;
344+
} else if (fc->file->size != new_file_size) {
345+
git_error_set(GIT_ERROR_FILESYSTEM, "file changed before we could read it");
346+
error = -1;
347+
goto cleanup;
348+
}
349+
341350
if ((diff_opts->flags & GIT_DIFF_SHOW_BINARY) == 0 &&
342351
diff_file_content_binary_by_size(fc))
343352
goto cleanup;

src/diff_generate.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@ static bool diff_pathspec_match(
117117
matched_pathspec, NULL);
118118
}
119119

120+
static void diff_delta__flag_known_size(git_diff_file *file)
121+
{
122+
/*
123+
* If we don't know the ID, that can only come from the workdir
124+
* iterator, which means we *do* know the file size. This is a
125+
* leaky abstraction, but alas. Otherwise, we test against the
126+
* empty blob id.
127+
*/
128+
if (file->size ||
129+
!(file->flags & GIT_DIFF_FLAG_VALID_ID) ||
130+
git_oid_equal(&file->id, &git_oid__empty_blob_sha1))
131+
file->flags |= GIT_DIFF_FLAG_VALID_SIZE;
132+
}
133+
134+
static void diff_delta__flag_known_sizes(git_diff_delta *delta)
135+
{
136+
diff_delta__flag_known_size(&delta->old_file);
137+
diff_delta__flag_known_size(&delta->new_file);
138+
}
139+
120140
static int diff_delta__from_one(
121141
git_diff_generated *diff,
122142
git_delta_t status,
@@ -182,6 +202,8 @@ static int diff_delta__from_one(
182202
if (has_old || !git_oid_is_zero(&delta->new_file.id))
183203
delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID;
184204

205+
diff_delta__flag_known_sizes(delta);
206+
185207
return diff_insert_delta(diff, delta, matched_pathspec);
186208
}
187209

@@ -244,6 +266,8 @@ static int diff_delta__from_two(
244266
delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID;
245267
}
246268

269+
diff_delta__flag_known_sizes(delta);
270+
247271
return diff_insert_delta(diff, delta, matched_pathspec);
248272
}
249273

src/diff_generate.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,10 @@ GIT_INLINE(int) git_diff_file__resolve_zero_size(
119119

120120
git_odb_free(odb);
121121

122-
if (!error)
122+
if (!error) {
123123
file->size = (git_object_size_t)len;
124+
file->flags |= GIT_DIFF_FLAG_VALID_SIZE;
125+
}
124126

125127
return error;
126128
}

src/diff_tform.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,8 @@ static int similarity_init(
460460
info->blob = NULL;
461461
git_str_init(&info->data, 0);
462462

463-
if (info->file->size > 0 || info->src == GIT_ITERATOR_WORKDIR)
463+
if ((info->file->flags & GIT_DIFF_FLAG_VALID_SIZE) ||
464+
info->src == GIT_ITERATOR_WORKDIR)
464465
return 0;
465466

466467
return git_diff_file__resolve_zero_size(

src/odb.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "filter.h"
1717
#include "repository.h"
1818
#include "blob.h"
19+
#include "oid.h"
1920

2021
#include "git2/odb_backend.h"
2122
#include "git2/oid.h"
@@ -58,10 +59,7 @@ static int error_null_oid(int error, const char *message);
5859

5960
static git_object_t odb_hardcoded_type(const git_oid *id)
6061
{
61-
static git_oid empty_tree = {{ 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60,
62-
0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04 }};
63-
64-
if (!git_oid_cmp(id, &empty_tree))
62+
if (!git_oid_cmp(id, &git_oid__empty_tree_sha1))
6563
return GIT_OBJECT_TREE;
6664

6765
return GIT_OBJECT_INVALID;

src/oid.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
#include <string.h>
1414
#include <limits.h>
1515

16+
const git_oid git_oid__empty_blob_sha1 =
17+
{{ 0xe6, 0x9d, 0xe2, 0x9b, 0xb2, 0xd1, 0xd6, 0x43, 0x4b, 0x8b,
18+
0x29, 0xae, 0x77, 0x5a, 0xd8, 0xc2, 0xe4, 0x8c, 0x53, 0x91 }};
19+
const git_oid git_oid__empty_tree_sha1 =
20+
{{ 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60,
21+
0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04 }};
22+
1623
static char to_hex[] = "0123456789abcdef";
1724

1825
static int oid_error_invalid(const char *msg)

src/oid.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
#include "git2/oid.h"
1313

14+
extern const git_oid git_oid__empty_blob_sha1;
15+
extern const git_oid git_oid__empty_tree_sha1;
16+
1417
/**
1518
* Format a git_oid into a newly allocated c-string.
1619
*

tests/diff/externalmodifications.c

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#include "clar_libgit2.h"
2+
#include "../checkout/checkout_helpers.h"
3+
4+
#include "index.h"
5+
#include "repository.h"
6+
7+
static git_repository *g_repo;
8+
9+
void test_diff_externalmodifications__initialize(void)
10+
{
11+
g_repo = cl_git_sandbox_init("testrepo2");
12+
}
13+
14+
void test_diff_externalmodifications__cleanup(void)
15+
{
16+
cl_git_sandbox_cleanup();
17+
g_repo = NULL;
18+
}
19+
20+
void test_diff_externalmodifications__file_becomes_smaller(void)
21+
{
22+
git_index *index;
23+
git_diff *diff;
24+
git_patch* patch;
25+
git_str path = GIT_STR_INIT;
26+
char big_string[500001];
27+
28+
cl_git_pass(git_str_joinpath(&path, git_repository_workdir(g_repo), "README"));
29+
30+
/* Modify the file with a large string */
31+
memset(big_string, '\n', sizeof(big_string) - 1);
32+
big_string[sizeof(big_string) - 1] = '\0';
33+
cl_git_mkfile(path.ptr, big_string);
34+
35+
/* Get a diff */
36+
cl_git_pass(git_repository_index(&index, g_repo));
37+
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, index, NULL));
38+
cl_assert_equal_i(1, git_diff_num_deltas(diff));
39+
cl_assert_equal_i(500000, git_diff_get_delta(diff, 0)->new_file.size);
40+
41+
/* Simulate file modification after we've gotten the diff.
42+
* Write a shorter string to ensure that we don't mmap 500KB from
43+
* the previous revision, which would most likely crash. */
44+
cl_git_mkfile(path.ptr, "hello");
45+
46+
/* Attempt to get a patch */
47+
cl_git_fail(git_patch_from_diff(&patch, diff, 0));
48+
49+
git_index_free(index);
50+
git_diff_free(diff);
51+
git_str_dispose(&path);
52+
}
53+
54+
void test_diff_externalmodifications__file_becomes_empty(void)
55+
{
56+
git_index *index;
57+
git_diff *diff;
58+
git_patch* patch;
59+
git_str path = GIT_STR_INIT;
60+
61+
cl_git_pass(git_str_joinpath(&path, git_repository_workdir(g_repo), "README"));
62+
63+
/* Modify the file */
64+
cl_git_mkfile(path.ptr, "hello");
65+
66+
/* Get a diff */
67+
cl_git_pass(git_repository_index(&index, g_repo));
68+
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, index, NULL));
69+
cl_assert_equal_i(1, git_diff_num_deltas(diff));
70+
cl_assert_equal_i(5, git_diff_get_delta(diff, 0)->new_file.size);
71+
72+
/* Empty out the file after we've gotten the diff */
73+
cl_git_mkfile(path.ptr, "");
74+
75+
/* Attempt to get a patch */
76+
cl_git_fail(git_patch_from_diff(&patch, diff, 0));
77+
78+
git_index_free(index);
79+
git_diff_free(diff);
80+
git_str_dispose(&path);
81+
}
82+
83+
void test_diff_externalmodifications__file_deleted(void)
84+
{
85+
git_index *index;
86+
git_diff *diff;
87+
git_patch* patch;
88+
git_str path = GIT_STR_INIT;
89+
90+
cl_git_pass(git_str_joinpath(&path, git_repository_workdir(g_repo), "README"));
91+
92+
/* Get a diff */
93+
cl_git_pass(git_repository_index(&index, g_repo));
94+
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, index, NULL));
95+
cl_assert_equal_i(0, git_diff_num_deltas(diff));
96+
97+
/* Delete the file */
98+
cl_git_rmfile(path.ptr);
99+
100+
/* Attempt to get a patch */
101+
cl_git_fail(git_patch_from_diff(&patch, diff, 0));
102+
103+
git_index_free(index);
104+
git_diff_free(diff);
105+
git_str_dispose(&path);
106+
}
107+
108+
void test_diff_externalmodifications__empty_file_becomes_non_empty(void)
109+
{
110+
git_index *index;
111+
git_diff *diff;
112+
git_patch* patch;
113+
git_str path = GIT_STR_INIT;
114+
115+
cl_git_pass(git_str_joinpath(&path, git_repository_workdir(g_repo), "README"));
116+
117+
/* Empty out the file */
118+
cl_git_mkfile(path.ptr, "");
119+
120+
/* Get a diff */
121+
cl_git_pass(git_repository_index(&index, g_repo));
122+
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, index, NULL));
123+
cl_assert_equal_i(1, git_diff_num_deltas(diff));
124+
cl_assert_equal_i(0, git_diff_get_delta(diff, 0)->new_file.size);
125+
126+
/* Simulate file modification after we've gotten the diff */
127+
cl_git_mkfile(path.ptr, "hello");
128+
cl_git_fail(git_patch_from_diff(&patch, diff, 0));
129+
130+
git_index_free(index);
131+
git_diff_free(diff);
132+
git_str_dispose(&path);
133+
}

0 commit comments

Comments
 (0)