php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #69295 segfault w/ memory corruption in _efree() on unserialize
Submitted: 2015-03-25 08:30 UTC Modified: 2017-01-01 13:48 UTC
Votes:1
Avg. Score:5.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:0 (0.0%)
Same OS:0 (0.0%)
From: brian dot carpenter at gmail dot com Assigned: nikic (profile)
Status: Closed Package: Reproducible crash
PHP Version: PHP-7 OS: Debian 7
Private report: No CVE-ID: None
 [2015-03-25 08:30 UTC] brian dot carpenter at gmail dot com
Description:
------------
valgrind -q ~/php-src/sapi/cli/php test.php
vex amd64->IR: unhandled instruction bytes: 0xF3 0x48 0xF 0xBC 0xDB 0x44 0x1 0xC3
==1842== valgrind: Unrecognised instruction at address 0x132ad42.
==1842==    at 0x132AD42: zend_mm_alloc_pages (zend_alloc.c:493)
==1842==    by 0x132C47C: zend_mm_alloc_small_slow (zend_alloc.c:1174)
==1842==    by 0x154C2F3: virtual_cwd_startup (zend_virtual_cwd.c:433)
==1842==    by 0x141840C: zend_startup (zend.c:609)
==1842==    by 0x11DA278: php_module_startup (main.c:2088)
==1842==    by 0x17D12B4: php_cli_startup (php_cli.c:421)
==1842==    by 0x43DECD: main (php_cli.c:1335)
==1842== Your program just tried to execute an instruction that Valgrind
==1842== did not recognise.  There are two possible reasons for this.
==1842== 1. Your program has a bug and erroneously jumped to a non-code
==1842==    location.  If you are running Memcheck and you just saw a
==1842==    warning about a bad jump, it's probably your program's fault.
==1842== 2. The instruction is legitimate but Valgrind doesn't handle it,
==1842==    i.e. it's Valgrind's fault.  If you think this is the case or
==1842==    you are not sure, please let us know and we'll try to fix it.
==1842== Either way, Valgrind will now raise a SIGILL signal which will
==1842== probably kill your program.
==1842== 
==1842== Process terminating with default action of signal 4 (SIGILL)
==1842==  Illegal opcode at address 0x132AD42
==1842==    at 0x132AD42: zend_mm_alloc_pages (zend_alloc.c:493)
==1842==    by 0x132C47C: zend_mm_alloc_small_slow (zend_alloc.c:1174)
==1842==    by 0x154C2F3: virtual_cwd_startup (zend_virtual_cwd.c:433)
==1842==    by 0x141840C: zend_startup (zend.c:609)
==1842==    by 0x11DA278: php_module_startup (main.c:2088)
==1842==    by 0x17D12B4: php_cli_startup (php_cli.c:421)
==1842==    by 0x43DECD: main (php_cli.c:1335)
Illegal instruction

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 60129542144 bytes) in Command line code on line 1

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x7ffff6000040 --> 0x0 
RCX: 0x1 
RDX: 0x14113e8 (<_zval_dtor_func_for_ptr+1016>:	test   WORD PTR [rdi+0x6],0x3fff)
RSI: 0xffffffffff862550 
RDI: 0x201bb08b0 
RBP: 0x7ffff6055348 --> 0x100000000 
RSP: 0x7fffffffb950 --> 0x1 
RIP: 0x133e319 (<_efree+249>:	cmp    rbx,QWORD PTR [r13+0x0])
R8 : 0x0 
R9 : 0x1f0b680 --> 0x113ef40 (<php_stream_url_wrap_php>:	lea    rsp,[rsp-0x98])
R10: 0x7ffff6000150 --> 0x4a00000 
R11: 0x7ffff6060500 --> 0x80800000002 
R12: 0x1b08b0 
R13: 0x201a00000 
R14: 0x1b0 
R15: 0x0
EFLAGS: 0x10203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x133e30b <_efree+235>:	mov    r14,r12
   0x133e30e <_efree+238>:	and    r13,0xffffffffffe00000
   0x133e315 <_efree+245>:	shr    r14,0xc
=> 0x133e319 <_efree+249>:	cmp    rbx,QWORD PTR [r13+0x0]
   0x133e31d <_efree+253>:	lea    r10,[r13+r14*4+0x0]
   0x133e322 <_efree+258>:	mov    ebp,DWORD PTR [r10+0x1f8]
   0x133e329 <_efree+265>:	jne    0x133eda5 <_efree+2949>
   0x133e32f <_efree+271>:	nop
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffb950 --> 0x1 
0008| 0x7fffffffb958 --> 0x148b542 (<zend_array_destroy+34>:	mov    rax,QWORD PTR [rsp+0x10])
0016| 0x7fffffffb960 --> 0x14113e8 (<_zval_dtor_func_for_ptr+1016>:	test   WORD PTR [rdi+0x6],0x3fff)
0024| 0x7fffffffb968 --> 0x1 
0032| 0x7fffffffb970 --> 0x0 
0040| 0x7fffffffb978 --> 0xb054807b4bee9f00 
0048| 0x7fffffffb980 --> 0x0 
0056| 0x7fffffffb988 --> 0x7ffff605b6a0 --> 0x7ffff6055348 --> 0x100000000 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
_efree () at /home/geeknik/php-src/Zend/zend_alloc.c:1341
1341			ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted");

This bug was found with american fuzzy lop (http://lcamtuf.coredump.cx/afl).

Test script:
---------------
<?php unserialize(file_get_contents("test")); ?>

download test here: https://www.dropbox.com/s/n7wo7dmnc0fl1ug/test?dl=0

Expected result:
----------------
No crash, like with with PHP 5.4.39-0+deb7u1 (cli) where this is the behavior:
 
PHP Fatal error:  Out of memory (allocated 17301504) (tried to allocate 17179869184 bytes) in /home/geeknik/findings/unserialize/crashes/test.php on line 1


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-03-25 15:36 UTC] ab@php.net
-PHP Version: master-Git-2015-03-25 (Git) +PHP Version: PHP-5.5
 [2015-03-25 15:36 UTC] ab@php.net
I can repro this with 5.5 and the string from the test linked

unserialize('a:1126666:{i:0;r:1;i:-09610;b:1;i:-0;i:0;i:0;O:1:"A":2119X:i:0;'
            'i:0;i:0;i:0;i:0;O:1:"A":2116:{i:0;r:5;i:-0967;a:1126626666:{i:0;'
            'r:1;i:-09610;66:{');

So this is probably bigger than just master (wouldn't need  to be security probably).

5.4 looks good, it throws a fatal error instead.
 [2015-03-25 15:36 UTC] ab@php.net
-Status: Open +Status: Verified
 [2015-04-06 00:02 UTC] stas@php.net
I wasn't able to reproduce it, neither on OS X nor on Linux. Though I am using 64-bit builds, maybe it only happens on 32-bit? 

Also note that if you run code that OOMs under valgrind with USE_ZEND_ALLOC=0, you'll get segfault, since we do not check for memory allocation fails (emalloc bails out on OOM). But SIGILL is something different, null pointer does not produce SIGILL. So I don't know what to do this yet without a reproduction.
 [2015-04-06 04:09 UTC] brian dot carpenter at gmail dot com
My PHP build was 64-bit, I never tried to reproduce it on a 32-bit build.
 [2015-04-20 00:20 UTC] brian dot carpenter at gmail dot com
I just rebuilt PHP directly from git source (PHP 7.0.0-dev (cli) (built: Apr 19 2015 19:01:39)) and this issue is still present, however, the GDB output is a bit different than before:

Reading symbols from /home/geeknik/php-src/sapi/cli/php...done.
gdb-peda$ run test.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 60129542144 bytes) in /home/geeknik/test.php on line 1

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7ffff6000040 --> 0x0 
RBX: 0x7ffff605d7e0 --> 0x7ffff6056348 --> 0x8000000100000000 
RCX: 0x1 
RDX: 0x1e4 
RSI: 0x201a00000 
RDI: 0x201be48f0 
RBP: 0x0 
RSP: 0x7fffffffb9b0 --> 0x7ffff6053000 --> 0x0 
RIP: 0x134f84e (<_efree+238>:	cmp    rax,QWORD PTR [rsi])
R8 : 0x0 
R9 : 0x20 (' ')
R10: 0x7ffff60555a0 --> 0x7ffff6055500 --> 0x7ffff6055000 --> 0x7ffff6055020 --> 0x7ffff6055460 --> 0x7ffff6055440 --> 0x7ffff6055400 --> 0x7ffff60553e0 --> 0x7ffff60553c0 --> 0x7ffff60553a0 --> 0x7ffff6055380 --> 0x7ffff6055360 --> 0x7ffff6055340 --> 0x7ffff6055320 --> 0x7ffff6055300 --> 0x7ffff60552e0 --> 0x7ffff60552c0 --> 0x7ffff60552a0 --> 0x7ffff6055280 --> 0x7ffff6055260 --> 0x7ffff6055240 --> 0x7ffff6055220 --> 0x7ffff6055200 --> 0x7ffff60551e0 --> 0x7ffff60551c0 --> 0x7ffff60551a0 --> 0x7ffff6055180 --> 0x7ffff6055160 --> 0x7ffff6055140 --> 0x7ffff6055120 --> 0x7ffff6055100 --> 0x7ffff60550e0 --> 0x7ffff60550c0 --> 0x7ffff60550a0 --> 0x7ffff6055080 --> 0x7ffff6055060 --> 0x7ffff6055040 --> 0x7ffff6055420 --> 0x7ffff6055600 --> 0x7ffff6055620 --> 0x7ffff6055640 --> 0x7ffff6055660 --> 0x7ffff6055680 --> 0x7ffff60556a0 --> 0x7ffff60556c0 --> 0x7ffff60556e0 --> 0x7ffff6055700 --> 0x7ffff6055720 --> 0x7ffff6055740 --> 0x7ffff6055760 --> 0x7ffff6055780 --> 0x7ffff60557a0 --> 0x7ffff60557c0 --> 0x7ffff60557e0 --> 0x7ffff6055800 --> 0x7ffff6055820 --> 0x7ffff6055840 --> 0x7ffff6055860 --> 0x7ffff6055880 --> 0x7ffff60558a0 --> 0x7ffff60558c0 --> 0x7ffff60558e0 --> 0x7ffff6055900 --> 0x7ffff6055920 --> 0x7ffff6055940 --> 0x7ffff6055960 --> 0x7ffff6055980 --> 0x7ffff60559a0 --> 0x7ffff60559c0 --> 0x7ffff60559e0 --> 0x7ffff6055a00 --> 0x7ffff6055a20 --> 0x7ffff6055a40 --> 0x7ffff6055a60 --> 0x7ffff6055a80 --> 0x7ffff6055aa0 --> 0x7ffff6055ac0 --> 0x7ffff6055ae0 --> 0x7ffff6055b00 --> 0x7ffff6055b20 --> 0x7ffff6055b40 --> 0x7ffff6055b60 --> 0x7ffff6055b80 --> 0x7ffff6055ba0 --> 0x7ffff6055bc0 --> 0x7ffff6055be0 --> 0x7ffff6055c00 --> 0x7ffff6055c20 --> 0x7ffff6055c40 --> 0x7ffff6055c60 --> 0x7ffff6055c80 --> 0x7ffff6055ca0 --> 0x7ffff6055cc0 --> 0x7ffff6055ce0 --> 0x7ffff6055d00 --> 0x7ffff6055d20 --> 0x7ffff6055d40 --> 0x7ffff6055d60 --> 0x7ffff6055d80 --> 0x7ffff6055da0 --> 0x7ffff6055dc0 --> 0x7ffff6055de0 --> 0x7ffff6055e00 --> 0x7ffff6055e20 --> 0x7ffff6055e40 --> 0x7ffff6055e60 --> 0x7ffff6055e80 --> 0x7ffff6055ea0 --> 0x7ffff6055ec0 --> 0x7ffff6055ee0 --> 0x7ffff6055f00 --> 0x7ffff6055f20 --> 0x7ffff6055f40 --> 0x7ffff6055f60 --> 0x7ffff6055f80 --> 0x7ffff6055fa0 --> 0x7ffff6055fc0 --> 0x7ffff6055fe0 --> 0x0 
R11: 0x7ffff605c6b8 --> 0x80800000002 
R12: 0x7ffff6056348 --> 0x8000000100000000 
R13: 0x0 
R14: 0x1 
R15: 0x1f6b900 --> 0x18
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x134f840 <_efree+224>:	mov    rsi,rdi
   0x134f843 <_efree+227>:	shr    rdx,0xc
   0x134f847 <_efree+231>:	and    rsi,0xffffffffffe00000
=> 0x134f84e <_efree+238>:	cmp    rax,QWORD PTR [rsi]
   0x134f851 <_efree+241>:	mov    ecx,DWORD PTR [rsi+rdx*4+0x1f8]
   0x134f858 <_efree+248>:	jne    0x134fa90 <_efree+816>
   0x134f85e <_efree+254>:	xchg   ax,ax
   0x134f860 <_efree+256>:	lea    rsp,[rsp-0x98]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffb9b0 --> 0x7ffff6053000 --> 0x0 
0008| 0x7fffffffb9b8 --> 0x41d3eb7ce9929000 
0016| 0x7fffffffb9c0 --> 0x0 
0024| 0x7fffffffb9c8 --> 0x149b306 (<zend_array_destroy+4246>:	xchg   ax,ax)
0032| 0x7fffffffb9d0 --> 0x0 
0040| 0x7fffffffb9d8 --> 0x41d3eb7ce9929000 
0048| 0x7fffffffb9e0 --> 0x14a28e6 (<zend_hash_graceful_reverse_destroy+2038>:	)
0056| 0x7fffffffb9e8 --> 0x7ffff605d7e0 --> 0x7ffff6056348 --> 0x8000000100000000 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x000000000134f84e in _efree ()
gdb-peda$ bt
#0  0x000000000134f84e in _efree ()
#1  0x000000000149b306 in zend_array_destroy ()
#2  0x000000000149bed5 in zend_array_destroy ()
#3  0x00000000015a1fdd in zend_object_std_dtor ()
#4  0x00000000015bab22 in zend_objects_store_free_object_storage ()
#5  0x00000000013be403 in shutdown_executor ()
#6  0x000000000142a4a8 in zend_deactivate ()
#7  0x00000000011ea1ad in php_request_shutdown ()
#8  0x0000000001807337 in do_cli ()
    at /home/geeknik/php-src/sapi/cli/php_cli.c:1135
#9  0x000000000043bbb1 in main ()
    at /home/geeknik/php-src/sapi/cli/php_cli.c:1334
#10 0x00007ffff6a1dead in __libc_start_main (main=<optimized out>, 
    argc=<optimized out>, ubp_av=<optimized out>, init=<optimized out>, 
    fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe358)
    at libc-start.c:244
#11 0x000000000043be41 in _start ()
gdb-peda$ p $rip
$1 = (void (*)()) 0x134f84e <_efree+238>
 [2015-04-28 05:38 UTC] stas@php.net
I am still unable to reproduce it anywhere but PHP 7.
 [2015-05-22 09:54 UTC] kaplan@php.net
Let's start by fixing it for PHP 7? Then we can dig into the 5.5/5.6 reproduction.
 [2015-06-08 18:37 UTC] brian dot carpenter at gmail dot com
I have another test case that causes a similar crash (at least according to the valgrind output).

vex amd64->IR: unhandled instruction bytes: 0xF3 0x4D 0xF 0xBC 0xE4 0x45 0x1 0xC4
==59766== valgrind: Unrecognised instruction at address 0x1319a3a.
==59766==    at 0x1319A3A: zend_mm_alloc_pages (zend_alloc.c:483)
==59766==    by 0x131B81C: zend_mm_alloc_small_slow (zend_alloc.c:1190)
==59766==    by 0x155F573: virtual_cwd_startup (zend_virtual_cwd.c:431)
==59766==    by 0x1412DCC: zend_startup (zend.c:640)
==59766==    by 0x11C6F98: php_module_startup (main.c:2066)
==59766==    by 0x181CFCC: php_cgi_startup (cgi_main.c:915)
==59766==    by 0x43B4AC: main (cgi_main.c:1894)
==59766== Your program just tried to execute an instruction that Valgrind
==59766== did not recognise.  There are two possible reasons for this.
==59766== 1. Your program has a bug and erroneously jumped to a non-code
==59766==    location.  If you are running Memcheck and you just saw a
==59766==    warning about a bad jump, it's probably your program's fault.
==59766== 2. The instruction is legitimate but Valgrind doesn't handle it,
==59766==    i.e. it's Valgrind's fault.  If you think this is the case or
==59766==    you are not sure, please let us know and we'll try to fix it.
==59766== Either way, Valgrind will now raise a SIGILL signal which will
==59766== probably kill your program.
==59766== 
==59766== Process terminating with default action of signal 4 (SIGILL)
==59766==  Illegal opcode at address 0x1319A3A
==59766==    at 0x1319A3A: zend_mm_alloc_pages (zend_alloc.c:483)
==59766==    by 0x131B81C: zend_mm_alloc_small_slow (zend_alloc.c:1190)
==59766==    by 0x155F573: virtual_cwd_startup (zend_virtual_cwd.c:431)
==59766==    by 0x1412DCC: zend_startup (zend.c:640)
==59766==    by 0x11C6F98: php_module_startup (main.c:2066)
==59766==    by 0x181CFCC: php_cgi_startup (cgi_main.c:915)
==59766==    by 0x43B4AC: main (cgi_main.c:1894)
Illegal instruction

I can't get a stack trace in gdb, all I get is this:
Starting program: /home/geeknik/php-src/sapi/cgi/php-cgi test00-min
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
X-Powered-By: PHP/7.0.0-dev
Content-type: text/html; charset=UTF-8

<br />
<b>Fatal error</b>:  Uncaught EngineException: Call to undefined function t() in /home/geeknik/php-tmp/out/fuzzer04/crashes/test00-min:2
Stack trace:
#0 {main}
  thrown in <b>/home/geeknik/php-tmp/out/fuzzer04/crashes/test00-min</b> on line <b>2</b><br />
[Inferior 1 (process 41113) exited with code 0377]

Hexdump:
0000000 3f3c 6870 0a70 6124 613d 7272 7961 8028
0000010 612e 7272 7961 2928 3b29 2874 3b29     
000001e

Test case: https://www.dropbox.com/s/zhelyjjnuw67v39/test00-min?dl=0
 [2015-06-08 20:09 UTC] stas@php.net
-Type: Security +Type: Bug -Package: Unknown/Other Function +Package: Reproducible crash -PHP Version: PHP-5.5 +PHP Version: PHP-7
 [2015-06-08 20:09 UTC] stas@php.net
As these seem to be reproducible only in PHP 7, no need to list as security. Also, the second one seems to be completely different issue, so I'd suggest filing a separate issue.
 [2016-08-08 15:27 UTC] cmb@php.net
I can reproduce invalid reads/writes (apparently NULL pointer
derefs) with current PHP-5.6 and master (didn't check other
versions) with Anatol's test script (the original isn't available
anymore; I had to concatenate the strings in Anatol's script to
make it work). However, valgrind doesn't report an unrecognised
instruction (which might be bogus anyway, as it's possible that
"the instruction is legitimate but Valgrind doesn't handle it").

The following has been produced by running against a debug build
of current master (e20e9c6):

vagrant@debian-8:/vagrant/php-src$ USE_ZEND_ALLOC=0 valgrind -q sapi/cli/php -n
 ../69295.php
==4581== Invalid write of size 8
==4581==    at 0x4C2F467: memset (vg_replace_strmem.c:1094)
==4581==    by 0x63F8B1: zend_hash_real_init_ex (zend_hash.c:154)
==4581==    by 0x63FB50: zend_hash_real_init (zend_hash.c:203)
==4581==    by 0x59000E: php_var_unserialize_ex (var_unserializer.re:716)
==4581==    by 0x58EAB1: process_nested_data (var_unserializer.re:372)
==4581==    by 0x58EEA0: object_common2 (var_unserializer.re:469)
==4581==    by 0x58FD4F: php_var_unserialize_ex (var_unserializer.re:875)
==4581==    by 0x58EAB1: process_nested_data (var_unserializer.re:372)
==4581==    by 0x58EEA0: object_common2 (var_unserializer.re:469)
==4581==    by 0x58FD4F: php_var_unserialize_ex (var_unserializer.re:875)
==4581==    by 0x58EAB1: process_nested_data (var_unserializer.re:372)
==4581==    by 0x590051: php_var_unserialize_ex (var_unserializer.re:719)
==4581==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==4581==
==4581==
==4581== Process terminating with default action of signal 11 (SIGSEGV)
==4581==  Access not within mapped region at address 0x0
==4581==    at 0x4C2F467: memset (vg_replace_strmem.c:1094)
==4581==    by 0x63F8B1: zend_hash_real_init_ex (zend_hash.c:154)
==4581==    by 0x63FB50: zend_hash_real_init (zend_hash.c:203)
==4581==    by 0x59000E: php_var_unserialize_ex (var_unserializer.re:716)
==4581==    by 0x58EAB1: process_nested_data (var_unserializer.re:372)
==4581==    by 0x58EEA0: object_common2 (var_unserializer.re:469)
==4581==    by 0x58FD4F: php_var_unserialize_ex (var_unserializer.re:875)
==4581==    by 0x58EAB1: process_nested_data (var_unserializer.re:372)
==4581==    by 0x58EEA0: object_common2 (var_unserializer.re:469)
==4581==    by 0x58FD4F: php_var_unserialize_ex (var_unserializer.re:875)
==4581==    by 0x58EAB1: process_nested_data (var_unserializer.re:372)
==4581==    by 0x590051: php_var_unserialize_ex (var_unserializer.re:719)
==4581==  If you believe this happened as a result of a stack
==4581==  overflow in your program's main thread (unlikely but
==4581==  possible), you can try to increase the size of the
==4581==  main thread stack using the --main-stacksize= flag.
==4581==  The main thread stack size used in this run was 8388608.
Segmentation fault
 [2017-01-01 13:48 UTC] nikic@php.net
-Status: Verified +Status: Closed -Assigned To: +Assigned To: nikic
 [2017-01-01 13:48 UTC] nikic@php.net
Some time ago the USE_ZEND_ALLOC=0 fallback allocator handlers have been adjusted to check for OOM, so this issue is resolved now.
 
PHP Copyright © 2001-2019 The PHP Group
All rights reserved.
Last updated: Mon May 20 16:01:26 2019 UTC