Skip to content

Commit 77566ed

Browse files
committed
Fix zend_dval_to_lval outside 64bit integers range
PHP should preserve the least significant bits when casting from double to long. Zend.m4 contains this: AC_DEFINE([ZEND_DVAL_TO_LVAL_CAST_OK], 1, [Define if double cast to long preserves least significant bits]) If ZEND_DVAL_TO_LVAL_CAST_OK is not defined, zend_operators.h had an inline implementation of zend_dval_to_lval() that would do a cast to an int64_t (when sizeof(long) == 4), then a cast to unsigned long and finally the cast to long. While this works well for doubles inside the range of values of the type used in the first cast (int64_t in the 32-bit version and unsigned long in the 64-bit version), if outside the range, it is undefined behavior that WILL give varying and not particularly useful results. This commit uses fmod() to first put the double in a range that can safely be cast to unsigned long and then casts this unsigned long to long. This last cast is implementation defined, but it's very likely that this gives the expected result (i.e. the internal 2's complement representation is unchanged) on all platforms that PHP supports. In any case, the previous implementationa already had this assumption. This alternative code path is indeed significantly slower than simply casting the double (almost an order of magnitude), but that should not matter because casting doubles with a very high absolute value is a rare event.
1 parent 64a2a8a commit 77566ed

File tree

4 files changed

+80
-6
lines changed

4 files changed

+80
-6
lines changed

Zend/tests/bug39018.phpt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ print "\nDone\n";
6464
--EXPECTF--
6565
Notice: String offset cast occurred in %s on line %d
6666

67+
Notice: Uninitialized string offset: %s in %s on line 6
68+
6769
Notice: Uninitialized string offset: 0 in %s on line %d
6870

6971
Notice: Uninitialized string offset: 0 in %s on line %d

Zend/tests/dval_to_lval_32.phpt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
zend_dval_to_lval preserves low bits (32 bit long)
3+
--SKIPIF--
4+
<?php
5+
if (PHP_INT_SIZE != 4)
6+
die("skip for machines with 32-bit longs");
7+
?>
8+
--FILE--
9+
<?php
10+
/* test doubles around -4e21 */
11+
$values = [
12+
-4000000000000001048576.,
13+
-4000000000000000524288.,
14+
-4000000000000000000000.,
15+
-3999999999999999475712.,
16+
-3999999999999998951424.,
17+
];
18+
19+
foreach ($values as $v) {
20+
var_dump((int)$v);
21+
}
22+
23+
?>
24+
--EXPECT--
25+
int(-2056257536)
26+
int(-2055733248)
27+
int(-2055208960)
28+
int(-2054684672)
29+
int(-2054160384)

Zend/tests/dval_to_lval_64.phpt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
zend_dval_to_lval preserves low bits (64 bit long)
3+
--SKIPIF--
4+
<?php
5+
if (PHP_INT_SIZE != 8)
6+
die("skip for machines with 64-bit longs");
7+
?>
8+
--FILE--
9+
<?php
10+
/* test doubles around -4e21 */
11+
$values = [
12+
-4000000000000001048576.,
13+
-4000000000000000524288.,
14+
-4000000000000000000000.,
15+
-3999999999999999475712.,
16+
-3999999999999998951424.,
17+
];
18+
19+
foreach ($values as $v) {
20+
var_dump((int)$v);
21+
}
22+
23+
?>
24+
--EXPECT--
25+
int(2943463994971652096)
26+
int(2943463994972176384)
27+
int(2943463994972700672)
28+
int(2943463994973224960)
29+
int(2943463994973749248)

Zend/zend_operators.h

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,36 @@ END_EXTERN_C()
6868

6969
#if ZEND_DVAL_TO_LVAL_CAST_OK
7070
# define zend_dval_to_lval(d) ((long) (d))
71-
#elif SIZEOF_LONG == 4 && defined(HAVE_ZEND_LONG64)
71+
#elif SIZEOF_LONG == 4
7272
static zend_always_inline long zend_dval_to_lval(double d)
7373
{
7474
if (d > LONG_MAX || d < LONG_MIN) {
75-
return (long)(unsigned long)(zend_long64) d;
75+
double two_pow_32 = pow(2., 32.),
76+
dmod;
77+
78+
dmod = fmod(d, two_pow_32);
79+
if (dmod < 0) {
80+
dmod += two_pow_32;
81+
}
82+
return (long)(unsigned long)dmod;
7683
}
77-
return (long) d;
84+
return (long)d;
7885
}
7986
#else
8087
static zend_always_inline long zend_dval_to_lval(double d)
8188
{
8289
/* >= as (double)LONG_MAX is outside signed range */
83-
if (d >= LONG_MAX) {
84-
return (long)(unsigned long) d;
90+
if (d >= LONG_MAX || d < LONG_MIN) {
91+
double two_pow_64 = pow(2., 64.),
92+
dmod;
93+
94+
dmod = fmod(d, two_pow_64);
95+
if (dmod < 0) {
96+
dmod += two_pow_64;
97+
}
98+
return (long)(unsigned long)dmod;
8599
}
86-
return (long) d;
100+
return (long)d;
87101
}
88102
#endif
89103
/* }}} */

0 commit comments

Comments
 (0)