Skip to content

Conversation

@carlossanlop
Copy link
Contributor

@carlossanlop carlossanlop commented Sep 21, 2021

Fixes #57768

Fixes in this PR:

  • The ACL extension method FileStream.Create should accept a null FileSecurity parameter so the behavior can match .NET Framework. The ref definition had to be changed to a nullable parameter. @bartonjs @GrabYourPitchforks @stephentoub Do I need to go through API review to request this change or can we validate it here? This will be marked as a breaking change because an exception case was removed.

  • FileStream throws ArgumentOutOfRangeException if the passed FileAccess is not valid. In .NET Framework, this was not a problem because all the ACL logic was contained within the FileStream constructor, so it was possible to simply pass the FileSystemRights enum value casted as a FileAccess enum value to the p/invoke. Here, the ACL logic is in an extension method, and we don't want to do such casting. To address this, I am now mapping all the writeable FileSystemRights values to a FileAccess write or read value. I left a detailed explanation here of what we were doing before.

  • Additional bug fix: The FileShare.Inheritable bit 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). Otherwise, the P/Invoke causes an IOException to be thrown with the message The parameter is incorrect.

  • Added unit tests to verify all the write combinations when the FileSecurity argument is null, which also includes the code example in the original issue.

@ghost
Copy link

ghost commented Sep 21, 2021

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost
Copy link

ghost commented Sep 21, 2021

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

Issue Details

Fixes #57768

Fixes in this PR:

  • The ACL extension method FileStream.Create should accept a null FileSecurity parameter so the behavior can match .NET Framework. The ref definition had to be changed to a nullable parameter. @bartonjs @GrabYourPitchforks @stephentoub Do I need to go through API review to request this change or can we validate it here? This will be marked as a breaking change because an exception case was removed.

  • FileStream throws ArgumentOutOfRangeException if the passed FileAccess is not valid. In .NET Framework, this was not a problem because all the ACL logic was contained within the FileStream constructor, so it was possible to simply pass the FileSystemRights enum value casted as a FileAccess enum value to the p/invoke. Here, the ACL logic is in an extension method, and we don't want to do such casting. To address this, I am now mapping all the writeable FileSystemRights values to a FileAccess write or read value. I left a detailed explanation here of what we were doing before.

  • Added unit tests to verify all the write combinations when the FileSecurity argument is null, which also includes the code example in the original issue.

Author: carlossanlop
Assignees: carlossanlop
Labels:

area-System.IO

Milestone: 7.0.0

@bartonjs
Copy link
Member

The ACL extension method FileStream.Create should accept a null FileSecurity parameter so the behavior can match .NET Framework. ... API Review ... ?

Moving from not allowing null to allowing null is fine. I don't think as reviewers we have the muscles developed to be good at inquiring about the state of question marks, and that direction isn't a breaking change.

This will be marked as a breaking change because an exception case was removed.

This the poster child for a "visible behavioral change" that isn't a breaking change. (ArgumentNullException means the problem was in their code. So if we allow null now and they didn't want to, that error is still in their code)

https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/breaking-change-rules.md (Exceptions, Allowed, last bullet)

Copy link
Member

@jeffhandley jeffhandley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Let's get another review from @adamsitnik or @jozkee though.

Comment on lines +259 to 286
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already have a SafeFileHandle that was opened with the correct FileSystemRights, the sole purpose of passing FileAccess here is to indicate whether the Stream supports reading/writing (or both).

With that said, I think this mapping logic is too complex and doesn't really achieve much. What do you think of simplifying it to always return Read as default and include Write in the special cases?

Copy link
Member

@jozkee jozkee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some suggestions for you to consider.

Comment on lines 381 to 382
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What things can you do with a file created with a non-null FileSecurity argument vs one created with a null one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can create a file with specific access rules and audit rules to allow or deny permissions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't include variable buffer size in this tests, I don't see how that relates with ACL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about readables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are disallowed.

If you run a test console app on .NET Framework:

string path = "file.txt";
FileMode mode = FileMode.CreateNew;
FileSystemRights rights = FileSystemRights.ReadAndExecute;
FileShare share = FileShare.None;
int bufferSize = 4096;
FileOptions options = FileOptions.None;

using (FileStream fs = new FileStream(path, mode, rights, share, bufferSize, options, null)){}

You get the following exception:

'Combining FileMode: CreateNew with FileSystemRights: ReadAndExecute is invalid.'

This exception was originally thrown at this call stack:
    System.IO.FileStream.Init(string, System.IO.FileMode, System.IO.FileAccess, int, bool, System.IO.FileShare, int, System.IO.FileOptions, Microsoft.Win32.Win32Native.SECURITY_ATTRIBUTES, string, bool, bool, bool) in filestream.cs
    System.IO.FileStream.FileStream(string, System.IO.FileMode, System.Security.AccessControl.FileSystemRights, System.IO.FileShare, int, System.IO.FileOptions, System.Security.AccessControl.FileSecurity) in filestream.cs
    ConsoleFramework.Program.Main() in Program.cs

So we do a similar verification in .NET 7:

 System.ArgumentException : Combining FileMode.CreateNew with FileSystemRights.ReadAndExecute is invalid.
        Stack Trace:
          C:\Users\calope\source\repos\runtime\src\libraries\System.IO.FileSystem.AccessControl\src\System\IO\FileSystemAclExtensions.cs(202,0): at System.IO.FileSystemAclExtensions.Create(FileInfo fileInfo, FileMode mode, FileSystemRights rights, FileShare share, Int32 bufferSize, FileOptions options, FileSecurity fileSecurity)

I'm comparing the output of .NET Framework vs .NET 7 with all the combinations of FileMode and FileSystemRights to determine if I need to special-case some unit tests.

Comment on lines +551 to +547
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not test all values in the enum?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be tested with null and non-null FileSecurity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this return when you create the file with a null FileSecurity? I assume it doesn't return null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns a FileSecurity object that contains zero access rules. I added a test for that case.

@jozkee
Copy link
Member

jozkee commented Oct 22, 2021

This will be marked as a breaking change because an exception case was removed.

That's not a breaking change, see https://docs.microsoft.com/en-us/dotnet/core/compatibility/#exceptions

✔️ ALLOWED: Removing an exception to enable more robust behavior or new scenarios

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetFileStreamFileAccess should not return 0, as FileStream is simply going to throw. Please see my comments.

Do we have a unit test that ensures that FileSystemRights.AppendData is working the way it was shown in #57768?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 is illegal value for FileAccess, the FileStream ctor is going to throw:

else if (access < FileAccess.Read || access > FileAccess.ReadWrite)

What was the Full .NET Framework doing when user was passing zero for FileSystemRights?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird. The unit tests are passing, the FileStream constructor is not throwing. I'm investigating.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, notice that we pass (FileAccess)0 as an argument to Init in .NET Framework:
https://referencesource.microsoft.com/#mscorlib/system/io/filestream.cs,569

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synchronize is not related to reading or writing:

/// <summary>
/// The right to use the object for synchronization. Enables a thread to wait until the object
/// is in the signaled state. This is required if opening a synchronous handle.
/// </summary>
SYNCHRONIZE = 0x00100000,

Suggested change
(rights & FileSystemRights.Synchronize) != 0 ||

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I wrote above, currently 0 is invalid value for FileAccess.

I would start with ReadWrite and just negate for cases when read|write intent was not specified.

private static FileAccess GetFileStreamFileAccess(FileSystemRights rights)
{
    FileAccess access = FileAccess.ReadWrite;
    if ((rights & FileSystemRights.Read) == 0 && ((int)rights & Interop.Kernel32.GenericOperations.GENERIC_READ) == 0)
        access &= ~FileAccess.Read;
    if ((rights & FileSystemRights.Write) == 0 && ((int)rights & Interop.Kernel32.GenericOperations.GENERIC_WRITE) == 0)
        access &= ~FileAccess.Write;
    if (access == 0)
        // do what Full Framework did here (use a default value or throw, based on what it actually was)
    return access;
}

But using the code provided above, we still end up with few cases where we end up with 0:

static void Main(string[] args)
{
    foreach (FileSystemRights flag in Enum.GetValues<FileSystemRights>().Select(flag => (int)flag).Distinct())
    {
        Console.WriteLine($"{flag} {GetFileStreamFileAccess(flag)}");
    }
}
ReadData Read
CreateFiles Write
AppendData Write
ReadExtendedAttributes Read
WriteExtendedAttributes Write
ExecuteFile 0
DeleteSubdirectoriesAndFiles 0
ReadAttributes Read
WriteAttributes Write
Write Write
Delete 0
ReadPermissions Read
Read Read
ReadAndExecute Read
Modify ReadWrite
ChangePermissions 0
TakeOwnership 0
Synchronize 0
FullControl ReadWrite

I don't know what Full .NET Framework was doing in cases where the intent was not specified, example:

FileSystemRights.Synchronize

it would be great to find out what is was doing and either:

  • use the same default value
  • throw the same exception (but improve error message if needed)

I am also not sure why anyone would like to create a FileStream without read or write rights. Perhaps just to access FileStream.SafeFileHandle and never use any of the FileStream.Read* and FileStream.Write* methods?

cc @JeremyKuhne who might know the answer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also not sure why anyone would like to create a FileStream without read or write rights.

To use as a sentinel perhaps. If all you want to do is keep a file handle open you wouldn't need anything but delete I believe.

Copy link
Contributor Author

@carlossanlop carlossanlop Nov 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joperezr Do we still need to check for PlatformDetection.IsNetFramework or can I remove those checks?

@carlossanlop
Copy link
Contributor Author

Closing and resending PR to make it easier to review.

@carlossanlop carlossanlop deleted the FSACnull branch November 7, 2021 21:40
@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FileSystemAclExtensions.Create broken in multiple ways

6 participants