Before you start#
Review Wine handles and object lifetimes, files and registry, and Wine services and IPC. The source baseline is Wine 10.0 commit b073859675060c9211fcbccfd90e4e87520dc2c2. The scenarios are synthetic; no installer, registry registration or Wine process is run.
Learning goals#
Explain COM identity and reference ownership, distinguish apartment models and cross-thread marshaling, trace an initialization error, and reason about installer actions and rollback without treating a prefix as a sandbox.
Source walkthrough#
- Calling threadCOM runtime
Initialize an apartment
- Calling threadCOM runtime
Request class and interface
- COM runtimeClass factory
Find and invoke factory
- Class factoryCalling thread
Return. Return interface or error
- Calling threadCOM runtime
Release interfaces and uninitialize
Time runs downward. Dashed arrows show returns.
Read the text version
- Calling thread → COM runtime: Initialize an apartment
- Calling thread → COM runtime: Request class and interface
- COM runtime → Class factory: Find and invoke factory
- Class factory → Calling thread (return): Return interface or error
- Calling thread → COM runtime: Release interfaces and uninitialize
COM is a binary interface and object model. An object can expose one or more interfaces, each identified by an IID. IUnknown provides QueryInterface, AddRef and Release; when a query succeeds, the returned interface pointer carries a reference that the caller must release. QueryInterface returning E_NOINTERFACE means the requested interface was not supplied. COM identity is tested through QueryInterface(IID_IUnknown): a conforming object returns the same physical IUnknown pointer for that instance. An interface pointer is not an object name, a Linux fd or an assurance that calls are safe from any thread. A CLSID identifies a class that can be activated; class registration and activation locate an implementation and a class factory under the selected context. An IID identifies the requested interface, not the class itself. (Microsoft: QueryInterface rules, IUnknown)
This exact excerpt is from Wine 10.0 dlls/combase/apartment.c, enter_apartment, licensed LGPL-2.1-or-later. The warning and surrounding branches are omitted:
else if (!apartment_is_model(data->apt, model))
{
WARN("Attempt to change threading model of this apartment from %s to %s\n",
data->apt->multi_threaded ? "multi-threaded" : "apartment threaded",
model & COINIT_APARTMENTTHREADED ? "apartment threaded" : "multi-threaded" );
return RPC_E_CHANGED_MODE;
}
The condition detects that the thread already has an apartment with a different requested model. The diagnostic describes the existing and requested models; the return rejects the change. It does not create a second apartment for the same thread. In the surrounding source a first initialization obtains or creates an apartment, a repeated compatible initialization returns S_FALSE, and successful initialization increments the thread's initialization count. A caller balances successful calls with CoUninitialize; a failed changed-mode call must not be counted as another success.
The second excerpt is from Wine 10.0 dlls/msi/action.c, execute_script, also LGPL-2.1-or-later. It is within the rollback branch; surrounding loop setup and cleanup are omitted:
rc = ACTION_PerformAction(package, package->script_actions[script][i-1]);
if (rc != ERROR_SUCCESS)
The action is selected using i-1 because the surrounding rollback loop counts downward. Its returned status is checked; the following branch logs the failure and stops that loop. After the script path, Wine resets the script state, frees the action script and returns the result. This small branch demonstrates a real failure path but does not prove rollback always restores every external side effect. A custom action can interact with resources outside the installer's control, and an interruption or rollback error can leave state requiring investigation.
Interfaces, identity, and activation#
A well-behaved COM object exposes a stable identity through its IUnknown behavior. Querying different supported interfaces can yield different interface pointers, but they represent the same object identity when queried according to COM rules. A successful QueryInterface supplies a reference the caller must release. A failed query such as E_NOINTERFACE does not create the requested usable interface; the caller must check the returned HRESULT and output contract. Copying a pointer without retaining the object, or releasing a reference twice, can produce use-after-free even if the numeric pointer happens to remain unchanged.
A class factory is itself an interface with a lifetime. Activation can look up a class, load an in-process server DLL or contact an out-of-process server, obtain a factory and request an instance. In-process code executes inside the client's process and has its host authority; an out-of-process server has its own process and transport but is not automatically a security boundary stronger than the host policies applied to it. Registration links class identifiers to implementation information, but registration alone cannot make an absent binary load or make a class support every IID. Wine's COM implementation and apartment source contain version-specific activation and initialization paths.
A source investigation must keep distinct the class registration, factory reference, object interface, RPC proxy and host file involved in activation. An invalid CLSID, missing registration, wrong bitness, load failure or unsupported interface can fail at different stages. If a class factory is obtained and a later instance request fails, the factory reference still needs release. If a DLL was loaded only for the failed attempt, its module lifetime follows the implementation's ownership rules, not the lifetime of a local string containing its path. No single generic HRESULT explains every activation failure.
Apartments, threading, and marshaling#
A COM apartment is a concurrency context that governs how calls reach objects. A single-threaded apartment, or STA, is associated with a thread and often relies on that thread processing its message queue for incoming calls. A multithreaded apartment, or MTA, permits calls according to a different concurrency model and requires appropriate object synchronization. The choice affects reentrancy and dispatch; it does not mean an STA object cannot call out or that an MTA object is automatically protected by a global mutex.
In the worked case, thread T first initializes COM as an STA and receives S_OK. A second compatible initialization on T returns S_FALSE in the inspected source and increments its initialization count. T must eventually balance both successful initializations. If T instead requests an MTA while still initialized as an STA, enter_apartment returns RPC_E_CHANGED_MODE; that failed attempt does not add a successful initialization to balance. This is a source-derived state example, not captured Wine output. A separate thread has its own thread-local initialization state and must initialize appropriately before using interfaces under the relevant rules.
In Wine 10.0 dlls/combase/apartment.c, leave_apartment, LGPL-2.1-or-later (shown whole):
void leave_apartment(struct tlsdata *data)
{
if (!--data->inits)
{
if (data->ole_inits)
WARN( "Uninitializing apartment while Ole is still initialized\n" );
apartment_release(data->apt);
if (data->implicit_mta_cookie)
{
apartment_decrement_mta_usage(data->implicit_mta_cookie);
data->implicit_mta_cookie = NULL;
}
data->apt = NULL;
data->flags &= ~(OLETLS_DISABLE_OLE1DDE | OLETLS_APARTMENTTHREADED | OLETLS_MULTITHREADED);
}
}
Each balanced uninitialization runs this path once. The count reaches zero only after the last matching call, and only then does the code release the apartment and drop implicit MTA usage. The warning shows that uninitializing while OLE is still active is logged here, not refused.
- Thread TEntry (enter_apartment)
CoInitializeEx as STA
- Entry (enter_apartment)Thread apartment state
Create apartment; inits becomes 1
- Entry (enter_apartment)Thread T
Return. S_OK
- Thread TEntry (enter_apartment)
CoInitializeEx as MTA on the same thread
- Entry (enter_apartment)Thread T
Return. RPC_E_CHANGED_MODE; inits unchanged
- Thread TExit (leave_apartment)
CoUninitialize balances the successful call
- Exit (leave_apartment)Thread apartment state
inits reaches 0; release the apartment
Time runs downward. Dashed arrows show returns.
Read the text version
- Thread T → Entry (enter_apartment): CoInitializeEx as STA
- Entry (enter_apartment) → Thread apartment state: Create apartment; inits becomes 1
- Entry (enter_apartment) → Thread T (return): S_OK
- Thread T → Entry (enter_apartment): CoInitializeEx as MTA on the same thread
- Entry (enter_apartment) → Thread T (return): RPC_E_CHANGED_MODE; inits unchanged
- Thread T → Exit (leave_apartment): CoUninitialize balances the successful call
- Exit (leave_apartment) → Thread apartment state: inits reaches 0; release the apartment
Passing a raw interface pointer to another apartment can violate the object's threading contract. COM marshaling creates an appropriate representation and, where needed, a proxy/stub and RPC path so calls are delivered under the object's rules. The global interface table can help share registered interfaces between apartments; agile objects or explicitly agile references have different rules. These mechanisms have their own references and teardown. A proxy is not proof that the server process remains alive: calls can fail if the server exits or transport breaks, and a caller must still release the proxy and handle failures.
An STA's message pump is important for dispatch, but pumping can also introduce reentrancy: while a thread waits for a call, another incoming call can observe partially updated state. Holding an application lock across an outbound COM call can deadlock if a callback requires the same lock or a related lock in another thread. An MTA object may receive concurrent calls and must protect its own mutable state. Wine's marshaling implementation and RPC implementation are entry points, not proof that every Windows component or custom proxy works.
- First initialization
With no apartment yet, creation proceeds and hr stays S_OK before the count changes.
- Model mismatch
apartment_is_model fails; the function returns RPC_E_CHANGED_MODE before the count changes.
- Compatible repeat
The existing apartment is accepted and hr is S_FALSE.
- Count every success
data->inits increments on success paths, including the S_FALSE repeat.
- Balance on exit
leave_apartment decrements the count; at zero it releases the apartment and clears the pointer.
OLE and data-transfer boundaries#
OLE builds higher-level services on COM, including data transfer and drag-and-drop interfaces. A data object can advertise formats and provide data when asked; the recipient must handle unsupported formats and the ownership rules for returned storage. A drag operation can cross threads, windows or processes and can be cancelled before completion. The object's reference and any medium it returns have distinct release rules. A callback can arrive while another part of the application is tearing down a window, so the implementation needs both object lifetime and UI-thread coordination.
Clipboard and drag-and-drop integration can bridge Wine's Windows-facing formats with a host display system. Host selection ownership, delayed rendering, policy and format conversion are additional boundaries. This lesson does not read or modify a clipboard or capture input. A prefix does not isolate hostile COM code from host files or network authority; an in-process server loaded by a Wine application runs with that application's host credentials. Class registration in a prefix is configuration, not a sandbox.
Managed code is another runtime#
Managed .NET code executes under a managed runtime with its own metadata, loader, garbage collector, class libraries and native interoperation. mscoree is a Windows-facing activation boundary; it is not itself a complete guarantee that a managed application and all its dependencies run. Wine Mono is a separate project and release from Wine, with its own implementation and compatibility scope. Microsoft's .NET redistributables and a native Linux .NET installation are separate products and deployment choices. A native Linux build of a managed application may avoid the Wine Windows-facing path, but compatibility depends on that application's APIs and native dependencies.
A managed object's garbage collection lifetime does not replace COM reference rules for native interfaces crossing an interop boundary. Wrappers can retain COM references, and callbacks can require a thread and apartment context even when the calling code is managed. An installer that requests a runtime can fail because the runtime is absent, the requested version is unsupported, a component is missing or activation fails. Those hypotheses need specific evidence; downloading an unknown redistributable or running a helper script is not an acceptable substitute for understanding the failure. No additional runtime project version is asserted or tested here.
MSI actions, rollback, and authority#
An MSI package is a structured database with tables describing components, files, features, properties and action sequences. An immediate action can calculate or schedule work; deferred actions run later in the execution sequence under the applicable installer context. A package can include custom actions, whose behavior and side effects depend on their implementation. Rollback scripts attempt to undo applicable work when installation fails, but rollback can itself fail or be incomplete. A package's database, queued action script, temporary files and installed resources each have separate lifetimes.
Consider a synthetic transaction with actions A, B and C. A creates an authored temporary file, B schedules an update, and C fails. Suppose rollback has recorded undo actions for A and B. The inspected execute_script rollback branch runs its listed rollback actions in reverse order. If B's undo succeeds but A's undo returns an error, the loop stops at that failure in this function. The final state must be checked: saying “rollback was requested” does not prove the temporary file was removed. The table should record action, pre-state, forward result, undo result and observed post-state. This is a reasoning case, not an executed installation.
Installation authority belongs to the process and environment actually running it. An installer inside Wine may modify the prefix and any other host resources its user can access; a Windows administrator label does not grant Linux root, and a prefix does not confine malicious code. Per-user and machine-style Windows registry locations are Windows-facing distinctions, not permission to modify arbitrary host files. Do not execute an untrusted installer or proprietary redistributable to explore this mechanism.
Winetricks is a separate collection of helper scripts with its own versions, recipes and downloaded components. It is not part of Wine's COM implementation and is not a universal compatibility fix. A recipe can change a prefix, install components or depend on third-party licenses. This chapter does not run it, pin a recipe, or recommend a blanket DLL override. A careful investigation records an exact script revision, dependencies, origin and authorized isolated environment before any separate experiment; those are not evidence supplied by this source-only chapter.
An activation failure ledger#
Imagine an STA caller requests a class factory, obtains it successfully, then requests an interface that the class does not implement. The failure does not erase the factory reference already obtained. The caller records the failed interface request, releases the factory once and balances its successful apartment initialization. A null requested-interface result cannot be used as a substitute for a valid object. If the same class is provided by an out-of-process server, process termination can instead make a later proxy call fail after activation succeeded. The activation result and the later call result belong to different times and must both be preserved in an investigation. This ledger is hypothetical and does not claim a particular class is present.
Practice#
This own-process model exercises apartment accounting and a rollback order. It has no COM runtime and changes no registry or files:
python3 - <<'PY'
model, count = 'STA', 1
same_model = 'STA' == model
if same_model:
count += 1
changed_mode = 'MTA' != model
print('second compatible initialization:', 'S_FALSE' if same_model else 'error')
print('different model rejected:', changed_mode, 'successful initializations:', count)
actions = ['undo A', 'undo B']
print('synthetic rollback order:', list(reversed(actions)))
PY
Expected output is S_FALSE, a rejected model change with count two, and B before A. The model does not call COM or MSI. As a counterexample, counting the rejected change as a successful initialization would require an extra uninitialization that the cited branch does not justify. If Python is unavailable, write the same state and action table by hand.
For the source-only route, use the supplied STA/MTA timeline and A/B/C installer case. Record each successful initialization, interface or factory reference, forward action and rollback result. Explain why the failure of undo A leaves its post-state uncertain and why a failed QueryInterface cannot be released as a valid requested interface. Deliver the table without invoking Wine, installers, clipboard or a registry.
Knowledge check#
- What is the difference between a CLSID and an IID?
- How many successful initializations must the synthetic STA thread balance after the changed-mode attempt?
- Why can a raw interface pointer be unsafe when passed to another apartment?
- What does the MSI rollback excerpt do when an undo action fails?
- Why does a managed runtime or helper script not prove an arbitrary installer will work safely?
Answers#
- A CLSID identifies a class to activate; an IID identifies an interface requested from an object or factory. Registration and supported interfaces are separate questions.
- Two: the first
S_OKand the compatibleS_FALSE. The later changed-mode request returns an error before incrementing the initialization count. - Its object may have apartment affinity and require marshaling, proxy lifetime and dispatch through the appropriate context. A pointer copy neither creates those mechanisms nor retains the object automatically.
- The surrounding rollback loop checks the return, logs the failure and breaks. Cleanup of the script follows, but complete restoration of all external state is not established.
- Managed API coverage, native dependencies, installer actions, source versions and host authority all matter. A helper can make changes or download components; none of that establishes compatibility or isolation.
Checkpoint#
Submit the apartment count, COM reference ledger and installer forward/rollback table. Pass when failed changed-mode initialization is not balanced as success, interface ownership is explicit and rollback failure leaves a question about final state. Revisit the two cited source branches and retry if class identity is confused with an interface identifier. This is not certification.