Download raw body.
fix 'got blame' segfault
On Thu, Dec 17, 2020 at 09:35:46PM +0100, Stefan Sperling wrote:
> On IRC jrick reported that this command triggers a segfault:
>
> $ got blame distrib/miniroot/install.sub
>
> assertion "blame->f2" failed: file "/home/jrick/src/got/tog/../lib/blame.c", line 480, function "close_file2_and_reuse_file1"
>
> In fact, both file pointers in the 'blame' struct are NULL.
>
> The problem is that the commit graph traverses one commit too far.
> This can be more easily reproduced with 'got log':
>
> got log -c 05f568 -P distrib/miniroot/install.sub
>
> This lists two commits:
> 05f568ecc6aadefa1aff9064a29e798874a71409
> 7c0d87f00e480cdf004324dad6f3e6f4418f8f42
>
> commit 05f568ecc6aade... added the file distrib/miniroot/install.sub.
> The above 'got log' command returns not just this commit as expected,
> it additionally returns commit 7c0d87f00e480cd...
>
> The problem is a bug in the logic of got_object_tree_path_changed(),
> which causes the commit graph to add 7c0d87f00e480... to the list of
> commits which must still be traversed, even though the history of the
> logged path has already ended.
>
> Note that "distrib/miniroot" exists in 7c0d87f00e480cdf004324... but not
> in the parent of this commit.
>
> At the point where are traversing "distrib/miniroot" the tree entry te1
> exists and corresponds to this tree listing the contents of 'miniroot'
> in 7c0d87f00e480cdf004324...:
>
> $ got cat 180aa33df8d1
Where does object come from?
> 32da652eda37ea59d2f1a37b2a289a95aff7aa2a 0100644 Makefile
> 40d8ab1f48c46b8ff676812ed68cce2de57f5aa4 0100644 list
> 8fce89bffd7b91b1d4e679f05f707c7981744560 0100644 list2sh.awk
> a0af0e56d446e56a5253d7ae25721ba405ac1898 0100644 makeconf.awk
> 27bcebb1e71174a0bbcc6d24495ce0894c02e54c 0100644 mtree.conf
> 1ea9ff121981ba5cf3042227aea3c455924e779d 0100644 runlist.sh
> $
>
> And now try to open te2, which fails because "distrib/miniroot" does not
> exist in the parent commit of 7c0d87f00e480cdf004324...:
>
> te2 = find_entry_by_name(tree2, seg, seglen);
> if (te2 == NULL) {
> *changed = 1;
> goto done;
> }
>
> Because te2 cannot be opened, the code assumes success.
> We now return with err == NULL and *changed = 1; the commit graph interprets
> this as 'install.sub' having changed. What's obviously missing here is an
> existence check for 'install.sub' in te1! (Alternatively, we could allow
> the caller to differentiate between the cases where the file was 'added'
> and where the file was 'changed', so they can stop asking for more commits
> if the file was added. But that would require changing all callers...)
>
> The patch below ensures that we traverse the full path in tree1 even if an
> intermediate tree2 cannot be opened. Now we eventually hit this attempt to
> open the file 'install.sub' in tree object 180aa33df8d1 (tree1):
>
> te1 = find_entry_by_name(tree1, seg, seglen);
> if (te1 == NULL) {
> err = got_error(GOT_ERR_NO_OBJ);
> goto done;
> }
>
> The result is now ERR_NO_OBJ and *changed = 0. The commit graph will not
> add this commit to its iteration list. The command:
>
> $ got log -c 05f568 -P distrib/miniroot/install.sub
>
> will only list commit 05f568ecc6aade... which added the file.
>
> ok?
>
> diff e8f0f2f60d1eb5651a01b13607ca04014f092753 e8e36d3a9d44ec3db6e43abebe3db47e062f49a9
> blob - 7c0aac05143e35448209231ec4347e3ad6e2252b
> blob + 7bd55922b5b83ac06fbadac4c69364802b1a6179
> --- lib/object.c
> +++ lib/object.c
> @@ -1771,22 +1771,21 @@ got_object_tree_path_changed(int *changed,
> goto done;
> }
>
> - te2 = find_entry_by_name(tree2, seg, seglen);
> - if (te2 == NULL) {
> - *changed = 1;
> - goto done;
> - }
> + if (tree2)
> + te2 = find_entry_by_name(tree2, seg, seglen);
>
> - mode1 = normalize_mode_for_comparison(te1->mode);
> - mode2 = normalize_mode_for_comparison(te2->mode);
> - if (mode1 != mode2) {
> - *changed = 1;
> - goto done;
> - }
> + if (te2) {
> + mode1 = normalize_mode_for_comparison(te1->mode);
> + mode2 = normalize_mode_for_comparison(te2->mode);
> + if (mode1 != mode2) {
> + *changed = 1;
> + goto done;
> + }
>
> - if (got_object_id_cmp(&te1->id, &te2->id) == 0) {
> - *changed = 0;
> - goto done;
> + if (got_object_id_cmp(&te1->id, &te2->id) == 0) {
> + *changed = 0;
> + goto done;
> + }
> }
>
> if (*s == '\0') { /* final path element */
> @@ -1807,14 +1806,20 @@ got_object_tree_path_changed(int *changed,
> got_object_tree_close(tree1);
> tree1 = next_tree1;
>
> - err = got_object_open_as_tree(&next_tree2, repo,
> - &te2->id);
> - te2 = NULL;
> - if (err)
> - goto done;
> - if (tree2 != tree02)
> - got_object_tree_close(tree2);
> - tree2 = next_tree2;
> + if (te2) {
> + err = got_object_open_as_tree(&next_tree2, repo,
> + &te2->id);
> + te2 = NULL;
> + if (err)
> + goto done;
> + if (tree2 != tree02)
> + got_object_tree_close(tree2);
> + tree2 = next_tree2;
> + } else if (tree2) {
> + if (tree2 != tree02)
> + got_object_tree_close(tree2);
> + tree2 = NULL;
> + }
> }
> }
> done:
>
Other than the question above (which is just me trying to understand
how this was debugged), this makes sense to me and the patch looks
right. I confirmed that the segfault and the debug assert i added is
no longer being hit, and got log -P only shows the 05f568ecc6aade
commit.
fix 'got blame' segfault