diff mbox series

[v2,09/19] test-cutils: Add coverage of qemu_strtod

Message ID 20230512021033.1378730-10-eblake@redhat.com (mailing list archive)
State New, archived
Headers show
Series Fix qemu_strtosz() read-out-of-bounds | expand

Commit Message

Eric Blake May 12, 2023, 2:10 a.m. UTC
It's hard to tweak code for consistency if I can't prove what will or
won't break from those tweaks.  Time to add unit tests for
qemu_strtod() and qemu_strtod_finite().

Among other things, I wrote a check whether we have C99 semantics for
strtod("0x1") (which MUST parse hex numbers) rather than C89 (which
must stop parsing at 'x').  These days, I suspect that is okay; but if
it fails CI checks, knowing the difference will help us decide what we
want to do about it.  Note that C2x, while not final at the time of
this patch, has been considering whether to make strtol("0b1") parse
as 1 with no slop instead of the C17 parse of 0 with slop "b1"; that
decision may also bleed over to strtod().  But for now, I didn't think
it worth adding unit tests on that front (to strtol or strtod) as
things may still change.

Likewise, there are plenty more corner cases of strtod proper that I
don't explicitly test here, but there are enough unit tests added here
that it covers all the branches reached in our wrappers.  In
particular, it demonstrates the difference on when *value is left
uninitialized, which an upcoming patch will normalize.

Signed-off-by: Eric Blake <eblake@redhat.com>

---

v2: Added g_assert_false(signbit(res)) anywhere I used
g_assert_cmpfloat(res,==,0.0); add a test for strtod() hex parsing and
handling of junk after ERANGE, which is major enough that I dropped
R-b
---
 tests/unit/test-cutils.c | 510 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 510 insertions(+)

Comments

Hanna Czenczek May 19, 2023, 3:05 p.m. UTC | #1
On 12.05.23 04:10, Eric Blake wrote:
> It's hard to tweak code for consistency if I can't prove what will or
> won't break from those tweaks.  Time to add unit tests for
> qemu_strtod() and qemu_strtod_finite().
>
> Among other things, I wrote a check whether we have C99 semantics for
> strtod("0x1") (which MUST parse hex numbers) rather than C89 (which
> must stop parsing at 'x').  These days, I suspect that is okay; but if
> it fails CI checks, knowing the difference will help us decide what we
> want to do about it.  Note that C2x, while not final at the time of
> this patch, has been considering whether to make strtol("0b1") parse
> as 1 with no slop instead of the C17 parse of 0 with slop "b1"; that
> decision may also bleed over to strtod().  But for now, I didn't think
> it worth adding unit tests on that front (to strtol or strtod) as
> things may still change.
>
> Likewise, there are plenty more corner cases of strtod proper that I
> don't explicitly test here, but there are enough unit tests added here
> that it covers all the branches reached in our wrappers.  In
> particular, it demonstrates the difference on when *value is left
> uninitialized, which an upcoming patch will normalize.
>
> Signed-off-by: Eric Blake <eblake@redhat.com>
>
> ---
>
> v2: Added g_assert_false(signbit(res)) anywhere I used
> g_assert_cmpfloat(res,==,0.0); add a test for strtod() hex parsing and
> handling of junk after ERANGE, which is major enough that I dropped
> R-b
> ---
>   tests/unit/test-cutils.c | 510 +++++++++++++++++++++++++++++++++++++++
>   1 file changed, 510 insertions(+)
>
> diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
> index d3076c3fec1..1763839a157 100644
> --- a/tests/unit/test-cutils.c
> +++ b/tests/unit/test-cutils.c

[...]

> +static void test_qemu_strtod_erange_junk(void)
> +{
> +    const char *str;
> +    const char *endptr;
> +    int err;
> +    double res;
> +
> +    /* EINVAL has priority over ERANGE */

By being placed here, this comment confused me a bit, because the first 
case does return ERANGE.  So I’d prefer it above the second case, where 
we actually expect EINVAL, but understand that’s a personal preference.  
(Same for the _finite_ variant)

Reviewed-by: Hanna Czenczek <hreitz@redhat.com>

> +    str = "1e-999junk";
> +    endptr = "somewhere";
> +    res = 999;
> +    err = qemu_strtod(str, &endptr, &res);
> +    g_assert_cmpint(err, ==, -ERANGE);
> +    g_assert_cmpfloat(res, <=, DBL_MIN);
> +    g_assert_cmpfloat(res, >=, 0.0);
> +    g_assert_false(signbit(res));
> +    g_assert_true(endptr == str + 6);
> +
> +    endptr = "somewhere";
> +    res = 999;
> +    err = qemu_strtod(str, NULL, &res);
> +    g_assert_cmpint(err, ==, -EINVAL);
> +    g_assert_cmpfloat(res, ==, 0.0);
> +    g_assert_false(signbit(res));
> +}
Eric Blake May 19, 2023, 5:52 p.m. UTC | #2
On Fri, May 19, 2023 at 05:05:20PM +0200, Hanna Czenczek wrote:
> On 12.05.23 04:10, Eric Blake wrote:
> > It's hard to tweak code for consistency if I can't prove what will or
> > won't break from those tweaks.  Time to add unit tests for
> > qemu_strtod() and qemu_strtod_finite().
> > 
> > Among other things, I wrote a check whether we have C99 semantics for
> > strtod("0x1") (which MUST parse hex numbers) rather than C89 (which
> > must stop parsing at 'x').  These days, I suspect that is okay; but if
> > it fails CI checks, knowing the difference will help us decide what we
> > want to do about it.  Note that C2x, while not final at the time of
> > this patch, has been considering whether to make strtol("0b1") parse
> > as 1 with no slop instead of the C17 parse of 0 with slop "b1"; that
> > decision may also bleed over to strtod().  But for now, I didn't think
> > it worth adding unit tests on that front (to strtol or strtod) as
> > things may still change.
> > 
> > Likewise, there are plenty more corner cases of strtod proper that I
> > don't explicitly test here, but there are enough unit tests added here
> > that it covers all the branches reached in our wrappers.  In
> > particular, it demonstrates the difference on when *value is left
> > uninitialized, which an upcoming patch will normalize.
> > 
> > Signed-off-by: Eric Blake <eblake@redhat.com>
> > 
> > ---
> > 
> > v2: Added g_assert_false(signbit(res)) anywhere I used
> > g_assert_cmpfloat(res,==,0.0); add a test for strtod() hex parsing and
> > handling of junk after ERANGE, which is major enough that I dropped
> > R-b
> > ---
> >   tests/unit/test-cutils.c | 510 +++++++++++++++++++++++++++++++++++++++
> >   1 file changed, 510 insertions(+)
> > 
> > diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
> > index d3076c3fec1..1763839a157 100644
> > --- a/tests/unit/test-cutils.c
> > +++ b/tests/unit/test-cutils.c
> 
> [...]
> 
> > +static void test_qemu_strtod_erange_junk(void)
> > +{
> > +    const char *str;
> > +    const char *endptr;
> > +    int err;
> > +    double res;
> > +
> > +    /* EINVAL has priority over ERANGE */
> 
> By being placed here, this comment confused me a bit, because the first case
> does return ERANGE.  So I’d prefer it above the second case, where we
> actually expect EINVAL, but understand that’s a personal preference.  (Same
> for the _finite_ variant)

The test is what happens when both conditions apply.  For
qemu_strtod("1e-999junk", &endptr), only ERANGE applies (because
"junk" is returned in endptr); it is not until
qemu_strtod("1e-999junk", NULL) where EINVAL is also possible
(trailing junk takes precedence over underflow).  For qemu_strtosz(),
I made it a bit more obvious by writing a helper function that shows
both errno values in a single line, rather than spreading out the
boilerplate over multiple lines.

Should I do a similar helper function for qemu_strtod[_finite] in v3?
Hanna Czenczek May 22, 2023, 10:56 a.m. UTC | #3
On 19.05.23 19:52, Eric Blake wrote:
> On Fri, May 19, 2023 at 05:05:20PM +0200, Hanna Czenczek wrote:
>> On 12.05.23 04:10, Eric Blake wrote:
>>> It's hard to tweak code for consistency if I can't prove what will or
>>> won't break from those tweaks.  Time to add unit tests for
>>> qemu_strtod() and qemu_strtod_finite().
>>>
>>> Among other things, I wrote a check whether we have C99 semantics for
>>> strtod("0x1") (which MUST parse hex numbers) rather than C89 (which
>>> must stop parsing at 'x').  These days, I suspect that is okay; but if
>>> it fails CI checks, knowing the difference will help us decide what we
>>> want to do about it.  Note that C2x, while not final at the time of
>>> this patch, has been considering whether to make strtol("0b1") parse
>>> as 1 with no slop instead of the C17 parse of 0 with slop "b1"; that
>>> decision may also bleed over to strtod().  But for now, I didn't think
>>> it worth adding unit tests on that front (to strtol or strtod) as
>>> things may still change.
>>>
>>> Likewise, there are plenty more corner cases of strtod proper that I
>>> don't explicitly test here, but there are enough unit tests added here
>>> that it covers all the branches reached in our wrappers.  In
>>> particular, it demonstrates the difference on when *value is left
>>> uninitialized, which an upcoming patch will normalize.
>>>
>>> Signed-off-by: Eric Blake <eblake@redhat.com>
>>>
>>> ---
>>>
>>> v2: Added g_assert_false(signbit(res)) anywhere I used
>>> g_assert_cmpfloat(res,==,0.0); add a test for strtod() hex parsing and
>>> handling of junk after ERANGE, which is major enough that I dropped
>>> R-b
>>> ---
>>>    tests/unit/test-cutils.c | 510 +++++++++++++++++++++++++++++++++++++++
>>>    1 file changed, 510 insertions(+)
>>>
>>> diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
>>> index d3076c3fec1..1763839a157 100644
>>> --- a/tests/unit/test-cutils.c
>>> +++ b/tests/unit/test-cutils.c
>> [...]
>>
>>> +static void test_qemu_strtod_erange_junk(void)
>>> +{
>>> +    const char *str;
>>> +    const char *endptr;
>>> +    int err;
>>> +    double res;
>>> +
>>> +    /* EINVAL has priority over ERANGE */
>> By being placed here, this comment confused me a bit, because the first case
>> does return ERANGE.  So I’d prefer it above the second case, where we
>> actually expect EINVAL, but understand that’s a personal preference.  (Same
>> for the _finite_ variant)
> The test is what happens when both conditions apply.  For
> qemu_strtod("1e-999junk", &endptr), only ERANGE applies (because
> "junk" is returned in endptr); it is not until
> qemu_strtod("1e-999junk", NULL) where EINVAL is also possible
> (trailing junk takes precedence over underflow).

Yep; it’s just that because the comment is directly above one test case, 
I assumed it applied to just that case, and was looking for the EINVAL 
there.  Only then I realized that EINVAL won’t occur there, and the 
comment instead points out the difference between the two cases there are.

> For qemu_strtosz(),
> I made it a bit more obvious by writing a helper function that shows
> both errno values in a single line, rather than spreading out the
> boilerplate over multiple lines.
>
> Should I do a similar helper function for qemu_strtod[_finite] in v3?

I mean, from my perspective, all I can see is that it would make 
reviewing v3 more tedious…
Eric Blake May 22, 2023, 12:59 p.m. UTC | #4
On Mon, May 22, 2023 at 12:56:31PM +0200, Hanna Czenczek wrote:
> > > > +static void test_qemu_strtod_erange_junk(void)
> > > > +{
> > > > +    const char *str;
> > > > +    const char *endptr;
> > > > +    int err;
> > > > +    double res;
> > > > +
> > > > +    /* EINVAL has priority over ERANGE */
> > > By being placed here, this comment confused me a bit, because the first case
> > > does return ERANGE.  So I’d prefer it above the second case, where we
> > > actually expect EINVAL, but understand that’s a personal preference.  (Same
> > > for the _finite_ variant)
> > The test is what happens when both conditions apply.  For
> > qemu_strtod("1e-999junk", &endptr), only ERANGE applies (because
> > "junk" is returned in endptr); it is not until
> > qemu_strtod("1e-999junk", NULL) where EINVAL is also possible
> > (trailing junk takes precedence over underflow).
> 
> Yep; it’s just that because the comment is directly above one test case, I
> assumed it applied to just that case, and was looking for the EINVAL there. 
> Only then I realized that EINVAL won’t occur there, and the comment instead
> points out the difference between the two cases there are.
> 
> > For qemu_strtosz(),
> > I made it a bit more obvious by writing a helper function that shows
> > both errno values in a single line, rather than spreading out the
> > boilerplate over multiple lines.
> > 
> > Should I do a similar helper function for qemu_strtod[_finite] in v3?
> 
> I mean, from my perspective, all I can see is that it would make reviewing
> v3 more tedious…

Okay, v3 will NOT include a helper function for strtoi or strtod (but
the helper already in place for strtosz remains).
diff mbox series

Patch

diff --git a/tests/unit/test-cutils.c b/tests/unit/test-cutils.c
index d3076c3fec1..1763839a157 100644
--- a/tests/unit/test-cutils.c
+++ b/tests/unit/test-cutils.c
@@ -25,6 +25,8 @@ 
  * THE SOFTWARE.
  */

+#include <math.h>
+
 #include "qemu/osdep.h"
 #include "qemu/cutils.h"
 #include "qemu/units.h"
@@ -2668,6 +2670,485 @@  static void test_qemu_strtou64_full_erange_junk(void)
     g_assert_cmpint(res, ==, UINT64_MAX);
 }

+static void test_qemu_strtod_simple(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* no radix or exponent */
+    str = "1";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 1.0);
+    g_assert_true(endptr == str + 1);
+
+    /* leading space and sign */
+    str = " -0.0";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, -0.0);
+    g_assert_true(signbit(res));
+    g_assert_true(endptr == str + 5);
+
+    /* fraction only */
+    str = "+.5";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 0.5);
+    g_assert_true(endptr == str + 3);
+
+    /* exponent */
+    str = "1.e+1";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 10.0);
+    g_assert_true(endptr == str + 5);
+
+    /* hex without radix */
+    str = "0x10";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 16.0);
+    g_assert_true(endptr == str + 4);
+}
+
+static void test_qemu_strtod_einval(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* empty */
+    str = "";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 0.0);
+    g_assert_false(signbit(res));
+    g_assert_true(endptr == str);
+
+    /* NULL */
+    str = NULL;
+    endptr = "random";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_null(endptr);
+
+    /* not recognizable */
+    str = " junk";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 0.0);
+    g_assert_false(signbit(res));
+    g_assert_true(endptr == str);
+}
+
+static void test_qemu_strtod_erange(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* overflow */
+    str = "9e999";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, ==, HUGE_VAL);
+    g_assert_true(endptr == str + 5);
+
+    str = "-9e+999";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, ==, -HUGE_VAL);
+    g_assert_true(endptr == str + 7);
+
+    /* underflow */
+    str = "-9e-999";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, >=, -DBL_MIN);
+    g_assert_cmpfloat(res, <=, -0.0);
+    g_assert_true(signbit(res));
+    g_assert_true(endptr == str + 7);
+}
+
+static void test_qemu_strtod_nonfinite(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* infinity */
+    str = "inf";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_true(isinf(res));
+    g_assert_false(signbit(res));
+    g_assert_true(endptr == str + 3);
+
+    str = "-infinity";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_true(isinf(res));
+    g_assert_true(signbit(res));
+    g_assert_true(endptr == str + 9);
+
+    /* not a number */
+    str = " NaN";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_true(isnan(res));
+    g_assert_true(endptr == str + 4);
+}
+
+static void test_qemu_strtod_trailing(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* trailing whitespace */
+    str = "1. ";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 1.0);
+    g_assert_true(endptr == str + 2);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 1.0);
+
+    /* trailing e is not an exponent */
+    str = ".5e";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 0.5);
+    g_assert_true(endptr == str + 2);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 0.5);
+
+    /* trailing ( not part of long NaN */
+    str = "nan(";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_true(isnan(res));
+    g_assert_true(endptr == str + 3);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_true(isnan(res));
+}
+
+static void test_qemu_strtod_erange_junk(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* EINVAL has priority over ERANGE */
+    str = "1e-999junk";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, <=, DBL_MIN);
+    g_assert_cmpfloat(res, >=, 0.0);
+    g_assert_false(signbit(res));
+    g_assert_true(endptr == str + 6);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 0.0);
+    g_assert_false(signbit(res));
+}
+
+static void test_qemu_strtod_finite_simple(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* no radix or exponent */
+    str = "1";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 1.0);
+    g_assert_true(endptr == str + 1);
+
+    /* leading space and sign */
+    str = " -0.0";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, -0.0);
+    g_assert_true(signbit(res));
+    g_assert_true(endptr == str + 5);
+
+    /* fraction only */
+    str = "+.5";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 0.5);
+    g_assert_true(endptr == str + 3);
+
+    /* exponent */
+    str = "1.e+1";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 10.0);
+    g_assert_true(endptr == str + 5);
+
+    /* hex without radix */
+    str = "0x10";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 16.0);
+    g_assert_true(endptr == str + 4);
+}
+
+static void test_qemu_strtod_finite_einval(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* empty */
+    str = "";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_true(endptr == str);
+
+    /* NULL */
+    str = NULL;
+    endptr = "random";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_null(endptr);
+
+    /* not recognizable */
+    str = " junk";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_true(endptr == str);
+}
+
+static void test_qemu_strtod_finite_erange(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* overflow */
+    str = "9e999";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, ==, HUGE_VAL);
+    g_assert_true(endptr == str + 5);
+
+    str = "-9e+999";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, ==, -HUGE_VAL);
+    g_assert_true(endptr == str + 7);
+
+    /* underflow */
+    str = "-9e-999";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, >=, -DBL_MIN);
+    g_assert_cmpfloat(res, <=, -0.0);
+    g_assert_true(signbit(res));
+    g_assert_true(endptr == str + 7);
+}
+
+static void test_qemu_strtod_finite_nonfinite(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* infinity */
+    str = "inf";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_true(endptr == str);
+
+    str = "-infinity";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_true(endptr == str);
+
+    /* not a number */
+    str = " NaN";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_true(endptr == str);
+}
+
+static void test_qemu_strtod_finite_trailing(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* trailing whitespace */
+    str = "1. ";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 1.0);
+    g_assert_true(endptr == str + 2);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+
+    /* trailing e is not an exponent */
+    str = ".5e";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, 0);
+    g_assert_cmpfloat(res, ==, 0.5);
+    g_assert_true(endptr == str + 2);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+
+    /* trailing ( not part of long NaN */
+    str = "nan(";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+    g_assert_true(endptr == str);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+}
+
+static void test_qemu_strtod_finite_erange_junk(void)
+{
+    const char *str;
+    const char *endptr;
+    int err;
+    double res;
+
+    /* EINVAL has priority over ERANGE */
+    str = "1e-999junk";
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, &endptr, &res);
+    g_assert_cmpint(err, ==, -ERANGE);
+    g_assert_cmpfloat(res, <=, DBL_MIN);
+    g_assert_cmpfloat(res, >=, 0.0);
+    g_assert_false(signbit(res));
+    g_assert_true(endptr == str + 6);
+
+    endptr = "somewhere";
+    res = 999;
+    err = qemu_strtod_finite(str, NULL, &res);
+    g_assert_cmpint(err, ==, -EINVAL);
+    g_assert_cmpfloat(res, ==, 999.0);
+}
+
 static void test_qemu_strtosz_simple(void)
 {
     const char *str;
@@ -3510,6 +3991,35 @@  int main(int argc, char **argv)
     g_test_add_func("/cutils/qemu_strtou64_full/erange_junk",
                     test_qemu_strtou64_full_erange_junk);

+    /* qemu_strtod() tests */
+    g_test_add_func("/cutils/qemu_strtod/simple",
+                    test_qemu_strtod_simple);
+    g_test_add_func("/cutils/qemu_strtod/einval",
+                    test_qemu_strtod_einval);
+    g_test_add_func("/cutils/qemu_strtod/erange",
+                    test_qemu_strtod_erange);
+    g_test_add_func("/cutils/qemu_strtod/nonfinite",
+                    test_qemu_strtod_nonfinite);
+    g_test_add_func("/cutils/qemu_strtod/trailing",
+                    test_qemu_strtod_trailing);
+    g_test_add_func("/cutils/qemu_strtod/erange_junk",
+                    test_qemu_strtod_erange_junk);
+
+    /* qemu_strtod_finite() tests */
+    g_test_add_func("/cutils/qemu_strtod_finite/simple",
+                    test_qemu_strtod_finite_simple);
+    g_test_add_func("/cutils/qemu_strtod_finite/einval",
+                    test_qemu_strtod_finite_einval);
+    g_test_add_func("/cutils/qemu_strtod_finite/erange",
+                    test_qemu_strtod_finite_erange);
+    g_test_add_func("/cutils/qemu_strtod_finite/nonfinite",
+                    test_qemu_strtod_finite_nonfinite);
+    g_test_add_func("/cutils/qemu_strtod_finite/trailing",
+                    test_qemu_strtod_finite_trailing);
+    g_test_add_func("/cutils/qemu_strtod_finite/erange_junk",
+                    test_qemu_strtod_finite_erange_junk);
+
+    /* qemu_strtosz() tests */
     g_test_add_func("/cutils/strtosz/simple",
                     test_qemu_strtosz_simple);
     g_test_add_func("/cutils/strtosz/hex",