From: Patrick Steinhardt <ps@pks.im>
To: Jeff King <peff@peff.net>
Cc: Nicolas Le Cam <niko.lecam@gmail.com>, git@vger.kernel.org
Subject: Re: [PATCH] revision: hang on to "freed" argv elements
Date: Tue, 1 Sep 2026 10:51:26 +0200 [thread overview]
Message-ID: <apaSDqIEyc82Q_zE@pks.im> (raw)
In-Reply-To: <20260901062815.GC1075462@coredump.intra.peff.net>
On Tue, Sep 01, 2026 at 02:28:15AM -0400, Jeff King wrote:
> On Sun, Aug 30, 2026 at 11:55:55PM +0200, Nicolas Le Cam wrote:
>
> > The prefixes are replaced by fragments of unrelated heap data, and the
> > value changes between runs of the same command:
> >
> > $ git stash show --src-prefix=a/ --dst-prefix=b/ | head -1
> > diff --git Uf.txt Uf.txt
> > $ git stash show --src-prefix=a/ --dst-prefix=b/ | head -1
> > diff --git Vf.txt Vf.txt
> >
> > On other versions the garbage is recognisable as pieces of other
> > strings live in the process -- "ributes" (from "attributes"),
> > "bjectmode" (from "objectmode"), "4c/" -- which is what suggests a
> > use-after-free rather than an off-by-one.
>
> Thanks for a clear and thorough bug report! The cause is indeed the
> related to the commits you found. The explanation (and fix) are below.
>
> -- >8 --
> Subject: revision: hang on to "freed" argv elements
>
> In setup_revisions() we rewrite the incoming argv array, losing
> references to the strings it contains. For a synthetic argv array
> constructed from heap strings, that traditionally meant we leaked those
> allocated strings.
>
> We fixed the leak in cd43948798 (revision: manage memory ownership of
> argv in setup_revisions(), 2025-09-19). Now callers can tell the
> revision code that argv entries are allocated and should be freed, which
> it will do before overwriting them.
>
> But this introduced a new bug! The overwritten entries go away as soon
> as option parsing is finished, but a few options may actually create new
> references to those strings. And once we free the strings, those stale
> references become use-after-free bugs. For example, running:
>
> git stash show --src-prefix=foo/
>
> demonstrates the problem:
>
> 1. The stash command generates its own synthetic argv (because it has
> to treat the stash specifiers specially) which it then passes to
> setup_revisions().
>
> 2. Parsing will create a reference to the partial string "foo/" in
> revs.diffopt.a_prefix.
>
> 3. When setup_revisions() finishes, we rewrite argv to throw away
> parsed strings. This frees the entry holding "--src-prefix=foo",
> at which point we have a dangling reference in revs.diffopt.
>
> 4. We generate an actual diff, accessing garbage memory via
> revs.diffopt.a_prefix. The output is usually garbled, but ASan also
> detects this reliably.
>
> One obvious fix here is to allocate new strings when we pull data out of
> the argv array. But doing so is error prone (every string option must
> remember to do it or risk a subtle bug), and creates more questions
> about memory ownership (e.g., some callers assign string literals
> directly to a_prefix, and we would not want to free those).
>
> Instead we can fix this centrally by delaying the free() calls. We'll
> collect any "freed" strings in a new array, hold on to it for the life
> of the rev_info struct, and then release it at the end. We can easily
> use a strvec for this, since it handles growth and cleanup for us.
>
> This fixes the prefix case above (which is now tested in t3903), and
> should fix any other stray cases. Though I could not find any; we use
> OPT_STRING only in the prefix diff options, and very few revision opts
> store strings. Those that do (like --format and --encoding) already make
> a copy of the string. They do not need for us to hold on to the memory
> longer, but it does not hurt them if we do.
So the fix could've been as trivial as you mention above, where we
simply perform a copy of the string for "--src-prefix", and everything
else works just fine?
In any case though, your approach is more defensive and makes it way
harder for such use-after-free bugs to be introduced going forward, so
I'm in line with the proposed patch.
> diff --git a/revision.c b/revision.c
> index 50dc8b1991..7aee96bd8e 100644
> --- a/revision.c
> +++ b/revision.c
> @@ -2307,9 +2307,27 @@ static timestamp_t parse_age(const char *arg)
> return num;
> }
>
> +/*
> + * When asked to free argv strings, we should not do so immediately. Some
> + * option parsing may have stored a reference to the string (either the whole
> + * thing, or a substring inside it). We should keep it valid until the rev_info
> + * struct itself is freed.
> + *
> + * Note that we take a const str for the convenience of callers (who have the
> + * usual const argv array, even when opt->free_removed_argv_elements is set).
> + * We cast away the const on their behalf.
> + */
> +static void mark_argv_for_free(struct rev_info *revs, const char *str)
> +{
> + if (!str)
> + return;
> + strvec_push_nodup(&revs->argv_to_free, (char *)str);
> +}
Hm. Doesn't this mean that we take ownership of the string and then
eventually try to release it when releasing the vector? I wonder whether
this could introduce subtle lifetime issues where the caller passes a
non-heap-allocated string.
I don't think it's that bad when seeing where we use these. But I feel
like hiding this fact by marking the parameter as `const` is a bit of a
weird design choice. I'd much rather prefer we force this onto the
callers so that they are aware of this, but I haven't seen the end
result of that. So maybe it's just too ugly.
Thanks!
Patrick
next prev parent reply other threads:[~2026-09-01 8:51 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-30 21:55 [BUG] git stash show --src-prefix prints freed memory since 2.52.0 Nicolas Le Cam
2026-09-01 6:28 ` [PATCH] revision: hang on to "freed" argv elements Jeff King
2026-09-01 6:36 ` [PATCH 2/1] revision: simplify mark_argv_for_free() callers Jeff King
2026-09-01 18:02 ` Junio C Hamano
2026-09-01 8:51 ` Patrick Steinhardt [this message]
2026-09-01 9:21 ` [PATCH] revision: hang on to "freed" argv elements Jeff King
2026-09-01 11:08 ` Patrick Steinhardt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=apaSDqIEyc82Q_zE@pks.im \
--to=ps@pks.im \
--cc=git@vger.kernel.org \
--cc=niko.lecam@gmail.com \
--cc=peff@peff.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox