-
Notifications
You must be signed in to change notification settings - Fork 197
trace2: stop allowing die() #2178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c483a4b
754fffb
87d3f1b
5bf6ab9
3e419c5
ccd284f
fa10e8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| #ifndef BANNED_DIE_H | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Elijah Newren wrote on the Git mailing list (how to reply to this email): On Tue, Aug 25, 2026 at 11:58 AM Derrick Stolee via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
[...]
> +#undef die
> +#define die banned(die)
Shouldn't that be BANNED(die) to match all the other cases in the code
(and avoid an obtuse "implicit declaration of function 'banned'"
instead of the nicer "sorry_die_is_a_banned_function" message)?
> +
> +#endif /* BANNED_DIE_H */
> diff --git a/trace2.c b/trace2.c
> index c23c0a227b..1d0ed2db2b 100644
> --- a/trace2.c
> +++ b/trace2.c
> @@ -17,6 +17,7 @@
> #include "trace2/tr2_tgt.h"
> #include "trace2/tr2_tls.h"
> #include "trace2/tr2_tmr.h"
> +#include "banned-die.h"
>
Is there a risk that future folks add new includes at the end of the
list, then functions in them get added to banned-die.h, but are
silently ignored because banned-die.h wasn't the last include?There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Derrick Stolee wrote on the Git mailing list (how to reply to this email): On 8/25/2026 6:14 PM, Elijah Newren wrote:
> On Tue, Aug 25, 2026 at 11:58 AM Derrick Stolee via GitGitGadget
> <gitgitgadget@gmail.com> wrote:
>>
> [...]
>> +#undef die
>> +#define die banned(die)
>
> Shouldn't that be BANNED(die) to match all the other cases in the code
> (and avoid an obtuse "implicit declaration of function 'banned'"
> instead of the nicer "sorry_die_is_a_banned_function" message)?
Oops. Yes, a mistake during a rebase.
>> +
>> +#endif /* BANNED_DIE_H */
>> diff --git a/trace2.c b/trace2.c
>> index c23c0a227b..1d0ed2db2b 100644
>> --- a/trace2.c
>> +++ b/trace2.c
>> @@ -17,6 +17,7 @@
>> #include "trace2/tr2_tgt.h"
>> #include "trace2/tr2_tls.h"
>> #include "trace2/tr2_tmr.h"
>> +#include "banned-die.h"
>>
>
> Is there a risk that future folks add new includes at the end of the
> list, then functions in them get added to banned-die.h, but are
> silently ignored because banned-die.h wasn't the last include?
There is a risk. The "must be last" part is documented in the
header, but maybe it should be in a comment here, too.
Thanks,
-Stolee
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Jeff King wrote on the Git mailing list (how to reply to this email): On Tue, Aug 25, 2026 at 06:56:15PM +0000, Derrick Stolee via GitGitGadget wrote:
> We have universally-banned functions listed in banned.h since
> c8af66ab8ad (automatically ban strcpy(), 2018-07-26), but some layers of
> the code should be more strict than others.
>
> One such example is the trace2 API which runs during atexit() and can
> prove to cause die()-handler recursion problems if it calls die().
>
> Create a new banned-die.h header file that will ban some Git methods
> that call die(). Include that in all trace2 API implementation files.
> This currently only bans die() itself, and that was already not used.
There's a subtle but big difference between the universal code bans in
banned.h and this banned-die.h. In the former case we are deciding
strcpy() is unfit for our code base and outlawing it everywhere. The
potential problem is in the source code, so catching it while compiling
the source code is OK.
But we are not doing that with die(). It is a perfectly OK function in
general, but we do not want to ever trigger its runtime effects from
certain code paths. Banning it from being called from those code paths
can catch _some_ instances, but not any transitive calls. If we call
foo(), it may call die() itself, and we would not want to ban foo() from
doing so. And recursively for functions called by foo() and so on.
So you end up playing whack-a-mole with functions that might call die()
and adding them to this ban list.
I think that's _probably_ the best we can do in practice. I think the
framing above suggests that we could approach the problem more directly
with a runtime flag: when we enter those code paths, set a flag to avoid
the unwanted behavior, and have the low-level code respect that. But
die() is a special case here, because we'd want to suppress its
no-return behavior. And its callers are not prepared for die() to
suddenly start returning because of some global flag.
So I think the whack-a-mole is the best we can do. But I would not want
to see this strategy extended to other areas. In most cases some kind of
runtime support is probably a better solution.
-PeffThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Derrick Stolee wrote on the Git mailing list (how to reply to this email): On 8/27/2026 1:10 AM, Jeff King wrote:
> On Tue, Aug 25, 2026 at 06:56:15PM +0000, Derrick Stolee via GitGitGadget wrote:
>
>> We have universally-banned functions listed in banned.h since
>> c8af66ab8ad (automatically ban strcpy(), 2018-07-26), but some layers of
>> the code should be more strict than others.
>>
>> One such example is the trace2 API which runs during atexit() and can
>> prove to cause die()-handler recursion problems if it calls die().
>>
>> Create a new banned-die.h header file that will ban some Git methods
>> that call die(). Include that in all trace2 API implementation files.
>> This currently only bans die() itself, and that was already not used.
>
> There's a subtle but big difference between the universal code bans in
> banned.h and this banned-die.h. In the former case we are deciding
> strcpy() is unfit for our code base and outlawing it everywhere. The
> potential problem is in the source code, so catching it while compiling
> the source code is OK.
>
> But we are not doing that with die(). It is a perfectly OK function in
> general, but we do not want to ever trigger its runtime effects from
> certain code paths. Banning it from being called from those code paths
> can catch _some_ instances, but not any transitive calls. If we call
> foo(), it may call die() itself, and we would not want to ban foo() from
> doing so. And recursively for functions called by foo() and so on.
Yes, this makes it tricky to be 100% sure without some kind of static
analysis.
> So you end up playing whack-a-mole with functions that might call die()
> and adding them to this ban list.
This does have some benefit that we can gradually remove these
transitive callers in the multi-commit series. But it's unsatisfying
as a full protection in the end.
> I think that's _probably_ the best we can do in practice. I think the
> framing above suggests that we could approach the problem more directly
> with a runtime flag: when we enter those code paths, set a flag to avoid
> the unwanted behavior, and have the low-level code respect that. But
> die() is a special case here, because we'd want to suppress its
> no-return behavior. And its callers are not prepared for die() to
> suddenly start returning because of some global flag.
>
> So I think the whack-a-mole is the best we can do. But I would not want
> to see this strategy extended to other areas. In most cases some kind of
> runtime support is probably a better solution.
The other alternative that we could consider is to reorganize the
codebase in such a way that certain sections of code don't have
access to headers that could lead to die() or other "higher" methods
that are acceptable for user-facing processes but are best to avoid
in library APIs. Even then, we'd need some checks at compile time to
avoid crossing boundaries.
I don't think such a reorganization is desirable overall, because
that will be very disruptive to the project and file history.
Having some amount of protection through this header gives us a
mechanism to demonstrate and enforce some protection.
Thanks,
-Stolee
|
||
| #define BANNED_DIE_H | ||
|
|
||
| #include "banned.h" | ||
|
|
||
| /* | ||
| * This header lists functions that must not be used by low-level APIs | ||
| * because they can cause Git to terminate. | ||
| */ | ||
|
|
||
| #undef die | ||
| #define die BANNED(die) | ||
|
|
||
| #undef xsnprintf | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Elijah Newren wrote on the Git mailing list (how to reply to this email): On Tue, Aug 25, 2026 at 11:58 AM Derrick Stolee via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
[...]
> For full defense in depth, we remove the xstrdup() calls from
> trace2/tr2_sysenv.c.
>
> First, in tr2_sysenv_cb(), we need to handle a failed assignment of the
> value with a negative return to halt the config parsing loop.
>
[...]
> --- a/trace2/tr2_sysenv.c
> +++ b/trace2/tr2_sysenv.c
> @@ -74,7 +74,9 @@ static int tr2_sysenv_cb(const char *key, const char *value,
> if (!value)
> return config_error_nonbool(key);
> free(tr2_sysenv_settings[k].value);
> - tr2_sysenv_settings[k].value = xstrdup(value);
> + tr2_sysenv_settings[k].value = strdup(value);
> + if (!tr2_sysenv_settings[k].value)
> + return -1;
I'm not sure if this matters, but I think the call sequence from
config.c to this function is:
read_very_early_config ->
config_with_options ->
git_config_from_file_with_options ->
do_config_from_file ->
do_config_from ->
git_parse_source ->
get_value ->
git_config_include ->
tr2_sysenv_cb
and the -1 unwinds back to git_parse_source, which breaks, formats an
error message, and calls die:
error_msg = xstrfmt(_("bad config line %d in file %s")...)
die("%s", error_msg)
Am I reading this right? If so, the -1 actually triggers a die as
well -- unless the allocation in xstrfmt manages to kill it first.
This isn't a regression (the old xstrdup() also died) and the die
isn't inside the trace functions, but the commit message might read as
promising more than it delivers.There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Derrick Stolee wrote on the Git mailing list (how to reply to this email): On 8/25/2026 6:14 PM, Elijah Newren wrote:
> On Tue, Aug 25, 2026 at 11:58 AM Derrick Stolee via GitGitGadget
> <gitgitgadget@gmail.com> wrote:
>>
> [...]
>> For full defense in depth, we remove the xstrdup() calls from
>> trace2/tr2_sysenv.c.
>>
>> First, in tr2_sysenv_cb(), we need to handle a failed assignment of the
>> value with a negative return to halt the config parsing loop.
>>
> [...]
>> --- a/trace2/tr2_sysenv.c
>> +++ b/trace2/tr2_sysenv.c
>> @@ -74,7 +74,9 @@ static int tr2_sysenv_cb(const char *key, const char *value,
>> if (!value)
>> return config_error_nonbool(key);
>> free(tr2_sysenv_settings[k].value);
>> - tr2_sysenv_settings[k].value = xstrdup(value);
>> + tr2_sysenv_settings[k].value = strdup(value);
>> + if (!tr2_sysenv_settings[k].value)
>> + return -1;
>
> I'm not sure if this matters, but I think the call sequence from
> config.c to this function is:
>
> read_very_early_config ->
> config_with_options ->
> git_config_from_file_with_options ->
> do_config_from_file ->
> do_config_from ->
> git_parse_source ->
> get_value ->
> git_config_include ->
> tr2_sysenv_cb
>
> and the -1 unwinds back to git_parse_source, which breaks, formats an
> error message, and calls die:
>
> error_msg = xstrfmt(_("bad config line %d in file %s")...)
> die("%s", error_msg)
Thanks for the careful read! It's particularly important that we
don't suggest that the config value is bad because we couldn't
allocate memory.
> Am I reading this right? If so, the -1 actually triggers a die as
> well -- unless the allocation in xstrfmt manages to kill it first.
> This isn't a regression (the old xstrdup() also died) and the die
> isn't inside the trace functions, but the commit message might read as
> promising more than it delivers.
Yes, I believe you are correct. We should return 0 to terminate
early without a failure.
That said, I think that the die() in the config code will remain a
"safe" place to die(), as we won't re-trigger this config-parsing
code during any tracing of that die() message. But it's best to be
safe and have the tracing continue to be "best effort" when system
calls fail.
Thanks,
-Stolee
|
||
| #define xsnprintf(...) BANNED(xsnprintf) | ||
|
|
||
| #undef xstrdup | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Elijah Newren wrote on the Git mailing list (how to reply to this email): On Tue, Aug 25, 2026 at 11:59 AM Derrick Stolee via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
[...]
>+ const char *redact = ":<REDACTED>";
>+ char *redacted;
[...]
> + memcpy(redacted, arg, prefix_len);
> + memcpy(redacted + prefix_len, redact, redact_len - 1);
Only copy redact_len - 1 bytes? So only ":<REDACTED" without the
trailing ">" ? Why?
> + memcpy(redacted + prefix_len + redact_len - 1, p + at,
> + suffix_len + 1);
> + return redacted;
> }
>There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Junio C Hamano wrote on the Git mailing list (how to reply to this email): Elijah Newren <newren@gmail.com> writes:
> On Tue, Aug 25, 2026 at 11:59 AM Derrick Stolee via GitGitGadget
> <gitgitgadget@gmail.com> wrote:
>>
> [...]
>>+ const char *redact = ":<REDACTED>";
>>+ char *redacted;
> [...]
>> + memcpy(redacted, arg, prefix_len);
>> + memcpy(redacted + prefix_len, redact, redact_len - 1);
>
> Only copy redact_len - 1 bytes? So only ":<REDACTED" without the
> trailing ">" ? Why?
Yeah, if it were (redact_len + 1) it would have worked better, perhaps?
>
>
>> + memcpy(redacted + prefix_len + redact_len - 1, p + at,
>> + suffix_len + 1);
>> + return redacted;
>> }
>>There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Derrick Stolee wrote on the Git mailing list (how to reply to this email): On 8/25/2026 6:36 PM, Junio C Hamano wrote:
> Elijah Newren <newren@gmail.com> writes:
>
>> On Tue, Aug 25, 2026 at 11:59 AM Derrick Stolee via GitGitGadget
>> <gitgitgadget@gmail.com> wrote:
>>>
>> [...]
>>> + const char *redact = ":<REDACTED>";
>>> + char *redacted;
>> [...]
>>> + memcpy(redacted, arg, prefix_len);
>>> + memcpy(redacted + prefix_len, redact, redact_len - 1);
>>
>> Only copy redact_len - 1 bytes? So only ":<REDACTED" without the
>> trailing ">" ? Why?
>
> Yeah, if it were (redact_len + 1) it would have worked better, perhaps?
I should have been more careful and realized that we don't have any
tests that cover this logic.
We have tests for ":<redacted>" in pkt-line output, but not for the
trace2 version.
Thanks,
-Stolee
|
||
| #define xstrdup(str) BANNED(xstrdup) | ||
|
|
||
| #undef xcalloc | ||
| #define xcalloc(nmemb, size) BANNED(xcalloc) | ||
|
|
||
| #undef xstrfmt | ||
| #define xstrfmt(...) BANNED(xstrfmt) | ||
|
|
||
| #undef ALLOC_ARRAY | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Elijah Newren wrote on the Git mailing list (how to reply to this email): On Tue, Aug 25, 2026 at 11:57 AM Derrick Stolee via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
> From: Derrick Stolee <stolee@gmail.com>
>
> The ALLOC_GROW() helper can call die() on a failed memory allocation.
> We need to remove this from the trace2 API code to prevent a recursive
> die() handler.
>
> This helper is used to track the nested region stack. Use a new
> skipped_regions member to track how many times a region was entered
> without being added to the stack, and decrease that amount as we leave
> each region. This allows us to avoid a failure and instead stop
> deepening the stack, giving as much nesting behavior as possible without
> failing the entire process.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
Checking out this commit and running
GIT_TRACE2_PERF=1 ./bin-wrappers/git status
dies with
no open regions in thread 'main'
Seems to be fixed by 7/7, though. Maybe a bad splitting? |
||
| #define ALLOC_ARRAY(x, alloc) BANNED(ALLOC_ARRAY) | ||
|
|
||
| #undef ALLOC_GROW | ||
| #define ALLOC_GROW(x, nr, alloc) BANNED(ALLOC_GROW) | ||
|
|
||
| #endif /* BANNED_DIE_H */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,68 @@ | ||
| #include "git-compat-util.h" | ||
| #include "tr2_tbuf.h" | ||
| /* banned-die must be last. */ | ||
| #include "banned-die.h" | ||
|
|
||
| void tr2_tbuf_local_time(struct tr2_tbuf *tb) | ||
| { | ||
| struct timeval tv; | ||
| struct tm tm; | ||
| struct timeval tv = { 0 }; | ||
| struct tm tm = { 0 }; | ||
| time_t secs; | ||
| int len; | ||
|
|
||
| gettimeofday(&tv, NULL); | ||
| secs = tv.tv_sec; | ||
| localtime_r(&secs, &tm); | ||
|
|
||
| xsnprintf(tb->buf, sizeof(tb->buf), "%02d:%02d:%02d.%06ld", tm.tm_hour, | ||
| tm.tm_min, tm.tm_sec, (long)tv.tv_usec); | ||
| len = snprintf(tb->buf, sizeof(tb->buf), "%02d:%02d:%02d.%06ld", | ||
| tm.tm_hour, tm.tm_min, tm.tm_sec, (long)tv.tv_usec); | ||
|
|
||
| if (len < 0 || (size_t)len >= sizeof(tb->buf)) { | ||
| const char *blank = "00:00:00.000000"; | ||
| strlcpy(tb->buf, blank, sizeof(tb->buf)); | ||
| } | ||
| } | ||
|
|
||
| void tr2_tbuf_utc_datetime_extended(struct tr2_tbuf *tb) | ||
| { | ||
| struct timeval tv; | ||
| struct tm tm; | ||
| struct timeval tv = { 0 }; | ||
| struct tm tm = { 0 }; | ||
| time_t secs; | ||
| int len; | ||
|
|
||
| gettimeofday(&tv, NULL); | ||
| secs = tv.tv_sec; | ||
| gmtime_r(&secs, &tm); | ||
|
|
||
| xsnprintf(tb->buf, sizeof(tb->buf), | ||
| "%4d-%02d-%02dT%02d:%02d:%02d.%06ldZ", tm.tm_year + 1900, | ||
| tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, | ||
| (long)tv.tv_usec); | ||
| len = snprintf(tb->buf, sizeof(tb->buf), | ||
| "%4d-%02d-%02dT%02d:%02d:%02d.%06ldZ", | ||
| tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, | ||
| tm.tm_hour, tm.tm_min, tm.tm_sec, (long)tv.tv_usec); | ||
|
|
||
| if (len < 0 || (size_t)len >= sizeof(tb->buf)) { | ||
| const char *blank = "1900-00-00T00:00:00.000000Z"; | ||
| strlcpy(tb->buf, blank, sizeof(tb->buf)); | ||
| } | ||
| } | ||
|
|
||
| void tr2_tbuf_utc_datetime(struct tr2_tbuf *tb) | ||
| { | ||
| struct timeval tv; | ||
| struct tm tm; | ||
| struct timeval tv = { 0 }; | ||
| struct tm tm = { 0 }; | ||
| time_t secs; | ||
| int len; | ||
|
|
||
| gettimeofday(&tv, NULL); | ||
| secs = tv.tv_sec; | ||
| gmtime_r(&secs, &tm); | ||
|
|
||
| xsnprintf(tb->buf, sizeof(tb->buf), "%4d%02d%02dT%02d%02d%02d.%06ldZ", | ||
| tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, | ||
| tm.tm_min, tm.tm_sec, (long)tv.tv_usec); | ||
| len = snprintf(tb->buf, sizeof(tb->buf), | ||
| "%4d%02d%02dT%02d%02d%02d.%06ldZ", | ||
| tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, | ||
| tm.tm_hour, tm.tm_min, tm.tm_sec, (long)tv.tv_usec); | ||
|
|
||
| if (len < 0 || (size_t)len >= sizeof(tb->buf)) { | ||
| const char *blank = "19000000T000000.000000Z"; | ||
| strlcpy(tb->buf, blank, sizeof(tb->buf)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Junio C Hamano wrote on the Git mailing list (how to reply to this email):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Derrick Stolee wrote on the Git mailing list (how to reply to this email):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Patrick Steinhardt wrote on the Git mailing list (how to reply to this email):