diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialProvider3.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialProvider3.cs new file mode 100644 index 0000000..b834ec4 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialProvider3.cs @@ -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 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; + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialTile3.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialTile3.cs new file mode 100644 index 0000000..f1d2c7f --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialTile3.cs @@ -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; + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialAdviseDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialAdviseDelegate.cs new file mode 100644 index 0000000..98a3f91 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialAdviseDelegate.cs @@ -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); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialCallbackAbiTests.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialCallbackAbiTests.cs new file mode 100644 index 0000000..3315f3d --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialCallbackAbiTests.cs @@ -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(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("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("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("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(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(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(interfacePointer, 2); + release(interfacePointer); + } + + private static void CollectComWrappers() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialCallbackScenario.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialCallbackScenario.cs new file mode 100644 index 0000000..b647664 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialCallbackScenario.cs @@ -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(RawCredentialEventAbi.CredentialAdviseSlot); + this.unAdvise = this.credential.GetMethod(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(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(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(CredentialProviderAbi.GetCredentialCountSlot); + GetCredentialAtDelegate getCredentialAt = providerInterface.GetMethod(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)); + } + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialUnAdviseDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialUnAdviseDelegate.cs new file mode 100644 index 0000000..d7f5125 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialUnAdviseDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int CredentialUnAdviseDelegate(IntPtr instance); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventAbi.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventAbi.cs new file mode 100644 index 0000000..6b11117 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventAbi.cs @@ -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); + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventBitmapBufferDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventBitmapBufferDelegate.cs new file mode 100644 index 0000000..ab4c161 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventBitmapBufferDelegate.cs @@ -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); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldCheckboxDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldCheckboxDelegate.cs new file mode 100644 index 0000000..3ee86d5 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldCheckboxDelegate.cs @@ -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); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldPointerDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldPointerDelegate.cs new file mode 100644 index 0000000..9d97502 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldPointerDelegate.cs @@ -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); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldUIntDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldUIntDelegate.cs new file mode 100644 index 0000000..c203d30 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventFieldUIntDelegate.cs @@ -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); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventNoArgumentsDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventNoArgumentsDelegate.cs new file mode 100644 index 0000000..c6b9fe6 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventNoArgumentsDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int RawCredentialEventNoArgumentsDelegate(IntPtr instance); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventOnCreatingWindowDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventOnCreatingWindowDelegate.cs new file mode 100644 index 0000000..ed7d0f1 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEventOnCreatingWindowDelegate.cs @@ -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); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEvents.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEvents.cs new file mode 100644 index 0000000..ae4ef61 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawCredentialEvents.cs @@ -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 Registry = new HashSet(); + + 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; + } + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawQueryInterfaceDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawQueryInterfaceDelegate.cs new file mode 100644 index 0000000..bb63445 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawQueryInterfaceDelegate.cs @@ -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); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawReferenceDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawReferenceDelegate.cs new file mode 100644 index 0000000..20e70e2 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/RawReferenceDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate uint RawReferenceDelegate(IntPtr instance); +} diff --git a/src/Lithnet.CredentialProvider/CredentialTile.ICredentialProviderCredential.cs b/src/Lithnet.CredentialProvider/CredentialTile.ICredentialProviderCredential.cs index 247a9f6..80ec4c9 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile.ICredentialProviderCredential.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile.ICredentialProviderCredential.cs @@ -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; diff --git a/src/Lithnet.CredentialProvider/CredentialTile.cs b/src/Lithnet.CredentialProvider/CredentialTile.cs index 82862c2..1793984 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile.cs @@ -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;