Search code examples
.netwinapidirectoryfilesystems

What am I getting a file access error using `DeviceIoControl`?


I am recursing a folder structure in Windows, getting the unique filesystem identifiers for each file and folder. For each folder, I am using the CreateFile() and DeviceIoControl() WinAPI functions to get access to the folder ID, as per an answer to this question:

Getting a Folder ID C#

However, for some paths (not all), despite being able to browse them in Explorer, and even System.IO.Path.Exists() asserts they exist and are accessible, DeviceIoControl() returns error code 2, ERROR_FILE_NOT_FOUND.

The last test seems to confirm that it is not a matter of folder permissions, but it is interesting that the error is specifically about a file not found, not a path not found (would be error code 3, ERROR_PATH_NOT_FOUND). I have confirmed that System.IO.File.Exists() correctly rejects all folders, even if DeviceIoControl() does access the path and return meaningful values.

What specifically is DeviceIoControl() sensitive to, and why is it tripping over on valid paths?

  • Windows 10

  • Visual Studio Express 2022

  • .NET 8.0

WinAPI calls:

Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles

Public Class WinAPI
  Public Shared GENERIC_READ As Integer = &H80000000
  Public Shared FILE_FLAG_BACKUP_SEMANTICS As Integer = &H2000000
  Public Shared OPEN_EXISTING As Integer = &H3

  <StructLayout(LayoutKind.Sequential)>
  Public Structure FILE_OBJECTID_BUFFER
    Public Structure Union

      <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
      Public BirthVolumeID() As Byte

      <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
      Public BirthObjectId As Byte()

      <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
      Public DomainID As Byte()
    End Structure

    <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
    Public ObjectID As Byte()

    Public BirthInfo As Union

    '<MarshalAs(UnmanagedType.ByValArray, SizeConst:=48)>
    'Public ExtendedInfo As Byte()
  End Structure

  Public Structure FILETIME
    Public dwLowDateTime As UInteger
    Public dwHighDateTime As UInteger
  End Structure

  <StructLayout(LayoutKind.Sequential)>
  Public Structure BY_HANDLE_FILE_INFORMATION
    Public FileAttributes As UInteger
    Public CreationTime As FILETIME
    Public LastAccessTime As FILETIME
    Public LastWriteTime As FILETIME
    Public VolumeSerialNumber As UInteger
    Public FileSizeHigh As UInteger
    Public FileSizeLow As UInteger
    Public NumberOfLinks As UInteger
    Public FileIndexHigh As UInteger
    Public FileIndexLow As UInteger
  End Structure

  <DllImport("kernel32.dll", SetLastError:=True)>
  Public Shared Function DeviceIoControl(
                                        hDevice As SafeFileHandle,
                                        dwIoControlCode As UInteger,
                                        lpInBuffer As IntPtr,
                                        nInBufferSize As UInteger,
                                        lpOutBuffer As IntPtr,
                                        nOutBufferSize As Integer,
                                        ByRef lpBytesReturned As UInteger,
                                        lpOverlapped As IntPtr
                                        ) As Boolean
  End Function

  <DllImport("kernel32.dll", SetLastError:=True)>
  Public Shared Function CreateFile(
                                   fileName As String,
                                   dwDesiredAccess As Integer,
                                   dwShareMode As System.IO.FileShare,
                                   securityAttrs_MustBeZero As IntPtr,
                                   dwCreationDisposition As System.IO.FileMode,
                                   dwFlagsAndAttributes As Integer,
                                   hTemplateFile_MustBeZero As IntPtr
                                   ) As SafeFileHandle
  End Function

  <DllImport("kernel32.dll", SetLastError:=True)>
  Public Shared Function GetFileInformationByHandle(
                                                   hFile As IntPtr,
                                                   ByRef lpFileInformation As BY_HANDLE_FILE_INFORMATION) As Boolean
  End Function

End Class

Implementation functions:

Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles

Public Class FileAccess
  Shared FSCTL_GET_OBJECT_ID As UInteger = &H9009C

  Public Shared Function GetFileId(path As String) As UInt128


    Using fs = IO.File.Open(
      path,
      IO.FileMode.OpenOrCreate,
      IO.FileAccess.ReadWrite,
      IO.FileShare.ReadWrite)


      Dim info As WinAPI.BY_HANDLE_FILE_INFORMATION
      WinAPI.GetFileInformationByHandle(fs.Handle, info)
      Dim fileID As UInt128 = info.FileIndexHigh
      fileID <<= 32
      fileID = fileID Or info.FileIndexLow
      Return fileID
      'Return String.Format("{0:x}", ((info.FileIndexHigh << 32) Or info.FileIndexLow))
    End Using

  End Function

  Public Shared Function GetFolderID(path As String) As UInt128
    Dim result As UInt128

    Dim buffer As WinAPI.FILE_OBJECTID_BUFFER = GetFolderIdBuffer(path)
    result = BitConverter.GetUint128(buffer.ObjectID)

    Return result
  End Function

  Private Shared Function GetFolderIdBuffer(path As String) As WinAPI.FILE_OBJECTID_BUFFER
    Using hFile As SafeFileHandle = WinAPI.CreateFile(
            path,
            WinAPI.GENERIC_READ, IO.FileShare.Read,
            IntPtr.Zero,
            WinAPI.OPEN_EXISTING,
            WinAPI.FILE_FLAG_BACKUP_SEMANTICS,
            IntPtr.Zero
            )
      If hFile Is Nothing Or hFile.IsInvalid Then Throw New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error())

      Dim buffer As New WinAPI.FILE_OBJECTID_BUFFER

      Dim nOutBufferSize As Integer = Marshal.SizeOf(buffer)
      Dim lpOutBuffer As IntPtr = Marshal.AllocHGlobal(nOutBufferSize)
      Dim lpBytesReturned As UInteger

      Dim result As Boolean =
                WinAPI.DeviceIoControl(
                    hFile, FSCTL_GET_OBJECT_ID,
                    IntPtr.Zero, 0,
                    lpOutBuffer, nOutBufferSize,
                    lpBytesReturned,
                    IntPtr.Zero
                    )

      If Not result Then Throw New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error())

      Dim type As Type = GetType(WinAPI.FILE_OBJECTID_BUFFER)

      buffer = Marshal.PtrToStructure(lpOutBuffer, type)

      Marshal.FreeHGlobal(lpOutBuffer)
      Return buffer
    End Using
  End Function


End Class

Consuming method:

Dim folderID As UInt128 = FileAccess.GetFolderID(path)

Solution

  • From the FSCTL_GET_OBJECT_ID documentation

    If there is no object identifier associated with the specified handle, none is created and an error is returned.

    It doesn't say which error, but ERROR_FILE_NOT_FOUND is a likely match.

    Next it says

    To create an object identifier, use FSCTL_SET_OBJECT_ID. To retrieve an existing object identifier or generate one if there is no existing object identifier in one step, use FSCTL_CREATE_OR_GET_OBJECT_ID.

    which provides you with a solution.


    NB: There is no single semantic for DeviceIoControl, as it provides you a way to dispatch to a large number of different functions in different drivers (your case is probably a call into the NTFS filesystem driver), identified by IOCTL codes, and each of these different codes has different preconditions and postconditions.