diff mbox series

[14/28] http: fix leak of http_object_request struct

Message ID 20240924220109.GN1143820@coredump.intra.peff.net (mailing list archive)
State Accepted
Commit a1bc3c88de1526c83882143b2e47400f7e3ee4b2
Headers show
Series leak fixes for http fetch/push | expand

Commit Message

Jeff King Sept. 24, 2024, 10:01 p.m. UTC
The new_http_object_request() function allocates a struct on the heap,
along with some fields inside the struct. But the matching function to
clean it up, release_http_object_request(), only frees the interior
fields without freeing the struct itself, causing a leak.

The related http_pack_request new/release pair gets this right, and at
first glance we should be able to do the same thing and just add a
single free() call. But there's a catch.

These http_object_request structs are typically embedded in the
object_request struct of http-walker.c. And when we clean up that parent
struct, it sanity-checks the embedded struct to make sure we are not
leaking descriptors. Which means a use-after-free if we simply free()
the embedded struct.

I have no idea how valuable that sanity-check is, or whether it can
simply be deleted. This all goes back to 5424bc557f (http*: add helper
methods for fetching objects (loose), 2009-06-06). But the obvious way
to make it all work is to be sure we set the pointer to NULL after
freeing it (and our freeing process closes the descriptor, so we know
there is no leak).

To make sure we do that consistently, we'll switch the pointer we take
in release_http_object_request() to a pointer-to-pointer, and we'll set
it to NULL ourselves. And then the compiler can help us find each caller
which needs to be updated.

Most cases will just pass "&obj_req->req", which will obviously do the
right thing. In a few cases, like http-push's finish_request(), we are
working with a copy of the pointer, so we don't NULL the original. But
it's OK because the next step is to free the struct containing the
original pointer anyway.

This lets us mark t5551 as leak-free. Ironically this is the "smart"
http test, and the leak here only affects dumb http. But there's a
single dumb-http invocation in there. The full dumb tests are in t5550,
which still has some more leaks.

This also makes t5559 leak-free, as it's just an HTTP/2 variant of
t5551. But we don't need to mark it as such, since it inherits the flag
from t5551.

Signed-off-by: Jeff King <peff@peff.net>
---
 http-push.c                 |  4 ++--
 http-walker.c               |  8 ++++----
 http.c                      | 11 ++++++++---
 http.h                      |  4 ++--
 t/t5551-http-fetch-smart.sh |  1 +
 5 files changed, 17 insertions(+), 11 deletions(-)

Comments

Patrick Steinhardt Sept. 26, 2024, 1:50 p.m. UTC | #1
On Tue, Sep 24, 2024 at 06:01:09PM -0400, Jeff King wrote:
> The new_http_object_request() function allocates a struct on the heap,
> along with some fields inside the struct. But the matching function to
> clean it up, release_http_object_request(), only frees the interior
> fields without freeing the struct itself, causing a leak.

Oh yeah, I remember staring at this code and being completely confused
as to how this all works.

> diff --git a/http.c b/http.c
> index cc136408c0..d0242ffb50 100644
> --- a/http.c
> +++ b/http.c
> @@ -2816,15 +2816,17 @@ int finish_http_object_request(struct http_object_request *freq)
>  	return freq->rename;
>  }
>  
> -void abort_http_object_request(struct http_object_request *freq)
> +void abort_http_object_request(struct http_object_request **freq_p)
>  {
> +	struct http_object_request *freq = *freq_p;
>  	unlink_or_warn(freq->tmpfile.buf);
>  
> -	release_http_object_request(freq);
> +	release_http_object_request(freq_p);
>  }
>  
> -void release_http_object_request(struct http_object_request *freq)
> +void release_http_object_request(struct http_object_request **freq_p)
>  {
> +	struct http_object_request *freq = *freq_p;
>  	if (freq->localfile != -1) {
>  		close(freq->localfile);
>  		freq->localfile = -1;
> @@ -2838,4 +2840,7 @@ void release_http_object_request(struct http_object_request *freq)
>  	}
>  	curl_slist_free_all(freq->headers);
>  	strbuf_release(&freq->tmpfile);
> +
> +	free(freq);
> +	*freq_p = NULL;
>  }

Okay, looks simple enough. But I found the whole code in "http.c" to be
quite... elusive, so I had a hard time finding my way.

Patrick
Jeff King Sept. 27, 2024, 3:50 a.m. UTC | #2
On Thu, Sep 26, 2024 at 03:50:17PM +0200, Patrick Steinhardt wrote:

> Okay, looks simple enough. But I found the whole code in "http.c" to be
> quite... elusive, so I had a hard time finding my way.

You are not the only one. ;)

-Peff
diff mbox series

Patch

diff --git a/http-push.c b/http-push.c
index 7315a694aa..7196ffa525 100644
--- a/http-push.c
+++ b/http-push.c
@@ -275,7 +275,7 @@  static void start_fetch_loose(struct transfer_request *request)
 	if (!start_active_slot(slot)) {
 		fprintf(stderr, "Unable to start GET request\n");
 		repo->can_update_info_refs = 0;
-		release_http_object_request(obj_req);
+		release_http_object_request(&obj_req);
 		release_request(request);
 	}
 }
@@ -580,7 +580,7 @@  static void finish_request(struct transfer_request *request)
 
 		/* Try fetching packed if necessary */
 		if (request->obj->flags & LOCAL) {
-			release_http_object_request(obj_req);
+			release_http_object_request(&obj_req);
 			release_request(request);
 		} else
 			start_fetch_packed(request);
diff --git a/http-walker.c b/http-walker.c
index e417a7f51c..9c1e5c37e6 100644
--- a/http-walker.c
+++ b/http-walker.c
@@ -74,7 +74,7 @@  static void start_object_request(struct object_request *obj_req)
 	obj_req->state = ACTIVE;
 	if (!start_active_slot(slot)) {
 		obj_req->state = ABORTED;
-		release_http_object_request(req);
+		release_http_object_request(&req);
 		return;
 	}
 }
@@ -110,7 +110,7 @@  static void process_object_response(void *callback_data)
 		if (obj_req->repo->next) {
 			obj_req->repo =
 				obj_req->repo->next;
-			release_http_object_request(obj_req->req);
+			release_http_object_request(&obj_req->req);
 			start_object_request(obj_req);
 			return;
 		}
@@ -495,7 +495,7 @@  static int fetch_object(struct walker *walker, unsigned char *hash)
 
 	if (repo_has_object_file(the_repository, &obj_req->oid)) {
 		if (obj_req->req)
-			abort_http_object_request(obj_req->req);
+			abort_http_object_request(&obj_req->req);
 		abort_object_request(obj_req);
 		return 0;
 	}
@@ -543,7 +543,7 @@  static int fetch_object(struct walker *walker, unsigned char *hash)
 		strbuf_release(&buf);
 	}
 
-	release_http_object_request(req);
+	release_http_object_request(&obj_req->req);
 	release_object_request(obj_req);
 	return ret;
 }
diff --git a/http.c b/http.c
index cc136408c0..d0242ffb50 100644
--- a/http.c
+++ b/http.c
@@ -2816,15 +2816,17 @@  int finish_http_object_request(struct http_object_request *freq)
 	return freq->rename;
 }
 
-void abort_http_object_request(struct http_object_request *freq)
+void abort_http_object_request(struct http_object_request **freq_p)
 {
+	struct http_object_request *freq = *freq_p;
 	unlink_or_warn(freq->tmpfile.buf);
 
-	release_http_object_request(freq);
+	release_http_object_request(freq_p);
 }
 
-void release_http_object_request(struct http_object_request *freq)
+void release_http_object_request(struct http_object_request **freq_p)
 {
+	struct http_object_request *freq = *freq_p;
 	if (freq->localfile != -1) {
 		close(freq->localfile);
 		freq->localfile = -1;
@@ -2838,4 +2840,7 @@  void release_http_object_request(struct http_object_request *freq)
 	}
 	curl_slist_free_all(freq->headers);
 	strbuf_release(&freq->tmpfile);
+
+	free(freq);
+	*freq_p = NULL;
 }
diff --git a/http.h b/http.h
index a516ca4a9a..46e334c2c2 100644
--- a/http.h
+++ b/http.h
@@ -240,8 +240,8 @@  struct http_object_request *new_http_object_request(
 	const char *base_url, const struct object_id *oid);
 void process_http_object_request(struct http_object_request *freq);
 int finish_http_object_request(struct http_object_request *freq);
-void abort_http_object_request(struct http_object_request *freq);
-void release_http_object_request(struct http_object_request *freq);
+void abort_http_object_request(struct http_object_request **freq);
+void release_http_object_request(struct http_object_request **freq);
 
 /*
  * Instead of using environment variables to determine if curl tracing happens,
diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
index 7b5ab0eae1..e36dfde17e 100755
--- a/t/t5551-http-fetch-smart.sh
+++ b/t/t5551-http-fetch-smart.sh
@@ -5,6 +5,7 @@  test_description="test smart fetching over http via http-backend ($HTTP_PROTO)"
 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
+TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 . "$TEST_DIRECTORY"/lib-httpd.sh
 test "$HTTP_PROTO" = "HTTP/2" && enable_http2