Fix credential event callback reference lifetime

This commit is contained in:
Ryan Newington
2026-08-30 15:42:48 +10:00
parent b0a9886708
commit 7fad9cb351
18 changed files with 1105 additions and 8 deletions
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("2C87FFBB-6497-4D1A-8291-74B5DB3A0C3A")]
internal sealed class AbiTestCredentialProvider3 : CredentialProviderBase
{
public AbiTestCredentialProvider3()
{
this.Field = new SmallLabelControl("message", "Callback ABI test");
this.Logo = new CredentialProviderLogoControl("logo", "Callback ABI logo");
}
public SmallLabelControl Field { get; }
public CredentialProviderLogoControl Logo { get; }
public AbiTestCredentialTile3 Tile { get; private set; }
public override bool IsUsageScenarioSupported(UsageScenario cpus, CredUIWinFlags dwFlags)
{
return cpus == UsageScenario.CredUI;
}
public override IEnumerable<ControlBase> GetControls(UsageScenario cpus)
{
return new ControlBase[] { this.Field, this.Logo };
}
public override bool ShouldIncludeUserTile(CredentialProviderUser user)
{
return false;
}
public override bool ShouldIncludeGenericTile()
{
return true;
}
public override CredentialTile CreateGenericTile()
{
this.Tile = new AbiTestCredentialTile3(this);
return this.Tile;
}
public override CredentialTile2 CreateUserTile(CredentialProviderUser user)
{
return null;
}
}
}
@@ -0,0 +1,26 @@
using System;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
internal sealed class AbiTestCredentialTile3 : CredentialTile3
{
public AbiTestCredentialTile3(CredentialProviderBase credentialProvider) : base(credentialProvider)
{
}
public bool ThrowOnLoad { get; set; }
public override void OnLoad()
{
if (this.ThrowOnLoad)
{
throw new InvalidOperationException("Callback ABI test OnLoad failure");
}
}
protected override CredentialResponseBase GetCredentials()
{
return null;
}
}
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int CredentialAdviseDelegate(IntPtr instance, IntPtr credentialEvents);
}
@@ -0,0 +1,295 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Threading;
using NUnit.Framework;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[TestFixture]
[NonParallelizable]
[Apartment(ApartmentState.STA)]
public class CredentialCallbackAbiTests
{
[Test]
public void RawEventInterfacesPreserveQueryInterfaceIdentityAndReferenceCounts()
{
RawCredentialEvents events = new RawCredentialEvents(3);
try
{
AssertIUnknownIdentity(events, events.Events1Interface);
AssertIUnknownIdentity(events, events.Events2Interface);
AssertIUnknownIdentity(events, events.Events3Interface);
RawQueryInterfaceDelegate queryInterface = GetRawMethod<RawQueryInterfaceDelegate>(events.Events1Interface, 0);
Guid events2Id = RawCredentialEventAbi.ICredentialProviderCredentialEvents2;
int beforeEvents2Query = events.CurrentReferenceCount;
Assert.That(queryInterface(events.Events1Interface, ref events2Id, out IntPtr events2Pointer), Is.EqualTo(CredentialProviderAbi.S_OK));
Assert.That(events2Pointer, Is.EqualTo(events.Events2Interface));
Assert.That(events.CurrentReferenceCount, Is.EqualTo(beforeEvents2Query + 1));
ReleaseRaw(events2Pointer);
Assert.That(events.CurrentReferenceCount, Is.EqualTo(beforeEvents2Query));
Guid unsupportedId = new Guid("757DA58F-742C-4840-9692-10A9D6392E93");
int beforeUnsupportedQuery = events.CurrentReferenceCount;
Assert.That(queryInterface(events.Events1Interface, ref unsupportedId, out IntPtr unsupportedPointer), Is.EqualTo(RawCredentialEventAbi.E_NOINTERFACE));
Assert.That(unsupportedPointer, Is.EqualTo(IntPtr.Zero));
Assert.That(events.CurrentReferenceCount, Is.EqualTo(beforeUnsupportedQuery));
}
finally
{
events.Dispose();
}
CollectComWrappers();
Assert.That(events.CurrentReferenceCount, Is.EqualTo(0));
}
[Test]
public void FailedAdviseReleasesCallbackState()
{
RawCredentialEvents events = ExerciseFailedAdvise();
CollectComWrappers();
Assert.That(events.CurrentReferenceCount, Is.EqualTo(0));
}
[Test]
public void RepeatedAdviseReplacesTheExistingCallbackReference()
{
RawCredentialEvents events;
using (CredentialCallbackScenario scenario = new CredentialCallbackScenario(2))
{
events = scenario.Events;
Assert.That(scenario.Advise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterFirstAdvise = events.CurrentReferenceCount;
Assert.That(scenario.Advise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterSecondAdvise = events.CurrentReferenceCount;
Assert.That(afterSecondAdvise, Is.EqualTo(afterFirstAdvise));
Assert.That(scenario.UnAdvise(), Is.EqualTo(CredentialProviderAbi.S_OK));
Assert.That(events.CurrentReferenceCount, Is.LessThan(afterSecondAdvise));
}
CollectComWrappers();
Assert.That(events.CurrentReferenceCount, Is.EqualTo(0));
}
private static RawCredentialEvents ExerciseFailedAdvise()
{
RawCredentialEvents events;
using (CredentialCallbackScenario scenario = new CredentialCallbackScenario(2))
{
events = scenario.Events;
scenario.Provider.Tile.ThrowOnLoad = true;
int releaseCallsBeforeAdvise = events.ReleaseCallCount;
Assert.That(scenario.Advise(), Is.EqualTo(CredentialProviderAbi.E_FAIL));
Assert.That(events.ReleaseCallCount, Is.GreaterThan(releaseCallsBeforeAdvise));
scenario.Provider.Field.Label = "No callback after failed Advise";
Assert.That(events.SetFieldStringCallCount, Is.EqualTo(0));
}
return events;
}
[Test]
public void Events1SetFieldStringPreservesSlotArgumentsAndLifetimePhases()
{
RawCredentialEvents events;
using (CredentialCallbackScenario scenario = new CredentialCallbackScenario(1))
{
events = scenario.Events;
int beforeAdvise = scenario.Events.CurrentReferenceCount;
Assert.That(scenario.Advise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterAdvise = scenario.Events.CurrentReferenceCount;
SmallLabelControl field = scenario.Provider.Tile.Controls.GetControl<SmallLabelControl>("message");
field.Label = "Updated through Events1";
int duringUpdate = scenario.Events.CurrentReferenceCount;
Assert.Multiple(() =>
{
Assert.That(scenario.Events.SetFieldStringCallCount, Is.EqualTo(1));
Assert.That(scenario.Events.LastCredential, Is.Not.EqualTo(IntPtr.Zero));
Assert.That(scenario.Events.LastFieldId, Is.EqualTo(field.Id));
Assert.That(scenario.Events.LastString, Is.EqualTo("Updated through Events1"));
Assert.That(scenario.Events.Events2QueryCount, Is.GreaterThan(0));
Assert.That(scenario.Events.Events3QueryCount, Is.EqualTo(0));
});
int releaseCallsBeforeUnAdvise = scenario.Events.ReleaseCallCount;
Assert.That(scenario.UnAdvise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterUnAdvise = scenario.Events.CurrentReferenceCount;
AssertLifetimePhases(scenario.Events, beforeAdvise, afterAdvise, duringUpdate, afterUnAdvise, releaseCallsBeforeUnAdvise);
}
CollectComWrappers();
Assert.That(events.CurrentReferenceCount, Is.EqualTo(0));
}
[Test]
public void Events2FieldUpdatesPreserveSlotsArgumentsAndLifetimePhases()
{
RawCredentialEvents events;
using (CredentialCallbackScenario scenario = new CredentialCallbackScenario(2))
{
events = scenario.Events;
int beforeAdvise = scenario.Events.CurrentReferenceCount;
Assert.That(scenario.Advise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterAdvise = scenario.Events.CurrentReferenceCount;
SmallLabelControl field = scenario.Provider.Tile.Controls.GetControl<SmallLabelControl>("message");
scenario.Provider.Tile.BeginBulkFieldUpdate();
field.Options = FieldOptions.Email;
scenario.Provider.Tile.EndBulkFieldUpdate();
int duringUpdate = scenario.Events.CurrentReferenceCount;
Assert.Multiple(() =>
{
Assert.That(scenario.Events.BeginFieldUpdatesCallCount, Is.EqualTo(1));
Assert.That(scenario.Events.EndFieldUpdatesCallCount, Is.EqualTo(1));
Assert.That(scenario.Events.SetFieldOptionsCallCount, Is.EqualTo(1));
Assert.That(scenario.Events.LastCredential, Is.Not.EqualTo(IntPtr.Zero));
Assert.That(scenario.Events.LastFieldId, Is.EqualTo(field.Id));
Assert.That(scenario.Events.LastUIntValue, Is.EqualTo((uint)FieldOptions.Email));
Assert.That(scenario.Events.Events2QueryCount, Is.GreaterThan(0));
Assert.That(scenario.Events.Events3QueryCount, Is.EqualTo(0));
});
int releaseCallsBeforeUnAdvise = scenario.Events.ReleaseCallCount;
Assert.That(scenario.UnAdvise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterUnAdvise = scenario.Events.CurrentReferenceCount;
AssertLifetimePhases(scenario.Events, beforeAdvise, afterAdvise, duringUpdate, afterUnAdvise, releaseCallsBeforeUnAdvise);
}
CollectComWrappers();
Assert.That(events.CurrentReferenceCount, Is.EqualTo(0));
}
[Test]
public void Events3BitmapBufferPreservesSlotArgumentsCallShapeAndLifetimePhases()
{
RawCredentialEvents events = ExerciseEvents3BitmapBuffer();
CollectComWrappers();
Assert.That(events.CurrentReferenceCount, Is.EqualTo(0));
}
private static RawCredentialEvents ExerciseEvents3BitmapBuffer()
{
RawCredentialEvents events;
using (CredentialCallbackScenario scenario = new CredentialCallbackScenario(3))
{
events = scenario.Events;
int beforeAdvise = scenario.Events.CurrentReferenceCount;
Assert.That(scenario.Advise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterAdvise = scenario.Events.CurrentReferenceCount;
CredentialProviderLogoControl logo = scenario.Provider.Tile.Controls.GetControl<CredentialProviderLogoControl>("logo");
using (Bitmap bitmap = new Bitmap(2, 2))
{
bitmap.SetPixel(0, 0, Color.Transparent);
bitmap.SetPixel(1, 0, Color.Red);
logo.Bitmap = bitmap;
}
int duringUpdate = scenario.Events.CurrentReferenceCount;
byte[] buffer = scenario.Events.LastBitmapBuffer;
Assert.Multiple(() =>
{
Assert.That(scenario.Events.SetFieldBitmapBufferCallCount, Is.EqualTo(1));
Assert.That(scenario.Events.LastCredential, Is.Not.EqualTo(IntPtr.Zero));
Assert.That(scenario.Events.LastFieldId, Is.EqualTo(logo.Id));
Assert.That(buffer, Is.Not.Null);
Assert.That(buffer.Length, Is.GreaterThan(8));
Assert.That(buffer[0], Is.EqualTo(0x89));
Assert.That(buffer[1], Is.EqualTo(0x50));
Assert.That(buffer[2], Is.EqualTo(0x4E));
Assert.That(buffer[3], Is.EqualTo(0x47));
Assert.That(buffer[4], Is.EqualTo(0x0D));
Assert.That(buffer[5], Is.EqualTo(0x0A));
Assert.That(buffer[6], Is.EqualTo(0x1A));
Assert.That(buffer[7], Is.EqualTo(0x0A));
Assert.That(scenario.Events.BitmapBufferReturnValuePointerWasPresent, Is.True);
Assert.That(scenario.Events.Events2QueryCount, Is.GreaterThan(0));
Assert.That(scenario.Events.Events3QueryCount, Is.GreaterThan(0));
});
int releaseCallsBeforeUnAdvise = scenario.Events.ReleaseCallCount;
Assert.That(scenario.UnAdvise(), Is.EqualTo(CredentialProviderAbi.S_OK));
int afterUnAdvise = scenario.Events.CurrentReferenceCount;
AssertLifetimePhases(scenario.Events, beforeAdvise, afterAdvise, duringUpdate, afterUnAdvise, releaseCallsBeforeUnAdvise);
}
return events;
}
private static void AssertLifetimePhases(RawCredentialEvents events, int beforeAdvise, int afterAdvise, int duringUpdate, int afterUnAdvise, int releaseCallsBeforeUnAdvise)
{
TestContext.WriteLine($"Callback reference counts: before Advise={beforeAdvise}, after Advise={afterAdvise}, during update={duringUpdate}, after UnAdvise={afterUnAdvise}; AddRef calls={events.AddRefCallCount}, Release calls={events.ReleaseCallCount}");
Assert.Multiple(() =>
{
Assert.That(beforeAdvise, Is.EqualTo(1));
Assert.That(afterAdvise, Is.GreaterThan(beforeAdvise));
Assert.That(duringUpdate, Is.GreaterThanOrEqualTo(afterAdvise));
Assert.That(afterUnAdvise, Is.LessThan(duringUpdate));
Assert.That(afterUnAdvise, Is.GreaterThanOrEqualTo(beforeAdvise));
Assert.That(events.ReleaseCallCount, Is.GreaterThan(releaseCallsBeforeUnAdvise));
});
}
private static void AssertIUnknownIdentity(RawCredentialEvents events, IntPtr source)
{
RawQueryInterfaceDelegate queryInterface = GetRawMethod<RawQueryInterfaceDelegate>(source, 0);
Guid unknownId = RawCredentialEventAbi.IUnknown;
int beforeQuery = events.CurrentReferenceCount;
Assert.That(queryInterface(source, ref unknownId, out IntPtr unknown), Is.EqualTo(CredentialProviderAbi.S_OK));
Assert.That(unknown, Is.EqualTo(events.Events1Interface));
Assert.That(events.CurrentReferenceCount, Is.EqualTo(beforeQuery + 1));
ReleaseRaw(unknown);
Assert.That(events.CurrentReferenceCount, Is.EqualTo(beforeQuery));
}
private static TDelegate GetRawMethod<TDelegate>(IntPtr interfacePointer, int slot) where TDelegate : class
{
IntPtr vtable = Marshal.ReadIntPtr(interfacePointer);
IntPtr method = Marshal.ReadIntPtr(vtable, checked(slot * IntPtr.Size));
return (TDelegate)(object)Marshal.GetDelegateForFunctionPointer(method, typeof(TDelegate));
}
private static void ReleaseRaw(IntPtr interfacePointer)
{
RawReferenceDelegate release = GetRawMethod<RawReferenceDelegate>(interfacePointer, 2);
release(interfacePointer);
}
private static void CollectComWrappers()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
}
@@ -0,0 +1,148 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
internal sealed class CredentialCallbackScenario : IDisposable
{
private readonly TestCredentialProviderUserArray users;
private readonly ComInterfacePointer credential;
private readonly CredentialAdviseDelegate advise;
private readonly CredentialUnAdviseDelegate unAdvise;
private bool advised;
private bool disposed;
public CredentialCallbackScenario(int maximumEventsVersion)
{
this.Provider = new AbiTestCredentialProvider3();
this.users = new TestCredentialProviderUserArray();
this.Events = new RawCredentialEvents(maximumEventsVersion);
try
{
this.InitializeProvider();
this.credential = this.CreateCredentialInterface();
this.advise = this.credential.GetMethod<CredentialAdviseDelegate>(RawCredentialEventAbi.CredentialAdviseSlot);
this.unAdvise = this.credential.GetMethod<CredentialUnAdviseDelegate>(RawCredentialEventAbi.CredentialUnAdviseSlot);
}
catch
{
this.credential?.Dispose();
this.Events.Dispose();
throw;
}
}
public AbiTestCredentialProvider3 Provider { get; }
public RawCredentialEvents Events { get; }
public int Advise()
{
this.ThrowIfDisposed();
int hresult = this.advise(this.credential.Value, this.Events.Events1Interface);
if (hresult == CredentialProviderAbi.S_OK)
{
this.advised = true;
}
return hresult;
}
public int UnAdvise()
{
this.ThrowIfDisposed();
int hresult = this.unAdvise(this.credential.Value);
if (hresult == CredentialProviderAbi.S_OK)
{
this.advised = false;
}
return hresult;
}
public void Dispose()
{
if (this.disposed)
{
return;
}
if (this.advised)
{
this.unAdvise(this.credential.Value);
this.advised = false;
}
this.credential.Dispose();
this.Events.Dispose();
this.disposed = true;
GC.KeepAlive(this.users);
GC.KeepAlive(this.Provider);
}
private void InitializeProvider()
{
using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(this.Provider, CredentialProviderAbi.ICredentialProvider))
{
SetUsageScenarioDelegate setUsageScenario = providerInterface.GetMethod<SetUsageScenarioDelegate>(CredentialProviderAbi.SetUsageScenarioSlot);
this.ThrowIfFailed(setUsageScenario(providerInterface.Value, (int)UsageScenario.CredUI, 0), "SetUsageScenario");
}
using (ComInterfacePointer setUserArrayInterface = ComInterfacePointer.Create(this.Provider, CredentialProviderAbi.ICredentialProviderSetUserArray))
using (ComInterfacePointer userArrayInterface = ComInterfacePointer.Create(this.users, CredentialProviderAbi.ICredentialProviderUserArray))
{
SetUserArrayDelegate setUserArray = setUserArrayInterface.GetMethod<SetUserArrayDelegate>(CredentialProviderAbi.SetUserArraySlot);
this.ThrowIfFailed(setUserArray(setUserArrayInterface.Value, userArrayInterface.Value), "SetUserArray");
}
}
private ComInterfacePointer CreateCredentialInterface()
{
using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(this.Provider, CredentialProviderAbi.ICredentialProvider))
{
GetCredentialCountDelegate getCredentialCount = providerInterface.GetMethod<GetCredentialCountDelegate>(CredentialProviderAbi.GetCredentialCountSlot);
GetCredentialAtDelegate getCredentialAt = providerInterface.GetMethod<GetCredentialAtDelegate>(CredentialProviderAbi.GetCredentialAtSlot);
this.ThrowIfFailed(getCredentialCount(providerInterface.Value, out uint count, out uint defaultCredential, out int autoLogonWithDefault), "GetCredentialCount");
if (count != 1)
{
throw new InvalidOperationException($"The callback ABI provider returned {count} credentials instead of one");
}
IntPtr credentialPointer = IntPtr.Zero;
try
{
this.ThrowIfFailed(getCredentialAt(providerInterface.Value, 0, out credentialPointer), "GetCredentialAt");
ComInterfacePointer result = ComInterfacePointer.TakeOwnership(credentialPointer);
credentialPointer = IntPtr.Zero;
return result;
}
finally
{
if (credentialPointer != IntPtr.Zero)
{
Marshal.Release(credentialPointer);
}
}
}
}
private void ThrowIfFailed(int hresult, string operation)
{
if (hresult != CredentialProviderAbi.S_OK)
{
throw new COMException($"{operation} failed", hresult);
}
}
private void ThrowIfDisposed()
{
if (this.disposed)
{
throw new ObjectDisposedException(nameof(CredentialCallbackScenario));
}
}
}
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int CredentialUnAdviseDelegate(IntPtr instance);
}
@@ -0,0 +1,39 @@
using System;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
internal static class RawCredentialEventAbi
{
public static readonly Guid IUnknown = new Guid("00000000-0000-0000-C000-000000000046");
public static readonly Guid ICredentialProviderCredentialEvents = new Guid("FA6FA76B-66B7-4B11-95F1-86171118E816");
public static readonly Guid ICredentialProviderCredentialEvents2 = new Guid("B53C00B6-9922-4B78-B1F4-DDFE774DC39B");
public static readonly Guid ICredentialProviderCredentialEvents3 = new Guid("2D8DEEB8-1322-4973-8DF9-B282F2468290");
public const int CredentialAdviseSlot = 3;
public const int CredentialUnAdviseSlot = 4;
public const int SetFieldStringSlot = 5;
public const int BeginFieldUpdatesSlot = 13;
public const int EndFieldUpdatesSlot = 14;
public const int SetFieldOptionsSlot = 15;
public const int SetFieldBitmapBufferSlot = 16;
public const int Events1SlotCount = 13;
public const int Events2SlotCount = 16;
public const int Events3SlotCount = 17;
public const int E_NOINTERFACE = unchecked((int)0x80004002);
public const int E_POINTER = unchecked((int)0x80004003);
}
}
@@ -0,0 +1,10 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
// The trailing returnValue pointer characterizes the current CLR projection without PreserveSig.
// It does not establish the native ABI of the undocumented Windows Events3 interface.
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int RawCredentialEventBitmapBufferDelegate(IntPtr instance, IntPtr credential, uint fieldId, uint imageBufferSize, IntPtr imageBuffer, IntPtr returnValue);
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int RawCredentialEventFieldCheckboxDelegate(IntPtr instance, IntPtr credential, uint fieldId, int isChecked, IntPtr label);
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int RawCredentialEventFieldPointerDelegate(IntPtr instance, IntPtr credential, uint fieldId, IntPtr value);
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int RawCredentialEventFieldUIntDelegate(IntPtr instance, IntPtr credential, uint fieldId, uint value);
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int RawCredentialEventNoArgumentsDelegate(IntPtr instance);
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int RawCredentialEventOnCreatingWindowDelegate(IntPtr instance, out IntPtr ownerWindow);
}
@@ -0,0 +1,430 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
internal sealed class RawCredentialEvents : IDisposable
{
private static readonly object RegistryLock = new object();
private static readonly HashSet<RawCredentialEvents> Registry = new HashSet<RawCredentialEvents>();
private readonly object observationLock = new object();
private readonly int maximumVersion;
private readonly RawQueryInterfaceDelegate queryInterfaceDelegate;
private readonly RawReferenceDelegate addRefDelegate;
private readonly RawReferenceDelegate releaseDelegate;
private readonly RawCredentialEventFieldUIntDelegate fieldUIntStubDelegate;
private readonly RawCredentialEventFieldPointerDelegate fieldPointerStubDelegate;
private readonly RawCredentialEventFieldCheckboxDelegate fieldCheckboxStubDelegate;
private readonly RawCredentialEventFieldPointerDelegate setFieldStringDelegate;
private readonly RawCredentialEventNoArgumentsDelegate beginFieldUpdatesDelegate;
private readonly RawCredentialEventNoArgumentsDelegate endFieldUpdatesDelegate;
private readonly RawCredentialEventFieldUIntDelegate setFieldOptionsDelegate;
private readonly RawCredentialEventOnCreatingWindowDelegate onCreatingWindowDelegate;
private readonly RawCredentialEventBitmapBufferDelegate setFieldBitmapBufferDelegate;
private IntPtr events1Vtable;
private IntPtr events2Vtable;
private IntPtr events3Vtable;
private IntPtr events1Interface;
private IntPtr events2Interface;
private IntPtr events3Interface;
private int referenceCount;
private int addRefCallCount;
private int releaseCallCount;
private int events2QueryCount;
private int events3QueryCount;
private int setFieldStringCallCount;
private int beginFieldUpdatesCallCount;
private int endFieldUpdatesCallCount;
private int setFieldOptionsCallCount;
private int setFieldBitmapBufferCallCount;
private int ownerReferenceReleased;
private IntPtr lastCredential;
private uint lastFieldId;
private uint lastUIntValue;
private string lastString;
private byte[] lastBitmapBuffer;
private bool bitmapBufferReturnValuePointerWasPresent;
public RawCredentialEvents(int maximumVersion)
{
if (maximumVersion < 1 || maximumVersion > 3)
{
throw new ArgumentOutOfRangeException(nameof(maximumVersion));
}
this.maximumVersion = maximumVersion;
this.referenceCount = 1;
this.queryInterfaceDelegate = this.QueryInterface;
this.addRefDelegate = this.AddRef;
this.releaseDelegate = this.Release;
this.fieldUIntStubDelegate = this.FieldUIntStub;
this.fieldPointerStubDelegate = this.FieldPointerStub;
this.fieldCheckboxStubDelegate = this.FieldCheckboxStub;
this.setFieldStringDelegate = this.SetFieldString;
this.beginFieldUpdatesDelegate = this.BeginFieldUpdates;
this.endFieldUpdatesDelegate = this.EndFieldUpdates;
this.setFieldOptionsDelegate = this.SetFieldOptions;
this.onCreatingWindowDelegate = this.OnCreatingWindow;
this.setFieldBitmapBufferDelegate = this.SetFieldBitmapBuffer;
try
{
this.events1Vtable = this.CreateVtable(RawCredentialEventAbi.Events1SlotCount);
this.events1Interface = this.CreateInterface(this.events1Vtable);
if (maximumVersion >= 2)
{
this.events2Vtable = this.CreateVtable(RawCredentialEventAbi.Events2SlotCount);
this.events2Interface = this.CreateInterface(this.events2Vtable);
}
if (maximumVersion >= 3)
{
this.events3Vtable = this.CreateVtable(RawCredentialEventAbi.Events3SlotCount);
this.events3Interface = this.CreateInterface(this.events3Vtable);
}
lock (RegistryLock)
{
Registry.Add(this);
}
}
catch
{
this.FreeNativeMemory();
throw;
}
}
public IntPtr Events1Interface => this.events1Interface;
public IntPtr Events2Interface => this.events2Interface;
public IntPtr Events3Interface => this.events3Interface;
public int CurrentReferenceCount => Volatile.Read(ref this.referenceCount);
public int AddRefCallCount => Volatile.Read(ref this.addRefCallCount);
public int ReleaseCallCount => Volatile.Read(ref this.releaseCallCount);
public int Events2QueryCount => Volatile.Read(ref this.events2QueryCount);
public int Events3QueryCount => Volatile.Read(ref this.events3QueryCount);
public int SetFieldStringCallCount => Volatile.Read(ref this.setFieldStringCallCount);
public int BeginFieldUpdatesCallCount => Volatile.Read(ref this.beginFieldUpdatesCallCount);
public int EndFieldUpdatesCallCount => Volatile.Read(ref this.endFieldUpdatesCallCount);
public int SetFieldOptionsCallCount => Volatile.Read(ref this.setFieldOptionsCallCount);
public int SetFieldBitmapBufferCallCount => Volatile.Read(ref this.setFieldBitmapBufferCallCount);
public IntPtr LastCredential
{
get
{
lock (this.observationLock)
{
return this.lastCredential;
}
}
}
public uint LastFieldId
{
get
{
lock (this.observationLock)
{
return this.lastFieldId;
}
}
}
public uint LastUIntValue
{
get
{
lock (this.observationLock)
{
return this.lastUIntValue;
}
}
}
public string LastString
{
get
{
lock (this.observationLock)
{
return this.lastString;
}
}
}
public byte[] LastBitmapBuffer
{
get
{
lock (this.observationLock)
{
return this.lastBitmapBuffer == null ? null : (byte[])this.lastBitmapBuffer.Clone();
}
}
}
public bool BitmapBufferReturnValuePointerWasPresent
{
get
{
lock (this.observationLock)
{
return this.bitmapBufferReturnValuePointerWasPresent;
}
}
}
public void Dispose()
{
if (Interlocked.Exchange(ref this.ownerReferenceReleased, 1) == 0)
{
this.Release(IntPtr.Zero);
}
}
private IntPtr CreateVtable(int slotCount)
{
IntPtr vtable = Marshal.AllocHGlobal(checked(slotCount * IntPtr.Size));
for (int slot = 0; slot < slotCount; slot++)
{
Marshal.WriteIntPtr(vtable, checked(slot * IntPtr.Size), IntPtr.Zero);
}
this.WriteMethod(vtable, 0, this.queryInterfaceDelegate);
this.WriteMethod(vtable, 1, this.addRefDelegate);
this.WriteMethod(vtable, 2, this.releaseDelegate);
this.WriteMethod(vtable, 3, this.fieldUIntStubDelegate);
this.WriteMethod(vtable, 4, this.fieldUIntStubDelegate);
this.WriteMethod(vtable, RawCredentialEventAbi.SetFieldStringSlot, this.setFieldStringDelegate);
this.WriteMethod(vtable, 6, this.fieldCheckboxStubDelegate);
this.WriteMethod(vtable, 7, this.fieldPointerStubDelegate);
this.WriteMethod(vtable, 8, this.fieldUIntStubDelegate);
this.WriteMethod(vtable, 9, this.fieldUIntStubDelegate);
this.WriteMethod(vtable, 10, this.fieldPointerStubDelegate);
this.WriteMethod(vtable, 11, this.fieldUIntStubDelegate);
this.WriteMethod(vtable, 12, this.onCreatingWindowDelegate);
if (slotCount >= RawCredentialEventAbi.Events2SlotCount)
{
this.WriteMethod(vtable, RawCredentialEventAbi.BeginFieldUpdatesSlot, this.beginFieldUpdatesDelegate);
this.WriteMethod(vtable, RawCredentialEventAbi.EndFieldUpdatesSlot, this.endFieldUpdatesDelegate);
this.WriteMethod(vtable, RawCredentialEventAbi.SetFieldOptionsSlot, this.setFieldOptionsDelegate);
}
if (slotCount >= RawCredentialEventAbi.Events3SlotCount)
{
this.WriteMethod(vtable, RawCredentialEventAbi.SetFieldBitmapBufferSlot, this.setFieldBitmapBufferDelegate);
}
return vtable;
}
private IntPtr CreateInterface(IntPtr vtable)
{
IntPtr result = Marshal.AllocHGlobal(IntPtr.Size);
Marshal.WriteIntPtr(result, vtable);
return result;
}
private void WriteMethod(IntPtr vtable, int slot, Delegate method)
{
Marshal.WriteIntPtr(vtable, checked(slot * IntPtr.Size), Marshal.GetFunctionPointerForDelegate(method));
}
private int QueryInterface(IntPtr instance, ref Guid interfaceId, out IntPtr interfacePointer)
{
interfacePointer = IntPtr.Zero;
if (interfaceId == RawCredentialEventAbi.IUnknown || interfaceId == RawCredentialEventAbi.ICredentialProviderCredentialEvents)
{
interfacePointer = this.events1Interface;
}
else if (interfaceId == RawCredentialEventAbi.ICredentialProviderCredentialEvents2)
{
Interlocked.Increment(ref this.events2QueryCount);
if (this.maximumVersion >= 2)
{
interfacePointer = this.events2Interface;
}
}
else if (interfaceId == RawCredentialEventAbi.ICredentialProviderCredentialEvents3)
{
Interlocked.Increment(ref this.events3QueryCount);
if (this.maximumVersion >= 3)
{
interfacePointer = this.events3Interface;
}
}
if (interfacePointer == IntPtr.Zero)
{
return RawCredentialEventAbi.E_NOINTERFACE;
}
this.AddRef(instance);
return CredentialProviderAbi.S_OK;
}
private uint AddRef(IntPtr instance)
{
Interlocked.Increment(ref this.addRefCallCount);
return checked((uint)Interlocked.Increment(ref this.referenceCount));
}
private uint Release(IntPtr instance)
{
Interlocked.Increment(ref this.releaseCallCount);
int result = Interlocked.Decrement(ref this.referenceCount);
if (result == 0)
{
lock (RegistryLock)
{
Registry.Remove(this);
}
this.FreeNativeMemory();
}
return checked((uint)result);
}
private int SetFieldString(IntPtr instance, IntPtr credential, uint fieldId, IntPtr value)
{
try
{
lock (this.observationLock)
{
this.lastCredential = credential;
this.lastFieldId = fieldId;
this.lastString = value == IntPtr.Zero ? null : Marshal.PtrToStringUni(value);
}
Interlocked.Increment(ref this.setFieldStringCallCount);
return CredentialProviderAbi.S_OK;
}
catch
{
return CredentialProviderAbi.E_FAIL;
}
}
private int BeginFieldUpdates(IntPtr instance)
{
Interlocked.Increment(ref this.beginFieldUpdatesCallCount);
return CredentialProviderAbi.S_OK;
}
private int EndFieldUpdates(IntPtr instance)
{
Interlocked.Increment(ref this.endFieldUpdatesCallCount);
return CredentialProviderAbi.S_OK;
}
private int SetFieldOptions(IntPtr instance, IntPtr credential, uint fieldId, uint value)
{
lock (this.observationLock)
{
this.lastCredential = credential;
this.lastFieldId = fieldId;
this.lastUIntValue = value;
}
Interlocked.Increment(ref this.setFieldOptionsCallCount);
return CredentialProviderAbi.S_OK;
}
private int SetFieldBitmapBuffer(IntPtr instance, IntPtr credential, uint fieldId, uint imageBufferSize, IntPtr imageBuffer, IntPtr returnValue)
{
try
{
if (returnValue == IntPtr.Zero)
{
return RawCredentialEventAbi.E_POINTER;
}
byte[] buffer = new byte[checked((int)imageBufferSize)];
if (buffer.Length > 0)
{
Marshal.Copy(imageBuffer, buffer, 0, buffer.Length);
}
Marshal.WriteInt32(returnValue, CredentialProviderAbi.S_OK);
lock (this.observationLock)
{
this.lastCredential = credential;
this.lastFieldId = fieldId;
this.lastBitmapBuffer = buffer;
this.bitmapBufferReturnValuePointerWasPresent = true;
}
Interlocked.Increment(ref this.setFieldBitmapBufferCallCount);
return CredentialProviderAbi.S_OK;
}
catch
{
return CredentialProviderAbi.E_FAIL;
}
}
private int FieldUIntStub(IntPtr instance, IntPtr credential, uint fieldId, uint value)
{
return CredentialProviderAbi.S_OK;
}
private int FieldPointerStub(IntPtr instance, IntPtr credential, uint fieldId, IntPtr value)
{
return CredentialProviderAbi.S_OK;
}
private int FieldCheckboxStub(IntPtr instance, IntPtr credential, uint fieldId, int isChecked, IntPtr label)
{
return CredentialProviderAbi.S_OK;
}
private int OnCreatingWindow(IntPtr instance, out IntPtr ownerWindow)
{
ownerWindow = IntPtr.Zero;
return CredentialProviderAbi.S_OK;
}
private void FreeNativeMemory()
{
this.FreeNativeMemory(ref this.events1Interface);
this.FreeNativeMemory(ref this.events2Interface);
this.FreeNativeMemory(ref this.events3Interface);
this.FreeNativeMemory(ref this.events1Vtable);
this.FreeNativeMemory(ref this.events2Vtable);
this.FreeNativeMemory(ref this.events3Vtable);
}
private void FreeNativeMemory(ref IntPtr pointer)
{
if (pointer != IntPtr.Zero)
{
Marshal.FreeHGlobal(pointer);
pointer = IntPtr.Zero;
}
}
}
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate int RawQueryInterfaceDelegate(IntPtr instance, ref Guid interfaceId, out IntPtr interfacePointer);
}
@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
namespace Lithnet.CredentialProvider.UnitTests.ComInterop
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate uint RawReferenceDelegate(IntPtr instance);
}
@@ -12,14 +12,17 @@ namespace Lithnet.CredentialProvider
{
this.logger.LogTrace("Advise");
if (this.eventsUnknown != IntPtr.Zero)
{
this.ReleaseCredentialEvents();
}
if (pcpce != null)
{
this.eventsUnknown = Marshal.GetIUnknownForObject(pcpce);
this.Controls.AssignEvents(pcpce);
this.events = pcpce;
this.events2 = pcpce as ICredentialProviderCredentialEvents2;
var intPtr = Marshal.GetIUnknownForObject(pcpce);
Marshal.AddRef(intPtr);
}
this.OnLoad();
@@ -28,6 +31,7 @@ namespace Lithnet.CredentialProvider
}
catch (Exception ex)
{
this.ReleaseCredentialEvents();
this.logger.LogError(ex, "Advise failed");
return HRESULT.E_FAIL;
}
@@ -39,12 +43,9 @@ namespace Lithnet.CredentialProvider
{
this.logger.LogTrace("Unadvise");
if (this.events != null)
if (this.eventsUnknown != IntPtr.Zero)
{
this.Controls.UnassignEvents();
var intPtr = Marshal.GetIUnknownForObject(this.events);
Marshal.Release(intPtr);
this.events = null;
this.ReleaseCredentialEvents();
}
this.OnUnload();
@@ -58,6 +59,26 @@ namespace Lithnet.CredentialProvider
}
}
private void ReleaseCredentialEvents()
{
try
{
this.Controls.UnassignEvents();
}
finally
{
this.events2 = null;
this.events = null;
if (this.eventsUnknown != IntPtr.Zero)
{
IntPtr unknown = this.eventsUnknown;
this.eventsUnknown = IntPtr.Zero;
Marshal.Release(unknown);
}
}
}
int ICredentialProviderCredential.SetSelected(out int pbAutoLogon)
{
pbAutoLogon = 0;
@@ -11,6 +11,7 @@ namespace Lithnet.CredentialProvider
public abstract partial class CredentialTile
{
private protected readonly ICredentialProviderLogger logger;
private IntPtr eventsUnknown;
private protected ICredentialProviderCredentialEvents events;
private protected ICredentialProviderCredentialEvents2 events2;
private protected ControlCollection controls;