Compare commits

...

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
7ecb3939d1 Fix CmdPal extension E_NOINTERFACE activation failure and access violation
- ExtensionWrapper.cs: Add named HRESULT constants (HResultNoInterface,
  HResultPathNotFound) and IID_IUnknown field. Fix critical bug where
  code continued past a non-E_PATH_NOT_FOUND CoCreateInstance failure,
  causing a null-pointer access violation. Add E_NOINTERFACE→IID_IUnknown
  retry fallback for Windows 23H2 compatibility. Use safe MarshalInspectable
  path after IUnknown fallback to avoid wrong-vtable access.

- TopLevelCommandManager.cs: Add IsRunning() check before constructing
  CommandProviderWrapper to avoid the confusing 'You forgot to call
  StartExtensionAsync' exception. Log a clear, actionable message that
  mentions Windows 23H2 marshaling limitations.

- ExtensionInstanceManager.cs: Fix IClassFactory.CreateInstance to accept
  IID_IExtension and IID_IInspectable in addition to IID_IUnknown and the
  class CLSID. This prevents spurious E_NOINTERFACE when COM delivers the
  WinRT interface IID directly to CreateInstance (observed on some Windows
  builds).

Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/1cfc86cd-d682-4f81-950d-616843143067

Co-authored-by: MuyuanMS <116717757+MuyuanMS@users.noreply.github.com>
2026-04-29 11:49:52 +00:00
copilot-swe-agent[bot]
9a1727ba83 Initial plan 2026-04-29 08:52:30 +00:00
3 changed files with 85 additions and 14 deletions

View File

@@ -18,6 +18,13 @@ public class ExtensionWrapper : IExtensionWrapper
{
private const int HResultRpcServerNotRunning = -2147023174;
// COM/WinRT HRESULT constants used during extension activation
private const int HResultNoInterface = unchecked((int)0x80004002); // E_NOINTERFACE
private const int HResultPathNotFound = unchecked((int)0x80070003); // E_PATH_NOT_FOUND (HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))
// IID_IUnknown - the base COM interface always accepted by any well-formed class factory
private static readonly Guid IID_IUnknown = new("00000000-0000-0000-C000-000000000046");
private readonly string _appUserModelId;
private readonly string _extensionId;
@@ -111,28 +118,68 @@ public class ExtensionWrapper : IExtensionWrapper
var extensionPtr = (void*)nint.Zero;
try
{
// -2147024809: E_INVALIDARG
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
// First attempt: request IExtension directly.
// On Windows 11 23H2 (Build 22631), CoCreateInstance with a custom
// WinRT interface IID (like IExtension) may return E_NOINTERFACE because
// the OS cannot marshal the interface cross-process without a registered
// proxy/stub. Newer Windows builds (24H2+) handle this automatically via
// WinRT metadata. In that case, we fall back to IID_IUnknown so the
// server can return an IInspectable CCW, which is always marshalable.
var extensionIid = typeof(IExtension).GUID;
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out extensionPtr);
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, extensionIid, out extensionPtr);
if (hr.Value == -2147024893)
var usedIUnknownFallback = false;
if (hr.Value == HResultNoInterface)
{
// On Windows 23H2, the OS may be unable to marshal the IExtension
// WinRT interface cross-process. Retry with IID_IUnknown so the
// server can return an IInspectable CCW (marshalable on all Windows
// versions). We then QI for IExtension from the returned pointer.
Logger.LogWarning($"CoCreateInstance for {ExtensionDisplayName} returned E_NOINTERFACE for IID_IExtension (hr=0x{hr.Value:X8}). " +
$"Retrying with IID_IUnknown as a Windows 23H2 compatibility fallback.");
extensionPtr = (void*)nint.Zero;
hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, IID_IUnknown, out extensionPtr);
usedIUnknownFallback = true;
}
if (hr.Value == HResultPathNotFound)
{
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
}
else if (hr.Value != 0)
{
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value}");
// All other failures — log and bail out. Do NOT fall through to
// MarshalInterface below, which would dereference a null pointer and
// cause an access violation.
Logger.LogError($"Failed to activate {ExtensionDisplayName}: hr=0x{hr.Value:X8}. " +
$"On Windows 23H2 this may indicate that WinRT cross-process interface " +
$"marshaling is unsupported for custom interfaces without a registered proxy/stub.");
return;
}
// Marshal.ThrowExceptionForHR(hr);
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
if (usedIUnknownFallback)
{
// extensionPtr is an IUnknown/IInspectable cross-process proxy.
// Wrap it as IInspectable and then try to QI for IExtension.
// On Windows 23H2, this QI may fail (no proxy/stub for IExtension),
// resulting in a null _extensionObject. On newer Windows the QI
// should succeed via WinRT metadata-based marshaling.
var inspectable = MarshalInspectable<object>.FromAbi((nint)extensionPtr);
_extensionObject = inspectable as IExtension;
if (_extensionObject == null)
{
Logger.LogError($"Extension {ExtensionDisplayName} does not expose IExtension across the COM process boundary. " +
$"On Windows 23H2 (Build 22631) and earlier, custom WinRT interfaces cannot be marshaled " +
$"cross-process without a registered proxy/stub. The extension will not be available.");
}
}
else
{
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
}
}
catch (Exception e)
{

View File

@@ -456,6 +456,18 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
try
{
await startTask.WaitAsync(ExtensionStartTimeout, ct).ConfigureAwait(false);
// If the extension server failed to activate (e.g. E_NOINTERFACE on older Windows),
// IsRunning() will be false and CommandProviderWrapper will throw. Check first so we
// can log a more actionable message than the generic "You forgot to call StartExtensionAsync".
if (!extension.IsRunning())
{
Logger.LogError($"Extension {extension.PackageFullName} did not activate after {sw.ElapsedMilliseconds} ms. " +
$"This can happen on Windows 11 23H2 (Build 22631) and earlier due to WinRT cross-process " +
$"interface marshaling limitations for custom interfaces. Check extension logs for HRESULT details.");
return ExtensionStartResult.Failed(extension);
}
Logger.LogInfo($"Started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
return ExtensionStartResult.Started(extension, new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache));
}

View File

@@ -28,6 +28,15 @@ internal sealed partial class ExtensionInstanceManager : IClassFactory
private static readonly Guid IID_IUnknown = Guid.Parse("00000000-0000-0000-C000-000000000046");
// IInspectable is the WinRT base interface (always marshalable cross-process on all Windows versions).
private static readonly Guid IID_IInspectable = Guid.Parse("AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90");
// IExtension's WinRT interface IID. On Windows 11 23H2 (Build 22631), CoCreateInstance may
// invoke CreateInstance with this IID directly. We handle it by returning the IInspectable CCW,
// which COM on all Windows versions can marshal cross-process. On 24H2+ the OS uses WinRT
// metadata (winmd) to marshal custom interfaces transparently.
private static readonly Guid IID_IExtension = typeof(IExtension).GUID;
#pragma warning restore SA1310 // Field names should not contain underscore
private readonly Func<IExtension> _createExtension;
@@ -60,9 +69,12 @@ internal sealed partial class ExtensionInstanceManager : IClassFactory
Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION);
}
if (riid == _clsid || riid == IID_IUnknown)
if (riid == _clsid || riid == IID_IUnknown || riid == IID_IInspectable || riid == IID_IExtension)
{
// Create the instance of the .NET object
// Create the instance of the .NET object and return it as IInspectable.
// Returning IInspectable ensures the CCW is marshalable cross-process on all
// Windows versions (including 23H2), since IInspectable has built-in OS support.
// The client will resolve the specific WinRT interface (IExtension) via CsWinRT.
var managed = _createExtension();
var ins = MarshalInspectable<object>.FromManaged(managed);
ppvObject = ins;