From: Rose Hellsing <rose@pinkro.se> --- dlls/ntdll/unix/sync.c | 165 ++++++++++++++-- server/lpc_port.c | 419 ++++++++++++++++++++++++++++++++++++++++- server/protocol.def | 52 +++++ 3 files changed, 617 insertions(+), 19 deletions(-) diff --git a/dlls/ntdll/unix/sync.c b/dlls/ntdll/unix/sync.c index c478a19db52..3179698202e 100644 --- a/dlls/ntdll/unix/sync.c +++ b/dlls/ntdll/unix/sync.c @@ -3136,10 +3136,89 @@ NTSTATUS WINAPI NtConnectPort( HANDLE *handle, UNICODE_STRING *name, SECURITY_QU LPC_SECTION_WRITE *write, LPC_SECTION_READ *read, ULONG *max_len, void *info, ULONG *info_len ) { - FIXME( "(%p,%s,%p,%p,%p,%p,%p,%p),stub!\n", handle, debugstr_us(name), qos, - write, read, max_len, info, info_len ); - if (info && info_len) TRACE("msg = %s\n", debugstr_an( info, *info_len )); - return STATUS_NOT_IMPLEMENTED; + unsigned int ret; + data_size_t len; + struct object_attributes *objattr; + OBJECT_ATTRIBUTES attr; + ULONG in_len = (info && info_len) ? *info_len : 0; + HANDLE port_handle; + + TRACE( "(%p,%s,%p,%p,%p,%p,%p,%p)\n", handle, debugstr_us(name), qos, write, read, max_len, info, info_len ); + + if (write) + FIXME( "LPC_SECTION_WRITE not supported\n" ); + if (read) + FIXME( "LPC_SECTION_READ not supported\n" ); + + *handle = 0; + attr.Length = sizeof(attr); + attr.RootDirectory = 0; + attr.ObjectName = name; + attr.Attributes = 0; + attr.SecurityDescriptor = NULL; + attr.SecurityQualityOfService = qos; + + if ((ret = alloc_object_attributes( &attr, &objattr, &len ))) + return ret; + + SERVER_START_REQ( connect_lpc_port ) + { + req->access = PORT_ALL_ACCESS; + req->info_size = in_len; + wine_server_add_data( req, objattr, len ); + if (in_len) wine_server_add_data( req, info, in_len ); + if (!(ret = wine_server_call( req ))) + { + port_handle = wine_server_ptr_handle( reply->handle ); + if (info_len) + *info_len = reply->info_size; + } + } + SERVER_END_REQ; + free( objattr ); + + if (ret) return ret; + + /* Wait for the connection to be accepted/rejected by the server */ + for (;;) + { + unsigned int connect_status; + + SERVER_START_REQ( get_lpc_connect_status ) + { + req->handle = wine_server_obj_handle( port_handle ); + ret = wine_server_call( req ); + connect_status = reply->status; + } + SERVER_END_REQ; + + if (ret) + { + NtClose( port_handle ); + return ret; + } + + if (connect_status != STATUS_PENDING) + { + if (connect_status == STATUS_SUCCESS) + { + *handle = port_handle; + return STATUS_SUCCESS; + } + else + { + NtClose( port_handle ); + return connect_status; + } + } + + ret = NtWaitForSingleObject( port_handle, FALSE, NULL ); + if (ret) + { + NtClose( port_handle ); + return ret; + } + } } @@ -3150,9 +3229,11 @@ NTSTATUS WINAPI NtSecureConnectPort( HANDLE *handle, UNICODE_STRING *name, SECUR LPC_SECTION_WRITE *write, PSID sid, LPC_SECTION_READ *read, ULONG *max_len, void *info, ULONG *info_len ) { - FIXME( "(%p,%s,%p,%p,%p,%p,%p,%p,%p),stub!\n", handle, debugstr_us(name), qos, - write, sid, read, max_len, info, info_len ); - return STATUS_NOT_IMPLEMENTED; + TRACE( "(%p,%s,%p,%p,%p,%p,%p,%p,%p)\n", handle, debugstr_us(name), qos, write, sid, read, max_len, info, info_len ); + + if (sid) + FIXME( "SID verification not implemented\n" ); + return NtConnectPort( handle, name, qos, write, read, max_len, info, info_len ); } @@ -3161,8 +3242,37 @@ NTSTATUS WINAPI NtSecureConnectPort( HANDLE *handle, UNICODE_STRING *name, SECUR */ NTSTATUS WINAPI NtListenPort( HANDLE handle, LPC_MESSAGE *msg ) { - FIXME("(%p,%p),stub!\n", handle, msg ); - return STATUS_NOT_IMPLEMENTED; + unsigned int ret; + + TRACE( "(%p,%p)\n", handle, msg ); + + for (;;) + { + SERVER_START_REQ( listen_lpc_port ) + { + req->handle = wine_server_obj_handle( handle ); + wine_server_set_reply( req, msg ? msg->Data : NULL, msg ? 0x1000 : 0 ); + ret = wine_server_call( req ); + if (!ret && msg) + { + msg->DataSize = reply->msg_size; + msg->MessageSize = sizeof(*msg) + reply->msg_size; + msg->MessageType = 10; /* LPC_CONNECTION_REQUEST */ + msg->VirtualRangesOffset = 0; + msg->ClientId.UniqueProcess = ULongToHandle( reply->client_pid ); + msg->ClientId.UniqueThread = ULongToHandle( reply->client_tid ); + msg->MessageId = reply->msg_id; + msg->SectionSize = 0; + } + } + SERVER_END_REQ; + + if (ret != STATUS_PENDING) break; + + ret = NtWaitForSingleObject( handle, FALSE, NULL ); + if (ret) break; + } + return ret; } @@ -3172,8 +3282,28 @@ NTSTATUS WINAPI NtListenPort( HANDLE handle, LPC_MESSAGE *msg ) NTSTATUS WINAPI NtAcceptConnectPort( HANDLE *handle, ULONG id, LPC_MESSAGE *msg, BOOLEAN accept, LPC_SECTION_WRITE *write, LPC_SECTION_READ *read ) { - FIXME("(%p,%u,%p,%d,%p,%p),stub!\n", handle, id, msg, accept, write, read ); - return STATUS_NOT_IMPLEMENTED; + unsigned int ret; + + TRACE( "(%p,%u,%p,%d,%p,%p)\n", handle, id, msg, accept, write, read ); + + if (write) + FIXME( "LPC_SECTION_WRITE not supported\n" ); + if (read) + FIXME( "LPC_SECTION_READ not supported\n" ); + + *handle = 0; + + SERVER_START_REQ( accept_lpc_connect ) + { + req->handle = 0; + req->accept = accept; + req->msg_id = msg ? msg->MessageId : 0; + req->context = id; + if (!(ret = wine_server_call( req )) && accept) + *handle = wine_server_ptr_handle( reply->handle ); + } + SERVER_END_REQ; + return ret; } @@ -3182,8 +3312,17 @@ NTSTATUS WINAPI NtAcceptConnectPort( HANDLE *handle, ULONG id, LPC_MESSAGE *msg, */ NTSTATUS WINAPI NtCompleteConnectPort( HANDLE handle ) { - FIXME( "(%p),stub!\n", handle ); - return STATUS_NOT_IMPLEMENTED; + unsigned int ret; + + TRACE( "(%p)\n", handle ); + + SERVER_START_REQ( complete_lpc_connect ) + { + req->handle = wine_server_obj_handle( handle ); + ret = wine_server_call( req ); + } + SERVER_END_REQ; + return ret; } diff --git a/server/lpc_port.c b/server/lpc_port.c index 5905facb6ba..cc13de09da7 100644 --- a/server/lpc_port.c +++ b/server/lpc_port.c @@ -24,6 +24,7 @@ #include <stdio.h> #include <stdlib.h> #include <stdarg.h> +#include <string.h> #include "ntstatus.h" #define WIN32_NO_STATUS @@ -41,11 +42,26 @@ #define PORT_CONNECT 0x0001 #define PORT_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | PORT_CONNECT) +/* Port types */ +#define PORT_TYPE_SERVER 0x01 /* Named port that server listens on */ +#define PORT_TYPE_CLIENT 0x02 /* Client's end of connection */ +#define PORT_TYPE_CHANNEL 0x03 /* Server's per-client communication channel */ + /* Port flags */ #define PORT_FLAG_WAITABLE 0x0001 +/* LPC message types */ +#define LPC_CONNECTION_REQUEST 10 + /* Maximum message size */ #define MAX_LPC_MESSAGE_SIZE 0x40000 +#define MAX_LPC_DATA_SIZE 0x100000 + +/* Global counter for generating unique message IDs */ +static unsigned int global_msg_id_counter = 0; + +/* Global list of pending connection requests */ +static struct list global_pending_connects = LIST_INIT(global_pending_connects); static const WCHAR lpc_port_name[] = {'L','P','C',' ','P','o','r','t'}; @@ -61,15 +77,39 @@ struct type_descr lpc_port_type = }, }; +/* Internal message structure */ +struct lpc_message +{ + struct list entry; /* queue entry */ + struct list global_entry; /* entry in global_pending_connects */ + struct lpc_port *sender_port; /* port that sent this message */ + struct lpc_port *server_port; /* server port (for connection requests) */ + struct thread *sender_thread; /* thread that sent this message */ + unsigned int msg_id; /* unique message ID */ + unsigned int msg_type; /* LPC message type */ + process_id_t client_pid; /* sender's process ID */ + thread_id_t client_tid; /* sender's thread ID */ + data_size_t data_size; /* size of message data */ + char data[1]; /* variable-length message data */ +}; + /* LPC port object */ struct lpc_port { struct object obj; /* object header */ + unsigned int port_type; /* PORT_TYPE_* */ unsigned int flags; /* PORT_FLAG_* */ + struct lpc_port *connection_port; /* reference to connection port */ + struct lpc_port *connected_port; /* paired port: client <-> channel */ + struct list pending_connects;/* list of pending connection messages */ + struct object *queue_event; /* event signaled when message arrives */ unsigned int max_msg_len; /* maximum message length */ unsigned int max_connect_info;/* maximum connection info length */ - struct process *server_process; /* server process (for named ports) */ struct object *wait_event; /* event for WaitForSingleObject (waitable ports) */ + struct process *server_process; /* server process (for named ports) */ + struct thread *client_thread; /* client thread (for NtCompleteConnectPort) */ + struct object *connect_event; /* event signaled when connection completes */ + unsigned int connect_status; /* STATUS_SUCCESS or error code from accept */ }; static void lpc_port_dump( struct object *obj, int verbose ); @@ -101,6 +141,70 @@ static const struct object_ops lpc_port_ops = lpc_port_destroy /* destroy */ }; +/* Allocate a new message with the given data size */ +static struct lpc_message *alloc_lpc_message( data_size_t data_size ) +{ + struct lpc_message *msg; + data_size_t alloc_size; + + if (data_size > MAX_LPC_DATA_SIZE) + { + set_error( STATUS_INVALID_PARAMETER ); + return NULL; + } + + alloc_size = max( sizeof(struct lpc_message), offsetof(struct lpc_message, data) + data_size ); + msg = mem_alloc( alloc_size ); + if (msg) + { + memset( msg, 0, alloc_size ); + list_init( &msg->global_entry ); + msg->data_size = data_size; + } + return msg; +} + +/* Free a message */ +static void free_lpc_message( struct lpc_message *msg ) +{ + if (msg) + { + if (!list_empty( &msg->global_entry )) + list_remove( &msg->global_entry ); + if (msg->sender_port) release_object( msg->sender_port ); + if (msg->server_port) release_object( msg->server_port ); + if (msg->sender_thread) release_object( msg->sender_thread ); + free( msg ); + } +} + +/* Get the next globally unique message ID */ +static unsigned int get_next_msg_id( void ) +{ + return ++global_msg_id_counter; +} + +/* Signal the port's queue event to wake waiting threads */ +static void signal_port_queue( struct lpc_port *port ) +{ + if (port->queue_event) + signal_sync( port->queue_event ); + if (port->wait_event) + signal_sync( port->wait_event ); +} + +/* Reset the port's queue event after receiving a message */ +static void reset_port_queue( struct lpc_port *port ) +{ + if (list_empty( &port->pending_connects )) + { + if (port->queue_event) + reset_sync( port->queue_event ); + if (port->wait_event) + reset_sync( port->wait_event ); + } +} + static struct lpc_port *create_lpc_port( struct object *root, const struct unicode_str *name, unsigned int attr, unsigned int flags, unsigned int max_msg_len, unsigned int max_connect_info, @@ -117,14 +221,25 @@ static struct lpc_port *create_lpc_port( struct object *root, const struct unico { if (get_error() != STATUS_OBJECT_NAME_EXISTS) { + port->port_type = PORT_TYPE_SERVER; port->flags = flags; + port->connection_port = (struct lpc_port *)grab_object( port ); + port->connected_port = NULL; + list_init( &port->pending_connects ); + port->queue_event = create_internal_sync( 0, 0 ); port->max_msg_len = max_msg_len ? max_msg_len : MAX_LPC_MESSAGE_SIZE; port->max_connect_info = max_connect_info ? max_connect_info : 256; - port->server_process = NULL; port->wait_event = NULL; + port->server_process = (struct process *)grab_object( current->process ); + port->client_thread = NULL; + port->connect_event = NULL; + port->connect_status = STATUS_PENDING; - if (name->len) - port->server_process = (struct process *)grab_object( current->process ); + if (!port->queue_event) + { + release_object( port ); + return NULL; + } if (flags & PORT_FLAG_WAITABLE) { @@ -140,12 +255,69 @@ static struct lpc_port *create_lpc_port( struct object *root, const struct unico return port; } +/* Create a client or communication port (internal, no name) */ +static struct lpc_port *create_port_internal( unsigned int port_type, struct lpc_port *connection_port ) +{ + struct lpc_port *port; + + port = alloc_object( &lpc_port_ops ); + if (!port) return NULL; + + port->port_type = port_type; + port->flags = 0; + port->connection_port = (struct lpc_port *)grab_object( connection_port ); + port->connected_port = NULL; + list_init( &port->pending_connects ); + port->queue_event = create_internal_sync( 0, 0 ); + port->max_msg_len = connection_port->max_msg_len; + port->max_connect_info = connection_port->max_connect_info; + port->wait_event = NULL; + port->server_process = NULL; + port->client_thread = NULL; + port->connect_event = NULL; + port->connect_status = STATUS_PENDING; + + if (port_type == PORT_TYPE_CLIENT) + { + port->connect_event = (struct object *)create_server_internal_sync( 1, 0 ); + if (!port->connect_event) + { + release_object( port ); + return NULL; + } + } + + if (!port->queue_event) + { + release_object( port ); + return NULL; + } + + return port; +} + +/* Find a pending connection message by message ID */ +static struct lpc_message *find_pending_connect_global( unsigned int msg_id ) +{ + struct lpc_message *msg; + + LIST_FOR_EACH_ENTRY( msg, &global_pending_connects, struct lpc_message, global_entry ) + { + if (msg->msg_id == msg_id) + return msg; + } + return NULL; +} + static void lpc_port_dump( struct object *obj, int verbose ) { struct lpc_port *port = (struct lpc_port *)obj; + static const char *type_names[] = { "???", "SERVER", "CLIENT", "CHANNEL" }; + const char *type_name = port->port_type < 4 ? type_names[port->port_type] : "???"; + assert( obj->ops == &lpc_port_ops ); - fprintf( stderr, "LPC Port flags=%04x max_msg=%u max_connect=%u\n", - port->flags, port->max_msg_len, port->max_connect_info ); + fprintf( stderr, "LPC Port type=%s flags=%04x max_msg=%u max_connect=%u\n", + type_name, port->flags, port->max_msg_len, port->max_connect_info ); } static struct object *lpc_port_get_sync( struct object *obj ) @@ -153,19 +325,40 @@ static struct object *lpc_port_get_sync( struct object *obj ) struct lpc_port *port = (struct lpc_port *)obj; assert( obj->ops == &lpc_port_ops ); + if (port->port_type == PORT_TYPE_CLIENT && port->connect_event) + return grab_object( port->connect_event ); + if (port->wait_event) return grab_object( port->wait_event ); + if (port->queue_event) + return grab_object( port->queue_event ); + return NULL; } static void lpc_port_destroy( struct object *obj ) { struct lpc_port *port = (struct lpc_port *)obj; + struct lpc_message *msg, *next_msg; + assert( obj->ops == &lpc_port_ops ); + LIST_FOR_EACH_ENTRY_SAFE( msg, next_msg, &port->pending_connects, struct lpc_message, entry ) + { + list_remove( &msg->entry ); + free_lpc_message( msg ); + } + + if (port->queue_event) release_object( port->queue_event ); if (port->wait_event) release_object( port->wait_event ); + if (port->connect_event) release_object( port->connect_event ); + if (port->connection_port && port->connection_port != port) + release_object( port->connection_port ); + if (port->connected_port && port->connected_port != port) + release_object( port->connected_port ); if (port->server_process) release_object( port->server_process ); + if (port->client_thread) release_object( port->client_thread ); } /* Create an LPC port */ @@ -192,3 +385,217 @@ DECL_HANDLER(create_lpc_port) if (root) release_object( root ); } + +/* Connect to an LPC port */ +DECL_HANDLER(connect_lpc_port) +{ + struct lpc_port *connection_port; + struct lpc_port *client_port; + struct lpc_message *msg; + struct unicode_str name; + struct object *root; + const struct security_descriptor *sd; + const struct object_attributes *objattr = get_req_object_attributes( &sd, &name, &root ); + data_size_t info_size = get_req_data_size(); + + if (!objattr) return; + + connection_port = (struct lpc_port *)open_named_object( root, &lpc_port_ops, &name, objattr->attributes ); + if (root) release_object( root ); + + if (!connection_port) + { + set_error( STATUS_OBJECT_NAME_NOT_FOUND ); + return; + } + + if (connection_port->port_type != PORT_TYPE_SERVER) + { + release_object( connection_port ); + set_error( STATUS_OBJECT_TYPE_MISMATCH ); + return; + } + + client_port = create_port_internal( PORT_TYPE_CLIENT, connection_port ); + if (!client_port) + { + release_object( connection_port ); + return; + } + + msg = alloc_lpc_message( info_size ); + if (!msg) + { + release_object( client_port ); + release_object( connection_port ); + return; + } + + msg->sender_port = (struct lpc_port *)grab_object( client_port ); + msg->server_port = (struct lpc_port *)grab_object( connection_port ); + msg->sender_thread = (struct thread *)grab_object( current ); + msg->msg_id = get_next_msg_id(); + msg->msg_type = LPC_CONNECTION_REQUEST; + msg->client_pid = current->process->id; + msg->client_tid = current->id; + if (info_size) + memcpy( msg->data, get_req_data(), info_size ); + + list_add_tail( &connection_port->pending_connects, &msg->entry ); + list_add_tail( &global_pending_connects, &msg->global_entry ); + signal_port_queue( connection_port ); + + reply->handle = alloc_handle_no_access_check( current->process, client_port, + req->access, objattr->attributes ); + reply->info_size = 0; + + release_object( client_port ); + release_object( connection_port ); +} + +/* Listen for connection requests on a port */ +DECL_HANDLER(listen_lpc_port) +{ + struct lpc_port *port; + struct lpc_message *msg; + + port = (struct lpc_port *)get_handle_obj( current->process, req->handle, + PORT_CONNECT, &lpc_port_ops ); + if (!port) return; + + if (port->port_type != PORT_TYPE_SERVER) + { + set_error( STATUS_INVALID_PORT_HANDLE ); + release_object( port ); + return; + } + + if (list_empty( &port->pending_connects )) + { + set_error( STATUS_PENDING ); + release_object( port ); + return; + } + + msg = LIST_ENTRY( list_head( &port->pending_connects ), struct lpc_message, entry ); + + reply->message = 0; + reply->msg_size = msg->data_size; + reply->client_pid = msg->client_pid; + reply->client_tid = msg->client_tid; + reply->msg_id = msg->msg_id; + + if (msg->data_size) + set_reply_data( msg->data, min( msg->data_size, get_reply_max_size() ) ); + + reset_port_queue( port ); + release_object( port ); +} + +/* Accept or reject a connection */ +DECL_HANDLER(accept_lpc_connect) +{ + struct lpc_port *connection_port; + struct lpc_port *comm_port = NULL; + struct lpc_port *client_port; + struct lpc_message *msg; + + msg = find_pending_connect_global( req->msg_id ); + if (!msg) + { + set_error( STATUS_INVALID_CID ); + return; + } + + connection_port = msg->server_port; + if (!connection_port || connection_port->port_type != PORT_TYPE_SERVER) + { + set_error( STATUS_INVALID_PORT_HANDLE ); + return; + } + + client_port = msg->sender_port; + + /* Remove from port's pending list */ + list_remove( &msg->entry ); + + if (req->accept) + { + comm_port = create_port_internal( PORT_TYPE_CHANNEL, connection_port ); + if (!comm_port) + { + free_lpc_message( msg ); + return; + } + + comm_port->connected_port = (struct lpc_port *)grab_object( client_port ); + client_port->connected_port = (struct lpc_port *)grab_object( comm_port ); + client_port->connect_status = STATUS_SUCCESS; + + if (msg->sender_thread) + comm_port->client_thread = (struct thread *)grab_object( msg->sender_thread ); + + reply->handle = alloc_handle_no_access_check( current->process, comm_port, + PORT_ALL_ACCESS, 0 ); + release_object( comm_port ); + } + else + { + client_port->connect_status = STATUS_PORT_CONNECTION_REFUSED; + if (client_port->connect_event) + signal_sync( client_port->connect_event ); + reply->handle = 0; + } + + free_lpc_message( msg ); +} + +/* Complete the connection (wake the client) */ +DECL_HANDLER(complete_lpc_connect) +{ + struct lpc_port *port; + struct lpc_port *client_port; + + port = (struct lpc_port *)get_handle_obj( current->process, req->handle, + PORT_CONNECT, &lpc_port_ops ); + if (!port) return; + + if (port->port_type != PORT_TYPE_CHANNEL) + { + set_error( STATUS_INVALID_PORT_HANDLE ); + release_object( port ); + return; + } + + client_port = port->connected_port; + if (client_port && client_port->connect_event) + signal_sync( client_port->connect_event ); + + if (port->client_thread) + { + release_object( port->client_thread ); + port->client_thread = NULL; + } + + release_object( port ); +} + +/* Get connection status for client port */ +DECL_HANDLER(get_lpc_connect_status) +{ + struct lpc_port *port; + + port = (struct lpc_port *)get_handle_obj( current->process, req->handle, + PORT_CONNECT, &lpc_port_ops ); + if (!port) return; + + if (port->port_type != PORT_TYPE_CLIENT) + { + set_error( STATUS_INVALID_PORT_HANDLE ); + release_object( port ); + return; + } + + reply->status = port->connect_status; + release_object( port ); +} diff --git a/server/protocol.def b/server/protocol.def index a26e92f7dd3..8116f197fe2 100644 --- a/server/protocol.def +++ b/server/protocol.def @@ -4286,3 +4286,55 @@ enum inproc_sync_type @REPLY obj_handle_t handle; /* handle to the port */ @END + + +/* Connect to an LPC port */ +@REQ(connect_lpc_port) + unsigned int access; /* desired access rights */ + data_size_t info_size; /* size of connection info */ + VARARG(objattr,object_attributes); /* object attributes */ + VARARG(info,bytes); /* connection info data */ +@REPLY + obj_handle_t handle; /* handle to client port */ + data_size_t info_size; /* size of returned info */ + VARARG(info,bytes); /* returned connection info */ +@END + + +/* Listen for connection requests */ +@REQ(listen_lpc_port) + obj_handle_t handle; /* handle to server port */ +@REPLY + client_ptr_t message; /* message pointer (unused) */ + data_size_t msg_size; /* size of connection info */ + process_id_t client_pid; /* client process ID */ + thread_id_t client_tid; /* client thread ID */ + unsigned int msg_id; /* message ID for accept */ + VARARG(info,bytes); /* connection info data */ +@END + + +/* Accept or reject a connection */ +@REQ(accept_lpc_connect) + obj_handle_t handle; /* handle (unused, connection found by msg_id) */ + int accept; /* accept or reject */ + unsigned int msg_id; /* message ID from listen */ + client_ptr_t context; /* port context */ + VARARG(info,bytes); /* connection info to return */ +@REPLY + obj_handle_t handle; /* handle to communication port */ +@END + + +/* Complete the connection */ +@REQ(complete_lpc_connect) + obj_handle_t handle; /* handle to communication port */ +@END + + +/* Get connection status for client port */ +@REQ(get_lpc_connect_status) + obj_handle_t handle; /* handle to client port */ +@REPLY + unsigned int status; /* connection status */ +@END -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10611