Before you start#
Review tasks, fork, exit and wait, Unix sockets and IPC, Wine prefixes, and the ntdll Unix boundary. The source baseline is upstream Wine wine-10.0, commit b073859675060c9211fcbccfd90e4e87520dc2c2. All handle values and timelines below are synthetic. No Wine server or prefix was started for this lesson.
Learning goals#
Trace a client request to a server object, distinguish Windows-facing handles from host fds, follow access checks and object references through close, and explain what remains unknown after a client or server failure.
Source walkthrough#
Read the text version
- Process A handle: A table entry with access rights → Server object: References keep shared state alive (references)
- Process B handle: A separate table entry → Server object: References keep shared state alive (can reference the same object)
- Process A handle: A table entry with access rights → Host descriptor: Present only for objects that use one (may mediate host I/O)
wineserver is a userspace process in Wine's architecture. It coordinates selected Windows-like objects, process and thread state, synchronization and handle operations among clients. It is not the Linux kernel and does not mediate every Windows API call. A Wine client can perform work locally, call Unix helpers or ask the server depending on the operation. Linux still schedules the actual host processes and threads and enforces host credentials, filesystem permissions, namespaces and security policy.
Wine's client-side server communication code establishes a local Unix-domain connection and per-thread communication state. Requests and replies use Wine's versioned internal protocol and generated structures; server_protocol.h describes those structures for this source version. A client and server from incompatible builds can disagree about protocol layout; the startup code checks a protocol version rather than assuming that any executable named wineserver is compatible.
The check is in Wine's dlls/ntdll/unix/server.c, Wine 10.0, LGPL-2.1-or-later. The code that receives the peer's version above these lines is omitted:
if (version != SERVER_PROTOCOL_VERSION)
server_protocol_error( "version mismatch %d/%d.\n"
"Your %s binary was not upgraded correctly,\n"
"or you have an older one somewhere in your PATH.\n"
"Or maybe the wrong wineserver is still running?\n",
version, SERVER_PROTOCOL_VERSION,
(version > SERVER_PROTOCOL_VERSION) ? "wine" : "wineserver" );
The comparison is between the number received from the other side and the build's SERVER_PROTOCOL_VERSION. On a mismatch the message names the wine binary when the received number is larger and wineserver otherwise. The check rejects a layout disagreement at startup. It does not validate later requests, and a matching number does not prove that the two builds are compatible in every other respect.
In server/request.c, the server receives a request for a thread, dispatches a request-specific handler and sends a reply containing an error and any reply data when the reply channel is available. Its event loop and fd callbacks determine when requests are read and handled. A local socket connection is an IPC transport; it does not imply that every request passes raw pointers valid in the other process. Messages must use the protocol's data representation and validate sizes and object references.
Server objects and handle tables#
A server struct object represents a shared Wine-managed object with a reference count, a type-specific operations table and other state such as waiters or naming information where applicable. The operations table supplies behavior appropriate to that object. A process's handle table contains entries that refer to objects and record granted access and attributes. A Windows-facing HANDLE is a token interpreted in the relevant Wine process/object context, not inherently a Linux fd, a pointer to the object, or a Linux capability. Some object types use Unix fds internally; others do not.
In server/handle.c, alloc_handle maps requested access through the object's operations and checks access before allocating an entry. alloc_entry finds or grows the table, stores a reference obtained for the handle, records access and returns an encoded handle. If the process no longer has a handle table, alloc_handle_entry reports a terminating process and returns zero. A failed allocation is not a usable handle, and a caller must release any temporary object reference it already owns according to its path.
The entry takes its own object reference, separate from any reference the caller holds. In server/handle.c, alloc_entry in Wine 10.0 (LGPL-2.1-or-later; the free-slot search and table growth before these lines are omitted):
table->free = i + 1;
entry->ptr = grab_object_for_handle( obj );
entry->access = access;
return index_to_handle(i);
The reference and the access mask are stored together, and the return value is an index encoded as a handle. The excerpt does not show the earlier return 0 when the table cannot grow, so a zero result must still be treated as failure rather than as a handle.
get_handle_obj validates a handle, optionally checks object type and required access, and obtains a reference for a successful lookup. This exact Wine 10.0 excerpt is from that function in server/handle.c; the file's notice grants LGPL-2.1-or-later, copyright Alexandre Julliard and contributors. Surrounding handle and type checks are omitted:
if ((entry->access & access) != access)
{
set_error( STATUS_ACCESS_DENIED );
return NULL;
}
The expression compares the requested access bits with the rights stored in the handle entry. Missing rights cause an access-denied status and return NULL rather than a borrowed object pointer. Earlier code can reject an invalid handle or mismatched type; successful lookup later calls grab_object, increasing a reference that the caller must eventually release. Pseudo-handles have a separate path. This is Wine's emulation of Windows object-access semantics; host Linux permissions and LSM checks remain separate when a host resource is used.
Closing, reuse and final release#
The close_handle function finds the entry, checks close protection and the object's close operation, clears the entry and releases the handle's object reference. This exact Wine 10.0 LGPL-2.1-or-later excerpt from server/handle.c omits table bookkeeping between the two shown operations:
if (!obj->ops->close_handle( obj, process, handle )) return STATUS_HANDLE_NOT_CLOSABLE;
entry->ptr = NULL;
The object-specific callback can refuse close; in that case the function returns an error and does not clear the entry on this path. If it permits close, the entry pointer is cleared before later bookkeeping and release_object_from_handle. An invalid handle or a protected entry has earlier failure branches. The server request handler for close calls this function and places its status in the reply; a client must not assume that issuing a close request guarantees success.
In server/object.c, grab_object increments the reference count and release_object decrements it. When the count reaches zero, the code asserts that no handle references or waiters remain, frees related kernel-object state, unlinks the name, invokes the type's destroy operation and frees the object. Those assertions express invariants expected by the server implementation. An outstanding wait, temporary lookup or other object reference can keep an object alive after one handle closes. Object-specific teardown and host fd closure must be inspected in the relevant type.
- alloc_entry
Store a grabbed object reference and the access mask in a free slot.
- get_handle_obj
Check requested access against the entry, then take a temporary reference on success.
- close_handle
Let the object refuse close; otherwise clear the entry and drop the handle reference.
- release_object
When the count reaches zero, run the type teardown and free the object.
A handle-table slot can later be reused. Therefore, after a handle is closed, retaining its numeric value and using it later can refer to an invalid entry or, after reuse, a different object. A robust program does not infer identity solely from a stale integer. The source has special pseudo-handle and global-handle cases, so a simplified per-process table is a teaching model rather than an exhaustive description of every handle encoding.
Worked two-handle and fd timeline#
Assume a synthetic Wine-managed file-like object O exists, with one handle H1 in process A and one handle H2 in process B. For this teaching example, suppose each handle contributes one object reference and a temporary server lookup contributes one additional reference. At time 0, the two handles account for two references. A lookup of H1 with permitted access calls grab_object, temporarily raising the count to three. The handler finishes and releases that lookup reference, returning the count to two. Exact implementation references can include additional owners; the counts here are intentionally simplified.
Process A closes H1 successfully. The entry is cleared and its handle reference is released, leaving H2's reference. O remains alive. If A tries H1 again before any reuse, lookup can return STATUS_INVALID_HANDLE. If the slot is later reused for a different object, the same numeric handle value must not be assumed to identify O. Process B can still use H2 if its rights and object state permit. A request for rights absent from H2 fails the access-mask check even though the object remains alive.
Now suppose O also uses a Unix fd, and a host fd is passed to a client using Unix SCM_RIGHTS where that particular protocol and object support it. Passing an fd creates a receiver-side descriptor reference to a host open-file description; it does not turn H2 into that numeric fd. Closing A's H1 does not necessarily close B's handle or every duplicated host fd. Conversely, closing a received host fd does not automatically close an unrelated server handle. The exact server object can retain its own fd and release it in its type-specific destroy path. Never infer final destruction solely from one close without accounting for all references and in-flight operations.
After H2 closes and all temporary references and waiters are gone, the last release can reach the object-destruction path. If a handler still holds a reference, destruction waits for that reference's release. If a process terminates while a request is in flight, communication failure, handle-table cleanup and object lifetime interact; the server and client must not use freed request buffers or stale objects. This is a conceptual timeline, not a claim that every Wine file operation has exactly these reference counts or uses fd passing.
Protocol dispatch, errors and context#
A client constructs a request with the expected protocol layout and sends it over the relevant channel. The server associates the request with a Wine thread, dispatches the handler and returns a reply status and optional data. The client-side wine_server_call blocks selected signals around server_call_unlocked, then restores the prior signal mask. A short read, broken reply channel or protocol mismatch has explicit error handling; it is not a successful empty reply. A request and its reply can outlive an individual higher-level API call's assumptions if the client exits or the server is shutting down, so buffers and descriptors must follow the protocol's ownership rules.
- Client threadClient send path
Build request in the protocol layout
- Client send pathServer request loop
Send over the request channel
- Server request loopRequest handler
Dispatch handler for the thread
- Request handlerServer request loop
Return. Return status and optional data
- Server request loopClient send path
Return. Send reply or report a channel error
- Client send pathClient thread
Return. Interpret status; a short read is not success
Time runs downward. Dashed arrows show returns.
Read the text version
- Client thread → Client send path: Build request in the protocol layout
- Client send path → Server request loop: Send over the request channel
- Server request loop → Request handler: Dispatch handler for the thread
- Request handler → Server request loop (return): Return status and optional data
- Server request loop → Client send path (return): Send reply or report a channel error
- Client send path → Client thread (return): Interpret status; a short read is not success
Wine can pass host fds over Unix sockets using ancillary data such as SCM_RIGHTS for operations that need it. The server's request code checks received control messages and handles partial or failed transfers; it closes a received fd on a relevant error branch. A duplicated fd refers to a host kernel object, while a Wine handle refers to server-managed semantics and access rights. The mappings between them are implementation-specific. A server request's success does not guarantee that later host I/O succeeds; Linux can reject an operation because of file permissions, mount state, LSM policy or resource exhaustion.
The server's event-driven implementation and client threads have different synchronization contexts from Linux kernel code. A Wine server object operation can manage its own wait queue and references, while a host syscall can block or fail separately. Windows process and thread IDs are names in Wine's emulated environment and are not automatically identical to host PIDs or thread IDs. A wait on a Windows-facing handle has object-specific semantics and is not simply “wait on this Linux fd.” Later lessons study synchronization in more detail; here the key invariant is to retain the correct object until all handlers and waiters are done.
Security tokens and host authority#
Wine implements Windows-facing access masks, tokens and security-descriptor behavior to support compatible applications. These emulated checks do not create host kernel privilege or replace Linux UID/GID, capabilities, namespaces and LSM enforcement. A Windows-facing token that appears privileged to an application is not proof that the process is root on the host. Conversely, a Wine process launched by a powerful Unix user retains that user's host authority unless separately restricted. A private prefix is not a sandbox.
A handle can grant rights within Wine's object model and can refer to a file or device whose host access has its own checks. If the server opens or passes an fd, both the server's object-access decision and the host kernel's operation-specific rules matter. Security descriptors, host ACLs and mount rules are different policies. Do not use an application with unknown provenance, proprietary DLLs, a game installation, DRM or anti-cheat bypass to test these concepts. Source reasoning and a self-authored program in a new private environment are sufficient starting points.
A denied lookup and a concurrent close#
Consider a server handler that needs write access to O but receives H2 with only read rights. get_handle_obj checks the stored access mask against the requested mask and returns NULL with access denied. The handler must not dereference O as if lookup had succeeded, and it must not release a reference that was never acquired by that failed lookup. This is separate from an invalid handle, where no valid entry was found, and from a close-protected handle, where an existing entry cannot be closed through that request.
Now suppose a successful handler has already grabbed a temporary reference to O and another request closes H2. Clearing the handle entry releases the handle's reference, but the handler's temporary reference can keep O alive until its work completes. The exact order in which server events are dispatched and asynchronous host operations finish depends on the object type and implementation. A correct design does not save a raw object pointer after releasing its reference and assume that a later request will find the same object. Table reuse, process termination and outstanding waits all need their own lifecycle handling.
A useful source review follows each successful acquisition to its matching release and each error branch to its return status. It also checks whether a host fd has been transferred or duplicated: failure after receiving one can require closing that received descriptor, while failure before receipt cannot justify closing an unrelated fd. This resource-accounting method is more reliable than assuming a one-to-one relationship between Windows handles and Unix descriptors.
Practice#
Source-based reference accounting
This userspace model uses the deliberately simplified counts from the worked example. It is not Wine server code and does not create a prefix, open a server socket or inspect another process:
python3 - <<'PY'
refs = 2
for event, delta in [('lookup H1', 1), ('release lookup', -1), ('close H1', -1), ('close H2', -1)]:
refs += delta
assert refs >= 0
print(event, 'references:', refs, 'destroy eligible:', refs == 0)
PY
Expected counts are 3, 2, 1 and 0. In the simplified model, destruction is eligible only at the last step; real Wine objects can have additional references or waiters. As a counterexample, leave the lookup reference unreleased before both closes: the count remains one and final destruction is not yet eligible. If Python is unavailable, use the supplied numbers without installing it.
Source-only alternative: construct a table for valid H1 lookup, missing access, protected close and successful close using the two quoted branches. Add the fd-passing distinction and the exact point where a temporary lookup reference must be released. Deliver that table and the two-handle timeline; do not inspect a default prefix, another user's server or a live handle table.
Knowledge check#
- Why is a Windows-facing Wine handle not automatically a Linux fd?
- What does
get_handle_objdo when requested access is absent from the entry? - Why can object O remain alive after H1 is closed?
- What happens if the object's close callback rejects closing the handle?
- Why can a stale numeric handle be dangerous after table-slot reuse?
Answers#
- A Wine handle names a server-managed object and rights in its handle context. Some objects also use host fds, but the two tables and lifetimes are distinct.
- The cited branch sets
STATUS_ACCESS_DENIEDand returns NULL rather than handing out an object reference. - H2, a temporary lookup, a waiter or another owner can retain references. One handle's close releases only its own relevant reference.
- The function returns
STATUS_HANDLE_NOT_CLOSABLEbefore clearing the entry on that path; the caller must handle failure. - A closed value can later name a different entry after reuse. Numeric equality alone does not establish object identity or authority.
Checkpoint#
Submit the lookup/close table and a two-handle, temporary-reference and optional host-fd timeline. Ready means access denial, close failure, successful release and final destruction are distinct, and Wine token semantics are not confused with host privilege. Revisit get_handle_obj, close_handle and release_object if a reference has no owner. This is learning progress, not certification.