Unable to port FSCTL_GET_RETRIEVAL_POINTERS call to C#

0

I'm running into an odd problem trying to call DeviceIOControl with FSCTL_GET_RETRIEVAL_POINTERS in C#. I've tried the code from Jeffrey Walls blog as well as my own code but keep running into the same problem. The problem seems to be in the Extents structure in RETRIEVAL_POINTERS_BUFFER. When Extents[0]->NextVcn is 1 and Extents[0]->Lcn is 217550 in C++, in C#, I keep getting Extents[0]->NextVcn is 4294967296 and Extents[0]->Lcn is 934370135244800. The same with RETRIEVAL_POINTERS_BUFFER->StartingVcn, when it should be 0, in C# it becomes some other number (like 101 or 2016).

C# Code (Not Working)

        // Get cluster allocation information
        PInvoke.LARGE_INTEGER StartingVCN = new PInvoke.LARGE_INTEGER();
        PInvoke.RETRIEVAL_POINTERS_BUFFER Retrieval;
        IntPtr pDest = IntPtr.Zero;
        uint RetSize;
        uint Extents;
        uint BytesReturned = 0;

        // Grab info one extent at a time, until it's done grabbing all the extent data
        // Yeah, well it doesn't give us a way to ask L"how many extents?" that I know of ...
        // btw, the Extents variable tends to only reflect memory usage, so when we have
        // all the extents we look at the structure Win32 gives us for the REAL count!
        Extents = 10;
        RetSize = 0;

        const uint RETRIEVAL_POINTERS_BUFFER_SIZE = 28;

        StartingVCN.QuadPart = 0;

        GCHandle handle = GCHandle.Alloc(StartingVCN, GCHandleType.Pinned);
        IntPtr StartingVCNPtr = handle.AddrOfPinnedObject();

        do
        {
            Extents *= 2;
            RetSize = RETRIEVAL_POINTERS_BUFFER_SIZE + (uint)((Extents - 1) * Marshal.SizeOf(typeof(PInvoke.LARGE_INTEGER)) * 2);

            if (pDest != IntPtr.Zero)
                pDest = Marshal.ReAllocHGlobal(pDest, (IntPtr)RetSize);
            else
                pDest = Marshal.AllocHGlobal((int)RetSize);

            Result = PInvoke.DeviceIoControl
            (
                Handle,
                PInvoke.FSConstants.FSCTL_GET_RETRIEVAL_POINTERS,
                StartingVCNPtr,
                (uint)Marshal.SizeOf(typeof(PInvoke.LARGE_INTEGER)),
                pDest,
                RetSize,
                ref BytesReturned,
                IntPtr.Zero
            );

            if (!Result)
            {
                if (Marshal.GetLastWin32Error() != PInvoke.ERROR_MORE_DATA)
                {
                    Debug.WriteLine("Error #{0} occurred trying to get retrieval pointers for file '{1}", Marshal.GetLastWin32Error(), FullName);

                    Info.Clusters = 0;
                    Info.Attributes.AccessDenied = true;
                    Info.Attributes.Process = false;
                    Info.Fragments.Clear();
                    PInvoke.CloseHandle(Handle);
                    Marshal.FreeHGlobal(pDest);

                    return false;
                }

                Extents++;
            }
        } while (!Result);

        Retrieval = new PInvoke.RETRIEVAL_POINTERS_BUFFER(pDest);

        // Readjust extents, as it only reflects how much memory was allocated and may not
        // be accurate
        Extents = (uint)Retrieval.ExtentCount;

        // Ok, we have the info. Now translate it. hrmrmr

        Info.Fragments.Clear();
        for (int i = 0; i < Extents; i++)
        {
            Extent Add;

            Add.StartLCN = (ulong)Retrieval.Extents[(int)i].Lcn.QuadPart;
            if (i != 0)
                Add.Length = (ulong)Retrieval.Extents[(int)i].NextVcn.QuadPart - (ulong)Retrieval.Extents[(int)i - 1].NextVcn.QuadPart;
            else
                Add.Length = (ulong)Retrieval.Extents[(int)i].NextVcn.QuadPart - (ulong)Retrieval.StartingVcn;

            Info.Fragments.Add(Add);
        }

P/Invoke Code

    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern bool DeviceIoControl(
        IntPtr hDevice,
        uint dwIoControlCode,
        IntPtr lpInBuffer,
        uint nInBufferSize,
        [Out] IntPtr lpOutBuffer,
        uint nOutBufferSize,
        ref uint lpBytesReturned,
        IntPtr lpOverlapped);

    [StructLayout(LayoutKind.Explicit, Size = 8)]
    internal struct LARGE_INTEGER
    {
        [FieldOffset(0)]
        public UInt64 QuadPart;
        [FieldOffset(0)]
        public UInt32 LowPart;
        [FieldOffset(4)]
        public Int32 HighPart;
    }
    internal struct Extent
    {
        public LARGE_INTEGER NextVcn;
        public LARGE_INTEGER Lcn;

        public Extent(IntPtr ptr)
        {
            IntPtr extentPtr = ptr;

            NextVcn = (LARGE_INTEGER)Marshal.PtrToStructure(extentPtr, typeof(LARGE_INTEGER));
            Lcn = (LARGE_INTEGER)Marshal.PtrToStructure(IntPtr.Add(extentPtr, 8), typeof(LARGE_INTEGER));
        }
    }

    internal struct RETRIEVAL_POINTERS_BUFFER
    {
        public int ExtentCount;
        public Int64 StartingVcn;
        public List<Extent> Extents;

        public RETRIEVAL_POINTERS_BUFFER(IntPtr ptr)
        {
            this.ExtentCount = (Int32)Marshal.PtrToStructure(ptr, typeof(Int32));

            ptr = IntPtr.Add(ptr, 4); 

            // StartingVcn
            this.StartingVcn = (Int64)Marshal.PtrToStructure(ptr, typeof(Int64));

            ptr = IntPtr.Add(ptr, 8); 

            this.Extents = new List<Extent>();

            for (int i = 0; i < this.ExtentCount; i++)
            {
                this.Extents.Add(new Extent(ptr));

                ptr = IntPtr.Add(ptr, 16);
            }
        }
    }

    /// <summary>
    /// constants lifted from winioctl.h from platform sdk
    /// </summary>
    internal class FSConstants
    {
        const uint FILE_DEVICE_DISK = 0x00000007;
        const uint IOCTL_DISK_BASE = FILE_DEVICE_DISK;
        const uint FILE_DEVICE_FILE_SYSTEM = 0x00000009;

        const uint METHOD_NEITHER = 3;
        const uint METHOD_BUFFERED = 0;

        const uint FILE_ANY_ACCESS = 0;
        const uint FILE_SPECIAL_ACCESS = FILE_ANY_ACCESS;

        public static uint FSCTL_GET_VOLUME_BITMAP = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 27, METHOD_NEITHER, FILE_ANY_ACCESS);
        public static uint FSCTL_GET_RETRIEVAL_POINTERS = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 28, METHOD_NEITHER, FILE_ANY_ACCESS);
        public static uint FSCTL_MOVE_FILE = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 29, METHOD_BUFFERED, FILE_SPECIAL_ACCESS);

        public static uint IOCTL_DISK_GET_DRIVE_GEOMETRY = CTL_CODE(IOCTL_DISK_BASE, 0x0000, METHOD_BUFFERED, FILE_ANY_ACCESS);

        static uint CTL_CODE(uint DeviceType, uint Function, uint Method, uint Access)
        {
            return ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method);
        }
    }

C++ to C# Code (Working)

C# Code

        uint Extents = 0;
        ulong StartingVcn = 0;
        IntPtr ExtentsPtr = IntPtr.Zero;

        bool ret = PInvoke.GetRetrievalPointers(Handle, ref Extents, ref StartingVcn, ref ExtentsPtr);

        IntPtr ptr = ExtentsPtr;

        Info.Fragments.Clear();

        for (uint i = 0; i < Extents; i++)
        {
            PInvoke.Extent Extent = new PInvoke.Extent(ptr);
            Extent Add;

            Add.StartLCN = Extent.Lcn.QuadPart;

            if (i != 0)
            {
                // Get previous extent
                PInvoke.Extent PrevExtent = new PInvoke.Extent(IntPtr.Subtract(ptr, 16));
                Add.Length = Extent.NextVcn.QuadPart - PrevExtent.NextVcn.QuadPart;
            }
            else
            {
                Add.Length = Extent.NextVcn.QuadPart - StartingVcn;
            }

            Info.Fragments.Add(Add);

            ptr = IntPtr.Add(ptr, 16);
        }

P/Invoke C# Code

    [DllImport("DLL.dll", CallingConvention=CallingConvention.Cdecl)]
    internal static extern bool GetRetrievalPointers(
        IntPtr hFile,
        [MarshalAs(UnmanagedType.U4)] ref uint ExtentCount,
        [MarshalAs(UnmanagedType.U8)] ref ulong StartingVcn,
        ref IntPtr ExtentsPtr
        );

C++ Code

#ifdef __cplusplus
extern "C" {
#endif
    EXTERNAPI bool GetRetrievalPointers(HANDLE hFile, DWORD &ExtentCount, UINT64 &StartingVcn, void **Extents) {
        // Get cluster allocation information
        STARTING_VCN_INPUT_BUFFER  StartingVCN;
        RETRIEVAL_POINTERS_BUFFER *Retrieval;
        unsigned __int64           RetSize;
        unsigned __int64           ExtentsCount;
        DWORD                      BytesReturned;
        BOOL                       Result;

        // Grab info one extent at a time, until it's done grabbing all the extent data
        // Yeah, well it doesn't give us a way to ask L"how many extents?" that I know of ...
        // btw, the Extents variable tends to only reflect memory usage, so when we have
        // all the extents we look at the structure Win32 gives us for the REAL count!
        ExtentsCount = 10;
        Retrieval = NULL;
        RetSize = 0;
        StartingVCN.StartingVcn.QuadPart = 0;

        do
        {
            ExtentsCount *= 2;
            RetSize = sizeof (RETRIEVAL_POINTERS_BUFFER)+((ExtentsCount - 1) * sizeof (LARGE_INTEGER)* 2);

            if (Retrieval != NULL)
                Retrieval = (RETRIEVAL_POINTERS_BUFFER *)realloc(Retrieval, RetSize);
            else
                Retrieval = (RETRIEVAL_POINTERS_BUFFER *)malloc(RetSize);

            Result = DeviceIoControl
                (
                hFile,
                FSCTL_GET_RETRIEVAL_POINTERS,
                &StartingVCN,
                sizeof (StartingVCN),
                Retrieval,
                RetSize,
                &BytesReturned,
                NULL
                );

            if (Result == FALSE)
            {
                DWORD dwLastError = GetLastError();
                if (dwLastError != ERROR_MORE_DATA)
                {
                    free(Retrieval);

                    return (false);
                }

                ExtentsCount++;
            }
        } while (Result == FALSE);

        // Readjust extents, as it only reflects how much memory was allocated and may not
        // be accurate
        ExtentsCount = Retrieval->ExtentCount;

        // Ok, we have the info. Now translate it. hrmrmr
        ExtentCount = ExtentsCount;
        StartingVcn = Retrieval->StartingVcn.QuadPart;
        *Extents = Retrieval->Extents;

        return true;
    }

#ifdef __cplusplus
};
#endif

Other

The one thing I noticed is that when I convert Extents[0]->NextVcn and Extents[0]->Lcn to a LARGE_INTEGER (see code above) then the HighPart is switched with LowPart, like the following. However, switching HighPart with LowPart does not work RETRIEVAL_POINTERS_BUFFER->StartingVcn (because HighPart/LowPart is a number and they should both be 0).

Wrong LARGE_INTEGER

LARGE_INTEGER.HighPart = 2;
LARGE_INTEGER.LowPart = 0;

Expected LARGE_INTEGER

LARGE_INTEGER.HighPart = 0;
LARGE_INTEGER.LowPart = 2;

I'm wondering if there's something that's changing the memory or if I'm not working with the memory properly? Any ideas?

c#
c++
winapi
memory
interop
asked on Stack Overflow May 27, 2014 by ub3rst4r

1 Answer

1

Extents[0]->NextVcn is 4294967296 and Extents[0]->Lcn is 934370135244800

These are not random numbers. Convert them hex and you'll see that they are the exact same values you got in your C++ code, just offset by 4 bytes. In other words, you are reading misaligned data. The bug is located here:

    ptr = IntPtr.Add(ptr, 4); 

You hard-coded the offset to RETRIEVAL_POINTERS_BUFFER.StartingVcn to 4. But that field requires alignment to 8 since it contains a 64-bit value. So there's 4 bytes of extra padding between the fields. Fix:

    ptr = IntPtr.Add(ptr, 8); 

Okay both in 32-bit and 64-bit code.

answered on Stack Overflow May 27, 2014 by Hans Passant

User contributions licensed under CC BY-SA 3.0