Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ PHP NEWS
TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du)
. Fixed various memory related issues in ext/sockets. (David Carlier)

- Standard:
. Fixed bug GH-22844 (Use-after-free in StreamPollHandle after the
underlying stream is closed). (iliaal, arnaud-lb)

- Streams:
. Added a new IO copy API used by php_stream_copy_to_stream_ex() that
leverages platform primitives (sendfile, splice, copy_file_range,
Expand Down
141 changes: 118 additions & 23 deletions ext/standard/io_poll.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "zend_exceptions.h"
#include "php_network.h"
#include "php_poll.h"
#include "io_poll.h"
#include "io_poll_arginfo.h"
#include "io_poll_decl.h"

Expand Down Expand Up @@ -54,6 +55,8 @@ typedef struct php_io_poll_watcher_object {
zval data;
bool active;
php_io_poll_context_object *context; /* Back reference to Context object */
php_socket_t fd;
php_stream *stream;
zend_object std;
} php_io_poll_watcher_object;

Expand All @@ -66,7 +69,6 @@ struct php_io_poll_context_object {

/* Stream poll handle specific data */
typedef struct php_stream_poll_handle_data {
php_stream *stream;
zend_resource *res;
} php_stream_poll_handle_data;

Expand Down Expand Up @@ -179,16 +181,24 @@ static const char *php_io_poll_backend_type_to_name(php_poll_backend_type type)

/* Stream Poll Handle Implementation */

static zend_always_inline php_stream *php_stream_poll_handle_get_stream(php_stream_poll_handle_data *data)
{
if (!data || !data->res) {
return NULL;
}
return (php_stream *) zend_fetch_resource2(data->res, NULL, php_file_le_stream(), php_file_le_pstream());
}

static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle)
{
php_stream_poll_handle_data *data = handle->handle_data;
php_stream *stream = php_stream_poll_handle_get_stream(handle->handle_data);
php_socket_t fd;

if (!data || !data->stream) {
if (!stream) {
return SOCK_ERR;
}

if (php_stream_cast(data->stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL,
if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL,
(void *) &fd, 1)
!= SUCCESS
|| fd == -1) {
Expand All @@ -200,8 +210,8 @@ static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle

static int php_stream_poll_handle_is_valid(php_poll_handle_object *handle)
{
php_stream_poll_handle_data *data = handle->handle_data;
return data && data->stream && !php_stream_eof(data->stream);
php_stream *stream = php_stream_poll_handle_get_stream(handle->handle_data);
return stream && !php_stream_eof(stream);
}

static void php_stream_poll_handle_cleanup(php_poll_handle_object *handle)
Expand Down Expand Up @@ -254,6 +264,8 @@ static zend_object *php_io_poll_watcher_create_object(zend_class_entry *ce)
intern->triggered_events = 0;
intern->active = false;
intern->context = NULL;
intern->fd = SOCK_ERR;
intern->stream = NULL;
ZVAL_NULL(&intern->data);

return &intern->std;
Expand All @@ -272,12 +284,94 @@ static zend_object *php_io_poll_context_create_object(zend_class_entry *ce)
return &intern->std;
}

static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr)
{
zend_ulong key = (zend_ulong) (uintptr_t) ptr;
return (key >> 3) | (key << ((sizeof(key) * 8) - 3));
}

static zend_always_inline php_stream *php_io_poll_watcher_get_stream(php_io_poll_watcher_object *watcher)
{
if (!watcher->handle || watcher->handle->ops != &php_stream_poll_handle_ops) {
return NULL;
}
return php_stream_poll_handle_get_stream(watcher->handle->handle_data);
}

static void php_io_poll_stream_watch(php_stream *stream, php_io_poll_watcher_object *watcher)
{
if (!stream->poll_watchers) {
stream->poll_watchers = pemalloc(sizeof(HashTable), stream->is_persistent);
zend_hash_init(stream->poll_watchers, 4, NULL, NULL, stream->is_persistent);
}

zval zv;
ZVAL_PTR(&zv, watcher);
zend_hash_index_update(stream->poll_watchers, php_io_poll_compute_ptr_key(watcher), &zv);
}

static void php_io_poll_stream_unwatch(php_io_poll_watcher_object *watcher)
{
php_stream *stream = watcher->stream;
watcher->stream = NULL;

if (!stream || !stream->poll_watchers) {
return;
}

zend_hash_index_del(stream->poll_watchers, php_io_poll_compute_ptr_key(watcher));

if (zend_hash_num_elements(stream->poll_watchers) == 0) {
zend_hash_destroy(stream->poll_watchers);
pefree(stream->poll_watchers, stream->is_persistent);
stream->poll_watchers = NULL;
}
}

PHPAPI void php_io_poll_stream_notify_close(php_stream *stream)
{
HashTable *watchers = stream->poll_watchers;
if (!watchers) {
return;
}
stream->poll_watchers = NULL;

ZEND_HASH_FOREACH_VAL(watchers, zval *zv) {
GC_ADDREF(&((php_io_poll_watcher_object *) Z_PTR_P(zv))->std);
} ZEND_HASH_FOREACH_END();

ZEND_HASH_FOREACH_VAL(watchers, zval *zv) {
php_io_poll_watcher_object *watcher = Z_PTR_P(zv);
php_io_poll_context_object *context = watcher->context;
watcher->stream = NULL;
if (context) {
if (watcher->fd != SOCK_ERR && context->ctx) {
php_poll_remove(context->ctx, (int) watcher->fd);
}
watcher->active = false;
watcher->context = NULL;
if (context->watchers) {
zend_hash_index_del(context->watchers, php_io_poll_compute_ptr_key(watcher->handle));
}
}
} ZEND_HASH_FOREACH_END();

ZEND_HASH_FOREACH_VAL(watchers, zval *zv) {
OBJ_RELEASE(&((php_io_poll_watcher_object *) Z_PTR_P(zv))->std);
} ZEND_HASH_FOREACH_END();

zend_hash_destroy(watchers);
pefree(watchers, stream->is_persistent);
}

/* Object Destruction Functions */

static void php_io_poll_watcher_free_object(zend_object *obj)
{
php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(obj);

php_io_poll_stream_unwatch(intern);

zval_ptr_dtor(&intern->data);

if (intern->handle) {
Expand All @@ -294,6 +388,7 @@ static void php_io_poll_context_free_object(zend_object *obj)
if (intern->watchers) {
ZEND_HASH_FOREACH_VAL(intern->watchers, zval *zv) {
php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(zv));
php_io_poll_stream_unwatch(watcher);
watcher->active = false;
watcher->context = NULL;
} ZEND_HASH_FOREACH_END();
Expand Down Expand Up @@ -340,14 +435,6 @@ static HashTable *php_io_poll_context_get_gc(zend_object *obj, zval **table, int
return NULL;
}

/* Utility functions */

static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr)
{
zend_ulong key = (zend_ulong) (uintptr_t) ptr;
return (key >> 3) | (key << ((sizeof(key) * 8) - 3));
}

static zend_result php_io_poll_watcher_modify_events(
php_io_poll_watcher_object *watcher, uint32_t events)
{
Expand All @@ -357,7 +444,7 @@ static zend_result php_io_poll_watcher_modify_events(
return FAILURE;
}

php_socket_t fd = php_poll_handle_get_fd(watcher->handle);
php_socket_t fd = watcher->fd;
if (fd == SOCK_ERR) {
zend_throw_exception(
php_io_poll_invalid_handle_class_entry, "Invalid handle for polling", 0);
Expand Down Expand Up @@ -453,7 +540,6 @@ PHP_METHOD(StreamPollHandle, __construct)

/* Set up stream-specific data */
php_stream_poll_handle_data *data = emalloc(sizeof(php_stream_poll_handle_data));
data->stream = stream;
data->res = stream->res;
intern->handle_data = data;

Expand All @@ -466,14 +552,14 @@ PHP_METHOD(StreamPollHandle, getStream)
ZEND_PARSE_PARAMETERS_NONE();

php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis());
php_stream_poll_handle_data *data = intern->handle_data;
php_stream *stream = php_stream_poll_handle_get_stream(intern->handle_data);

if (!data || !data->stream) {
if (!stream) {
RETURN_NULL();
}

GC_ADDREF(data->stream->res);
php_stream_to_zval(data->stream, return_value);
GC_ADDREF(stream->res);
php_stream_to_zval(stream, return_value);
}

PHP_METHOD(StreamPollHandle, isValid)
Expand Down Expand Up @@ -645,13 +731,15 @@ PHP_METHOD(Io_Poll_Watcher, remove)
php_poll_ctx *poll_ctx = context->ctx;
HashTable *watchers = context->watchers;
zend_ulong hash_key = php_io_poll_compute_ptr_key(intern->handle);
php_socket_t fd = php_poll_handle_get_fd(intern->handle);
if (fd != SOCK_ERR) {
php_poll_remove(poll_ctx, (int) fd);
if (intern->fd != SOCK_ERR) {
php_poll_remove(poll_ctx, (int) intern->fd);
}

php_io_poll_stream_unwatch(intern);

intern->active = false;
intern->context = NULL;
intern->fd = SOCK_ERR;

if (watchers) {
zend_hash_index_del(watchers, hash_key);
Expand Down Expand Up @@ -770,6 +858,13 @@ PHP_METHOD(Io_Poll_Context, add)

watcher->active = true;
watcher->context = intern;
watcher->fd = fd;

php_stream *watched_stream = php_io_poll_watcher_get_stream(watcher);
if (watched_stream) {
watcher->stream = watched_stream;
php_io_poll_stream_watch(watched_stream, watcher);
}
}

PHP_METHOD(Io_Poll_Context, wait)
Expand Down
24 changes: 24 additions & 0 deletions ext/standard/io_poll.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
*/

#ifndef PHP_IO_POLL_H
#define PHP_IO_POLL_H

#include "php.h"

BEGIN_EXTERN_C()

PHPAPI void php_io_poll_stream_notify_close(struct _php_stream *stream);

END_EXTERN_C()

#endif /* PHP_IO_POLL_H */
2 changes: 1 addition & 1 deletion ext/standard/io_poll.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ final class StreamPollHandle implements Io\Poll\Handle
/** @param resource $stream */
public function __construct($stream) {}

/** @return resource */
/** @return resource|null */
public function getStream() {}

public function isValid(): bool {}
Expand Down
2 changes: 1 addition & 1 deletion ext/standard/io_poll_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions ext/standard/io_poll_decl.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions ext/standard/tests/poll/gh22844.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
GH-22844 (StreamPollHandle accessors are safe after the stream is closed)
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

[$r, $w] = pt_new_socket_pair();
$context = pt_new_stream_poll();
$handle = new StreamPollHandle($r);

echo "before close:\n";
echo "isValid: "; var_dump($handle->isValid());
echo "getStream is resource: "; var_dump(is_resource($handle->getStream()));

fclose($r);
fclose($w);

echo "after close:\n";
echo "isValid: "; var_dump($handle->isValid());
echo "getStream: "; var_dump($handle->getStream());

try {
$context->add($handle, [Io\Poll\Event::Read]);
echo "add: no exception\n";
} catch (\Throwable $e) {
echo "add: ", $e::class, ": ", $e->getMessage(), "\n";
}

echo "done\n";
?>
--EXPECT--
before close:
isValid: bool(true)
getStream is resource: bool(true)
after close:
isValid: bool(false)
getStream: NULL
add: Io\Poll\InvalidHandleException: Invalid handle for polling
done
Loading
Loading