-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Fix FileSystemAclExtensions.Create when passing a null FileSecurity #59389
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
Conversation
|
Note regarding the 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. |
|
Tagging subscribers to this area: @dotnet/area-system-io Issue DetailsFixes #57768 Fixes in this PR:
|
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 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) |
jeffhandley
left a comment
There was a problem hiding this 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.
There was a problem hiding this comment.
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?
jozkee
left a comment
There was a problem hiding this 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about readables?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
That's not a breaking change, see https://docs.microsoft.com/en-us/dotnet/core/compatibility/#exceptions
|
adamsitnik
left a comment
There was a problem hiding this 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?
There was a problem hiding this comment.
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:
runtime/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs
Line 73 in 9e795c0
| else if (access < FileAccess.Read || access > FileAccess.ReadWrite) |
What was the Full .NET Framework doing when user was passing zero for FileSystemRights?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
runtime/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs
Lines 565 to 569 in 9e795c0
| /// <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, |
| (rights & FileSystemRights.Synchronize) != 0 || |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
…le FileSystemRights.
There was a problem hiding this comment.
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?
|
Closing and resending PR to make it easier to review. |
Fixes #57768
Fixes in this PR:
The ACL extension method
FileStream.Createshould accept anullFileSecurityparameter 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.FileStreamthrowsArgumentOutOfRangeExceptionif the passedFileAccessis not valid. In .NET Framework, this was not a problem because all the ACL logic was contained within theFileStreamconstructor, so it was possible to simply pass theFileSystemRightsenum value casted as aFileAccessenum 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 writeableFileSystemRightsvalues to aFileAccesswrite or read value. I left a detailed explanation here of what we were doing before.Additional bug fix: The
FileShare.Inheritablebit needs to be saved in theSECURITY_ATTRIBUTESstruct (we are already doing this), but should not be passed to theCreateFileP/Invoke (we were not doing this). Otherwise, the P/Invoke causes anIOExceptionto be thrown with the messageThe parameter is incorrect.Added unit tests to verify all the write combinations when the
FileSecurityargument isnull, which also includes the code example in the original issue.