Skip to content

Commit 302de7f

Browse files
committed
[Proposal] Warn about the loss of precision in binary literals
Emit an E_COMPILE_WARNING if these are seen in hexadecimal, octal, or binary literals. - E_COMPILE_WARNING is also emitted for "Octal escape sequence overflow" but it's been long enough to consider changing that. See GH-4758. - Making this proposal suddenly a ParseError in php 8.1 seems too soon. I expect this to behave the same on 32-bit and 64-bit builds (floats are 64 bits on both) For example, `0xffff_ffff_ffff_f400` overflows and becomes the **float** `0xffff_ffff_ffff_f000`. This PR will warn about that. Instead of using `0xffff_ffff_ffff_f400` with binary bitwise operands, most code should use the signed 64-bit int `~0xbff`. - E.g. PHP code ported from cryptography algorithms or other C code doing bitwise operations.
1 parent bdacd2a commit 302de7f

File tree

7 files changed

+230
-6
lines changed

7 files changed

+230
-6
lines changed

Zend/tests/binary.phpt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,16 @@ var_dump(-0b1111111111111111111111111111111111111111111111111111111111111111);
7979
var_dump(-0b111111111111111111111111111111111111111111111111111111111111111);
8080
var_dump(-0b11111111111111111111111111111111111111111111111111111111111111);
8181
var_dump(-0b1);
82-
--EXPECT--
82+
--EXPECTF--
83+
Warning: Saw imprecise float binary literal - the last 11 non-zero bits were truncated in %sbinary.php on line 66
84+
85+
Warning: Saw imprecise float binary literal - the last 11 non-zero bits were truncated in %sbinary.php on line 67
86+
87+
Warning: Saw imprecise float binary literal - the last 12 non-zero bits were truncated in %sbinary.php on line 68
88+
89+
Warning: Saw imprecise float binary literal - the last 12 non-zero bits were truncated in %sbinary.php on line 69
90+
91+
Warning: Saw imprecise float binary literal - the last 11 non-zero bits were truncated in %sbinary.php on line 71
8392
int(1)
8493
int(3)
8594
int(7)
@@ -151,4 +160,4 @@ float(3.68934881474191E+19)
151160
float(-1.844674407370955E+19)
152161
int(-9223372036854775807)
153162
int(-4611686018427387903)
154-
int(-1)
163+
int(-1)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Octal overflow in numeric literal warning
3+
--FILE--
4+
<?php
5+
// rounding down
6+
var_dump(eval('return 0b1011111111111111111111111111111111111111111111111111100000000000;'));
7+
var_dump(eval('return 0b1011111111111111111111111111111111111111111111111111100000000001;'));
8+
// rounding up
9+
var_dump(eval('return 0b1011111111111111111111111111111111111111111111111111111111111111;'));
10+
var_dump(eval('return 0b1011111111111111111111111111111111111111111111111111110000000000;'));
11+
var_dump(eval('return 0b1011111111111111111111111111111111111111111111111111111000000000;'));
12+
// don't count _ or leading 0s
13+
var_dump(eval('return 0b111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_000_000_000_0;'));
14+
var_dump(eval('return 0b000_111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_111_000_000_000_0;'));
15+
var_dump(eval('return 0b1111111111111111111111111111111111111111111111111111111111111111;'));
16+
var_dump(eval('return 0b1000000000000000000000000000000000000000000000000000010000000000;'));
17+
--EXPECTF--
18+
float(1.383505805528216E+19)
19+
20+
Warning: Saw imprecise float binary literal - the last 11 non-zero bits were truncated in %sbinary_overflow_number.php(4) : eval()'d code on line 1
21+
float(1.383505805528216E+19)
22+
23+
Warning: Saw imprecise float binary literal - the last 11 non-zero bits were truncated in %sbinary_overflow_number.php(6) : eval()'d code on line 1
24+
float(1.3835058055282164E+19)
25+
26+
Warning: Saw imprecise float binary literal - the last 1 non-zero bits were truncated in %sbinary_overflow_number.php(7) : eval()'d code on line 1
27+
float(1.3835058055282164E+19)
28+
29+
Warning: Saw imprecise float binary literal - the last 2 non-zero bits were truncated in %sbinary_overflow_number.php(8) : eval()'d code on line 1
30+
float(1.3835058055282164E+19)
31+
32+
Warning: Saw imprecise float binary literal - the last 1 non-zero bits were truncated in %sbinary_overflow_number.php(10) : eval()'d code on line 1
33+
float(1.844674407370955E+19)
34+
35+
Warning: Saw imprecise float binary literal - the last 1 non-zero bits were truncated in %sbinary_overflow_number.php(11) : eval()'d code on line 1
36+
float(1.844674407370955E+19)
37+
38+
Warning: Saw imprecise float binary literal - the last 11 non-zero bits were truncated in %sbinary_overflow_number.php(12) : eval()'d code on line 1
39+
float(1.844674407370955E+19)
40+
41+
Warning: Saw imprecise float binary literal - the last 1 non-zero bits were truncated in %sbinary_overflow_number.php(13) : eval()'d code on line 1
42+
float(9.223372036854775E+18)

Zend/tests/hex_overflow_number.phpt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Hex overflow in numeric literal warning
3+
--FILE--
4+
<?php
5+
var_dump(eval('return 0xffff_ffff_ffff_f800;'));
6+
var_dump(eval('return 0xffff_ffff_ffff_fa00;'));
7+
var_dump(eval('return 0xffff_ffff_ffff_fb00;'));
8+
var_dump(eval('return 0xffff_ffff_ffff_ffff;'));
9+
var_dump(eval('return 0x1_ffff_ffff_ffff_ffff;'));
10+
var_dump(eval('return 0x3_ffff_ffff_ffff_ffff;'));
11+
var_dump(eval('return 0x5_ffff_ffff_ffff_ffff;'));
12+
var_dump(eval('return 0x8_ffff_ffff_ffff_ffff;'));
13+
var_dump(eval('return 0x0008_ffff_ffff_ffff_ffff;'));
14+
--EXPECTF--
15+
float(1.844674407370955E+19)
16+
17+
Warning: Saw imprecise float hex literal - the last 2 non-zero bits were truncated in %shex_overflow_number.php(3) : eval()'d code on line 1
18+
float(1.844674407370955E+19)
19+
20+
Warning: Saw imprecise float hex literal - the last 3 non-zero bits were truncated in %shex_overflow_number.php(4) : eval()'d code on line 1
21+
float(1.844674407370955E+19)
22+
23+
Warning: Saw imprecise float hex literal - the last 11 non-zero bits were truncated in %shex_overflow_number.php(5) : eval()'d code on line 1
24+
float(1.8446744073709552E+19)
25+
26+
Warning: Saw imprecise float hex literal - the last 12 non-zero bits were truncated in %shex_overflow_number.php(6) : eval()'d code on line 1
27+
float(3.6893488147419103E+19)
28+
29+
Warning: Saw imprecise float hex literal - the last 13 non-zero bits were truncated in %shex_overflow_number.php(7) : eval()'d code on line 1
30+
float(7.378697629483821E+19)
31+
32+
Warning: Saw imprecise float hex literal - the last 14 non-zero bits were truncated in %shex_overflow_number.php(8) : eval()'d code on line 1
33+
float(1.1068046444225731E+20)
34+
35+
Warning: Saw imprecise float hex literal - the last 15 non-zero bits were truncated in %shex_overflow_number.php(9) : eval()'d code on line 1
36+
float(1.6602069666338596E+20)
37+
38+
Warning: Saw imprecise float hex literal - the last 15 non-zero bits were truncated in %shex_overflow_number.php(10) : eval()'d code on line 1
39+
float(1.6602069666338596E+20)

Zend/tests/octal_overflow_number.phpt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
Octal overflow in numeric literal warning
3+
--FILE--
4+
<?php
5+
var_dump(eval('return 01777777777777777770000;'));
6+
var_dump(eval('return 0_00_1777777777777777770000;'));
7+
var_dump(eval('return 01777777777777777772000;'));
8+
var_dump(eval('return 01777777777777777774000;'));
9+
var_dump(eval('return 02777777777777777774000;'));
10+
var_dump(eval('return 07777777777777777774000;'));
11+
var_dump(eval('return 04_777_777_7777777777774000;'));
12+
--EXPECTF--
13+
float(1.8446744073709548E+19)
14+
float(1.8446744073709548E+19)
15+
16+
Warning: Saw imprecise float octal literal - the last 1 non-zero bits were truncated in %soctal_overflow_number.php(4) : eval()'d code on line 1
17+
float(1.8446744073709548E+19)
18+
float(1.8446744073709552E+19)
19+
20+
Warning: Saw imprecise float octal literal - the last 1 non-zero bits were truncated in %soctal_overflow_number.php(6) : eval()'d code on line 1
21+
float(2.7670116110564327E+19)
22+
23+
Warning: Saw imprecise float octal literal - the last 2 non-zero bits were truncated in %soctal_overflow_number.php(7) : eval()'d code on line 1
24+
float(7.378697629483821E+19)
25+
26+
Warning: Saw imprecise float octal literal - the last 5 non-zero bits were truncated in %soctal_overflow_number.php(8) : eval()'d code on line 1
27+
float(3.68934881474191E+20)

Zend/zend_language_scanner.l

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,77 @@ static void strip_underscores(char *str, size_t *len)
135135
*dest = '\0';
136136
}
137137

138+
/* Get the number of bits in the representation of a hex literal. Precondition: *str represents a non-zero number that overflowed an int. */
139+
static int bits_in_hex_representation(const char *str, size_t len)
140+
{
141+
size_t bits = len * 4;
142+
const char *end = str + len - 1;
143+
int last_digit;
144+
while (*end == '0') {
145+
bits -= 4;
146+
end--;
147+
ZEND_ASSERT(end >= str);
148+
}
149+
if ('0' <= *end && *end <= '9') {
150+
last_digit = *end - '0';
151+
} else if ('a' <= *end && *end <= 'f') {
152+
last_digit = *end - 'a' + 10;
153+
} else {
154+
ZEND_ASSERT('A' <= *end && *end <= 'F');
155+
last_digit = *end - 'A' + 10;
156+
}
157+
if ((last_digit & 1) == 0) {
158+
bits--;
159+
if ((last_digit & 2) == 0) {
160+
bits--;
161+
if ((last_digit & 4) == 0) {
162+
bits--;
163+
}
164+
}
165+
}
166+
/* Check how many bits the first character started with */
167+
if (*str < '2') {
168+
bits -= 3;
169+
} else if (*str < '4') {
170+
bits -= 2;
171+
} else if (*str < '8') {
172+
bits -= 1;
173+
}
174+
return bits;
175+
}
176+
177+
/* Get the number of bits in the representation of an octal literal. Precondition: *str represents a non-zero number that overflowed an int. */
178+
static size_t bits_in_octal_representation(const char *str, size_t len)
179+
{
180+
size_t bits = len * 3;
181+
const char *end = str + len - 1;
182+
int last_digit;
183+
while (*str == '0') {
184+
bits -= 3;
185+
str++;
186+
ZEND_ASSERT(end >= str);
187+
}
188+
while (*end == '0') {
189+
bits -= 3;
190+
end--;
191+
ZEND_ASSERT(end >= str);
192+
}
193+
last_digit = *end - '0';
194+
if ((last_digit & 1) == 0) {
195+
bits--;
196+
if ((last_digit & 2) == 0) {
197+
bits--;
198+
}
199+
}
200+
if (*str < '2') {
201+
bits -= 2;
202+
} else if (*str < '4') {
203+
bits -= 1;
204+
}
205+
return bits;
206+
}
207+
208+
138209
static size_t encoding_filter_script_to_internal(unsigned char **to, size_t *to_length, const unsigned char *from, size_t from_length)
139210
{
140211
const zend_encoding *internal_encoding = zend_multibyte_get_internal_encoding();
@@ -1961,9 +2032,17 @@ NEWLINE ("\r"|"\n"|"\r\n")
19612032
}
19622033
RETURN_TOKEN_WITH_VAL(T_LNUMBER);
19632034
} else {
2035+
const char* last_one_bit = bin + len - 1;
2036+
while (*last_one_bit == '0') {
2037+
last_one_bit--;
2038+
ZEND_ASSERT(last_one_bit > bin);
2039+
}
19642040
ZVAL_DOUBLE(zendlval, zend_bin_strtod(bin, (const char **)&end));
19652041
/* errno isn't checked since we allow HUGE_VAL/INF overflow */
19662042
ZEND_ASSERT(end == bin + len);
2043+
if (last_one_bit - bin + 1> 53) {
2044+
zend_error(E_COMPILE_WARNING, "Saw imprecise float binary literal - the last %zu non-zero bits were truncated", (size_t)(last_one_bit - bin + 1 - 53));
2045+
}
19672046
if (contains_underscores) {
19682047
efree(bin);
19692048
}
@@ -1975,6 +2054,7 @@ NEWLINE ("\r"|"\n"|"\r\n")
19752054
size_t len = yyleng;
19762055
char *end, *lnum = yytext;
19772056
zend_bool is_octal = lnum[0] == '0';
2057+
zend_bool is_truncated = 0;
19782058
zend_bool contains_underscores = (memchr(lnum, '_', len) != NULL);
19792059

19802060
if (contains_underscores) {
@@ -1998,6 +2078,7 @@ NEWLINE ("\r"|"\n"|"\r\n")
19982078

19992079
/* Continue in order to determine if this is T_LNUMBER or T_DNUMBER. */
20002080
len = i;
2081+
is_truncated = 1;
20012082
break;
20022083
}
20032084
}
@@ -2016,6 +2097,12 @@ NEWLINE ("\r"|"\n"|"\r\n")
20162097
errno = 0;
20172098
if (is_octal) { /* octal overflow */
20182099
ZVAL_DOUBLE(zendlval, zend_oct_strtod(lnum, (const char **)&end));
2100+
if (!is_truncated) {
2101+
size_t bits_in_representation = bits_in_octal_representation(lnum, len);
2102+
if (bits_in_representation > 53) {
2103+
zend_error(E_COMPILE_WARNING, "Saw imprecise float octal literal - the last %zu non-zero bits were truncated", bits_in_representation - 53);
2104+
}
2105+
}
20192106
} else {
20202107
ZVAL_DOUBLE(zendlval, zend_strtod(lnum, (const char **)&end));
20212108
}
@@ -2066,9 +2153,13 @@ NEWLINE ("\r"|"\n"|"\r\n")
20662153
}
20672154
RETURN_TOKEN_WITH_VAL(T_LNUMBER);
20682155
} else {
2156+
size_t bits_in_representation = bits_in_hex_representation(hex, len);
20692157
ZVAL_DOUBLE(zendlval, zend_hex_strtod(hex, (const char **)&end));
20702158
/* errno isn't checked since we allow HUGE_VAL/INF overflow */
20712159
ZEND_ASSERT(end == hex + len);
2160+
if (bits_in_representation > 53) {
2161+
zend_error(E_COMPILE_WARNING, "Saw imprecise float hex literal - the last %zu non-zero bits were truncated", bits_in_representation - 53);
2162+
}
20722163
if (contains_underscores) {
20732164
efree(hex);
20742165
}

ext/standard/tests/strings/pack64.phpt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ print_r(unpack("q", pack("q", 0x8000000000000002)));
3232
print_r(unpack("q", pack("q", -1)));
3333
print_r(unpack("q", pack("q", 0x8000000000000000)));
3434
?>
35-
--EXPECT--
35+
--EXPECTF--
36+
Warning: Saw imprecise float hex literal - the last 10 non-zero bits were truncated in %spack64.php on line 4
37+
38+
Warning: Saw imprecise float hex literal - the last 10 non-zero bits were truncated in %spack64.php on line 10
39+
40+
Warning: Saw imprecise float hex literal - the last 10 non-zero bits were truncated in %spack64.php on line 16
41+
42+
Warning: Saw imprecise float hex literal - the last 10 non-zero bits were truncated in %spack64.php on line 22
3643
Array
3744
(
3845
[1] => 281474976710654
@@ -112,4 +119,4 @@ Array
112119
Array
113120
(
114121
[1] => -9223372036854775808
115-
)
122+
)

ext/tokenizer/tests/invalid_octal_dnumber.phpt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ Invalid octal number that overflows to double
44
<?php if (!extension_loaded("tokenizer")) print "skip tokenizer extension not enabled"; ?>
55
--FILE--
66
<?php
7-
echo token_name(token_get_all('<?php 0177777777777777777777787')[1][0]), "\n";
7+
$token = token_get_all('<?php 0177777777777777777777787')[1];
8+
echo token_name($token[0]), "\n";
9+
echo $token[1], "\n";
10+
// The tokenizer should only warn about lost precision for octal literals when valid
11+
$token = token_get_all('<?php 0177777777777777777777777')[1];
12+
echo $token[1], "\n";
813
?>
9-
--EXPECT--
14+
--EXPECTF--
1015
T_DNUMBER
16+
0177777777777777777777787
17+
18+
Warning: Saw imprecise float octal literal - the last 17 non-zero bits were truncated in %sinvalid_octal_dnumber.php on line 6
19+
0177777777777777777777777

0 commit comments

Comments
 (0)