Skip to content

Commit 8786283

Browse files
committed
Fix undefined behaviour in unpack()
atoi()'s return value is actually undefined when an underflow or overflow occurs. For example on 32-bit on my system the overflow test which inputs "h2147483648" results in repetitions==2147483647 and on 64-bit this gives repetitions==-2147483648. The reason the test works on 32-bit is because there's a second undefined behaviour problem: in case 'h' when repetitions==2147483647, we add 1 and divide by 2. This is signed-wrap undefined behaviour and accidentally triggers the overflow check like we wanted to. Avoid all this trouble and use strtol with explicit error checking. This also fixes a semantic bug where repetitions==INT_MAX would result in the overflow check to trigger, even though there is no overflow. Closes GH-10943.
1 parent f9cbeaa commit 8786283

File tree

3 files changed

+21
-8
lines changed

3 files changed

+21
-8
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ PHP NEWS
7575
(apache2)). (nielsdos)
7676
. Fixed oss-fuzz #57392 (Buffer-overflow in php_fgetcsv() with \0 delimiter
7777
and enclosure). (ilutov)
78+
. Fixed undefined behaviour in unpack(). (nielsdos)
7879

7980
16 Mar 2023, PHP 8.1.17
8081

ext/standard/pack.c

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,16 @@ PHP_FUNCTION(unpack)
750750
c = *format;
751751

752752
if (c >= '0' && c <= '9') {
753-
repetitions = atoi(format);
753+
errno = 0;
754+
long tmp = strtol(format, NULL, 10);
755+
/* There is not strtoi. We have to check the range ourselves.
756+
* With 32-bit long the INT_{MIN,MAX} are useless because long == int, but with 64-bit they do limit us to 32-bit. */
757+
if (errno || tmp < INT_MIN || tmp > INT_MAX) {
758+
php_error_docref(NULL, E_WARNING, "Type %c: integer overflow", type);
759+
zend_array_destroy(Z_ARR_P(return_value));
760+
RETURN_FALSE;
761+
}
762+
repetitions = tmp;
754763

755764
while (formatlen > 0 && *format >= '0' && *format <= '9') {
756765
format++;
@@ -800,7 +809,7 @@ PHP_FUNCTION(unpack)
800809

801810
case 'h':
802811
case 'H':
803-
size = (repetitions > 0) ? (repetitions + (repetitions % 2)) / 2 : repetitions;
812+
size = (repetitions > 0) ? ((unsigned int) repetitions + 1) / 2 : repetitions;
804813
repetitions = 1;
805814
break;
806815

@@ -865,12 +874,6 @@ PHP_FUNCTION(unpack)
865874
RETURN_THROWS();
866875
}
867876

868-
if (size != 0 && size != -1 && size < 0) {
869-
php_error_docref(NULL, E_WARNING, "Type %c: integer overflow", type);
870-
zend_array_destroy(Z_ARR_P(return_value));
871-
RETURN_FALSE;
872-
}
873-
874877

875878
/* Do actual unpacking */
876879
for (i = 0; i != repetitions; i++ ) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
--TEST--
2+
Test unpacking at the 32-bit integer limit
3+
--FILE--
4+
<?php
5+
$a = pack("AAAAAAAAAAAA", 1,2,3,4,5,6,7,8,9,10,11,12);
6+
unpack('h2147483647', $a);
7+
?>
8+
--EXPECTF--
9+
Warning: unpack(): Type h: not enough input, need 1073741824, have 12 in %s on line %d

0 commit comments

Comments
 (0)