From f9b2ecfabbcf80bd79fb812ee33eccfba9540ed8 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 07:53:54 -0400 Subject: [PATCH 1/6] ext/ftp: preserve bare CR bytes in ftp_get() ASCII mode (#22364) In ASCII mode ftp_get() stripped every '\r' and emitted only a following '\n', dropping bare CR bytes not part of a CRLF sequence. Fold CRLF to LF but write a lone '\r' through unchanged, carrying a '\r' on the final byte of a read into the next read and flushing it at EOF, so the buffer boundary behaves the same as the in-buffer case. ftp_nb_get() already does this via the lastch carry in ftp_nb_continue_read(). --- ext/ftp/ftp.c | 31 +++++++++++++++++++---- ext/ftp/tests/ftp_get_ascii_bare_cr.phpt | 32 ++++++++++++++++++++++++ ext/ftp/tests/server.inc | 14 +++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 ext/ftp/tests/ftp_get_ascii_bare_cr.phpt diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c index 1b1af31bb0c9..900f26804b74 100644 --- a/ext/ftp/ftp.c +++ b/ext/ftp/ftp.c @@ -850,6 +850,7 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_ goto bail; } + bool pending_cr = false; while ((rcvd = my_recv(ftp, data->fd, data->buf, FTP_BUFSIZE))) { if (rcvd == (size_t)-1) { goto bail; @@ -869,13 +870,30 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_ php_stream_write(outstream, ptr, (e - ptr)); ptr = e; #else - while (e > ptr && (s = memchr(ptr, '\r', (e - ptr)))) { - php_stream_write(outstream, ptr, (s - ptr)); - if (s + 1 < e && *(s + 1) == '\n') { - s++; + if (pending_cr) { + pending_cr = false; + if (*ptr == '\n') { php_stream_putc(outstream, '\n'); + ptr++; + } else { + php_stream_putc(outstream, '\r'); + } + } + while (e > ptr && (s = memchr(ptr, '\r', (e - ptr)))) { + if (s + 1 < e) { + if (*(s + 1) == '\n') { + php_stream_write(outstream, ptr, (s - ptr)); + php_stream_putc(outstream, '\n'); + ptr = s + 2; + } else { + php_stream_write(outstream, ptr, (s - ptr) + 1); + ptr = s + 1; + } + } else { + php_stream_write(outstream, ptr, (s - ptr)); + pending_cr = true; + ptr = s + 1; } - ptr = s + 1; } #endif if (ptr < e) { @@ -885,6 +903,9 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_ goto bail; } } + if (pending_cr) { + php_stream_putc(outstream, '\r'); + } data_close(ftp); diff --git a/ext/ftp/tests/ftp_get_ascii_bare_cr.phpt b/ext/ftp/tests/ftp_get_ascii_bare_cr.phpt new file mode 100644 index 000000000000..1fe92bf8de7e --- /dev/null +++ b/ext/ftp/tests/ftp_get_ascii_bare_cr.phpt @@ -0,0 +1,32 @@ +--TEST-- +ftp_get() ASCII mode: bare CR is preserved, CRLF folds to LF +--EXTENSIONS-- +ftp +pcntl +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index 04e2ceefa278..251536ff8202 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -398,6 +398,20 @@ if ($pid) { fputs($fs, str_repeat("A", 4095) . "\r\n" . str_repeat("B", 10)); fputs($s, "226 Closing data Connection.\r\n"); break; + case "bare_cr": + // A bare CR (not part of CRLF) mid-stream, plus a bare CR on + // the final byte of the first FTP_BUFSIZE (4096) read followed + // by a non-LF byte in the next read. + fputs($s, "150 File status okay; about to open data connection.\r\n"); + fputs($fs, "line1\r\nba\rre\r\nend" . str_repeat("X", 4078) . "\r" . str_repeat("Y", 10)); + fputs($s, "226 Closing data Connection.\r\n"); + break; + case "trailing_cr": + // The whole transfer ends on a bare CR. + fputs($s, "150 File status okay; about to open data connection.\r\n"); + fputs($fs, "trail\r"); + fputs($s, "226 Closing data Connection.\r\n"); + break; default: fputs($s, "550 {$matches[1]}: No such file or directory \r\n"); From 3704b9d3c9de0dbea28ce602d05181d1f811a429 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 16 Jun 2026 15:36:11 -0400 Subject: [PATCH 2/6] Fix leak of resolved address list in netsnmp_session_init() When php_network_getaddresses() succeeds but no resolved address yields a usable peer name (for example bracketed IPv6 syntax over an IPv4-only host), netsnmp_session_init() returned without php_network_freeaddresses(), leaking the address list on every such call. Free it on that error path, matching the success path in the same function. Closes GH-22340 --- ext/snmp/snmp.c | 1 + ext/snmp/tests/snmp_resolve_fail_leak.phpt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 ext/snmp/tests/snmp_resolve_fail_leak.phpt diff --git a/ext/snmp/snmp.c b/ext/snmp/snmp.c index 22eb6525f8e0..afbdda285ec4 100644 --- a/ext/snmp/snmp.c +++ b/ext/snmp/snmp.c @@ -910,6 +910,7 @@ static bool netsnmp_session_init(php_snmp_session **session_p, int version, zend if (strlen(session->peername) == 0) { php_error_docref(NULL, E_WARNING, "Unknown failure while resolving '%s'", ZSTR_VAL(hostname)); + php_network_freeaddresses(psal); return false; } /* XXX FIXME diff --git a/ext/snmp/tests/snmp_resolve_fail_leak.phpt b/ext/snmp/tests/snmp_resolve_fail_leak.phpt new file mode 100644 index 000000000000..c8793b09e928 --- /dev/null +++ b/ext/snmp/tests/snmp_resolve_fail_leak.phpt @@ -0,0 +1,16 @@ +--TEST-- +snmp: resolved address list is freed when peername resolution drains empty +--EXTENSIONS-- +snmp +--FILE-- + +--EXPECTF-- +Warning: snmpget(): Unknown failure while resolving '[127.0.0.1]' in %s on line %d +bool(false) From e463914fb2f273ecc49f7ac0a7b089c9bcd835fa Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 16 Jun 2026 14:38:31 -0400 Subject: [PATCH 3/6] Fix socket_sendmsg() sending wrong fd for Socket objects in SCM_RIGHTS The Socket-object branch of from_zval_write_fd_array_aux() indexed the fd array with the 1-based loop counter (iarr[i]) instead of iarr[i - 1] like the resource branch, leaving slot 0 zeroed so a single Socket delivered fd 0 (stdin) to the receiver. Broken since the 8.0 resource-to-object conversion. Closes GH-22338 --- ext/sockets/conversions.c | 2 +- ext/sockets/tests/socket_cmsg_rights.phpt | 2 +- .../socket_sendmsg_scm_rights_object.phpt | 77 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 ext/sockets/tests/socket_sendmsg_scm_rights_object.phpt diff --git a/ext/sockets/conversions.c b/ext/sockets/conversions.c index a419a20c873c..eb4e58b3988e 100644 --- a/ext/sockets/conversions.c +++ b/ext/sockets/conversions.c @@ -1385,7 +1385,7 @@ static void from_zval_write_fd_array_aux(zval *elem, unsigned i, void **args, se return; } - iarr[i] = sock->bsd_socket; + iarr[i - 1] = sock->bsd_socket; return; } diff --git a/ext/sockets/tests/socket_cmsg_rights.phpt b/ext/sockets/tests/socket_cmsg_rights.phpt index 1794eaf767ac..8bfbcd57157f 100644 --- a/ext/sockets/tests/socket_cmsg_rights.phpt +++ b/ext/sockets/tests/socket_cmsg_rights.phpt @@ -58,7 +58,7 @@ if ($data["control"]) { if ($control["level"] == SOL_SOCKET && $control["type"] == SCM_RIGHTS) { foreach ($control["data"] as $resource) { - if (!is_resource($resource)) { + if (!is_resource($resource) && !($resource instanceof Socket)) { echo "FAIL RES\n"; var_dump($data); exit; diff --git a/ext/sockets/tests/socket_sendmsg_scm_rights_object.phpt b/ext/sockets/tests/socket_sendmsg_scm_rights_object.phpt new file mode 100644 index 000000000000..907ea70ac169 --- /dev/null +++ b/ext/sockets/tests/socket_sendmsg_scm_rights_object.phpt @@ -0,0 +1,77 @@ +--TEST-- +socket_sendmsg(): SCM_RIGHTS transfers Socket objects with the correct fds +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + ['x'], + 'control' => [[ + 'level' => SOL_SOCKET, + 'type' => SCM_RIGHTS, + 'data' => [$payload1, $payload2], + ]], +], 0); + +$data = [ + 'name' => [], + 'buffer_size' => 64, + 'controllen' => socket_cmsg_space(SOL_SOCKET, SCM_RIGHTS, 2), +]; +socket_recvmsg($recv, $data, 0); + +$got = $data['control'][0]['data']; +var_dump(count($got)); +var_dump($got[0] instanceof Socket); +var_dump($got[1] instanceof Socket); +socket_getsockname($got[0], $addr0); +socket_getsockname($got[1], $addr1); +var_dump($addr0 === $ppath1); +var_dump($addr1 === $ppath2); +?> +--CLEAN-- + +--EXPECT-- +int(2) +bool(true) +bool(true) +bool(true) +bool(true) From 0a8133032b4b7d599759d9e5e229ac045eb67834 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Sat, 20 Jun 2026 15:08:12 +0200 Subject: [PATCH 4/6] Use zend_enum_fetch_case_id() for poll API (GH-22367) --- ext/standard/io_poll.c | 40 ++-------------------------------- ext/standard/io_poll.stub.php | 7 +++++- ext/standard/io_poll_arginfo.h | 3 ++- ext/standard/io_poll_decl.h | 26 ++++++++++++++++++++++ main/php_poll.h | 4 ++-- 5 files changed, 38 insertions(+), 42 deletions(-) create mode 100644 ext/standard/io_poll_decl.h diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index e8366c7e8802..c048d43ccb4b 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -85,26 +85,7 @@ static inline void php_io_poll_throw_failed_operation( /* Event enum to bit mask mapping */ static uint32_t php_io_poll_event_enum_to_bit(zend_object *event_enum) { - zval *case_name = zend_enum_fetch_case_name(event_enum); - const char *name = Z_STRVAL_P(case_name); - - if (strcmp(name, "Read") == 0) { - return PHP_POLL_READ; - } else if (strcmp(name, "Write") == 0) { - return PHP_POLL_WRITE; - } else if (strcmp(name, "Error") == 0) { - return PHP_POLL_ERROR; - } else if (strcmp(name, "HangUp") == 0) { - return PHP_POLL_HUP; - } else if (strcmp(name, "ReadHangUp") == 0) { - return PHP_POLL_RDHUP; - } else if (strcmp(name, "OneShot") == 0) { - return PHP_POLL_ONESHOT; - } else if (strcmp(name, "EdgeTriggered") == 0) { - return PHP_POLL_ET; - } - - return 0; + return 1 << (zend_enum_fetch_case_id(event_enum) - 1); } static uint32_t php_io_poll_event_enums_to_events(zval *event_enums) @@ -179,24 +160,7 @@ static zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *even /* Backend enum name to backend type mapping */ static php_poll_backend_type php_io_poll_backend_enum_to_type(zend_object *backend_enum) { - zval *case_name = zend_enum_fetch_case_name(backend_enum); - const char *name = Z_STRVAL_P(case_name); - - if (strcmp(name, "Auto") == 0) { - return PHP_POLL_BACKEND_AUTO; - } else if (strcmp(name, "Poll") == 0) { - return PHP_POLL_BACKEND_POLL; - } else if (strcmp(name, "Epoll") == 0) { - return PHP_POLL_BACKEND_EPOLL; - } else if (strcmp(name, "Kqueue") == 0) { - return PHP_POLL_BACKEND_KQUEUE; - } else if (strcmp(name, "EventPorts") == 0) { - return PHP_POLL_BACKEND_EVENTPORT; - } else if (strcmp(name, "WSAPoll") == 0) { - return PHP_POLL_BACKEND_WSAPOLL; - } - - return PHP_POLL_BACKEND_AUTO; + return zend_enum_fetch_case_id(backend_enum) - 2; } static const char *php_io_poll_backend_type_to_name(php_poll_backend_type type) diff --git a/ext/standard/io_poll.stub.php b/ext/standard/io_poll.stub.php index 82bc00e0aaca..83c1ba5cbe2e 100644 --- a/ext/standard/io_poll.stub.php +++ b/ext/standard/io_poll.stub.php @@ -1,6 +1,9 @@ Date: Sat, 20 Jun 2026 15:09:06 +0200 Subject: [PATCH 5/6] [skip ci] Name unnamed enums in poll API --- main/php_poll.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/php_poll.h b/main/php_poll.h index 708d957b89ef..9b878d44bd20 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -38,7 +38,7 @@ #define PHP_POLL_FLAG_RAW_EVENTS 0x02 /* Poll backend types. Keep in sync with io_poll.stub.php! */ -typedef enum { +typedef enum php_poll_backend_type { PHP_POLL_BACKEND_AUTO = -1, PHP_POLL_BACKEND_POLL = 0, PHP_POLL_BACKEND_EPOLL, @@ -62,7 +62,7 @@ typedef enum { #define PHP_POLL_ERROR_CODE_NOSUPPORT 11 /* Error codes */ -typedef enum { +typedef enum php_poll_error { PHP_POLL_ERR_NONE = PHP_POLL_ERROR_CODE_NONE, /* No error */ PHP_POLL_ERR_SYSTEM = PHP_POLL_ERROR_CODE_SYSTEM, /* Generic system error */ PHP_POLL_ERR_NOMEM = PHP_POLL_ERROR_CODE_NOMEM, /* Out of memory (ENOMEM) */ From a3ef478bd1ab51aa50fbf89526a7f135ece876a3 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Sat, 20 Jun 2026 15:15:34 +0200 Subject: [PATCH 6/6] Move decls into ZEND_HASH_FOREACH macros in poll API --- ext/standard/io_poll.c | 9 +++------ main/poll/poll_backend_kqueue.c | 4 +--- main/poll/poll_fd_table.c | 9 ++------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index c048d43ccb4b..b1eb7513f1cb 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -91,7 +91,6 @@ static uint32_t php_io_poll_event_enum_to_bit(zend_object *event_enum) static uint32_t php_io_poll_event_enums_to_events(zval *event_enums) { HashTable *ht; - zval *entry; uint32_t events = 0; if (Z_TYPE_P(event_enums) != IS_ARRAY) { @@ -100,7 +99,7 @@ static uint32_t php_io_poll_event_enums_to_events(zval *event_enums) ht = Z_ARRVAL_P(event_enums); - ZEND_HASH_FOREACH_VAL(ht, entry) { + ZEND_HASH_FOREACH_VAL(ht, zval *entry) { if (Z_TYPE_P(entry) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(entry), php_io_poll_event_class_entry)) { return 0; @@ -297,8 +296,7 @@ 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) { + ZEND_HASH_FOREACH_VAL(intern->watchers, zval *zv) { php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(zv)); watcher->active = false; watcher->poll_ctx = NULL; @@ -337,8 +335,7 @@ static HashTable *php_io_poll_context_get_gc(zend_object *obj, zval **table, int zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); if (intern->watchers) { - zval *zv; - ZEND_HASH_FOREACH_VAL(intern->watchers, zv) { + ZEND_HASH_FOREACH_VAL(intern->watchers, zval *zv) { zend_get_gc_buffer_add_zval(gc_buffer, zv); } ZEND_HASH_FOREACH_END(); } diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 3d8e1feb4734..59b5efa9cacb 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -420,10 +420,8 @@ static int kqueue_backend_wait( if (garbage_events > 0) { /* Clean up orphaned filters from complete oneshot FDs */ - zend_ulong fd_key; - zval *tracking; struct kevent cleanup_change; - ZEND_HASH_FOREACH_NUM_KEY_VAL(backend_data->fd_tracking, fd_key, tracking) + ZEND_HASH_FOREACH_NUM_KEY_VAL(backend_data->fd_tracking, zend_ulong fd_key, zval *tracking) { zend_long flags = Z_LVAL_P(tracking); if (flags & KQUEUE_FD_HAS_GARBAGE) { diff --git a/main/poll/poll_fd_table.c b/main/poll/poll_fd_table.c index 97231072322c..1c76da79b7eb 100644 --- a/main/poll/poll_fd_table.c +++ b/main/poll/poll_fd_table.c @@ -34,9 +34,7 @@ php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent) void php_poll_fd_table_cleanup(php_poll_fd_table *table) { if (table) { - zval *zv; - - ZEND_HASH_FOREACH_VAL(&table->entries_ht, zv) + ZEND_HASH_FOREACH_VAL(&table->entries_ht, zval *zv) { php_poll_fd_entry *entry = Z_PTR_P(zv); pefree(entry, table->persistent); @@ -105,10 +103,7 @@ typedef bool (*php_poll_fd_iterator_func_t)(int fd, php_poll_fd_entry *entry, vo void php_poll_fd_table_foreach( php_poll_fd_table *table, php_poll_fd_iterator_func_t callback, void *user_data) { - zend_ulong fd; - zval *zv; - - ZEND_HASH_FOREACH_NUM_KEY_VAL(&table->entries_ht, fd, zv) + ZEND_HASH_FOREACH_NUM_KEY_VAL(&table->entries_ht, zend_ulong fd, zval *zv) { php_poll_fd_entry *entry = Z_PTR_P(zv); if (entry->active && !callback((int) fd, entry, user_data)) {