diff mbox series

[06/11] test-cutils: Add more coverage to qemu_strtosz

Message ID 20230508200343.791450-7-eblake@redhat.com
State New
Headers show
Series Fix qemu_strtosz() read-out-of-bounds | expand

Commit Message

Eric Blake May 8, 2023, 8:03 p.m. UTC
Add some more strings that the user might send our way.  In
particular, some of these additions include FIXME comments showing
where our parser doesn't quite behave the way we want.

Signed-off-by: Eric Blake <eblake@redhat.com>
---
 tests/unit/test-cutils.c | 226 +++++++++++++++++++++++++++++++++++++--
 1 file changed, 215 insertions(+), 11 deletions(-)

Comments

Hanna Czenczek May 9, 2023, 12:31 p.m. UTC | #1
On 08.05.23 22:03, Eric Blake wrote:
> Add some more strings that the user might send our way.  In
> particular, some of these additions include FIXME comments showing
> where our parser doesn't quite behave the way we want.
>
> Signed-off-by: Eric Blake <eblake@redhat.com>
> ---
>   tests/unit/test-cutils.c | 226 +++++++++++++++++++++++++++++++++++++--
>   1 file changed, 215 insertions(+), 11 deletions(-)

I wonder: The plan is to have "1.5e+1k" be parsed as "1.5e" + endptr == 
"+1k"; but "0x1p1" is not parsed at all (could be "0x1" + "p1"). Is that 
fully intentional?

(Similarly, "1.1.k" is also not parsed at all, but the problem there is 
not just two decimal points, but also that "1.1" would be an invalid 
size in itself, so it really shouldn’t be parsed at all.)

I don’t think it matters to users, really, but I still wonder.

> diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
> index afae2ee5331..9fa6fb042e8 100644
> --- a/tests/unit/test-cutils.c
> +++ b/tests/unit/test-cutils.c

[...]

> @@ -2875,6 +3056,20 @@ static void test_qemu_strtosz_trailing(void)
>       err = qemu_strtosz(str, NULL, &res);
>       g_assert_cmpint(err, ==, -EINVAL);
>       g_assert_cmphex(res, ==, 0xbaadf00d);
> +
> +    /* FIXME overflow in fraction is buggy */
> +    str = "1.5E999";
> +    endptr = NULL;
> +    res = 0xbaadf00d;
> +    err = qemu_strtosz(str, &endptr, &res);
> +    g_assert_cmpint(err, ==, 0);
> +    g_assert_cmpuint(res, ==, EiB /* FIXME EiB * 1.5 */);
> +    g_assert(endptr == str + 9 /* FIXME + 4 */);

This is “correct” (i.e. it’s the value we’ll get right now, which is the 
wrong one), but gcc complains that the array index is out of bounds 
(well...), which breaks the build.

Hanna

> +
> +    res = 0xbaadf00d;
> +    err = qemu_strtosz(str, NULL, &res);
> +    g_assert_cmpint(err, ==, -EINVAL);
> +    g_assert_cmphex(res, ==, 0xbaadf00d);
>   }
Hanna Czenczek May 9, 2023, 12:42 p.m. UTC | #2
On 09.05.23 14:31, Hanna Czenczek wrote:
> On 08.05.23 22:03, Eric Blake wrote:
>> Add some more strings that the user might send our way.  In
>> particular, some of these additions include FIXME comments showing
>> where our parser doesn't quite behave the way we want.
>>
>> Signed-off-by: Eric Blake <eblake@redhat.com>
>> ---
>>   tests/unit/test-cutils.c | 226 +++++++++++++++++++++++++++++++++++++--
>>   1 file changed, 215 insertions(+), 11 deletions(-)
>
> I wonder: The plan is to have "1.5e+1k" be parsed as "1.5e" + endptr 
> == "+1k"; but "0x1p1" is not parsed at all (could be "0x1" + "p1"). Is 
> that fully intentional?
>
> (Similarly, "1.1.k" is also not parsed at all, but the problem there 
> is not just two decimal points, but also that "1.1" would be an 
> invalid size in itself, so it really shouldn’t be parsed at all.)
>
> I don’t think it matters to users, really, but I still wonder.
>
>> diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
>> index afae2ee5331..9fa6fb042e8 100644
>> --- a/tests/unit/test-cutils.c
>> +++ b/tests/unit/test-cutils.c
>
> [...]
>
>> @@ -2875,6 +3056,20 @@ static void test_qemu_strtosz_trailing(void)
>>       err = qemu_strtosz(str, NULL, &res);
>>       g_assert_cmpint(err, ==, -EINVAL);
>>       g_assert_cmphex(res, ==, 0xbaadf00d);
>> +
>> +    /* FIXME overflow in fraction is buggy */
>> +    str = "1.5E999";
>> +    endptr = NULL;
>> +    res = 0xbaadf00d;
>> +    err = qemu_strtosz(str, &endptr, &res);
>> +    g_assert_cmpint(err, ==, 0);
>> +    g_assert_cmpuint(res, ==, EiB /* FIXME EiB * 1.5 */);

So…  I have no idea what happens here but this always fails with 
“assertion failed (res == EiB): (1 == 1152921504606846976)”.  But when I 
replace the EiB by 1, it suddenly fails with “assertion failed (res == 
1): (1152921504606846976 == 1)” instead.  Replacing the EiB by anything 
but 1 also tells me that res is 1.

Now, here’s the kicker.  I put an `fprintf(stderr, "res == %" PRIu64 
"\n", res);` before this g_assert_cmpuint() (changed to (res, ==, 1))…  
And it passes.

Sometimes I really want to change professions.

(Of note is that changing the g_assert() below into a g_assert_true() 
also has g_assert_cmpuint(res, ==, 1) pass.)

>> +    g_assert(endptr == str + 9 /* FIXME + 4 */);
>
> This is “correct” (i.e. it’s the value we’ll get right now, which is 
> the wrong one), but gcc complains that the array index is out of 
> bounds (well...), which breaks the build.

Oh, it also isn’t correct, I think it needs to be str + 8.  As a bonus, 
the compiler doesn’t complain then (for some reason…?  it still seems 
out of bounds).

(Otherwise, to get around the complaint, I used 
g_assert_cmphex((uintptr_t)endptr, ==, (uintptr_t)str + 8).  Which is 
another thing, patch 1 explained to me that we shouldn’t use g_assert() :))

Hanna
Hanna Czenczek May 9, 2023, 3:15 p.m. UTC | #3
On 08.05.23 22:03, Eric Blake wrote:
> Add some more strings that the user might send our way.  In
> particular, some of these additions include FIXME comments showing
> where our parser doesn't quite behave the way we want.
>
> Signed-off-by: Eric Blake <eblake@redhat.com>
> ---
>   tests/unit/test-cutils.c | 226 +++++++++++++++++++++++++++++++++++++--
>   1 file changed, 215 insertions(+), 11 deletions(-)
>
> diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
> index afae2ee5331..9fa6fb042e8 100644
> --- a/tests/unit/test-cutils.c
> +++ b/tests/unit/test-cutils.c

[...]

> @@ -2875,6 +3056,20 @@ static void test_qemu_strtosz_trailing(void)
>       err = qemu_strtosz(str, NULL, &res);
>       g_assert_cmpint(err, ==, -EINVAL);
>       g_assert_cmphex(res, ==, 0xbaadf00d);
> +
> +    /* FIXME overflow in fraction is buggy */
> +    str = "1.5E999";
> +    endptr = NULL;
> +    res = 0xbaadf00d;
> +    err = qemu_strtosz(str, &endptr, &res);
> +    g_assert_cmpint(err, ==, 0);
> +    g_assert_cmpuint(res, ==, EiB /* FIXME EiB * 1.5 */);
> +    g_assert(endptr == str + 9 /* FIXME + 4 */);
> +
> +    res = 0xbaadf00d;
> +    err = qemu_strtosz(str, NULL, &res);
> +    g_assert_cmpint(err, ==, -EINVAL);
> +    g_assert_cmphex(res, ==, 0xbaadf00d);

Got it now!

Our problem is that `endptr` is beyond the end of the string, precisely 
as gcc complains.  The data there is undefined, and depending on the 
value in the g_assert_cmpuint() (which is converted to strings for the 
potential error message) it sometimes is "endptr == str + 9" (the one in 
the g_assert()) and sometimes isn’t.

If it is "endptr == str + 9", then the 'e' is taken as a suffix, which 
makes `res == EiB`, and `endptr == "ndptr == str + 9"`.

If it isn’t, well, it might be anything, so there often is no valid 
suffix, making `res == 1`.

So the solution is to set `str = "1.5E999\0\0"`, so we don’t get out of 
bounds and know exactly what will be parsed.  Then, at str[8] there is 
no valid suffix (it’s \0), so `res == 1` and `endptr == str + 8`.  This 
will then lead to the qemu_strtosz(str, NULL, &res) below succeed, 
because, well, it’s a valid number.  I suppose it failed on your end 
because the out-of-bounds `str[9]` value was not '\0'.

That was a fun debugging session.

Hanna
Eric Blake May 9, 2023, 3:50 p.m. UTC | #4
On Tue, May 09, 2023 at 05:15:04PM +0200, Hanna Czenczek wrote:
> On 08.05.23 22:03, Eric Blake wrote:
> > Add some more strings that the user might send our way.  In
> > particular, some of these additions include FIXME comments showing
> > where our parser doesn't quite behave the way we want.
> > 
> > Signed-off-by: Eric Blake <eblake@redhat.com>
> > ---
> >   tests/unit/test-cutils.c | 226 +++++++++++++++++++++++++++++++++++++--
> >   1 file changed, 215 insertions(+), 11 deletions(-)
> > 
> > diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
> > index afae2ee5331..9fa6fb042e8 100644
> > --- a/tests/unit/test-cutils.c
> > +++ b/tests/unit/test-cutils.c
> 
> [...]
> 
> > @@ -2875,6 +3056,20 @@ static void test_qemu_strtosz_trailing(void)
> >       err = qemu_strtosz(str, NULL, &res);
> >       g_assert_cmpint(err, ==, -EINVAL);
> >       g_assert_cmphex(res, ==, 0xbaadf00d);
> > +
> > +    /* FIXME overflow in fraction is buggy */
> > +    str = "1.5E999";
> > +    endptr = NULL;
> > +    res = 0xbaadf00d;
> > +    err = qemu_strtosz(str, &endptr, &res);
> > +    g_assert_cmpint(err, ==, 0);
> > +    g_assert_cmpuint(res, ==, EiB /* FIXME EiB * 1.5 */);
> > +    g_assert(endptr == str + 9 /* FIXME + 4 */);
> > +
> > +    res = 0xbaadf00d;
> > +    err = qemu_strtosz(str, NULL, &res);
> > +    g_assert_cmpint(err, ==, -EINVAL);
> > +    g_assert_cmphex(res, ==, 0xbaadf00d);
> 
> Got it now!
> 
> Our problem is that `endptr` is beyond the end of the string, precisely as
> gcc complains.  The data there is undefined, and depending on the value in
> the g_assert_cmpuint() (which is converted to strings for the potential
> error message) it sometimes is "endptr == str + 9" (the one in the
> g_assert()) and sometimes isn’t.
> 
> If it is "endptr == str + 9", then the 'e' is taken as a suffix, which makes
> `res == EiB`, and `endptr == "ndptr == str + 9"`.
> 
> If it isn’t, well, it might be anything, so there often is no valid suffix,
> making `res == 1`.
> 
> So the solution is to set `str = "1.5E999\0\0"`, so we don’t get out of
> bounds and know exactly what will be parsed.  Then, at str[8] there is no
> valid suffix (it’s \0), so `res == 1` and `endptr == str + 8`.  This will
> then lead to the qemu_strtosz(str, NULL, &res) below succeed, because, well,
> it’s a valid number.  I suppose it failed on your end because the
> out-of-bounds `str[9]` value was not '\0'.
> 
> That was a fun debugging session.

Wow, yeah, that's a mess.  The very test proving that we have a
read-out-of-bounds bug is super-sensitive to what is at that location.
Your hack of passing in extra \0 is awesome; I'll fold that in whether
we need a v2 for other reasons, or in-place if we can take the rest of
this series as-is.  It all goes away at the end of the series,
anyways, once the out-of-bounds read is fixed.
Eric Blake May 9, 2023, 4:06 p.m. UTC | #5
On Tue, May 09, 2023 at 02:31:08PM +0200, Hanna Czenczek wrote:
> On 08.05.23 22:03, Eric Blake wrote:
> > Add some more strings that the user might send our way.  In
> > particular, some of these additions include FIXME comments showing
> > where our parser doesn't quite behave the way we want.
> > 
> > Signed-off-by: Eric Blake <eblake@redhat.com>
> > ---
> >   tests/unit/test-cutils.c | 226 +++++++++++++++++++++++++++++++++++++--
> >   1 file changed, 215 insertions(+), 11 deletions(-)
> 
> I wonder: The plan is to have "1.5e+1k" be parsed as "1.5e" + endptr ==
> "+1k"; but "0x1p1" is not parsed at all (could be "0x1" + "p1"). Is that
> fully intentional?

Pre-series: "1.5e+1k" is parsed as "1" ".5e+1" "k", no slop
Post-series: "1.5e+1k" is parsed as "1" ".5" "e", slop "+1k"

If you call qemu_strtosz(,NULL,), both versions still give EINVAL
error (pre-patch: we detected an exponent in the middle portion of the
parse, which is not allowed; post-series: we detect slop, which is not
allowed).  If you call qemu_strtosz(,&endptr), the pre-patch version
fails with EINVAL but the new code succeeds.  But of all the callers,
the only one that passed non-NULL endptr did it's own check that *slop
is only whitespace ("+1k" fails that check), so the end result is that
at the high level, we reject "1.5e+1k" from the user, but the
rejection is now at a different point in the call stack.  The reason I
made the change is that it was less code to guarantee that the
internal qemu_strtod_finite() is passed a string that cannot possibly
contain an exponent, than to blindly call qemu_strtod_finite() on an
arbitrary suffix and then check after the fact on whether an exponent
was actually consumed as part of that parse.

For "0x1p1", the parse is "0x1" plus slop "p1" (unchanged by the
series).  This is rejected with EINVAL (the moment you have hex, we
insist on no suffix or slop, regardless of endptr being NULL or not).
I could have changed it similar to how I changed "1.5e+1k" to allow
the parse at the qemu_strtosz layer and then detect the slop at the
caller, but that was more lines of code and I didn't feel like it was
worth it.

Either way, the unit tests accurately cover the difference in parse
behavior, and I tried to make the documentation (by patch 11/11) be
consistent as well.  But since this is why we have a review process,
I'm open to feedback if you think I need to tweak which parse styles
we should be favoring.

> 
> (Similarly, "1.1.k" is also not parsed at all, but the problem there is not
> just two decimal points, but also that "1.1" would be an invalid size in
> itself, so it really shouldn’t be parsed at all.)

"1.1k" is a valid (if unusual) size.  This particular test addition
did not happen to improve coverage for my final code, but given that
qemu_strtod_finite(".1.k") would stop parsing after the ".1" and still
be pointing at '.', and our read-out-of-bounds was caused by assuming
that qemu_strtod_finite() failures leave *endptr == '.' (which was
invalidated for ".9e999" failing with ERANGE), it didn't hurt to add
the coverage.

> 
> I don’t think it matters to users, really, but I still wonder.

If any of our callers had actually done something like: "size %llx
followed by junk %s" with the parsed value and the junk string, then
it would matter.  But since all of the callers either passed in NULL
endptr (and got EINVAL before even knowing how much of the string was
parsed) or insisted that the junk be whitespace only, and did not
print details about the parsed value before reporting such errors,
you're right that I don't see this as mattering to end users.  But it
took me quite a bit of audit time to convince myself of that.

> > @@ -2875,6 +3056,20 @@ static void test_qemu_strtosz_trailing(void)
> >       err = qemu_strtosz(str, NULL, &res);
> >       g_assert_cmpint(err, ==, -EINVAL);
> >       g_assert_cmphex(res, ==, 0xbaadf00d);
> > +
> > +    /* FIXME overflow in fraction is buggy */
> > +    str = "1.5E999";
> > +    endptr = NULL;
> > +    res = 0xbaadf00d;
> > +    err = qemu_strtosz(str, &endptr, &res);
> > +    g_assert_cmpint(err, ==, 0);
> > +    g_assert_cmpuint(res, ==, EiB /* FIXME EiB * 1.5 */);
> > +    g_assert(endptr == str + 9 /* FIXME + 4 */);
> 
> This is “correct” (i.e. it’s the value we’ll get right now, which is the
> wrong one), but gcc complains that the array index is out of bounds
> (well...), which breaks the build.

Huh - wonder what build settings you are using that I wasn't for
seeing that warning - but it makes total sense that 'str + 9' would
trigger a build failure if we don't pad str with enough '\0' to keep
things in bounds.
Eric Blake May 9, 2023, 4:10 p.m. UTC | #6
On Mon, May 08, 2023 at 03:03:38PM -0500, Eric Blake wrote:
> Add some more strings that the user might send our way.  In
> particular, some of these additions include FIXME comments showing
> where our parser doesn't quite behave the way we want.
> 
> Signed-off-by: Eric Blake <eblake@redhat.com>
> ---
>  tests/unit/test-cutils.c | 226 +++++++++++++++++++++++++++++++++++++--
>  1 file changed, 215 insertions(+), 11 deletions(-)
> 

> @@ -2704,13 +2749,30 @@ static void test_qemu_strtosz_invalid(void)
> 
>      str = " \t ";
>      endptr = NULL;
> +    res = 0xbaadf00d;
>      err = qemu_strtosz(str, &endptr, &res);
>      g_assert_cmpint(err, ==, -EINVAL);
>      g_assert_cmphex(res, ==, 0xbaadf00d);
>      g_assert_true(endptr == str);
> 
> +    str = ".";
> +    endptr = NULL;
> +    res = 0xbaadf00d;
> +    err = qemu_strtosz(str, &endptr, &res);
> +    g_assert_cmpint(err, ==, -EINVAL);
> +    g_assert_cmphex(res, ==, 0xbaadf00d);
> +    g_assert(endptr == str);

Rebase botch.  I should be using g_assert_true() here in line with
earlier in the series.  I think I cleaned it up later in the series,
but shouldn't be churning on it that badly.  Looks like I get to send
a v2 to fix this and other things; I'll wait another day for other
reviews first.  (That's what I get for rearranging patches after the
fact for a nicer presentation order...)
diff mbox series

Patch

diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
index afae2ee5331..9fa6fb042e8 100644
--- a/tests/unit/test-cutils.c
+++ b/tests/unit/test-cutils.c
@@ -2478,14 +2478,14 @@  static void test_qemu_strtosz_simple(void)
     g_assert_cmpuint(res, ==, 8);
     g_assert_true(endptr == str + 2);

-    /* Leading space is ignored */
-    str = " 12345";
+    /* Leading space and + are ignored */
+    str = " +12345";
     endptr = str;
     res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, 0);
     g_assert_cmpuint(res, ==, 12345);
-    g_assert_true(endptr == str + 6);
+    g_assert_true(endptr == str + 7);

     res = 0xbaadf00d;
     err = qemu_strtosz(str, NULL, &res);
@@ -2564,13 +2564,13 @@  static void test_qemu_strtosz_hex(void)
     g_assert_cmpuint(res, ==, 171);
     g_assert_true(endptr == str + 4);

-    str = "0xae";
+    str = " +0xae";
     endptr = str;
     res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, 0);
     g_assert_cmpuint(res, ==, 174);
-    g_assert_true(endptr == str + 4);
+    g_assert_true(endptr == str + 6);
 }

 static void test_qemu_strtosz_units(void)
@@ -2669,14 +2669,23 @@  static void test_qemu_strtosz_float(void)
     g_assert_cmpuint(res, ==, 1);
     g_assert_true(endptr == str + 4);

-    /* An empty fraction is tolerated */
-    str = "1.k";
+    /* An empty fraction tail is tolerated */
+    str = " 1.k";
     endptr = str;
     res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, 0);
     g_assert_cmpuint(res, ==, 1024);
-    g_assert_true(endptr == str + 3);
+    g_assert_true(endptr == str + 4);
+
+    /* FIXME An empty fraction head should be tolerated */
+    str = " .5k";
+    endptr = str;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL /* FIXME 0 */);
+    g_assert_cmpuint(res, ==, 0xbaadf00d /* FIXME 512 */);
+    g_assert_true(endptr == str /* FIXME + 4 */);

     /* For convenience, we permit values that are not byte-exact */
     str = "12.345M";
@@ -2686,6 +2695,32 @@  static void test_qemu_strtosz_float(void)
     g_assert_cmpint(err, ==, 0);
     g_assert_cmpuint(res, ==, (uint64_t) (12.345 * MiB + 0.5));
     g_assert_true(endptr == str + 7);
+
+    /* FIXME Fraction tail should round correctly */
+    str = "1.9999999999999999999999999999999999999999999999999999k";
+    endptr = str;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpint(res, ==, 1024 /* FIXME 2048 */);
+    g_assert_true(endptr == str + 55);
+
+    /* FIXME ERANGE underflow in the fraction tail should not matter for 'k' */
+    str = "1."
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "1k";
+    endptr = str;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpuint(res, ==, 1 /* FIXME 1024 */);
+    g_assert_true(endptr == str + 354);
 }

 static void test_qemu_strtosz_invalid(void)
@@ -2693,10 +2728,20 @@  static void test_qemu_strtosz_invalid(void)
     const char *str;
     const char *endptr;
     int err;
-    uint64_t res = 0xbaadf00d;
+    uint64_t res;
+
+    /* Must parse at least one digit */
+    str = NULL;
+    endptr = "somewhere";
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+    g_assert_null(endptr);

     str = "";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
@@ -2704,13 +2749,30 @@  static void test_qemu_strtosz_invalid(void)

     str = " \t ";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
     g_assert_true(endptr == str);

+    str = ".";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+    g_assert(endptr == str);
+
+    str = " .";
+    endptr = NULL;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+    g_assert(endptr == str);
+
     str = "crap";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
@@ -2718,6 +2780,7 @@  static void test_qemu_strtosz_invalid(void)

     str = "inf";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
@@ -2725,6 +2788,7 @@  static void test_qemu_strtosz_invalid(void)

     str = "NaN";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
@@ -2733,6 +2797,7 @@  static void test_qemu_strtosz_invalid(void)
     /* Fractional values require scale larger than bytes */
     str = "1.1B";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
@@ -2740,14 +2805,42 @@  static void test_qemu_strtosz_invalid(void)

     str = "1.1";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
     g_assert_true(endptr == str);

+    /* FIXME Fraction tail can cause ERANGE overflow */
+    str = "15.9999999999999999999999999999999999999999999999999999E";
+    endptr = str;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0 /* FIXME -ERANGE */);
+    g_assert_cmpuint(res, ==, 15ULL * EiB /* FIXME 0xbaadf00d */);
+    g_assert_true(endptr == str + 56 /* FIXME str */);
+
+    /* FIXME ERANGE underflow in the fraction tail should matter for 'B' */
+    str = "1."
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "00000000000000000000000000000000000000000000000000"
+        "1B";
+    endptr = str;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0 /* FIXME -EINVAL */);
+    g_assert_cmpuint(res, ==, 1 /* FIXME 0xbaadf00d */);
+    g_assert_true(endptr == str + 354 /* FIXME str */);
+
     /* No hex fractions */
     str = "0x1.8k";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
@@ -2756,14 +2849,24 @@  static void test_qemu_strtosz_invalid(void)
     /* No suffixes */
     str = "0x18M";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
     g_assert_true(endptr == str);

+    str = "0x1p1";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+    g_assert(endptr == str);
+
     /* No negative values */
-    str = "-0";
+    str = " -0";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
@@ -2771,10 +2874,28 @@  static void test_qemu_strtosz_invalid(void)

     str = "-1";
     endptr = NULL;
+    res = 0xbaadf00d;
     err = qemu_strtosz(str, &endptr, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
     g_assert_true(endptr == str);
+
+    str = "-.0k";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+    g_assert_true(endptr == str);
+
+    /* Too many decimals */
+    str = "1.1.k";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+    g_assert(endptr == str);
 }

 static void test_qemu_strtosz_trailing(void)
@@ -2784,6 +2905,21 @@  static void test_qemu_strtosz_trailing(void)
     int err;
     uint64_t res;

+    /* Trailing whitespace */
+    str = "1k ";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpuint(res, ==, 1024);
+    g_assert(endptr == str + 2);
+
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+
+    /* Unknown suffix */
     str = "123xxx";
     endptr = NULL;
     res = 0xbaadf00d;
@@ -2797,6 +2933,7 @@  static void test_qemu_strtosz_trailing(void)
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);

+    /* Junk after one-byte suffix */
     str = "1kiB";
     endptr = NULL;
     res = 0xbaadf00d;
@@ -2810,6 +2947,7 @@  static void test_qemu_strtosz_trailing(void)
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);

+    /* Incomplete hex is an unknown suffix */
     str = "0x";
     endptr = NULL;
     res = 0xbaadf00d;
@@ -2823,6 +2961,7 @@  static void test_qemu_strtosz_trailing(void)
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);

+    /* Junk after decimal */
     str = "0.NaN";
     endptr = NULL;
     res = 0xbaadf00d;
@@ -2836,6 +2975,35 @@  static void test_qemu_strtosz_trailing(void)
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);

+    /* No support for binary literals; 'b' is valid suffix */
+    str = "0b1000";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpuint(res, ==, 0);
+    g_assert(endptr == str + 2);
+
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+
+    /* Hex literals use only one leading zero */
+    str = "00x1";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpuint(res, ==, 0);
+    g_assert(endptr == str + 2);
+
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+
+    /* Although negatives are invalid, '-' may be in trailing junk */
     str = "123-45";
     endptr = NULL;
     res = 0xbaadf00d;
@@ -2849,6 +3017,19 @@  static void test_qemu_strtosz_trailing(void)
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);

+    str = " 123 - 45";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpuint(res, ==, 123);
+    g_assert(endptr == str + 4);
+
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
+
     /* FIXME should stop parse after 'e'. No floating point exponents */
     str = "1.5e1k";
     endptr = NULL;
@@ -2861,7 +3042,7 @@  static void test_qemu_strtosz_trailing(void)
     res = 0xbaadf00d;
     err = qemu_strtosz(str, NULL, &res);
     g_assert_cmpint(err, ==, -EINVAL);
-    g_assert_cmpint(res, ==, 0xbaadf00d);
+    g_assert_cmphex(res, ==, 0xbaadf00d);

     str = "1.5E+0k";
     endptr = NULL;
@@ -2875,6 +3056,20 @@  static void test_qemu_strtosz_trailing(void)
     err = qemu_strtosz(str, NULL, &res);
     g_assert_cmpint(err, ==, -EINVAL);
     g_assert_cmphex(res, ==, 0xbaadf00d);
+
+    /* FIXME overflow in fraction is buggy */
+    str = "1.5E999";
+    endptr = NULL;
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpuint(res, ==, EiB /* FIXME EiB * 1.5 */);
+    g_assert(endptr == str + 9 /* FIXME + 4 */);
+
+    res = 0xbaadf00d;
+    err = qemu_strtosz(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmphex(res, ==, 0xbaadf00d);
 }

 static void test_qemu_strtosz_erange(void)
@@ -2921,6 +3116,15 @@  static void test_qemu_strtosz_metric(void)
     g_assert_cmpint(err, ==, 0);
     g_assert_cmpuint(res, ==, 12345000);
     g_assert_true(endptr == str + 7);
+
+    /* Fraction is affected by floating-point rounding */
+    str = "18.446744073709550591E"; /* resembles 0xfffffffffffffbff */
+    endptr = str;
+    res = 0xbaadf00d;
+    err = qemu_strtosz_metric(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpuint(res, ==, 0xfffffffffffffc0c);
+    g_assert_true(endptr == str + 22);
 }

 static void test_freq_to_str(void)