Before you start#
Review Wine handles, Wine memory and exceptions, COM apartments, and userspace debugging. This chapter uses Wine 10.0 commit b073859675060c9211fcbccfd90e4e87520dc2c2. Its logs and test outcomes are explicitly synthetic; neither Wine nor Windows was run for this capstone.
Learning goals#
Interpret a Wine diagnostic without confusing an early exception with a crash, follow a real COM apartment invariant and its test, design a falsifiable regression investigation, and distinguish a source explanation from executed compatibility evidence.
Source walkthrough#
- Capture context
Record versions, API result and relevant object state.
- Form hypotheses
Find source branches that could explain the observation.
- Test and compare
Use controlled positive and negative cases.
- Review evidence
Distinguish source reasoning from executed conformance tests.
A compatibility report needs the exact Windows-facing request, result and environment. “The program fails” is not a reproducible observation: it could refer to loader failure, a missing interface, an unhandled exception, a host permission error or a graphics device error. Begin with a small authorized input, preserve the first relevant failure and record the Wine build, architecture, configuration and host context. A prefix is not a sandbox and diagnostics can expose file paths, account names, tokens, arguments or document contents; collect only data you are authorized to inspect and review it before sharing.
This actual excerpt is from Wine 10.0 dlls/ole32/tests/compobj.c, from a COM initialization test. The file notice grants LGPL-2.1-or-later; surrounding setup and cleanup are omitted:
hr = OleInitialize(NULL);
ok(hr == RPC_E_CHANGED_MODE, "OleInitialize should have returned 0x%08lx instead of 0x%08lx\n", RPC_E_CHANGED_MODE, hr);
The preceding source initializes COM with CoInitializeEx(NULL, COINIT_MULTITHREADED) and asserts S_OK; the excerpt above follows that setup. The test then calls OleUninitialize after the failed OLE initialization and later balances the successful COM initialization with CoUninitialize. A different nearby OleInitialize(NULL) call is unasserted and is not this test. Microsoft's API contract independently specifies that OLE initialization requests STA and an incompatible prior model yields RPC_E_CHANGED_MODE (Microsoft CoInitializeEx).
The corresponding implementation branch is in Wine 10.0 dlls/combase/apartment.c, enter_apartment, also LGPL-2.1-or-later. This exact excerpt omits the warning text and surrounding condition:
return RPC_E_CHANGED_MODE;
The return occurs in the branch where existing apartment state does not match the requested threading model. It exits before the later increment of the successful initialization count. The source invariant is therefore specific: a mismatched request on an already initialized thread is rejected rather than counted as another compatible initialization. The test assertion and implementation branch support a source-based hypothesis. They do not substitute for actually executing the test against a built Wine revision or a native Windows reference.
Logging without inventing a trace#
WINEDEBUG selects Wine debug channels and classes for an actual run. Channels can expose loader, server, exception and other activity, depending on build and configuration. Relay-style call tracing can produce enormous output, affect scheduling and reveal arguments. Logging perturbs a race: adding output can change thread timing, hide a failure or create a timeout. A debugging plan should begin with the narrowest relevant channel and a controlled input, then compare with logging disabled. This chapter does not enable tracing or create a log.
A first-chance exception is an initial notification that an exception occurred; an application, debugger or runtime handler may handle it. It is not automatically the fatal cause of a process exit. A later unhandled exception, stack corruption or unrelated failure may be decisive. When correlating a signal, Windows exception and subsequent API result, record thread, timestamp and context but do not assume that log ordering across threads proves causal ordering. A missing record can be due to filtering, buffering, process exit or logging configuration.
A debug channel name is not an API guarantee. The exact messages and available flags depend on the selected Wine build and source revision. A server trace can show selected requests without revealing every client-side operation; a missing server request does not prove a function was never called. The ntdll boundary, host system calls and server object transitions have different visibility. Never place passwords, private file contents, authentication tokens or another user's data into a report merely because a verbose trace captured them.
Symbols, stacks, and architecture#
winedbg and GDB can help examine Wine processes in an authorized environment. A useful stack requires matching binaries, debug symbols and source revision. A Windows-facing PE module and a Unix-side ELF module have different address and symbol conventions; a mixed stack can cross both. Record architecture and bitness, loaded module versions, relocation and the debugger's interpretation of each frame. An address from one build is not automatically meaningful in another. A minidump can preserve selected process state but is not necessarily a complete memory image and can contain sensitive data.
On WoW64 paths, a stack may cross 32-bit and 64-bit contexts. Registers, calling conventions and unwind metadata differ, and a debugger or dump may not contain enough information to reconstruct every frame. A corrupted stack, missing unwind information or asynchronous signal can yield an incomplete or misleading backtrace. The presence of an ntdll frame does not establish that ntdll caused the defect; it may simply be where an earlier invalid argument was detected. Connect each frame to the actual source branch and object lifetime before proposing a fix.
A useful crash report separates observed facts from interpretations: “thread T returned this HRESULT” is an observation if captured reliably; “the server destroyed the object” is a hypothesis unless the relevant object transition was observed or established by the source and prior state. A crash dump or debug log must be handled under the user's authority and may require redaction. This lesson uses supplied synthetic evidence instead of opening personal dumps, prefixes or application files.
Wine's test vocabulary#
Wine's test framework header defines ok, skip, win_skip, todo_wine, broken and other test constructs. ok evaluates an assertion and reports according to the framework. A skip means a test was not performed under its preconditions; it is not a passing result. todo_wine marks a condition expected to fail on Wine in the applicable test context, so an expected failure records an outstanding difference rather than proving conformance. An unexpected success needs investigation and may justify removing an outdated marker after appropriate validation.
In Wine 10.0 include/wine/test.h, the ok, skip and todo macros, LGPL-2.1-or-later (lines omitted, marked with a comment: other macro definitions):
#define ok ok_(__FILE__, __LINE__)
#define skip skip_(__FILE__, __LINE__)
/* ... */
#define todo_if(is_todo) for (winetest_start_todo(is_todo); \
winetest_loop_todo(); \
winetest_end_todo())
#define todo_wine todo_if(winetest_platform_is_wine)
ok is an assertion whose failure the framework reports, and skip records a case that did not run. todo_wine expands to a todo_if loop tied to the Wine platform. The loop starts and ends todo mode around the statement it governs; this excerpt does not show how the framework reports inside it.
broken can express acceptance of a documented alternative behavior on some Windows versions. It should not be used as a blanket excuse for a newly observed Wine failure. A test can also have architecture, OS version, configuration or environmental conditions; a result has meaning only with those conditions and a known test binary. A native Windows reference run can help characterize behavior, but this lesson performed none. Source comments and an assertion are useful evidence about the test author's intended expectation, not a claim that every Windows release has been independently measured here.
The test framework itself has ownership and execution boundaries. A test that launches a child must wait for and clean up that child; a timeout is not the same result as an assertion failure in the intended operation. A test involving global registration, services or UI can affect a host and belongs only in an appropriately isolated environment. A small source-only analysis can still evaluate control flow and expected state, but cannot establish that a compiled binary behaves correctly. Conformance is a body of evidence across implementations, versions and cases, not a badge awarded for one passing assertion.
- ok
- Evaluates an assertion; a failed assertion is reported by the framework.
- skip
- The precondition was not met, so there is no pass result for that case.
- todo_wine
- An expected failure on Wine; an outstanding difference, not conformance.
- broken
- Documents an accepted alternative on some Windows versions; not a shield for new Wine failures.
Capstone: a changed apartment model#
The following evidence is synthetic. A small, self-authored Windows test program specification starts one thread, calls CoInitializeEx with the multithreaded model, then asks for OLE initialization on that same thread. It prints only the two HRESULT categories and balances resources as required by the calls that actually succeeded. It loads no third-party DLL, opens no personal file and performs no clipboard operation. No PE program has been compiled or executed here.
Suppose an imaginary report says: first call S_OK; second call S_OK; process exits normally. The expected result from the cited Wine test for its corresponding setup is RPC_E_CHANGED_MODE for the second call. This discrepancy is a hypothesis trigger, not a measured Wine regression. Alternative explanation one is that the second call ran on a different thread, whose apartment state was not initialized. Alternative two is that the reported binary was built from a different Wine version or loaded a different implementation. Alternative three is that the test program interpreted or printed the return value incorrectly. These alternatives can explain an apparent mismatch without changing Wine's apartment code.
The source walkthrough gives a candidate invariant. In enter_apartment, thread-local data->apt is created if absent. If it exists and apartment_is_model rejects the requested model, the function returns RPC_E_CHANGED_MODE before data->inits++. If the model matches, it returns S_FALSE after incrementing the successful initialization count. These branches distinguish absent state, incompatible state and repeated compatible state. The apartment object, thread-local data and interface references have separate lifetimes. A fix that merely changes a printed error string would not restore an incorrect initialization count.
In Wine 10.0 dlls/combase/apartment.c, enter_apartment, LGPL-2.1-or-later (shown whole):
HRESULT enter_apartment(struct tlsdata *data, DWORD model)
{
HRESULT hr = S_OK;
if (!data->apt)
{
if (!apartment_get_or_create(model))
return E_OUTOFMEMORY;
}
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;
}
else
hr = S_FALSE;
data->inits++;
return hr;
}
The increment follows both the creation branch and the mismatch return, so a changed-model request skips it. S_FALSE is set only in the existing, matching-model branch. A candidate fix must keep that branch and its count together.
A falsifiable test matrix would include: first MTA then OLE on the same thread; first STA then a compatible initialization on the same thread; first MTA on one thread and OLE on a separately initialized thread; and an uninitialized thread's first OLE call. The first case tests the changed-mode branch. The compatible case checks S_FALSE and balanced initialization. The separate-thread case tests the alternative explanation that apartment state belongs to another thread. The uninitialized case prevents a proposed fix from rejecting every OLE initialization. Exact setup, success results and cleanup must be validated against the relevant API and test code before execution.
- Test thread Tenter_apartment
CoInitializeEx with MTA on thread T
- enter_apartmentThread apartment state
Create apartment; inits becomes 1
- Test thread TOleInitialize
OleInitialize requests STA on the same thread
- OleInitializeenter_apartment
enter_apartment compares the requested model
- enter_apartmentTest thread T
Return. RPC_E_CHANGED_MODE; inits stays 1
Time runs downward. Dashed arrows show returns.
Read the text version
- Test thread T → enter_apartment: CoInitializeEx with MTA on thread T
- enter_apartment → Thread apartment state: Create apartment; inits becomes 1
- Test thread T → OleInitialize: OleInitialize requests STA on the same thread
- OleInitialize → enter_apartment: enter_apartment compares the requested model
- enter_apartment → Test thread T (return): RPC_E_CHANGED_MODE; inits stays 1
For a hypothetical bug where a local change mistakenly increments the count before rejecting a mismatch, a minimal design would move or guard that increment so only successful initialization paths count. It must preserve the first-initialization and compatible-repeat branches, apartment references and cleanup. This is a hypothetical patch idea, not a claim about the pinned upstream code or an actual fix. Before accepting it, demonstrate the incorrect count on the buggy build, run the reproducer before and after the change, and add a negative control that verifies compatible repeated initialization still requires balanced uninitialization. Independent review should check thread lifetime, error unwinding and existing callers.
A real regression matrix would record exact Wine source and build, PE architecture, compiler, selected test, host architecture and relevant configuration. Compare a baseline and candidate with the same test binary and environment. If available in a separate authorized environment, compare native Windows behavior and label its version; do not silently import an unverified expected result. Run existing Wine tests around the affected COM behavior, then targeted new tests. A timeout, skip, expected failure and pass must remain separate outcomes. Repeated runs can reveal a flaky race but cannot prove the absence of all races.
Root-cause evidence and review#
A strong report contains the minimal test specification, source revision, exact input, expected and observed results, complete cleanup plan and one alternative explanation tested. For the synthetic case, the most decisive missing observation is whether both calls ran on the same thread and which COM implementation actually handled them. A thread ID printed by an owned test and matching module/version information would narrow those alternatives. If the first initialization failed, the second call's result has a different interpretation; therefore the test must assert the first result before applying the changed-mode expectation.
A source-only reader can complete the logic by following the actual mismatch condition, return and later increment, and by explaining the test's expectation. That demonstrates understanding of a bounded source path. It does not establish runtime conformance, Windows compatibility, a GPU result or expert status. Runtime engineering additionally requires a compatible build, an isolated environment, repeated tests and collected results; an independently reviewed change requires review and regression evidence. A checkmark for reading this chapter is not certification.
Practice#
Run this owned Python model of the source-level count invariant. It does not call COM or create a Wine prefix:
python3 - <<'PY'
model, count = 'MTA', 1
requested = 'STA'
result = 'RPC_E_CHANGED_MODE' if model != requested else 'S_FALSE'
if result == 'S_FALSE':
count += 1
print('synthetic result:', result, 'successful initialization count:', count)
PY
The expected result is a changed-mode error and count one. Change the requested model to MTA: the model returns S_FALSE and count two. The counterexample shows why rejecting every second initialization would break a valid same-model case. This is a model of selected branch logic, not Wine or Windows execution.
For the source-only route, build the four-row matrix from the capstone and annotate each row with the relevant source condition and cleanup obligation. Add a column for the alternative explanation involving a second thread and another for evidence not available without execution. Deliver the hypothesized fix and regression plan without running an untrusted binary or collecting personal logs. If Python is unavailable, compute the same states by hand.
Knowledge check#
- Why is a first-chance exception not necessarily the cause of a crash?
- What does
todo_winecommunicate, and why is a skip different from a pass? - Why must the synthetic changed-mode case establish that both calls ran on the same thread?
- What source invariant would a hypothetical count-ordering fix need to preserve?
- Why can a source-only solution not claim native Windows or Wine runtime conformance?
Answers#
- It is an early notification; a debugger or handler may handle it. A later unhandled exception or another failure can be responsible for termination.
- It marks an expected Wine failure in the applicable framework context, recording an outstanding difference. A skipped test did not exercise its assertion, so it supplies no passing result for that case.
- Apartment state is associated with a thread. A second thread can have different or absent initialization, so different-thread execution is a competing explanation for the reported result.
- A mismatched request must return before increasing successful initialization count, while a compatible repeated request still increments it and returns the appropriate repeat result. Object and apartment cleanup must remain balanced.
- Reading and modeling source shows what selected code appears to do under stated assumptions. It does not build or execute that code, measure another implementation or cover untested configurations.
Checkpoint#
Submit the four-case matrix, synthetic evidence classification, hypothetical minimal change and before/after/negative-control plan. Pass when same-thread state, successful initialization count, cleanup and alternative hypotheses are explicit. Revisit the source and retry if an expected failure or skip is counted as a conformance pass. This checkpoint is not certification or independent review.