]> git.ipfire.org Git - thirdparty/git.git/commitdiff
upload-pack: free tree buffers after parsing
authorJeff King <peff@peff.net>
Wed, 28 Feb 2024 22:39:07 +0000 (17:39 -0500)
committerJunio C Hamano <gitster@pobox.com>
Wed, 28 Feb 2024 22:42:01 +0000 (14:42 -0800)
When a client sends us a "want" or "have" line, we call parse_object()
to get an object struct. If the object is a tree, then the parsed state
means that tree->buffer points to the uncompressed contents of the tree.
But we don't really care about it. We only really need to parse commits
and tags; for trees and blobs, the important output is just a "struct
object" with the correct type.

But much worse, we do not ever free that tree buffer. It's not leaked in
the traditional sense, in that we still have a pointer to it from the
global object hash. But if the client requests many trees, we'll hold
all of their contents in memory at the same time.

Nobody really noticed because it's rare for clients to directly request
a tree. It might happen for a lightweight tag pointing straight at a
tree, or it might happen for a "tree:depth" partial clone filling in
missing trees.

But it's also possible for a malicious client to request a lot of trees,
causing upload-pack's memory to balloon. For example, without this
patch, requesting every tree in git.git like:

  pktline() {
    local msg="$*"
    printf "%04x%s\n" $((1+4+${#msg})) "$msg"
  }

  want_trees() {
    pktline command=fetch
    printf 0001
    git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype)' |
      while read oid type; do
        test "$type" = "tree" || continue
        pktline want $oid
      done
      pktline done
      printf 0000
  }

  want_trees | GIT_PROTOCOL=version=2 valgrind --tool=massif ./git upload-pack . >/dev/null

shows a peak heap usage of ~3.7GB. Which is just about the sum of the
sizes of all of the uncompressed trees. For linux.git, it's closer to
17GB.

So the obvious thing to do is to call free_tree_buffer() after we
realize that we've parsed a tree. We know that upload-pack won't need it
later. But let's push the logic into parse_object_with_flags(), telling
it to discard the tree buffer immediately. There are two reasons for
this. One, all of the relevant call-sites already call the with_options
variant to pass the SKIP_HASH flag. So it actually ends up as less code
than manually free-ing in each spot. And two, it enables an extra
optimization that I'll discuss below.

I've touched all of the sites that currently use SKIP_HASH in
upload-pack. That drops the peak heap of the upload-pack invocation
above from 3.7GB to ~24MB.

I've also modified the caller in get_reference(); a partial clone
benefits from its use in pack-objects for the reasons given in
0bc2557951 (upload-pack: skip parse-object re-hashing of "want" objects,
2022-09-06), where we were measuring blob requests. But note that the
results of get_reference() are used for traversing, as well; so we
really would _eventually_ use the tree contents. That makes this at
first glance a space/time tradeoff: we won't hold all of the trees in
memory at once, but we'll have to reload them each when it comes time to
traverse.

And here's where our extra optimization comes in. If the caller is not
going to immediately look at the tree contents, and it doesn't care
about checking the hash, then parse_object() can simply skip loading the
tree entirely, just like we do for blobs! And now it's not a space/time
tradeoff in get_reference() anymore. It's just a lazy-load: we're
delaying reading the tree contents until it's time to actually traverse
them one by one.

And of course for upload-pack, this optimization means we never load the
trees at all, saving lots of CPU time. Timing the "every tree from
git.git" request above shows upload-pack dropping from 32 seconds of CPU
to 19 (the remainder is mostly due to pack-objects actually sending the
pack; timing just the upload-pack portion shows we go from 13s to
~0.28s).

These are all highly gamed numbers, of course. For real-world
partial-clone requests we're saving only a small bit of time in
practice. But it does help harden upload-pack against malicious
denial-of-service attacks.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
object.c
object.h
revision.c
upload-pack.c

index 2c61e4c86217e633d2e28acd0b3ae654584ede7d..e305a3080409dbaecac5e7ed9e2c70c62ec8639b 100644 (file)
--- a/object.c
+++ b/object.c
@@ -272,6 +272,7 @@ struct object *parse_object_with_flags(struct repository *r,
                                       enum parse_object_flags flags)
 {
        int skip_hash = !!(flags & PARSE_OBJECT_SKIP_HASH_CHECK);
+       int discard_tree = !!(flags & PARSE_OBJECT_DISCARD_TREE);
        unsigned long size;
        enum object_type type;
        int eaten;
@@ -299,6 +300,17 @@ struct object *parse_object_with_flags(struct repository *r,
                return lookup_object(r, oid);
        }
 
+       /*
+        * If the caller does not care about the tree buffer and does not
+        * care about checking the hash, we can simply verify that we
+        * have the on-disk object with the correct type.
+        */
+       if (skip_hash && discard_tree &&
+           (!obj || obj->type == OBJ_TREE) &&
+           oid_object_info(r, oid, NULL) == OBJ_TREE) {
+               return &lookup_tree(r, oid)->object;
+       }
+
        buffer = repo_read_object_file(r, oid, &type, &size);
        if (buffer) {
                if (!skip_hash &&
@@ -312,6 +324,8 @@ struct object *parse_object_with_flags(struct repository *r,
                                          buffer, &eaten);
                if (!eaten)
                        free(buffer);
+               if (discard_tree && type == OBJ_TREE)
+                       free_tree_buffer((struct tree *)obj);
                return obj;
        }
        return NULL;
index 114d45954d082229ed747dee16f2510387145771..c7123cade622cd2a5b6aafc99ad924aafeaaa301 100644 (file)
--- a/object.h
+++ b/object.h
@@ -197,6 +197,7 @@ void *object_as_type(struct object *obj, enum object_type type, int quiet);
  */
 enum parse_object_flags {
        PARSE_OBJECT_SKIP_HASH_CHECK = 1 << 0,
+       PARSE_OBJECT_DISCARD_TREE = 1 << 1,
 };
 struct object *parse_object(struct repository *r, const struct object_id *oid);
 struct object *parse_object_with_flags(struct repository *r,
index 2424c9bd674e534909df89e25c21b5eb119fda05..b10f63a607a4925f245ed8818faa364c602a79cd 100644 (file)
@@ -381,7 +381,8 @@ static struct object *get_reference(struct rev_info *revs, const char *name,
 
        object = parse_object_with_flags(revs->repo, oid,
                                         revs->verify_objects ? 0 :
-                                        PARSE_OBJECT_SKIP_HASH_CHECK);
+                                        PARSE_OBJECT_SKIP_HASH_CHECK |
+                                        PARSE_OBJECT_DISCARD_TREE);
 
        if (!object) {
                if (revs->ignore_missing)
index b7211554429fc631eefd913fe0d02a0b0fb72988..761af4a53288010433afa3d468a255d6ce0c9363 100644 (file)
@@ -470,7 +470,8 @@ static int do_got_oid(struct upload_pack_data *data, const struct object_id *oid
 {
        int we_knew_they_have = 0;
        struct object *o = parse_object_with_flags(the_repository, oid,
-                                                  PARSE_OBJECT_SKIP_HASH_CHECK);
+                                                  PARSE_OBJECT_SKIP_HASH_CHECK |
+                                                  PARSE_OBJECT_DISCARD_TREE);
 
        if (!o)
                die("oops (%s)", oid_to_hex(oid));
@@ -1150,7 +1151,8 @@ static void receive_needs(struct upload_pack_data *data,
                }
 
                o = parse_object_with_flags(the_repository, &oid_buf,
-                                           PARSE_OBJECT_SKIP_HASH_CHECK);
+                                           PARSE_OBJECT_SKIP_HASH_CHECK |
+                                           PARSE_OBJECT_DISCARD_TREE);
                if (!o) {
                        packet_writer_error(&data->writer,
                                            "upload-pack: not our ref %s",
@@ -1467,7 +1469,8 @@ static int parse_want(struct packet_writer *writer, const char *line,
                            "expected to get oid, not '%s'", line);
 
                o = parse_object_with_flags(the_repository, &oid,
-                                           PARSE_OBJECT_SKIP_HASH_CHECK);
+                                           PARSE_OBJECT_SKIP_HASH_CHECK |
+                                           PARSE_OBJECT_DISCARD_TREE);
 
                if (!o) {
                        packet_writer_error(writer,