Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Samba file share - File.Move not working #54966

Closed
markusmobius opened this issue Jun 30, 2021 · 5 comments · Fixed by #55256
Closed

Samba file share - File.Move not working #54966

markusmobius opened this issue Jun 30, 2021 · 5 comments · Fixed by #55256
Assignees
Milestone

Comments

@markusmobius
Copy link

After upgrading from 3.1 to 5 a program stopped working on ubuntu (both 18 and 20.04) that relies heavily on File.Move to move files across two different CIFS shares (with Windows as the host).

I upgraded cifs-utils but to no avail: the file seems to have been copied all right but then it throws the following error:
System.UnauthorizedAccessException exception - Access to the path is denied.

I assume this happens when the API call tries to delete the original file. I can delete that file manually with File.Delete - so it doesn't seem to be a permission issue.

I now changed the code which seems to work:

                            /*if (File.Exists(localFileName))
                            {
                                File.Delete(localFileName);
                            }
                            File.Move(tempFN, localFileName);*/
                            var arr = File.ReadAllBytes(tempFN);
                            File.WriteAllBytes(localFileName, arr);
                            File.Delete(tempFN);

But the original way should work too and seems much faster if the two files are on the same drive.

@mairaw mairaw transferred this issue from dotnet/core Jun 30, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Jun 30, 2021
@ghost
Copy link

ghost commented Jun 30, 2021

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

After upgrading from 3.1 to 5 a program stopped working on ubuntu (both 18 and 20.04) that relies heavily on File.Move to move files across two different CIFS shares (with Windows as the host).

I upgraded cifs-utils but to no avail: the file seems to have been copied all right but then it throws the following error:
System.UnauthorizedAccessException exception - Access to the path is denied.

I assume this happens when the API call tries to delete the original file. I can delete that file manually with File.Delete - so it doesn't seem to be a permission issue.

I now changed the code which seems to work:

                            /*if (File.Exists(localFileName))
                            {
                                File.Delete(localFileName);
                            }
                            File.Move(tempFN, localFileName);*/
                            var arr = File.ReadAllBytes(tempFN);
                            File.WriteAllBytes(localFileName, arr);
                            File.Delete(tempFN);

But the original way should work too and seems much faster if the two files are on the same drive.

Author: markusmobius
Assignees: -
Labels:

area-System.IO, untriaged

Milestone: -

@adamsitnik
Copy link
Member

Hi @markusmobius

Could you please provide a full stack trace?

@adamsitnik adamsitnik self-assigned this Jul 1, 2021
@adamsitnik adamsitnik added this to the 7.0.0 milestone Jul 1, 2021
@adamsitnik adamsitnik added needs author feedback and removed untriaged New issue has not been triaged by the area owner labels Jul 1, 2021
@adamsitnik
Copy link
Member

@markusmobius I currently don't have an access to a CIFS and I can't properly test a fix that I've been working on. Is there any chance that you could run the following app with a path to a writeable folder as an argument and share the output with me?

The app creates a file and tries to acquire few different type of locks before writing to it.

dotnet run -- $pathToFolder
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    </PropertyGroup>
    <ItemGroup>
      <PackageReference Include="Tmds.LibC" Version="0.5.0" />
    </ItemGroup>
</Project>
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using Tmds.Linux;
using static Tmds.Linux.LibC;

namespace FileSystemRepro
{
    class Program
    {
        static void Main(string[] args)
        {
            string directoryPath = args[0];
            string fileName = Guid.NewGuid().ToString();
            string filePath = Path.Combine(directoryPath, fileName);

            try
            {
                CreateNoLocks(filePath);
                File.Delete(filePath);

                CreateFLock(filePath, LockOperations.LOCK_EX);
                File.Delete(filePath);
                
                CreateFLock(filePath, LockOperations.LOCK_SH);
                File.Delete(filePath);
                
                CreateFcntl(filePath, LockOperations.LOCK_EX);
                File.Delete(filePath);
                
                CreateFcntl(filePath, LockOperations.LOCK_SH);
                File.Delete(filePath);
            }
            finally
            {
                if (File.Exists(filePath))
                {
                    File.Delete(filePath);
                }
            }
        }

        private static void CreateNoLocks(string filePath)
        {
            using (SafeFileHandle sfh = CreateNewFile(filePath))
            using (FileStream fs = new FileStream(sfh, FileAccess.ReadWrite, bufferSize: 1, isAsync: false))
            {
                WriteAndReadSimple(fs);
            }
        }
        
        private static void CreateFLock(string filePath, LockOperations lockOperations)
        {
            using (SafeFileHandle sfh = CreateNewFile(filePath))
            using (FileStream fs = new FileStream(sfh, FileAccess.ReadWrite, bufferSize: 1, isAsync: false))
            {
                Console.WriteLine($"Using {lockOperations} flock to lock the file");
                int lockResult = flock(sfh.DangerousGetHandle().ToInt32(), lockOperations);
                if (lockResult == -1)
                {
                    int errorCode = errno;
                    Console.WriteLine($"Locking {filePath} failed with error code {errorCode} and message: {GetErrorMessage(errorCode)}");
                }

                try
                {
                    WriteAndReadSimple(fs);
                }
                finally
                {
                    flock(sfh.DangerousGetHandle().ToInt32(), LockOperations.LOCK_UN);
                }
            }
        }
        
        private static unsafe void CreateFcntl(string filePath, LockOperations lockOperations)
        {
            using (SafeFileHandle sfh = CreateNewFile(filePath))
            using (FileStream fs = new FileStream(sfh, FileAccess.ReadWrite, bufferSize: 1, isAsync: false))
            {
                Console.WriteLine($"Using {lockOperations} fcntl to lock the file");
                flock flock = default;
                flock.l_whence = (short)SEEK_SET;
                flock.l_start = 0;
                flock.l_len = 0; // lock entire file
                flock.l_type = (short) lockOperations;
                
                int lockResult = fcntl(sfh.DangerousGetHandle().ToInt32(), F_SETLK, &flock);
                if (lockResult == -1)
                {
                    int errorCode = errno;
                    Console.WriteLine($"Locking {filePath} failed with error code {errorCode} and message: {GetErrorMessage(errorCode)}");
                }

                try
                {
                    WriteAndReadSimple(fs);
                }
                finally
                {
                    flock.l_type = (short) LockOperations.LOCK_UN;
                    fcntl(sfh.DangerousGetHandle().ToInt32(), F_SETLK, &flock);
                }
            }
        }

        private static void WriteAndReadSimple(FileStream fs)
        {
            Console.WriteLine($"Length: {fs.Length}, position: {fs.Position}");
            fs.WriteByte(123);
            Console.WriteLine($"After writing one byte: length: {fs.Length}, position: {fs.Position}");
            fs.Position -= 1;
            int read = fs.ReadByte();
            if (read != 123)
            {
                Console.WriteLine($"Invalid content {read}");
            }
        }

        private static unsafe SafeFileHandle CreateNewFile(string filePath)
        {
            byte[] utf8Path = Encoding.UTF8.GetBytes(filePath);
            fixed (byte* pinnedPath = utf8Path)
            {
                int createNewFileFlags = O_CREAT | O_RDWR | O_EXCL;
                mode_t openPermissions =  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; // read and write for all
                int result = open(pinnedPath, createNewFileFlags, openPermissions);
                
                if (result == -1)
                {
                    int errorCode = errno;
                    throw new Exception($"Creating {filePath} failed with error code {errorCode} and message: {GetErrorMessage(errorCode)}");
                }

                return new SafeFileHandle(new IntPtr(result), true);
            }
        }
        
        private static unsafe string GetErrorMessage(int errno)
        {
            int bufferLength = 1024;
            byte* buffer = stackalloc byte[bufferLength];

            int rv = strerror_r(errno, buffer, bufferLength);

            return rv == 0 ? Marshal.PtrToStringAnsi((IntPtr)buffer) : $"errno {errno}";
        }

        private enum LockOperations : int
        {
            LOCK_SH = 1,    /* shared lock */
            LOCK_EX = 2,    /* exclusive lock */
            LOCK_NB = 4,    /* don't block when locking*/
            LOCK_UN = 8,    /* unlock */
        }
        
        [DllImport("libc", SetLastError = true, ExactSpelling = true)]
        private static extern int flock(int fd, LockOperations operation);
    }
}

@jeffhandley jeffhandley modified the milestones: 7.0.0, 6.0.0 Jul 7, 2021
@jeffhandley jeffhandley self-assigned this Jul 7, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 8, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 9, 2021
@markusmobius
Copy link
Author

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    </PropertyGroup>
    <ItemGroup>
      <PackageReference Include="Tmds.LibC" Version="0.5.0" />
    </ItemGroup>
</Project>

I am sorry about the delay. Thank you so much for responding so quickly.

I ran the program and here is the output:

Length: 0, position: 0
After writing one byte: length: 1, position: 1
Using LOCK_EX flock to lock the file
Length: 0, position: 0
After writing one byte: length: 1, position: 1
Using LOCK_SH flock to lock the file
Length: 0, position: 0
After writing one byte: length: 1, position: 1
Using LOCK_EX fcntl to lock the file
Length: 0, position: 0
After writing one byte: length: 1, position: 1
Using LOCK_SH fcntl to lock the file
Length: 0, position: 0
After writing one byte: length: 1, position: 1

This was on runtime 5.0.300.

@ghost ghost locked as resolved and limited conversation to collaborators Aug 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
5 participants