FSCTL_LOCK_VOLUME and WriteFile fail

1

I've created a small app to make backups of disks and to restore a disk from a backup. The logic is pretty simple:

  • Call CreateFile on \\.\PhysicalDrive<index>
  • Do the same for destination physical drive
  • Call DeviceIoControl with FSCTL_LOCK_VOLUME on source + destination handles
  • Call DeviceIoControl with FSCTL_DISMOUNT_VOLUME on source + destination handles
  • In a loop, use ReadFile to read from source and write it to destination using WriteFile until all bytes have been written.
  • Call DeviceIoControl with FSCTL_UNLOCK_VOLUME on both source and destination handles
  • Call CloseHandle on both source and destination handles

I always check that all operations are successful before continuing to the next call. This works really well, but I noticed something that is a bit puzzling:

If, while a copy is taking place, the OS tries to do something with the physical disk (which I know is locked) then the next WriteFile fails.

Examples of things that cause this to fail:

  • Removing a separate disk (like removing a USB stick not used as either the source or the destination)
  • Opening disk management
  • In PowerShell, doing Get-Disk

Those things all cause the very next WriteFile call to fail, and I have no idea why. According to the documentation I read FSCTL_LOCK_VOLUME should prevent any other process from acquiring a handle to the disk, so I can't understand why those things could cause issues on a locked volume. Can anyone elaborate and provide a way around it?

Here's my code: it's written in PowerShell but it could easily be converted to C# or something similar.

function Start-RawCopy
{
    [CmdletBinding()]

    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='FileAndFile')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='FileAndDisk')]
        [System.IO.FileInfo]
        $SourceFile,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndFile')]
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndDisk')]
        [UInt16]
        $SourceDiskNumber,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='FileAndFile')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndFile')]
        [System.IO.FileInfo]
        $DestinationFile,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='FileAndDisk')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndDisk')]
        [UInt16]
        $DestinationDiskNumber
    )

    Begin
    {
        $FSCTL_LOCK_VOLUME = 0x00090018
        $FSCTL_UNLOCK_VOLUME = 0x0009001c
        $FSCTL_DISMOUNT_VOLUME = 0x00090020

        $DELETE = 0x00010000
        $READ_CONTROL = 0x00020000
        $WRITE_DAC = 0x00040000
        $WRITE_OWNER = 0x00080000
        $SYNCHRONIZE = 0x00100000
        $STANDARD_RIGHTS_REQUIRED = 0x000F0000
        $STANDARD_RIGHTS_READ = $READ_CONTROL
        $STANDARD_RIGHTS_WRITE = $READ_CONTROL
        $STANDARD_RIGHTS_EXECUTE = $READ_CONTROL
        $FILE_READ_DATA = 0x0001  # file & pipe
        $FILE_LIST_DIRECTORY = 0x0001  # directory
        $FILE_WRITE_DATA = 0x0002  # file & pipe
        $FILE_ADD_FILE = 0x0002  # directory
        $FILE_APPEND_DATA = 0x0004  # file
        $FILE_ADD_SUBDIRECTORY = 0x0004  # directory
        $FILE_CREATE_PIPE_INSTANCE = 0x0004  # named pipe
        $FILE_READ_EA = 0x0008  # file & directory
        $FILE_WRITE_EA = 0x0010  # file & directory
        $FILE_EXECUTE = 0x0020  # file
        $FILE_TRAVERSE = 0x0020  # directory
        $FILE_DELETE_CHILD = 0x0040  # directory
        $FILE_READ_ATTRIBUTES = 0x0080  # all
        $FILE_WRITE_ATTRIBUTES = 0x0100  # all

        $FILE_ALL_ACCESS = $STANDARD_RIGHTS_REQUIRED -bor $SYNCHRONIZE -bor 0x1FF
        $FILE_GENERIC_READ = $STANDARD_RIGHTS_READ -bor $FILE_READ_DATA -bor $FILE_READ_ATTRIBUTES -bor $FILE_READ_EA -bor $SYNCHRONIZE
        $FILE_GENERIC_WRITE = $STANDARD_RIGHTS_WRITE -bor $FILE_WRITE_DATA -bor $FILE_WRITE_ATTRIBUTES -bor $FILE_WRITE_EA -bor $FILE_APPEND_DATA -bor $SYNCHRONIZE
        $FILE_GENERIC_EXECUTE = $STANDARD_RIGHTS_EXECUTE -bor $FILE_READ_ATTRIBUTES -bor $FILE_EXECUTE -bor $SYNCHRONIZE

        $FILE_SHARE_DELETE = 0x00000004
        $FILE_SHARE_READ = 0x00000001
        $FILE_SHARE_WRITE = 0x00000002

        $CREATE_NEW = 1
        $CREATE_ALWAYS = 2
        $OPEN_EXISTING = 3
        $OPEN_ALWAYS = 4
        $TRUNCATE_EXISTING = 5

        Add-Type -AssemblyName System.Core
    }

    Process
    {
        # Validate parameters        
        if ($PSBoundParameters.ContainsKey('SourceFile'))
        {
            if (!$SourceFile.Exists)
            {
                throw "Source file does not exist, cannot continue"
            }

            $source = $SourceFile.FullName
        }
        else 
        {
            $source = "\\.\PhysicalDrive$SourceDiskNumber"
        }

        if ($PSBoundParameters.ContainsKey('DestinationFile'))
        {
            $destination = $DestinationFile.FullName
            $creationDisposition = $CREATE_ALWAYS
        }
        else 
        {
            $destination = "\\.\PhysicalDrive$DestinationDiskNumber"
            $creationDisposition = $OPEN_EXISTING
        }

        try 
        {
            # Start process
            Write-Warning "Starting raw copy - $(Get-Date)"

            # Open source object
            $handleSource = [NativeMethods]::CreateFile($source, $FILE_GENERIC_READ, $FILE_SHARE_READ -bor $FILE_SHARE_WRITE, 0, $OPEN_EXISTING, 0, [IntPtr]::Zero)

            if (!$handleSource)
            {
                throw "Failed to open source object to read"
            }

            # Clean destination volume
            if ($PSBoundParameters.ContainsKey('DestinationDiskNumber'))
            {
                Get-Disk -Number $DestinationDiskNumber | Get-Partition | Remove-Partition -Confirm:$false
                # Without a sleep this will fail on the 5th WriteFile call all the time. Some caching issue?
                Start-Sleep -Seconds 5
            }

            # Open destination object
            $handleDestination = [NativeMethods]::CreateFile($destination, $FILE_ALL_ACCESS, $FILE_SHARE_READ -bor $FILE_SHARE_WRITE -bor $FILE_SHARE_DELETE, 0, $creationDisposition, 0, [IntPtr]::Zero)

            if (!$handleDestination)
            {
                throw "Failed to open destination object to write"
            }

            $i = 0

            # Lock and dismount source volume
            if ($PSBoundParameters.ContainsKey('SourceDiskNumber'))
            {
                if (![NativeMethods]::DeviceIoControl($handleSource, $FSCTL_LOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
                {
                    throw "Failed to lock source volume"
                }

                if (![NativeMethods]::DeviceIoControl($handleSource, $FSCTL_DISMOUNT_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
                {
                    throw "Failed to dismount source volume"
                }
            }

            # Lock and dismount destination volume
            if ($PSBoundParameters.ContainsKey('DestinationDiskNumber'))
            {
                if (![NativeMethods]::DeviceIoControl($handleDestination, $FSCTL_LOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
                {
                    throw "Failed to lock destination volume"
                }

                if (![NativeMethods]::DeviceIoControl($handleDestination, $FSCTL_DISMOUNT_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
                {
                    throw "Failed to dismount destination volume"
                }
            }

            # Get source size
            if ($PSBoundParameters.ContainsKey('SourceDiskNumber'))
            {
                $sourceSize = (Get-CimInstance -Namespace ROOT/Microsoft/Windows/Storage -Class MSFT_Disk -Filter "Number = $SourceDiskNumber").Size
            }
            else
            {
                $sourceSize = $SourceFile.Length
            }

            $buffer = [Byte[]]::new(32MB)
            $bytesRead = [UInt32] 0
            $bytesWritten = [UInt32] 0
            [UInt64] $totalBytesRead = 0

            # Copy data
            do
            {
                if ($totalBytesRead + $buffer.Length -gt $sourceSize)
                {
                    $buffer = [Byte[]]::new($sourceSize - $totalBytesRead)

                    if ($Validate)
                    {
                        $verifyBuffer = [Byte[]]::new($buffer.Length)
                    }
                }

                if (![NativeMethods]::ReadFile($handleSource, $buffer, $buffer.Length, [ref] $bytesRead, [IntPtr]::Zero))
                {
                    throw "Failed to read from source"
                }

                if (![NativeMethods]::WriteFile($handleDestination, $buffer, $bytesRead, [ref] $bytesWritten, [IntPtr]::Zero))
                {
                    throw "Failed to write to destination"
                }

                if ($bytesRead -ne $bytesWritten)
                {
                    throw "Read $bytesRead bytes but only wrote $bytesWritten bytes"
                }

                $totalBytesRead += $bytesRead                
            } until ($totalBytesRead -eq $sourceSize)
        }
        finally
        {
            # Unlock source volume
            if ($PSBoundParameters.ContainsKey('SourceDiskNumber'))
            {
                if ($handleSource -and ![NativeMethods]::DeviceIoControl($handleSource, $FSCTL_UNLOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
                {
                    throw "Failed to unlock source volume"
                }
            }

            # Unlock destination volume
            if ($PSBoundParameters.ContainsKey('DestinationDiskNumber'))
            {
                if ($handleDestination -and ![NativeMethods]::DeviceIoControl($handleDestination, $FSCTL_UNLOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
                {
                    throw "Failed to unlock destination volume"
                }
            }

            # Close source handle
            if ($handleSource -and ![NativeMethods]::CloseHandle($handleSource))
            {
                throw "Failed to close source object"
            }

            # Close destination handle
            if ($handleDestination -and ![NativeMethods]::CloseHandle($handleDestination))
            {
                throw "Failed to close destination object"
            }
            Write-Warning "Ended raw copy - $(Get-Date)"
        }

    }
}
c#
powershell
winapi
pinvoke
asked on Stack Overflow May 25, 2018 by cogumel0

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0