Skip to content
Closed
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
130 changes: 125 additions & 5 deletions ext/standard/proc_open.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "zend_smart_str.h"
#ifdef PHP_WIN32
# include "win32/sockets.h"
# include "ext/random/php_random_csprng.h"
#endif

#ifdef HAVE_SYS_WAIT_H
Expand Down Expand Up @@ -913,18 +914,122 @@ static php_file_descriptor_t make_descriptor_cloexec(php_file_descriptor_t fd)
#endif
}

#ifdef PHP_WIN32
/* Makes a child's pipe as libuv's uv_spawn() does: the parent's end is an overlapped named pipe,
* which an I/O completion port can complete, and the child's end is synchronous and inheritable.
* One instance and a name that cannot be guessed, so no other client can take the child's place.
* pair is filled as pipe() fills it: the read end, then the write end. */
static zend_result create_overlapped_pipe(HANDLE pair[2], bool parent_reads)
{
const DWORD parent_access = (parent_reads ? PIPE_ACCESS_INBOUND : PIPE_ACCESS_OUTBOUND)
| FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE | WRITE_DAC;
const DWORD child_access = (parent_reads ? GENERIC_WRITE | FILE_READ_ATTRIBUTES : GENERIC_READ | FILE_WRITE_ATTRIBUTES)
| WRITE_DAC;
wchar_t name[64];
HANDLE server = INVALID_HANDLE_VALUE;
DWORD err = ERROR_SUCCESS;

for (int attempt = 0; attempt < 16; attempt++) {
uint64_t name_bits;
if (php_random_bytes_silent(&name_bits, sizeof(name_bits)) == FAILURE) {
errno = EIO;
return FAILURE;
}

_snwprintf_s(name, sizeof(name) / sizeof(name[0]), _TRUNCATE, L"\\\\.\\pipe\\php-%lu-%016llx",
GetCurrentProcessId(), (unsigned long long) name_bits);
server = CreateNamedPipeW(name, parent_access,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, 1,
PHP_IO_PIPE_BUFFER_SIZE, PHP_IO_PIPE_BUFFER_SIZE, 0, NULL);
if (server != INVALID_HANDLE_VALUE) {
break;
}

err = GetLastError();
/* A taken name: ERROR_ACCESS_DENIED under FILE_FLAG_FIRST_PIPE_INSTANCE, else ERROR_PIPE_BUSY */
if (err != ERROR_ACCESS_DENIED && err != ERROR_PIPE_BUSY) {
break;
}
}

if (server == INVALID_HANDLE_VALUE) {
SET_ERRNO_FROM_WIN32_CODE(err);
return FAILURE;
}

const HANDLE client = CreateFileW(name, child_access, 0, &php_proc_open_security, OPEN_EXISTING, 0, NULL);
if (client == INVALID_HANDLE_VALUE) {
SET_ERRNO_FROM_WIN32_CODE(GetLastError());
CloseHandle(server);
return FAILURE;
}

/* Both ends exist, so the connect completes at once; the wait keeps a late completion off the
* stack */
OVERLAPPED ov;
memset(&ov, 0, sizeof(ov));
BOOL connected = ConnectNamedPipe(server, &ov);
if (!connected) {
const DWORD connect_err = GetLastError();
DWORD unused;
connected = connect_err == ERROR_PIPE_CONNECTED
|| (connect_err == ERROR_IO_PENDING && GetOverlappedResult(server, &ov, &unused, TRUE));
}

if (!connected) {
SET_ERRNO_FROM_WIN32_CODE(GetLastError());
CloseHandle(client);
CloseHandle(server);
return FAILURE;
}

pair[0] = parent_reads ? server : client;
pair[1] = parent_reads ? client : server;
return SUCCESS;
}

/* PHP_STREAM_OPTION_OVERLAPPED_PIPE on every stream of the spec, up to the first that fails (only
* RELEASE can); one closed meanwhile is skipped */
static zend_result set_passed_streams_pipe_option(const HashTable *descriptorspec, int value)
{
zval *descitem;

ZEND_HASH_FOREACH_VAL(descriptorspec, descitem) {
ZVAL_DEREF(descitem);
if (Z_TYPE_P(descitem) != IS_RESOURCE) {
continue;
}

php_stream *const passed = (php_stream *) zend_fetch_resource(Z_RES_P(descitem), NULL, php_file_le_stream());
if (passed && php_stream_set_option(passed, PHP_STREAM_OPTION_OVERLAPPED_PIPE, value, NULL)
== PHP_STREAM_OPTION_RETURN_ERR) {
return FAILURE;
}
} ZEND_HASH_FOREACH_END();

return SUCCESS;
}
#endif

static zend_result set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *zmode)
{
php_file_descriptor_t newpipe[2];
const bool parent_reads = zend_string_starts_with_literal(zmode, "w");

if (pipe(newpipe)) {
#ifdef PHP_WIN32
const bool failed = php_io_overlapped_pipes ? create_overlapped_pipe(newpipe, parent_reads) == FAILURE
: pipe(newpipe) != 0;
#else
const bool failed = pipe(newpipe) != 0;
#endif
if (failed) {
php_error_docref(NULL, E_WARNING, "Unable to create pipe %s", strerror(errno));
return FAILURE;
}

desc->type = DESCRIPTOR_TYPE_PIPE;

if (!zend_string_starts_with_literal(zmode, "w")) {
if (!parent_reads) {
desc->parentend = newpipe[1];
desc->childend = newpipe[0];
desc->mode_flags = O_WRONLY;
Expand All @@ -937,7 +1042,8 @@ static zend_result set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_s
desc->parentend = make_descriptor_cloexec(desc->parentend);

#ifdef PHP_WIN32
if (ZSTR_LEN(zmode) >= 2 && ZSTR_VAL(zmode)[1] == 'b')
/* An overlapped pipe's reads and writes bypass the CRT's text mode */
if ((ZSTR_LEN(zmode) >= 2 && ZSTR_VAL(zmode)[1] == 'b') || php_io_overlapped_pipes)
desc->mode_flags |= O_BINARY;
#endif

Expand Down Expand Up @@ -1359,6 +1465,12 @@ PHP_FUNCTION(proc_open)
goto exit_fail;
}

/* Not in the descriptor loop above, which runs PHP code: another coroutine's read there would
* tie the pipe to a completion port again */
if (set_passed_streams_pipe_option(descriptorspec, PHP_STREAM_OVERLAPPED_PIPE_RELEASE) == FAILURE) {
goto exit_fail;
}

init_startup_info(&si, descriptors, ndesc);
init_process_info(&pi);

Expand Down Expand Up @@ -1415,6 +1527,12 @@ PHP_FUNCTION(proc_open)
childHandle = pi.hProcess;
child = pi.dwProcessId;
CloseHandle(pi.hThread);

/* The child holds the descriptors of the streams passed to it: the parent's own calls on an
* overlapped pipe among them stop using the provider for good, since a child may bind the pipe
* to its own port with FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, as libuv does, and a Ring op that
* completes at once would then never post. */
set_passed_streams_pipe_option(descriptorspec, PHP_STREAM_OVERLAPPED_PIPE_HAND_OUT);
#elif defined(USE_POSIX_SPAWN)
posix_spawn_file_actions_t factions;
int r;
Expand Down Expand Up @@ -1554,8 +1672,10 @@ PHP_FUNCTION(proc_open)
}

#ifdef PHP_WIN32
stream = php_stream_fopen_from_fd(_open_osfhandle((intptr_t)descriptors[i].parentend,
descriptors[i].mode_flags), mode_string, NULL);
const int parent_fd = _open_osfhandle((intptr_t)descriptors[i].parentend, descriptors[i].mode_flags);
stream = php_io_overlapped_pipes
? php_stream_fopen_from_overlapped_pipe(parent_fd, mode_string)
: php_stream_fopen_from_fd(parent_fd, mode_string, NULL);
php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, blocking_pipes, NULL);
#else
stream = php_stream_fopen_from_fd(descriptors[i].parentend, mode_string, NULL);
Expand Down
136 changes: 125 additions & 11 deletions ext/standard/streamsfuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -691,9 +691,46 @@ typedef struct php_select_member {
php_stream *stream;
php_socket_t fd;
uint32_t events;
#ifdef PHP_WIN32
bool is_overlapped_pipe; /* PHP_IO_OP_F_PIPE: readable when it holds bytes, no socket poll takes it */
bool in_except; /* in the except set: a backend without priority events drops it from events */
#endif
} php_select_member;

static void stream_array_collect_members(HashTable *stream_array, uint32_t events,
#ifdef PHP_WIN32
/* stream_select_any(): a member no provider can wait for, so php_select() takes the call */
# define STREAM_SELECT_USE_PHP_SELECT (-2)

/* A pipe has no write readiness: always writable, and in the except set as php_select() answers */
static void stream_select_report_pipe(const php_select_member *m, fd_set *rfds, fd_set *wfds, fd_set *efds,
int *found)
{
if ((m->events & PHP_POLL_READ) && php_io_pipe_readable((int) m->fd)) {
PHP_SAFE_FD_SET(m->fd, rfds);
(*found)++;
}

if (m->events & PHP_POLL_WRITE) {
PHP_SAFE_FD_SET(m->fd, wfds);
(*found)++;
}

if (m->in_except) {
PHP_SAFE_FD_SET(m->fd, efds);
(*found)++;
}
}

static bool stream_select_is_socket(php_socket_t fd)
{
int type;
int len = sizeof(type);
return getsockopt((SOCKET) fd, SOL_SOCKET, SO_TYPE, (char *) &type, &len) == 0
|| WSAGetLastError() != WSAENOTSOCK;
}
#endif

static void stream_array_collect_members(HashTable *stream_array, uint32_t events, bool except,
php_select_member **members, uint32_t *n, uint32_t *cap)
{
zval *elem;
Expand All @@ -713,6 +750,9 @@ static void stream_array_collect_members(HashTable *stream_array, uint32_t event
for (uint32_t i = 0; i < *n; i++) {
if ((*members)[i].fd == this_fd) {
(*members)[i].events |= events;
#ifdef PHP_WIN32
(*members)[i].in_except |= except;
#endif
merged = true;
break;
}
Expand All @@ -725,6 +765,9 @@ static void stream_array_collect_members(HashTable *stream_array, uint32_t event
(*members)[*n].stream = stream;
(*members)[*n].fd = this_fd;
(*members)[*n].events = events;
#ifdef PHP_WIN32
(*members)[*n].in_except = except;
#endif
(*n)++;
}
} ZEND_HASH_FOREACH_END();
Expand Down Expand Up @@ -799,14 +842,34 @@ static int stream_select_any(zval *r_array, zval *w_array, zval *e_array, struct
}

if (r_array) {
stream_array_collect_members(Z_ARRVAL_P(r_array), PHP_POLL_READ, &members, &n, &cap);
stream_array_collect_members(Z_ARRVAL_P(r_array), PHP_POLL_READ, false, &members, &n, &cap);
}
if (w_array) {
stream_array_collect_members(Z_ARRVAL_P(w_array), PHP_POLL_WRITE, &members, &n, &cap);
stream_array_collect_members(Z_ARRVAL_P(w_array), PHP_POLL_WRITE, false, &members, &n, &cap);
}
if (e_array) {
stream_array_collect_members(Z_ARRVAL_P(e_array), priority ? PHP_POLL_PRI : 0, &members, &n, &cap);
stream_array_collect_members(Z_ARRVAL_P(e_array), priority ? PHP_POLL_PRI : 0, true, &members, &n, &cap);
}

#ifdef PHP_WIN32
for (uint32_t i = 0; i < n; i++) {
bool handed_out = false;
const bool is_overlapped_pipe = php_stream_set_option(members[i].stream,
PHP_STREAM_OPTION_OVERLAPPED_PIPE, PHP_STREAM_OVERLAPPED_PIPE_QUERY, &handed_out)
== PHP_STREAM_OPTION_RETURN_OK;
/* php_select() takes the call when another process holds the pipe, or when no socket poll
* takes the descriptor (an anonymous pipe, a file, a console): WSAPoll() fails as a whole on
* one */
if (handed_out || (php_io_overlapped_pipes && !is_overlapped_pipe
&& !stream_select_is_socket(members[i].fd))) {
efree(members);
stream_select_unfreeze(frozen, n_frozen);
return STREAM_SELECT_USE_PHP_SELECT;
}

members[i].is_overlapped_pipe = is_overlapped_pipe;
}
#endif

/* Syscall first, as for a read: a zero-timeout poll() over the sets answers a select
* with a stream ready now, or one with a zero timeout, without an op, and is the arm-time
Expand All @@ -817,18 +880,34 @@ static int stream_select_any(zval *r_array, zval *w_array, zval *e_array, struct
if (n > 0) {
php_pollfd stack[16];
php_pollfd *fds = n > sizeof(stack) / sizeof(stack[0]) ? safe_emalloc(n, sizeof(*fds), 0) : stack;
uint32_t n_fds = 0;
int found = 0;
for (uint32_t i = 0; i < n; i++) {
fds[i].fd = members[i].fd;
fds[i].events = ((members[i].events & PHP_POLL_READ) ? POLLIN : 0)
#ifdef PHP_WIN32
if (members[i].is_overlapped_pipe) {
continue;
}
#endif

fds[n_fds].fd = members[i].fd;
fds[n_fds].events = ((members[i].events & PHP_POLL_READ) ? POLLIN : 0)
| ((members[i].events & PHP_POLL_WRITE) ? POLLOUT : 0)
| ((members[i].events & PHP_POLL_PRI) ? POLLPRI : 0);
fds[i].revents = 0;
fds[n_fds].revents = 0;
n_fds++;
}
int ready = php_poll2(fds, n, 0);
int found = 0;
const int ready = n_fds > 0 ? php_poll2(fds, n_fds, 0) : 0;
if (ready >= 0) {
for (uint32_t i = 0; i < n; i++) {
short revents = ready > 0 ? fds[i].revents : 0;
for (uint32_t i = 0, polled = 0; i < n; i++) {
#ifdef PHP_WIN32
if (members[i].is_overlapped_pipe) {
stream_select_report_pipe(&members[i], rfds, wfds, efds, &found);
continue;
}
#endif

const short revents = ready > 0 ? fds[polled].revents : 0;
polled++;
if (revents & POLLNVAL) {
/* The Any reports the descriptor's failure */
found = 0;
Expand Down Expand Up @@ -879,6 +958,11 @@ static int stream_select_any(zval *r_array, zval *w_array, zval *e_array, struct
if (checked) {
ops[n_members].flags |= PHP_IO_OP_F_CHECKED;
}
#ifdef PHP_WIN32
if (members[i].is_overlapped_pipe) {
ops[n_members].flags |= PHP_IO_OP_F_PIPE;
}
#endif
op_ptrs[n_members] = &ops[n_members];
n_members++;
}
Expand Down Expand Up @@ -906,6 +990,22 @@ static int stream_select_any(zval *r_array, zval *w_array, zval *e_array, struct
errno = EINTR;
ret = -1;
} else {
#ifdef PHP_WIN32
for (uint32_t i = 0; i < any.u.any.n_results; i++) {
if (results[i].status == PHP_IO_UNSUPPORTED && (ops[results[i].index].flags & PHP_IO_OP_F_PIPE)) {
/* No provider waits for this pipe: the thread does, for the time left */
if (tv) {
const zend_hrtime_t left_ns = php_io_deadline_remaining(&ops[timer_index].deadline,
zend_hrtime());
tv->tv_sec = (long) (left_ns / ZEND_NANO_IN_SEC);
tv->tv_usec = (long) (left_ns % ZEND_NANO_IN_SEC / 1000);
}

ret = STREAM_SELECT_USE_PHP_SELECT;
goto done;
}
}
#endif
for (uint32_t i = 0; i < any.u.any.n_results; i++) {
uint32_t index = results[i].index;
if (index == timer_index || (results[i].status != PHP_IO_DONE && results[i].status != PHP_IO_READY)) {
Expand All @@ -930,6 +1030,9 @@ static int stream_select_any(zval *r_array, zval *w_array, zval *e_array, struct
}
}

#ifdef PHP_WIN32
done:
#endif
efree(results);
efree(op_ptrs);
efree(ops);
Expand Down Expand Up @@ -1141,11 +1244,22 @@ PHP_FUNCTION(stream_select)
}

if (php_io_hooks_active()) {
#ifdef PHP_WIN32
const fd_set all_rfds = rfds, all_wfds = wfds, all_efds = efds;
#endif
/* The provider waits: the sets are rebuilt from what it reported */
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_ZERO(&efds);
retval = stream_select_any(r_array, w_array, e_array, tv_p, &rfds, &wfds, &efds);
#ifdef PHP_WIN32
if (retval == STREAM_SELECT_USE_PHP_SELECT) {
rfds = all_rfds;
wfds = all_wfds;
efds = all_efds;
retval = php_select(max_fd+1, &rfds, &wfds, &efds, tv_p);
}
#endif
} else {
retval = php_select(max_fd+1, &rfds, &wfds, &efds, tv_p);
}
Expand Down
Loading
Loading