Skip to content

COM Late Binding Fails to Find Member #125008

@jborean93

Description

@jborean93

Description

It looks like the code to find members of a COM object fails for late binding when casting to a different interface than what was originally returned. I'm not too experienced in COM so apologies if my terms are incorrect this is just how I understand what is happening.

In this example I see that .NET fails to invoke a member when finding by name through IDispatch. It seems like when it tries to find the member through the IDispatch interface it is using the iid of the original object that was retrieved rather than the iid of what was casted.

In the reproduction case I am using ITaskDefinition.get_Principal which returns an IPrincipal object. I then cast it to IPrincipal2 which inherits from IDispatch but the late binding method fails to find any of the members returns DISP_E_UNKNOWNNAME.

Reproduction Steps

Use the following PowerShell code to define a Scheduled task with a custom ProcessTokenSidType

$princParams = @{
    ProcessTokenSidType = 'Unrestricted'
}
$principal = New-ScheduledTaskPrincipal -UserId LocalService -LogonType ServiceAccount @princParams
$action = New-ScheduledTaskAction -Execute cmd.exe
$settings = New-ScheduledTaskSettingsSet -Compatibility Win8
New-ScheduledTask -Principal $principal -Action $action -Settings $settings | Register-ScheduledTask -TaskName MyTask -Force

Create a csproj with

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net472;net10.0</TargetFrameworks>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

</Project>

Create C# file next to csproj with

Expand for C# code
using System;
using System.Runtime.InteropServices;

namespace TaskScheduler;

class Program
{
    static int Main(string[] args)
    {
        if (args.Length == 0)
        {
            Console.WriteLine("Usage: program.exe <task_path>");
            return 1;
        }

        string taskPath = args[0];

        var scheduler = new TaskScheduler();
        ITaskService service = (ITaskService)scheduler;
        service.Connect(null, null, null, null);

        ITaskFolder rootFolder = service.GetFolder("\\");
        IRegisteredTask task = rootFolder.GetTask(taskPath);
        ITaskDefinition definition = task.Definition;
        IPrincipal principal = definition.Principal;

        Console.WriteLine($"Task: {taskPath}");
        Console.WriteLine($"Principal.Id: {principal.Id}");
        if (principal is IPrincipal2 principal2)
        {
            Console.WriteLine($"ProcessTokenSidType: {principal2.ProcessTokenSidType}");
        }

        return 0;
    }
}

[ComImport, Guid("0f87369f-a4e5-4cfc-bd3e-73e6154572dd")]
public class TaskScheduler { }

[ComImport, Guid("2faba4c7-4da9-4013-9697-20cc3fd40f85")]
#if IDISPATCH
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
#else
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
#endif
public interface ITaskService
{
    ITaskFolder GetFolder(string path);
    void GetRunningTasks(int flags, out object runningTasks);
    void NewTask(uint flags, out ITaskDefinition definition);
    void Connect(object serverName, object user, object domain, object password);
}

[ComImport, Guid("8cfac062-a080-4c15-9a88-aa7c2af80dfc")]
#if IDISPATCH
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
#else
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
#endif
public interface ITaskFolder
{
    string Name { get; }
    string Path { get; }
    ITaskFolder GetFolder(string path);
    object GetFolders(int flags);
    void CreateFolder(string subFolderName, object sddl, out ITaskFolder folder);
    void DeleteFolder(string subFolderName, int flags);
    IRegisteredTask GetTask(string path);
}

[ComImport, Guid("9c86f320-dee3-4dd1-b972-a303f26b061e")]
#if IDISPATCH
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
#else
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
#endif
public interface IRegisteredTask
{
    string Name { get; }
    string Path { get; }
    int State { get; }
    bool Enabled { get; set; }
    void Run(object parameters, out object runningTask);
    void RunEx(object parameters, int flags, int sessionID, string user, out object runningTask);
    object GetInstances(int flags);
    DateTime LastRunTime { get; }
    int LastTaskResult { get; }
    uint NumberOfMissedRuns { get; }
    DateTime NextRunTime { get; }
    ITaskDefinition Definition { get; }
}

[ComImport, Guid("f5bc8fc5-536d-4f77-b852-fbc1356fdeb6")]
#if IDISPATCH
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
#else
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
#endif
public interface ITaskDefinition
{
    object RegistrationInfo { get; set; }
    object Triggers { get; set;  }
    object Settings { get; set; }
    string Data { get; set; }
    IPrincipal Principal { get; set; }
}

[ComImport, Guid("d98d51e5-c9b4-496a-a9c1-18980261cf0f")]
#if IDISPATCH
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
#else
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
#endif
public interface IPrincipal
{
    string Id { get; set; }
}

[ComImport, Guid("248919ae-e345-4a6d-8aeb-e0d3165c904e")]
#if IDISPATCH
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
#else
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
#endif
public interface IPrincipal2
{
    int ProcessTokenSidType { get; set; }
}

Finally run these 4 permutations

dotnet run --framework net472 -- MyTask
dotnet run --framework net10.0 -- MyTask
dotnet run --framework net472 --property DefineConstants="IDISPATCH" -- MyTask
dotnet run --framework net10.0 --property DefineConstants="IDISPATCH" -- MyTask

Expected behavior

Task: MyTask
Principal.Id: Author
ProcessTokenSidType: 2

For all scenarios

Actual behavior

Early binding for net472 and net10.0 works, late binding for net472 works but late binding for net10.0 fails with

Task: MyTask
Principal.Id: Author
Unhandled exception. System.Runtime.InteropServices.COMException (0x80020006): Unknown name. (0x80020006 (DISP_E_UNKNOWNNAME))
   at System.RuntimeType.InvokeDispMethod(String name, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, Int32 culture, String[] namedParameters)
   at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
   at System.RuntimeType.ForwardCallToInvokeMember(String memberName, BindingFlags flags, Object target, Object[] aArgs, Boolean[] aArgsIsByRef, Int32[] aArgsWrapperTypes, Type[] aArgsTypes, Type retType)
   at TaskScheduler.IPrincipal2.get_ProcessTokenSidType()
   at TaskScheduler.Program.Main(String[] args) in C:\temp\TaskScheduler\Program.cs:line 31

If I was to change the IPrincipal2 interface to:

[ComImport, Guid("248919ae-e345-4a6d-8aeb-e0d3165c904e")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IPrincipal2
{
    string Id { get; set; }
}

Getting the Id field on the casted IPrincipal2 object works and returns Id from IPrincipal (d98d51e5-c9b4-496a-a9c1-18980261cf0f). It seems like the code that tries to find the members is still using the IPrincipal interface on the lookup and is why it fails to find get_ProcessTokenSidType.

Regression?

.NET Framework works as per the reproducer, not sure when it has fails but it also fails in .NET 9.

Known Workarounds

Use the early bind setup which requires the interface to be defined in the correct order as AFAIK it relies on the vtable to be correct to invoke the member rather than going through the IDispatch interface.

Configuration

Ran on .NET Framework 4.7.2 and .NET 10.0. Relates to Windows only, I am running on ARM64 and don't have an X86 host to test on right now but I assume it also fails there.

Other information

N/A

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions