* [PATCH 1/3] odb: use size_t for the length of objects to write
2026-10-21 10:16 [PATCH 0/3] use size_t for a few more object and buffer sizes Elia Pinto
@ 2026-10-21 10:16 ` Elia Pinto
2026-10-21 11:24 ` Patrick Steinhardt
2026-10-21 10:16 ` [PATCH 2/3] diff: use size_t for the allocation size of word-diff buffers Elia Pinto
2026-10-21 10:16 ` [PATCH 3/3] convert: use size_t for buffer sizes Elia Pinto
2 siblings, 1 reply; 7+ messages in thread
From: Elia Pinto @ 2026-10-21 10:16 UTC (permalink / raw)
To: git
Cc: Johannes Schindelin, Jeff King, Elia Pinto, Junio C Hamano,
Patrick Steinhardt
c6a4629e32 (odb: use size_t for object_info.sizep and the size APIs,
2026-06-15) widened the read side of the object database API, and the
write_object() callback of odb_source already takes a size_t. But
odb_write_object_ext(), odb_write_object() and odb_source_write_object()
in front of it, as well as write_loose_object() behind it, still take
an "unsigned long", which is only 32 bits wide on Windows.
Most callers hand them a size_t from a strbuf or from odb_read_object(),
which silently gets truncated. The most visible case is index_mem() and
index_stream_convert_blob(): when "git add" or "git hash-object -w"
runs a file over 4GiB through a conversion (core.autocrlf, which is on
by default in Git for Windows, an eol or ident attribute, or a clean
filter), we write a blob made of the first "size mod 2^32" bytes. The
index records it with the stat data of the real file, so "git status"
stays clean and nobody notices until the file is checked out again.
"git hash-object" without "-w" uses hash_object_file(), which takes a
size_t, so the two disagree on the object ID of the same file.
Use size_t throughout, including for the size kept by the in-memory
source.
The existing "hash filtered files over 4GB correctly" test in t1007
only hashes. Running the same with "git hash-object -w" on Windows
shows the problem, but needs another pass over a 5GiB file, so no new
test is added here.
Signed-off-by: Elia Pinto <gitter.spiros@gmail.com>
---
odb.c | 2 +-
odb.h | 4 ++--
odb/source-inmemory.c | 2 +-
odb/source-loose.c | 2 +-
odb/source.h | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/odb.c b/odb.c
index 98c5c862f5..6e9daf11e4 100644
--- a/odb.c
+++ b/odb.c
@@ -667,7 +667,7 @@ void odb_assert_oid_type(struct object_database *odb,
}
int odb_write_object_ext(struct object_database *odb,
- const void *buf, unsigned long len,
+ const void *buf, size_t len,
enum object_type type,
struct object_id *oid,
const struct object_id *compat_oid_in,
diff --git a/odb.h b/odb.h
index 0ccc47ee73..25d02fae75 100644
--- a/odb.h
+++ b/odb.h
@@ -633,14 +633,14 @@ enum odb_write_object_flags {
* Returns 0 on success, a negative error code otherwise.
*/
int odb_write_object_ext(struct object_database *odb,
- const void *buf, unsigned long len,
+ const void *buf, size_t len,
enum object_type type,
struct object_id *oid,
const struct object_id *compat_oid,
enum odb_write_object_flags flags);
static inline int odb_write_object(struct object_database *odb,
- const void *buf, unsigned long len,
+ const void *buf, size_t len,
enum object_type type,
struct object_id *oid)
{
diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c
index 868e723c81..ab7173cd2d 100644
--- a/odb/source-inmemory.c
+++ b/odb/source-inmemory.c
@@ -10,7 +10,7 @@
struct inmemory_object {
enum object_type type;
const void *buf;
- unsigned long size;
+ size_t size;
};
static const struct inmemory_object *find_cached_object(struct odb_source_inmemory *source,
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 0664634938..fe1a298364 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -770,7 +770,7 @@ static int end_loose_object_common(struct odb_source_loose *loose,
static int write_loose_object(struct odb_source_loose *loose,
const struct object_id *oid, char *hdr,
- int hdrlen, const void *buf, unsigned long len,
+ int hdrlen, const void *buf, size_t len,
const time_t *mtime, unsigned flags)
{
int fd, ret;
diff --git a/odb/source.h b/odb/source.h
index 1867347dfc..8e6ed96399 100644
--- a/odb/source.h
+++ b/odb/source.h
@@ -469,7 +469,7 @@ static inline int odb_source_freshen_object(struct odb_source *source,
* object ID and the compatibility object ID, if non-NULL.
*/
static inline int odb_source_write_object(struct odb_source *source,
- const void *buf, unsigned long len,
+ const void *buf, size_t len,
enum object_type type,
const struct object_id *oid,
const struct object_id *compat_oid,
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH 1/3] odb: use size_t for the length of objects to write
2026-10-21 10:16 ` [PATCH 1/3] odb: use size_t for the length of objects to write Elia Pinto
@ 2026-10-21 11:24 ` Patrick Steinhardt
0 siblings, 0 replies; 7+ messages in thread
From: Patrick Steinhardt @ 2026-10-21 11:24 UTC (permalink / raw)
To: Elia Pinto; +Cc: git, Johannes Schindelin, Jeff King, Junio C Hamano
On Wed, Oct 21, 2026 at 12:16:37PM +0200, Elia Pinto wrote:
> c6a4629e32 (odb: use size_t for object_info.sizep and the size APIs,
> 2026-06-15) widened the read side of the object database API, and the
> write_object() callback of odb_source already takes a size_t. But
> odb_write_object_ext(), odb_write_object() and odb_source_write_object()
> in front of it, as well as write_loose_object() behind it, still take
> an "unsigned long", which is only 32 bits wide on Windows.
Sometimes I wish that C would give us an easier way to detect such
mismatches. It of course does have optional warnings, but enabling those
now would probably lead to thousands of warnings.
> Most callers hand them a size_t from a strbuf or from odb_read_object(),
> which silently gets truncated. The most visible case is index_mem() and
> index_stream_convert_blob(): when "git add" or "git hash-object -w"
> runs a file over 4GiB through a conversion (core.autocrlf, which is on
> by default in Git for Windows, an eol or ident attribute, or a clean
> filter), we write a blob made of the first "size mod 2^32" bytes. The
> index records it with the stat data of the real file, so "git status"
> stays clean and nobody notices until the file is checked out again.
> "git hash-object" without "-w" uses hash_object_file(), which takes a
> size_t, so the two disagree on the object ID of the same file.
Awesome.
> Use size_t throughout, including for the size kept by the in-memory
> source.
>
> The existing "hash filtered files over 4GB correctly" test in t1007
> only hashes. Running the same with "git hash-object -w" on Windows
> shows the problem, but needs another pass over a 5GiB file, so no new
> test is added here.
But wouldn't it make sense to add a test? It's a rather serious bug, and
the existing tests are marked with EXPENSIVE already anyway. So I don't
think this is a good enough argument to not add another EXPENSIVE test.
> diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c
> index 868e723c81..ab7173cd2d 100644
> --- a/odb/source-inmemory.c
> +++ b/odb/source-inmemory.c
> @@ -10,7 +10,7 @@
> struct inmemory_object {
> enum object_type type;
> const void *buf;
> - unsigned long size;
> + size_t size;
> };
I really hope nobody writes 4GB blobs into memory :) But despite that,
this fix is correct anyway.
Patrick
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 2/3] diff: use size_t for the allocation size of word-diff buffers
2026-10-21 10:16 [PATCH 0/3] use size_t for a few more object and buffer sizes Elia Pinto
2026-10-21 10:16 ` [PATCH 1/3] odb: use size_t for the length of objects to write Elia Pinto
@ 2026-10-21 10:16 ` Elia Pinto
2026-10-21 11:24 ` Patrick Steinhardt
2026-10-21 10:16 ` [PATCH 3/3] convert: use size_t for buffer sizes Elia Pinto
2 siblings, 1 reply; 7+ messages in thread
From: Elia Pinto @ 2026-10-21 10:16 UTC (permalink / raw)
To: git; +Cc: Johannes Schindelin, Jeff King, Elia Pinto, Junio C Hamano
Since 3c7a31204f (xdiff: use size_t for buffer sizes, 2026-09-30) the
size of an mmfile_t is a size_t, but the word-diff code still grows
these buffers with ALLOC_GROW() using an "unsigned long" (in struct
diff_words_buffer) or a "long" (in diff_words_fill()) for the allocated
size.
On platforms where "long" is 32 bits wide this is not just cosmetic.
diff_words_fill() writes every word followed by a newline, so its
output can be up to twice as large as its input, which is limited by
MAX_XDIFF_SIZE to just under 1GiB. alloc_nr() computes (alloc + 16) * 3
in the type of "alloc", which overflows a signed 32-bit "long" as soon
as the buffer grows past about 700MiB. The result is undefined behavior,
in practice a negative size that REALLOC_ARRAY() turns into a huge
allocation request and a die().
The buffer in diff_words_append() is bounded by MAX_XDIFF_SIZE, so
there it is only a matter of consistency, but if that limit ever went
away, ALLOC_GROW() would truncate the requested size, and the memcpy()
right after it would overflow the buffer.
Use size_t for both.
Signed-off-by: Elia Pinto <gitter.spiros@gmail.com>
---
diff.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/diff.c b/diff.c
index b4ac17f8ef..9483931c05 100644
--- a/diff.c
+++ b/diff.c
@@ -1959,7 +1959,7 @@ static void emit_rewrite_diff(const char *name_a,
struct diff_words_buffer {
mmfile_t text;
- unsigned long alloc;
+ size_t alloc;
struct diff_words_orig {
const char *begin, *end;
} *orig;
@@ -2192,7 +2192,7 @@ static void diff_words_fill(struct diff_words_buffer *buffer, mmfile_t *out,
regex_t *word_regex)
{
int i, j;
- long alloc = 0;
+ size_t alloc = 0;
out->size = 0;
out->ptr = NULL;
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH 2/3] diff: use size_t for the allocation size of word-diff buffers
2026-10-21 10:16 ` [PATCH 2/3] diff: use size_t for the allocation size of word-diff buffers Elia Pinto
@ 2026-10-21 11:24 ` Patrick Steinhardt
0 siblings, 0 replies; 7+ messages in thread
From: Patrick Steinhardt @ 2026-10-21 11:24 UTC (permalink / raw)
To: Elia Pinto; +Cc: git, Johannes Schindelin, Jeff King, Junio C Hamano
On Wed, Oct 21, 2026 at 12:16:38PM +0200, Elia Pinto wrote:
> diff --git a/diff.c b/diff.c
> index b4ac17f8ef..9483931c05 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -1959,7 +1959,7 @@ static void emit_rewrite_diff(const char *name_a,
>
> struct diff_words_buffer {
> mmfile_t text;
> - unsigned long alloc;
> + size_t alloc;
> struct diff_words_orig {
> const char *begin, *end;
> } *orig;
> @@ -2192,7 +2192,7 @@ static void diff_words_fill(struct diff_words_buffer *buffer, mmfile_t *out,
> regex_t *word_regex)
> {
> int i, j;
> - long alloc = 0;
> + size_t alloc = 0;
This is especially offending. I double checked that we really don't make
use of a potential negative value, but all this is ever used for is
`ALLOC_GROW()`.
Patrick
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 3/3] convert: use size_t for buffer sizes
2026-10-21 10:16 [PATCH 0/3] use size_t for a few more object and buffer sizes Elia Pinto
2026-10-21 10:16 ` [PATCH 1/3] odb: use size_t for the length of objects to write Elia Pinto
2026-10-21 10:16 ` [PATCH 2/3] diff: use size_t for the allocation size of word-diff buffers Elia Pinto
@ 2026-10-21 10:16 ` Elia Pinto
2026-10-21 11:24 ` Patrick Steinhardt
2 siblings, 1 reply; 7+ messages in thread
From: Elia Pinto @ 2026-10-21 10:16 UTC (permalink / raw)
To: git
Cc: Johannes Schindelin, Jeff King, Elia Pinto, Junio C Hamano,
Shuqi Liang, Karthik Nayak, Toon Claes
The conversion code receives its buffers with a size_t length, but the
helpers that scan them, gather_stats(), gather_convert_stats(),
gather_convert_stats_ascii() and count_ident(), take an "unsigned long".
On platforms where that is 32 bits wide, like Windows, they only look
at the first "size mod 2^32" bytes of a file over 4GiB. The decision
whether a file is binary, or contains CRLF line endings or "$Id$"
keywords, is then based on that prefix, and we may convert the line
endings of a file that is binary further down, or not expand keywords
that appear after the first 4GiB.
read_blob_data_from_index(), which feeds has_crlf_in_index(),
get_cached_convert_stats_ascii() and the reading of .gitattributes from
the index, gets the size of the blob as a size_t, but hands it out
through cast_size_t_to_ulong(), so on such platforms "git add" of a file
whose staged version is over 4GiB dies when core.autocrlf is in effect.
Use size_t for all of them.
Signed-off-by: Elia Pinto <gitter.spiros@gmail.com>
---
attr.c | 2 +-
convert.c | 14 +++++++-------
read-cache-ll.h | 2 +-
read-cache.c | 4 ++--
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/attr.c b/attr.c
index 0e63f1b6de..1acb4ef430 100644
--- a/attr.c
+++ b/attr.c
@@ -793,7 +793,7 @@ static struct attr_stack *read_attr_from_index(struct index_state *istate,
{
struct attr_stack *stack = NULL;
char *buf;
- unsigned long size;
+ size_t size;
int sparse_dir_pos = -1;
if (!istate)
diff --git a/convert.c b/convert.c
index 036506842c..ca5594cab0 100644
--- a/convert.c
+++ b/convert.c
@@ -42,9 +42,9 @@ struct text_stat {
unsigned printable, nonprintable;
};
-static void gather_stats(const char *buf, unsigned long size, struct text_stat *stats)
+static void gather_stats(const char *buf, size_t size, struct text_stat *stats)
{
- unsigned long i;
+ size_t i;
memset(stats, 0, sizeof(*stats));
@@ -102,7 +102,7 @@ static int convert_is_binary(const struct text_stat *stats)
return 0;
}
-static unsigned int gather_convert_stats(const char *data, unsigned long size)
+static unsigned int gather_convert_stats(const char *data, size_t size)
{
struct text_stat stats;
int ret = 0;
@@ -119,7 +119,7 @@ static unsigned int gather_convert_stats(const char *data, unsigned long size)
return ret;
}
-static const char *gather_convert_stats_ascii(const char *data, unsigned long size)
+static const char *gather_convert_stats_ascii(const char *data, size_t size)
{
unsigned int convert_stats = gather_convert_stats(data, size);
@@ -141,7 +141,7 @@ const char *get_cached_convert_stats_ascii(struct index_state *istate,
const char *path)
{
const char *ret;
- unsigned long sz;
+ size_t sz;
void *data = read_blob_data_from_index(istate, path, &sz);
ret = gather_convert_stats_ascii(data, sz);
free(data);
@@ -223,7 +223,7 @@ static void check_global_conv_flags_eol(const char *path,
static int has_crlf_in_index(struct index_state *istate, const char *path)
{
- unsigned long sz;
+ size_t sz;
void *data;
const char *crp;
int has_crlf = 0;
@@ -1077,7 +1077,7 @@ static int read_convert_config(const char *var, const char *value,
return 0;
}
-static int count_ident(const char *cp, unsigned long size)
+static int count_ident(const char *cp, size_t size)
{
/*
* "$Id: 0000000000000000000000000000000000000000 $" <=> "$Id$"
diff --git a/read-cache-ll.h b/read-cache-ll.h
index 066dd8bc3b..d279059b5f 100644
--- a/read-cache-ll.h
+++ b/read-cache-ll.h
@@ -416,7 +416,7 @@ int chmod_index_entry(struct index_state *, struct cache_entry *ce, char flip);
int ce_same_name(const struct cache_entry *a, const struct cache_entry *b);
void set_object_name_for_intent_to_add_entry(struct cache_entry *ce);
int index_name_is_other(struct index_state *, const char *, int);
-void *read_blob_data_from_index(struct index_state *, const char *, unsigned long *);
+void *read_blob_data_from_index(struct index_state *, const char *, size_t *);
/* do stat comparison even if CE_VALID is true */
#define CE_MATCH_IGNORE_VALID 01
diff --git a/read-cache.c b/read-cache.c
index c4cf08a3a3..635dad1ba3 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -3472,7 +3472,7 @@ int index_name_is_other(struct index_state *istate, const char *name,
}
void *read_blob_data_from_index(struct index_state *istate,
- const char *path, unsigned long *size)
+ const char *path, size_t *size)
{
int pos, len;
size_t sz;
@@ -3503,7 +3503,7 @@ void *read_blob_data_from_index(struct index_state *istate,
return NULL;
}
if (size)
- *size = cast_size_t_to_ulong(sz);
+ *size = sz;
return data;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH 3/3] convert: use size_t for buffer sizes
2026-10-21 10:16 ` [PATCH 3/3] convert: use size_t for buffer sizes Elia Pinto
@ 2026-10-21 11:24 ` Patrick Steinhardt
0 siblings, 0 replies; 7+ messages in thread
From: Patrick Steinhardt @ 2026-10-21 11:24 UTC (permalink / raw)
To: Elia Pinto
Cc: git, Johannes Schindelin, Jeff King, Junio C Hamano, Shuqi Liang,
Karthik Nayak, Toon Claes
On Wed, Oct 21, 2026 at 12:16:39PM +0200, Elia Pinto wrote:
> diff --git a/convert.c b/convert.c
> index 036506842c..ca5594cab0 100644
> --- a/convert.c
> +++ b/convert.c
> @@ -42,9 +42,9 @@ struct text_stat {
> unsigned printable, nonprintable;
> };
>
> -static void gather_stats(const char *buf, unsigned long size, struct text_stat *stats)
> +static void gather_stats(const char *buf, size_t size, struct text_stat *stats)
> {
> - unsigned long i;
> + size_t i;
>
> memset(stats, 0, sizeof(*stats));
>
One thing to note here is that `struct text_stat` still uses `unsigned`
counters. Is this something we should fix, as well?
> @@ -1077,7 +1077,7 @@ static int read_convert_config(const char *var, const char *value,
> return 0;
> }
>
> -static int count_ident(const char *cp, unsigned long size)
> +static int count_ident(const char *cp, size_t size)
> {
> /*
> * "$Id: 0000000000000000000000000000000000000000 $" <=> "$Id$"
This function is counting `$Id$` markers, so it's rather unlikely that
it ever overflows. Theoretically it's possible though.
Patrick
^ permalink raw reply [flat|nested] 7+ messages in thread