{"thread":{"id":"66712","subject":"[PATCH 0/6] loose object size checks and a few size truncations","startedAt":"2026-10-30T10:28:33Z","lastAt":"2026-10-30T10:28:39Z","messageCount":7,"participants":["Elia Pinto"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"556883","messageId":"cover.1793355390.git.gitter.spiros@gmail.com","threadId":"66712","inReplyTo":null,"subject":"[PATCH 0/6] loose object size checks and a few size truncations","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-30T10:28:33Z","receivedAt":"2026-10-30T10:28:33Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"These patches come from the same search for truncated sizes on LLP64\n(64-bit Windows, where \"long\" is 32 bits but size_t is 64 bits) as the\n\"use size_t for a few more object and buffer sizes\" series:\n\n  https://lore.kernel.org/git/cover.1793352943.git.gitter.spiros@gmail.com/\n\nIts cover letter explains the method (a MinGW cross build and\nclang-tidy with -Wshorten-64-to-32). For these I followed each value\nback to where it comes from and forward to where it is used, because\nthey can lead to out-of-bounds memory accesses. For that reason I sent\nall of them but 3/6 to the git-security list first; they do not\nconsider them security issues, so here they are. While checking the\nloose object reader I also found two missing size checks that have\nnothing to do with LLP64 (1/6 and 3/6).\n\n 1/6 A loose object whose zlib stream is shorter than the size in its\n     header is accepted, and the uninitialized tail of the buffer is\n     handed out as object content (all platforms; e.g. \"git cat-file\n     commit\" prints heap memory). A longer stream is silently cut.\n\n 2/6 On Windows the size from the loose object header is truncated, so\n     the caller gets a buffer much smaller than the size it is told,\n     and reads beyond its end.\n\n 3/6 The same check as 1/6 for the streaming reader of loose objects,\n     which today streams out a short or long object without an error.\n\n 4/6 \"git cat-file --batch\" writes memory past the end of objects\n     between 2GiB and 4GiB (all platforms; non-blob objects, or blobs\n     with --filters/--textconv).\n\n 5/6 On Windows, concatenating notes that add up to 4GiB or more\n     overflows a heap buffer.\n\n 6/6 On Windows, diffing a 2-4GiB file as text reads one byte before\n     its buffer.\n\n1/6 and 3/6 come with tests, which are cheap, and fail without the\nfix. The others would need objects of several GiB (and 2/6, 5/6 and\n6/6 a 32-bit \"long\"), so their commit messages describe how to\nreproduce them instead. I have not run anything on Windows: the\nWindows-only issues are based on reading the code and on the\ncross-compiled build.\n\n5/6 passes the combined size to odb_write_object() through\ncast_size_t_to_ulong(), as the length parameter is still an \"unsigned\nlong\" on master. With 1/4 of the series above, which makes it a\nsize_t, the cast becomes unnecessary but harmless; the two series do\nnot conflict.\n\nThe series is based on master (b9e5cff180).\n\nElia Pinto (6):\n  object-file: check size of inflated loose object against its header\n  object-file: use size_t for loose object sizes\n  odb/source-loose: check object size when streaming loose objects\n  cat-file: use size_t for the length passed to batch_write()\n  notes: avoid truncating the size of concatenated notes\n  diff: use size_t in count_trailing_blank()\n\n builtin/cat-file.c           |  2 +-\n diff.c                       |  2 +-\n notes.c                      |  7 +++----\n object-file.c                | 29 ++++++++++++++++++-----------\n object-file.h                |  6 +++---\n odb/source-loose.c           | 22 ++++++++++++++++++----\n t/lib-loose.sh               |  5 ++++-\n t/t1006-cat-file.sh          | 18 ++++++++++++++++++\n t/t1060-object-corruption.sh | 28 ++++++++++++++++++++++++++++\n 9 files changed, 94 insertions(+), 25 deletions(-)\n\n\nbase-commit: b9e5cff1803ab24582201ef059516eb16014b9d9\n-- \n2.53.0\n\n\n"},{"id":"556884","messageId":"6c2efb87fbb95d3d24ac8e4ef34b586c2b25980e.1793355390.git.gitter.spiros@gmail.com","threadId":"66712","inReplyTo":"cover.1793355390.git.gitter.spiros@gmail.com","subject":"[PATCH 1/6] object-file: check size of inflated loose object against its header","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-30T10:28:34Z","receivedAt":"2026-10-30T10:28:34Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"unpack_loose_rest() allocates a buffer of the size given in the object\nheader and inflates the rest of the stream into it, but it never checks\nthat the stream actually produced that many bytes. If the compressed\ndata ends early we return success with the tail of the buffer never\nwritten, and callers like odb_read_object() hand it out as object\ncontent. For example, \"git cat-file commit\" on a loose object whose\nheader says \"commit 200\" but which contains only 44 bytes prints 156\nbytes of uninitialized heap memory.\n\nThe opposite case slips through, too: when the whole object fits into\nthe buffer filled by unpack_loose_header(), we copy only \"size\" bytes\nout of it, the stream has already ended, and the extra data is silently\ndropped.\n\nThe packfile reader in unpack_compressed_entry() and index-pack already\ncompare total_out with the expected size. Do the same here and report\nthe object as corrupt if they differ. Today fsck notices the first case\nonly by accident, because it hashes the uninitialized bytes and gets a\nmismatch.\n\nTeach loose_obj() in lib-loose.sh to write a bogus size, so that the\ntests can create such objects.\n\nSigned-off-by: Elia Pinto <gitter.spiros@gmail.com>\n---\n object-file.c                | 15 +++++++++++----\n t/lib-loose.sh               |  5 ++++-\n t/t1060-object-corruption.sh | 28 ++++++++++++++++++++++++++++\n 3 files changed, 43 insertions(+), 5 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex c049bf13f7..6d39138511 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -212,14 +212,14 @@ void *unpack_loose_rest(git_zstream *stream,\n \t\t\tvoid *buffer, unsigned long size,\n \t\t\tconst struct object_id *oid)\n {\n-\tsize_t bytes = strlen(buffer) + 1, n;\n+\tsize_t hdrlen = strlen(buffer) + 1, bytes, n;\n \tunsigned char *buf = xmallocz(size);\n \tint status = Z_OK;\n \n-\tn = stream->total_out - bytes;\n+\tn = stream->total_out - hdrlen;\n \tif (n > size)\n \t\tn = size;\n-\tmemcpy(buf, (char *) buffer + bytes, n);\n+\tmemcpy(buf, (char *) buffer + hdrlen, n);\n \tbytes = n;\n \tif (bytes <= size) {\n \t\t/*\n@@ -244,7 +244,14 @@ void *unpack_loose_rest(git_zstream *stream,\n \t\t}\n \t}\n \n-\tif (status != Z_STREAM_END) {\n+\t/*\n+\t * Make sure that the stream produced exactly as many bytes as the\n+\t * header claims. Otherwise we would hand back a buffer whose tail\n+\t * was never written (if the stream is short), or silently drop data\n+\t * (if the stream is long but ended before we noticed).\n+\t */\n+\tif (status != Z_STREAM_END ||\n+\t    stream->total_out - hdrlen != size) {\n \t\terror(_(\"corrupt loose object '%s'\"), oid_to_hex(oid));\n \t\tFREE_AND_NULL(buf);\n \t} else if (stream->avail_in) {\ndiff --git a/t/lib-loose.sh b/t/lib-loose.sh\nindex 3613631eaf..807fca6f05 100644\n--- a/t/lib-loose.sh\n+++ b/t/lib-loose.sh\n@@ -5,9 +5,12 @@\n #\n #   oid=$(echo foo | loose_obj .git/objects blob)\n #\n+# If $3 is given, it is written as the size in the object header instead\n+# of the real size of the contents.\n+#\n loose_obj () {\n \tcat >tmp_loose.content &&\n-\tsize=$(wc -c <tmp_loose.content) &&\n+\tsize=${3:-$(wc -c <tmp_loose.content)} &&\n \t{\n \t\t# Do not quote $size here; we want the shell\n \t\t# to strip whitespace that \"wc\" adds on some platforms.\ndiff --git a/t/t1060-object-corruption.sh b/t/t1060-object-corruption.sh\nindex d2ef468b45..bd3d1adf5d 100755\n--- a/t/t1060-object-corruption.sh\n+++ b/t/t1060-object-corruption.sh\n@@ -3,6 +3,7 @@\n test_description='see how we handle various forms of corruption'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-loose.sh\n \n # convert \"1234abcd\" to \".git/objects/12/34abcd\"\n obj_to_file() {\n@@ -163,4 +164,31 @@ test_expect_success 'corrupted loose commit can be read from alternate' '\n \ttest_grep \"inflate: data stream error\" err\n '\n \n+test_expect_success 'setup loose objects with bogus header size' '\n+\tgit init bogus-size &&\n+\t(\n+\t\tcd bogus-size &&\n+\t\tprintf \"short\" |\n+\t\tloose_obj .git/objects commit 200 >../oid-too-big &&\n+\t\tprintf \"much longer\" |\n+\t\tloose_obj .git/objects commit 3 >../oid-too-small\n+\t)\n+'\n+\n+test_expect_success 'reading loose object shorter than its header fails' '\n+\ttest_must_fail git -C bogus-size cat-file commit $(cat oid-too-big) 2>err &&\n+\ttest_grep \"corrupt loose object\" err\n+'\n+\n+test_expect_success 'reading loose object longer than its header fails' '\n+\ttest_must_fail git -C bogus-size cat-file commit $(cat oid-too-small) 2>err &&\n+\ttest_grep \"corrupt loose object\" err\n+'\n+\n+test_expect_success 'fsck reports loose objects with bogus header size' '\n+\ttest_must_fail git -C bogus-size fsck 2>err &&\n+\ttest_grep \"corrupt loose object .$(cat oid-too-big)\" err &&\n+\ttest_grep \"corrupt loose object .$(cat oid-too-small)\" err\n+'\n+\n test_done\n-- \n2.53.0\n\n\n"},{"id":"556885","messageId":"ee4439354afda7637f935293d5accff91cc0ad71.1793355390.git.gitter.spiros@gmail.com","threadId":"66712","inReplyTo":"cover.1793355390.git.gitter.spiros@gmail.com","subject":"[PATCH 2/6] object-file: use size_t for loose object sizes","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-30T10:28:35Z","receivedAt":"2026-10-30T10:28:35Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"Since c6a4629e32 (odb: use size_t for object_info.sizep and the size\nAPIs, 2026-06-15), parse_loose_header() reports the object size as a\nsize_t. But unpack_loose_rest() still takes an \"unsigned long\", which\nis only 32 bits on Windows. A header claiming 4GiB + 5 bytes thus\nmakes us allocate a 6-byte buffer, the size check added by the previous\ncommit is happy because the stream really has 5 bytes, and the caller\ngets that buffer back together with *oi->sizep set to 4GiB + 5. Any\nuser of the object then reads far beyond the end of the buffer;\nread_loose_object() already does so when it hashes the contents.\n\nUse size_t for the size in unpack_loose_rest() and check_stream_oid(),\nand while at it for the size of the mapped file in map_fd(),\nodb_source_loose_map_object() and unpack_loose_header(), so that the\nwhole loose object reading path agrees on the type.\n\nThere is no test, as the reader allocates a buffer of the size claimed\nin the header before it can notice that the stream is short, so a test\nwould need 4GiB of memory. The problem can be reproduced on Windows\nwith the loose_obj() helper from the previous commit:\n\n\tprintf short |\n\tloose_obj .git/objects commit $((4 * 1024 * 1024 * 1024 + 5))\n\nfollowed by \"git cat-file commit\" on the resulting object ID.\n\nSigned-off-by: Elia Pinto <gitter.spiros@gmail.com>\n---\n object-file.c      | 14 +++++++-------\n object-file.h      |  6 +++---\n odb/source-loose.c |  8 ++++----\n 3 files changed, 14 insertions(+), 14 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 6d39138511..0fc240b1e8 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -155,7 +155,7 @@ int stream_object_signature(struct repository *r,\n  * Map and close the given loose object fd. The path argument is used for\n  * error reporting.\n  */\n-static void *map_fd(int fd, const char *path, unsigned long *size)\n+static void *map_fd(int fd, const char *path, size_t *size)\n {\n \tvoid *map = NULL;\n \tstruct stat st;\n@@ -176,9 +176,9 @@ static void *map_fd(int fd, const char *path, unsigned long *size)\n \n enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n \t\t\t\t\t\t    unsigned char *map,\n-\t\t\t\t\t\t    unsigned long mapsize,\n+\t\t\t\t\t\t    size_t mapsize,\n \t\t\t\t\t\t    void *buffer,\n-\t\t\t\t\t\t    unsigned long bufsiz)\n+\t\t\t\t\t\t    size_t bufsiz)\n {\n \tint status;\n \n@@ -209,7 +209,7 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n }\n \n void *unpack_loose_rest(git_zstream *stream,\n-\t\t\tvoid *buffer, unsigned long size,\n+\t\t\tvoid *buffer, size_t size,\n \t\t\tconst struct object_id *oid)\n {\n \tsize_t hdrlen = strlen(buffer) + 1, bytes, n;\n@@ -1135,7 +1135,7 @@ int for_each_loose_file_in_source(struct odb_source *source,\n \n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n-\t\t\t    unsigned long size,\n+\t\t\t    size_t size,\n \t\t\t    const char *path,\n \t\t\t    const struct object_id *expected_oid,\n \t\t\t    const struct git_hash_algo *algop)\n@@ -1143,7 +1143,7 @@ static int check_stream_oid(git_zstream *stream,\n \tstruct git_hash_ctx c;\n \tstruct object_id real_oid;\n \tunsigned char buf[4096];\n-\tunsigned long total_read;\n+\tsize_t total_read;\n \tint status = Z_OK;\n \n \tgit_hash_init(&c, algop);\n@@ -1203,7 +1203,7 @@ int read_loose_object(struct repository *repo,\n \tint ret = -1;\n \tint fd;\n \tvoid *map = NULL;\n-\tunsigned long mapsize;\n+\tsize_t mapsize;\n \tgit_zstream stream;\n \tchar hdr[MAX_HEADER_LEN];\n \tsize_t *size = oi->sizep;\ndiff --git a/object-file.h b/object-file.h\nindex f44758c4f8..a8da9dcd22 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -157,11 +157,11 @@ enum unpack_loose_header_result {\n  */\n enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n \t\t\t\t\t\t    unsigned char *map,\n-\t\t\t\t\t\t    unsigned long mapsize,\n+\t\t\t\t\t\t    size_t mapsize,\n \t\t\t\t\t\t    void *buffer,\n-\t\t\t\t\t\t    unsigned long bufsiz);\n+\t\t\t\t\t\t    size_t bufsiz);\n void *unpack_loose_rest(git_zstream *stream,\n-\t\t\tvoid *buffer, unsigned long size,\n+\t\t\tvoid *buffer, size_t size,\n \t\t\tconst struct object_id *oid);\n \n int parse_loose_header(const char *hdr, struct object_info *oi);\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 50cb388c35..d77e93e7c4 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -73,7 +73,7 @@ static int read_object_info_from_path(struct odb_source_loose *loose,\n {\n \tint ret;\n \tint fd;\n-\tunsigned long mapsize;\n+\tsize_t mapsize;\n \tvoid *map = NULL;\n \tgit_zstream stream, *stream_to_end = NULL;\n \tchar hdr[MAX_HEADER_LEN];\n@@ -250,7 +250,7 @@ static int open_loose_object(struct odb_source_loose *loose,\n \n static void *odb_source_loose_map_object(struct odb_source_loose *loose,\n \t\t\t\t\t const struct object_id *oid,\n-\t\t\t\t\t unsigned long *size)\n+\t\t\t\t\t size_t *size)\n {\n \tconst char *p;\n \tint fd = open_loose_object(loose, oid, &p);\n@@ -285,7 +285,7 @@ struct odb_loose_read_stream {\n \t\tODB_LOOSE_READ_STREAM_ERROR,\n \t} z_state;\n \tvoid *mapped;\n-\tunsigned long mapsize;\n+\tsize_t mapsize;\n \tchar hdr[32];\n \tint hdr_avail;\n \tint hdr_used;\n@@ -356,7 +356,7 @@ static int odb_source_loose_read_object_stream(struct odb_stream **out,\n \tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n \tstruct object_info oi = OBJECT_INFO_INIT;\n \tstruct odb_loose_read_stream *st;\n-\tunsigned long mapsize;\n+\tsize_t mapsize;\n \tvoid *mapped;\n \n \tmapped = odb_source_loose_map_object(loose, oid, &mapsize);\n-- \n2.53.0\n\n\n"},{"id":"556886","messageId":"dee492c3d8dd58196f0524f7b682ab08ad2b066f.1793355390.git.gitter.spiros@gmail.com","threadId":"66712","inReplyTo":"cover.1793355390.git.gitter.spiros@gmail.com","subject":"[PATCH 3/6] odb/source-loose: check object size when streaming loose objects","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-30T10:28:36Z","receivedAt":"2026-10-30T10:28:36Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"read_istream_loose() inflates a loose object until zlib reports the end\nof the stream, but never compares the number of bytes it produced with\nthe size given in the object header. A corrupt object whose stream is\nshorter than its header claims is thus streamed out without any error,\ne.g. \"git cat-file blob\" exits with 0 after writing fewer bytes than\n\"git cat-file -s\" reports. The same happens if the stream is longer than\nthe header claims.\n\nKeep track of how many bytes we handed out and report the object as\ncorrupt if that does not match the size from the header once the stream\nends.\n\nThe tests create such objects with the loose_obj() helper, which can\nalready write a bogus size into the header.\n\nSigned-off-by: Elia Pinto <gitter.spiros@gmail.com>\n---\n odb/source-loose.c  | 14 ++++++++++++++\n t/t1006-cat-file.sh | 18 ++++++++++++++++++\n 2 files changed, 32 insertions(+)\n\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex d77e93e7c4..2233714ac4 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -289,6 +289,8 @@ struct odb_loose_read_stream {\n \tchar hdr[32];\n \tint hdr_avail;\n \tint hdr_used;\n+\tsize_t total_read;\n+\tstruct object_id oid;\n };\n \n static ssize_t read_istream_loose(struct odb_stream *_st, char *buf, size_t sz)\n@@ -326,6 +328,16 @@ static ssize_t read_istream_loose(struct odb_stream *_st, char *buf, size_t sz)\n \n \t\tif (status == Z_STREAM_END) {\n \t\t\tgit_inflate_end(&st->z);\n+\t\t\t/*\n+\t\t\t * The object must be exactly as large as its\n+\t\t\t * header claims.\n+\t\t\t */\n+\t\t\tif (st->total_read + total_read != st->base.size) {\n+\t\t\t\terror(_(\"corrupt loose object '%s'\"),\n+\t\t\t\t      oid_to_hex(&st->oid));\n+\t\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_ERROR;\n+\t\t\t\treturn -1;\n+\t\t\t}\n \t\t\tst->z_state = ODB_LOOSE_READ_STREAM_DONE;\n \t\t\tbreak;\n \t\t}\n@@ -335,6 +347,7 @@ static ssize_t read_istream_loose(struct odb_stream *_st, char *buf, size_t sz)\n \t\t\treturn -1;\n \t\t}\n \t}\n+\tst->total_read += total_read;\n \treturn total_read;\n }\n \n@@ -388,6 +401,7 @@ static int odb_source_loose_read_object_stream(struct odb_stream **out,\n \n \tst->mapped = mapped;\n \tst->mapsize = mapsize;\n+\toidcpy(&st->oid, oid);\n \tst->hdr_used = strlen(st->hdr) + 1;\n \tst->hdr_avail = st->z.total_out;\n \tst->z_state = ODB_LOOSE_READ_STREAM_INUSE;\ndiff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh\nindex 72fc8acc61..31a76c1b5d 100755\n--- a/t/t1006-cat-file.sh\n+++ b/t/t1006-cat-file.sh\n@@ -1481,4 +1481,22 @@ test_objects_filter \"object:type=commit\"\n test_objects_filter \"object:type=tag\"\n test_objects_filter \"object:type=tree\"\n \n+test_expect_success 'setup blobs with bogus size in header' '\n+\tgit init bogus-size &&\n+\techo_without_newline short |\n+\tloose_obj bogus-size/.git/objects blob 100 >oid-too-big &&\n+\techo_without_newline \"much longer\" |\n+\tloose_obj bogus-size/.git/objects blob 3 >oid-too-small\n+'\n+\n+test_expect_success 'streaming blob shorter than its header fails' '\n+\ttest_must_fail git -C bogus-size cat-file blob $(cat oid-too-big) >out 2>err &&\n+\ttest_grep \"corrupt loose object\" err\n+'\n+\n+test_expect_success 'streaming blob longer than its header fails' '\n+\ttest_must_fail git -C bogus-size cat-file blob $(cat oid-too-small) >out 2>err &&\n+\ttest_grep \"corrupt loose object\" err\n+'\n+\n test_done\n-- \n2.53.0\n\n\n"},{"id":"556887","messageId":"b2083632a1373b53380ead18ec32ca302143feec.1793355390.git.gitter.spiros@gmail.com","threadId":"66712","inReplyTo":"cover.1793355390.git.gitter.spiros@gmail.com","subject":"[PATCH 4/6] cat-file: use size_t for the length passed to batch_write()","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-30T10:28:37Z","receivedAt":"2026-10-30T10:28:37Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"batch_write() takes the length of the data as an \"int\", but its callers\npass the size of whole objects as returned by odb_read_object() or\nfilter_object(), which is a size_t. For an object between 2GiB and\n4GiB the length wraps to a negative value, which fwrite() and\nwrite_or_die() then see as an enormous size_t. We happily write the\nobject and then keep going past the end of the buffer, sending whatever\nmemory follows to stdout, until we hit an unmapped page and die with\n\"write error: Bad address\". Objects of 4GiB and more are truncated\ninstead, which desynchronizes the --batch output.\n\nThis affects all platforms, not only those with a 32-bit \"long\". It can\nbe triggered with \"--batch\" on any non-blob object, or on blobs with\n--filters or --textconv, as blobs are otherwise streamed. For example:\n\n\tblob=$(test-tool genzeros $((3 * 1024 * 1024 * 1024)) |\n\t       git hash-object -w --stdin) &&\n\techo \"$blob big\" |\n\tgit cat-file --batch=\"%(objectname) %(rest)\" --filters >out\n\nfails with \"write error: Bad address\"; with stdout going to /dev/null,\nstrace shows write() being called on addresses far beyond the end of the\n3GiB buffer. There is no test, as it would need several GiB of memory\nand disk.\n\nBoth fwrite() and write_or_die() take a size_t already, so just change\nthe parameter type.\n\nSigned-off-by: Elia Pinto <gitter.spiros@gmail.com>\n---\n builtin/cat-file.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/cat-file.c b/builtin/cat-file.c\nindex 8870a210ec..c846fe3116 100644\n--- a/builtin/cat-file.c\n+++ b/builtin/cat-file.c\n@@ -425,7 +425,7 @@ static void expand_format(struct strbuf *sb, const char *start,\n \t}\n }\n \n-static void batch_write(struct batch_options *opt, const void *data, int len)\n+static void batch_write(struct batch_options *opt, const void *data, size_t len)\n {\n \tif (opt->buffer_output) {\n \t\tif (fwrite(data, 1, len, stdout) != len)\n-- \n2.53.0\n\n\n"},{"id":"556888","messageId":"21f5a4e009155e36865f6b01d59a00b644669e41.1793355390.git.gitter.spiros@gmail.com","threadId":"66712","inReplyTo":"cover.1793355390.git.gitter.spiros@gmail.com","subject":"[PATCH 5/6] notes: avoid truncating the size of concatenated notes","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-30T10:28:38Z","receivedAt":"2026-10-30T10:28:38Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"combine_notes_concatenate() reads both notes with odb_read_object(),\nwhich returns their sizes as size_t, but computes the size of the\ncombined buffer in an \"unsigned long\". On platforms where that type is\n32 bits wide, like Windows, two notes adding up to 4GiB or more give a\nsmall buf_len: we allocate that many bytes and then memcpy() the full\ncontents of both notes into the buffer, writing far past its end.\n\nThe notes can come from a fetched notes ref. Besides notes merges with\nthe \"union\" strategy and notes.rewriteMode=concatenate (the default),\nthe function is also used when loading a notes tree that contains more\nthan one note for the same object, e.g. at different fanout levels, so\nmerely displaying such notes is enough to trigger it.\n\nCompute the size as a size_t with st_add3(), and go through\ncast_size_t_to_ulong() when passing it to odb_write_object(), whose\nlength parameter is still an \"unsigned long\". That makes us die on such\nplatforms instead of writing a truncated blob, until the write API\nlearns about size_t, too.\n\nNo test, as triggering this needs a platform with a 32-bit \"long\" and\nnotes larger than 4GiB.\n\nSigned-off-by: Elia Pinto <gitter.spiros@gmail.com>\n---\n notes.c | 7 +++----\n 1 file changed, 3 insertions(+), 4 deletions(-)\n\ndiff --git a/notes.c b/notes.c\nindex ec9c2cb150..05da28b8d4 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -811,8 +811,7 @@ int combine_notes_concatenate(struct object_id *cur_oid,\n \t\t\t      const struct object_id *new_oid)\n {\n \tchar *cur_msg = NULL, *new_msg = NULL, *buf;\n-\tunsigned long buf_len;\n-\tsize_t cur_len, new_len;\n+\tsize_t cur_len, new_len, buf_len;\n \tenum object_type cur_type, new_type;\n \tint ret;\n \n@@ -839,7 +838,7 @@ int combine_notes_concatenate(struct object_id *cur_oid,\n \t\tcur_len--;\n \n \t/* concatenate cur_msg and new_msg into buf */\n-\tbuf_len = cur_len + 2 + new_len;\n+\tbuf_len = st_add3(cur_len, 2, new_len);\n \tbuf = (char *) xmalloc(buf_len);\n \tmemcpy(buf, cur_msg, cur_len);\n \tbuf[cur_len] = '\\n';\n@@ -850,7 +849,7 @@ int combine_notes_concatenate(struct object_id *cur_oid,\n \n \t/* create a new blob object from buf */\n \tret = odb_write_object(the_repository->objects, buf,\n-\t\t\t       buf_len, OBJ_BLOB, cur_oid);\n+\t\t\t       cast_size_t_to_ulong(buf_len), OBJ_BLOB, cur_oid);\n \tfree(buf);\n \treturn ret;\n }\n-- \n2.53.0\n\n\n"},{"id":"556889","messageId":"79f1781abdd196ac35ae888ea5470705c56eaa4a.1793355390.git.gitter.spiros@gmail.com","threadId":"66712","inReplyTo":"cover.1793355390.git.gitter.spiros@gmail.com","subject":"[PATCH 6/6] diff: use size_t in count_trailing_blank()","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-30T10:28:39Z","receivedAt":"2026-10-30T10:28:39Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"Since 3c7a31204f (xdiff: use size_t for buffer sizes, 2026-09-30),\nmmfile_t stores its size as a size_t, but count_trailing_blank() still\ncopies it into a \"long\". On platforms where \"long\" is 32 bits wide,\nlike Windows, a buffer between 2GiB and 4GiB gives a negative size, and\n\"ptr += size - 1\" points about 2GiB before the start of the buffer,\nwhich we then dereference.\n\ncheck_blank_at_eof() calls this function whenever the whitespace rule\nincludes blank-at-eof, which is the default, and it does so before\nxdi_diff() rejects inputs larger than MAX_XDIFF_SIZE. Large files are\nnormally treated as binary and never get here, but a \"diff\" attribute\nor a textconv driver makes us diff them as text. A repository can ship\nsuch an attribute in its .gitattributes, so \"git show\" or \"git log -p\"\non a commit adding a 3GiB file can be made to crash.\n\nUse size_t; the rest of the function works unchanged with it.\n\nNo test, as triggering this needs a platform with a 32-bit \"long\" and\na blob larger than 2GiB.\n\nSigned-off-by: Elia Pinto <gitter.spiros@gmail.com>\n---\n diff.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/diff.c b/diff.c\nindex b4ac17f8ef..e42e53d43e 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -711,7 +711,7 @@ static unsigned long diff_filespec_size(struct repository *r,\n static int count_trailing_blank(mmfile_t *mf)\n {\n \tchar *ptr = mf->ptr;\n-\tlong size = mf->size;\n+\tsize_t size = mf->size;\n \tint cnt = 0;\n \n \tif (!size)\n-- \n2.53.0\n\n\n"}]}