From dcd319476add64e892e517faa71966f09010029a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 00:55:51 +0200 Subject: [PATCH 1/5] archive-tar: guard get_path_prefix against zero-length input The `get_path_prefix()` function uses a `do ... while()` loop that unconditionally decrements the `size_t` index variable before checking the loop condition. When that variable is 0 on entry, the decrement wraps to `SIZE_MAX`, and `path[SIZE_MAX]` is an out-of-bounds read. The current caller (`write_tar_entry()`) only invokes this function when `pathlen > sizeof(header.name)` i.e. when `pathlen` is at least 100. Therefore, the bug is unreachable in practice. However, the function's interface accepts arbitrary pathlen and maxlen values, and a future caller or a maxlen of 0 could trigger the wrap, so let's add a check for that. Pointed out by Coverity. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- archive-tar.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/archive-tar.c b/archive-tar.c index a1c66024d4dff4..2da6e554842b8b 100644 --- a/archive-tar.c +++ b/archive-tar.c @@ -210,6 +210,8 @@ static size_t get_path_prefix(const char *path, size_t pathlen, size_t maxlen) i--; if (i > maxlen) i = maxlen; + if (!i) + return 0; do { i--; } while (i > 0 && path[i] != '/'); From 70a909de18e8cd8d282439fb51a9399baa8c7be8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 00:57:09 +0200 Subject: [PATCH 2/5] http: use `size_t` for byte position in fwrite_sha1_file The `fwrite_sha1_file()` function tracks the number of bytes written via an `int` variable, but the total size to write is of type `size_t`. When the HTTP response body exceeds INT_MAX (~2GB), this causes an overflow and a heap under-read, then writes that data to the local object file. The hash check afterward would reject the corrupt object (so no bad data enters the object store), but the under-read itself could leak adjacent heap contents to the temporary file. In practice this is unlikely because curl delivers data in small chunks (typically 16KB per callback invocation), so in practice, the total size is far below INT_MAX in a single call. However, the function interface accepts arbitrary sizes, and the accumulation across the loop means that the `int` variable grows across calls within a single invocation, not across callbacks. Change the type from `int` to `size_t` to match the type of the total size. This is a hardening fix: the attack surface (a malicious HTTP server triggering a >2GB single-callback delivery) is theoretical under normal curl configurations, but the type mismatch is a genuine defect. Pointed out by Coverity. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- http.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http.c b/http.c index 4af3c29c769559..809471f436b7ba 100644 --- a/http.c +++ b/http.c @@ -2848,7 +2848,7 @@ static size_t fwrite_sha1_file(char *ptr, size_t eltsize, size_t nmemb, { unsigned char expn[4096]; size_t size = eltsize * nmemb; - int posn = 0; + size_t posn = 0; struct http_object_request *freq = data; struct active_request_slot *slot = freq->slot; From b1ed701935027a83574fae753e5cc11d23f0ea4b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 01:03:34 +0200 Subject: [PATCH 3/5] wrapper: reject insanely large byte counts when reading/writing Both the `read_in_full()` as well as the `write_in_full()` function accumulate the bytes transferred count in a variable of type `ssize_t`, but accept a `size_t` count parameter. When count exceeds `SSIZE_MAX`, that total count overflows after processing `SSIZE_MAX` bytes, producing a negative return value that callers interpret as an error (or worse, a small positive value after further wrapping). In practice, git never passes such large counts because file sizes and buffer allocations are bounded well below SSIZE_MAX on all target platforms. The POSIX specification for `read()` and `write()` also limits single operations to `SSIZE_MAX` bytes, and git's `xread()`/`xwrite()` wrappers further clamp to `MAX_IO_SIZE`. But the accumulation in the `*_in_full()` wrappers means that even with per-call clamping, total can exceed `SSIZE_MAX` when count does. Reject a total count that exceeds the range of `ssize_t` upfront with `EINVAL` rather than silently producing wrong return values, matching the POSIX convention for invalid arguments. Pointed out by Coverity. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- git-compat-util.h | 4 ++++ wrapper.c | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/git-compat-util.h b/git-compat-util.h index a0f901ce793ed6..afce8b85b8aa1f 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -740,6 +740,10 @@ static inline uint64_t u64_add(uint64_t a, uint64_t b) */ #define DEFAULT_IO_BUFFER_SIZE (128 * 1024) +#ifndef SSIZE_MAX +# define SSIZE_MAX ((ssize_t)(((size_t)-1) >> 1)) +#endif + #ifdef HAVE_ALLOCA_H # include # define xalloca(size) (alloca(size)) diff --git a/wrapper.c b/wrapper.c index 05a9cd369c5971..ea9c14f834f793 100644 --- a/wrapper.c +++ b/wrapper.c @@ -288,6 +288,11 @@ ssize_t read_in_full(int fd, void *buf, size_t count) char *p = buf; ssize_t total = 0; + if (count > SSIZE_MAX) { + errno = EINVAL; + return -1; + } + while (count > 0) { ssize_t loaded = xread(fd, p, count); if (loaded < 0) @@ -307,6 +312,11 @@ ssize_t write_in_full(int fd, const void *buf, size_t count) const char *p = buf; ssize_t total = 0; + if (count > SSIZE_MAX) { + errno = EINVAL; + return -1; + } + while (count > 0) { ssize_t written = xwrite(fd, p, count); if (written < 0) From 3dcc3d97570ea7f96628d02941d7424f84f7bf39 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 01:34:55 +0200 Subject: [PATCH 4/5] git-compat-util: add `signed_mult_overflows` and `mult_overflows` macros `unsigned_mult_overflows()` does not cover signed arithmetic, but types such as `time_t` can be signed or unsigned. Add signed and type-generic guards so callers can check multiplication overflow in either case. Overflow depends on the product type selected by C's usual arithmetic conversions. Products equal to the signed minimum are representable and must not be rejected. Assisted-by: Claude Opus 4.6, GPT-6 and GPT-6.1 Signed-off-by: Johannes Schindelin --- git-compat-util.h | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/git-compat-util.h b/git-compat-util.h index afce8b85b8aa1f..077fa948733d9c 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -85,9 +85,27 @@ struct strbuf; #define bitsizeof(x) (CHAR_BIT * sizeof(x)) +#if GIT_GNUC_PREREQ(3, 1) +#define is_unsigned_type(a) ((__typeof__(a))-1 >= 0) +#elif __STDC_VERSION__ >= 201112L +#define is_unsigned_type(a) _Generic((a), \ + unsigned char: 1, \ + unsigned short: 1, \ + unsigned int: 1, \ + unsigned long: 1, \ + unsigned long long: 1, \ + _Bool: 1, \ + char: (CHAR_MIN == 0), \ + default: 0) +#endif + #define maximum_signed_value_of_type(a) \ (INTMAX_MAX >> (bitsizeof(intmax_t) - bitsizeof(a))) +/* Account for the extra negative value on two's-complement targets. */ +#define minimum_signed_value_of_type(a) \ + (-maximum_signed_value_of_type(a) - (INTMAX_MIN < -INTMAX_MAX)) + #define maximum_unsigned_value_of_type(a) \ (UINTMAX_MAX >> (bitsizeof(uintmax_t) - bitsizeof(a))) @@ -111,6 +129,38 @@ struct strbuf; #define unsigned_mult_overflows(a, b) \ ((a) && (b) > maximum_unsigned_value_of_type(a) / (a)) +/* + * Returns true if the multiplication of "a" and "b" will + * overflow or underflow a signed type. The types of "a" and "b" + * must match and must be signed. Note that this macro evaluates + * both "a" and "b" twice! + */ +#define signed_mult_overflows(a, b) \ + (((a) > 0 && (b) > 0 && (a) > maximum_signed_value_of_type(a) / (b)) || \ + ((a) < 0 && (b) < 0 && (a) < maximum_signed_value_of_type(a) / (b)) || \ + ((a) > 0 && (b) < 0 && (b) < minimum_signed_value_of_type(a) / (a)) || \ + ((a) < 0 && (b) > 0 && (a) < minimum_signed_value_of_type(b) / (b))) + +/* + * Returns true if the product of "a" and "b", after the usual arithmetic + * conversions, cannot be represented in the multiplication expression's + * type. The operands must be integers and may be evaluated more than once. + */ +#if GIT_GNUC_PREREQ(3, 1) +#define mult_overflows(a, b) \ + (is_unsigned_type((a) * (b)) \ + ? unsigned_mult_overflows((__typeof__((a) * (b)))(a), \ + (__typeof__((a) * (b)))(b)) \ + : signed_mult_overflows((__typeof__((a) * (b)))(a), \ + (__typeof__((a) * (b)))(b))) +#else +/* + * This implementation needs GNU `typeof` to convert both operands to the + * multiplication's type. Skip the check without that extension. + */ +#define mult_overflows(a, b) 0 +#endif + /* * Returns true if the left shift of "a" by "shift" bits will * overflow. The type of "a" must be unsigned. From a4e602b9e726081aaaf73c627548b823a4d35477 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 01:38:45 +0200 Subject: [PATCH 5/5] date: avoid overflow in approxidate relative-time multiplication The `approxidate_alpha()` function multiplies a time unit length (e.g., 604800 for weeks) by a user-supplied number to compute a time offset in seconds. Both operands are `int`, and the product is passed to `update_tm()` as `time_t`. For inputs like "99999 weeks ago", the int multiplication overflows (604800 * 99999 is roughly 60 billion, far beyond `INT_MAX`), producing a wrapped value that results in a wrong date. Use the checked `time_t` multiplication so oversized products do not overflow or wrap around. On platforms where `time_t` is unsigned (and where the pre-processor allows us to determine this fact), reject counts that are too large and report a parse error. Narrowing them first could otherwise produce negative or truncated values that become bogus offsets. Leave signed `time_t` count parsing unchanged. Pointed out by Coverity. Assisted-by: Claude Opus 4.6 and GPT-6.1 Sol Signed-off-by: Johannes Schindelin --- date.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/date.c b/date.c index 014065b419aee7..0d65804d2f939b 100644 --- a/date.c +++ b/date.c @@ -1298,7 +1298,18 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, struct tm while (tl->type) { size_t len = strlen(tl->type); if (match_string(date, tl->type) >= len-1) { - update_tm(tm, now, tl->length * *num); + time_t length = tl->length, num_t = *num; + time_t seconds; +#ifndef is_unsigned_type + seconds = length * num_t; +#else + time_t max_val = is_unsigned_type(length) + ? maximum_unsigned_value_of_type(length) + : maximum_signed_value_of_type(length); + seconds = mult_overflows(length, num_t) + ? max_val : length * num_t; +#endif + update_tm(tm, now, seconds); *num = 0; *touched = 1; return end; @@ -1368,8 +1379,15 @@ static const char *approxidate_digit(const char *date, struct tm *tm, int *num, } /* Accept zero-padding only for small numbers ("Dec 02", never "Dec 0002") */ - if (date[0] != '0' || end - date <= 2) + if (date[0] != '0' || end - date <= 2) { +#ifdef is_unsigned_type + if (is_unsigned_type((time_t)0) && + (number > INT_MAX || + number > maximum_unsigned_value_of_type((time_t)0))) + return NULL; +#endif *num = number; + } return end; } @@ -1398,6 +1416,10 @@ static timestamp_t approxidate_str(const char *date, if (isdigit(c)) { pending_number(&tm, &number); date = approxidate_digit(date-1, &tm, &number, time_sec); + if (!date) { + *error_ret = 1; + return 0; + } touched = 1; continue; }