When `RoGetAgileReference` is used with `AGILEREFERENCE_DELAYEDMARSHAL`, the returned `IAgileReference` also contains a reference to the apartment the object belongs to. Thus, when `IAgileReference::Resolve()` gets called, the object will get marshaled inside the owning apartment, not the caller's apartment.
This seems to be made possible by the `IContextCallback` interface. It provides a way to (ab)use the marshaller to execute arbitrary code inside a COM context, which is an additional bit of state nested inside apartments. An apartment can contain multiple COM contexts, and the standard marshaller will keep track of which context a marshaled interface is bound to. All method calls through that marshalled pointer will be wrapped around a context-switch, even when the caller is in the same apartment. Additionally, creating an instance of the class `CLSID_IContextCallback` allows us to create new contexts inside an apartment.
C++/WinRT makes use of `IContextCallback` to [implement support for C++ coroutines ](https://github.com/microsoft/cppwinrt/blob/ebace4abb8f48b6b9a612e8b84667e604..., et al) inside application STA code. Another use of COM contexts seems to be the UI code in WinRT applications, where certain objects can only be `Released`/destructed from the apartment they were created in.
(I was able to find another instance of `IContextCallback` usage in the wild [here](https://github.com/Open-Shell/Open-Shell-Menu/blob/4ebeadd949d05506428dacf36...), to get an `IAccessible` implementation that has apartment-affinity).
From: Vibhav Pant vibhavp@gmail.com
When RoGetAgileReference is used with AGILEREFERENCE_DELAYEDMARSHAL, the returned IAgileReference also contains a reference to the apartment/context the object belongs to. Thus, when IAgileReference::Resolve() gets called, the object will get marshaled inside the owning apartment/context, not the caller's apartment. --- dlls/combase/tests/roapi.c | 177 +++++++++++++++++++++++++++++++++++++ include/roapi.h | 1 + 2 files changed, 178 insertions(+)
diff --git a/dlls/combase/tests/roapi.c b/dlls/combase/tests/roapi.c index e4100cb11bc..e9e320f2489 100644 --- a/dlls/combase/tests/roapi.c +++ b/dlls/combase/tests/roapi.c @@ -450,6 +450,120 @@ static const IUnknownVtbl unk_agile_vtbl = unk_Release };
+/* IUnknown implementation that has affinity to the STA it was created in. */ +struct unk_ctx_impl +{ + IUnknown IUnknown_iface; + BOOL todo; + UINT64 id; /* Identifier of the apartment this object belongs to. */ + ULONG_PTR context; /* The COM context this object belongs to. */ + APTTYPE type; /* APTTYPE of the apartment this object belongs to. */ + LONG ref; +}; + +static inline struct unk_ctx_impl *impl_unk_ctx_impl_from_IUnknown(IUnknown *iface) +{ + return CONTAINING_RECORD(iface, struct unk_ctx_impl, IUnknown_iface); +} + +#define test_apartment_context(impl) test_apartment_context_(__LINE__, impl) +static void test_apartment_context_(int line, const struct unk_ctx_impl *impl) +{ + APTTYPEQUALIFIER qualifier; + ULONG_PTR cur_ctx; + UINT64 cur_id; + APTTYPE type; + HRESULT hr; + + hr = CoGetContextToken(&cur_ctx); + ok_(__FILE__, line)(hr == S_OK, "CoGetContextToken failed, got hr %#lx\n", hr); + /* As this object has apartment-affinity, its methods can only be called from the apartment and context it was + * created in. */ + todo_wine_if(impl->todo) ok_(__FILE__, line)(cur_ctx == impl->context, "got cur_ctx %#Ix != %#Ix\n", cur_ctx, impl->context); + + hr = CoGetApartmentType(&type, &qualifier); + ok_(__FILE__, line)(hr == S_OK, "CoGetApartmentType failed, got hr %#lx\n", hr); + todo_wine_if(impl->todo) ok_(__FILE__, line)(type == impl->type, "got type %d\n", type); + + hr = RoGetApartmentIdentifier(&cur_id); + ok_(__FILE__, line)(hr == S_OK, "RoGetApartmentIdentifier failed, got hr %#lx\n", hr); + ok_(__FILE__, line)(cur_id == impl->id, "got cur_id %#I64x != %#I64x\n", cur_id, impl->id); +} + +static HRESULT WINAPI unk_ctx_impl_QueryInterface(IUnknown *iface, const GUID *iid, void **out) +{ + struct unk_ctx_impl *impl = impl_unk_ctx_impl_from_IUnknown(iface); + + if (winetest_debug > 1) + trace("(%p, %s, %p)\n", iface, debugstr_guid(iid), out); + + test_apartment_context(impl); + if (IsEqualGUID(iid, &IID_IUnknown) || IsEqualGUID(iid, &IID_IUnknown)) + { + IUnknown_AddRef((IUnknown *)(*out = &impl->IUnknown_iface)); + return S_OK; + } + + *out = NULL; + if (winetest_debug > 1) + trace("%s not implemeneted, returning E_NOINTERFACE\n", debugstr_guid(iid)); + return E_NOINTERFACE; +} + +static ULONG WINAPI unk_ctx_impl_AddRef(IUnknown *iface) +{ + struct unk_ctx_impl *impl = impl_unk_ctx_impl_from_IUnknown(iface); + + test_apartment_context(impl); + return InterlockedIncrement(&impl->ref); +} + +static ULONG WINAPI unk_ctx_impl_Release(IUnknown *iface) +{ + struct unk_ctx_impl *impl = impl_unk_ctx_impl_from_IUnknown(iface); + ULONG ref = InterlockedDecrement(&impl->ref); + + test_apartment_context(impl); + if (!ref) free(impl); + return ref; +} + +static const IUnknownVtbl unk_ctx_impl_IUnknown_vtbl = +{ + unk_ctx_impl_QueryInterface, + unk_ctx_impl_AddRef, + unk_ctx_impl_Release, +}; + +static HRESULT unk_ctx_impl_create(IUnknown **out) +{ + APTTYPEQUALIFIER qualifier; + struct unk_ctx_impl *impl; + HRESULT hr; + + if (!(impl = calloc(1, sizeof(*impl)))) return E_OUTOFMEMORY; + + impl->IUnknown_iface.lpVtbl = &unk_ctx_impl_IUnknown_vtbl; + if (FAILED(hr = CoGetContextToken(&impl->context))) + { + free(impl); + return hr; + } + if (FAILED(hr = CoGetApartmentType(&impl->type, &qualifier))) + { + free(impl); + return hr; + } + if (FAILED(hr = RoGetApartmentIdentifier(&impl->id))) + { + free(impl); + return hr; + } + impl->ref = 1; + *out = &impl->IUnknown_iface; + return S_OK; +} + struct test_RoGetAgileReference_thread_param { enum AgileReferenceOptions option; @@ -499,12 +613,39 @@ static DWORD CALLBACK test_RoGetAgileReference_thread_proc(void *arg) return 0; }
+struct test_agile_resolve_context_params +{ + RO_INIT_TYPE from_type; + RO_INIT_TYPE to_type; + IAgileReference *ref; +}; + +static DWORD CALLBACK test_agile_resolve_context(void *arg) +{ + struct test_agile_resolve_context_params *params = arg; + IUnknown *unknown; + HRESULT hr; + + RoInitialize(params->to_type); + + winetest_push_context("from_type=%d, to_type=%d", params->from_type, params->to_type); + hr = IAgileReference_Resolve(params->ref, &IID_IUnknown, (void **)&unknown); + todo_wine_if(params->to_type == RO_INIT_MULTITHREADED) ok(hr == S_OK, "got hr %#lx\n", hr); + if (SUCCEEDED(hr)) + IUnknown_Release(unknown); + winetest_pop_context(); + + RoUninitialize(); + return 0; +} + static void test_RoGetAgileReference(void) { struct test_RoGetAgileReference_thread_param param; struct unk_impl unk_no_marshal_obj = {{&unk_no_marshal_vtbl}, 1}; struct unk_impl unk_obj = {{&unk_vtbl}, 1}; struct unk_impl unk_agile_obj = {{&unk_agile_vtbl}, 1}; + struct unk_ctx_impl *unk_ctx_impl; enum AgileReferenceOptions option; IAgileReference *agile_reference; RO_INIT_TYPE from_type, to_type; @@ -624,6 +765,42 @@ static void test_RoGetAgileReference(void) winetest_pop_context(); } } + + /* Tests specific to delayed marshaling */ + for (from_type = RO_INIT_SINGLETHREADED; from_type <= RO_INIT_MULTITHREADED; from_type++) + { + winetest_push_context("from_type=%d", from_type); + RoInitialize(from_type); + + hr = unk_ctx_impl_create(&unknown); + ok(hr == S_OK, "got hr %#lx\n", hr); + + unk_ctx_impl = impl_unk_ctx_impl_from_IUnknown(unknown); + hr = RoGetAgileReference(AGILEREFERENCE_DELAYEDMARSHAL, &IID_IUnknown, unknown, &agile_reference); + ok(hr == S_OK, "got hr %#lx\n", hr); + EXPECT_REF(unknown, 2); + + for (to_type = RO_INIT_SINGLETHREADED; to_type <= RO_INIT_MULTITHREADED; to_type++) + { + struct test_agile_resolve_context_params params = {from_type, to_type, agile_reference}; + + winetest_push_context("to_type=%d", to_type); + unk_ctx_impl->todo = TRUE; + thread = CreateThread(NULL, 0, test_agile_resolve_context, ¶ms, 0, NULL); + flush_events(); + ret = WaitForSingleObject(thread, 100); + ok(!ret, "got ret %lu\n", ret); + CloseHandle(thread); + unk_ctx_impl->todo = FALSE; + winetest_pop_context(); + } + + IAgileReference_Release(agile_reference); + EXPECT_REF(unknown, 1); + IUnknown_Release(unknown); + RoUninitialize(); + winetest_pop_context(); + } }
static void test_RoGetErrorReportingFlags(void) diff --git a/include/roapi.h b/include/roapi.h index bcaa8e9fea0..18baf9e2017 100644 --- a/include/roapi.h +++ b/include/roapi.h @@ -46,6 +46,7 @@ HRESULT WINAPI RoActivateInstance(HSTRING classid, IInspectable **instance); HRESULT WINAPI RoGetActivationFactory(HSTRING classid, REFIID iid, void **class_factory); HRESULT WINAPI RoInitialize(RO_INIT_TYPE type); void WINAPI RoUninitialize(void); +HRESULT WINAPI RoGetApartmentIdentifier(UINT64 *identifier);
#ifdef __cplusplus }
From: Vibhav Pant vibhavp@gmail.com
--- dlls/ole32/tests/compobj.c | 283 +++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+)
diff --git a/dlls/ole32/tests/compobj.c b/dlls/ole32/tests/compobj.c index 2c4cc566f3a..811b7936530 100644 --- a/dlls/ole32/tests/compobj.c +++ b/dlls/ole32/tests/compobj.c @@ -72,6 +72,7 @@ DEFINE_EXPECT(PostUninitialize);
/* functions that are not present on all versions of Windows */ static HRESULT (WINAPI * pCoGetObjectContext)(REFIID riid, LPVOID *ppv); +static HRESULT (WINAPI *pCoDisconnectContext)(DWORD); static HRESULT (WINAPI * pCoSwitchCallContext)(IUnknown *pObject, IUnknown **ppOldObject); static HRESULT (WINAPI * pCoGetContextToken)(ULONG_PTR *token); static HRESULT (WINAPI * pCoGetApartmentType)(APTTYPE *type, APTTYPEQUALIFIER *qualifier); @@ -4065,6 +4066,7 @@ static void init_funcs(void) HMODULE hkernel32 = GetModuleHandleA("kernel32");
pCoGetObjectContext = (void*)GetProcAddress(hOle32, "CoGetObjectContext"); + pCoDisconnectContext = (void *)GetProcAddress(hOle32, "CoDisconnectContext"); pCoSwitchCallContext = (void*)GetProcAddress(hOle32, "CoSwitchCallContext"); pCoGetContextToken = (void*)GetProcAddress(hOle32, "CoGetContextToken"); pCoGetApartmentType = (void*)GetProcAddress(hOle32, "CoGetApartmentType"); @@ -4472,6 +4474,286 @@ static void test_oletlsdata(void) "Unexpected flags %#lx.\n", flags); }
+void flush_events(void) +{ + int diff = 200; + DWORD time; + MSG msg; + + time = GetTickCount() + diff; + while (diff > 0) + { + if (MsgWaitForMultipleObjects(0, NULL, FALSE, 100, QS_ALLINPUT) == WAIT_TIMEOUT) + break; + while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) + DispatchMessageA(&msg); + diff = time - GetTickCount(); + } +} + +/* A simple-enough interface that inherits from IUnknown, to test IContextCallback. */ +struct root_storage +{ + IRootStorage IRootStorage_iface; + ULONG_PTR token; + APTTYPE type; + APTTYPEQUALIFIER qualifier; + LONG ref; +}; + +static inline struct root_storage *impl_from_IRootStorage(IRootStorage *impl) +{ + return CONTAINING_RECORD(impl, struct root_storage, IRootStorage_iface); +} + +static HRESULT WINAPI root_storage_QueryInterface(IRootStorage *iface, const GUID *iid, void **out) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + + if (IsEqualGUID(iid, &IID_IUnknown) || IsEqualGUID(iid, &IID_IRootStorage)) + { + IRootStorage_AddRef((*out = &impl->IRootStorage_iface)); + return S_OK; + } + + *out = NULL; + return E_NOINTERFACE; +} + +static ULONG WINAPI root_storage_AddRef(IRootStorage *iface) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + return InterlockedIncrement(&impl->ref); +} + +static ULONG WINAPI root_storage_Release(IRootStorage *iface) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + ULONG ref = InterlockedDecrement(&impl->ref); + + if (!ref) free(impl); + return ref; +} + +static HRESULT WINAPI root_storage_SwitchToFile(IRootStorage *iface, LPOLESTR file) +{ + struct root_storage *impl = impl_from_IRootStorage(iface); + APTTYPEQUALIFIER qualifier; + ULONG_PTR token; + APTTYPE type; + HRESULT hr; + + hr = CoGetContextToken(&token); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* The call takes place inside the context the object was created in. */ + ok(token == impl->token, "got token %#Ix != %#Ix\n", token, impl->token); + + hr = CoGetApartmentType(&type, &qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + ok(type == impl->type, "got type %d != %d\n", type, impl->type); + ok(qualifier == impl->qualifier, "got qualifier %d != %d\n", qualifier, impl->qualifier); + + return S_OK; +} + +static const struct IRootStorageVtbl root_storage_vtbl = +{ + root_storage_QueryInterface, + root_storage_AddRef, + root_storage_Release, + root_storage_SwitchToFile +}; + +static HRESULT root_storage_create(IRootStorage **out, ULONG_PTR token, APTTYPE type, APTTYPEQUALIFIER qualifier) +{ + struct root_storage *impl; + + if (!(impl = calloc(1, sizeof(*impl)))) return E_OUTOFMEMORY; + impl->IRootStorage_iface.lpVtbl = &root_storage_vtbl; + impl->token = token; + impl->type = type; + impl->qualifier = qualifier; + impl->ref = 1; + *out = &impl->IRootStorage_iface; + return S_OK; +} + +struct context_callback_data +{ + APTTYPE from_type; + APTTYPEQUALIFIER from_qualifier; + ULONG_PTR from_token; + IStream *marshaled; +}; + +HRESULT CALLBACK create_object_callback(ComCallData *params) +{ + struct context_callback_data *data; + APTTYPEQUALIFIER qualifier; + ULONG_PTR cur_token; + IRootStorage *obj; + APTTYPE type; + ULONG count; + HRESULT hr; + + ok(!!params, "got params %p\n", params); + data = params->pUserDefined; + ok(!!data, "got data %p\n", data); + + hr = CoGetContextToken(&cur_token); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* We are now inside a new context, but the same apartment. */ + ok(cur_token != data->from_token, "got cur_token %#Ix\n", cur_token); + + hr = CoGetApartmentType(&type, &qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + ok(type == data->from_type, "got type %d != %d\n", type, data->from_type); + ok(qualifier == data->from_qualifier, "got qualifier %d != %d\n", qualifier, data->from_qualifier); + + hr = root_storage_create(&obj, cur_token, type, qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoMarshalInterThreadInterfaceInStream(&IID_IRootStorage, (IUnknown *)obj, &data->marshaled); + ok(hr == S_OK, "got hr %#lx\n", hr); + + count = IRootStorage_Release(obj); + ok(count == 4, "got count %lu\n", count); + + return S_OK; +} + +HRESULT CALLBACK disconnect_context_callback(ComCallData *data) +{ + return pCoDisconnectContext(100); +} + +struct test_marshaled_data +{ + IStream *marshaled; + COINIT from_type; + COINIT to_type; +}; + +DEFINE_GUID(CLSID_ContextSwitcher, 0x0000034e,0x0000,0x0000,0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46); +static DWORD CALLBACK test_marshaled(void *param) +{ + struct test_marshaled_data *data = param; + WCHAR *file = wcsdup(L"foo"); + IStream *stream = data->marshaled; + IRootStorage *obj; + ULONG count; + HRESULT hr; + + winetest_push_context("from_type=%#x, to_type=%#x", data->from_type, data->to_type); + + hr = CoInitializeEx(NULL, data->to_type); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoGetInterfaceAndReleaseStream(stream, &IID_IRootStorage, (void **)&obj); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = IRootStorage_SwitchToFile(obj, file); + ok(hr == S_OK, "got hr %#lx\n", hr); + + count = IRootStorage_Release(obj); + ok(count == 0, "got count %lu\n", count); + + winetest_pop_context(); + + free(file); + return 0; +} + +static void test_IContextCallback(void) +{ + static const COINIT apt_types[] = {COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED}; + WCHAR *file = wcsdup(L"foo"); + HRESULT hr; + int i, j; + + for (i = 0; i < ARRAY_SIZE(apt_types); i++) + { + for (j = 0; j < ARRAY_SIZE(apt_types); j++) + { + struct test_marshaled_data data = {NULL, apt_types[i], apt_types[j]}; + struct context_callback_data callback_data = {0}; + ComCallData call_data = {0}; + IContextCallback *context; + IRootStorage *obj; + HANDLE thread; + DWORD ret; + + winetest_push_context("from_type=%#x, to_type=%#x", apt_types[i], apt_types[j]); + hr = CoInitializeEx(NULL, apt_types[i]); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoGetApartmentType(&callback_data.from_type, &callback_data.from_qualifier); + ok(hr == S_OK, "got hr %#lx\n", hr); + + hr = CoGetContextToken(&callback_data.from_token); + ok(hr == S_OK, "got hr %#lx\n", hr); + + /* Create a new context inside this apartment. */ + hr = CoCreateInstance(&CLSID_ContextSwitcher, NULL, CLSCTX_INPROC_SERVER, &IID_IContextCallback, (void **)&context); + todo_wine ok(hr == S_OK, "got hr %#lx\n", hr); + if (FAILED(hr)) + { + CoUninitialize(); + winetest_pop_context(); + continue; + } + + /* Use ContextCallback to create and marshal and object bound to the new context. + * All methods in this object will be called inside the new context. */ + call_data.pUserDefined = &callback_data; + hr = IContextCallback_ContextCallback(context, create_object_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + data.marshaled = callback_data.marshaled; + thread = CreateThread(NULL, 0, test_marshaled, &data, 0, NULL); + flush_events(); + ret = WaitForSingleObject(thread, 100); + ok(!ret, "got ret %lu\n", ret); + CloseHandle(thread); + + hr = IContextCallback_ContextCallback(context, create_object_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + hr = CoGetInterfaceAndReleaseStream(callback_data.marshaled, &IID_IRootStorage, (void **)&obj); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* Calls within the same apartment through the marshaled pointer will *also* be wrapped within a + * context-switch. */ + hr = IRootStorage_SwitchToFile(obj, file); + ok(hr == S_OK, "got hr %#lx\n", hr); + ret = IRootStorage_Release(obj); + ok(ret == 0, "got ret %lu\n", ret); + + if (pCoDisconnectContext) + { + hr = IContextCallback_ContextCallback(context, create_object_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + hr = CoGetInterfaceAndReleaseStream(callback_data.marshaled, &IID_IRootStorage, (void **)&obj); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* Disonnect the proxy from its context. */ + hr = IContextCallback_ContextCallback(context, disconnect_context_callback, &call_data, &IID_IContextCallback, 5, NULL); + ok(hr == S_OK, "got hr %#lx\n", hr); + /* All calls should now return RPC_E_DISCONNECTED. */ + hr = IRootStorage_SwitchToFile(obj, file); + ok(hr == RPC_E_DISCONNECTED, "got hr %#lx\n", hr); + ret = IRootStorage_Release(obj); + ok(ret == 0, "got ret %lu\n", ret); + } + else + todo_wine win_skip("CoDisconnectContext not available\n"); + + ret = IContextCallback_Release(context); + ok(ret == 0, "got ret %lu\n", ret); + CoUninitialize(); + winetest_pop_context(); + } + } + + free(file); +} + START_TEST(compobj) { init_funcs(); @@ -4524,6 +4806,7 @@ START_TEST(compobj) test_mta_usage(); test_CoCreateInstanceFromApp(); test_call_cancellation(); + test_IContextCallback();
DeleteFileA( testlib ); }
Jinoh Kang (@iamahuman) commented about dlls/combase/tests/roapi.c:
- APTTYPEQUALIFIER qualifier;
 - ULONG_PTR cur_ctx;
 - UINT64 cur_id;
 - APTTYPE type;
 - HRESULT hr;
 - hr = CoGetContextToken(&cur_ctx);
 - ok_(__FILE__, line)(hr == S_OK, "CoGetContextToken failed, got hr %#lx\n", hr);
 - /* As this object has apartment-affinity, its methods can only be called from the apartment and context it was
 * created in. */- todo_wine_if(impl->todo) ok_(__FILE__, line)(cur_ctx == impl->context, "got cur_ctx %#Ix != %#Ix\n", cur_ctx, impl->context);
 - hr = CoGetApartmentType(&type, &qualifier);
 - ok_(__FILE__, line)(hr == S_OK, "CoGetApartmentType failed, got hr %#lx\n", hr);
 - todo_wine_if(impl->todo) ok_(__FILE__, line)(type == impl->type, "got type %d\n", type);
 
Does RoGetApartmentIdentifier exist on Windows 7?
On Tue Oct 28 14:25:01 2025 +0000, Jinoh Kang wrote:
Does RoGetApartmentIdentifier exist on Windows 7?
Combase doesn't exist on win7. Resolving
Jinoh Kang (@iamahuman) commented about dlls/combase/tests/roapi.c:
ok(hr == S_OK, "got hr %#lx\n", hr);unk_ctx_impl = impl_unk_ctx_impl_from_IUnknown(unknown);hr = RoGetAgileReference(AGILEREFERENCE_DELAYEDMARSHAL, &IID_IUnknown, unknown, &agile_reference);ok(hr == S_OK, "got hr %#lx\n", hr);EXPECT_REF(unknown, 2);for (to_type = RO_INIT_SINGLETHREADED; to_type <= RO_INIT_MULTITHREADED; to_type++){struct test_agile_resolve_context_params params = {from_type, to_type, agile_reference};winetest_push_context("to_type=%d", to_type);unk_ctx_impl->todo = TRUE;thread = CreateThread(NULL, 0, test_agile_resolve_context, ¶ms, 0, NULL);flush_events();ret = WaitForSingleObject(thread, 100);
This timeout is too short to defend against flakiness, especially since this wait is always supposed to complete.
```suggestion:-0+0 ret = WaitForSingleObject(thread, INFINITE); ```
Jinoh Kang (@iamahuman) commented about dlls/combase/tests/roapi.c:
winetest_pop_context(); } }
- /* Tests specific to delayed marshaling */
 - for (from_type = RO_INIT_SINGLETHREADED; from_type <= RO_INIT_MULTITHREADED; from_type++)
 - {
 winetest_push_context("from_type=%d", from_type);RoInitialize(from_type);
Anything mutating global state that persist across test functions should be error checked IMO.
If initialization fails for some reason, further RoUninitialize() will be unbalanced, leading to subtle bugs.
Jinoh Kang (@iamahuman) commented about dlls/combase/tests/roapi.c:
winetest_push_context("to_type=%d", to_type);unk_ctx_impl->todo = TRUE;thread = CreateThread(NULL, 0, test_agile_resolve_context, ¶ms, 0, NULL);flush_events();ret = WaitForSingleObject(thread, 100);ok(!ret, "got ret %lu\n", ret);CloseHandle(thread);unk_ctx_impl->todo = FALSE;winetest_pop_context();}IAgileReference_Release(agile_reference);EXPECT_REF(unknown, 1);IUnknown_Release(unknown);RoUninitialize();
Ditto.