diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..74fc999 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +## 1.0.2 - 2026-05-23 + +### Fixed + +- Kept callbacks alive while they are being dispatched, preventing use-after-free when callbacks cancel themselves. +- Made I/O watcher dispatch resilient when watchers cancel themselves or each other during event processing. +- Restored previous signal handlers when signal watchers are cancelled or the request shuts down. +- Cancelled replaced signal watchers when registering a new watcher for the same signal. +- Rejected non-finite and excessively large timer values before scheduling timers. +- Guarded internal queue and timer heap capacity growth against integer overflow. + +## 1.0.1 - 2026-05-23 + +### Changed + +- Skipped unnecessary driver polling when no I/O watchers are active. + +## 1.0.0 - 2026-05-23 + +### Added + +- Initial native PHP event loop extension. +- Revolt-compatible `EventLoop\EventLoop` API for deferred callbacks, timers, repeaters, I/O watchers, signal watchers, error handlers, and loop control. +- Fiber suspension support via `EventLoop\Suspension`. +- Auto-selected I/O drivers for epoll, kqueue, poll, and select. +- PHP stubs and generated arginfo for the public API. +- `.phpt` test suite and benchmark scripts. diff --git a/README.md b/README.md index 7dc051b..07d1156 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,11 @@ If you use `Revolt\EventLoop\Suspension`: make test ``` -The extension ships with 26 `.phpt` tests covering defer, delay, repeat, I/O watchers, signals, suspensions, error handling, and edge cases. +The extension ships with 31 `.phpt` tests covering defer, delay, repeat, I/O watchers, signals, suspensions, error handling, and edge cases. + +## Changelog + +Release notes are available in [CHANGELOG.md](CHANGELOG.md). ## Acknowledgements @@ -236,4 +240,4 @@ This project is built on the ideas and API design of [Revolt](https://github.com ## License -Licensed under the [MIT License](LICENSE). \ No newline at end of file +Licensed under the [MIT License](LICENSE). diff --git a/drivers/kqueue.c b/drivers/kqueue.c index f8ca87a..783e2da 100644 --- a/drivers/kqueue.c +++ b/drivers/kqueue.c @@ -94,7 +94,7 @@ static int eventloop_kqueue_add(eventloop_callback *cb) } /* Defer the syscall — will be flushed in poll() */ - kq_changelist_push(cb->io.fd, filter, EV_ADD | EV_CLEAR, cb); + kq_changelist_push(cb->io.fd, filter, EV_ADD | EV_CLEAR, NULL); return SUCCESS; } @@ -121,6 +121,7 @@ static int eventloop_kqueue_poll(double timeout) int ret; int i; eventloop_callback *cb; + HashTable *callbacks; if (timeout < 0) { ts.tv_sec = 1; @@ -142,9 +143,16 @@ static int eventloop_kqueue_poll(double timeout) } for (i = 0; i < ret; i++) { - cb = (eventloop_callback *)kq_events[i].udata; - if (cb && (cb->flags & EVENTLOOP_CB_FLAG_ENABLED) && - !(cb->flags & EVENTLOOP_CB_FLAG_CANCELLED)) { + if (kq_events[i].filter == EVFILT_READ) { + callbacks = &kq_read_cbs; + } else if (kq_events[i].filter == EVFILT_WRITE) { + callbacks = &kq_write_cbs; + } else { + continue; + } + + cb = zend_hash_index_find_ptr(callbacks, (zend_ulong)kq_events[i].ident); + if (cb) { eventloop_dispatch_callback(cb); } } diff --git a/drivers/poll.c b/drivers/poll.c index 6e87798..439ff64 100644 --- a/drivers/poll.c +++ b/drivers/poll.c @@ -55,8 +55,26 @@ static int eventloop_poll_add(eventloop_callback *cb) { uint32_t idx; zend_ulong key; + zval *idx_zv; zval zv; + /* Store index for quick removal. Use a composite key: fd + type to allow + * both readable and writable on the same fd. */ + key = ((zend_ulong)cb->io.fd << 1) | (cb->type == EVENTLOOP_CB_WRITABLE ? 1 : 0); + idx_zv = zend_hash_index_find(&fd_to_index, key); + if (idx_zv) { + idx = (uint32_t)Z_LVAL_P(idx_zv); + pollfds[idx].fd = cb->io.fd; + pollfds[idx].events = (cb->type == EVENTLOOP_CB_READABLE) ? POLLIN : POLLOUT; + pollfds[idx].revents = 0; + + if (pollfd_cbs[idx] != cb) { + pollfd_cbs[idx] = cb; + } + + return SUCCESS; + } + if (pollfds_size >= pollfds_capacity) { pollfds_capacity *= 2; pollfds = erealloc(pollfds, sizeof(struct pollfd) * pollfds_capacity); @@ -69,9 +87,6 @@ static int eventloop_poll_add(eventloop_callback *cb) pollfds[idx].revents = 0; pollfd_cbs[idx] = cb; - /* Store index for quick removal. Use a composite key: fd + type to allow - * both readable and writable on the same fd. */ - key = ((zend_ulong)cb->io.fd << 1) | (cb->type == EVENTLOOP_CB_WRITABLE ? 1 : 0); ZVAL_LONG(&zv, idx); zend_hash_index_update(&fd_to_index, key, &zv); @@ -111,6 +126,7 @@ static void eventloop_poll_remove(eventloop_callback *cb) ZVAL_LONG(&zv, idx); zend_hash_index_update(&fd_to_index, moved_key, &zv); } + pollfd_cbs[pollfds_size] = NULL; } static int eventloop_poll_poll(double timeout) @@ -119,6 +135,8 @@ static int eventloop_poll_poll(double timeout) int ret; uint32_t n; uint32_t i; + uint32_t ready_count = 0; + eventloop_callback **ready; if (pollfds_size == 0) { /* No fds to poll -- just sleep for the timeout duration */ @@ -150,13 +168,21 @@ static int eventloop_poll_poll(double timeout) /* Dispatch events. Iterate a snapshot of the current size since * callbacks may modify the array. */ n = pollfds_size; + ready = safe_emalloc(n, sizeof(eventloop_callback *), 0); for (i = 0; i < n && i < pollfds_size; i++) { if (pollfds[i].revents != 0) { - eventloop_dispatch_callback(pollfd_cbs[i]); + eventloop_cb_addref(pollfd_cbs[i]); + ready[ready_count++] = pollfd_cbs[i]; pollfds[i].revents = 0; } } + for (i = 0; i < ready_count; i++) { + eventloop_dispatch_callback(ready[i]); + eventloop_cb_release(ready[i]); + } + efree(ready); + return ret; } diff --git a/drivers/select.c b/drivers/select.c index d0f9be9..b16392b 100644 --- a/drivers/select.c +++ b/drivers/select.c @@ -23,6 +23,18 @@ static php_socket_t max_fd; static HashTable read_cbs; static HashTable write_cbs; +static void select_ready_push(eventloop_callback ***ready, uint32_t *count, + uint32_t *capacity, eventloop_callback *cb) +{ + if (*count >= *capacity) { + *capacity = *capacity ? *capacity * 2 : 8; + *ready = erealloc(*ready, sizeof(eventloop_callback *) * *capacity); + } + + eventloop_cb_addref(cb); + (*ready)[(*count)++] = cb; +} + static int eventloop_select_init(void) { FD_ZERO(&read_fds); @@ -85,7 +97,11 @@ static int eventloop_select_poll(double timeout) fd_set tmp_read, tmp_write; struct timeval tv; eventloop_callback *cb; + eventloop_callback **ready = NULL; zend_ulong fd; + uint32_t ready_count = 0; + uint32_t ready_capacity = 0; + uint32_t i; int ret; memcpy(&tmp_read, &read_fds, sizeof(fd_set)); @@ -118,17 +134,25 @@ static int eventloop_select_poll(double timeout) /* Dispatch readable events */ ZEND_HASH_FOREACH_NUM_KEY_PTR(&read_cbs, fd, cb) { if (FD_ISSET((php_socket_t)fd, &tmp_read)) { - eventloop_dispatch_callback(cb); + select_ready_push(&ready, &ready_count, &ready_capacity, cb); } } ZEND_HASH_FOREACH_END(); /* Dispatch writable events */ ZEND_HASH_FOREACH_NUM_KEY_PTR(&write_cbs, fd, cb) { if (FD_ISSET((php_socket_t)fd, &tmp_write)) { - eventloop_dispatch_callback(cb); + select_ready_push(&ready, &ready_count, &ready_capacity, cb); } } ZEND_HASH_FOREACH_END(); + for (i = 0; i < ready_count; i++) { + eventloop_dispatch_callback(ready[i]); + eventloop_cb_release(ready[i]); + } + if (ready) { + efree(ready); + } + return ret; } diff --git a/eventloop.c b/eventloop.c index 45f6adc..e7f47e3 100644 --- a/eventloop.c +++ b/eventloop.c @@ -18,6 +18,8 @@ #else # include #endif +# include +# include #ifndef PHP_WIN32 # include @@ -37,6 +39,8 @@ zend_class_entry *eventloop_suspension_ce; static zval callback_type_cases[6]; +#define EVENTLOOP_MAX_TIMER_SECONDS ((double)INT_MAX / 1000.0) + /* {{{ eventloop_now */ double eventloop_now(void) { @@ -89,19 +93,25 @@ eventloop_driver *eventloop_select_best_driver(void) /* {{{ eventloop_dispatch_callback */ void eventloop_dispatch_callback(eventloop_callback *cb) { + eventloop_cb_type type; zval retval; zval params[1]; zend_fcall_info fci; zend_fcall_info_cache fcc; char *errstr = NULL; + eventloop_cb_addref(cb); + if (UNEXPECTED(!(cb->flags & EVENTLOOP_CB_FLAG_ENABLED) || (cb->flags & EVENTLOOP_CB_FLAG_CANCELLED))) { + eventloop_cb_release(cb); return; } + type = cb->type; + /* For defer/delay: auto-cancel after execution */ - if (cb->type == EVENTLOOP_CB_DEFER || cb->type == EVENTLOOP_CB_DELAY) { + if (type == EVENTLOOP_CB_DEFER || type == EVENTLOOP_CB_DELAY) { cb->flags &= ~EVENTLOOP_CB_FLAG_ENABLED; cb->flags |= EVENTLOOP_CB_FLAG_CANCELLED; } @@ -114,6 +124,7 @@ void eventloop_dispatch_callback(eventloop_callback *cb) efree(errstr); } zval_ptr_dtor(params); + eventloop_cb_release(cb); return; } @@ -161,9 +172,11 @@ void eventloop_dispatch_callback(eventloop_callback *cb) zval_ptr_dtor(params); - if (cb->type == EVENTLOOP_CB_DEFER || cb->type == EVENTLOOP_CB_DELAY) { + if (type == EVENTLOOP_CB_DEFER || type == EVENTLOOP_CB_DELAY) { zend_hash_del(&EVENTLOOP_G(callbacks), cb->id); } + + eventloop_cb_release(cb); } /* }}} */ @@ -283,7 +296,10 @@ void eventloop_process_timers(void) if (cb->type == EVENTLOOP_CB_REPEAT) { cb->repeat.expiry = now + cb->repeat.interval; - eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb); + if (!eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb)) { + EVENTLOOP_G(stopped) = true; + break; + } } eventloop_dispatch_callback(cb); @@ -297,6 +313,8 @@ void eventloop_process_timers(void) #ifndef PHP_WIN32 static volatile sig_atomic_t pending_signals[32]; +static struct sigaction previous_signal_actions[32]; +static bool signal_handler_installed[32]; static void eventloop_signal_handler(int signo) { @@ -305,6 +323,57 @@ static void eventloop_signal_handler(int signo) } } +static int eventloop_install_signal_handler(int signo) +{ + struct sigaction sa; + + if (!signal_handler_installed[signo]) { + if (sigaction(signo, NULL, &previous_signal_actions[signo]) != 0) { + return FAILURE; + } + signal_handler_installed[signo] = true; + } + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = eventloop_signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + + if (sigaction(signo, &sa, NULL) != 0) { + sigaction(signo, &previous_signal_actions[signo], NULL); + signal_handler_installed[signo] = false; + return FAILURE; + } + + return SUCCESS; +} + +static void eventloop_restore_signal_handler(int signo) +{ + if (signo < 1 || signo >= 32 || !signal_handler_installed[signo]) { + return; + } + + sigaction(signo, &previous_signal_actions[signo], NULL); + signal_handler_installed[signo] = false; + pending_signals[signo] = 0; + zend_hash_index_del(&EVENTLOOP_G(signal_callbacks), (zend_ulong)signo); +} + +void eventloop_signal_callback_cancelled(eventloop_callback *cb) +{ + zval *id_zv; + + if (cb->type != EVENTLOOP_CB_SIGNAL) { + return; + } + + id_zv = zend_hash_index_find(&EVENTLOOP_G(signal_callbacks), (zend_ulong)cb->signal.signo); + if (id_zv && zend_string_equals(Z_STR_P(id_zv), cb->id)) { + eventloop_restore_signal_handler(cb->signal.signo); + } +} + static void eventloop_process_signals(void) { int i; @@ -401,6 +470,48 @@ static eventloop_callback *eventloop_find_or_throw(zend_string *id) return cb; } +static bool eventloop_validate_timer_value(double value) +{ + if (UNEXPECTED(!isfinite(value))) { + zend_argument_value_error(1, "must be finite, %f given", value); + return false; + } + + if (UNEXPECTED(value < 0)) { + zend_argument_value_error(1, "must be non-negative, %f given", value); + return false; + } + + if (UNEXPECTED(value > EVENTLOOP_MAX_TIMER_SECONDS)) { + zend_argument_value_error(1, "must be less than or equal to %f, %f given", + EVENTLOOP_MAX_TIMER_SECONDS, value); + return false; + } + + return true; +} + +static bool eventloop_next_capacity(uint32_t current, uint32_t initial, + size_t element_size, uint32_t *next, const char *name) +{ + if (current == 0) { + *next = initial; + } else { + if (UNEXPECTED(current > UINT32_MAX / 2)) { + zend_throw_error(NULL, "EventLoop %s capacity exceeded", name); + return false; + } + *next = current * 2; + } + + if (UNEXPECTED(*next > SIZE_MAX / element_size)) { + zend_throw_error(NULL, "EventLoop %s allocation size exceeded", name); + return false; + } + + return true; +} + static php_socket_t eventloop_stream_to_fd(zval *stream_zv) { php_stream *stream; @@ -432,6 +543,7 @@ ZEND_METHOD(EventLoop_EventLoop, queue) uint32_t argc; eventloop_microtask *mt; uint32_t i; + uint32_t new_capacity; zval tmp; ZEND_PARSE_PARAMETERS_START(1, -1) @@ -440,8 +552,11 @@ ZEND_METHOD(EventLoop_EventLoop, queue) ZEND_PARSE_PARAMETERS_END(); if (EVENTLOOP_G(microtask_count) >= EVENTLOOP_G(microtask_capacity)) { - EVENTLOOP_G(microtask_capacity) = EVENTLOOP_G(microtask_capacity) ? - EVENTLOOP_G(microtask_capacity) * 2 : 8; + if (!eventloop_next_capacity(EVENTLOOP_G(microtask_capacity), 8, + sizeof(eventloop_microtask), &new_capacity, "microtask queue")) { + RETURN_THROWS(); + } + EVENTLOOP_G(microtask_capacity) = new_capacity; EVENTLOOP_G(microtask_queue) = erealloc(EVENTLOOP_G(microtask_queue), sizeof(eventloop_microtask) * EVENTLOOP_G(microtask_capacity)); } @@ -466,6 +581,7 @@ ZEND_METHOD(EventLoop_EventLoop, defer) { zval *closure; eventloop_callback *cb; + uint32_t new_capacity; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_OBJECT_OF_CLASS(closure, zend_ce_closure) @@ -474,8 +590,12 @@ ZEND_METHOD(EventLoop_EventLoop, defer) cb = eventloop_cb_create(EVENTLOOP_CB_DEFER, closure); if (EVENTLOOP_G(deferred_count) >= EVENTLOOP_G(deferred_capacity)) { - EVENTLOOP_G(deferred_capacity) = EVENTLOOP_G(deferred_capacity) ? - EVENTLOOP_G(deferred_capacity) * 2 : 8; + if (!eventloop_next_capacity(EVENTLOOP_G(deferred_capacity), 8, + sizeof(zend_string *), &new_capacity, "deferred queue")) { + eventloop_cb_cancel(cb); + RETURN_THROWS(); + } + EVENTLOOP_G(deferred_capacity) = new_capacity; EVENTLOOP_G(deferred_queue) = erealloc(EVENTLOOP_G(deferred_queue), sizeof(zend_string *) * EVENTLOOP_G(deferred_capacity)); } @@ -499,8 +619,7 @@ ZEND_METHOD(EventLoop_EventLoop, delay) Z_PARAM_OBJECT_OF_CLASS(closure, zend_ce_closure) ZEND_PARSE_PARAMETERS_END(); - if (UNEXPECTED(delay < 0)) { - zend_argument_value_error(1, "must be non-negative, %f given", delay); + if (!eventloop_validate_timer_value(delay)) { RETURN_THROWS(); } @@ -508,7 +627,10 @@ ZEND_METHOD(EventLoop_EventLoop, delay) cb->delay.delay = delay; cb->delay.expiry = eventloop_now() + delay; - eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb); + if (!eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb)) { + eventloop_cb_cancel(cb); + RETURN_THROWS(); + } RETURN_STR_COPY(cb->id); } @@ -526,8 +648,7 @@ ZEND_METHOD(EventLoop_EventLoop, repeat) Z_PARAM_OBJECT_OF_CLASS(closure, zend_ce_closure) ZEND_PARSE_PARAMETERS_END(); - if (UNEXPECTED(interval < 0)) { - zend_argument_value_error(1, "must be non-negative, %f given", interval); + if (!eventloop_validate_timer_value(interval)) { RETURN_THROWS(); } @@ -535,7 +656,10 @@ ZEND_METHOD(EventLoop_EventLoop, repeat) cb->repeat.interval = interval; cb->repeat.expiry = eventloop_now() + interval; - eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb); + if (!eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb)) { + eventloop_cb_cancel(cb); + RETURN_THROWS(); + } RETURN_STR_COPY(cb->id); } @@ -617,10 +741,8 @@ ZEND_METHOD(EventLoop_EventLoop, onSignal) zend_long signal; zval *closure; eventloop_callback *cb; + zval *existing_id_zv; zval id_zv; -#ifndef PHP_WIN32 - struct sigaction sa; -#endif ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_LONG(signal) @@ -636,18 +758,28 @@ ZEND_METHOD(EventLoop_EventLoop, onSignal) RETURN_THROWS(); } + existing_id_zv = zend_hash_index_find(&EVENTLOOP_G(signal_callbacks), (zend_ulong)signal); + if (existing_id_zv) { + eventloop_callback *existing_cb = eventloop_cb_find(Z_STR_P(existing_id_zv)); + if (existing_cb) { + eventloop_cb_cancel(existing_cb); + } else { + zend_hash_index_del(&EVENTLOOP_G(signal_callbacks), (zend_ulong)signal); + } + } + cb = eventloop_cb_create(EVENTLOOP_CB_SIGNAL, closure); cb->signal.signo = (int)signal; + if (UNEXPECTED(eventloop_install_signal_handler((int)signal) != SUCCESS)) { + eventloop_cb_cancel(cb); + zend_throw_error(NULL, "Failed to register signal handler"); + RETURN_THROWS(); + } + ZVAL_STR_COPY(&id_zv, cb->id); zend_hash_index_update(&EVENTLOOP_G(signal_callbacks), (zend_ulong)signal, &id_zv); - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = eventloop_signal_handler; - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_RESTART; - sigaction((int)signal, &sa, NULL); - RETURN_STR_COPY(cb->id); #endif } @@ -669,6 +801,9 @@ ZEND_METHOD(EventLoop_EventLoop, enable) } eventloop_cb_enable(cb); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } RETURN_STR_COPY(id); } /* }}} */ @@ -1025,7 +1160,7 @@ ZEND_METHOD(EventLoop_EventLoop, getDriver) static void eventloop_callback_dtor(zval *zv) { - eventloop_cb_free(Z_PTR_P(zv)); + eventloop_cb_release(Z_PTR_P(zv)); } /* {{{ PHP_GINIT_FUNCTION */ @@ -1099,6 +1234,7 @@ PHP_RINIT_FUNCTION(eventloop) #ifndef PHP_WIN32 memset((void *)pending_signals, 0, sizeof(pending_signals)); + memset(signal_handler_installed, 0, sizeof(signal_handler_installed)); #endif return SUCCESS; @@ -1110,6 +1246,12 @@ PHP_RSHUTDOWN_FUNCTION(eventloop) { uint32_t i; +#ifndef PHP_WIN32 + for (i = 1; i < 32; i++) { + eventloop_restore_signal_handler((int)i); + } +#endif + if (EVENTLOOP_G(driver)) { EVENTLOOP_G(driver)->shutdown(); EVENTLOOP_G(driver) = NULL; diff --git a/eventloop_cb.c b/eventloop_cb.c index 910b1dd..ce04af5 100644 --- a/eventloop_cb.c +++ b/eventloop_cb.c @@ -23,6 +23,7 @@ eventloop_callback *eventloop_cb_create(eventloop_cb_type type, zval *closure) cb->id = eventloop_generate_id(); cb->type = type; cb->flags = EVENTLOOP_CB_FLAG_ENABLED | EVENTLOOP_CB_FLAG_REFERENCED; + cb->refcount = 1; /* Held by EVENTLOOP_G(callbacks). */ ZVAL_COPY(&cb->closure, closure); cb->heap_index = UINT32_MAX; @@ -43,6 +44,25 @@ void eventloop_cb_free(eventloop_callback *cb) efree(cb); } +void eventloop_cb_addref(eventloop_callback *cb) +{ + ZEND_ASSERT(cb != NULL); + ZEND_ASSERT(cb->refcount > 0); + + cb->refcount++; +} + +void eventloop_cb_release(eventloop_callback *cb) +{ + ZEND_ASSERT(cb != NULL); + ZEND_ASSERT(cb->refcount > 0); + + cb->refcount--; + if (cb->refcount == 0) { + eventloop_cb_free(cb); + } +} + eventloop_callback *eventloop_cb_find(const zend_string *id) { return zend_hash_find_ptr(&EVENTLOOP_G(callbacks), (zend_string *)id); @@ -71,11 +91,15 @@ void eventloop_cb_enable(eventloop_callback *cb) break; case EVENTLOOP_CB_DELAY: cb->delay.expiry = eventloop_now() + cb->delay.delay; - eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb); + if (!eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb)) { + cb->flags &= ~EVENTLOOP_CB_FLAG_ENABLED; + } break; case EVENTLOOP_CB_REPEAT: cb->repeat.expiry = eventloop_now() + cb->repeat.interval; - eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb); + if (!eventloop_timer_heap_push(&EVENTLOOP_G(timer_heap), cb)) { + cb->flags &= ~EVENTLOOP_CB_FLAG_ENABLED; + } break; default: break; @@ -121,6 +145,11 @@ void eventloop_cb_cancel(eventloop_callback *cb) if (cb->type == EVENTLOOP_CB_READABLE || cb->type == EVENTLOOP_CB_WRITABLE) { EVENTLOOP_G(io_watcher_count)--; } +#ifndef PHP_WIN32 + if (cb->type == EVENTLOOP_CB_SIGNAL) { + eventloop_signal_callback_cancelled(cb); + } +#endif eventloop_cb_disable(cb); cb->flags |= EVENTLOOP_CB_FLAG_CANCELLED; diff --git a/eventloop_timer.c b/eventloop_timer.c index 756c814..de186dd 100644 --- a/eventloop_timer.c +++ b/eventloop_timer.c @@ -7,6 +7,22 @@ #define TIMER_HEAP_INITIAL_CAPACITY 16 +static bool timer_heap_next_capacity(uint32_t current, uint32_t *next) +{ + if (UNEXPECTED(current > UINT32_MAX / 2)) { + zend_throw_error(NULL, "EventLoop timer heap capacity exceeded"); + return false; + } + + *next = current * 2; + if (UNEXPECTED((size_t)*next > SIZE_MAX / sizeof(eventloop_callback *))) { + zend_throw_error(NULL, "EventLoop timer heap allocation size exceeded"); + return false; + } + + return true; +} + static inline double timer_expiry(const eventloop_callback *cb) { if (cb->type == EVENTLOOP_CB_DELAY) { @@ -87,12 +103,17 @@ void eventloop_timer_heap_destroy(eventloop_timer_heap *heap) heap->capacity = 0; } -void eventloop_timer_heap_push(eventloop_timer_heap *heap, eventloop_callback *cb) +bool eventloop_timer_heap_push(eventloop_timer_heap *heap, eventloop_callback *cb) { + uint32_t new_capacity; + ZEND_ASSERT(heap->data != NULL); if (heap->size >= heap->capacity) { - heap->capacity *= 2; + if (!timer_heap_next_capacity(heap->capacity, &new_capacity)) { + return false; + } + heap->capacity = new_capacity; heap->data = erealloc(heap->data, sizeof(eventloop_callback *) * heap->capacity); } @@ -101,6 +122,8 @@ void eventloop_timer_heap_push(eventloop_timer_heap *heap, eventloop_callback *c heap->size++; heap_sift_up(heap, cb->heap_index); + + return true; } eventloop_callback *eventloop_timer_heap_peek(const eventloop_timer_heap *heap) diff --git a/php_eventloop.h b/php_eventloop.h index f3bd060..19a6b9c 100644 --- a/php_eventloop.h +++ b/php_eventloop.h @@ -17,7 +17,7 @@ # include "zend_fibers.h" # endif -# define PHP_EVENTLOOP_VERSION "1.0.0" +# define PHP_EVENTLOOP_VERSION "1.0.2" extern zend_module_entry eventloop_module_entry; # define phpext_eventloop_ptr &eventloop_module_entry @@ -57,6 +57,7 @@ struct _eventloop_callback { zend_string *id; eventloop_cb_type type; uint8_t flags; + uint32_t refcount; zval closure; union { @@ -146,16 +147,22 @@ double eventloop_now(void); /* Callback management */ eventloop_callback *eventloop_cb_create(eventloop_cb_type type, zval *closure); void eventloop_cb_free(eventloop_callback *cb); +void eventloop_cb_addref(eventloop_callback *cb); +void eventloop_cb_release(eventloop_callback *cb); eventloop_callback *eventloop_cb_find(const zend_string *id); void eventloop_cb_enable(eventloop_callback *cb); void eventloop_cb_disable(eventloop_callback *cb); void eventloop_cb_cancel(eventloop_callback *cb); bool eventloop_has_referenced_callbacks(void); +# ifndef PHP_WIN32 +void eventloop_signal_callback_cancelled(eventloop_callback *cb); +# endif + /* Timer heap */ void eventloop_timer_heap_init(eventloop_timer_heap *heap); void eventloop_timer_heap_destroy(eventloop_timer_heap *heap); -void eventloop_timer_heap_push(eventloop_timer_heap *heap, eventloop_callback *cb); +bool eventloop_timer_heap_push(eventloop_timer_heap *heap, eventloop_callback *cb); eventloop_callback *eventloop_timer_heap_peek(const eventloop_timer_heap *heap); eventloop_callback *eventloop_timer_heap_pop(eventloop_timer_heap *heap); void eventloop_timer_heap_remove(eventloop_timer_heap *heap, eventloop_callback *cb); diff --git a/tests/027_defer_self_cancel.phpt b/tests/027_defer_self_cancel.phpt new file mode 100644 index 0000000..2112404 --- /dev/null +++ b/tests/027_defer_self_cancel.phpt @@ -0,0 +1,29 @@ +--TEST-- +EventLoop::cancel() inside a running deferred callback is safe +--EXTENSIONS-- +eventloop +--FILE-- + +--EXPECT-- +Running +Cancelled self +Invalid after run diff --git a/tests/028_io_cancel_peer.phpt b/tests/028_io_cancel_peer.phpt new file mode 100644 index 0000000..2f29372 --- /dev/null +++ b/tests/028_io_cancel_peer.phpt @@ -0,0 +1,41 @@ +--TEST-- +I/O watchers can cancel each other while events are being dispatched +--EXTENSIONS-- +eventloop +--FILE-- + +--EXPECT-- +Done diff --git a/tests/029_signal_replace_cancels_previous.phpt b/tests/029_signal_replace_cancels_previous.phpt new file mode 100644 index 0000000..cb88900 --- /dev/null +++ b/tests/029_signal_replace_cancels_previous.phpt @@ -0,0 +1,38 @@ +--TEST-- +EventLoop::onSignal() replaces and cancels an existing signal watcher +--EXTENSIONS-- +eventloop +pcntl +posix +--FILE-- + +--EXPECT-- +first cancelled +second +done diff --git a/tests/030_signal_cancel_restores_previous.phpt b/tests/030_signal_cancel_restores_previous.phpt new file mode 100644 index 0000000..2f0081c --- /dev/null +++ b/tests/030_signal_cancel_restores_previous.phpt @@ -0,0 +1,31 @@ +--TEST-- +EventLoop::cancel() restores the previous signal handler +--EXTENSIONS-- +eventloop +pcntl +posix +--FILE-- + +--EXPECT-- +previous +done diff --git a/tests/031_invalid_timer_values.phpt b/tests/031_invalid_timer_values.phpt new file mode 100644 index 0000000..23b650d --- /dev/null +++ b/tests/031_invalid_timer_values.phpt @@ -0,0 +1,48 @@ +--TEST-- +EventLoop::delay() and repeat() reject non-finite timer values +--EXTENSIONS-- +eventloop +--FILE-- + +--EXPECT-- +delay rejected +delay rejected +repeat rejected +repeat rejected +delay too large +repeat too large +Done