Skip to content

Commit d2a425a

Browse files
committed
Fix GH-11078: PHP Fatal error triggers pointer being freed was not allocated and malloc: double free for ptr errors
Although the issue was demonstrated using Curl, the issue is purely in the streams layer of PHP. Full analysis is written in GH-11078 [1], but here is the brief version: Here's what actually happens: 1) We're creating a FILE handle from a stream using the casting mechanism. This will create a cookie-based FILE handle using funopen. 2) We're reading stream data using fread from the userspace stream. This will temporarily set a buffer into a field _bf.base [2]. This buffer is now equal to the upload buffer that Curl allocated and note that that buffer is owned by Curl. 3) The fatal error occurs and we bail out from the fread function, notice how the reset code is never executed and so the buffer will still point to Curl's upload buffer instead of FILE's own buffer [3]. 4) The resources are destroyed, this includes our opened stream and because the FILE handle is cached, it gets destroyed as well. In fact, the stream code calls through fclose on purpose in this case. 5) The fclose code frees the _bs.base buffer [4]. However, this is not the buffer that FILE owns but the one that Curl owns because it isn't reset properly due to the bailout! 6) The objects are getting destroyed, and so the curl free logic is invoked. When Curl tries to gracefully clean up, it tries to free the buffer. But that buffer is actually already freed mistakingly by the C library! This also explains why we can't reproduce it on Linux: this bizarre buffer swapping only happens on macOS and BSD, not on Linux. To solve this, we switch to an unbuffered mode for cookie-based FILEs. This avoids any stateful problems related to buffers especially when the bailout mechanism triggers. As streams have their own buffering mechanism, I don't expect this to impact performance. [1] #11078 (comment) [2] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L102-L103 [3] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L117 [4] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fclose.c#L66-L67
1 parent 46013f1 commit d2a425a

File tree

5 files changed

+83
-2
lines changed

5 files changed

+83
-2
lines changed

ext/zend_test/test.c

+32
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,38 @@ static ZEND_FUNCTION(zend_test_set_fmode)
596596
}
597597
#endif
598598

599+
static ZEND_FUNCTION(zend_test_cast_fread)
600+
{
601+
zval *stream_zv;
602+
php_stream *stream;
603+
FILE *fp;
604+
605+
ZEND_PARSE_PARAMETERS_START(1, 1)
606+
Z_PARAM_RESOURCE(stream_zv);
607+
ZEND_PARSE_PARAMETERS_END();
608+
609+
php_stream_from_zval(stream, stream_zv);
610+
611+
if (php_stream_cast(stream, PHP_STREAM_AS_STDIO, (void *) &fp, REPORT_ERRORS) == FAILURE) {
612+
return;
613+
}
614+
615+
size_t size = 10240; /* Must be large enough to trigger the issue */
616+
char *buf = malloc(size);
617+
bool bail = false;
618+
zend_try {
619+
(void) !fread(buf, 1, size, fp);
620+
} zend_catch {
621+
bail = true;
622+
} zend_end_try();
623+
624+
free(buf);
625+
626+
if (bail) {
627+
zend_bailout();
628+
}
629+
}
630+
599631
static zend_object *zend_test_class_new(zend_class_entry *class_type)
600632
{
601633
zend_object *obj = zend_objects_new(class_type);

ext/zend_test/test.stub.php

+3
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ function zend_test_is_pcre_bundled(): bool {}
186186
#if defined(PHP_WIN32)
187187
function zend_test_set_fmode(bool $binary): void {}
188188
#endif
189+
190+
/** @param resource $stream */
191+
function zend_test_cast_fread($stream): void {}
189192
}
190193

191194
namespace ZendTestNS {

ext/zend_test/test_arginfo.h

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/zend_test/tests/gh11078.phpt

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
GH-11078 (PHP Fatal error triggers pointer being freed was not allocated and malloc: double free for ptr errors)
3+
--EXTENSIONS--
4+
zend_test
5+
--SKIPIF--
6+
<?php
7+
if (getenv('USE_ZEND_ALLOC') === '0') die('skip Zend MM disabled');
8+
?>
9+
--FILE--
10+
<?php
11+
12+
const MEM = 32 * 1024 * 1024;
13+
ini_set('memory_limit', MEM);
14+
15+
class CrashingFifo {
16+
public $context;
17+
18+
function stream_open($path, $mode, $options, &$opened_path): bool {
19+
return true;
20+
}
21+
22+
function stream_read(int $count): false|string|null {
23+
return str_repeat('x', MEM);
24+
}
25+
}
26+
27+
stream_register_wrapper('fifo', CrashingFifo::class);
28+
$readStream = fopen('fifo://1', 'r');
29+
zend_test_cast_fread($readStream);
30+
31+
?>
32+
--EXPECTF--
33+
Fatal error: Allowed memory size of %d bytes exhausted %s

main/streams/cast.c

+8-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@ typedef struct {
4646

4747
FILE *fopencookie(void *cookie, const char *mode, COOKIE_IO_FUNCTIONS_T *funcs)
4848
{
49-
return funopen(cookie, funcs->reader, funcs->writer, funcs->seeker, funcs->closer);
49+
FILE *file = funopen(cookie, funcs->reader, funcs->writer, funcs->seeker, funcs->closer);
50+
if (file) {
51+
/* Buffering of FILE handles is stateful.
52+
* A bailout during these can corrupt the state of the FILE handle
53+
* and cause memory corruption errors. See GH-11078. */
54+
setvbuf(file, NULL, _IONBF, 0);
55+
}
56+
return file;
5057
}
5158
# define HAVE_FOPENCOOKIE 1
5259
# define PHP_EMULATE_FOPENCOOKIE 1

0 commit comments

Comments
 (0)