All managed objects other than those derived from ServicedComponent, when exposed to COM, behave as if they have aggregated the free threaded marshaler (FTM). In other words, they can be called on any thread without any cross-apartment marshaling.
Although managed objects act as if they aggregate the FTM, they don’t actually aggregate it because that would be inefficient. Instead, the CLR implements IMarshal on behalf of all managed objects that don’t provide their own implementation. The IMarshal implementation we provide is similar to the one the FTM would do.
If a managed object implements its own IMarshal, it should be aware of a quirk of OLE32 on some versions of the operating system. The IStream argument passed by OLE when calling your IMarshal interface is sometimes allocated on the stack. So you better be done with that IStream before you return to OLE. Of course you are done with it, but the COM Interop layer might hold onto it until a garbage collection recovers it. To avoid the subsequent heap corruption and crash, you should explicitly call ReleaseComObject on the IStream argument before returning.
Obviously you are better off allowing the CLR to implement IMarshal on your behalf!
The exception to all this free-threaded behavior is any type that inherits from System.EnterpriseServices.ServicedComponent. The runtime treats such objects as if they really were unmanaged COM objects. All calls from managed clients to ServicedComponent servers will check the apartment and COM+ context. If these don't match, the runtime pipes the call through COM so that any thread marshaling or context transitions are correctly performed.
Of course, this is a special section of the type hierarchy. And the fact that such types obey COM rules is more of an implementation detail than it is a design point. All other managed objects are available directly from all COM apartments and COM+ contexts.
Furthermore, AppDomains and COM apartments are completely orthogonal.