Skip to content

Commit 63aca3f

Browse files
peffttaylorr
authored andcommitted
dumb-http: store downloaded pack idx as tempfile
This patch fixes a regression in b1b8dfd (finalize_object_file(): implement collision check, 2024-09-26) where fetching a v1 pack idx file over the dumb-http protocol would cause the fetch to fail. The core of the issue is that dumb-http stores the idx we fetch from the remote at the same path that will eventually hold the idx we generate from "index-pack --stdin". The sequence is something like this: 0. We realize we need some object X, which we don't have locally, and nor does the other side have it as a loose object. 1. We download the list of remote packs from objects/info/packs. 2. For each entry in that file, we download each pack index and store it locally in .git/objects/pack/pack-$hash.idx (the $hash is not something we can verify yet and is given to us by the remote). 3. We check each pack index we got to see if it has object X. When we find a match, we download the matching .pack file from the remote to a tempfile. We feed that to "index-pack --stdin", which reindexes the pack, rather than trusting that it has what the other side claims it does. In most cases, this will end up generating the exact same (byte-for-byte) pack index which we'll store at the same pack-$hash.idx path, because the index generation and $hash id are computed based on what's in the packfile. But: a. The other side might have used other options to generate the index. For instance we use index v2 by default, but long ago it was v1 (and you can still ask for v1 explicitly). b. The other side might even use a different mechanism to determine $hash. E.g., long ago it was based on the sorted list of objects in the packfile, but we switched to using the pack checksum in 1190a1a (pack-objects: name pack files after trailer hash, 2013-12-05). The regression we saw in the real world was (3a). A recent client fetching from a server with a v1 index downloaded that index, then complained about trying to overwrite it with its own v2 index. This collision is otherwise harmless; we know we want to replace the remote version with our local one, but the collision check doesn't realize that. There are a few options to fix it: - we could teach index-pack a command-line option to ignore only pack idx collisions, and use it when the dumb-http code invokes index-pack. This would be an awkward thing to expose users to and would involve a lot of boilerplate to get the option down to the collision code. - we could delete the remote .idx file right before running index-pack. It should be redundant at that point (since we've just downloaded the matching pack). But it feels risky to delete something from our own .git/objects based on what the other side has said. I'm not entirely positive that a malicious server couldn't lie about which pack-$hash.idx it has and get us to delete something precious. - we can stop co-mingling the downloaded idx files in our local objects directory. This is a slightly bigger change but I think fixes the root of the problem more directly. This patch implements the third option. The big design questions are: where do we store the downloaded files, and how do we manage their lifetimes? There are some additional quirks to the dumb-http system we should consider. Remember that in step 2 we downloaded every pack index, but in step 3 we may only download some of the matching packs. What happens to those other idx files now? They sit in the .git/objects/pack directory, possibly waiting to be used at a later date. That may save bandwidth for a subsequent fetch, but it also creates a lot of weird corner cases: - our local object directory now has semi-untrusted .idx files sitting around, without their matching .pack - in case 3b, we noted that we might not generate the same hash as the other side. In that case even if we download the matching pack, our index-pack invocation will store it in a different pack-$hash.idx file. And the unmatched .idx will sit there forever. - if the server repacks, it may delete the old packs. Now we have these orphaned .idx files sitting around locally that will never be used (nor deleted). - if we repack locally we may delete our local version of the server's pack index and not realize we have it. So we'll download it again, even though we have all of the objects it mentions. I think the right solution here is probably some more complex cache management system: download the remote .idx files to their own storage directory, mark them as "seen" when we get their matching pack (to avoid re-downloading even if we repack), and then delete them when the server's objects/info/refs no longer mentions them. But since the dumb http protocol is so ancient and so inferior to the smart http protocol, I don't think it's worth spending a lot of time creating such a system. For this patch I'm just downloading the idx files to .git/objects/tmp_pack_*, and marking them as tempfiles to be deleted when we exit (and due to the name, any we miss due to a crash, etc, should eventually be removed by "git gc" runs based on timestamps). That is slightly worse for one case: if we download an idx but not the matching pack, we won't retain that idx for subsequent runs. But the flip side is that we're making other cases better (we never hold on to useless idx files forever). I suspect that worse case does not even come up often, since it implies that the packs are generated to match distinct parts of history (i.e., in practice even in a repo with many packs you're going to end up grabbing all of those packs to do a clone). If somebody really cares about that, I think the right path forward is a managed cache directory as above, and this patch is providing the first step in that direction anyway (by moving things out of the objects/pack/ directory). There are two test changes. One demonstrates the broken v1 index case (it double-checks the resulting clone with fsck to be careful, but prior to this patch it actually fails at the clone step). The other tweaks the expectation for a test that covers the "slightly worse" case to accommodate the extra index download. The code changes are fairly simple. We stop using finalize_object_file() to copy the remote's index file into place, and leave it as a tempfile. We give the tempfile a real ".idx" name, since the packfile code expects that, and thus we make sure it is out of the usual packs/ directory (so we'd never mistake it for a real local .idx). We also have to change parse_pack_index(), which creates a temporary packed_git to access our index (we need this because all of the pack idx code assumes we have that struct). It reads the index data from the tempfile, but prior to this patch would speculatively write the finalized name into the packed_git struct using the pack-$hash we expect to use. I was mildly surprised that this worked at all, since we call verify_pack_index() on the packed_git which mentions the final name before moving the file into place! But it works because parse_pack_index() leaves the mmap-ed data in the struct, so the lazy-open in verify_pack_index() never triggers, and we read from the tempfile, ignoring the filename in the struct completely. Hacky, but it works. After this patch, parse_pack_index() now uses the index filename we pass in to derive a matching .pack name. This is OK to change because there are only two callers, both in the dumb http code (and the other passes in an existing pack-$hash.idx name, so the derived name is going to be pack-$hash.pack, which is what we were using anyway). I'll follow up with some more cleanups in that area, but this patch is sufficient to fix the regression. Reported-by: fox <[email protected]> Signed-off-by: Jeff King <[email protected]> Signed-off-by: Taylor Blau <[email protected]>
1 parent 019b21d commit 63aca3f

File tree

3 files changed

+41
-7
lines changed

3 files changed

+41
-7
lines changed

http.c

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "string-list.h"
2020
#include "object-file.h"
2121
#include "object-store-ll.h"
22+
#include "tempfile.h"
2223

2324
static struct trace_key trace_curl = TRACE_KEY_INIT(CURL);
2425
static int trace_curl_data = 1;
@@ -2388,8 +2389,24 @@ static char *fetch_pack_index(unsigned char *hash, const char *base_url)
23882389
strbuf_addf(&buf, "objects/pack/pack-%s.idx", hash_to_hex(hash));
23892390
url = strbuf_detach(&buf, NULL);
23902391

2391-
strbuf_addf(&buf, "%s.temp", sha1_pack_index_name(hash));
2392-
tmp = strbuf_detach(&buf, NULL);
2392+
/*
2393+
* Don't put this into packs/, since it's just temporary and we don't
2394+
* want to confuse it with our local .idx files. We'll generate our
2395+
* own index if we choose to download the matching packfile.
2396+
*
2397+
* It's tempting to use xmks_tempfile() here, but it's important that
2398+
* the file not exist, otherwise http_get_file() complains. So we
2399+
* create a filename that should be unique, and then just register it
2400+
* as a tempfile so that it will get cleaned up on exit.
2401+
*
2402+
* In theory we could hold on to the tempfile and delete these as soon
2403+
* as we download the matching pack, but it would take a bit of
2404+
* refactoring. Leaving them until the process ends is probably OK.
2405+
*/
2406+
tmp = xstrfmt("%s/tmp_pack_%s.idx",
2407+
repo_get_object_directory(the_repository),
2408+
hash_to_hex(hash));
2409+
register_tempfile(tmp);
23932410

23942411
if (http_get_file(url, tmp, NULL) != HTTP_OK) {
23952412
error("Unable to get pack index %s", url);
@@ -2427,10 +2444,8 @@ static int fetch_and_setup_pack_index(struct packed_git **packs_head,
24272444
}
24282445

24292446
ret = verify_pack_index(new_pack);
2430-
if (!ret) {
2447+
if (!ret)
24312448
close_pack_index(new_pack);
2432-
ret = finalize_object_file(tmp_idx, sha1_pack_index_name(sha1));
2433-
}
24342449
free(tmp_idx);
24352450
if (ret)
24362451
return -1;

packfile.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,22 @@ static struct packed_git *alloc_packed_git(int extra)
237237
return p;
238238
}
239239

240+
static char *pack_path_from_idx(const char *idx_path)
241+
{
242+
size_t len;
243+
if (!strip_suffix(idx_path, ".idx", &len))
244+
BUG("idx path does not end in .idx: %s", idx_path);
245+
return xstrfmt("%.*s.pack", (int)len, idx_path);
246+
}
247+
240248
struct packed_git *parse_pack_index(unsigned char *sha1, const char *idx_path)
241249
{
242-
const char *path = sha1_pack_name(sha1);
250+
char *path = pack_path_from_idx(idx_path);
243251
size_t alloc = st_add(strlen(path), 1);
244252
struct packed_git *p = alloc_packed_git(alloc);
245253

246254
memcpy(p->pack_name, path, alloc); /* includes NUL */
255+
free(path);
247256
hashcpy(p->hash, sha1, the_repository->hash_algo);
248257
if (check_packed_git_idx(idx_path, p)) {
249258
free(p);

t/t5550-http-fetch-dumb.sh

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ test_expect_success 'fetch can handle previously-fetched .idx files' '
335335
count_fetches 1 pack one.trace &&
336336
GIT_TRACE_CURL=$PWD/two.trace git --git-dir=clone_packed_branches.git \
337337
fetch "$HTTPD_URL"/dumb/repo_packed_branches.git branch2:branch2 &&
338-
count_fetches 0 idx two.trace &&
338+
count_fetches 1 idx two.trace &&
339339
count_fetches 1 pack two.trace
340340
'
341341

@@ -521,4 +521,14 @@ test_expect_success 'fetching via http alternates works' '
521521
git -c http.followredirects=true clone "$HTTPD_URL/dumb/alt-child.git"
522522
'
523523

524+
test_expect_success 'dumb http can fetch index v1' '
525+
server=$HTTPD_DOCUMENT_ROOT_PATH/idx-v1.git &&
526+
git init --bare "$server" &&
527+
git -C "$server" --work-tree=. commit --allow-empty -m foo &&
528+
git -C "$server" -c pack.indexVersion=1 gc &&
529+
530+
git clone "$HTTPD_URL/dumb/idx-v1.git" &&
531+
git -C idx-v1 fsck
532+
'
533+
524534
test_done

0 commit comments

Comments
 (0)