Skip to content
Merged
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
74 changes: 65 additions & 9 deletions ext/standard/io_poll.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ static zend_object_handlers php_io_poll_watcher_object_handlers;
static zend_object_handlers php_io_poll_handle_object_handlers;

/* Watcher object structure */
typedef struct {
typedef struct php_io_poll_watcher_object {
php_poll_handle_object *handle;
uint32_t watched_events;
uint32_t triggered_events;
Expand All @@ -55,23 +55,22 @@ typedef struct {
} php_io_poll_watcher_object;

/* Context object structure */
typedef struct {
typedef struct php_io_poll_context_object {
php_poll_ctx *ctx;
HashTable *watchers; /* Maps handle pointer -> watcher object */
zend_object std;
} php_io_poll_context_object;

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

/* Accessor macros */
#define PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(_obj) \
((php_io_poll_context_object *) ((char *) (_obj) - offsetof(php_io_poll_context_object, std)))
#define PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(_obj) ZEND_CONTAINER_OF(_obj, php_io_poll_context_object, std)

#define PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(_obj) \
((php_io_poll_watcher_object *) ((char *) (_obj) - offsetof(php_io_poll_watcher_object, std)))
#define PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(_obj) ZEND_CONTAINER_OF(_obj, php_io_poll_watcher_object, std)

#define PHP_POLL_WATCHER_OBJ_FROM_ZV(_zv) PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(_zv))
#define PHP_POLL_CONTEXT_OBJ_FROM_ZV(_zv) PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(Z_OBJ_P(_zv))
Expand Down Expand Up @@ -250,7 +249,9 @@ static void php_stream_poll_handle_cleanup(php_poll_handle_object *handle)
{
php_stream_poll_handle_data *data = (php_stream_poll_handle_data *) handle->handle_data;
if (data) {
/* Don't close the stream - user still owns it */
if (data->res) {
zend_list_delete(data->res);
}
efree(data);
handle->handle_data = NULL;
}
Expand Down Expand Up @@ -331,6 +332,15 @@ static void php_io_poll_context_free_object(zend_object *obj)
{
php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(obj);

if (intern->watchers) {
zval *zv;
ZEND_HASH_FOREACH_VAL(intern->watchers, zv) {
php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(zv));
watcher->active = false;
watcher->poll_ctx = NULL;
} ZEND_HASH_FOREACH_END();
}

if (intern->ctx) {
php_poll_destroy(intern->ctx);
}
Expand All @@ -343,6 +353,36 @@ static void php_io_poll_context_free_object(zend_object *obj)
zend_object_std_dtor(&intern->std);
}

static HashTable *php_io_poll_watcher_get_gc(zend_object *obj, zval **table, int *n)
{
php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(obj);
zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();

zend_get_gc_buffer_add_zval(gc_buffer, &intern->data);
if (intern->handle) {
zend_get_gc_buffer_add_obj(gc_buffer, &intern->handle->std);
}

zend_get_gc_buffer_use(gc_buffer, table, n);
return NULL;
}

static HashTable *php_io_poll_context_get_gc(zend_object *obj, zval **table, int *n)
{
php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(obj);
zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();

if (intern->watchers) {
zval *zv;
ZEND_HASH_FOREACH_VAL(intern->watchers, zv) {
zend_get_gc_buffer_add_zval(gc_buffer, zv);
} ZEND_HASH_FOREACH_END();
}

zend_get_gc_buffer_use(gc_buffer, table, n);
return NULL;
}

/* Utility functions */

static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr)
Expand Down Expand Up @@ -448,13 +488,19 @@ PHP_METHOD(StreamPollHandle, __construct)

php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis());

if (intern->handle_data) {
zend_throw_error(NULL, "StreamPollHandle object is already constructed");
RETURN_THROWS();
}

/* 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;

/* Add reference to stream */
GC_ADDREF(stream->res);
GC_ADDREF(data->res);
}

PHP_METHOD(StreamPollHandle, getStream)
Expand Down Expand Up @@ -657,6 +703,11 @@ PHP_METHOD(Io_Poll_Context, __construct)

php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis());

if (intern->ctx) {
zend_throw_error(NULL, "Io\\Poll\\Context object is already constructed");
RETURN_THROWS();
}

php_poll_backend_type backend_type = PHP_POLL_BACKEND_AUTO;
if (backend_obj != NULL) {
backend_type = php_io_poll_backend_enum_to_type(Z_OBJ_P(backend_obj));
Expand Down Expand Up @@ -861,6 +912,7 @@ PHP_MINIT_FUNCTION(poll)
memcpy(&php_io_poll_handle_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
php_io_poll_handle_object_handlers.offset = offsetof(php_poll_handle_object, std);
php_io_poll_handle_object_handlers.free_obj = php_poll_handle_object_free;
php_io_poll_handle_object_handlers.clone_obj = NULL;
php_stream_poll_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers;

/* Register Watcher class */
Expand All @@ -871,6 +923,8 @@ PHP_MINIT_FUNCTION(poll)
sizeof(zend_object_handlers));
php_io_poll_watcher_object_handlers.offset = offsetof(php_io_poll_watcher_object, std);
php_io_poll_watcher_object_handlers.free_obj = php_io_poll_watcher_free_object;
php_io_poll_watcher_object_handlers.get_gc = php_io_poll_watcher_get_gc;
php_io_poll_watcher_object_handlers.clone_obj = NULL;
php_io_poll_watcher_class_entry->default_object_handlers = &php_io_poll_watcher_object_handlers;

/* Register Context class */
Expand All @@ -881,6 +935,8 @@ PHP_MINIT_FUNCTION(poll)
sizeof(zend_object_handlers));
php_io_poll_context_object_handlers.offset = offsetof(php_io_poll_context_object, std);
php_io_poll_context_object_handlers.free_obj = php_io_poll_context_free_object;
php_io_poll_context_object_handlers.get_gc = php_io_poll_context_get_gc;
php_io_poll_context_object_handlers.clone_obj = NULL;
php_io_poll_context_class_entry->default_object_handlers = &php_io_poll_context_object_handlers;

/* Register exception hierarchy */
Expand Down
6 changes: 3 additions & 3 deletions ext/standard/io_poll.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface Handle
{
}

/*
/**
* @strict-properties
* @not-serializable
*/
Expand Down Expand Up @@ -70,7 +70,7 @@ public function modifyData(mixed $data): void {}
public function remove(): void {}
}

/*
/**
* @strict-properties
* @not-serializable
*/
Expand Down Expand Up @@ -145,7 +145,7 @@ class InvalidHandleException extends PollException {}
}

namespace {
/*
/**
* @strict-properties
* @not-serializable
*/
Expand Down
8 changes: 4 additions & 4 deletions ext/standard/io_poll_arginfo.h

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

26 changes: 26 additions & 0 deletions ext/standard/tests/poll/poll_clone_not_allowed.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Io\Poll: Context, Watcher and StreamPollHandle are not cloneable
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

list($r, $w) = pt_new_socket_pair();
$ctx = pt_new_stream_poll();
$handle = new StreamPollHandle($r);
$watcher = $ctx->add($handle, [Io\Poll\Event::Read]);

foreach ([$ctx, $handle, $watcher] as $obj) {
try {
clone $obj;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}

echo "done\n";
?>
--EXPECT--
Trying to clone an uncloneable object of class Io\Poll\Context
Trying to clone an uncloneable object of class StreamPollHandle
Trying to clone an uncloneable object of class Io\Poll\Watcher
done
28 changes: 28 additions & 0 deletions ext/standard/tests/poll/poll_double_construct.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Io\Poll: calling __construct() twice throws instead of leaking
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

list($r, $w) = pt_new_socket_pair();

$handle = new StreamPollHandle($r);
try {
$handle->__construct($r);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

$ctx = pt_new_stream_poll();
try {
$ctx->__construct();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

echo "done\n";
?>
--EXPECT--
StreamPollHandle object is already constructed
Io\Poll\Context object is already constructed
done
20 changes: 20 additions & 0 deletions ext/standard/tests/poll/poll_stream_handle_close_then_free.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Io\Poll: StreamPollHandle cleanup is safe when the stream is closed first
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

list($r, $w) = pt_new_socket_pair();
$ctx = pt_new_stream_poll();
$watcher = $ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Read]);

// Close the underlying streams before the watcher and handle are freed.
fclose($r);
fclose($w);

unset($watcher, $ctx);
gc_collect_cycles();
echo "ok\n";
?>
--EXPECT--
ok
28 changes: 28 additions & 0 deletions ext/standard/tests/poll/poll_stream_handle_fd_release.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Io\Poll: StreamPollHandle releases its stream resource (no fd leak)
--SKIPIF--
<?php
if (!is_dir('/proc/self/fd')) {
die("skip requires /proc/self/fd (Linux)\n");
}
?>
--FILE--
<?php
function open_fds(): int {
return count(scandir('/proc/self/fd'));
}

$before = open_fds();
for ($i = 0; $i < 100; $i++) {
list($r, $w) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
$h = new StreamPollHandle($r);
unset($h, $r, $w);
}
gc_collect_cycles();
$delta = open_fds() - $before;

// Without the fix each handle pins its stream, leaking ~100 fds.
var_dump($delta < 10);
?>
--EXPECT--
bool(true)
31 changes: 31 additions & 0 deletions ext/standard/tests/poll/poll_watcher_gc_cycle.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Io\Poll: cycle collector reclaims a Watcher referenced through its own data
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

class Canary {
public $ref;
public function __destruct() {
echo "Canary freed\n";
}
}

list($r, $w) = pt_new_socket_pair();
$ctx = pt_new_stream_poll();
$watcher = $ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Read]);

$c = new Canary();
$c->ref = $watcher; // Canary -> Watcher
$watcher->modifyData($c); // Watcher->data -> Canary (cycle)

unset($ctx, $watcher, $c, $r, $w);

echo "before gc\n";
gc_collect_cycles();
echo "after gc\n";
?>
--EXPECT--
before gc
Canary freed
after gc
35 changes: 35 additions & 0 deletions ext/standard/tests/poll/poll_watcher_outlives_context.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
Io\Poll: Watcher operations are safe after its Context is destroyed
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

list($r, $w) = pt_new_socket_pair();
$ctx = pt_new_stream_poll();
$watcher = $ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Read], "data");

// Drop the Context while still holding the Watcher it returned.
unset($ctx);
gc_collect_cycles();

var_dump($watcher->isActive());

try {
$watcher->remove();
} catch (Io\Poll\InactiveWatcherException $e) {
echo $e->getMessage(), "\n";
}

try {
$watcher->modifyEvents([Io\Poll\Event::Write]);
} catch (Io\Poll\InactiveWatcherException $e) {
echo $e->getMessage(), "\n";
}

echo "done\n";
?>
--EXPECT--
bool(false)
Cannot remove inactive watcher
Cannot modify inactive watcher
done
Loading