* [PATCH 0/9] hash algorithm leak fixes
@ 2026-07-02 7:52 Jeff King
2026-07-02 7:57 ` [PATCH 1/9] csum-file: drop discard_hashfile() Jeff King
` (8 more replies)
0 siblings, 9 replies; 20+ messages in thread
From: Jeff King @ 2026-07-02 7:52 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
This series fixes some leaks you can find by running:
make SANITIZE=leak \
OPENSSL_SHA256=1 \
GIT_TEST_DEFAULT_HASH=sha256 \
test
The crux of the issue is that we depend on calling git_hash_final() to
clean up any git_hash_ctx we've initialized. But we don't always call
that function (we may return early due to an error, etc).
We don't see these in our regular leak-test builds because the default
hash implementations we use treat the hash_ctx as a sequence of bytes.
So there's no cleanup needed, and just letting the context go out of
scope is fine. But other implementations do allocate on initialization,
and need to have some kind of free/discard function. So building with
OPENSSL_SHA256 above is what lets us see the leaks.
You can see the same thing with OPENSSL_SHA1, but of course we don't
recommend that. Using OPENSSL_SHA1_UNSAFE likewise, but it sees only a
subset of the leaks since it is only used in a few code paths. Those
leaks would be found if we turned on leak-checking in the
linux-TEST-vars job, but the rest of them would require leak-checking
the linux-sha256 job.
And as a special bonus, patch 8 is a semi-related leak that only affects
libgcrypt. I don't think we build against that in CI at all. :-/
[1/9]: csum-file: drop discard_hashfile()
[2/9]: hash: add discard primitive
[3/9]: csum-file: always finalize or discard hash
[4/9]: csum-file: provide a function to release checkpoints
[5/9]: patch-id: discard hash when done
[6/9]: check_stream_oid(): discard hash on read error
[7/9]: http: discard hash in dumb-http http_object_request
[8/9]: hash: fix memory leak copying sha256 gcrypt handles
[9/9]: hash: add platform-specific discard functions
builtin/fast-import.c | 1 +
builtin/patch-id.c | 1 +
csum-file.c | 30 +++++++++++++++++-------------
csum-file.h | 2 +-
diff.c | 1 +
hash.c | 29 +++++++++++++++++++++++++++++
hash.h | 22 ++++++++++++++++++++++
http.c | 4 ++++
http.h | 1 +
object-file.c | 4 ++++
sha1/openssl.h | 6 ++++++
sha256/gcrypt.h | 7 +++++++
sha256/openssl.h | 6 ++++++
13 files changed, 100 insertions(+), 14 deletions(-)
-Peff
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH 1/9] csum-file: drop discard_hashfile()
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
@ 2026-07-02 7:57 ` Jeff King
2026-07-02 18:19 ` Junio C Hamano
2026-07-02 7:59 ` [PATCH 2/9] hash: add discard primitive Jeff King
` (7 subsequent siblings)
8 siblings, 1 reply; 20+ messages in thread
From: Jeff King @ 2026-07-02 7:57 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
Commit c3d034df16 (csum-file: introduce discard_hashfile(), 2024-07-25)
added a cleanup function that no longer has any callers. In that commit
we adjusted do_write_index() to use the new function. But a similar fix
occurred on a parallel branch, making free_hashfile() public, and the
merge resolution in 1b6b2bfae5 (Merge branch 'ps/leakfixes-part-4',
2024-08-23) took the free_hashfile() version.
So now we have two functions, discard_hashfile() and free_hashfile(),
and we only need one. Which one do we want to keep?
The only difference between them is that the discard variant also closes
the descriptors held in the struct. Let's look at the three callers:
1. In finalize_hashfile() we've either already closed the descriptors
(if the CSUM_CLOSE flag is passed) or the caller didn't want them
closed (if it didn't pass that flag). So we want the more limited
free_hashfile().
2. In object-file.c:flush_packfile_transaction() we close the
descriptor ourselves. So discard_hashfile() could save us a line of
code.
3. In do_write_index() we don't close the descriptor. This was the spot
for which c3d034df16 added the discard function in the first place,
but I'm skeptical that closing the descriptor here is the right
thing. It is true that we are done with the descriptor at this
point and closing it would be ideal. But we don't really own it!
The descriptor comes from a tempfile struct (as part of a lock) and
that tempfile will hold on to the descriptor and try to close it
when it is deleted. This might happen at the end of the program, in
which case the double-close is mostly harmless (we might
accidentally close some other open descriptor, but at that point
we're just closing and unlinking everything we can).
But in theory it could also cause subtle bugs. If do_write_index()
fails, we return the error up the stack and would eventually end up
in write_locked_index(). There we roll back the lock file on error,
which will close the descriptor. So now we get our double close,
and we might actually close something else that was opened in the
interim.
This is probably unlikely in practice (as soon as we see the error
we'd mostly be unwinding the stack, not opening new files). But it
highlights a potential problem with the discard_hashfile()
interface: the hashfile doesn't necessarily own that descriptor.
Note that I said "descriptors" plural above. Those callers all care
about the "fd" member of the struct. But discard_hashfile() also closes
check_fd. That is only used if the struct is initialized with
hashfd_check(), and neither of its two callers call either discard or
free (they always "finalize" instead). So closing it is irrelevant for
the current callers.
I think we're better off sticking with the simpler free_hashfile()
interface, and the handful of callers can decide how to handle the
descriptors themselves.
Signed-off-by: Jeff King <peff@peff.net>
---
This is a semi-related cleanup that is in this series because we'll be
touching the free function in a bit. And at first I thought we'd
want the discard() variant, but after poking around a bit I'm pretty
sure we don't.
I do like the name discard() better, as it makes it more clear that it
is an alternative to finalize(). Since they have the same signature,
swapping the names/implementations _could_ confuse long-running branches
or topics in flight, but I kind of doubt there are any, given the
history.
csum-file.c | 9 ---------
csum-file.h | 1 -
2 files changed, 10 deletions(-)
diff --git a/csum-file.c b/csum-file.c
index d7a682c2b6..8ca9246a80 100644
--- a/csum-file.c
+++ b/csum-file.c
@@ -101,15 +101,6 @@ int finalize_hashfile(struct hashfile *f, unsigned char *result,
return fd;
}
-void discard_hashfile(struct hashfile *f)
-{
- if (0 <= f->check_fd)
- close(f->check_fd);
- if (0 <= f->fd)
- close(f->fd);
- free_hashfile(f);
-}
-
void hashwrite(struct hashfile *f, const void *buf, uint32_t count)
{
while (count) {
diff --git a/csum-file.h b/csum-file.h
index a270738a7a..d1a0ff29cd 100644
--- a/csum-file.h
+++ b/csum-file.h
@@ -74,7 +74,6 @@ void free_hashfile(struct hashfile *f);
* Finalize the hashfile by flushing data to disk and free'ing it.
*/
int finalize_hashfile(struct hashfile *, unsigned char *, enum fsync_component, unsigned int);
-void discard_hashfile(struct hashfile *);
void hashwrite(struct hashfile *, const void *, uint32_t);
void hashflush(struct hashfile *f);
void crc32_begin(struct hashfile *);
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 2/9] hash: add discard primitive
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
2026-07-02 7:57 ` [PATCH 1/9] csum-file: drop discard_hashfile() Jeff King
@ 2026-07-02 7:59 ` Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-02 8:01 ` [PATCH 3/9] csum-file: always finalize or discard hash Jeff King
` (6 subsequent siblings)
8 siblings, 1 reply; 20+ messages in thread
From: Jeff King @ 2026-07-02 7:59 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
The usual life-cycle for a git_hash_ctx is calling git_hash_init(),
adding some data, and then using git_hash_final() to get the output
digest and free any resources.
Sometimes we decide to abort the operation without the final() call
(e.g., due to errors or other reasons). In that case we just abandon the
hash_ctx completely and let it go out of scope. For most hash
implementations this is fine; they were just holding values directly in
the struct.
But some implementations do allocate memory, and in these cases we leak
the memory. Notably OpenSSL >= 3.0 requires us to allocate the digest
context on the heap with EVP_MD_CTX_new().
Let's provide a git_hash_discard() function that can be used in these
code paths to free any resources. For now we'll implement it by just
calling git_hash_final() into a dummy output, relying on its side effect
of freeing the resources. Our view of the underlying hash implementation
is abstracted behind the platform_SHA_* macros, so that's the best we
can do without widening that interface.
It's a little inefficient, but probably not noticeably so in practice,
especially as we'd usually hit this on an error code path. And by
abstracting it in this function, we can later swap it out when the
platform_SHA interface lets us do so.
Signed-off-by: Jeff King <peff@peff.net>
---
In case you're on the edge of your seat, that widening happens in patch
9. It was helpful to make sure the simple-and-stupid thing actually
fixed the leaks first, and then do the convoluted platform-macro magic
later.
hash.c | 12 ++++++++++++
hash.h | 1 +
2 files changed, 13 insertions(+)
diff --git a/hash.c b/hash.c
index e925b9754e..63672a3d22 100644
--- a/hash.c
+++ b/hash.c
@@ -283,6 +283,18 @@ void git_hash_final_oid(struct object_id *oid, struct git_hash_ctx *ctx)
ctx->algop->final_oid_fn(oid, ctx);
}
+void git_hash_discard(struct git_hash_ctx *ctx)
+{
+ /*
+ * XXX Many implementations do not need to do anything here,
+ * and a dummy final() call is wasteful. But we can't fix
+ * that unless our implementation API exposes a discard
+ * primitive.
+ */
+ unsigned char dummy[GIT_MAX_RAWSZ];
+ git_hash_final(dummy, ctx);
+}
+
uint32_t hash_algo_by_name(const char *name)
{
if (!name)
diff --git a/hash.h b/hash.h
index c082a53c9a..6b2f04e2a4 100644
--- a/hash.h
+++ b/hash.h
@@ -325,6 +325,7 @@ void git_hash_clone(struct git_hash_ctx *dst, const struct git_hash_ctx *src);
void git_hash_update(struct git_hash_ctx *ctx, const void *in, size_t len);
void git_hash_final(unsigned char *hash, struct git_hash_ctx *ctx);
void git_hash_final_oid(struct object_id *oid, struct git_hash_ctx *ctx);
+void git_hash_discard(struct git_hash_ctx *ctx);
const struct git_hash_algo *hash_algo_ptr_by_number(uint32_t algo);
struct git_hash_ctx *git_hash_alloc(void);
void git_hash_free(struct git_hash_ctx *ctx);
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 3/9] csum-file: always finalize or discard hash
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
2026-07-02 7:57 ` [PATCH 1/9] csum-file: drop discard_hashfile() Jeff King
2026-07-02 7:59 ` [PATCH 2/9] hash: add discard primitive Jeff King
@ 2026-07-02 8:01 ` Jeff King
2026-07-02 8:03 ` [PATCH 4/9] csum-file: provide a function to release checkpoints Jeff King
` (5 subsequent siblings)
8 siblings, 0 replies; 20+ messages in thread
From: Jeff King @ 2026-07-02 8:01 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
When a hashfile struct is created, we always initialize the git_hash_ctx
inside it. We usually end up in hashfile_finalize(), which passes that
ctx to git_hash_final(), cleaning it up.
But a few code paths don't do so:
1. If we bail on the hashfile and call free_hashfile() directly rather
than finalizing.
2. If the skip_hash flag is set, the hashfile_finalize() call will
never call git_hash_final(). (You might think that we should just
avoid git_hash_init() entirely in this case, but the skip_hash flag
is set by the caller after the hashfile is initialized).
For most hash implementations this is OK, but for ones that allocate on
initialization it causes a memory leak. You can see many failures by
running:
make SANITIZE=leak OPENSSL_SHA1_UNSAFE=1 test
since OpenSSL >= 3.0 is such an allocating hash implementation (and
csum-file uses the "unsafe" algorithm variant).
We can solve this by calling git_hash_discard() as appropriate.
Note that free_hashfile() is used both directly by callers to abort
without finalizing, and by hashfile_finalize() to free memory. In the
latter case we _don't_ want to call git_hash_discard(), because we'll
already have either finalized or discarded it. So we'll push that to an
internal "free_memory" function, and keep free_hashfile() as the public
interface to abort a hashfile without finalizing.
This fix makes several scripts leak-free with the command above: t1600,
t1601, t2107, t7008, t9210, t9211.
Signed-off-by: Jeff King <peff@peff.net>
---
csum-file.c | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/csum-file.c b/csum-file.c
index 8ca9246a80..44ff460692 100644
--- a/csum-file.c
+++ b/csum-file.c
@@ -55,24 +55,32 @@ void hashflush(struct hashfile *f)
}
}
-void free_hashfile(struct hashfile *f)
+static void free_hashfile_memory(struct hashfile *f)
{
free(f->buffer);
free(f->check_buffer);
free(f);
}
+void free_hashfile(struct hashfile *f)
+{
+ git_hash_discard(&f->ctx);
+ free_hashfile_memory(f);
+}
+
int finalize_hashfile(struct hashfile *f, unsigned char *result,
enum fsync_component component, unsigned int flags)
{
int fd;
hashflush(f);
- if (f->skip_hash)
+ if (f->skip_hash) {
+ git_hash_discard(&f->ctx);
hashclr(f->buffer, f->algop);
- else
+ } else {
git_hash_final(f->buffer, &f->ctx);
+ }
if (result)
hashcpy(result, f->buffer, f->algop);
@@ -97,7 +105,7 @@ int finalize_hashfile(struct hashfile *f, unsigned char *result,
if (close(f->check_fd))
die_errno("%s: sha1 file error on close", f->name);
}
- free_hashfile(f);
+ free_hashfile_memory(f);
return fd;
}
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 4/9] csum-file: provide a function to release checkpoints
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
` (2 preceding siblings ...)
2026-07-02 8:01 ` [PATCH 3/9] csum-file: always finalize or discard hash Jeff King
@ 2026-07-02 8:03 ` Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-02 8:04 ` [PATCH 5/9] patch-id: discard hash when done Jeff King
` (4 subsequent siblings)
8 siblings, 1 reply; 20+ messages in thread
From: Jeff King @ 2026-07-02 8:03 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
A hashfile_checkpoint struct is basically just a copy of the hash_ctx
state at a given point in the file. As such, it contains its own
git_hash_ctx which may (depending on the underlying hash implementation)
need to be discarded when we're done with it.
Let's add a "release" function which cleans up the hash context it
holds. I chose "release" here and not "discard" because you'd use this
to clean up every checkpoint, whether you used it or not. As opposed to
git_hash_discard(), which is needed only if you didn't call
git_hash_final().
There are only two callers which use hashfile_checkpoints, and we can
add release calls to both. When built with "SANITIZE=leak
OPENSSL_SHA1_UNSAFE=1", this makes both t1050 and t9300 leak-free.
Signed-off-by: Jeff King <peff@peff.net>
---
builtin/fast-import.c | 1 +
csum-file.c | 5 +++++
csum-file.h | 1 +
object-file.c | 2 ++
4 files changed, 9 insertions(+)
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index aa656c5195..f6473dcc8e 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -1216,6 +1216,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)
out:
free(in_buf);
free(out_buf);
+ hashfile_checkpoint_release(&checkpoint);
}
/* All calls must be guarded by find_object() or find_mark() to
diff --git a/csum-file.c b/csum-file.c
index 44ff460692..b166f89624 100644
--- a/csum-file.c
+++ b/csum-file.c
@@ -223,6 +223,11 @@ int hashfile_truncate(struct hashfile *f, struct hashfile_checkpoint *checkpoint
return 0;
}
+void hashfile_checkpoint_release(struct hashfile_checkpoint *checkpoint)
+{
+ git_hash_discard(&checkpoint->ctx);
+}
+
void crc32_begin(struct hashfile *f)
{
f->crc32 = crc32(0, NULL, 0);
diff --git a/csum-file.h b/csum-file.h
index d1a0ff29cd..6ed74d1637 100644
--- a/csum-file.h
+++ b/csum-file.h
@@ -39,6 +39,7 @@ struct hashfile_checkpoint {
void hashfile_checkpoint_init(struct hashfile *, struct hashfile_checkpoint *);
void hashfile_checkpoint(struct hashfile *, struct hashfile_checkpoint *);
int hashfile_truncate(struct hashfile *, struct hashfile_checkpoint *);
+void hashfile_checkpoint_release(struct hashfile_checkpoint *);
/* finalize_hashfile flags */
#define CSUM_CLOSE 1
diff --git a/object-file.c b/object-file.c
index e3d92bbda2..32a0d6d237 100644
--- a/object-file.c
+++ b/object-file.c
@@ -1352,6 +1352,8 @@ static int odb_transaction_files_write_object_stream(struct odb_transaction *bas
state->alloc_written);
state->written[state->nr_written++] = idx;
}
+
+ hashfile_checkpoint_release(&checkpoint);
return 0;
}
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 5/9] patch-id: discard hash when done
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
` (3 preceding siblings ...)
2026-07-02 8:03 ` [PATCH 4/9] csum-file: provide a function to release checkpoints Jeff King
@ 2026-07-02 8:04 ` Jeff King
2026-07-02 8:05 ` [PATCH 6/9] check_stream_oid(): discard hash on read error Jeff King
` (3 subsequent siblings)
8 siblings, 0 replies; 20+ messages in thread
From: Jeff King @ 2026-07-02 8:04 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
When computing a patch-id, we have a flush_one_hunk() helper that calls
git_hash_final() on our running hunk git_hash_ctx, and then
reinitializes that context for the next hunk.
When we run out of hunks to look at, we return, discarding the
git_hash_ctx. This can cause a leak if the hash implementation we are
using allocates any memory during its initialization. This includes
OpenSSL >= 3.0, for both SHA-1 and SHA-256. Normally we would not use
SHA-1 here at all, as we only recommend using non-DC implementations for
the "unsafe" variant (and patch-id, though they probably _could_ use the
unsafe variant, were never taught to do so).
But it is certainly a problem for SHA-256, which you can see with:
make SANITIZE=leak \
OPENSSL_SHA256=1 \
GIT_TEST_DEFAULT_HASH=sha256 \
test
That results in leak failures of 60 scripts, 57 of which are fixed by
this patch (basically anything which runs rebase will hit this case).
Signed-off-by: Jeff King <peff@peff.net>
---
builtin/patch-id.c | 1 +
diff.c | 1 +
2 files changed, 2 insertions(+)
diff --git a/builtin/patch-id.c b/builtin/patch-id.c
index 2781598ede..57d9bd4a65 100644
--- a/builtin/patch-id.c
+++ b/builtin/patch-id.c
@@ -173,6 +173,7 @@ static size_t get_one_patchid(struct object_id *next_oid, struct object_id *resu
oidclr(next_oid, the_repository->hash_algo);
flush_one_hunk(result, &ctx);
+ git_hash_discard(&ctx);
return patchlen;
}
diff --git a/diff.c b/diff.c
index 2a9d0d8687..1568f0ed9c 100644
--- a/diff.c
+++ b/diff.c
@@ -6987,6 +6987,7 @@ static int diff_get_patch_id(struct diff_options *options, struct object_id *oid
flush_one_hunk(oid, &ctx);
}
+ git_hash_discard(&ctx);
return 0;
}
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 6/9] check_stream_oid(): discard hash on read error
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
` (4 preceding siblings ...)
2026-07-02 8:04 ` [PATCH 5/9] patch-id: discard hash when done Jeff King
@ 2026-07-02 8:05 ` Jeff King
2026-07-02 8:07 ` [PATCH 7/9] http: discard hash in dumb-http http_object_request Jeff King
` (2 subsequent siblings)
8 siblings, 0 replies; 20+ messages in thread
From: Jeff King @ 2026-07-02 8:05 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
The happy path of check_stream_oid() is to initialize a hash, feed the
loose object zlib stream into it, and then get the final result. But if
we hit a zlib error or see extra cruft we'll bail early with an error.
Since we never call git_hash_final() in this cases, any resources held
by the git_hash_ctx may be leaked. Our default hash algorithms don't
allocate anything in the hash_ctx, but some implementations do. For
example, running:
make SANITIZE=leak \
OPENSSL_SHA256=1 \
GIT_TEST_DEFAULT_HASH=sha256 \
test
will fail t1450, since it feeds corrupted objects that cause us to bail
from check_stream_oid(). This patch fixes it by discarding the hash in
those early return paths. Trying to jump to a common "out:" label is not
worth it here, as we must _not_ discard a hash that was already fed to
git_hash_final(). And the hash_ctx itself does not carry any information
(so we cannot check for a NULL pointer, etc).
Signed-off-by: Jeff King <peff@peff.net>
---
object-file.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/object-file.c b/object-file.c
index 32a0d6d237..035d005279 100644
--- a/object-file.c
+++ b/object-file.c
@@ -1587,11 +1587,13 @@ static int check_stream_oid(git_zstream *stream,
if (status != Z_STREAM_END) {
error(_("corrupt loose object '%s'"), oid_to_hex(expected_oid));
+ git_hash_discard(&c);
return -1;
}
if (stream->avail_in) {
error(_("garbage at end of loose object '%s'"),
oid_to_hex(expected_oid));
+ git_hash_discard(&c);
return -1;
}
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 7/9] http: discard hash in dumb-http http_object_request
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
` (5 preceding siblings ...)
2026-07-02 8:05 ` [PATCH 6/9] check_stream_oid(): discard hash on read error Jeff King
@ 2026-07-02 8:07 ` Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-02 8:09 ` [PATCH 8/9] hash: fix memory leak copying sha256 gcrypt handles Jeff King
2026-07-02 8:13 ` [PATCH 9/9] hash: add platform-specific discard functions Jeff King
8 siblings, 1 reply; 20+ messages in thread
From: Jeff King @ 2026-07-02 8:07 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
Usually an object request results in finish_http_object_request()
calling git_hash_final_oid(), after we've received all of the data. But
if we hit an error, we'll bail early and free the http_object_request,
dropping the git_hash_ctx entirely. This can cause a leak for hash
implementations that allocate memory in their context, like OpenSSL >=
3.0.
The obvious fix is for abort_http_object_request() to call
git_hash_discard(), under the assumption that every request is either
finished or aborted. But that's not quite true:
1. Not everybody calls the abort function. Sometimes they jump
straight to release_http_object_request(). So we'd have to put it
there.
2. After the finish function finalizes the hash, we can still
encounter errors! In that case we end up aborting or releasing,
and they must not discard that hash (since that would be a
double-free).
So we'll keep a flag marking the validity of the hash_ctx field of the
request. The lifetime is simple: it is valid immediately after creation,
up until we call finalize. And then our release function can just
conditionally discard the hash based on that flag.
This fixes test failures in t5550 and t5619 when run with:
make SANITIZE=leak \
OPENSSL_SHA256=1 \
GIT_TEST_DEFAULT_HASH=sha256 \
test
The flag handling could be removed if the hash-discard function were
idempotent. This could be done easily-ish by having the underlying
hash functions (like the ones in sha256/openssl.h) set the context
pointer to NULL after free-ing. But it's something that every platform
implementation would have to remember to do, and the benefit for the
callers is not that huge (it would let us shave a few lines here and
probably in a few other spots).
Signed-off-by: Jeff King <peff@peff.net>
---
I think the "set to NULL" thing gets weird with gcrypt, too, which does
not even use a pointer (we typedef their libgcrypt handle into our own
context struct).
http.c | 4 ++++
http.h | 1 +
2 files changed, 5 insertions(+)
diff --git a/http.c b/http.c
index b4e7b8d00b..63abbaae8a 100644
--- a/http.c
+++ b/http.c
@@ -2880,6 +2880,7 @@ struct http_object_request *new_http_object_request(const char *base_url,
git_inflate_init(&freq->stream);
the_hash_algo->init_fn(&freq->c);
+ freq->hash_ctx_valid = 1;
freq->url = get_remote_object_url(base_url, hex, 0);
@@ -2988,6 +2989,7 @@ int finish_http_object_request(struct http_object_request *freq)
}
git_hash_final_oid(&freq->real_oid, &freq->c);
+ freq->hash_ctx_valid = 0;
if (freq->zret != Z_STREAM_END) {
unlink_or_warn(freq->tmpfile.buf);
return -1;
@@ -3028,6 +3030,8 @@ void release_http_object_request(struct http_object_request **freq_p)
curl_slist_free_all(freq->headers);
strbuf_release(&freq->tmpfile);
git_inflate_end(&freq->stream);
+ if (freq->hash_ctx_valid)
+ git_hash_discard(&freq->c);
free(freq);
*freq_p = NULL;
diff --git a/http.h b/http.h
index 729c51904d..6b0639150f 100644
--- a/http.h
+++ b/http.h
@@ -255,6 +255,7 @@ struct http_object_request {
struct object_id oid;
struct object_id real_oid;
struct git_hash_ctx c;
+ int hash_ctx_valid;
git_zstream stream;
int zret;
int rename;
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 8/9] hash: fix memory leak copying sha256 gcrypt handles
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
` (6 preceding siblings ...)
2026-07-02 8:07 ` [PATCH 7/9] http: discard hash in dumb-http http_object_request Jeff King
@ 2026-07-02 8:09 ` Jeff King
2026-07-02 8:13 ` [PATCH 9/9] hash: add platform-specific discard functions Jeff King
8 siblings, 0 replies; 20+ messages in thread
From: Jeff King @ 2026-07-02 8:09 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
Our abstracted hash-algorithm API allows for cloning a hash context. By
default this just memcpy()s the bytes, but specific implementations can
provide a custom clone function.
Our API is based around the way that OpenSSL works, which is that you
first initialize the destination context, then copy into it. In our code
that is this:
algo->init_fn(&dst);
git_hash_clone(&dst, src);
and that translates into OpenSSL calls like:
/* init_fn */
dst->ectx = EVP_MD_CTX_new();
EVP_DigestInit_ex(dst->ectx, EVP_sha256());
/* clone */
EVP_MD_CTX_copy_ex(dst->ectx, src->ectx);
So the allocation happens in the first step, and then the clone is just
copying values (the DigestInit is initializing values that just get
overwritten, but that's not wrong, just a little inefficient).
But libgcrypt doesn't work like that! Its copy function initializes dst
from scratch. So when using the sha256 gcrypt backend, that becomes:
/* init_fn; this allocates */
gcry_md_open(&dst, GCRY_MD_SHA256);
/* clone; this also allocates, leaking the previous value! */
gcry_md_copy(&dst, src);
You can see the leaks in the test suite by running:
make \
SANITIZE=leak \
GCRYPT_SHA256=1 \
GIT_TEST_DEFAULT_SHA=256 \
test
which has many failures, as opposed to building with OPENSSL_SHA256,
which is leak-free.
The easy fix here is for the clone function to close the open context
we're about to overwrite. It's a little inefficient (we did a pointless
open in the init function), but probably not a big deal in practice.
If our API went the other way, assuming that we're always cloning into
garbage bytes, then we could be more efficient. We'd teach OpenSSL's
clone function to do its own new(), skip the DigestInit, and then copy
into it. And gcrypt could stick with just the copy() call.
But look again at the asymmetry in the very first code example. We call
the init function straight from the git_hash_algo struct, and then
subsequent calls are dispatched through our git_hash_* wrappers. If you
wanted to clone into an uninitialized destination, you'd do something
like:
algo->clone_fn(&dst, src);
instead. That would require changing all of the callers. There's not
that many of them, but I don't know that it's worth changing our calling
conventions to try to reclaim this tiny bit of efficiency.
Signed-off-by: Jeff King <peff@peff.net>
---
sha256/gcrypt.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/sha256/gcrypt.h b/sha256/gcrypt.h
index 17a90f1052..694a2b70a1 100644
--- a/sha256/gcrypt.h
+++ b/sha256/gcrypt.h
@@ -27,6 +27,7 @@ static inline void gcrypt_SHA256_Final(unsigned char *digest, gcrypt_SHA256_CTX
static inline void gcrypt_SHA256_Clone(gcrypt_SHA256_CTX *dst, const gcrypt_SHA256_CTX *src)
{
+ gcry_md_close(*dst);
gcry_md_copy(dst, *src);
}
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH 9/9] hash: add platform-specific discard functions
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
` (7 preceding siblings ...)
2026-07-02 8:09 ` [PATCH 8/9] hash: fix memory leak copying sha256 gcrypt handles Jeff King
@ 2026-07-02 8:13 ` Jeff King
8 siblings, 0 replies; 20+ messages in thread
From: Jeff King @ 2026-07-02 8:13 UTC (permalink / raw)
To: git; +Cc: Patrick Steinhardt
Our git_hash_discard() is a bit hacky: it just calls git_hash_final()
into a dummy result buffer, using the side effect that each
implementation's Final() function will also free any resources.
This is probably not too terrible, since generating the final hash is
not that expensive and we'd mostly call discard on unusual or error code
paths. But we can do better by widening the platform API a bit to add an
explicit discard function.
This requires an annoying amount of boilerplate:
- Each algorithm needs a git_$ALGO_discard() wrapper that dereferences
the union'd git_hash_ctx into the type-safe field. So sha1 + sha256
+ sha1-unsafe, plus a BUG() for the unknown algo. And then these all
need to be referenced in the git_hash_algo structs.
- Platforms which don't do anything special to discard now need a
fallback function which does nothing. And we need this for each algo
(sha1, sha256, and sha1-unsafe).
- Platforms which do need to discard must define their discard
functions. This includes sha1/openssl, sha256/openssl, and
sha256/gcrypt (no sha1-unsafe here as it sits atop the sha1/openssl
functions).
- Algo selection needs to point platform_*_Discard to the appropriate
underlying macro, or indicate that the fallback should be used. We
have a similar situation for the Clone function (where a straight
memcpy() of the context struct is not enough for some platforms).
I've tied Discard to the same flag used by Clone here, since they
are basically the same problem: is the hash context a sequence of
bytes, or does it need smart copying/discarding?
It's easy to miss a case here since we don't even compile the
implementations we aren't using. I've tested with each of:
- no flags, which uses our internal sha1/sha256 implementations, both
of which exercise the noop fallback function
- OPENSSL_SHA1_UNSAFE=1, which checks that our unsafe macro
redirections work
- OPENSSL_SHA1=1, though you should not do that in real life!
- OPENSSL_SHA256=1, passes tests with GIT_TEST_DEFAULT_HASH=sha256
- GCRYPT_SHA256=1, which likewise passes
The other implementations do not set the CLONE_HELPER flag, so they
treat the context as bytes and should be fine with the fallback.
Signed-off-by: Jeff King <peff@peff.net>
---
One of the reasons I left this to the end is that I wasn't sure it would
be worth it. I think it probably is (hence posting it), but we could
live with the hacky implementation forever if we wanted. :)
It also prompted me to test with all of the backends I could build,
which is how I found the unrelated gcrypt leak fixed in patch 8.
hash.c | 33 +++++++++++++++++++++++++--------
hash.h | 21 +++++++++++++++++++++
sha1/openssl.h | 6 ++++++
sha256/gcrypt.h | 6 ++++++
sha256/openssl.h | 6 ++++++
5 files changed, 64 insertions(+), 8 deletions(-)
diff --git a/hash.c b/hash.c
index 63672a3d22..55d1d41770 100644
--- a/hash.c
+++ b/hash.c
@@ -72,6 +72,11 @@ static void git_hash_sha1_final_oid(struct object_id *oid, struct git_hash_ctx *
oid->algo = GIT_HASH_SHA1;
}
+static void git_hash_sha1_discard(struct git_hash_ctx *ctx)
+{
+ git_SHA1_Discard(&ctx->state.sha1);
+}
+
static void git_hash_sha1_init_unsafe(struct git_hash_ctx *ctx)
{
ctx->algop = unsafe_hash_algo(&hash_algos[GIT_HASH_SHA1]);
@@ -102,6 +107,11 @@ static void git_hash_sha1_final_oid_unsafe(struct object_id *oid, struct git_has
oid->algo = GIT_HASH_SHA1;
}
+static void git_hash_sha1_discard_unsafe(struct git_hash_ctx *ctx)
+{
+ git_SHA1_Discard_unsafe(&ctx->state.sha1_unsafe);
+}
+
static void git_hash_sha256_init(struct git_hash_ctx *ctx)
{
ctx->algop = unsafe_hash_algo(&hash_algos[GIT_HASH_SHA256]);
@@ -135,6 +145,11 @@ static void git_hash_sha256_final_oid(struct object_id *oid, struct git_hash_ctx
oid->algo = GIT_HASH_SHA256;
}
+static void git_hash_sha256_discard(struct git_hash_ctx *ctx)
+{
+ git_SHA256_Discard(&ctx->state.sha256);
+}
+
static void git_hash_unknown_init(struct git_hash_ctx *ctx UNUSED)
{
BUG("trying to init unknown hash");
@@ -165,6 +180,11 @@ static void git_hash_unknown_final_oid(struct object_id *oid UNUSED,
BUG("trying to finalize unknown hash");
}
+static void git_hash_unknown_discard(struct git_hash_ctx *ctx UNUSED)
+{
+ BUG("trying to discard unknown hash");
+}
+
static const struct git_hash_algo sha1_unsafe_algo = {
.name = "sha1",
.format_id = GIT_SHA1_FORMAT_ID,
@@ -176,6 +196,7 @@ static const struct git_hash_algo sha1_unsafe_algo = {
.update_fn = git_hash_sha1_update_unsafe,
.final_fn = git_hash_sha1_final_unsafe,
.final_oid_fn = git_hash_sha1_final_oid_unsafe,
+ .discard_fn = git_hash_sha1_discard_unsafe,
.empty_tree = &empty_tree_oid,
.empty_blob = &empty_blob_oid,
.null_oid = &null_oid_sha1,
@@ -193,6 +214,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = {
.update_fn = git_hash_unknown_update,
.final_fn = git_hash_unknown_final,
.final_oid_fn = git_hash_unknown_final_oid,
+ .discard_fn = git_hash_unknown_discard,
.empty_tree = NULL,
.empty_blob = NULL,
.null_oid = NULL,
@@ -208,6 +230,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = {
.update_fn = git_hash_sha1_update,
.final_fn = git_hash_sha1_final,
.final_oid_fn = git_hash_sha1_final_oid,
+ .discard_fn = git_hash_sha1_discard,
.unsafe = &sha1_unsafe_algo,
.empty_tree = &empty_tree_oid,
.empty_blob = &empty_blob_oid,
@@ -224,6 +247,7 @@ const struct git_hash_algo hash_algos[GIT_HASH_NALGOS] = {
.update_fn = git_hash_sha256_update,
.final_fn = git_hash_sha256_final,
.final_oid_fn = git_hash_sha256_final_oid,
+ .discard_fn = git_hash_sha256_discard,
.empty_tree = &empty_tree_oid_sha256,
.empty_blob = &empty_blob_oid_sha256,
.null_oid = &null_oid_sha256,
@@ -285,14 +309,7 @@ void git_hash_final_oid(struct object_id *oid, struct git_hash_ctx *ctx)
void git_hash_discard(struct git_hash_ctx *ctx)
{
- /*
- * XXX Many implementations do not need to do anything here,
- * and a dummy final() call is wasteful. But we can't fix
- * that unless our implementation API exposes a discard
- * primitive.
- */
- unsigned char dummy[GIT_MAX_RAWSZ];
- git_hash_final(dummy, ctx);
+ ctx->algop->discard_fn(ctx);
}
uint32_t hash_algo_by_name(const char *name)
diff --git a/hash.h b/hash.h
index 6b2f04e2a4..0a23ef4dfd 100644
--- a/hash.h
+++ b/hash.h
@@ -37,6 +37,7 @@
# define platform_SHA1_Clone_unsafe openssl_SHA1_Clone
# define platform_SHA1_Update_unsafe openssl_SHA1_Update
# define platform_SHA1_Final_unsafe openssl_SHA1_Final
+# define platform_SHA1_Discard_unsafe openssl_SHA1_Discard
# else
# define platform_SHA_CTX_unsafe SHA_CTX
# define platform_SHA1_Init_unsafe SHA1_Init
@@ -92,6 +93,7 @@
# define platform_SHA1_Final_unsafe platform_SHA1_Final
# ifdef platform_SHA1_Clone
# define platform_SHA1_Clone_unsafe platform_SHA1_Clone
+# define platform_SHA1_Discard_unsafe platform_SHA1_Discard
# endif
# ifdef SHA1_NEEDS_CLONE_HELPER
# define SHA1_NEEDS_CLONE_HELPER_UNSAFE
@@ -110,9 +112,11 @@
#ifdef platform_SHA1_Clone
#define git_SHA1_Clone platform_SHA1_Clone
+#define git_SHA1_Discard platform_SHA1_Discard
#endif
#ifdef platform_SHA1_Clone_unsafe
# define git_SHA1_Clone_unsafe platform_SHA1_Clone_unsafe
+# define git_SHA1_Discard_unsafe platform_SHA1_Discard_unsafe
#endif
#ifndef platform_SHA256_CTX
@@ -129,6 +133,7 @@
#ifdef platform_SHA256_Clone
#define git_SHA256_Clone platform_SHA256_Clone
+#define git_SHA256_Discard platform_SHA256_Discard
#endif
#ifdef SHA1_MAX_BLOCK_SIZE
@@ -142,20 +147,32 @@ static inline void git_SHA1_Clone(git_SHA_CTX *dst, const git_SHA_CTX *src)
{
memcpy(dst, src, sizeof(*dst));
}
+static inline void git_SHA1_Discard(git_SHA_CTX *ctx UNUSED)
+{
+ /* noop */
+}
#endif
#ifndef SHA1_NEEDS_CLONE_HELPER_UNSAFE
static inline void git_SHA1_Clone_unsafe(git_SHA_CTX_unsafe *dst,
const git_SHA_CTX_unsafe *src)
{
memcpy(dst, src, sizeof(*dst));
}
+static inline void git_SHA1_Discard_unsafe(git_SHA_CTX_unsafe *ctx UNUSED)
+{
+ /* noop */
+}
#endif
#ifndef SHA256_NEEDS_CLONE_HELPER
static inline void git_SHA256_Clone(git_SHA256_CTX *dst, const git_SHA256_CTX *src)
{
memcpy(dst, src, sizeof(*dst));
}
+static inline void git_SHA256_Discard(git_SHA256_CTX *ctx UNUSED)
+{
+ /* noop */
+}
#endif
/*
@@ -271,6 +288,7 @@ typedef void (*git_hash_clone_fn)(struct git_hash_ctx *dst, const struct git_has
typedef void (*git_hash_update_fn)(struct git_hash_ctx *ctx, const void *in, size_t len);
typedef void (*git_hash_final_fn)(unsigned char *hash, struct git_hash_ctx *ctx);
typedef void (*git_hash_final_oid_fn)(struct object_id *oid, struct git_hash_ctx *ctx);
+typedef void (*git_hash_discard_fn)(struct git_hash_ctx *ctx);
struct git_hash_algo {
/*
@@ -306,6 +324,9 @@ struct git_hash_algo {
/* The hash finalization function for object IDs. */
git_hash_final_oid_fn final_oid_fn;
+ /* Discard an initialized hash without finalizing. */
+ git_hash_discard_fn discard_fn;
+
/* The OID of the empty tree. */
const struct object_id *empty_tree;
diff --git a/sha1/openssl.h b/sha1/openssl.h
index 1038af47da..48deeb724a 100644
--- a/sha1/openssl.h
+++ b/sha1/openssl.h
@@ -40,12 +40,18 @@ static inline void openssl_SHA1_Clone(struct openssl_SHA1_CTX *dst,
EVP_MD_CTX_copy_ex(dst->ectx, src->ectx);
}
+static inline void openssl_SHA1_Discard(struct openssl_SHA1_CTX *ctx)
+{
+ EVP_MD_CTX_free(ctx->ectx);
+}
+
#ifndef platform_SHA_CTX
#define platform_SHA_CTX openssl_SHA1_CTX
#define platform_SHA1_Init openssl_SHA1_Init
#define platform_SHA1_Clone openssl_SHA1_Clone
#define platform_SHA1_Update openssl_SHA1_Update
#define platform_SHA1_Final openssl_SHA1_Final
+#define platform_SHA1_Discard openssl_SHA1_Discard
#endif
#endif /* SHA1_OPENSSL_H */
diff --git a/sha256/gcrypt.h b/sha256/gcrypt.h
index 694a2b70a1..d91ffe73d3 100644
--- a/sha256/gcrypt.h
+++ b/sha256/gcrypt.h
@@ -31,10 +31,16 @@ static inline void gcrypt_SHA256_Clone(gcrypt_SHA256_CTX *dst, const gcrypt_SHA2
gcry_md_copy(dst, *src);
}
+static inline void gcrypt_SHA256_Discard(gcrypt_SHA256_CTX *ctx)
+{
+ gcry_md_close(*ctx);
+}
+
#define platform_SHA256_CTX gcrypt_SHA256_CTX
#define platform_SHA256_Init gcrypt_SHA256_Init
#define platform_SHA256_Clone gcrypt_SHA256_Clone
#define platform_SHA256_Update gcrypt_SHA256_Update
#define platform_SHA256_Final gcrypt_SHA256_Final
+#define platform_SHA256_Discard gcrypt_SHA256_Discard
#endif
diff --git a/sha256/openssl.h b/sha256/openssl.h
index c1083d9491..3d457ca99d 100644
--- a/sha256/openssl.h
+++ b/sha256/openssl.h
@@ -40,10 +40,16 @@ static inline void openssl_SHA256_Clone(struct openssl_SHA256_CTX *dst,
EVP_MD_CTX_copy_ex(dst->ectx, src->ectx);
}
+static inline void openssl_SHA256_Discard(struct openssl_SHA256_CTX *ctx)
+{
+ EVP_MD_CTX_free(ctx->ectx);
+}
+
#define platform_SHA256_CTX openssl_SHA256_CTX
#define platform_SHA256_Init openssl_SHA256_Init
#define platform_SHA256_Clone openssl_SHA256_Clone
#define platform_SHA256_Update openssl_SHA256_Update
#define platform_SHA256_Final openssl_SHA256_Final
+#define platform_SHA256_Discard openssl_SHA256_Discard
#endif /* SHA256_OPENSSL_H */
--
2.55.0.418.g37da59dd42
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [PATCH 1/9] csum-file: drop discard_hashfile()
2026-07-02 7:57 ` [PATCH 1/9] csum-file: drop discard_hashfile() Jeff King
@ 2026-07-02 18:19 ` Junio C Hamano
2026-07-02 21:06 ` Jeff King
0 siblings, 1 reply; 20+ messages in thread
From: Junio C Hamano @ 2026-07-02 18:19 UTC (permalink / raw)
To: Jeff King; +Cc: git, Patrick Steinhardt
Jeff King <peff@peff.net> writes:
> So now we have two functions, discard_hashfile() and free_hashfile(),
> and we only need one. Which one do we want to keep?
>
> The only difference between them is that the discard variant also closes
> the descriptors held in the struct. Let's look at the three callers:
> ...
> Note that I said "descriptors" plural above. Those callers all care
> about the "fd" member of the struct. But discard_hashfile() also closes
> check_fd. That is only used if the struct is initialized with
> hashfd_check(), and neither of its two callers call either discard or
> free (they always "finalize" instead). So closing it is irrelevant for
> the current callers.
>
> I think we're better off sticking with the simpler free_hashfile()
> interface, and the handful of callers can decide how to handle the
> descriptors themselves.
Sonds good.
Our resident naming czar (already Cc'ed) may have preference about
the names and word order, though ;-)
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 1/9] csum-file: drop discard_hashfile()
2026-07-02 18:19 ` Junio C Hamano
@ 2026-07-02 21:06 ` Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
0 siblings, 1 reply; 20+ messages in thread
From: Jeff King @ 2026-07-02 21:06 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Patrick Steinhardt
On Thu, Jul 02, 2026 at 11:19:04AM -0700, Junio C Hamano wrote:
> Jeff King <peff@peff.net> writes:
>
> > So now we have two functions, discard_hashfile() and free_hashfile(),
> > and we only need one. Which one do we want to keep?
> >
> > The only difference between them is that the discard variant also closes
> > the descriptors held in the struct. Let's look at the three callers:
> > ...
> > Note that I said "descriptors" plural above. Those callers all care
> > about the "fd" member of the struct. But discard_hashfile() also closes
> > check_fd. That is only used if the struct is initialized with
> > hashfd_check(), and neither of its two callers call either discard or
> > free (they always "finalize" instead). So closing it is irrelevant for
> > the current callers.
> >
> > I think we're better off sticking with the simpler free_hashfile()
> > interface, and the handful of callers can decide how to handle the
> > descriptors themselves.
>
> Sonds good.
>
> Our resident naming czar (already Cc'ed) may have preference about
> the names and word order, though ;-)
Heh, yes, it should be hashfile_free() but that would require changing
the whole interface. We could do that on top, which might also be a good
time to do s/free/discard/ without worrying about a subtle behavior
change.
-Peff
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 1/9] csum-file: drop discard_hashfile()
2026-07-02 21:06 ` Jeff King
@ 2026-07-03 11:27 ` Patrick Steinhardt
0 siblings, 0 replies; 20+ messages in thread
From: Patrick Steinhardt @ 2026-07-03 11:27 UTC (permalink / raw)
To: Jeff King; +Cc: Junio C Hamano, git
On Thu, Jul 02, 2026 at 05:06:01PM -0400, Jeff King wrote:
> On Thu, Jul 02, 2026 at 11:19:04AM -0700, Junio C Hamano wrote:
>
> > Jeff King <peff@peff.net> writes:
> >
> > > So now we have two functions, discard_hashfile() and free_hashfile(),
> > > and we only need one. Which one do we want to keep?
> > >
> > > The only difference between them is that the discard variant also closes
> > > the descriptors held in the struct. Let's look at the three callers:
> > > ...
> > > Note that I said "descriptors" plural above. Those callers all care
> > > about the "fd" member of the struct. But discard_hashfile() also closes
> > > check_fd. That is only used if the struct is initialized with
> > > hashfd_check(), and neither of its two callers call either discard or
> > > free (they always "finalize" instead). So closing it is irrelevant for
> > > the current callers.
> > >
> > > I think we're better off sticking with the simpler free_hashfile()
> > > interface, and the handful of callers can decide how to handle the
> > > descriptors themselves.
> >
> > Sonds good.
> >
> > Our resident naming czar (already Cc'ed) may have preference about
> > the names and word order, though ;-)
>
> Heh, yes, it should be hashfile_free() but that would require changing
> the whole interface. We could do that on top, which might also be a good
> time to do s/free/discard/ without worrying about a subtle behavior
> change.
Heh :P
I think this being called a "free" function makes perfect sense, because
ultimately that's all we do here. So the semantics align with other free
functions.
We could of course fix the ordering while at it, but I don't want to
tack that onto this series. It already makes the codebase a better
place, so I'm happy enough.
Patrick
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 2/9] hash: add discard primitive
2026-07-02 7:59 ` [PATCH 2/9] hash: add discard primitive Jeff King
@ 2026-07-03 11:27 ` Patrick Steinhardt
0 siblings, 0 replies; 20+ messages in thread
From: Patrick Steinhardt @ 2026-07-03 11:27 UTC (permalink / raw)
To: Jeff King; +Cc: git
On Thu, Jul 02, 2026 at 03:59:53AM -0400, Jeff King wrote:
> diff --git a/hash.c b/hash.c
> index e925b9754e..63672a3d22 100644
> --- a/hash.c
> +++ b/hash.c
> @@ -283,6 +283,18 @@ void git_hash_final_oid(struct object_id *oid, struct git_hash_ctx *ctx)
> ctx->algop->final_oid_fn(oid, ctx);
> }
>
> +void git_hash_discard(struct git_hash_ctx *ctx)
As the resident naming czar: shouldn't this rather be called
`git_hash_release()`?
Patrick
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 4/9] csum-file: provide a function to release checkpoints
2026-07-02 8:03 ` [PATCH 4/9] csum-file: provide a function to release checkpoints Jeff King
@ 2026-07-03 11:27 ` Patrick Steinhardt
0 siblings, 0 replies; 20+ messages in thread
From: Patrick Steinhardt @ 2026-07-03 11:27 UTC (permalink / raw)
To: Jeff King; +Cc: git
On Thu, Jul 02, 2026 at 04:03:19AM -0400, Jeff King wrote:
> A hashfile_checkpoint struct is basically just a copy of the hash_ctx
> state at a given point in the file. As such, it contains its own
> git_hash_ctx which may (depending on the underlying hash implementation)
> need to be discarded when we're done with it.
>
> Let's add a "release" function which cleans up the hash context it
> holds. I chose "release" here and not "discard" because you'd use this
> to clean up every checkpoint, whether you used it or not. As opposed to
> git_hash_discard(), which is needed only if you didn't call
> git_hash_final().
Okay, I was wondering about that a bit. With this explanation I'm also
somewhat fine with the `git_hash_discard()` name. It's still a function
that has release semantics, but you want to convey more intent than
that.
One thing I was wondering: is it safe to have a `git_hash_discard()`
that is being called on a potentially-already-discarded hash? If so, we
wouldn't have to discern whether the hash context was used successfully
or not.
Patrick
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 7/9] http: discard hash in dumb-http http_object_request
2026-07-02 8:07 ` [PATCH 7/9] http: discard hash in dumb-http http_object_request Jeff King
@ 2026-07-03 11:27 ` Patrick Steinhardt
2026-07-03 13:47 ` brian m. carlson
2026-07-06 0:01 ` Jeff King
0 siblings, 2 replies; 20+ messages in thread
From: Patrick Steinhardt @ 2026-07-03 11:27 UTC (permalink / raw)
To: Jeff King; +Cc: git
On Thu, Jul 02, 2026 at 04:07:07AM -0400, Jeff King wrote:
> The flag handling could be removed if the hash-discard function were
> idempotent. This could be done easily-ish by having the underlying
> hash functions (like the ones in sha256/openssl.h) set the context
> pointer to NULL after free-ing. But it's something that every platform
> implementation would have to remember to do, and the benefit for the
> callers is not that huge (it would let us shave a few lines here and
> probably in a few other spots).
This answers an earlier question of mine. It would indeed be great if it
was idempotent -- I've been bitten by interfaces like this once too
much, where you have to be very careful to manage the lifetime of a
specific object. The prime example of this are (were? I don't quite
recall whether we fixed that interface) reference transactions, and that
caused a bunch of bugs in the past.
Patrick
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 7/9] http: discard hash in dumb-http http_object_request
2026-07-03 11:27 ` Patrick Steinhardt
@ 2026-07-03 13:47 ` brian m. carlson
2026-07-06 0:01 ` Jeff King
1 sibling, 0 replies; 20+ messages in thread
From: brian m. carlson @ 2026-07-03 13:47 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Jeff King, git
[-- Attachment #1: Type: text/plain, Size: 1410 bytes --]
On 2026-07-03 at 11:27:36, Patrick Steinhardt wrote:
> On Thu, Jul 02, 2026 at 04:07:07AM -0400, Jeff King wrote:
> > The flag handling could be removed if the hash-discard function were
> > idempotent. This could be done easily-ish by having the underlying
> > hash functions (like the ones in sha256/openssl.h) set the context
> > pointer to NULL after free-ing. But it's something that every platform
> > implementation would have to remember to do, and the benefit for the
> > callers is not that huge (it would let us shave a few lines here and
> > probably in a few other spots).
>
> This answers an earlier question of mine. It would indeed be great if it
> was idempotent -- I've been bitten by interfaces like this once too
> much, where you have to be very careful to manage the lifetime of a
> specific object. The prime example of this are (were? I don't quite
> recall whether we fixed that interface) reference transactions, and that
> caused a bunch of bugs in the past.
Yes, that would be fantastic. The Rust code will need a few fixes as
well (which I will send on top of this one when it's picked up) and it
really simplifies our Drop implementation if I can just do
`git_hash_discard`. Otherwise, I need to keep track of whether we've
already called one of the final functions or not to avoid a double free.
--
brian m. carlson (they/them)
Toronto, Ontario, CA
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 7/9] http: discard hash in dumb-http http_object_request
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-03 13:47 ` brian m. carlson
@ 2026-07-06 0:01 ` Jeff King
2026-07-06 0:44 ` Jeff King
2026-07-06 6:16 ` Patrick Steinhardt
1 sibling, 2 replies; 20+ messages in thread
From: Jeff King @ 2026-07-06 0:01 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: brian m. carlson, git
On Fri, Jul 03, 2026 at 01:27:36PM +0200, Patrick Steinhardt wrote:
> On Thu, Jul 02, 2026 at 04:07:07AM -0400, Jeff King wrote:
> > The flag handling could be removed if the hash-discard function were
> > idempotent. This could be done easily-ish by having the underlying
> > hash functions (like the ones in sha256/openssl.h) set the context
> > pointer to NULL after free-ing. But it's something that every platform
> > implementation would have to remember to do, and the benefit for the
> > callers is not that huge (it would let us shave a few lines here and
> > probably in a few other spots).
>
> This answers an earlier question of mine. It would indeed be great if it
> was idempotent -- I've been bitten by interfaces like this once too
> much, where you have to be very careful to manage the lifetime of a
> specific object. The prime example of this are (were? I don't quite
> recall whether we fixed that interface) reference transactions, and that
> caused a bunch of bugs in the past.
There are three tricky points I found while thinking about this.
First: how and when do we decide to skip a discard? For most
implementations (like sha1dc), it's always a noop, and we can ignore it.
For OpenSSL, we could be setting ctx->ectx to NULL. But for gcrypt we
typedef their opaque structure directly. So we'd have to push that down
into its own struct and add an "active" flag.
That's not too bad, but it does put the burden on each backend. Instead,
we could keep a flag in the top-level ctx like this:
diff --git a/hash.c b/hash.c
index 55d1d41770..f4c451b20a 100644
--- a/hash.c
+++ b/hash.c
@@ -285,6 +285,7 @@ void git_hash_free(struct git_hash_ctx *ctx)
void git_hash_init(struct git_hash_ctx *ctx, const struct git_hash_algo *algop)
{
algop->init_fn(ctx);
+ ctx->active = 1;
}
void git_hash_clone(struct git_hash_ctx *dst, const struct git_hash_ctx *src)
@@ -300,16 +301,19 @@ void git_hash_update(struct git_hash_ctx *ctx, const void *in, size_t len)
void git_hash_final(unsigned char *hash, struct git_hash_ctx *ctx)
{
ctx->algop->final_fn(hash, ctx);
+ ctx->active = 0;
}
void git_hash_final_oid(struct object_id *oid, struct git_hash_ctx *ctx)
{
ctx->algop->final_oid_fn(oid, ctx);
+ ctx->active = 0;
}
void git_hash_discard(struct git_hash_ctx *ctx)
{
- ctx->algop->discard_fn(ctx);
+ if (ctx->active)
+ ctx->algop->discard_fn(ctx);
}
uint32_t hash_algo_by_name(const char *name)
diff --git a/hash.h b/hash.h
index 0a23ef4dfd..2840f20793 100644
--- a/hash.h
+++ b/hash.h
@@ -281,6 +281,7 @@ struct git_hash_ctx {
git_SHA_CTX_unsafe sha1_unsafe;
git_SHA256_CTX sha256;
} state;
+ bool active;
};
typedef void (*git_hash_init_fn)(struct git_hash_ctx *ctx);
That nicely puts the responsibility in a single place, but now we have
the opposite problem: what if somebody calls algop->final_fn() directly?
Then the flag gets out of sync with the underlying state. There are two
such calls currently, in submodule--helper.c and test-synthesize.c.
AFAICT there is no reason they could not just use git_hash_final().
Maybe it would be enough to fix that spot and comment the algo function
pointers to warn people away from using them directly.
That by itself is enough to make:
algo->init_fn(&ctx);
git_hash_update(&ctx, ...);
git_hash_final(out, &ctx);
...
git_hash_discard(&ctx);
safe.
The second issue is related: what should we do in other functions when
the active flag is not set? For example, what should this do:
algo->init_fn(&ctx);
git_hash_update(&ctx, ...);
git_hash_final(out, &ctx);
git_hash_update(&ctx, ...);
git_hash_final(out, &ctx);
In the second git_hash_update() call, there are two obvious options:
1. It should do nothing; there is no active context to add to.
2. It should automatically re-init the context (using the algo from
the previous init) and add the data.
The second final() call has the added bonus that it returns data, but I
think there are two matching options:
1. It should do nothing, and hashclr() the output (leaving it
uninitialized just seems insane).
2. It should automatically re-init the context (assuming there was not
already an update() call that did so). And then I guess return
whatever hash that particular algo generates for the empty string?
Those all seem reasonable-ish to me and give a defined output at every
moment (which is better than crashing). But it kind of feels like they'd
be papering over potential bugs. Maybe crashing _is_ better (we don't do
so reliably now, but a BUG() could make sense).
And the third is related: do we check the active flag when initializing?
Right now the answer must be "no", because the point of the init
function is that the input is potentially garbage. But that means
something like:
struct git_hash_ctx ctx;
algo->init_fn(&ctx);
algo->init_fn(&ctx);
leaks. That's maybe OK in practice. We could do something more like:
struct git_hash_ctx = HASH_CTX_INIT;
git_hash_start(&ctx, algo);
where the INIT step doesn't actually allocate anything, and start() is
the moment where you must promise to call final() or discard(). And then
it would be OK for start() to BUG() when the active flag is already set.
That was maybe more than you wanted to read about the topic. But if the
request is for safer object lifetimes in general, then I think there are
a lot of details about what that means.
If we are going to do anything, I'd be inclined to stop mostly after the
diff I showed above. That's the only thing I've seen that would simplify
existing code. The rest are mostly hypotheticals, but since Rust was
mentioned, I wondered if you're trying to shoot for something safer.
At any rate, I would prefer to do any of this on top of the series I
posted. I took care there to avoid double-calling final()/discard(),
which could now be simplified away. But I think I'd rather see that
simplification its own step.
-Peff
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [PATCH 7/9] http: discard hash in dumb-http http_object_request
2026-07-06 0:01 ` Jeff King
@ 2026-07-06 0:44 ` Jeff King
2026-07-06 6:16 ` Patrick Steinhardt
1 sibling, 0 replies; 20+ messages in thread
From: Jeff King @ 2026-07-06 0:44 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: brian m. carlson, git
On Sun, Jul 05, 2026 at 08:01:05PM -0400, Jeff King wrote:
> That by itself is enough to make:
>
> algo->init_fn(&ctx);
> git_hash_update(&ctx, ...);
> git_hash_final(out, &ctx);
> ...
> git_hash_discard(&ctx);
>
> safe.
Actually, that's not quite true. Setting the active flag happens in
git_hash_init() in that model. But many callers use algo->init_fn()
directly instead. They'd all need to be adjusted to use git_hash_init().
I don't think there's any reason they should avoid it, and it's mostly
from inertia that they use the bare function pointer.
-Peff
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH 7/9] http: discard hash in dumb-http http_object_request
2026-07-06 0:01 ` Jeff King
2026-07-06 0:44 ` Jeff King
@ 2026-07-06 6:16 ` Patrick Steinhardt
1 sibling, 0 replies; 20+ messages in thread
From: Patrick Steinhardt @ 2026-07-06 6:16 UTC (permalink / raw)
To: Jeff King; +Cc: brian m. carlson, git
On Sun, Jul 05, 2026 at 08:01:05PM -0400, Jeff King wrote:
> On Fri, Jul 03, 2026 at 01:27:36PM +0200, Patrick Steinhardt wrote:
> > On Thu, Jul 02, 2026 at 04:07:07AM -0400, Jeff King wrote:
[snip]
> The second issue is related: what should we do in other functions when
> the active flag is not set? For example, what should this do:
>
> algo->init_fn(&ctx);
>
> git_hash_update(&ctx, ...);
> git_hash_final(out, &ctx);
>
> git_hash_update(&ctx, ...);
> git_hash_final(out, &ctx);
>
> In the second git_hash_update() call, there are two obvious options:
>
> 1. It should do nothing; there is no active context to add to.
>
> 2. It should automatically re-init the context (using the algo from
> the previous init) and add the data.
Or 3rd: we `BUG()` when any of the functions is called on an
uninitialized context. That to me feels like the most sensible solution.
> The second final() call has the added bonus that it returns data, but I
> think there are two matching options:
>
> 1. It should do nothing, and hashclr() the output (leaving it
> uninitialized just seems insane).
>
> 2. It should automatically re-init the context (assuming there was not
> already an update() call that did so). And then I guess return
> whatever hash that particular algo generates for the empty string?
>
> Those all seem reasonable-ish to me and give a defined output at every
> moment (which is better than crashing). But it kind of feels like they'd
> be papering over potential bugs. Maybe crashing _is_ better (we don't do
> so reliably now, but a BUG() could make sense).
Yes, agreed.
> And the third is related: do we check the active flag when initializing?
> Right now the answer must be "no", because the point of the init
> function is that the input is potentially garbage. But that means
> something like:
>
> struct git_hash_ctx ctx;
> algo->init_fn(&ctx);
> algo->init_fn(&ctx);
>
> leaks. That's maybe OK in practice. We could do something more like:
>
> struct git_hash_ctx = HASH_CTX_INIT;
> git_hash_start(&ctx, algo);
>
> where the INIT step doesn't actually allocate anything, and start() is
> the moment where you must promise to call final() or discard(). And then
> it would be OK for start() to BUG() when the active flag is already set.
I'd say being as strict as possible is the best way to go until we find
a case where it makes sense to be less strict.
> That was maybe more than you wanted to read about the topic. But if the
> request is for safer object lifetimes in general, then I think there are
> a lot of details about what that means.
>
> If we are going to do anything, I'd be inclined to stop mostly after the
> diff I showed above. That's the only thing I've seen that would simplify
> existing code. The rest are mostly hypotheticals, but since Rust was
> mentioned, I wondered if you're trying to shoot for something safer.
>
> At any rate, I would prefer to do any of this on top of the series I
> posted. I took care there to avoid double-calling final()/discard(),
> which could now be simplified away. But I think I'd rather see that
> simplification its own step.
Fully agreed.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-07-06 6:16 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-02 7:52 [PATCH 0/9] hash algorithm leak fixes Jeff King
2026-07-02 7:57 ` [PATCH 1/9] csum-file: drop discard_hashfile() Jeff King
2026-07-02 18:19 ` Junio C Hamano
2026-07-02 21:06 ` Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-02 7:59 ` [PATCH 2/9] hash: add discard primitive Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-02 8:01 ` [PATCH 3/9] csum-file: always finalize or discard hash Jeff King
2026-07-02 8:03 ` [PATCH 4/9] csum-file: provide a function to release checkpoints Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-02 8:04 ` [PATCH 5/9] patch-id: discard hash when done Jeff King
2026-07-02 8:05 ` [PATCH 6/9] check_stream_oid(): discard hash on read error Jeff King
2026-07-02 8:07 ` [PATCH 7/9] http: discard hash in dumb-http http_object_request Jeff King
2026-07-03 11:27 ` Patrick Steinhardt
2026-07-03 13:47 ` brian m. carlson
2026-07-06 0:01 ` Jeff King
2026-07-06 0:44 ` Jeff King
2026-07-06 6:16 ` Patrick Steinhardt
2026-07-02 8:09 ` [PATCH 8/9] hash: fix memory leak copying sha256 gcrypt handles Jeff King
2026-07-02 8:13 ` [PATCH 9/9] hash: add platform-specific discard functions Jeff King
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox