On Sun, Aug 25, 2019 at 01:13:48PM +0900, Mike Hommey wrote:

> command_buf.buf is also stored in cmd_hist, so instead of
> strbuf_release, the current code uses strbuf_detach in order to
> "leak" the buffer as far as the strbuf is concerned.
> 
> However, strbuf_detach does more than "leak" the strbuf buffer: it
> possibly reallocates it to ensure a terminating nul character. And when
> that happens, what is already stored in cmd_hist may now be a free()d
> buffer.
> 
> In practice, though, command_buf.buf is most of the time a nul
> terminated string, meaning command_buf.len < command_buf.alloc, and
> strbuf_detach is a no-op. BUT, when it's not (e.g. on the first call),
> command_buf.buf is &strbuf_slopbuf. In that case, strbuf_detach does
> allocate a 1 byte buffer to store a nul character in it, which is then
> leaked.

I think this is stronger than just "most of the time". It's an invariant
for strbufs to have a NUL, so the only case where detaching isn't a noop
is the empty slopbuf case you mention.

Splitting hairs, perhaps, but I think with that explanation, we could
probably argue that this case will never come up: strbuf_getline will
either have allocated a buffer or will have returned EOF.

That said, I do think it's quite confusing and is worth fixing, both for
readability and for future-proofing. But...

> diff --git a/fast-import.c b/fast-import.c
> index b44d6a467e..b1d07efe8c 100644
> --- a/fast-import.c
> +++ b/fast-import.c
> @@ -1763,7 +1763,9 @@ static int read_next_command(void)
>               } else {
>                       struct recent_command *rc;
>  
> -                     strbuf_detach(&command_buf, NULL);
> +                     // command_buf is enther empty or also stored in 
> cmd_hist,
> +                     // reinitialize it.
> +                     strbuf_init(&command_buf, 0);

This whole thing is a sign that the code is Doing It Wrong. Whoever is
taking ownership of the buffer should be calling strbuf_detach() at that
point.

It's a bit tricky, though, because we take ownership and then expect
people still look at command_buf.buf. Which makes me concerned that
there are other operations which might modify the buffer and have the
same issue.

I think it would be much easier to follow if we simply used the same
command_buf over and over, and just copied into the history. The cost is
about the same (we still alloc once per line, though we do an extra
memcpy now). So I thought something like this would work (we don't need
to convert those detaches into a strbuf_reset() calls because
strbuf_getline does so automatically):

diff --git a/fast-import.c b/fast-import.c
index b44d6a467e..31207acd61 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -1763,7 +1763,6 @@ static int read_next_command(void)
                } else {
                        struct recent_command *rc;
 
-                       strbuf_detach(&command_buf, NULL);
                        stdin_eof = strbuf_getline_lf(&command_buf, stdin);
                        if (stdin_eof)
                                return EOF;
@@ -1784,7 +1783,7 @@ static int read_next_command(void)
                                free(rc->buf);
                        }
 
-                       rc->buf = command_buf.buf;
+                       rc->buf = xstrdup(command_buf.buf);
                        rc->prev = cmd_tail;
                        rc->next = cmd_hist.prev;
                        rc->prev->next = rc;
@@ -1833,7 +1832,6 @@ static int parse_data(struct strbuf *sb, uintmax_t limit, 
uintmax_t *len_res)
                char *term = xstrdup(data);
                size_t term_len = command_buf.len - (data - command_buf.buf);
 
-               strbuf_detach(&command_buf, NULL);
                for (;;) {
                        if (strbuf_getline_lf(&command_buf, stdin) == EOF)
                                die("EOF in data (terminator '%s' not found)", 
term);

But it doesn't! It turns out that there are other places in the code
which assume they can call read_next_command() while hanging onto the
existing buffer. Which only works in the old code because the history
feature happens to hold on to the old pointer.

If I do this on top, then all tests pass:

diff --git a/fast-import.c b/fast-import.c
index 31207acd61..1f9160b645 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -2586,7 +2586,7 @@ static void parse_new_commit(const char *arg)
        struct branch *b;
        char *author = NULL;
        char *committer = NULL;
-       const char *encoding = NULL;
+       char *encoding = NULL;
        struct hash_list *merge_list = NULL;
        unsigned int merge_count;
        unsigned char prev_fanout, new_fanout;
@@ -2609,8 +2609,10 @@ static void parse_new_commit(const char *arg)
        }
        if (!committer)
                die("Expected committer but didn't get one");
-       if (skip_prefix(command_buf.buf, "encoding ", &encoding))
+       if (skip_prefix(command_buf.buf, "encoding ", &v)) {
+               encoding = xstrdup(v);
                read_next_command();
+       }
        parse_data(&msg, 0, NULL);
        read_next_command();
        parse_from(b);
@@ -2684,6 +2686,7 @@ static void parse_new_commit(const char *arg)
        strbuf_addbuf(&new_data, &msg);
        free(author);
        free(committer);
+       free(encoding);
 
        if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))
                b->pack_id = pack_id;

And I think this is actually a real bug in the current code! We keep a
pointer to the encoding string, which survives because of the history.
But that history is bounded, and we could have an indefinite number of
changed files in the middle. If I modify t9300 like this:

diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh
index 141b7fa35e..d4bbe630d5 100755
--- a/t/t9300-fast-import.sh
+++ b/t/t9300-fast-import.sh
@@ -3314,6 +3314,11 @@ test_expect_success 'X: handling encoding' '
 
        printf "Pi: \360\nCOMMIT\n" >>input &&
 
+       for i in $(test_seq 200)
+       do
+               echo "M 644 $EMPTY_BLOB file-$i"
+       done >>input &&
+
        git fast-import <input &&
        git cat-file -p encoding | grep $(printf "\360") &&
        git log -1 --format=%B encoding | grep $(printf "\317\200")

and run the test suite (using an unmodified git, without the earlier
patches I showed) then the test fails, putting garbage into the encoding
header (and when compiled with ASan, reports a use-after-free).

So I think the way the string handling is currently done papers over
such problems. You only see the problem if you have a hundred or more
modified files, so it works _most_ of the time.

That implies to me it's worth following a fix like the one I showed
above. I am slightly concerned there are other similar cases to the
"encoding" one lurking (and they _might_ not be bugs already, if there's
a fixed number of reads between the pointer being saved and being used),
but it seems worth the risk.

-Peff

Reply via email to