Use Most implementations of the %a or %A conversion specifiers has unspecified behavior when used on non-normalized floating-point numbers.
A double argument representing a floating-point number is converted in the
style ?0xh.hhhh p±d, where there is one hexadecimal digit (which is
nonzero if the argument is a normalized floating-point number and is
otherwise unspecified) before the decimal-point character (ISO/IEC 9899:TC3 §7.19.6.1)
C use the IEEE 754 standard for floating-point representation. In this representation, floats are encoded using 1 sign bit, 8 exponent bits, and 23 mantissa bits. Doubles are encoded and used exactly the same way, except they use 1 sign bit, 11 exponent bits, and 52 mantissa bits. These bits encode the values of s, the sign; M, the significand; and E, the exponent. Floating-point numbers are then calculated as (−1)s * M * 2 E.
Ordinarily, all of the mantissa bits are used to express significant figures, in addition to a leading 1, which is implied and therefore left out. Consequently, floats ordinarily have 24 significant bits of precision, and doubles ordinarily have 53 significant bits of precision. Such numbers are called normalized numbers. All floating-point numbers are limited in the sense that they have fixed precision. See FLP00-C. Understand the limitations of floating-point numbers.
Mantissa bits are used to express extremely small numbers that are too small to encode normally because of the lack of available exponent bits. Using mantissa bits extends the possible range of exponents. Because these bits no longer function as significant bits of precision, the total precision of extremely small numbers is less than usual. Such numbers are called denormalized, and they are more limited than normalized numbers. However, even using normalized numbers where precision is required can pose a risk. See FLP02-C. Avoid using floating-point numbers when precise computation is needed for more information.
Denormalized numbers can severely impair the precision of floating-point numbers and should not be usedRelying on the %a and %A specifiers to not produce values with a leading zero is error prone.
Noncompliant Code Example
This noncompliant code relies on the %a specifier to produce a result starting with 0x1. or -0x1.
This behavior is guaranteed only for normalized numbers, but may fail for non-normalized valuesattempts to reduce a floating-point number to a denormalized value and then restore the value. This operation is imprecise.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <stdio.h> #include <string.h> /* Return the index of the first non-zero hexadecimal double in the string line. Return NULL if there is no double is present in line. */ static char *findHexDouble(char *line) { char *index1 = strstr(line, "-0x1."); char *index2 = strstr(line, "0x1."); if (index1 == NULL && index2 == NULL) { return NULL; } else if (index1 == NULL) { return index2; } return (index1 < index2) ? index1 : index2; } static void printDouble(double val) { char buf[64]; char *convertedDouble; sprintf(buf, "%.8a", val); convertedDouble = findHexDouble(buf); if (convertedDouble != NULL) { printf("%e is a double\n", val); } else { printf("%e is not a double\n", val); } } int main() { double tiny = 0x1.0p-1020; int i; for (i = 0; i < 6; i++) { printDouble(tiny); tiny /= 2; } } |
On a 32-bit Linux machine using GCC 4.3.2 this code produces the following output
Code Block |
---|
8.900295e-308 is a double
4.450148e-308 is a double
2.225074e-308 is a double
1.112537e-308 is not a double
5.562685e-309 is not a double
2.781342e-309 is not a double
|
Compliant Solution
TODO
Code Block | ||
---|---|---|
| ||
TODO
|
Risk Assessment
float x = 1/3.0;
printf("Original : %e\n", x);
x = x * 7e-45;
printf("Denormalized: %e\n", x);
x = x / 7e-45;
printf("Restored : %e\n", x);
|
This code produces the following output on implementations that use IEEE 754 floats:
Code Block |
---|
Original : 3.333333e-01
Denormalized: 2.802597e-45
Restored : 4.003710e-01
|
Compliant Solution
Do not allow code to produce denormalized numbers. If floats are producing denormalized numbers, use doubles instead.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <stdio.h>
double x = 1/3.0;
printf("Original : %e\n", x);
x = x * 7e-45;
printf("Denormalized: %e\n", x);
x = x / 7e-45;
printf("Restored : %e\n", x);
|
Code Block |
---|
Original : 3.333333e-01
Denormalized: 2.333333e-45
Restored : 3.333333e-01
|
If using doubles also produces denormalized numbers, using long doubles may or may not help. (On some implementations, long double has the same exponent range as double.) If using long doubles produces denormalized numbers, some other solution must be found.
Printing Denormalized Numbers
Denormalized numbers can also be troublesome because some functions have implementation-defined behavior when used with denormalized values. For example, using the %a
or $%A
conversion specifier in a format string can produce implementation-defined results when applied to denormalized numbers.
Subclause 7.21.6.1, paragraph 8, of the C Standard [ISO/IEC 9899:2011], states:
A double argument representing a floating-point number is converted in the style
[-]0xh.hhhh p±d
, where there is one hexadecimal digit (which is nonzero if the argument is a normalized floating-point number and is otherwise unspecified) before the decimal-point character.
Relying on the %a
and %A
specifiers to produce values without a leading zero is error prone.
Code Block |
---|
#include<stdio.h>
float x = 0x1p-125;
double y = 0x1p-1020;
printf("normalized float with %%e : %e\n", x);
printf("normalized float with %%a : %a\n", x);
x = 0x1p-140;
printf("denormalized float with %%e : %e\n", x);
printf("denormalized float with %%a : %a\n", x);
printf("normalized double with %%e : %e\n", y);
printf("normalized double with %%a : %a\n", y);
y = 0x1p-1050;
printf("denormalized double with %%e : %e\n", y);
printf("denormalized double with %%a : %a\n", y);
|
Implementation Details
On a 32-bit Linux machine using GCC 4.3.2, this code produces the following output:
Code Block |
---|
normalized float with %e : 2.350989e-38
normalized float with %a : 0x1p-125
denormalized float with %e : 7.174648e-43
denormalized float with %a : 0x1p-140
normalized double with %e : 8.900295e-308
normalized double with %a : 0x1p-1020
denormalized double with %e : 8.289046e-317
denormalized double with %a : 0x0.0000001p-1022
|
Risk Assessment
Floating-point numbers are an approximation; using subnormal floating-point number are a worse approximation.TODO
Rule | Severity | Likelihood | Remediation Cost | Priority | Level | |
---|---|---|---|---|---|---|
FLP05-C | — Low — | Probable | — High | --- | P2 | L3 --- |
Automated Detection
TODO
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
Related Guidelines
Bibliography
[Bryant 2003] | Section 2. |
...
4, "Floating Point" | |
[IEEE 754] | |
[ISO/IEC 9899:2011] | Subclause 7.21.6.1, "The fprintf Function" |
...
References
TODO