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

Tar, Zip: respect umask when creating files. #71647

Merged
merged 5 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,5 @@ private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath)
Debug.Assert(!string.IsNullOrEmpty(hardLinkFilePath));
Interop.CheckIo(Interop.Sys.Link(targetFilePath, hardLinkFilePath), hardLinkFilePath);
}

// Unix specific implementation of the method that specifies the file permissions of the extracted file.
private void SetModeOnFile(SafeFileHandle handle)
{
// Only extract USR, GRP, and OTH file permissions, and ignore
// S_ISUID, S_ISGID, and S_ISVTX bits.
// It is off by default because it's possible that a file in an archive could have
// one of these bits set and, unknown to the person extracting, could allow others to
// execute the file as the user or group.
const int ExtractPermissionMask = 0x1FF;
int permissions = (int)Mode & ExtractPermissionMask;

// If the permissions weren't set at all, don't write the file's permissions.
if (permissions != 0)
{
File.SetUnixFileMode(handle, (UnixFileMode)permissions);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,5 @@ private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath)
Debug.Assert(!string.IsNullOrEmpty(hardLinkFilePath));
Interop.Kernel32.CreateHardLink(hardLinkFilePath, targetFilePath);
}

// Mode is not used on Windows.
#pragma warning disable CA1822 // Member 'SetModeOnFile' does not access instance data and can be marked as static
private void SetModeOnFile(SafeFileHandle handle)
#pragma warning restore CA1822
{
// TODO: Verify that executables get their 'executable' permission applied on Windows when extracted, if applicable. https://github.com/dotnet/runtime/issues/68230
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,19 @@ private void ExtractAsRegularFile(string destinationFileName)
{
Access = FileAccess.Write,
Mode = FileMode.CreateNew,
Share = FileShare.None,
PreallocationSize = Length,
Share = FileShare.None
};
if (OperatingSystem.IsWindows())
{
// On Windows, PreallocationSize gives a perf improvement.
tmds marked this conversation as resolved.
Show resolved Hide resolved
fileStreamOptions.PreallocationSize = Length;
}
else
{
// Restore permissions.
// For security, limit to ownership permissions, and respect umask (through UnixCreateMode).
fileStreamOptions.UnixCreateMode = Mode & (UnixFileMode)0x1FF;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
// Rely on FileStream's ctor for further checking destinationFileName parameter
using (FileStream fs = new FileStream(destinationFileName, fileStreamOptions))
{
Expand All @@ -556,7 +566,6 @@ private void ExtractAsRegularFile(string destinationFileName)
// Important: The DataStream will be written from its current position
DataStream.CopyTo(fs);
}
SetModeOnFile(fs.SafeFileHandle);
}

ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime);
Expand All @@ -578,6 +587,17 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell
PreallocationSize = Length,
Options = FileOptions.Asynchronous
};
if (OperatingSystem.IsWindows())
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
// On Windows, PreallocationSize gives a perf improvement.
fileStreamOptions.PreallocationSize = Length;
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
// Restore permissions.
// For security, limit to ownership permissions, and respect umask (through UnixCreateMode).
fileStreamOptions.UnixCreateMode = Mode & (UnixFileMode)0x1FF;
}
// Rely on FileStream's ctor for further checking destinationFileName parameter
FileStream fs = new FileStream(destinationFileName, fileStreamOptions);
await using (fs)
Expand All @@ -587,7 +607,6 @@ private async Task ExtractAsRegularFileAsync(string destinationFileName, Cancell
// Important: The DataStream will be written from its current position
await DataStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
SetModeOnFile(fs.SafeFileHandle);
}

ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, ModificationTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
<!-- Unix specific files -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == ''">
<Compile Include="System\IO\Compression\ZipFileExtensions.ZipArchive.Create.Unix.cs" />
<Compile Include="System\IO\Compression\ZipFileExtensions.ZipArchiveEntry.Extract.Unix.cs" />
<Compile Include="$(CommonPath)System\IO\Compression\ZipArchiveEntryConstants.Unix.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.IOErrors.cs"
Link="Common\Interop\Unix\Interop.IOErrors.cs" />
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,35 @@ public static void ExtractToFile(this ZipArchiveEntry source, string destination
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(destinationFileName);

// Rely on FileStream's ctor for further checking destinationFileName parameter
FileMode fMode = overwrite ? FileMode.Create : FileMode.CreateNew;
FileStreamOptions fileStreamOptions = new()
{
Access = FileAccess.Write,
Mode = overwrite ? FileMode.Create : FileMode.CreateNew,
Share = FileShare.None,
BufferSize = 0x1000
};

using (FileStream fs = new FileStream(destinationFileName, fMode, FileAccess.Write, FileShare.None, bufferSize: 0x1000, useAsync: false))
// Restore Unix permissions.
if (!OperatingSystem.IsWindows() && source.ExternalAttributes != 0)
{
// For security, limit to ownership permissions, and respect umask (through UnixCreateMode).
UnixFileMode mode = (UnixFileMode)((source.ExternalAttributes >> 16) & 0x1FF);

if (mode != UnixFileMode.None) // .zip files created on Windows don't include permissions.
{
fileStreamOptions.UnixCreateMode = mode;
}
}

using (FileStream fs = new FileStream(destinationFileName, fileStreamOptions))
{
using (Stream es = source.Open())
es.CopyTo(fs);

ExtractExternalAttributes(fs, source);
}

ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, source.LastWriteTime);
}

static partial void ExtractExternalAttributes(FileStream fs, ZipArchiveEntry entry);

internal static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryName) =>
ExtractRelativeToDirectory(source, destinationDirectoryName, overwrite: false);

Expand Down
29 changes: 14 additions & 15 deletions src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public partial class ZipFile_Unix : ZipFileTestBase
public void UnixCreateSetsPermissionsInExternalAttributes()
{
// '7600' tests that S_ISUID, S_ISGID, and S_ISVTX bits get preserved in ExternalAttributes
string[] testPermissions = new[] { "777", "755", "644", "600", "7600" };
string[] testPermissions = new[] { "700", "755", "644", "600", "7600" };
tmds marked this conversation as resolved.
Show resolved Hide resolved

using (var tempFolder = new TempDirectory(Path.Combine(GetTestFilePath(), "testFolder")))
{
Expand Down Expand Up @@ -60,7 +60,7 @@ void EnsureExternalAttributes(string permissions, ZipArchiveEntry entry)
public void UnixExtractSetsFilePermissionsFromExternalAttributes()
{
// '7600' tests that S_ISUID, S_ISGID, and S_ISVTX bits don't get extracted to file permissions
string[] testPermissions = new[] { "777", "755", "644", "754", "7600" };
string[] testPermissions = new[] { "700", "755", "644", "754", "7600" };

string archivePath = GetTestFilePath();
using (FileStream fileStream = new FileStream(archivePath, FileMode.CreateNew))
Expand Down Expand Up @@ -199,23 +199,22 @@ await Task.WhenAll(

private static string GetExpectedPermissions(string expectedPermissions)
{
if (string.IsNullOrEmpty(expectedPermissions))
using (var tempFolder = new TempDirectory())
{
// Create a new file, and get its permissions to get the current system default permissions

using (var tempFolder = new TempDirectory())
string filename = Path.Combine(tempFolder.Path, Path.GetRandomFileName());
FileStreamOptions fileStreamOptions = new()
{
string filename = Path.Combine(tempFolder.Path, Path.GetRandomFileName());
File.WriteAllText(filename, "contents");

Interop.Sys.FileStatus status;
Assert.Equal(0, Interop.Sys.Stat(filename, out status));

expectedPermissions = Convert.ToString(status.Mode & 0xFFF, 8);
Access = FileAccess.Write,
Mode = FileMode.CreateNew
};
if (expectedPermissions != null)
{
fileStreamOptions.UnixCreateMode = (UnixFileMode)Convert.ToInt32(expectedPermissions, 8);
}
}
new FileStream(filename, fileStreamOptions).Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Are the current tests failing without this change? Before we would only create a new file when the expectedPermissions was null or empty. For the other 3 .zip files, we were straight expecting the mode hard-coded into the tests.
Now we are always creating a new file and using its mode.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. The current tests require the exact permission. Here we're determining the expected permission that takes into account the umask.

Copy link
Member Author

Choose a reason for hiding this comment

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

The changes to this test suite are to update the expected permissions so they take into account the umask.
I think they still cover what was intended.

@eerhardt is this good for you?

Copy link
Member

Choose a reason for hiding this comment

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

My concern is that we aren't testing that GroupWrite and OtherWrite bits are set correctly anymore. I think we should have tests that respect the umask (what you are fixing here) and tests that clear the umask and ensure the full permissions are kept.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added tests that run with a zero umask. ptal.


return expectedPermissions;
return Convert.ToString((int)File.GetUnixFileMode(filename), 8);
}
}

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)]
Expand Down