* [PATCH v3 0/2] fix resource leaks in test helpers
@ 2025-07-22 17:41 Hoyoung Lee
2025-07-22 17:41 ` [PATCH v3 1/2] t/helper/test-truncate: close file descriptor after truncation Hoyoung Lee
2025-07-22 17:41 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Hoyoung Lee
0 siblings, 2 replies; 18+ messages in thread
From: Hoyoung Lee @ 2025-07-22 17:41 UTC (permalink / raw)
To: git; +Cc: Hoyoung Lee
Thanks for the detailed and thoughtful reviews. Your comments helped me better understand how resource cleanup should be handled, especially when dealing with early program termination.
I especially appreciate your in-depth explanation regarding the difference between true resource leaks and stack-unwinding cases. The example you gave was very insightful, and it clarified how automated tools and reviewers should assess whether a resource is truly leaked. Thank you for taking the time to provide such a clear and instructive explanation.
Hoyoung Lee (2):
t/helper/test-truncate: close file descriptor after truncation
t/helper/test-delta: fix possible resource leak and ensure safe
cleanup
t/helper/test-delta.c | 9 +++++----
t/helper/test-truncate.c | 3 +++
2 files changed, 8 insertions(+), 4 deletions(-)
--
2.34.1
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH v3 1/2] t/helper/test-truncate: close file descriptor after truncation
2025-07-22 17:41 [PATCH v3 0/2] fix resource leaks in test helpers Hoyoung Lee
@ 2025-07-22 17:41 ` Hoyoung Lee
2025-07-22 21:48 ` Junio C Hamano
2025-07-22 17:41 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Hoyoung Lee
1 sibling, 1 reply; 18+ messages in thread
From: Hoyoung Lee @ 2025-07-22 17:41 UTC (permalink / raw)
To: git; +Cc: Hoyoung Lee
Fix a resource leak where the file descriptor was not closed after
truncating a file in t/helper/test-truncate.c.
Signed-off-by: Hoyoung Lee <lhywkd22@gmail.com>
---
t/helper/test-truncate.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/t/helper/test-truncate.c b/t/helper/test-truncate.c
index 3931deaec7..104bc36cc0 100644
--- a/t/helper/test-truncate.c
+++ b/t/helper/test-truncate.c
@@ -21,5 +21,8 @@ int cmd__truncate(int argc, const char **argv)
if (ftruncate(fd, (off_t) sz) < 0)
die_errno("failed to truncate file");
+
+ close(fd);
+
return 0;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-22 17:41 [PATCH v3 0/2] fix resource leaks in test helpers Hoyoung Lee
2025-07-22 17:41 ` [PATCH v3 1/2] t/helper/test-truncate: close file descriptor after truncation Hoyoung Lee
@ 2025-07-22 17:41 ` Hoyoung Lee
2025-07-23 7:28 ` Eric Sunshine
1 sibling, 1 reply; 18+ messages in thread
From: Hoyoung Lee @ 2025-07-22 17:41 UTC (permalink / raw)
To: git; +Cc: Hoyoung Lee
Initialize `fd` to -1 and unify all `open()`-related `close()` calls
under a single cleanup label. This prevents undefined behavior when
`fd` is used without initialization in error paths.
The cleanup logic now safely avoids calling `close()` on invalid
descriptors and ensures consistent resource management.
Signed-off-by: Hoyoung Lee <lhywkd22@gmail.com>
---
t/helper/test-delta.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/t/helper/test-delta.c b/t/helper/test-delta.c
index 6bc787a474..f5811e96ad 100644
--- a/t/helper/test-delta.c
+++ b/t/helper/test-delta.c
@@ -17,7 +17,7 @@ static const char usage_str[] =
int cmd__delta(int argc, const char **argv)
{
- int fd;
+ int fd = -1;
struct stat st;
void *from_buf = NULL, *data_buf = NULL, *out_buf = NULL;
unsigned long from_size, data_size, out_size;
@@ -31,13 +31,12 @@ int cmd__delta(int argc, const char **argv)
fd = open(argv[2], O_RDONLY);
if (fd < 0 || fstat(fd, &st)) {
perror(argv[2]);
- return 1;
+ goto cleanup;
}
from_size = st.st_size;
from_buf = xmalloc(from_size);
if (read_in_full(fd, from_buf, from_size) < 0) {
perror(argv[2]);
- close(fd);
goto cleanup;
}
close(fd);
@@ -51,7 +50,6 @@ int cmd__delta(int argc, const char **argv)
data_buf = xmalloc(data_size);
if (read_in_full(fd, data_buf, data_size) < 0) {
perror(argv[3]);
- close(fd);
goto cleanup;
}
close(fd);
@@ -81,5 +79,8 @@ int cmd__delta(int argc, const char **argv)
free(data_buf);
free(out_buf);
+ if (fd >= 0)
+ close(fd);
+
return ret;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [PATCH v3 1/2] t/helper/test-truncate: close file descriptor after truncation
2025-07-22 17:41 ` [PATCH v3 1/2] t/helper/test-truncate: close file descriptor after truncation Hoyoung Lee
@ 2025-07-22 21:48 ` Junio C Hamano
0 siblings, 0 replies; 18+ messages in thread
From: Junio C Hamano @ 2025-07-22 21:48 UTC (permalink / raw)
To: Hoyoung Lee; +Cc: git
Hoyoung Lee <lhywkd22@gmail.com> writes:
> Fix a resource leak where the file descriptor was not closed after
> truncating a file in t/helper/test-truncate.c.
>
> Signed-off-by: Hoyoung Lee <lhywkd22@gmail.com>
> ---
> t/helper/test-truncate.c | 3 +++
> 1 file changed, 3 insertions(+)
While it is not wrong per-se, it is not like a function that can
potentially be called number of times returns without closing it.
Nobody other than the main() function would be calling this
function, so the only thing that is done after this function returns
to its caller is for the caller to return leading to exit from the
process at which point the leftover file descriptor would be
closed.
So I am a bit curious what triggered you to send in these changes.
Are there some automated resource leak checker, without fixes like
these whose output would be noisy with complaints about these "known
leaking, no ill effects in practice, yet gets flagged by checkers"
code to be useful, or something?
Will queue; thanks.
> diff --git a/t/helper/test-truncate.c b/t/helper/test-truncate.c
> index 3931deaec7..104bc36cc0 100644
> --- a/t/helper/test-truncate.c
> +++ b/t/helper/test-truncate.c
> @@ -21,5 +21,8 @@ int cmd__truncate(int argc, const char **argv)
>
> if (ftruncate(fd, (off_t) sz) < 0)
> die_errno("failed to truncate file");
> +
> + close(fd);
> +
> return 0;
> }
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-22 17:41 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Hoyoung Lee
@ 2025-07-23 7:28 ` Eric Sunshine
2025-07-23 7:55 ` Jeff King
2025-07-23 16:44 ` Junio C Hamano
0 siblings, 2 replies; 18+ messages in thread
From: Eric Sunshine @ 2025-07-23 7:28 UTC (permalink / raw)
To: Hoyoung Lee; +Cc: git
On Tue, Jul 22, 2025 at 1:41 PM Hoyoung Lee <lhywkd22@gmail.com> wrote:
> Initialize `fd` to -1 and unify all `open()`-related `close()` calls
> under a single cleanup label. This prevents undefined behavior when
> `fd` is used without initialization in error paths.
It's not clear what this means. As far as I can tell, the original
code never used an uninitialized `fd` in error paths.
> The cleanup logic now safely avoids calling `close()` on invalid
> descriptors and ensures consistent resource management.
Again, it's not clear what this means. Although your previous version
of this patch did add a call to close() with an invalid descriptor,
the original code did not do so. So, the above statement seems to be
misleading.
Those issues aside, the patch itself has problems, some minor, such as
making the code a bit confusing or misleading, and some major, such as
calling close() on an already closed descriptor.
Unfortunately, there are enough important context lines missing from
the patch itself that, instead of directly reviewing the patch
directly, I'm going to review the code following the application of
your patch...
> int fd = -1;
This new initialization (-1) is useless because...
> if (argc != 5 || (strcmp(argv[1], "-d") && strcmp(argv[1], "-p"))) {
> fprintf(stderr, "usage: %s\n", usage_str);
> return 1;
> }
>
> fd = open(argv[2], O_RDONLY);
...the very first time `fd` is mentioned (aside from the declaration)
is here where it is unconditionally assigned a value. Thus, the -1
initialization is wasted (and potentially confusing for readers).
> if (fd < 0 || fstat(fd, &st)) {
> perror(argv[2]);
> goto cleanup;
> }
Okay, no problem here. The `if (fd >= 0) close(fd)` you added to the
"cleanup" action handles both the cases here when `fd` might be
negative after the open() call or a valid descriptor.
> from_size = st.st_size;
> from_buf = xmalloc(from_size);
> if (read_in_full(fd, from_buf, from_size) < 0) {
> perror(argv[2]);
> goto cleanup;
> }
> close(fd);
Here `fd` is closed manually which is good because...
> fd = open(argv[3], O_RDONLY);
...this code immediately assigns it a new value.
> if (fd < 0 || fstat(fd, &st)) {
> perror(argv[3]);
> goto cleanup;
> }
Okay for the same reason mentioned above.
> data_size = st.st_size;
> data_buf = xmalloc(data_size);
> if (read_in_full(fd, data_buf, data_size) < 0) {
> perror(argv[3]);
> goto cleanup;
> }
> close(fd);
The descriptor is closed manually (again) because a subsequent open()
call is going to reuse the variable. However...
> if (argv[1][1] == 'd')
> out_buf = diff_delta(from_buf, from_size,
> data_buf, data_size,
> &out_size, 0);
> else
> out_buf = patch_delta(from_buf, from_size,
> data_buf, data_size,
> &out_size);
> if (!out_buf) {
> fprintf(stderr, "delta operation failed (returned NULL)\n");
> goto cleanup;
> }
...although `fd` was closed, it still holds the previously-open
non-negative file descriptor, which means that this `goto cleanup`...
> fd = open (argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
> if (fd < 0 || write_in_full(fd, out_buf, out_size) < 0) {
> perror(argv[4]);
> goto cleanup;
> }
>
> ret = 0;
> cleanup:
> free(from_buf);
> free(data_buf);
> free(out_buf);
>
> if (fd >= 0)
> close(fd);
...will arrive here and the condition will evaluate to "true",
resulting in the already-closed descriptor being closed again.
> return ret;
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 7:28 ` Eric Sunshine
@ 2025-07-23 7:55 ` Jeff King
2025-07-23 8:06 ` Jeff King
` (2 more replies)
2025-07-23 16:44 ` Junio C Hamano
1 sibling, 3 replies; 18+ messages in thread
From: Jeff King @ 2025-07-23 7:55 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
On Wed, Jul 23, 2025 at 03:28:05AM -0400, Eric Sunshine wrote:
> > close(fd);
>
> The descriptor is closed manually (again) because a subsequent open()
> call is going to reuse the variable. However...
>
> > if (argv[1][1] == 'd')
> > out_buf = diff_delta(from_buf, from_size,
> > data_buf, data_size,
> > &out_size, 0);
> > else
> > out_buf = patch_delta(from_buf, from_size,
> > data_buf, data_size,
> > &out_size);
> > if (!out_buf) {
> > fprintf(stderr, "delta operation failed (returned NULL)\n");
> > goto cleanup;
> > }
>
> ...although `fd` was closed, it still holds the previously-open
> non-negative file descriptor, which means that this `goto cleanup`...
Oof, good catch. This iteration of the patch was based on my suggestion,
but I didn't notice the jump to cleanup between that close/open pair.
I wonder if it would be more clear written as:
int from_fd = -1;
int data_fd = -1;
int out_fd = -1;
...
from_fd = open(...);
if (...errors...)
goto cleanup;
close(from_fd);
from_fd = -1;
[...same for other fds...]
cleanup:
if (from_fd >= 0)
close(from_fd);
[etc]
You could even drop that first close() in the happy path entirely, if
you don't mind holding all three descriptors open at once.
I dunno. We are reaching diminishing returns spending brainpower on a
function that is meant to be somewhat quick-and-dirty.
-Peff
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 7:55 ` Jeff King
@ 2025-07-23 8:06 ` Jeff King
2025-07-23 8:17 ` Eric Sunshine
2025-07-23 23:59 ` Jeff King
2025-07-23 8:11 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Eric Sunshine
2025-07-23 16:48 ` Junio C Hamano
2 siblings, 2 replies; 18+ messages in thread
From: Jeff King @ 2025-07-23 8:06 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
On Wed, Jul 23, 2025 at 03:55:13AM -0400, Jeff King wrote:
> I dunno. We are reaching diminishing returns spending brainpower on a
> function that is meant to be somewhat quick-and-dirty.
OK, I clearly could not resist spending more brainpower on it. If we are
doing quick-and-dirty, why not just die()? The end result is the same,
but per my argument in the earlier iteration of the series, that means
we do not have to worry about cleaning up at all.
Like:
t/helper/test-delta.c | 49 ++++++++++++-----------------------
1 file changed, 16 insertions(+), 33 deletions(-)
diff --git a/t/helper/test-delta.c b/t/helper/test-delta.c
index 6bc787a474..769b68839d 100644
--- a/t/helper/test-delta.c
+++ b/t/helper/test-delta.c
@@ -21,39 +21,28 @@ int cmd__delta(int argc, const char **argv)
struct stat st;
void *from_buf = NULL, *data_buf = NULL, *out_buf = NULL;
unsigned long from_size, data_size, out_size;
- int ret = 1;
if (argc != 5 || (strcmp(argv[1], "-d") && strcmp(argv[1], "-p"))) {
fprintf(stderr, "usage: %s\n", usage_str);
return 1;
}
- fd = open(argv[2], O_RDONLY);
- if (fd < 0 || fstat(fd, &st)) {
- perror(argv[2]);
- return 1;
- }
+ fd = xopen(argv[2], O_RDONLY);
+ if (fstat(fd, &st) < 0)
+ die_errno("fstat(%s)", argv[2]);
from_size = st.st_size;
from_buf = xmalloc(from_size);
- if (read_in_full(fd, from_buf, from_size) < 0) {
- perror(argv[2]);
- close(fd);
- goto cleanup;
- }
+ if (read_in_full(fd, from_buf, from_size) < 0)
+ die_errno("read(%s)", argv[2]);
close(fd);
- fd = open(argv[3], O_RDONLY);
- if (fd < 0 || fstat(fd, &st)) {
- perror(argv[3]);
- goto cleanup;
- }
+ fd = xopen(argv[3], O_RDONLY);
+ if (fstat(fd, &st) < 0)
+ die_errno("fstat(%s)", argv[3]);
data_size = st.st_size;
data_buf = xmalloc(data_size);
- if (read_in_full(fd, data_buf, data_size) < 0) {
- perror(argv[3]);
- close(fd);
- goto cleanup;
- }
+ if (read_in_full(fd, data_buf, data_size) < 0)
+ die_errno("read(%s)", argv[3]);
close(fd);
if (argv[1][1] == 'd')
@@ -64,22 +53,16 @@ int cmd__delta(int argc, const char **argv)
out_buf = patch_delta(from_buf, from_size,
data_buf, data_size,
&out_size);
- if (!out_buf) {
- fprintf(stderr, "delta operation failed (returned NULL)\n");
- goto cleanup;
- }
+ if (!out_buf)
+ die("delta operation failed (returned NULL)");
- fd = open (argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if (fd < 0 || write_in_full(fd, out_buf, out_size) < 0) {
- perror(argv[4]);
- goto cleanup;
- }
+ fd = xopen(argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
+ if (write_in_full(fd, out_buf, out_size) < 0)
+ die_errno("write(%s)", argv[4]);
- ret = 0;
-cleanup:
free(from_buf);
free(data_buf);
free(out_buf);
- return ret;
+ return 0;
}
I'd guess that one could probably go even further by replacing the bare
pointers with strbufs. And then you could use strbuf_read_file().
Incidentally that would also fix two minor bugs I noticed:
- passing st.st_size directly to xmalloc() is wrong, because of
truncation from off_t to size_t. This should use the xsize_t helper.
This is even a potential security vulnerability, but probably not
important in a test helper.
- likewise read_in_full() might return a non-negative value smaller
than the requested size (if the file racily changes and we get an
early EOF). But we only check whether we got a negative error value.
So we may read fewer bytes than expected and feed uninitialized
garbage to the delta code.
-Peff
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 7:55 ` Jeff King
2025-07-23 8:06 ` Jeff King
@ 2025-07-23 8:11 ` Eric Sunshine
2025-07-23 8:46 ` Jeff King
2025-07-23 17:11 ` Junio C Hamano
2025-07-23 16:48 ` Junio C Hamano
2 siblings, 2 replies; 18+ messages in thread
From: Eric Sunshine @ 2025-07-23 8:11 UTC (permalink / raw)
To: Jeff King; +Cc: Hoyoung Lee, git
On Wed, Jul 23, 2025 at 3:55 AM Jeff King <peff@peff.net> wrote:
> On Wed, Jul 23, 2025 at 03:28:05AM -0400, Eric Sunshine wrote:
> > The descriptor is closed manually (again) because a subsequent open()
> > call is going to reuse the variable. However...
> > ...although `fd` was closed, it still holds the previously-open
> > non-negative file descriptor, which means that this `goto cleanup`...
>
> Oof, good catch. This iteration of the patch was based on my suggestion,
> but I didn't notice the jump to cleanup between that close/open pair.
>
> I dunno. We are reaching diminishing returns spending brainpower on a
> function that is meant to be somewhat quick-and-dirty.
Aside from preferring that the patch not make the code worse or more
confusing, I don't have a strong opinion. Superficially, the existing
code presents itself as being careful by cleaning up after itself, but
as Hoyoung Lee discovered, there are holes in the cleanup
implementation. So, I do like that the intention of the patch is to
plug those holes, but we don't necessarily need to be particularly
clever about it. For such simple test-related code, even a pure
brute-force fix should be acceptable.
For completeness, I'll mention that I even had the thought that
another "fix" would be to tear out all the cleanup code entirely since
we _know_ that this function will be exiting immediately and the OS
will clean up any dangling resources.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 8:06 ` Jeff King
@ 2025-07-23 8:17 ` Eric Sunshine
2025-07-23 23:59 ` Jeff King
1 sibling, 0 replies; 18+ messages in thread
From: Eric Sunshine @ 2025-07-23 8:17 UTC (permalink / raw)
To: Jeff King; +Cc: Hoyoung Lee, git
On Wed, Jul 23, 2025 at 4:06 AM Jeff King <peff@peff.net> wrote:
> On Wed, Jul 23, 2025 at 03:55:13AM -0400, Jeff King wrote:
> > I dunno. We are reaching diminishing returns spending brainpower on a
> > function that is meant to be somewhat quick-and-dirty.
>
> OK, I clearly could not resist spending more brainpower on it. If we are
> doing quick-and-dirty, why not just die()? The end result is the same,
> but per my argument in the earlier iteration of the series, that means
> we do not have to worry about cleaning up at all.
Yes, die() seems sensible here. It's nice and tidy and makes the code
easier to reason about.
> Incidentally that would also fix two minor bugs I noticed:
>
> - passing st.st_size directly to xmalloc() is wrong, because of
> truncation from off_t to size_t. This should use the xsize_t helper.
> This is even a potential security vulnerability, but probably not
> important in a test helper.
>
> - likewise read_in_full() might return a non-negative value smaller
> than the requested size (if the file racily changes and we get an
> early EOF). But we only check whether we got a negative error value.
> So we may read fewer bytes than expected and feed uninitialized
> garbage to the delta code.
Can of worms opened.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 8:11 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Eric Sunshine
@ 2025-07-23 8:46 ` Jeff King
2025-07-23 16:37 ` Eric Sunshine
2025-07-23 17:11 ` Junio C Hamano
1 sibling, 1 reply; 18+ messages in thread
From: Jeff King @ 2025-07-23 8:46 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
On Wed, Jul 23, 2025 at 04:11:26AM -0400, Eric Sunshine wrote:
> For completeness, I'll mention that I even had the thought that
> another "fix" would be to tear out all the cleanup code entirely since
> we _know_ that this function will be exiting immediately and the OS
> will clean up any dangling resources.
The reason we have the "cleanup" label at all is because of the memory
leaks. And there the issue is that we build the test helpers with the
same compiler settings as the rest of the code, so SANITIZE=leak will
complain. So I think that is a non-starter.
But if you just meant leaking descriptors, sure, I don't think any tools
complain about that. ;)
-Peff
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 8:46 ` Jeff King
@ 2025-07-23 16:37 ` Eric Sunshine
0 siblings, 0 replies; 18+ messages in thread
From: Eric Sunshine @ 2025-07-23 16:37 UTC (permalink / raw)
To: Jeff King; +Cc: Hoyoung Lee, git
On Wed, Jul 23, 2025 at 4:46 AM Jeff King <peff@peff.net> wrote:
> On Wed, Jul 23, 2025 at 04:11:26AM -0400, Eric Sunshine wrote:
> > For completeness, I'll mention that I even had the thought that
> > another "fix" would be to tear out all the cleanup code entirely since
> > we _know_ that this function will be exiting immediately and the OS
> > will clean up any dangling resources.
>
> The reason we have the "cleanup" label at all is because of the memory
> leaks. And there the issue is that we build the test helpers with the
> same compiler settings as the rest of the code, so SANITIZE=leak will
> complain. So I think that is a non-starter.
>
> But if you just meant leaking descriptors, sure, I don't think any tools
> complain about that. ;)
At the time the momentary (but not really serious) thought flashed
through my brain, I probably was imagining dropping all of the cleanup
code; I almost certainly wasn't thinking about the SANITIZE=leak case.
But immediately upon seeing your idea to use die(), I recognized it
(and liked it) as a superior version of my half-baked thought.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 7:28 ` Eric Sunshine
2025-07-23 7:55 ` Jeff King
@ 2025-07-23 16:44 ` Junio C Hamano
1 sibling, 0 replies; 18+ messages in thread
From: Junio C Hamano @ 2025-07-23 16:44 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
Eric Sunshine <sunshine@sunshineco.com> writes:
> On Tue, Jul 22, 2025 at 1:41 PM Hoyoung Lee <lhywkd22@gmail.com> wrote:
>> Initialize `fd` to -1 and unify all `open()`-related `close()` calls
>> under a single cleanup label. This prevents undefined behavior when
>> `fd` is used without initialization in error paths.
>
> It's not clear what this means. As far as I can tell, the original
> code never used an uninitialized `fd` in error paths.
>
>> The cleanup logic now safely avoids calling `close()` on invalid
>> descriptors and ensures consistent resource management.
>
> Again, it's not clear what this means. Although your previous version
> of this patch did add a call to close() with an invalid descriptor,
> the original code did not do so. So, the above statement seems to be
> misleading.
Great to see that good guidance is offered to new folks. Yes, it is
a great thing to keep in mind to describe the latest iteration for
readers who haven't even seen the previous iterations.
> Those issues aside, the patch itself has problems, some minor, such as
> making the code a bit confusing or misleading, and some major, such as
> calling close() on an already closed descriptor.
>> data_size = st.st_size;
>> data_buf = xmalloc(data_size);
>> if (read_in_full(fd, data_buf, data_size) < 0) {
>> perror(argv[3]);
>> goto cleanup;
>> }
>> close(fd);
>
> The descriptor is closed manually (again) because a subsequent open()
> call is going to reuse the variable. However...
>
>> if (argv[1][1] == 'd')
>> out_buf = diff_delta(from_buf, from_size,
>> data_buf, data_size,
>> &out_size, 0);
>> else
>> out_buf = patch_delta(from_buf, from_size,
>> data_buf, data_size,
>> &out_size);
>> if (!out_buf) {
>> fprintf(stderr, "delta operation failed (returned NULL)\n");
>> goto cleanup;
>> }
>
> ...although `fd` was closed, it still holds the previously-open
> non-negative file descriptor, which means that this `goto cleanup`...
Good eyes.
>> fd = open (argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
>> if (fd < 0 || write_in_full(fd, out_buf, out_size) < 0) {
>> perror(argv[4]);
>> goto cleanup;
>> }
>>
>> ret = 0;
>> cleanup:
>> free(from_buf);
>> free(data_buf);
>> free(out_buf);
>>
>> if (fd >= 0)
>> close(fd);
>
> ...will arrive here and the condition will evaluate to "true",
> resulting in the already-closed descriptor being closed again.
Very clear description.
Thanks for your review.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 7:55 ` Jeff King
2025-07-23 8:06 ` Jeff King
2025-07-23 8:11 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Eric Sunshine
@ 2025-07-23 16:48 ` Junio C Hamano
2 siblings, 0 replies; 18+ messages in thread
From: Junio C Hamano @ 2025-07-23 16:48 UTC (permalink / raw)
To: Jeff King; +Cc: Eric Sunshine, Hoyoung Lee, git
Jeff King <peff@peff.net> writes:
> I dunno. We are reaching diminishing returns spending brainpower on a
> function that is meant to be somewhat quick-and-dirty.
True. After all, the function is ready to exit and have the system
close the file descriptor anyway ;-)
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 8:11 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Eric Sunshine
2025-07-23 8:46 ` Jeff King
@ 2025-07-23 17:11 ` Junio C Hamano
1 sibling, 0 replies; 18+ messages in thread
From: Junio C Hamano @ 2025-07-23 17:11 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Jeff King, Hoyoung Lee, git
Eric Sunshine <sunshine@sunshineco.com> writes:
> For completeness, I'll mention that I even had the thought that
> another "fix" would be to tear out all the cleanup code entirely since
> we _know_ that this function will be exiting immediately and the OS
> will clean up any dangling resources.
;-)
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup
2025-07-23 8:06 ` Jeff King
2025-07-23 8:17 ` Eric Sunshine
@ 2025-07-23 23:59 ` Jeff King
2025-07-24 0:00 ` [PATCH 1/3] test-delta: handle errors with die() Jeff King
` (2 more replies)
1 sibling, 3 replies; 18+ messages in thread
From: Jeff King @ 2025-07-23 23:59 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
On Wed, Jul 23, 2025 at 04:06:39AM -0400, Jeff King wrote:
> On Wed, Jul 23, 2025 at 03:55:13AM -0400, Jeff King wrote:
>
> > I dunno. We are reaching diminishing returns spending brainpower on a
> > function that is meant to be somewhat quick-and-dirty.
>
> OK, I clearly could not resist spending more brainpower on it. If we are
> doing quick-and-dirty, why not just die()? The end result is the same,
> but per my argument in the earlier iteration of the series, that means
> we do not have to worry about cleaning up at all.
So...I feel a little bad about hijacking Hoyoung's thread. But after all
of the discussion, it seemed simplest to just stick it all into patches.
So here is a potential replacement for patch 2.
[1/3]: test-delta: handle errors with die()
[2/3]: test-delta: use strbufs to hold input files
[3/3]: test-delta: close output descriptor after use
t/helper/test-delta.c | 77 ++++++++++++++-----------------------------
1 file changed, 24 insertions(+), 53 deletions(-)
-Peff
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 1/3] test-delta: handle errors with die()
2025-07-23 23:59 ` Jeff King
@ 2025-07-24 0:00 ` Jeff King
2025-07-24 0:02 ` [PATCH 2/3] test-delta: use strbufs to hold input files Jeff King
2025-07-24 0:03 ` [PATCH 3/3] test-delta: close output descriptor after use Jeff King
2 siblings, 0 replies; 18+ messages in thread
From: Jeff King @ 2025-07-24 0:00 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
This is a short test helper that does all of its work in the main
function. When we encounter an error, we try to clean up memory and
descriptors and then jump to an error return, which exits the program.
We can get the same effect by just calling die(), which means we do not
have to bother with cleaning up. This simplifies the code, and also
removes some inconsistencies where a few code paths forgot to clean up
descriptors (though in practice it was not a big deal since we were
exiting anyway).
In addition to die() and die_errno(), we'll also use a few of our usual
helpers like xopen() and usage() that make things more ergonomic.
This does change the exit code in these cases from 1 to 128, but I
don't think it matters (and arguably is better, as we'd already exit 128
for other errors like xmalloc() failure).
Signed-off-by: Jeff King <peff@peff.net>
---
t/helper/test-delta.c | 55 ++++++++++++++-----------------------------
1 file changed, 18 insertions(+), 37 deletions(-)
diff --git a/t/helper/test-delta.c b/t/helper/test-delta.c
index 6bc787a474..4495b32b49 100644
--- a/t/helper/test-delta.c
+++ b/t/helper/test-delta.c
@@ -21,39 +21,26 @@ int cmd__delta(int argc, const char **argv)
struct stat st;
void *from_buf = NULL, *data_buf = NULL, *out_buf = NULL;
unsigned long from_size, data_size, out_size;
- int ret = 1;
- if (argc != 5 || (strcmp(argv[1], "-d") && strcmp(argv[1], "-p"))) {
- fprintf(stderr, "usage: %s\n", usage_str);
- return 1;
- }
+ if (argc != 5 || (strcmp(argv[1], "-d") && strcmp(argv[1], "-p")))
+ usage(usage_str);
- fd = open(argv[2], O_RDONLY);
- if (fd < 0 || fstat(fd, &st)) {
- perror(argv[2]);
- return 1;
- }
+ fd = xopen(argv[2], O_RDONLY);
+ if (fstat(fd, &st) < 0)
+ die_errno("fstat(%s)", argv[2]);
from_size = st.st_size;
from_buf = xmalloc(from_size);
- if (read_in_full(fd, from_buf, from_size) < 0) {
- perror(argv[2]);
- close(fd);
- goto cleanup;
- }
+ if (read_in_full(fd, from_buf, from_size) < 0)
+ die_errno("read(%s)", argv[2]);
close(fd);
- fd = open(argv[3], O_RDONLY);
- if (fd < 0 || fstat(fd, &st)) {
- perror(argv[3]);
- goto cleanup;
- }
+ fd = xopen(argv[3], O_RDONLY);
+ if (fstat(fd, &st) < 0)
+ die_errno("fstat(%s)", argv[3]);
data_size = st.st_size;
data_buf = xmalloc(data_size);
- if (read_in_full(fd, data_buf, data_size) < 0) {
- perror(argv[3]);
- close(fd);
- goto cleanup;
- }
+ if (read_in_full(fd, data_buf, data_size) < 0)
+ die_errno("read(%s)", argv[3]);
close(fd);
if (argv[1][1] == 'd')
@@ -64,22 +51,16 @@ int cmd__delta(int argc, const char **argv)
out_buf = patch_delta(from_buf, from_size,
data_buf, data_size,
&out_size);
- if (!out_buf) {
- fprintf(stderr, "delta operation failed (returned NULL)\n");
- goto cleanup;
- }
+ if (!out_buf)
+ die("delta operation failed (returned NULL)");
- fd = open (argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if (fd < 0 || write_in_full(fd, out_buf, out_size) < 0) {
- perror(argv[4]);
- goto cleanup;
- }
+ fd = xopen(argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
+ if (write_in_full(fd, out_buf, out_size) < 0)
+ die_errno("write(%s)", argv[4]);
- ret = 0;
-cleanup:
free(from_buf);
free(data_buf);
free(out_buf);
- return ret;
+ return 0;
}
--
2.50.1.666.gdb1e186d6a
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 2/3] test-delta: use strbufs to hold input files
2025-07-23 23:59 ` Jeff King
2025-07-24 0:00 ` [PATCH 1/3] test-delta: handle errors with die() Jeff King
@ 2025-07-24 0:02 ` Jeff King
2025-07-24 0:03 ` [PATCH 3/3] test-delta: close output descriptor after use Jeff King
2 siblings, 0 replies; 18+ messages in thread
From: Jeff King @ 2025-07-24 0:02 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
We want to read the whole contents of two files into memory. If we
switch from raw ptr/len pairs to strbufs, we can use strbuf_read_file()
to shorten the code.
This incidentally fixes two small bugs:
1. We stat() the files and allocate our buffers based on st.st_size.
But that is an off_t which may be larger than the size_t we'd use
to allocate. We should use xsize_t() to do a checked conversion.
Otherwise integer truncation (on a file >4GB) could cause us to
under-allocate (though in practice this does not result in a buffer
overflow because the same truncation happens when read_in_full()
also takes a size_t).
2. We get the size from st.st_size, and then try to read_in_full()
that many bytes. But it may return fewer bytes than expected (if
the file changed racily and we get an early EOF), leading us to
read uninitialized bytes in the allocated buffer. We don't notice
because we only check the value for error, not that we got the
expected number of bytes.
The strbuf code doesn't run into this, because it just reads to EOF,
expanding the buffer dynamically as necessary. Neither bug is a big deal
for a test helper, but fixing them is a nice bonus on top of simplifying
the code.
Signed-off-by: Jeff King <peff@peff.net>
---
t/helper/test-delta.c | 40 ++++++++++++++--------------------------
1 file changed, 14 insertions(+), 26 deletions(-)
diff --git a/t/helper/test-delta.c b/t/helper/test-delta.c
index 4495b32b49..7945793078 100644
--- a/t/helper/test-delta.c
+++ b/t/helper/test-delta.c
@@ -11,45 +11,33 @@
#include "test-tool.h"
#include "git-compat-util.h"
#include "delta.h"
+#include "strbuf.h"
static const char usage_str[] =
"test-tool delta (-d|-p) <from_file> <data_file> <out_file>";
int cmd__delta(int argc, const char **argv)
{
int fd;
- struct stat st;
- void *from_buf = NULL, *data_buf = NULL, *out_buf = NULL;
- unsigned long from_size, data_size, out_size;
+ struct strbuf from = STRBUF_INIT, data = STRBUF_INIT;
+ char *out_buf;
+ unsigned long out_size;
if (argc != 5 || (strcmp(argv[1], "-d") && strcmp(argv[1], "-p")))
usage(usage_str);
- fd = xopen(argv[2], O_RDONLY);
- if (fstat(fd, &st) < 0)
- die_errno("fstat(%s)", argv[2]);
- from_size = st.st_size;
- from_buf = xmalloc(from_size);
- if (read_in_full(fd, from_buf, from_size) < 0)
- die_errno("read(%s)", argv[2]);
- close(fd);
-
- fd = xopen(argv[3], O_RDONLY);
- if (fstat(fd, &st) < 0)
- die_errno("fstat(%s)", argv[3]);
- data_size = st.st_size;
- data_buf = xmalloc(data_size);
- if (read_in_full(fd, data_buf, data_size) < 0)
- die_errno("read(%s)", argv[3]);
- close(fd);
+ if (strbuf_read_file(&from, argv[2], 0) < 0)
+ die_errno("unable to read '%s'", argv[2]);
+ if (strbuf_read_file(&data, argv[3], 0) < 0)
+ die_errno("unable to read '%s'", argv[3]);
if (argv[1][1] == 'd')
- out_buf = diff_delta(from_buf, from_size,
- data_buf, data_size,
+ out_buf = diff_delta(from.buf, from.len,
+ data.buf, data.len,
&out_size, 0);
else
- out_buf = patch_delta(from_buf, from_size,
- data_buf, data_size,
+ out_buf = patch_delta(from.buf, from.len,
+ data.buf, data.len,
&out_size);
if (!out_buf)
die("delta operation failed (returned NULL)");
@@ -58,8 +46,8 @@ int cmd__delta(int argc, const char **argv)
if (write_in_full(fd, out_buf, out_size) < 0)
die_errno("write(%s)", argv[4]);
- free(from_buf);
- free(data_buf);
+ strbuf_release(&from);
+ strbuf_release(&data);
free(out_buf);
return 0;
--
2.50.1.666.gdb1e186d6a
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 3/3] test-delta: close output descriptor after use
2025-07-23 23:59 ` Jeff King
2025-07-24 0:00 ` [PATCH 1/3] test-delta: handle errors with die() Jeff King
2025-07-24 0:02 ` [PATCH 2/3] test-delta: use strbufs to hold input files Jeff King
@ 2025-07-24 0:03 ` Jeff King
2 siblings, 0 replies; 18+ messages in thread
From: Jeff King @ 2025-07-24 0:03 UTC (permalink / raw)
To: Eric Sunshine; +Cc: Hoyoung Lee, git
After we write to the output file, the program exits. This naturally
closes the descriptor. But we should do an explicit close for two
reasons:
1. It's possible to hit an error on close(), which we should detect
and report via our exit code.
2. Leaking descriptors is a bad practice in general. Even if it isn't
meaningful here, it sets a bad example.
It is tempting to write:
if (write_in_full(fd, ...) < 0 || close(fd) < 0)
die_errno(...);
But that pattern contains a subtle problem that has resulted in
descriptor leaks before. If write_in_full() fails, we'll short-circuit
and never call close(), leaking the descriptor.
That's not a problem here, since our error path dies instead of
returning up the stack. But since we're trying to set a good example,
let's write it out as two separate conditions. As a bonus, that lets us
produce a slightly more specific error message.
Signed-off-by: Jeff King <peff@peff.net>
---
Another option is that the program should just write to stdout, then we
do not have to open or close it at all. ;)
t/helper/test-delta.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/t/helper/test-delta.c b/t/helper/test-delta.c
index 7945793078..52ea00c937 100644
--- a/t/helper/test-delta.c
+++ b/t/helper/test-delta.c
@@ -45,6 +45,8 @@ int cmd__delta(int argc, const char **argv)
fd = xopen(argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666);
if (write_in_full(fd, out_buf, out_size) < 0)
die_errno("write(%s)", argv[4]);
+ if (close(fd) < 0)
+ die_errno("close(%s)", argv[4]);
strbuf_release(&from);
strbuf_release(&data);
--
2.50.1.666.gdb1e186d6a
^ permalink raw reply related [flat|nested] 18+ messages in thread
end of thread, other threads:[~2025-07-24 0:03 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-07-22 17:41 [PATCH v3 0/2] fix resource leaks in test helpers Hoyoung Lee
2025-07-22 17:41 ` [PATCH v3 1/2] t/helper/test-truncate: close file descriptor after truncation Hoyoung Lee
2025-07-22 21:48 ` Junio C Hamano
2025-07-22 17:41 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Hoyoung Lee
2025-07-23 7:28 ` Eric Sunshine
2025-07-23 7:55 ` Jeff King
2025-07-23 8:06 ` Jeff King
2025-07-23 8:17 ` Eric Sunshine
2025-07-23 23:59 ` Jeff King
2025-07-24 0:00 ` [PATCH 1/3] test-delta: handle errors with die() Jeff King
2025-07-24 0:02 ` [PATCH 2/3] test-delta: use strbufs to hold input files Jeff King
2025-07-24 0:03 ` [PATCH 3/3] test-delta: close output descriptor after use Jeff King
2025-07-23 8:11 ` [PATCH v3 2/2] t/helper/test-delta: fix possible resource leak and ensure safe cleanup Eric Sunshine
2025-07-23 8:46 ` Jeff King
2025-07-23 16:37 ` Eric Sunshine
2025-07-23 17:11 ` Junio C Hamano
2025-07-23 16:48 ` Junio C Hamano
2025-07-23 16:44 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).