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

FileSystemAclExtensions.Create broken in multiple ways #57768

Closed
stephentoub opened this issue Aug 19, 2021 · 10 comments · Fixed by #61297
Closed

FileSystemAclExtensions.Create broken in multiple ways #57768

stephentoub opened this issue Aug 19, 2021 · 10 comments · Fixed by #61297
Assignees
Milestone

Comments

@stephentoub
Copy link
Member

On .NET Framework, this:

using System;
using System.IO;
using System.Security.AccessControl;

internal class Program
{
    static void Main()
    {
        string path = @"tmp.txt";
        using (FileStream fs = new FileStream(path, FileMode.Create, FileSystemRights.AppendData, FileShare.None, 1, FileOptions.None, null))
        {
            for (int i = 0; i < 26; i++)
            {
                fs.WriteByte((byte)('a' + i));
                fs.Position = 0;
            }
        }

        const string Expected = "abcdefghijklmnopqrstuvwxyz";
        string text = File.ReadAllText(path);
        Console.WriteLine(text == Expected ?
            "Success" :
            $"Expected {Expected}, got {text}");
    }
}

produces:

Success

On .NET 5/6, you should be able to use FileStreamAclExtensions.Create in the exact same way (its whole purpose for existing is to provide the exact same functionality on core):

using System;
using System.IO;
using System.Security.AccessControl;

internal class Program
{
    static void Main()
    {
        string path = @"tmp.txt";
        using (FileStream fs = FileSystemAclExtensions.Create(new FileInfo(path), FileMode.Create, FileSystemRights.AppendData, FileShare.None, 1, FileOptions.None, null))
        {
            for (int i = 0; i < 26; i++)
            {
                fs.WriteByte((byte)('a' + i));
                fs.Position = 0;
            }
        }

        const string Expected = "abcdefghijklmnopqrstuvwxyz";
        string text = File.ReadAllText(path);
        Console.WriteLine(text == Expected ?
            "Success" :
            $"Expected {Expected}, got {text}");
    }
}

but that blows up with an ArgumentNullException:

Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'fileSecurity')
   at System.IO.FileSystemAclExtensions.Create(FileInfo fileInfo, FileMode mode, FileSystemRights rights, FileShare share, Int32 bufferSize, FileOptions options, FileSecurity fileSecurity)
   at Program.Main()

If you then fix that to allow null, it then fails deeper with how it's mapping to FileStream validation of FileAccess:

Unhandled exception. System.ArgumentOutOfRangeException: Enum value was out of legal range. (Parameter 'access')
   at System.IO.FileStream.ValidateHandle(SafeFileHandle handle, FileAccess access, Int32 bufferSize) in D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\IO\FileStream.cs:line 75
   at System.IO.FileStream.ValidateHandle(SafeFileHandle handle, FileAccess access, Int32 bufferSize, Boolean isAsync) in D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\IO\FileStream.cs:line 89
   at System.IO.FileStream..ctor(SafeFileHandle handle, FileAccess access, Int32 bufferSize, Boolean isAsync) in D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\IO\FileStream.cs:line 115
   at System.IO.FileSystemAclExtensions.Create(FileInfo fileInfo, FileMode mode, FileSystemRights rights, FileShare share, Int32 bufferSize, FileOptions options, FileSecurity fileSecurity) in D:\repos\runtime\src\libraries\System.IO.FileSystem.AccessControl\src\System\IO\FileSystemAclExtensions.cs:line 204
   at Program.Main()
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Aug 19, 2021
@ghost
Copy link

ghost commented Aug 19, 2021

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

Issue Details

On .NET Framework, this:

using System;
using System.IO;
using System.Security.AccessControl;

internal class Program
{
    static void Main()
    {
        string path = @"tmp.txt";
        using (FileStream fs = new FileStream(path, FileMode.Create, FileSystemRights.AppendData, FileShare.None, 1, FileOptions.None, null))
        {
            for (int i = 0; i < 26; i++)
            {
                fs.WriteByte((byte)('a' + i));
                fs.Position = 0;
            }
        }

        const string Expected = "abcdefghijklmnopqrstuvwxyz";
        string text = File.ReadAllText(path);
        Console.WriteLine(text == Expected ?
            "Success" :
            $"Expected {Expected}, got {text}");
    }
}

produces:

Success

On .NET 5/6, you should be able to use FileStreamAclExtensions.Create in the exact same way (its whole purpose for existing is to provide the exact same functionality on core):

using System;
using System.IO;
using System.Security.AccessControl;

internal class Program
{
    static void Main()
    {
        string path = @"tmp.txt";
        using (FileStream fs = FileSystemAclExtensions.Create(new FileInfo(path), FileMode.Create, FileSystemRights.AppendData, FileShare.None, 1, FileOptions.None, null))
        {
            for (int i = 0; i < 26; i++)
            {
                fs.WriteByte((byte)('a' + i));
                fs.Position = 0;
            }
        }

        const string Expected = "abcdefghijklmnopqrstuvwxyz";
        string text = File.ReadAllText(path);
        Console.WriteLine(text == Expected ?
            "Success" :
            $"Expected {Expected}, got {text}");
    }
}

but that blows up with an ArgumentNullException:

Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'fileSecurity')
   at System.IO.FileSystemAclExtensions.Create(FileInfo fileInfo, FileMode mode, FileSystemRights rights, FileShare share, Int32 bufferSize, FileOptions options, FileSecurity fileSecurity)
   at Program.Main()

If you then fix that to allow null, it then fails deeper with how it's mapping to FileStream validation of FileAccess:

Unhandled exception. System.ArgumentOutOfRangeException: Enum value was out of legal range. (Parameter 'access')
   at System.IO.FileStream.ValidateHandle(SafeFileHandle handle, FileAccess access, Int32 bufferSize) in D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\IO\FileStream.cs:line 75
   at System.IO.FileStream.ValidateHandle(SafeFileHandle handle, FileAccess access, Int32 bufferSize, Boolean isAsync) in D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\IO\FileStream.cs:line 89
   at System.IO.FileStream..ctor(SafeFileHandle handle, FileAccess access, Int32 bufferSize, Boolean isAsync) in D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\IO\FileStream.cs:line 115
   at System.IO.FileSystemAclExtensions.Create(FileInfo fileInfo, FileMode mode, FileSystemRights rights, FileShare share, Int32 bufferSize, FileOptions options, FileSecurity fileSecurity) in D:\repos\runtime\src\libraries\System.IO.FileSystem.AccessControl\src\System\IO\FileSystemAclExtensions.cs:line 204
   at Program.Main()
Author: stephentoub
Assignees: -
Labels:

area-System.IO

Milestone: -

@jozkee
Copy link
Member

jozkee commented Aug 19, 2021

I don't think this meets the bar for 6.0, we should probably address it in 7.0 though.

@jozkee jozkee added this to the 7.0.0 milestone Aug 19, 2021
@jozkee jozkee removed the untriaged New issue has not been triaged by the area owner label Aug 19, 2021
@adamsitnik
Copy link
Member

The fix would most probably be easy, the question is whether we would be able to include it in 6.0

@jeffhandley
Copy link
Member

As a regression from .NET Framework, this would be a reasonable fix to take in RC2 so long as we're very confident in the changes and we evaluate/mitigate the potential risks. Ideally we'd get validation from main.

@carlossanlop
Copy link
Member

Is the behavior not expected to be the same if the last argument is new FileSecurity() instead of null?

@carlossanlop
Copy link
Member

This was introduced in 5.0, and also the FileSecurity argument was not approved in the API proposal as nullable. But we can change this in 7.0, and backport it to 6.0.

@stephentoub
Copy link
Member Author

Is the behavior not expected to be the same if the last argument is new FileSecurity() instead of null?

No, the implementation on .NET Framework has multiple checks that differentiate behavior when it's null and not null, e.g.
https://referencesource.microsoft.com/#mscorlib/system/io/filestream.cs,1095

also the FileSecurity argument was not approved in the API proposal as nullable

The API reviewers aren't experts in all the functionality for each area and trust the proposer for things like this. If it should be nullable, we can make it nullable.

@carlossanlop carlossanlop self-assigned this Sep 20, 2021
@carlossanlop
Copy link
Member

carlossanlop commented Sep 21, 2021

I have a partial fix for this - The API now accepts a null FileSecurity argument and handles that case by creating a SECURITY_ATTRIBUTES without a security descriptor. This handles the first exception.

For the second exception, I am still trying to decide how to best address this.

In .NET Framework, if the FileSecurity is null, but the user passes a valid FileSystemRights value, then the FileAccess value that is passed to the file creation p/invoke is the FileSystemRights value casted to a FileAccess value: https://referencesource.microsoft.com/#mscorlib/system/io/filestream.cs,705

And because all this ACL logic was done directly in the FileStream class, at that point there was no FileAccess out of range validation.

In our .NET 7.0, we generate the ACL logic in the extension method, then pass the handle and arguments to the FileStream constructor, which only allows the 3 expected FileAccess values:

else if (access < FileAccess.Read || access > FileAccess.ReadWrite)
{
badArg = nameof(access);
}

And because not all of their values match, we can't just cast the FileSystemRights enum value to a FileAccess like before:

So I'm inclined to fix this by analyzing all the values of FileSystemRights and choosing the most appropriate FileAccess value to which they belong, particularly because of the comment on the top of the method that currently does that conversion:

// In the context of a FileStream, the only ACCESS_MASK ACE rights we care about are reading/writing data and the generic read/write rights.
// See: https://docs.microsoft.com/en-us/windows/win32/secauthz/access-mask
private static FileAccess GetFileStreamFileAccess(FileSystemRights rights)
{
FileAccess access = 0;
if ((rights & FileSystemRights.ReadData) != 0 || ((int)rights & Interop.Kernel32.GenericOperations.GENERIC_READ) != 0)
{
access = FileAccess.Read;
}
if ((rights & FileSystemRights.WriteData) != 0 || ((int)rights & Interop.Kernel32.GenericOperations.GENERIC_WRITE) != 0)
{
access = access == FileAccess.Read ? FileAccess.ReadWrite : FileAccess.Write;
}
return access;
}

FileSystemRights values casted to FileAccess.Read:

  • ReadData
  • ListDirectory (same as ReadData)
  • ReadExtendedAttributes
  • ExecuteFile
  • Traverse (same as ExecuteFile)
  • ReadAttributes
  • ReadPermissions
  • Read (contains all reads)
  • ReadAndExecute (contains all reads + execute)

FileSystemRights values casted to FileAccess.Write:

  • WriteData
  • CreateFiles (same as WriteData)
  • AppendData
  • CreateDirectories (same as AppendData)
  • WriteExtendedAttributes
  • DeleteSubdirectoriesAndFiles
  • WriteAttributes
  • Delete
  • ChangePermissions
  • TakeOwnership
  • Synchronize
  • Write (contains all writes)

FileSystemRights values casted to FileAccess.ReadWrite:

  • FullControl
  • Modify (contains all reads, writes, executes)

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Sep 21, 2021
@FlsZen
Copy link

FlsZen commented Nov 4, 2021

@carlossanlop I have code that was just migrated from .NET 4.8 to .NET 6 and ran into FileSystemRights.AppendData being not mapped to FileAccess.Write (audit log files that we create with ReadData | AppendData rights). Glad to see it will be fixed in the future. 👍

@carlossanlop
Copy link
Member

I found another bug in the extension method: If FileShare.Inheritable is passed, an IOException is thrown with the message The parameter is incorrect.

This does not happen in .NET Framework.

I found the root cause: The FileShare.Inheritable needs to be saved in the SECURITY_ATTRIBUTES struct (we are already doing this), but should not be passed to the CreateFile P/Invoke (we were not doing this).

I'll include this fix in the PR.

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Nov 7, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Nov 7, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jan 24, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Feb 23, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.