Skip to content

Commit c2ac548

Browse files
committed
Zend: Add zval_try_get_double()
Add a failure-reporting double conversion API analogous to zval_try_get_long(), and use it to validate floating-point values passed to pack().
1 parent 1053403 commit c2ac548

8 files changed

Lines changed: 223 additions & 26 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ PHP NEWS
7070
- Standard:
7171
. Fixed a segfault when a stream filter callback unsets StreamBucket::$data
7272
before re-attaching the bucket. (iliaal)
73+
. Fixed pack() accepting values that cannot be converted to float for the f,
74+
g, G, d, e, and E format codes. (Weilin Du)
7375
. Fixed an out-of-bounds read when following a redirect response with an
7476
empty Location header. (iliaal)
7577
. Fixed read buffer compaction in php_stream_filter_flush(). (crystarm)

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ PHP 8.6 UPGRADE NOTES
234234
and DirectoryIterator::current() returns string|SplFileInfo|static.
235235

236236
- Standard:
237+
. pack() now throws a TypeError when a value for the f, g, G, d, e, or E
238+
format code cannot be converted to float, instead of silently coercing it.
237239
. array_intersect() with at least two arrays now converts values to strings
238240
while scanning its inputs instead of during sort comparisons. This can
239241
change the number and order of conversion warnings and __toString() calls,

UPGRADING.INTERNALS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ PHP 8.6 INTERNALS UPGRADE NOTES
227227
. Added zend_string_ends_with() and related variants.
228228
. Added trait support for internal classes.
229229
. Added do_php_cli().
230+
. Added zval_try_get_double(), which converts a zval to a double and reports
231+
conversion failures through a bool pointer, analogous to
232+
zval_try_get_long().
230233

231234
========================
232235
2. Build system changes

Zend/zend_operators.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,66 @@ ZEND_API double ZEND_FASTCALL zval_get_double_func(const zval *op) /* {{{ */
10581058
}
10591059
/* }}} */
10601060

1061+
ZEND_API zend_never_inline double ZEND_FASTCALL zval_try_get_double_func(const zval *op, bool *failed) /* {{{ */
1062+
{
1063+
*failed = false;
1064+
try_again:
1065+
switch (Z_TYPE_P(op)) {
1066+
case IS_NULL:
1067+
case IS_FALSE:
1068+
return 0.0;
1069+
case IS_TRUE:
1070+
return 1.0;
1071+
case IS_LONG:
1072+
return (double) Z_LVAL_P(op);
1073+
case IS_DOUBLE:
1074+
return Z_DVAL_P(op);
1075+
case IS_STRING:
1076+
{
1077+
uint8_t type;
1078+
zend_long lval;
1079+
double dval;
1080+
bool trailing_data = false;
1081+
1082+
/* For BC reasons we allow errors so that we can warn on leading numeric string */
1083+
type = is_numeric_string_ex(Z_STRVAL_P(op), Z_STRLEN_P(op), &lval, &dval,
1084+
/* allow errors */ true, NULL, &trailing_data);
1085+
if (type == 0) {
1086+
*failed = true;
1087+
return 0.0;
1088+
}
1089+
if (UNEXPECTED(trailing_data)) {
1090+
zend_error(E_WARNING, "A non-numeric value encountered");
1091+
if (UNEXPECTED(EG(exception))) {
1092+
*failed = true;
1093+
return 0.0;
1094+
}
1095+
}
1096+
return type == IS_LONG ? (double) lval : dval;
1097+
}
1098+
case IS_OBJECT:
1099+
{
1100+
zval dst;
1101+
if (Z_OBJ_HT_P(op)->cast_object(Z_OBJ_P(op), &dst, IS_DOUBLE) == FAILURE
1102+
|| EG(exception)) {
1103+
*failed = true;
1104+
return 0.0;
1105+
}
1106+
ZEND_ASSERT(Z_TYPE(dst) == IS_DOUBLE);
1107+
return Z_DVAL(dst);
1108+
}
1109+
case IS_RESOURCE:
1110+
case IS_ARRAY:
1111+
*failed = true;
1112+
return 0.0;
1113+
case IS_REFERENCE:
1114+
op = Z_REFVAL_P(op);
1115+
goto try_again;
1116+
default: ZEND_UNREACHABLE();
1117+
}
1118+
}
1119+
/* }}} */
1120+
10611121
static zend_always_inline zend_string* __zval_get_string_func(const zval *op, bool try) /* {{{ */
10621122
{
10631123
try_again:

Zend/zend_operators.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ ZEND_API void ZEND_FASTCALL convert_to_object(zval *op);
323323
ZEND_API zend_long ZEND_FASTCALL zval_get_long_func(const zval *op, bool is_strict);
324324
ZEND_API zend_long ZEND_FASTCALL zval_try_get_long(const zval *op, bool *failed);
325325
ZEND_API double ZEND_FASTCALL zval_get_double_func(const zval *op);
326+
ZEND_API zend_never_inline double ZEND_FASTCALL zval_try_get_double_func(const zval *op, bool *failed);
326327
ZEND_API zend_string* ZEND_FASTCALL zval_get_string_func(const zval *op);
327328
ZEND_API zend_string* ZEND_FASTCALL zval_try_get_string_func(const zval *op);
328329

@@ -335,6 +336,13 @@ static zend_always_inline zend_long zval_get_long_ex(const zval *op, bool is_str
335336
static zend_always_inline double zval_get_double(const zval *op) {
336337
return EXPECTED(Z_TYPE_P(op) == IS_DOUBLE) ? Z_DVAL_P(op) : zval_get_double_func(op);
337338
}
339+
static zend_always_inline double zval_try_get_double(const zval *op, bool *failed) {
340+
if (EXPECTED(Z_TYPE_P(op) == IS_DOUBLE)) {
341+
*failed = false;
342+
return Z_DVAL_P(op);
343+
}
344+
return zval_try_get_double_func(op, failed);
345+
}
338346
static zend_always_inline zend_string *zval_get_string(const zval *op) {
339347
return EXPECTED(Z_TYPE_P(op) == IS_STRING) ? zend_string_copy(Z_STR_P(op)) : zval_get_string_func(op);
340348
}

ext/standard/pack.c

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ static void php_pack(const zval *val, size_t size, php_pack_endianness endiannes
7474
}
7575
/* }}} */
7676

77+
static bool php_pack_try_get_double(const zval *value, uint32_t arg_num, double *result)
78+
{
79+
bool failed;
80+
81+
*result = zval_try_get_double(value, &failed);
82+
if (UNEXPECTED(failed)) {
83+
if (!EG(exception)) {
84+
zend_argument_type_error(arg_num, "must be of type float, %s given", zend_zval_value_name(value));
85+
}
86+
return false;
87+
}
88+
89+
return true;
90+
}
91+
7792
ZEND_ATTRIBUTE_CONST static inline uint16_t php_pack_reverse_int16(uint16_t arg)
7893
{
7994
return ((arg & 0xFF) << 8) | ((arg >> 8) & 0xFF);
@@ -702,7 +717,17 @@ PHP_FUNCTION(pack)
702717
case 'g':
703718
case 'G': {
704719
while (arg-- > 0) {
705-
float v = (float) zval_get_double(&argv[currentarg++]);
720+
double d;
721+
float v;
722+
if (!php_pack_try_get_double(&argv[currentarg], currentarg + 2, &d)) {
723+
zend_string_release(output);
724+
efree(formatcodes);
725+
efree(formatargs);
726+
efree(formatendian);
727+
RETURN_THROWS();
728+
}
729+
currentarg++;
730+
v = (float) d;
706731
if (code == 'g' || formatendian[i] == PHP_LITTLE_ENDIAN) {
707732
php_pack_copy_float(1, &ZSTR_VAL(output)[outputpos], v);
708733
} else if (code == 'G' || formatendian[i] == PHP_BIG_ENDIAN) {
@@ -719,7 +744,15 @@ PHP_FUNCTION(pack)
719744
case 'e':
720745
case 'E': {
721746
while (arg-- > 0) {
722-
double v = zval_get_double(&argv[currentarg++]);
747+
double v;
748+
if (!php_pack_try_get_double(&argv[currentarg], currentarg + 2, &v)) {
749+
zend_string_release(output);
750+
efree(formatcodes);
751+
efree(formatargs);
752+
efree(formatendian);
753+
RETURN_THROWS();
754+
}
755+
currentarg++;
723756
if (code == 'e' || formatendian[i] == PHP_LITTLE_ENDIAN) {
724757
php_pack_copy_double(1, &ZSTR_VAL(output)[outputpos], v);
725758
} else if (code == 'E' || formatendian[i] == PHP_BIG_ENDIAN) {

ext/standard/tests/strings/pack_float.phpt

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ pack()/unpack(): float/double tests
44
<?php
55
var_dump(
66
'pack e',
7-
bin2hex(pack("e", "")),
8-
bin2hex(pack("e", "a")),
9-
bin2hex(pack("e", " ")),
107
bin2hex(pack("e", NULL)),
118
bin2hex(pack("e", 0)),
129
bin2hex(pack("e", 1)),
@@ -21,9 +18,6 @@ var_dump(
2118
bin2hex(pack("e", -12345678901234567890.1234567898765432123456789)),
2219

2320
'pack E',
24-
bin2hex(pack("E", "")),
25-
bin2hex(pack("E", "a")),
26-
bin2hex(pack("E", " ")),
2721
bin2hex(pack("E", NULL)),
2822
bin2hex(pack("E", 0)),
2923
bin2hex(pack("E", 1)),
@@ -38,9 +32,6 @@ var_dump(
3832
bin2hex(pack("E", -12345678901234567890.1234567898765432123456789)),
3933

4034
'pack g',
41-
bin2hex(pack("g", "")),
42-
bin2hex(pack("g", "a")),
43-
bin2hex(pack("g", " ")),
4435
bin2hex(pack("g", NULL)),
4536
bin2hex(pack("g", 0)),
4637
bin2hex(pack("g", 1)),
@@ -55,9 +46,6 @@ var_dump(
5546
bin2hex(pack("g", -12345678901234567890.1234567898765432123456789)),
5647

5748
'pack G',
58-
bin2hex(pack("G", "")),
59-
bin2hex(pack("G", "a")),
60-
bin2hex(pack("G", " ")),
6149
bin2hex(pack("G", NULL)),
6250
bin2hex(pack("G", 0)),
6351
bin2hex(pack("G", 1)),
@@ -117,9 +105,6 @@ var_dump(
117105
string(6) "pack e"
118106
string(16) "0000000000000000"
119107
string(16) "0000000000000000"
120-
string(16) "0000000000000000"
121-
string(16) "0000000000000000"
122-
string(16) "0000000000000000"
123108
string(16) "000000000000f03f"
124109
string(16) "000000000000f03f"
125110
string(16) "0080e03779c34143"
@@ -133,9 +118,6 @@ string(16) "e1639d31956ae5c3"
133118
string(6) "pack E"
134119
string(16) "0000000000000000"
135120
string(16) "0000000000000000"
136-
string(16) "0000000000000000"
137-
string(16) "0000000000000000"
138-
string(16) "0000000000000000"
139121
string(16) "3ff0000000000000"
140122
string(16) "3ff0000000000000"
141123
string(16) "4341c37937e08000"
@@ -149,9 +131,6 @@ string(16) "c3e56a95319d63e1"
149131
string(6) "pack g"
150132
string(8) "00000000"
151133
string(8) "00000000"
152-
string(8) "00000000"
153-
string(8) "00000000"
154-
string(8) "00000000"
155134
string(8) "0000803f"
156135
string(8) "0000803f"
157136
string(8) "ca1b0e5a"
@@ -165,9 +144,6 @@ string(8) "aa542bdf"
165144
string(6) "pack G"
166145
string(8) "00000000"
167146
string(8) "00000000"
168-
string(8) "00000000"
169-
string(8) "00000000"
170-
string(8) "00000000"
171147
string(8) "3f800000"
172148
string(8) "3f800000"
173149
string(8) "5a0e1bca"
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
--TEST--
2+
pack() float and double value conversions
3+
--FILE--
4+
<?php
5+
6+
$values = [
7+
'null' => null,
8+
'false' => false,
9+
'true' => true,
10+
'int' => 42,
11+
'float' => 42.5,
12+
'numeric integer string' => '42',
13+
'numeric float string' => '42.5',
14+
'numeric scientific string' => '1e3',
15+
];
16+
17+
foreach ($values as $name => $value) {
18+
echo "$name: ";
19+
var_dump(unpack('d', pack('d', $value))[1]);
20+
}
21+
22+
echo "reference: ";
23+
$value = 1.5;
24+
$reference =& $value;
25+
var_dump(unpack('d', pack('d', $reference))[1]);
26+
27+
echo "trailing data:\n";
28+
var_dump(unpack('d', pack('d', '42 with trailing data'))[1]);
29+
30+
$invalidValues = [
31+
'empty string' => '',
32+
'whitespace string' => ' ',
33+
'non-numeric string' => 'not numeric',
34+
'array' => [],
35+
'object' => new stdClass(),
36+
'resource' => fopen(__FILE__, 'r'),
37+
];
38+
39+
foreach ($invalidValues as $name => $value) {
40+
echo "$name:\n";
41+
try {
42+
pack('d', $value);
43+
} catch (Throwable $e) {
44+
echo $e::class, ': ', $e->getMessage(), "\n";
45+
}
46+
}
47+
48+
echo "all format codes:\n";
49+
foreach (['f', 'g', 'G', 'd', 'e', 'E'] as $format) {
50+
try {
51+
pack($format, []);
52+
} catch (Throwable $e) {
53+
echo "$format: ", $e->getMessage(), "\n";
54+
}
55+
}
56+
57+
echo "later argument:\n";
58+
try {
59+
pack('d2', 1.0, []);
60+
} catch (Throwable $e) {
61+
echo $e::class, ': ', $e->getMessage(), "\n";
62+
}
63+
64+
echo "warning converted to exception:\n";
65+
set_error_handler(static function (int $errno, string $errstr): never {
66+
throw new Exception($errstr);
67+
});
68+
try {
69+
pack('d', '42 with trailing data');
70+
} catch (Throwable $e) {
71+
echo $e::class, ': ', $e->getMessage(), "\n";
72+
}
73+
restore_error_handler();
74+
fclose($invalidValues['resource']);
75+
76+
?>
77+
--EXPECTF--
78+
null: float(0)
79+
false: float(0)
80+
true: float(1)
81+
int: float(42)
82+
float: float(42.5)
83+
numeric integer string: float(42)
84+
numeric float string: float(42.5)
85+
numeric scientific string: float(1000)
86+
reference: float(1.5)
87+
trailing data:
88+
89+
Warning: A non-numeric value encountered in %s on line %d
90+
float(42)
91+
empty string:
92+
TypeError: pack(): Argument #2 must be of type float, string given
93+
whitespace string:
94+
TypeError: pack(): Argument #2 must be of type float, string given
95+
non-numeric string:
96+
TypeError: pack(): Argument #2 must be of type float, string given
97+
array:
98+
TypeError: pack(): Argument #2 must be of type float, array given
99+
object:
100+
TypeError: pack(): Argument #2 must be of type float, stdClass given
101+
resource:
102+
TypeError: pack(): Argument #2 must be of type float, resource given
103+
all format codes:
104+
f: pack(): Argument #2 must be of type float, array given
105+
g: pack(): Argument #2 must be of type float, array given
106+
G: pack(): Argument #2 must be of type float, array given
107+
d: pack(): Argument #2 must be of type float, array given
108+
e: pack(): Argument #2 must be of type float, array given
109+
E: pack(): Argument #2 must be of type float, array given
110+
later argument:
111+
TypeError: pack(): Argument #3 must be of type float, array given
112+
warning converted to exception:
113+
Exception: A non-numeric value encountered

0 commit comments

Comments
 (0)