Set apphost and bundle file permission to 755₈ (dotnet/core-setup#8510)
authorAdeel Mujahid <adeelbm@outlook.com>
Thu, 17 Oct 2019 21:47:27 +0000 (00:47 +0300)
committerSwaroop Sridhar <Swaroop.Sridhar@microsoft.com>
Thu, 17 Oct 2019 21:47:27 +0000 (14:47 -0700)
Set apphost and bundle file permission to 755₈

When building a .net core 3 app, the SDK currently simply copies the apphost template (including its permissions) from the install-location.

This caused two problems in Unix systems:
* If the dotnet install location is write-protected, the build fails when SDK tries update the apphost (to set the app-path, etc.)  (dotnet/core-setup#8511)
* The built apphost can only be run by the owner (dotnet/core-setup#7062)

This change explicitly sets the file permissions of the Apphost in the SDK to fix the above issues.

Commit migrated from https://github.com/dotnet/core-setup/commit/a6a9202dc186f622f5df7125b12d95d9107c193a

src/installer/corehost/CMakeLists.txt
src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
src/installer/test/Directory.Build.props
src/installer/test/HostActivation.Tests/HostActivation.Tests.csproj
src/installer/test/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUpdateTests.cs
src/installer/test/TestUtils/Command.cs
src/installer/test/TestUtils/TestUtils.csproj

index 85f6832..2b8473f 100644 (file)
@@ -1,5 +1,7 @@
 cmake_minimum_required (VERSION 3.14)
 
+project(corehost)
+
 include(../settings.cmake)
 include(../functions.cmake)
 add_subdirectory(cli)
index 9922c61..3215ed4 100644 (file)
@@ -2,8 +2,10 @@
 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
 using System;
+using System.ComponentModel;
 using System.IO;
 using System.IO.MemoryMappedFiles;
+using System.Runtime.InteropServices;
 using System.Text;
 using System.Threading;
 
@@ -19,7 +21,7 @@ namespace Microsoft.NET.HostModel.AppHost
         /// hash value embedded in default apphost executable in a place where the path to the app binary should be stored.
         /// </summary>
         private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
-        private readonly static byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
+        private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
 
         /// <summary>
         /// Create an AppHost with embedded configuration of app binary location
@@ -43,6 +45,7 @@ namespace Microsoft.NET.HostModel.AppHost
             }
 
             BinaryUtils.CopyFile(appHostSourceFilePath, appHostDestinationFilePath);
+
             bool appHostIsPEImage = false;
 
             void RewriteAppHost()
@@ -70,7 +73,7 @@ namespace Microsoft.NET.HostModel.AppHost
             }
 
             void UpdateResources()
-            { 
+            {
                 if (assemblyToCopyResorcesFrom != null && appHostIsPEImage)
                 {
                     if (ResourceUpdater.IsSupportedOS())
@@ -89,6 +92,24 @@ namespace Microsoft.NET.HostModel.AppHost
 
             try
             {
+                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                {
+                    var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
+                    const int EINTR = 4;
+                    int chmodReturnCode = 0;
+
+                    do
+                    {
+                        chmodReturnCode = chmod(appHostDestinationFilePath, filePermissionOctal);
+                    }
+                    while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
+
+                    if (chmodReturnCode == -1)
+                    {
+                        throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}.");
+                    }
+                }
+
                 RetryUtil.RetryOnIOError(RewriteAppHost);
 
                 RetryUtil.RetryOnWin32Error(UpdateResources);
@@ -133,14 +154,14 @@ namespace Microsoft.NET.HostModel.AppHost
             };
 
             // Re-write the destination apphost with the proper contents.
-            RetryUtil.RetryOnIOError(() => 
+            RetryUtil.RetryOnIOError(() =>
                 BinaryUtils.SearchAndReplace(appHostPath,
                                              bundleHeaderPlaceholder,
-                                             BitConverter.GetBytes(bundleHeaderOffset), 
-                                             pad0s:false));
+                                             BitConverter.GetBytes(bundleHeaderOffset),
+                                             pad0s: false));
 
             // Memory-mapped write does not updating last write time
-            RetryUtil.RetryOnIOError(() => 
+            RetryUtil.RetryOnIOError(() =>
                 File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow));
         }
 
@@ -183,5 +204,8 @@ namespace Microsoft.NET.HostModel.AppHost
 
             return headerOffset != 0;
         }
+
+        [DllImport("libc", SetLastError = true)]
+        private static extern int chmod(string pathname, int mode);
     }
 }
index 4b971c5..b3eaee0 100644 (file)
     <TestInfraTargetFramework>netcoreapp3.0</TestInfraTargetFramework>
   </PropertyGroup>
 
+  <PropertyGroup>
+    <TargetOSTrait Condition="'$(OSGroup)' == 'Windows_NT'">nonwindowstests</TargetOSTrait>
+    <TargetOSTrait Condition="'$(OSGroup)' == 'Linux'">nonlinuxtests</TargetOSTrait>
+    <TargetOSTrait Condition="'$(OSGroup)' == 'OSX'">nonosxtests</TargetOSTrait>
+    <TargetOSTrait Condition="'$(OSGroup)' == 'FreeBSD'">nonfreebsdtests</TargetOSTrait>
+    <TargetOSTrait Condition="'$(OSGroup)' == 'NetBSD'">nonnetbsdtests</TargetOSTrait>
+
+    <TestRunnerAdditionalArguments Condition="'$(TargetOSTrait)' != ''">-notrait category=$(TargetOSTrait)</TestRunnerAdditionalArguments>
+  </PropertyGroup>
+
 </Project>
index 76c3720..47fbf56 100644 (file)
@@ -21,9 +21,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
     <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" />
-    <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="2.1.0" />
+    <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="$(MicrosoftDotNetPlatformAbstractionsPackageVersion)" />
     <PackageReference Include="Microsoft.Win32.Registry" Version="4.3.0" />
   </ItemGroup>
 
index aaf2ae7..a48ea53 100644 (file)
@@ -1,7 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
 using System;
 using System.IO;
 using System.Linq;
+using System.Reflection;
 using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using System.Text;
 using FluentAssertions;
 using Xunit;
@@ -160,6 +166,42 @@ namespace Microsoft.NET.HostModel.Tests
             }
         }
 
+        [Fact]
+        [PlatformSpecific(TestPlatforms.AnyUnix)]
+        public void ItGeneratesExecutableImage()
+        {
+            using (TestDirectory testDirectory = TestDirectory.Create())
+            {
+                string sourceAppHostMock = PrepareAppHostMockFile(testDirectory);
+                string destinationFilePath = Path.Combine(testDirectory.Path, "DestinationAppHost.exe.mock");
+                string appBinaryFilePath = "Test/App/Binary/Path.dll";
+
+                chmod(sourceAppHostMock, Convert.ToInt32("444", 8)) // make it readonly: -r--r--r--
+                    .Should()
+                    .NotBe(-1);
+
+                GetLastError()
+                    .Should()
+                    .NotBe(4); // EINTR
+
+                GetFilePermissionValue(sourceAppHostMock)
+                    .Should()
+                    .Be(Convert.ToInt32("444", 8));
+
+                HostWriter.CreateAppHost(
+                    sourceAppHostMock,
+                    destinationFilePath,
+                    appBinaryFilePath,
+                    windowsGraphicalUserInterface: true);
+
+                GetFilePermissionValue(destinationFilePath)
+                    .Should()
+                    .Be(Convert.ToInt32("755", 8));
+            }
+
+            int GetLastError() => Marshal.GetLastWin32Error();
+        }
+
         private string PrepareAppHostMockFile(TestDirectory testDirectory, Action<byte[]> customize = null)
         {
             // For now we're testing the AppHost on Windows PE files only.
@@ -206,6 +248,64 @@ namespace Microsoft.NET.HostModel.Tests
             0, 112, 2, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3, 0, 96, 193, 0, 0, 24,
             0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0 };
 
+        [DllImport("libc", SetLastError = true)]
+        private static extern int chmod(string pathname, int mode);
+
+        private static int GetFilePermissionValue(string path)
+        {
+            var modeValue = CoreFxFileStatusProvider.GetFileMode(path);
+
+            // st_mode is typically a 16-bits value, high 4 bits are filetype and low 12
+            // bits are permission. we will clear first 20 bits (a byte and a nibble) with
+            // the following mask:
+            modeValue &= 0x1ff;
+
+            modeValue
+                .Should()
+                .BeInRange(0, 511);
+
+            return modeValue;
+        }
+
+        private static class CoreFxFileStatusProvider
+        {
+            private static FieldInfo s_fileSystem_fileStatusField, s_fileStatus_fileStatusField, s_fileStatusModeField;
+
+            static CoreFxFileStatusProvider()
+            {
+                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                {
+                    try
+                    {
+                        s_fileSystem_fileStatusField = typeof(FileSystemInfo).GetField("_fileStatus", BindingFlags.NonPublic | BindingFlags.Instance);
+                        s_fileStatus_fileStatusField = s_fileSystem_fileStatusField.FieldType.GetField("_fileStatus", BindingFlags.NonPublic | BindingFlags.Instance);
+                        s_fileStatusModeField = s_fileStatus_fileStatusField.FieldType.GetField("Mode", BindingFlags.NonPublic | BindingFlags.Instance);
+                    }
+                    catch (Exception ex)
+                    {
+                        throw new Exception("Cannot setup _fileStatus via private reflection from CoreFX. Verify if the FileSystem._fileStatus._fileStatus.Mode chain is intact in CoreFX, otherwise adjust this implementation", ex);
+                    }
+                }
+            }
+
+            public static int GetFileMode(string path)
+            {
+                try
+                {
+                    var fileInfo = new FileInfo(path);
+                    _ = fileInfo.IsReadOnly; // this is to implicitly initialize FileInfo -> FileSystem -> fielStatus instance
+
+                    return (int)s_fileStatusModeField.GetValue(
+                               s_fileStatus_fileStatusField.GetValue(
+                                   s_fileSystem_fileStatusField.GetValue(fileInfo)));
+                }
+                catch (Exception ex)
+                {
+                    throw new Exception("Cannot get stat (2) st_mode via private reflection from CoreFX. Verify if the FileSystem._fileStatus.Initialize logic is exercised via FileInfo.IsReadOnly in CoreFX, otherwise adjust this implementation.", ex);
+                }
+            }
+        }
+
         private class TestDirectory : IDisposable
         {
             public string Path { get; private set; }
@@ -233,4 +333,4 @@ namespace Microsoft.NET.HostModel.Tests
             }
         }
     }
-}
\ No newline at end of file
+}
index b818c2b..47ef88d 100644 (file)
@@ -10,7 +10,6 @@ using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Threading;
-using Microsoft.DotNet.InternalAbstractions;
 
 namespace Microsoft.DotNet.Cli.Build.Framework
 {
@@ -251,7 +250,7 @@ namespace Microsoft.DotNet.Cli.Build.Framework
         public Command WithUserProfile(string userprofile)
         {
             string userDir;
-            if (InternalAbstractions.RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             {
                 userDir = "USERPROFILE";
             }
@@ -383,7 +382,8 @@ namespace Microsoft.DotNet.Cli.Build.Framework
                 bool success = exitCode == 0;
                 string msgExpectedToFail = "";
 
-                if (fExpectedToFail) {
+                if (fExpectedToFail)
+                {
                     success = !success;
                     msgExpectedToFail = "failed as expected and ";
                 }
index c8513a0..a608e30 100644 (file)
 
   <ItemGroup>
     <PackageReference Include="FluentAssertions" Version="4.19.4" />
-    <PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.1.1" />
-    <PackageReference Include="xunit" Version="2.2.0" />
-    <PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.0.0" />
-    <PackageReference Include="Microsoft.DotNet.InternalAbstractions" Version="1.0.0" />
+    <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="$(MicrosoftDotNetPlatformAbstractionsPackageVersion)" />
+    <PackageReference Include="Microsoft.DotNet.XUnitExtensions" Version="$(MicrosoftDotNetXUnitExtensionsPackageVersion)" />
+    <PackageReference Include="xunit.core" Version="$(XUnitPackageVersion)" ExcludeAssets="build" />
   </ItemGroup>
 
 </Project>