Skip to content

Signed-ness problems with NANOPRINTF_CONVERSION_BUFFER_SIZE #302

@stefano-zanotti-88

Description

@stefano-zanotti-88

This line:

if (spec->prec > (NANOPRINTF_CONVERSION_BUFFER_SIZE - 2)) { goto exit; }

misbehaves if NANOPRINTF_CONVERSION_BUFFER_SIZE is unsigned.
This may actually happen, as the symbol can be provided by the user. In this case, the failure would be very hard to spot and even to understand, for the user. Also, it's not uncommon for the user to use an unsigned literal, since this symbol determines a buffer size.
The problem arises because in case of a signed-unsigned operation, the signed one gets casted to unsigned (after possible promotions).

Similarly in these places:

#if (NANOPRINTF_CONVERSION_BUFFER_SIZE <= UINT_FAST8_MAX) && (UINT_FAST8_MAX <= INT_MAX)

if (dec >= NANOPRINTF_CONVERSION_BUFFER_SIZE) { goto exit; }

if (end >= NANOPRINTF_CONVERSION_BUFFER_SIZE) { goto exit; }

if (dec >= NANOPRINTF_CONVERSION_BUFFER_SIZE) { goto exit; }

Edit: the problem does not exist if dec and end are never negative, regardless of their being signed. The same for prec above; but, are we sure that it is never negative? Not even when it was indeed negative and it was ignored by setting prec_opt = NPF_FMT_SPEC_OPT_NONE but without setting prec = 0 ?

This is a tricky topic at least for me; maybe the problem is not real at all? However, I think it can arise in some cases and not in others -- ie it might depend on the relative bit-size of NANOPRINTF_CONVERSION_BUFFER_SIZE and the other variables involved in math operations with it.
For instance:
-1 > 23u - 2 is true [unexpected] (-1 > 21u) -> (0xFFFFFFFFu > 21u)
-1 > 23ull - 2 is true [unexpected] (-1 > 21ull) -> (0xFFFFFFFFFFFFFFFFu > 21ull)
-1 > (unsigned short)23 - 2 is false [expected] (-1 > (int)(unsigned short)23 - 2) -> (-1 > 23 - 2) -> (-1 > 21)

I think a cast-to-signed of NANOPRINTF_CONVERSION_BUFFER_SIZE is needed everywhere, possibly with a type tailored to every occurrence. Or maybe a cast to (int) is always acceptable, as long as we change the check here:

#if NANOPRINTF_CONVERSION_BUFFER_SIZE < 23

by adding

&& NANOPRINTF_CONVERSION_BUFFER_SIZE < UINT_MAX / 2

which ensures that it already fits in an (int), so the cast to (int) just removes the unsigned-ness.

Note that the check on line 510 is not enough to prevent the problem, as can be demonstrated by the 23ull example above.

Maybe the code can be kept the same, and we just need the following check. We already know from this:

#if NANOPRINTF_CONVERSION_BUFFER_SIZE < 23

that NANOPRINTF_CONVERSION_BUFFER_SIZE is positive.
So, this check should be enough:

#if (-1 < (NANOPRINTF_CONVERSION_BUFFER_SIZE) && -1l < (NANOPRINTF_CONVERSION_BUFFER_SIZE) && -1ll < (NANOPRINTF_CONVERSION_BUFFER_SIZE))
  // ok, it's a signed literal
#else
  #error NANOPRINTF_CONVERSION_BUFFER_SIZE can't be an unsigned literal
#endif

Finally: this symbol is provided by the user, so every usage of it should be parenthesized. Especially since the user might compute it from other values they have.
This is not the case for NANOPRINTF_CONVERSION_FLOAT_TYPE, because it is supposed to be a type, and parentheses would not be parsed; also, it is much less likely to be the result of an expression, than it is for a numeric option like the buffer size.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions