diff: do not use null sha1 as a sentinel value

The diff code represents paths using the diff_filespec struct. This struct has a sha1 to represent the sha1 of the content at that path, as well as a sha1_valid member which indicates whether its sha1 field is actually useful. If sha1_valid is not true, then the filespec represents a working tree file (e.g., for the no-index case, or for when the index is not up-to-date). The diff_filespec is only used internally, though. At the interfaces to the diff subsystem, callers feed the sha1 directly, and we create a diff_filespec from it. It's at that point that we look at the sha1 and decide whether it is valid or not; callers may pass the null sha1 as a sentinel value to indicate that it is not. We should not typically see the null sha1 coming from any other source (e.g., in the index itself, or from a tree). However, a corrupt tree might have a null sha1, which would cause "diff --patch" to accidentally diff the working tree version of a file instead of treating it as a blob. This patch extends the edges of the diff interface to accept a "sha1_valid" flag whenever we accept a sha1, and to use that flag when creating a filespec. In some cases, this means passing the flag through several layers, making the code change larger than would be desirable. One alternative would be to simply die() upon seeing corrupted trees with null sha1s. However, this fix more directly addresses the problem (while bogus sha1s in a tree are probably a bad thing, it is really the sentinel confusion sending us down the wrong code path that is what makes it devastating). And it means that git is more capable of examining and debugging these corrupted trees. For example, you can still "diff --raw" such a tree to find out when the bogus entry was introduced; you just cannot do a "--patch" diff (just as you could not with any other corrupted tree, as we do not have any content to diff). Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed Jul 28, 2012 at 11:03 UTC e54501004abbd20fa8d813c1e5b82c3b50bb9361
14 files changed +135 -32
builtin.h
+1 -1
@@ -42,7 +42,7 @@ void finish_copy_notes_for_rewrite(struct notes_rewrite_cfg *c);
42
43 extern int check_pager_config(const char *cmd);
44
45 -extern int textconv_object(const char *path, unsigned mode, const unsigned char *sha1, char **buf, unsigned long *buf_size);
45 +extern int textconv_object(const char *path, unsigned mode, const unsigned char *sha1, int sha1_valid, char **buf, unsigned long *buf_size);
46
47 extern int cmd_add(int argc, const char **argv, const char *prefix);
48 extern int cmd_annotate(int argc, const char **argv, const char *prefix);
builtin/blame.c
+5 -4
@@ -96,6 +96,7 @@ struct origin {
96 int textconv_object(const char *path,
97 unsigned mode,
98 const unsigned char *sha1,
99 + int sha1_valid,
100 char **buf,
101 unsigned long *buf_size)
102 {
@@ -103,7 +104,7 @@ int textconv_object(const char *path,
104 struct userdiff_driver *textconv;
105
106 df = alloc_filespec(path);
106 - fill_filespec(df, sha1, mode);
107 + fill_filespec(df, sha1, sha1_valid, mode);
108 textconv = get_textconv(df);
109 if (!textconv) {
110 free_filespec(df);
@@ -128,7 +129,7 @@ static void fill_origin_blob(struct diff_options *opt,
129
130 num_read_blob++;
131 if (DIFF_OPT_TST(opt, ALLOW_TEXTCONV) &&
131 - textconv_object(o->path, o->mode, o->blob_sha1, &file->ptr, &file_size))
132 + textconv_object(o->path, o->mode, o->blob_sha1, 1, &file->ptr, &file_size))
133 ;
134 else
135 file->ptr = read_sha1_file(o->blob_sha1, &type, &file_size);
@@ -2114,7 +2115,7 @@ static struct commit *fake_working_tree_commit(struct diff_options *opt,
2115 switch (st.st_mode & S_IFMT) {
2116 case S_IFREG:
2117 if (DIFF_OPT_TST(opt, ALLOW_TEXTCONV) &&
2117 - textconv_object(read_from, mode, null_sha1, &buf_ptr, &buf_len))
2118 + textconv_object(read_from, mode, null_sha1, 0, &buf_ptr, &buf_len))
2119 strbuf_attach(&buf, buf_ptr, buf_len, buf_len + 1);
2120 else if (strbuf_read_file(&buf, read_from, st.st_size) != st.st_size)
2121 die_errno("cannot open or read '%s'", read_from);
@@ -2506,7 +2507,7 @@ parse_done:
2507 die("no such path %s in %s", path, final_commit_name);
2508
2509 if (DIFF_OPT_TST(&sb.revs->diffopt, ALLOW_TEXTCONV) &&
2509 - textconv_object(path, o->mode, o->blob_sha1, (char **) &sb.final_buf,
2510 + textconv_object(path, o->mode, o->blob_sha1, 1, (char **) &sb.final_buf,
2511 &sb.final_buf_size))
2512 ;
2513 else
builtin/cat-file.c
+1 -1
@@ -143,7 +143,7 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name)
143 die("git cat-file --textconv %s: <object> must be <sha1:path>",
144 obj_name);
145
146 - if (!textconv_object(obj_context.path, obj_context.mode, sha1, &buf, &size))
146 + if (!textconv_object(obj_context.path, obj_context.mode, sha1, 1, &buf, &size))
147 die("git cat-file --textconv: unable to run textconv on %s",
148 obj_name);
149 break;
builtin/diff.c
+6 -2
@@ -29,6 +29,8 @@ static void stuff_change(struct diff_options *opt,
29 unsigned old_mode, unsigned new_mode,
30 const unsigned char *old_sha1,
31 const unsigned char *new_sha1,
32 + int old_sha1_valid,
33 + int new_sha1_valid,
34 const char *old_name,
35 const char *new_name)
36 {
@@ -54,8 +56,8 @@ static void stuff_change(struct diff_options *opt,
56
57 one = alloc_filespec(old_name);
58 two = alloc_filespec(new_name);
57 - fill_filespec(one, old_sha1, old_mode);
58 - fill_filespec(two, new_sha1, new_mode);
59 + fill_filespec(one, old_sha1, old_sha1_valid, old_mode);
60 + fill_filespec(two, new_sha1, new_sha1_valid, new_mode);
61
62 diff_queue(&diff_queued_diff, one, two);
63 }
@@ -84,6 +86,7 @@ static int builtin_diff_b_f(struct rev_info *revs,
86 stuff_change(&revs->diffopt,
87 blob[0].mode, canon_mode(st.st_mode),
88 blob[0].sha1, null_sha1,
89 + 1, 0,
90 path, path);
91 diffcore_std(&revs->diffopt);
92 diff_flush(&revs->diffopt);
@@ -108,6 +111,7 @@ static int builtin_diff_blobs(struct rev_info *revs,
111 stuff_change(&revs->diffopt,
112 blob[0].mode, blob[1].mode,
113 blob[0].sha1, blob[1].sha1,
114 + 1, 1,
115 blob[0].name, blob[1].name);
116 diffcore_std(&revs->diffopt);
117 diff_flush(&revs->diffopt);
combine-diff.c
+2 -2
@@ -111,7 +111,7 @@ static char *grab_blob(const unsigned char *sha1, unsigned int mode,
111 return xcalloc(1, 1);
112 } else if (textconv) {
113 struct diff_filespec *df = alloc_filespec(path);
114 - fill_filespec(df, sha1, mode);
114 + fill_filespec(df, sha1, 1, mode);
115 *size = fill_textconv(textconv, df, &blob);
116 free_filespec(df);
117 } else {
@@ -823,7 +823,7 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent,
823 &result_size, NULL, NULL);
824 } else if (textconv) {
825 struct diff_filespec *df = alloc_filespec(elem->path);
826 - fill_filespec(df, null_sha1, st.st_mode);
826 + fill_filespec(df, null_sha1, 0, st.st_mode);
827 result_size = fill_textconv(textconv, df, &result);
828 free_filespec(df);
829 } else if (0 <= (fd = open(elem->path, O_RDONLY))) {
diff-lib.c
+12 -8
@@ -206,7 +206,8 @@ int run_diff_files(struct rev_info *revs, unsigned int option)
206 if (silent_on_removed)
207 continue;
208 diff_addremove(&revs->diffopt, '-', ce->ce_mode,
209 - ce->sha1, ce->name, 0);
209 + ce->sha1, !is_null_sha1(ce->sha1),
210 + ce->name, 0);
211 continue;
212 }
213 changed = match_stat_with_submodule(&revs->diffopt, ce, &st,
@@ -220,6 +221,7 @@ int run_diff_files(struct rev_info *revs, unsigned int option)
221 newmode = ce_mode_from_stat(ce, st.st_mode);
222 diff_change(&revs->diffopt, oldmode, newmode,
223 ce->sha1, (changed ? null_sha1 : ce->sha1),
224 + !is_null_sha1(ce->sha1), (changed ? 0 : !is_null_sha1(ce->sha1)),
225 ce->name, 0, dirty_submodule);
226
227 }
@@ -236,11 +238,12 @@ int run_diff_files(struct rev_info *revs, unsigned int option)
238 static void diff_index_show_file(struct rev_info *revs,
239 const char *prefix,
240 struct cache_entry *ce,
239 - const unsigned char *sha1, unsigned int mode,
241 + const unsigned char *sha1, int sha1_valid,
242 + unsigned int mode,
243 unsigned dirty_submodule)
244 {
245 diff_addremove(&revs->diffopt, prefix[0], mode,
243 - sha1, ce->name, dirty_submodule);
246 + sha1, sha1_valid, ce->name, dirty_submodule);
247 }
248
249 static int get_stat_data(struct cache_entry *ce,
@@ -295,7 +298,7 @@ static void show_new_file(struct rev_info *revs,
298 &dirty_submodule, &revs->diffopt) < 0)
299 return;
300
298 - diff_index_show_file(revs, "+", new, sha1, mode, dirty_submodule);
301 + diff_index_show_file(revs, "+", new, sha1, !is_null_sha1(sha1), mode, dirty_submodule);
302 }
303
304 static int show_modified(struct rev_info *revs,
@@ -312,7 +315,7 @@ static int show_modified(struct rev_info *revs,
315 &dirty_submodule, &revs->diffopt) < 0) {
316 if (report_missing)
317 diff_index_show_file(revs, "-", old,
315 - old->sha1, old->ce_mode, 0);
318 + old->sha1, 1, old->ce_mode, 0);
319 return -1;
320 }
321
@@ -347,7 +350,8 @@ static int show_modified(struct rev_info *revs,
350 return 0;
351
352 diff_change(&revs->diffopt, oldmode, mode,
350 - old->sha1, sha1, old->name, 0, dirty_submodule);
353 + old->sha1, sha1, 1, !is_null_sha1(sha1),
354 + old->name, 0, dirty_submodule);
355 return 0;
356 }
357
@@ -380,7 +384,7 @@ static void do_oneway_diff(struct unpack_trees_options *o,
384 struct diff_filepair *pair;
385 pair = diff_unmerge(&revs->diffopt, idx->name);
386 if (tree)
383 - fill_filespec(pair->one, tree->sha1, tree->ce_mode);
387 + fill_filespec(pair->one, tree->sha1, 1, tree->ce_mode);
388 return;
389 }
390
@@ -396,7 +400,7 @@ static void do_oneway_diff(struct unpack_trees_options *o,
400 * Something removed from the tree?
401 */
402 if (!idx) {
399 - diff_index_show_file(revs, "-", tree, tree->sha1, tree->ce_mode, 0);
403 + diff_index_show_file(revs, "-", tree, tree->sha1, 1, tree->ce_mode, 0);
404 return;
405 }
406
diff-no-index.c
+2 -2
@@ -141,8 +141,8 @@ static int queue_diff(struct diff_options *o,
141 name2 = "/dev/null";
142 d1 = alloc_filespec(name1);
143 d2 = alloc_filespec(name2);
144 - fill_filespec(d1, null_sha1, mode1);
145 - fill_filespec(d2, null_sha1, mode2);
144 + fill_filespec(d1, null_sha1, 0, mode1);
145 + fill_filespec(d2, null_sha1, 0, mode2);
146
147 diff_queue(&diff_queued_diff, d1, d2);
148 return 0;
diff.c
+10 -6
@@ -2443,12 +2443,12 @@ void free_filespec(struct diff_filespec *spec)
2443 }
2444
2445 void fill_filespec(struct diff_filespec *spec, const unsigned char *sha1,
2446 - unsigned short mode)
2446 + int sha1_valid, unsigned short mode)
2447 {
2448 if (mode) {
2449 spec->mode = canon_mode(mode);
2450 hashcpy(spec->sha1, sha1);
2451 - spec->sha1_valid = !is_null_sha1(sha1);
2451 + spec->sha1_valid = sha1_valid;
2452 }
2453 }
2454
@@ -4589,6 +4589,7 @@ static int is_submodule_ignored(const char *path, struct diff_options *options)
4589 void diff_addremove(struct diff_options *options,
4590 int addremove, unsigned mode,
4591 const unsigned char *sha1,
4592 + int sha1_valid,
4593 const char *concatpath, unsigned dirty_submodule)
4594 {
4595 struct diff_filespec *one, *two;
@@ -4620,9 +4621,9 @@ void diff_addremove(struct diff_options *options,
4621 two = alloc_filespec(concatpath);
4622
4623 if (addremove != '+')
4623 - fill_filespec(one, sha1, mode);
4624 + fill_filespec(one, sha1, sha1_valid, mode);
4625 if (addremove != '-') {
4625 - fill_filespec(two, sha1, mode);
4626 + fill_filespec(two, sha1, sha1_valid, mode);
4627 two->dirty_submodule = dirty_submodule;
4628 }
4629
@@ -4635,6 +4636,7 @@ void diff_change(struct diff_options *options,
4636 unsigned old_mode, unsigned new_mode,
4637 const unsigned char *old_sha1,
4638 const unsigned char *new_sha1,
4639 + int old_sha1_valid, int new_sha1_valid,
4640 const char *concatpath,
4641 unsigned old_dirty_submodule, unsigned new_dirty_submodule)
4642 {
@@ -4649,6 +4651,8 @@ void diff_change(struct diff_options *options,
4651 const unsigned char *tmp_c;
4652 tmp = old_mode; old_mode = new_mode; new_mode = tmp;
4653 tmp_c = old_sha1; old_sha1 = new_sha1; new_sha1 = tmp_c;
4654 + tmp = old_sha1_valid; old_sha1_valid = new_sha1_valid;
4655 + new_sha1_valid = tmp;
4656 tmp = old_dirty_submodule; old_dirty_submodule = new_dirty_submodule;
4657 new_dirty_submodule = tmp;
4658 }
@@ -4659,8 +4663,8 @@ void diff_change(struct diff_options *options,
4663
4664 one = alloc_filespec(concatpath);
4665 two = alloc_filespec(concatpath);
4662 - fill_filespec(one, old_sha1, old_mode);
4663 - fill_filespec(two, new_sha1, new_mode);
4666 + fill_filespec(one, old_sha1, old_sha1_valid, old_mode);
4667 + fill_filespec(two, new_sha1, new_sha1_valid, new_mode);
4668 one->dirty_submodule = old_dirty_submodule;
4669 two->dirty_submodule = new_dirty_submodule;
4670
diff.h
+5
@@ -19,12 +19,14 @@ typedef void (*change_fn_t)(struct diff_options *options,
19 unsigned old_mode, unsigned new_mode,
20 const unsigned char *old_sha1,
21 const unsigned char *new_sha1,
22 + int old_sha1_valid, int new_sha1_valid,
23 const char *fullpath,
24 unsigned old_dirty_submodule, unsigned new_dirty_submodule);
25
26 typedef void (*add_remove_fn_t)(struct diff_options *options,
27 int addremove, unsigned mode,
28 const unsigned char *sha1,
29 + int sha1_valid,
30 const char *fullpath, unsigned dirty_submodule);
31
32 typedef void (*diff_format_fn_t)(struct diff_queue_struct *q,
@@ -209,12 +211,15 @@ extern void diff_addremove(struct diff_options *,
211 int addremove,
212 unsigned mode,
213 const unsigned char *sha1,
214 + int sha1_valid,
215 const char *fullpath, unsigned dirty_submodule);
216
217 extern void diff_change(struct diff_options *,
218 unsigned mode1, unsigned mode2,
219 const unsigned char *sha1,
220 const unsigned char *sha2,
221 + int sha1_valid,
222 + int sha2_valid,
223 const char *fullpath,
224 unsigned dirty_submodule1, unsigned dirty_submodule2);
225
diffcore-rename.c
+1 -1
@@ -48,7 +48,7 @@ static struct diff_rename_dst *locate_rename_dst(struct diff_filespec *two,
48 memmove(rename_dst + first + 1, rename_dst + first,
49 (rename_dst_nr - first - 1) * sizeof(*rename_dst));
50 rename_dst[first].two = alloc_filespec(two->path);
51 - fill_filespec(rename_dst[first].two, two->sha1, two->mode);
51 + fill_filespec(rename_dst[first].two, two->sha1, two->sha1_valid, two->mode);
52 rename_dst[first].pair = NULL;
53 return &(rename_dst[first]);
54 }
diffcore.h
+1 -1
@@ -54,7 +54,7 @@ struct diff_filespec {
54 extern struct diff_filespec *alloc_filespec(const char *);
55 extern void free_filespec(struct diff_filespec *);
56 extern void fill_filespec(struct diff_filespec *, const unsigned char *,
57 - unsigned short);
57 + int, unsigned short);
58
59 extern int diff_populate_filespec(struct diff_filespec *, int);
60 extern void diff_free_filespec_data(struct diff_filespec *);
revision.c
+2
@@ -332,6 +332,7 @@ static int tree_difference = REV_TREE_SAME;
332 static void file_add_remove(struct diff_options *options,
333 int addremove, unsigned mode,
334 const unsigned char *sha1,
335 + int sha1_valid,
336 const char *fullpath, unsigned dirty_submodule)
337 {
338 int diff = addremove == '+' ? REV_TREE_NEW : REV_TREE_OLD;
@@ -345,6 +346,7 @@ static void file_change(struct diff_options *options,
346 unsigned old_mode, unsigned new_mode,
347 const unsigned char *old_sha1,
348 const unsigned char *new_sha1,
349 + int old_sha1_valid, int new_sha1_valid,
350 const char *fullpath,
351 unsigned old_dirty_submodule, unsigned new_dirty_submodule)
352 {
t/t4054-diff-bogus-tree.sh new
+83
@@ -0,0 +1,83 @@
1 +#!/bin/sh
2 +
3 +test_description='test diff with a bogus tree containing the null sha1'
4 +. ./test-lib.sh
5 +
6 +empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
7 +
8 +test_expect_success 'create bogus tree' '
9 + bogus_tree=$(
10 + printf "100644 fooQQQQQQQQQQQQQQQQQQQQQ" |
11 + q_to_nul |
12 + git hash-object -w --stdin -t tree
13 + )
14 +'
15 +
16 +test_expect_success 'create tree with matching file' '
17 + echo bar >foo &&
18 + git add foo &&
19 + good_tree=$(git write-tree)
20 + blob=$(git rev-parse :foo)
21 +'
22 +
23 +test_expect_success 'raw diff shows null sha1 (addition)' '
24 + echo ":000000 100644 $_z40 $_z40 A foo" >expect &&
25 + git diff-tree $empty_tree $bogus_tree >actual &&
26 + test_cmp expect actual
27 +'
28 +
29 +test_expect_success 'raw diff shows null sha1 (removal)' '
30 + echo ":100644 000000 $_z40 $_z40 D foo" >expect &&
31 + git diff-tree $bogus_tree $empty_tree >actual &&
32 + test_cmp expect actual
33 +'
34 +
35 +test_expect_success 'raw diff shows null sha1 (modification)' '
36 + echo ":100644 100644 $blob $_z40 M foo" >expect &&
37 + git diff-tree $good_tree $bogus_tree >actual &&
38 + test_cmp expect actual
39 +'
40 +
41 +test_expect_success 'raw diff shows null sha1 (other direction)' '
42 + echo ":100644 100644 $_z40 $blob M foo" >expect &&
43 + git diff-tree $bogus_tree $good_tree >actual &&
44 + test_cmp expect actual
45 +'
46 +
47 +test_expect_success 'raw diff shows null sha1 (reverse)' '
48 + echo ":100644 100644 $_z40 $blob M foo" >expect &&
49 + git diff-tree -R $good_tree $bogus_tree >actual &&
50 + test_cmp expect actual
51 +'
52 +
53 +test_expect_success 'raw diff shows null sha1 (index)' '
54 + echo ":100644 100644 $_z40 $blob M foo" >expect &&
55 + git diff-index $bogus_tree >actual &&
56 + test_cmp expect actual
57 +'
58 +
59 +test_expect_success 'patch fails due to bogus sha1 (addition)' '
60 + test_must_fail git diff-tree -p $empty_tree $bogus_tree
61 +'
62 +
63 +test_expect_success 'patch fails due to bogus sha1 (removal)' '
64 + test_must_fail git diff-tree -p $bogus_tree $empty_tree
65 +'
66 +
67 +test_expect_success 'patch fails due to bogus sha1 (modification)' '
68 + test_must_fail git diff-tree -p $good_tree $bogus_tree
69 +'
70 +
71 +test_expect_success 'patch fails due to bogus sha1 (other direction)' '
72 + test_must_fail git diff-tree -p $bogus_tree $good_tree
73 +'
74 +
75 +test_expect_success 'patch fails due to bogus sha1 (reverse)' '
76 + test_must_fail git diff-tree -R -p $good_tree $bogus_tree
77 +'
78 +
79 +test_expect_success 'patch fails due to bogus sha1 (index)' '
80 + test_must_fail git diff-index -p $bogus_tree
81 +'
82 +
83 +test_done
tree-diff.c
+4 -4
@@ -49,12 +49,12 @@ static int compare_tree_entry(struct tree_desc *t1, struct tree_desc *t2,
49 if (DIFF_OPT_TST(opt, RECURSIVE) && S_ISDIR(mode1)) {
50 if (DIFF_OPT_TST(opt, TREE_IN_RECURSIVE)) {
51 opt->change(opt, mode1, mode2,
52 - sha1, sha2, base->buf, 0, 0);
52 + sha1, sha2, 1, 1, base->buf, 0, 0);
53 }
54 strbuf_addch(base, '/');
55 diff_tree_sha1(sha1, sha2, base->buf, opt);
56 } else {
57 - opt->change(opt, mode1, mode2, sha1, sha2, base->buf, 0, 0);
57 + opt->change(opt, mode1, mode2, sha1, sha2, 1, 1, base->buf, 0, 0);
58 }
59 strbuf_setlen(base, old_baselen);
60 return 0;
@@ -100,7 +100,7 @@ static void show_entry(struct diff_options *opt, const char *prefix,
100 die("corrupt tree sha %s", sha1_to_hex(sha1));
101
102 if (DIFF_OPT_TST(opt, TREE_IN_RECURSIVE))
103 - opt->add_remove(opt, *prefix, mode, sha1, base->buf, 0);
103 + opt->add_remove(opt, *prefix, mode, sha1, 1, base->buf, 0);
104
105 strbuf_addch(base, '/');
106
@@ -108,7 +108,7 @@ static void show_entry(struct diff_options *opt, const char *prefix,
108 show_tree(opt, prefix, &inner, base);
109 free(tree);
110 } else
111 - opt->add_remove(opt, prefix[0], mode, sha1, base->buf, 0);
111 + opt->add_remove(opt, prefix[0], mode, sha1, 1, base->buf, 0);
112
113 strbuf_setlen(base, old_baselen);
114 }