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
3 changes: 3 additions & 0 deletions sapi/asynctest/Makefile.frag
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ sapi/asynctest/tests/test-timer_overflow: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(

sapi/asynctest/tests/test-listen_pause: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_ASYNCTEST_LISTEN_PAUSE_OBJS)
$(ASYNCTEST_BUILD) $(PHP_ASYNCTEST_LISTEN_PAUSE_OBJS) -o $@

sapi/asynctest/tests/test-write_resubmit: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_ASYNCTEST_WRITE_RESUBMIT_OBJS)
$(ASYNCTEST_BUILD) $(PHP_ASYNCTEST_WRITE_RESUBMIT_OBJS) -o $@
1 change: 1 addition & 0 deletions sapi/asynctest/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ if test "$PHP_ASYNCTEST" != "no"; then
PHP_ASYNCTEST_TARGET([listen_uaf], [PHP_ASYNCTEST_LISTEN_UAF_OBJS])
PHP_ASYNCTEST_TARGET([timer_overflow], [PHP_ASYNCTEST_TIMER_OVERFLOW_OBJS])
PHP_ASYNCTEST_TARGET([listen_pause], [PHP_ASYNCTEST_LISTEN_PAUSE_OBJS])
PHP_ASYNCTEST_TARGET([write_resubmit], [PHP_ASYNCTEST_WRITE_RESUBMIT_OBJS])

PHP_SUBST([PHP_ASYNCTEST_COMMON_OBJS])
PHP_SUBST([PHP_ASYNCTEST_BINARIES])
Expand Down
121 changes: 121 additions & 0 deletions sapi/asynctest/tests/test_write_resubmit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* test_write_resubmit.c — a write listener may dispose the finished request and
* submit the next write from inside the completion notify.
*
* A log sink does exactly that: it frees the request it was notified about and
* kicks the next write. The next request is allocated while the first one's
* completion is still on the stack, and the allocator hands back the address
* just freed, so the completion must not touch its request after the notify.
*/

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>

#include <sys/socket.h>
#include <unistd.h>

#include <main/php.h>
#include <Zend/zend_async_API.h>

#include "../asynctest_sapi.h"
#include "../reactor_harness.h"

/* Loop iterations that let both writes complete. */
#define SETTLE_TICKS 20

static const char first[] = "first write\n";
static const char second[] = "second write\n";

static zend_async_io_t *io;
static zend_async_io_req_t *active;
static zend_async_io_req_t *first_req;
static int completions;
static bool reused_address;

static void on_write_done(zend_async_event_t *event, zend_async_event_callback_t *callback,
void *result, zend_object *exception)
{
(void)event;
(void)callback;
(void)exception;

if (result == NULL || result != active) {
return;
}

zend_async_io_req_t *const req = active;
assert_null(req->exception);
completions++;
active = NULL;
req->dispose(req);

if (completions == 1) {
active = ZEND_ASYNC_IO_WRITE(io, second, sizeof(second) - 1);
assert_non_null(active);
reused_address = active == first_req;
}
}

static void test_dispose_and_resubmit_inside_the_notify(void **state)
{
(void)state;
assert_int_equal(asynctest_request_startup(), SUCCESS);

int fds[2];
assert_int_equal(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0);

io = ZEND_ASYNC_IO_CREATE((zend_file_descriptor_t) fds[0], ZEND_ASYNC_IO_TYPE_PIPE,
ZEND_ASYNC_IO_WRITABLE);
assert_non_null(io);

zend_async_event_callback_t *const cb = ZEND_ASYNC_EVENT_CALLBACK(on_write_done);
io->event.add_callback(&io->event, cb);

completions = 0;
reused_address = false;
first_req = active = ZEND_ASYNC_IO_WRITE(io, first, sizeof(first) - 1);
assert_non_null(active);

reactor_harness_tick(SETTLE_TICKS);

/* Without the reuse the defect has nothing to corrupt; the test then proves
* nothing either way. */
if (!reused_address) {
skip();
}

assert_int_equal(completions, 2);
assert_null(active);

char got[64] = {0};
const ssize_t n = read(fds[1], got, sizeof(got) - 1);
assert_int_equal(n, (ssize_t) (sizeof(first) - 1 + sizeof(second) - 1));
assert_string_equal(got, "first write\nsecond write\n");

io->event.del_callback(&io->event, cb);
ZEND_ASYNC_IO_CLOSE(io);
io->event.dispose(&io->event);
reactor_harness_tick(SETTLE_TICKS);

close(fds[1]);
asynctest_request_shutdown();
}

int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_dispose_and_resubmit_inside_the_notify),
};

if (asynctest_sapi_startup() != SUCCESS) {
fprintf(stderr, "Failed to start PHP runtime\n");
return 2;
}

const int rc = cmocka_run_group_tests(tests, NULL, NULL);

asynctest_sapi_shutdown();
return rc;
}
Loading