midx: prevent against racily disappearing packs
cover midx: prevent against racily disappearing packs
Here are a couple of patches that Peff and I wrote after noticing a problem where racily disappearing .idx files can cause a segfault. While investigating, we fixed a related issue which is described and fixed in the second patch.
(I'm a little behind in responding to review on the reachability generation improvements patches, but had these in my backlog and they seemed relatively easy to send and remove from my TODO list. So, here they are :-).)
Thanks,
Taylor
Taylor Blau (2):
packfile.c: protect against disappearing indexes midx.c: protect against disappearing packs
midx.c | 2 +- packfile.c | 19 ++----------------- t/t5319-multi-pack-index.sh | 30 ++++++++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 20 deletions(-)
--
2.29.2.368.ge1806d1bdc
PATCH 1/2 packfile.c: protect against disappearing indexes
In 17c35c8969 (packfile: skip loading index if in multi-pack-index, 2018-07-12) we stopped loading the .idx file for packs that are contained within a multi-pack index.
This saves us the effort of loading an .idx and doing some lightweight validity checks by way of 'packfile.c:load_idx()', but introduces a race between processes that need to load the index (e.g., to generate a reverse index) and processes that can delete the index.
For example, running the following in your shell:
$ git init repo && cd repo
$ git commit --allow-empty -m 'base'
$ git repack -ad && git multi-pack-index writefollowed by:
$ rm -f .git/objects/pack/pack-*.idx
$ git rev-parse HEAD | git cat-file --batch-check='%(objectsize:disk)'will result in a segfault prior to this patch. What's happening here is that we notice that the pack is in the multi-pack index, and so don't check that it still has a .idx. When we then try and load that index to generate a reverse index, we don't have it, so the call to 'find_pack_revindex()' in 'packfile.c:packed_object_info()' returns NULL, and then dereferencing it causes a segfault.
Of course, we don't ever expect someone to remove the index file by hand, or to be in a state where we never wrote it to begin with (yet find that pack in the multi-pack-index). But, this can happen in a timing race with 'git repack -ad', which removes all existing packs after writing a new pack containing all of their objects.
Avoid this by reverting the hunk of 17c35c8969 which stops loading the index when the pack is contained in a MIDX. This makes the latter half of 17c35c8969 useless, since we'll always have a non-NULL 'p->index_data', in which case that if statement isn't guarding anything.
These two together effectively revert 17c35c8969, and avoid the race explained above.
--- packfile.c | 19 ++----------------- t/t5319-multi-pack-index.sh | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 19 deletions(-)
packfile.c +2 -17
@@ -514,19 +514,8 @@ static int open_packed_git_1(struct packed_git *p)ssize_t read_result;const unsigned hashsz = the_hash_algo->rawsz; -if (!p->index_data) { -struct multi_pack_index *m; -const char *pack_name =pack_basename (p); - -for (m = the_repository->objects->multi_pack_index; - m; m = m->next) { -if (midx_contains_pack (m, pack_name)) -break ; - } - -if (!m &&open_pack_index (p)) -return error ("packfile %s index unavailable" , p->pack_name); - } +if (open_pack_index (p)) +return error ("packfile %s index unavailable" , p->pack_name);if (!pack_max_fds) {unsigned int max_fds =get_max_fd_limit ();
@@ -567,10 +556,6 @@ static int open_packed_git_1(struct packed_git *p)" supported (try upgrading GIT to a newer version)" , p->pack_name, ntohl(hdr.hdr_version)); -/* Skip index checking if in multi-pack-index */ -if (!p->index_data) -return 0 ; -/* Verify the pack matches its index. */ if (p->num_objects !=ntohl (hdr.hdr_entries)) return error("packfile %s claims to have %"PRIu32" objects"
t/t5319-multi-pack-index.sh +22 -2
@@ -138,7 +138,7 @@ test_expect_success 'write midx with one v2 pack' ' compare_results_with_midx "one v2 pack" -test_expect_success 'corrupt idx not opened' ' +test_expect_success 'corrupt idx reports errors' ' idx=$(test-tool read-midx $objdir | grep "\.idx\$") && mv $objdir/pack/$idx backup-$idx && test_when_finished "mv backup-\$idx \$objdir/pack/\$idx" &&
@@ -149,7 +149,7 @@ test_expect_success 'corrupt idx not opened' ' test_copy_bytes 1064 <backup-$idx >$objdir/pack/$idx && git -c core.multiPackIndex=true rev-list --objects --all 2>err && - test_must_be_empty err + grep "index unavailable" err ' test_expect_success 'add more objects' '
@@ -755,4 +755,24 @@ test_expect_success 'repack --batch-size=<large> repacks everything' ' ) ' +test_expect_success 'load reverse index when missing .idx' ' + git init repo && + test_when_finished "rm -fr repo" && + ( + cd repo && + + git config core.multiPackIndex true && + + test_commit base && + git repack -ad && + git multi-pack-index write && + + git rev-parse HEAD >tip && + idx=$(ls .git/objects/pack/pack-*.idx) && + + mv $idx $idx.bak && + git cat-file --batch-check="%(objectsize:disk)" <tip + ) +' + test_done
Signature · 1 line
2.29.2.368.ge1806d1bdc
Re: [PATCH 1/2] packfile.c: protect against disappearing indexes
Quoted 8 lines · Taylor Blau
In
17c35c8969(packfile: skip loading index if in multi-pack-index, 2018-07-12) we stopped loading the .idx file for packs that are contained within a multi-pack index.This saves us the effort of loading an .idx and doing some lightweight validity checks by way of 'packfile.c:load_idx()', but introduces a race between processes that need to load the index (e.g., to generate a reverse index) and processes that can delete the index.
Ah, OK. And demonstration using %(objectsize:disk) makes perfect sense given the above explanation.
Quoted 9 lines
These two together effectively revert
17c35c8969, and avoid the race explained above.Co-authored-by Jeff King <[email protected]>Signed-off-by Taylor Blau <[email protected]>--- packfile.c | 19 ++----------------- t/t5319-multi-pack-index.sh | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 19 deletions(-)
Makes sense. Thanks.
Quoted 84 lines
packfile.c +2
-17@@ -514,19 +514,8 @@ static int open_packed_git_1(struct packed_git *p)ssize_t read_result;const unsigned hashsz = the_hash_algo->rawsz; -if (!p->index_data) { -struct multi_pack_index *m; -const char *pack_name =pack_basename (p); - -for (m = the_repository->objects->multi_pack_index; - m; m = m->next) { -if (midx_contains_pack (m, pack_name)) -break ; - } - -if (!m &&open_pack_index (p)) -return error ("packfile %s index unavailable" , p->pack_name); - } +if (open_pack_index (p)) +return error ("packfile %s index unavailable" , p->pack_name);if (!pack_max_fds) {unsigned int max_fds =get_max_fd_limit ();@@ -567,10 +556,6 @@ static int open_packed_git_1(struct packed_git *p)" supported (try upgrading GIT to a newer version)" , p->pack_name, ntohl(hdr.hdr_version)); -/* Skip index checking if in multi-pack-index */ -if (!p->index_data) -return 0 ; -/* Verify the pack matches its index. */ if (p->num_objects !=ntohl (hdr.hdr_entries)) return error("packfile %s claims to have %"PRIu32" objects"t/t5319-multi-pack-index.sh +22
-2@@ -138,7 +138,7 @@ test_expect_success 'write midx with one v2 pack' ' compare_results_with_midx "one v2 pack" -test_expect_success 'corrupt idx not opened' ' +test_expect_success 'corrupt idx reports errors' ' idx=$(test-tool read-midx $objdir | grep "\.idx\$") && mv $objdir/pack/$idx backup-$idx && test_when_finished "mv backup-\$idx \$objdir/pack/\$idx" &&@@ -149,7 +149,7 @@ test_expect_success 'corrupt idx not opened' ' test_copy_bytes 1064 <backup-$idx >$objdir/pack/$idx && git -c core.multiPackIndex=true rev-list --objects --all 2>err && - test_must_be_empty err + grep "index unavailable" err ' test_expect_success 'add more objects' '@@ -755,4 +755,24 @@ test_expect_success 'repack --batch-size=<large> repacks everything' ' ) ' +test_expect_success 'load reverse index when missing .idx' ' + git init repo && + test_when_finished "rm -fr repo" && + ( + cd repo && + + git config core.multiPackIndex true && + + test_commit base && + git repack -ad && + git multi-pack-index write && + + git rev-parse HEAD >tip && + idx=$(ls .git/objects/pack/pack-*.idx) && + + mv $idx $idx.bak && + git cat-file --batch-check="%(objectsize:disk)" <tip + ) +' + test_done
PATCH 2/2 midx.c: protect against disappearing packs
In reply to Taylor BlauWhen a packed object is stored in a multi-pack index, but that pack has racily gone away, the MIDX code simply calls die(), when it could be returning an error to the caller, which would in turn lead to re-scanning the pack directory.
A pack can racily disappear, for example, due to a simultaneous 'git repack -ad',
You can also reproduce this with two terminals, where one is running:
git init
while true; do
git commit -q --allow-empty -m foo
git repack -ad
git multi-pack-index write
done
(in effect, constantly writing new MIDXs), and the other is running:
obj=$(git rev-parse HEAD)
while true; do
echo $obj | git cat-file --batch-check='%(objectsize:disk)' || break
doneThat will sometimes hit the error preparing packfile from multi-pack-index message, which this patch fixes.
Right now, that path to discovering a missing pack looks something like 'find_pack_entry()' calling 'fill_midx_entry()' and eventually making its way to call 'nth_midxed_pack_entry()'.
'nth_midxed_pack_entry()' already checks 'is_pack_valid()' and propagates an error if the pack is invalid. So, this works if the pack has gone away between calling 'prepare_midx_pack()' and before calling 'is_pack_valid()', but not if it disappears before then.
Catch the case where the pack has already disappeared before 'prepare_midx_pack()' by returning an error in that case, too.
--- midx.c | 2 +- t/t5319-multi-pack-index.sh | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-)
midx.c +1 -1
@@ -298,7 +298,7 @@ static int nth_midxed_pack_entry(struct repository *r, pack_int_id =nth_midxed_pack_int_id (m, pos);if (prepare_midx_pack (r, m, pack_int_id)) -die (_ ("error preparing packfile from multi-pack-index" )); +return 0 ; p = m->packs[pack_int_id];/*
t/t5319-multi-pack-index.sh +7 -1
@@ -755,7 +755,7 @@ test_expect_success 'repack --batch-size=<large> repacks everything' ' ) ' -test_expect_success 'load reverse index when missing .idx' ' +test_expect_success 'load reverse index when missing .idx, .pack' ' git init repo && test_when_finished "rm -fr repo" && (
@@ -768,9 +768,15 @@ test_expect_success 'load reverse index when missing .idx' ' git multi-pack-index write && git rev-parse HEAD >tip && + pack=$(ls .git/objects/pack/pack-*.pack) && idx=$(ls .git/objects/pack/pack-*.idx) && mv $idx $idx.bak && + git cat-file --batch-check="%(objectsize:disk)" <tip && + + mv $idx.bak $idx && + + mv $pack $pack.bak && git cat-file --batch-check="%(objectsize:disk)" <tip ) '
Signature · 1 line
2.29.2.368.ge1806d1bdc
Re: [PATCH 2/2] midx.c: protect against disappearing packs
Quoted 4 lines · Taylor Blau
When a packed object is stored in a multi-pack index, but that pack has racily gone away, the MIDX code simply calls
die(), when it could be returning an error to the caller, which would in turn lead to re-scanning the pack directory.
Makes sense. Will queue.
Thanks.
Quoted 4 lines · Taylor Blau
Here are a couple of patches that Peff and I wrote after noticing a problem where racily disappearing .idx files can cause a segfault. While investigating, we fixed a related issue which is described and fixed in the second patch.
In the cover letter it won't affect the end result, but when talking about "race", it always is a good idea to explicitly mention both sides of the race. It is clear what one side is in the above (i.e. somebody who removes .idx file that is still in use), but it is not so clear who gets hit and segfaults.
I am guessing that the other party is the user of .pack file(s) bypassing the corresponding .idx file(s) because the necessary data is mostly in .midx?
Thanks.
Quoted 13 lines · Junio C Hamano
Quoted 4 lines · Taylor Blau
Here are a couple of patches that Peff and I wrote after noticing a problem where racily disappearing .idx files can cause a segfault. While investigating, we fixed a related issue which is described and fixed in the second patch.
In the cover letter it won't affect the end result, but when talking about "race", it always is a good idea to explicitly mention both sides of the race. It is clear what one side is in the above (i.e. somebody who removes .idx file that is still in use), but it is not so clear who gets hit and segfaults.
Quoted 3 lines
I am guessing that the other party is the user of .pack file(s) bypassing the corresponding .idx file(s) because the necessary data is mostly in .midx?
Yeah, the race reproduction in the second commit message can actually reproduce the segfault as well (it depends on the exact timing which error you get). So the segfault is in the reader, who is not checking the result of find_revindex_entry().
Arguably every call there should be checking for NULL, but in practice I think it would always be a bug:
- we were somehow unable to open the index in order to generate the revindex (which is what happened here). But I think we are better off making sure that we can always do so, which is what this series does.
- the caller asked about an object at a position beyond the number of objects in the packfile. This is a bug in the caller.
So we could perhaps BUG() in find_revindex_entry() instead of returning NULL. A quick segfault accomplishes mostly the same thing, though the BUG() could distinguish the two cases more clearly.
-Peff
Quoted 13 lines · Jeff King
Yeah, the race reproduction in the second commit message can actually reproduce the segfault as well (it depends on the exact timing which error you get). So the segfault is in the reader, who is not checking the result of
find_revindex_entry().Arguably every call there should be checking for NULL, but in practice I think it would always be a bug:
- we were somehow unable to open the index in order to generate the revindex (which is what happened here). But I think we are better off making sure that we can always do so, which is what this series does.
Quoted 6 lines
- the caller asked about an object at a position beyond the number of objects in the packfile. This is a bug in the caller.
So we could perhaps
BUG()infind_revindex_entry()instead of returning NULL. A quick segfault accomplishes mostly the same thing, though theBUG()could distinguish the two cases more clearly.
Yeah, a find_revindex_entry() that returns NULL means that the caller is probably dead in the water.
FWIW, this function gets touched by a series that I'm working on here:
[1]. There, I think "returning NULL" is equivalent to "returning -1", and the problem exists there, too.
We could return a different negative number, call BUG(), or do nothing other than what's written. I don't have any strong feelings, though.
Quoted 1 line
-Peff
Thanks,
Taylor
[1]: https://github.com/ttaylorr/git/blob/tb/on-disk-revindex-part-one/pack-revindex.c#L177-L201
Quoted 7 lines · Taylor Blau
Quoted 3 lines
So we could perhaps
BUG()infind_revindex_entry()instead of returning NULL. A quick segfault accomplishes mostly the same thing, though theBUG()could distinguish the two cases more clearly.Yeah, a
find_revindex_entry()that returns NULL means that the caller is probably dead in the water.
Quoted 6 lines
FWIW, this function gets touched by a series that I'm working on here:
[1]. There, I think "returning NULL" is equivalent to "returning -1", and the problem exists there, too.We could return a different negative number, call
BUG(), or do nothing other than what's written. I don't have any strong feelings, though.
Yeah, I was suggesting _never_ returning a failure, and just hitting a BUG() within the function. So it does not matter then how you represent the error return type, because there isn't one. :)
Looking over the callers, there are actually a few that check the return value and handle it sanely. I suspect they can never trigger in practice, given that the other callers would all segfault, and we have not seen any reports in the 15 years during which that has been the case. But perhaps some of them can be triggered by bogus pack data that nobody has ever run into in the real world.
At any rate, I am content to ignore it until somebody feels like digging into each caller. A BUG() is only marginally better than an immediate segfault anyway, and I'd prefer not to disrupt more substantive improvements in the area.
-Peff