Menu

#2456 NtfsHandler CInStream cannot query for interface

open
nobody
None
5
2024-03-28
2024-03-27
No

The COM definition for this class contains a bad implementation that only can be queried for IInStream and cannot be QueryInterface'd for ISequentialInStream:

This code in NtfsHandler.cpp:

Z7_CLASS_IMP_COM_1(
  CInStream
  , IInStream
)
  Z7_IFACE_COM7_IMP(ISequentialInStream)

Should instead be this code:

Z7_CLASS_IMP_IInStream(
  CInStream
)

As a matter of fact, just a quick scan over the code, and I see similar implementation errors in XzHandler, DmgHandler, and MultiStream

In spite of ::GetStream() returning an ISequentialInStream * already, .NET (and possibly other languages) will execute a QueryInterface on the IUnknown anyway, and will be unable to get the interface back.

Discussion

  • Robert Simpson

    Robert Simpson - 2024-03-27

    These are all the implementations I found that do not allow QueryInterface on ISequentialInStream:

    InStreamWithCRC.h - CInStreamWithCRC
    MultiStream.h - CMultiStream
    DmgHandler.cpp - CInStream
    NtfsHandler.cpp - CInStream
    XzHandler.cpp - CInStream
    ArchiveOpenCallback.cpp - CInFileStreamVol

     
  • Robert Simpson

    Robert Simpson - 2024-03-28

    I'm finding more places where this happens. For example, CHandlerImg in HandlerCont.h implements IInStream but it doesn't support QueryInterface for ISequentialInStream

     
  • Robert Simpson

    Robert Simpson - 2024-03-28

    Looks like CHandlerImg is the last one I could find. Fortunately the fixes are super simple, like in HandlerCont.h:

    From this:

      Z7_COM_UNKNOWN_IMP_3(
          IInArchive,
          IInArchiveGetStream,
          IInStream)
    

    To this:

      Z7_COM_UNKNOWN_IMP_4(
          IInArchive,
          IInArchiveGetStream,
          IInStream,
          ISequentialInStream)
    
     
  • Igor Pavlov

    Igor Pavlov - 2024-03-28

    (and possibly other languages) will execute a QueryInterface on the IUnknown anyway

    In what exact code line it will call QueryInterface ?

     
  • Robert Simpson

    Robert Simpson - 2024-03-28

    Are you asking me how .NET's source code works when it interops with COM? That I'm not sure. All I know is that my code looks like this in .NET:

    [GeneratedComInterface]
    [Guid("23170F69-40C1-278A-0000-000600400000")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal partial interface IInArchiveGetStream
    {
        [PreserveSig]
        int GetStream(int index, out ISequentialInStream? stream);
    }
    
        [GeneratedComInterface]
        [Guid("23170F69-40C1-278A-0000-000300010000")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        internal partial interface ISequentialInStream
        {
            [PreserveSig]
            unsafe int Read(byte* data, int dataSize, int* actual);
        }
    

    When I call GetStream in .NET on certain Handlers like the NTFS handler, .NET throws an error saying it can't marshal the interface. When I started digging through the 7z source code to find out why, I discovered that 7z's GetStream implementation for NtfsHandler didn't support calling QueryInterface for ISequentialInStream directly. Once I changed the 7z source code, then .NET started working.

    I realize that 7z is already returning an ISequentialInStream, and it doesn't make sense that .NET would query for an interface it already has a reference to, but perhaps internally .NET is just treating everything as IUnknown and queries for the interface regardless of what you return.

     
  • Robert Simpson

    Robert Simpson - 2024-03-28

    Digging through the .NET source code, what I suspected is the case. .NET constructs a generic method proxy for GetStream() which returns back a void * which it then passes to ConvertToManaged

    The de-compiled auto-generated code in .NET looks like this:

    [global::System.Runtime.InteropServices.DynamicInterfaceCastableImplementationAttribute]
    file unsafe partial interface InterfaceImplementation : global::MyLibrary.Compression.SevenZ.Interfaces.IInArchiveGetStream
    {
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.ComInterfaceGenerator", "9.0.10.12805")]
        [global::System.Runtime.CompilerServices.SkipLocalsInitAttribute]
        int global::MyLibrary.Compression.SevenZ.Interfaces.IInArchiveGetStream.GetStream(int index, out global::MyLibrary.Compression.SevenZ.Interfaces.ISequentialInStream stream)
        {
            var(__this, __vtable_native) = ((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::MyLibrary.Compression.SevenZ.Interfaces.IInArchiveGetStream));
            bool __invokeSucceeded = default;
            global::System.Runtime.CompilerServices.Unsafe.SkipInit(out stream);
            void* __stream_native = default;
            int __retVal = default;
            try
            {
                {
                    __retVal = ((delegate* unmanaged[MemberFunction]<void*, int, void**, int> )__vtable_native[3])(__this, index, &__stream_native);
                }
    
                __invokeSucceeded = true;
                global::System.GC.KeepAlive(this);
                // Unmarshal - Convert native data to managed data.
                stream = global::System.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<global::MyLibrary.Compression.SevenZ.Interfaces.ISequentialInStream>.ConvertToManaged(__stream_native);
            }
            finally
            {
                if (__invokeSucceeded)
                {
                    // CleanupCalleeAllocated - Perform cleanup of callee allocated resources.
                    global::System.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<global::MyLibrary.Compression.SevenZ.Interfaces.ISequentialInStream>.Free(__stream_native);
                }
            }
    
            return __retVal;
        }
    }
    

    It's a bit of a tangled mess, but in short, .NET compiles a generic method for GetStream that changes the output from ISequentialInStream ** to void ** and then casts that void * to a managed interface using QueryInterface.

    This is all out of my control.

     

    Last edit: Robert Simpson 2024-03-28
  • Igor Pavlov

    Igor Pavlov - 2024-03-28

    Ok.
    I'll fix that code.

     

Log in to post a comment.