threads / patch / 40742

patchwt-status: use strncmp() for length-limited string comparison

Subject: [PATCH] wt-status: use strncmp() for length-limited string comparison

## tl;dr

8 messages between Nov 6, 2015 and Nov 28, 2015. Diffs are folded; open one to read it.

replies: 7people: 3as markdown or json

René Scharfe· Nov 6, 2015, 22:47 UTC · lore

When a branch name is longer than four characters, memcmp() can read past the end of the string literal "HEAD". Use strncmp() instead, which stops at the end of a string. This fixes the following test failures with AddressSanitizer:

t3203-branch-output.sh                           (Wstat: 256 Tests: 18 Failed: 4)
  Failed tests:  12, 15-17
  Non-zero exit status: 1
t3412-rebase-root.sh                             (Wstat: 256 Tests: 31 Failed: 3)
  Failed tests:  28-29, 31
  Non-zero exit status: 1
t3507-cherry-pick-conflict.sh                    (Wstat: 256 Tests: 31 Failed: 4)
  Failed tests:  14, 29-31
  Non-zero exit status: 1
t3510-cherry-pick-sequence.sh                    (Wstat: 256 Tests: 39 Failed: 14)
  Failed tests:  17, 22-26, 28-30, 34-35, 37-39
  Non-zero exit status: 1
t3420-rebase-autostash.sh                        (Wstat: 256 Tests: 28 Failed: 4)
  Failed tests:  24-27
  Non-zero exit status: 1
t3404-rebase-interactive.sh                      (Wstat: 256 Tests: 91 Failed: 57)
  Failed tests:  17, 19, 21-42, 44, 46-74, 77, 81-82
  Non-zero exit status: 1
t3900-i18n-commit.sh                             (Wstat: 256 Tests: 34 Failed: 1)
  Failed test:  34
  Non-zero exit status: 1
t5407-post-rewrite-hook.sh                       (Wstat: 256 Tests: 14 Failed: 6)
  Failed tests:  9-14
  Non-zero exit status: 1
t7001-mv.sh                                      (Wstat: 256 Tests: 46 Failed: 5)
  Failed tests:  39-43
  Non-zero exit status: 1
t7509-commit.sh                                  (Wstat: 256 Tests: 12 Failed: 2)
  Failed tests:  11-12
  Non-zero exit status: 1
t7512-status-help.sh                             (Wstat: 256 Tests: 39 Failed: 35)
  Failed tests:  5-39
  Non-zero exit status: 1
t6030-bisect-porcelain.sh                        (Wstat: 256 Tests: 70 Failed: 1)
  Failed test:  13
  Non-zero exit status: 1
Signed-off-by: Rene Scharfe <l.s.r@web.de>
---
 wt-status.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to wt-status.c +1 −1
diff --git a/wt-status.c b/wt-status.c
index 435fc28..8dc281b 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -1319,7 +1319,7 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
 	hashcpy(cb->nsha1, nsha1);
 	for (end = target; *end && *end != '\n'; end++)
 		;
-	if (!memcmp(target, "HEAD", end - target)) {
+	if (!strncmp(target, "HEAD", end - target)) {
 		/* HEAD is relative. Resolve it to the right reflog entry. */
 		strbuf_addstr(&cb->buf,
 			      find_unique_abbrev(nsha1, DEFAULT_ABBREV));
-- 
2.6.3
Jeff King· Nov 24, 2015, 21:36 UTC · re: René Scharfe · lore

Re: [PATCH] wt-status: use strncmp() for length-limited string comparison

On Fri, Nov 06, 2015 at 11:47:03PM +0100, René Scharfe wrote:
> When a branch name is longer than four characters, memcmp() can read
> past the end of the string literal "HEAD".  Use strncmp() instead, which
> stops at the end of a string.  This fixes the following test failures
> with AddressSanitizer:
Hmm. I think this is mostly harmless, as a comparison like:
  memcmp("HEAD and more", "HEAD", strlen("HEAD"))

would yield non-zero when we compare the NUL in the second string to whatever is in the first. So I assume what is going on is that memcmp is doing larger compares than byte by byte, and is examining 4 or 8 bytes starting at that NUL.

The outcome is equivalent, but we do touch memory that is not ours, so I think this is a positive direction in that sense.

But...
Show 10 quoted lines
> diff --git a/wt-status.c b/wt-status.c
> index 435fc28..8dc281b 100644
> --- a/wt-status.c
> +++ b/wt-status.c
> @@ -1319,7 +1319,7 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
>  	hashcpy(cb->nsha1, nsha1);
>  	for (end = target; *end && *end != '\n'; end++)
>  		;
> -	if (!memcmp(target, "HEAD", end - target)) {
> +	if (!strncmp(target, "HEAD", end - target)) {
This will match prefixes like "HEA" in the target, won't it?
I think you want something more like:
  if (end - target == 4 && !memcmp(target, "HEAD", 4))

I tried to think of a way that didn't involve a magic number. The best I came up with is:

  if (skip_prefix(target, "HEAD", &v) && v == end)
but that requires an extra variable, and is arguably more obfuscated.
-Peff
René Scharfe· Nov 25, 2015, 02:16 UTC · re: Jeff King · lore

Re: [PATCH] wt-status: use strncmp() for length-limited string comparison

Am 24.11.2015 um 22:36 schrieb Jeff King:
Show 18 quoted lines
> On Fri, Nov 06, 2015 at 11:47:03PM +0100, René Scharfe wrote:
> 
>> When a branch name is longer than four characters, memcmp() can read
>> past the end of the string literal "HEAD".  Use strncmp() instead, which
>> stops at the end of a string.  This fixes the following test failures
>> with AddressSanitizer:
> 
> Hmm. I think this is mostly harmless, as a comparison like:
> 
>    memcmp("HEAD and more", "HEAD", strlen("HEAD"))
> 
> would yield non-zero when we compare the NUL in the second string to
> whatever is in the first. So I assume what is going on is that memcmp is
> doing larger compares than byte by byte, and is examining 4 or 8 bytes
> starting at that NUL.
> 
> The outcome is equivalent, but we do touch memory that is not ours, so I
> think this is a positive direction in that sense.

Yes, except it should be strlen("HEAD and more") in your example code; with strlen("HEAD") it would compare just 4 bytes and return 0.

Show 14 quoted lines
> But...
> 
>> diff --git a/wt-status.c b/wt-status.c
>> index 435fc28..8dc281b 100644
>> --- a/wt-status.c
>> +++ b/wt-status.c
>> @@ -1319,7 +1319,7 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
>>   	hashcpy(cb->nsha1, nsha1);
>>   	for (end = target; *end && *end != '\n'; end++)
>>   		;
>> -	if (!memcmp(target, "HEAD", end - target)) {
>> +	if (!strncmp(target, "HEAD", end - target)) {
> 
> This will match prefixes like "HEA" in the target, won't it?
Oww, yes. :-/  NB: The existing code does the same.
Show 10 quoted lines
> I think you want something more like:
> 
>    if (end - target == 4 && !memcmp(target, "HEAD", 4))
> 
> I tried to think of a way that didn't involve a magic number. The best I
> came up with is:
> 
>    if (skip_prefix(target, "HEAD", &v) && v == end)
> 
> but that requires an extra variable, and is arguably more obfuscated.

Using one more variable isn't that bad, as long as it gets a fitting name. Or we could reuse "end" (I'm not worrying about scanning "HEAD" twice very much):

Show changes to wt-status.c +3 −3
diff --git a/wt-status.c b/wt-status.c
index 435fc28..96a731e 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -1317,14 +1317,14 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
 	target += strlen(" to ");
 	strbuf_reset(&cb->buf);
 	hashcpy(cb->nsha1, nsha1);
-	for (end = target; *end && *end != '\n'; end++)
-		;
-	if (!memcmp(target, "HEAD", end - target)) {
+	if (skip_prefix(target, "HEAD", &end) && (!*end || *end == '\n')) {
 		/* HEAD is relative. Resolve it to the right reflog entry. */
 		strbuf_addstr(&cb->buf,
 			      find_unique_abbrev(nsha1, DEFAULT_ABBREV));
 		return 1;
 	}
+	for (end = target; *end && *end != '\n'; end++)
+		;
 	strbuf_add(&cb->buf, target, end - target);
 	return 1;
 }
Jeff King· Nov 25, 2015, 09:15 UTC · re: René Scharfe · lore

Re: [PATCH] wt-status: use strncmp() for length-limited string comparison

On Wed, Nov 25, 2015 at 03:16:49AM +0100, René Scharfe wrote:
Show 7 quoted lines
> > Hmm. I think this is mostly harmless, as a comparison like:
> > 
> >    memcmp("HEAD and more", "HEAD", strlen("HEAD"))
> [...]
> 
> Yes, except it should be strlen("HEAD and more") in your example code;
> with strlen("HEAD") it would compare just 4 bytes and return 0.
Whoops, yeah. Thank you for figuring out what I meant. :)
Show 21 quoted lines
> Using one more variable isn't that bad, as long as it gets a fitting
> name.  Or we could reuse "end" (I'm not worrying about scanning "HEAD"
> twice very much):
> 
> diff --git a/wt-status.c b/wt-status.c
> index 435fc28..96a731e 100644
> --- a/wt-status.c
> +++ b/wt-status.c
> @@ -1317,14 +1317,14 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
>  	target += strlen(" to ");
>  	strbuf_reset(&cb->buf);
>  	hashcpy(cb->nsha1, nsha1);
> -	for (end = target; *end && *end != '\n'; end++)
> -		;
> -	if (!memcmp(target, "HEAD", end - target)) {
> +	if (skip_prefix(target, "HEAD", &end) && (!*end || *end == '\n')) {
>  		/* HEAD is relative. Resolve it to the right reflog entry. */
>  		strbuf_addstr(&cb->buf,
>  			      find_unique_abbrev(nsha1, DEFAULT_ABBREV));
>  		return 1;
>  	}

Yeah, I think parsing left-to-right like this makes things much more obvious. And regarding scanning HEAD twice, I think we already do that (we find the trailing newline first in the current code). Though I agree that is absurd premature optimization.

> +	for (end = target; *end && *end != '\n'; end++)
> +		;

This loop (which I know you just moved, not wrote) is basically strchrnul, isn't it? That might be more readable.

-Peff
Matthieu Moy· Nov 25, 2015, 10:29 UTC · re: Jeff King · lore

Re: [PATCH] wt-status: use strncmp() for length-limited string comparison

Jeff King <peff@peff.net> writes:
Show 20 quoted lines
>> diff --git a/wt-status.c b/wt-status.c
>> index 435fc28..96a731e 100644
>> --- a/wt-status.c
>> +++ b/wt-status.c
>> @@ -1317,14 +1317,14 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
>>  	target += strlen(" to ");
>>  	strbuf_reset(&cb->buf);
>>  	hashcpy(cb->nsha1, nsha1);
>> -	for (end = target; *end && *end != '\n'; end++)
>> -		;
>> -	if (!memcmp(target, "HEAD", end - target)) {
>> +	if (skip_prefix(target, "HEAD", &end) && (!*end || *end == '\n')) {
>>  		/* HEAD is relative. Resolve it to the right reflog entry. */
>>  		strbuf_addstr(&cb->buf,
>>  			      find_unique_abbrev(nsha1, DEFAULT_ABBREV));
>>  		return 1;
>>  	}
>
> Yeah, I think parsing left-to-right like this makes things much more
> obvious.
Agreed.
Show 5 quoted lines
>> +	for (end = target; *end && *end != '\n'; end++)
>> +		;
>
> This loop (which I know you just moved, not wrote) is basically
> strchrnul, isn't it? That might be more readable.
Agreed too.
-- 
Matthieu Moy
http://www-verimag.imag.fr/~moy/
René Scharfe· Nov 25, 2015, 14:10 UTC · re: Jeff King · lore

[PATCH v2] wt-status: correct and simplify check for detached HEAD

If a branch name is longer than four characters then memcmp() reads over the end of the static string "HEAD". This causes the following test failures with AddressSanitizer:

t3203-branch-output.sh                           (Wstat: 256 Tests: 18 Failed: 4)
  Failed tests:  12, 15-17
  Non-zero exit status: 1
t3412-rebase-root.sh                             (Wstat: 256 Tests: 31 Failed: 3)
  Failed tests:  28-29, 31
  Non-zero exit status: 1
t3507-cherry-pick-conflict.sh                    (Wstat: 256 Tests: 31 Failed: 4)
  Failed tests:  14, 29-31
  Non-zero exit status: 1
t3510-cherry-pick-sequence.sh                    (Wstat: 256 Tests: 39 Failed: 14)
  Failed tests:  17, 22-26, 28-30, 34-35, 37-39
  Non-zero exit status: 1
t3420-rebase-autostash.sh                        (Wstat: 256 Tests: 28 Failed: 4)
  Failed tests:  24-27
  Non-zero exit status: 1
t3404-rebase-interactive.sh                      (Wstat: 256 Tests: 91 Failed: 57)
  Failed tests:  17, 19, 21-42, 44, 46-74, 77, 81-82
  Non-zero exit status: 1
t3900-i18n-commit.sh                             (Wstat: 256 Tests: 34 Failed: 1)
  Failed test:  34
  Non-zero exit status: 1
t5407-post-rewrite-hook.sh                       (Wstat: 256 Tests: 14 Failed: 6)
  Failed tests:  9-14
  Non-zero exit status: 1
t7001-mv.sh                                      (Wstat: 256 Tests: 46 Failed: 5)
  Failed tests:  39-43
  Non-zero exit status: 1
t7509-commit.sh                                  (Wstat: 256 Tests: 12 Failed: 2)
  Failed tests:  11-12
  Non-zero exit status: 1
t7512-status-help.sh                             (Wstat: 256 Tests: 39 Failed: 35)
  Failed tests:  5-39
  Non-zero exit status: 1
t6030-bisect-porcelain.sh                        (Wstat: 256 Tests: 70 Failed: 1)
  Failed test:  13
  Non-zero exit status: 1

And if a branch is named "H", "HE", or "HEA" then the current if clause erroneously considers it as matching "HEAD" because it only compares up to the end of the branch name.

Fix that by doing the comparison using strcmp() and only after the branch name is extracted. This way neither too less nor too many characters are checked. While at it call strchrnul() to find the end of the branch name instead of open-coding it.

Helped-by: Jeff King <peff@peff.net>
Signed-off-by: Rene Scharfe <l.s.r@web.de>
---
We can be more careful when parsing -- or avoid parsing and backtrack
if we found "HEAD" after all.  The latter is simpler, and string
parsing is tricky enough that we better take such opportunities to
simplify the code..
Changes since v1:
* strcmp() instead of strncmp()
* strchrnul() (unrelated cleanup)
* adjusted subject (and commit message)
 wt-status.c | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)
Show changes to wt-status.c +4 −5
diff --git a/wt-status.c b/wt-status.c
index 435fc28..ced53dd 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -1317,15 +1317,14 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
 	target += strlen(" to ");
 	strbuf_reset(&cb->buf);
 	hashcpy(cb->nsha1, nsha1);
-	for (end = target; *end && *end != '\n'; end++)
-		;
-	if (!memcmp(target, "HEAD", end - target)) {
+	end = strchrnul(target, '\n');
+	strbuf_add(&cb->buf, target, end - target);
+	if (!strcmp(cb->buf.buf, "HEAD")) {
 		/* HEAD is relative. Resolve it to the right reflog entry. */
+		strbuf_reset(&cb->buf);
 		strbuf_addstr(&cb->buf,
 			      find_unique_abbrev(nsha1, DEFAULT_ABBREV));
-		return 1;
 	}
-	strbuf_add(&cb->buf, target, end - target);
 	return 1;
 }
 
-- 
2.6.3
Matthieu Moy· Nov 25, 2015, 16:21 UTC · re: René Scharfe · lore

Re: [PATCH v2] wt-status: correct and simplify check for detached HEAD

René Scharfe <l.s.r@web.de> writes:
Show 23 quoted lines
> diff --git a/wt-status.c b/wt-status.c
> index 435fc28..ced53dd 100644
> --- a/wt-status.c
> +++ b/wt-status.c
> @@ -1317,15 +1317,14 @@ static int grab_1st_switch(unsigned char *osha1, unsigned char *nsha1,
>  	target += strlen(" to ");
>  	strbuf_reset(&cb->buf);
>  	hashcpy(cb->nsha1, nsha1);
> -	for (end = target; *end && *end != '\n'; end++)
> -		;
> -	if (!memcmp(target, "HEAD", end - target)) {
> +	end = strchrnul(target, '\n');
> +	strbuf_add(&cb->buf, target, end - target);
> +	if (!strcmp(cb->buf.buf, "HEAD")) {
>  		/* HEAD is relative. Resolve it to the right reflog entry. */
> +		strbuf_reset(&cb->buf);
>  		strbuf_addstr(&cb->buf,
>  			      find_unique_abbrev(nsha1, DEFAULT_ABBREV));
> -		return 1;
>  	}
> -	strbuf_add(&cb->buf, target, end - target);
>  	return 1;
>  }

Indeed, the code is much simpler like this than with the previous attempts. Looks all right to me.

-- 
Matthieu Moy
http://www-verimag.imag.fr/~moy/
Jeff King· Nov 28, 2015, 17:31 UTC · re: René Scharfe · lore

Re: [PATCH v2] wt-status: correct and simplify check for detached HEAD

On Wed, Nov 25, 2015 at 03:10:18PM +0100, René Scharfe wrote:
Show 12 quoted lines
> Fix that by doing the comparison using strcmp() and only after the
> branch name is extracted.  This way neither too less nor too many
> characters are checked.  While at it call strchrnul() to find the end
> of the branch name instead of open-coding it.
> 
> Helped-by: Jeff King <peff@peff.net>
> Signed-off-by: Rene Scharfe <l.s.r@web.de>
> ---
> We can be more careful when parsing -- or avoid parsing and backtrack
> if we found "HEAD" after all.  The latter is simpler, and string
> parsing is tricky enough that we better take such opportunities to
> simplify the code..

Yeah, I think this is nicer. We end up allocating either way anyway, so the only extra effort is copy the string.

-Peff

← back to recent threads