diff --git a/src/Tasks.UnitTests/StrongNameUtils_Tests.cs b/src/Tasks.UnitTests/StrongNameUtils_Tests.cs new file mode 100644 index 00000000000..b72d43e1029 --- /dev/null +++ b/src/Tasks.UnitTests/StrongNameUtils_Tests.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK + +using System; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; +using Microsoft.Build.Tasks; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.UnitTests +{ + public sealed class StrongNameUtils_Tests + { + /// + /// The BCL (mscorlib) is always a fully signed managed assembly. + /// + [Fact] + public void GetAssemblyStrongNameLevel_FullySignedAssembly_ReturnsFullySigned() + { + string bclPath = typeof(string).Assembly.Location; + bclPath.ShouldNotBeNullOrEmpty(); + File.Exists(bclPath).ShouldBeTrue(); + + StrongNameUtils.GetAssemblyStrongNameLevel(bclPath).ShouldBe(StrongNameLevel.FullySigned); + } + + /// + /// A managed assembly with a strong-name signature directory present but with the + /// COMIMAGE_FLAGS_STRONGNAMESIGNED bit cleared must be reported as DelaySigned. + /// We emit one on the fly by attaching a public key and tagging the assembly + /// [assembly: AssemblyDelaySign(true)]. + /// + [Fact] + public void GetAssemblyStrongNameLevel_DelaySignedAssembly_ReturnsDelaySigned() + { + using TestEnvironment env = TestEnvironment.Create(); + TransientTestFolder folder = env.CreateFolder(); + + AssemblyName name = new AssemblyName("DelaySignedTestAssembly_" + Guid.NewGuid().ToString("N")); + name.SetPublicKey(typeof(string).Assembly.GetName().GetPublicKey()); + + // Mark as delay-signed via AssemblyDelaySignAttribute. + CustomAttributeBuilder delaySign = new CustomAttributeBuilder( + typeof(AssemblyDelaySignAttribute).GetConstructor([typeof(bool)]), + [true]); + + AssemblyBuilder asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( + name, + AssemblyBuilderAccess.Save, + folder.Path); + asmBuilder.SetCustomAttribute(delaySign); + + string fileName = name.Name + ".dll"; + ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule(name.Name, fileName); + modBuilder.DefineType("Stub", TypeAttributes.Public).CreateType(); + asmBuilder.Save(fileName); + + string asmPath = Path.Combine(folder.Path, fileName); + File.Exists(asmPath).ShouldBeTrue(); + + StrongNameUtils.GetAssemblyStrongNameLevel(asmPath).ShouldBe(StrongNameLevel.DelaySigned); + } + + /// + /// An unsigned managed assembly (COR20 header present, no strong name signature directory) + /// must be reported as None. + /// + [Fact] + public void GetAssemblyStrongNameLevel_UnsignedManagedAssembly_ReturnsNone() + { + using TestEnvironment env = TestEnvironment.Create(); + TransientTestFolder folder = env.CreateFolder(); + + // Emit a trivial dynamic assembly to disk with no key file / key pair attached. + AssemblyName name = new AssemblyName("UnsignedTestAssembly_" + Guid.NewGuid().ToString("N")); + AssemblyBuilder asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( + name, + AssemblyBuilderAccess.Save, + folder.Path); + string fileName = name.Name + ".dll"; + ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule(name.Name, fileName); + modBuilder.DefineType("Stub", TypeAttributes.Public).CreateType(); + asmBuilder.Save(fileName); + + string asmPath = Path.Combine(folder.Path, fileName); + File.Exists(asmPath).ShouldBeTrue(); + + StrongNameUtils.GetAssemblyStrongNameLevel(asmPath).ShouldBe(StrongNameLevel.None); + } + + /// + /// A native PE without a COR20 header must be reported as Unknown (not None) so that + /// callers like AxTlbBaseReference.SigningRequirementsMatchExistingWrapper can distinguish + /// "no managed signature present" from "not a managed image at all". + /// + [WindowsOnlyFact] + public void GetAssemblyStrongNameLevel_NativePE_ReturnsUnknown() + { + string systemRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? Environment.GetFolderPath(Environment.SpecialFolder.Windows); + string kernel32 = Path.Combine(systemRoot, "System32", "kernel32.dll"); + File.Exists(kernel32).ShouldBeTrue(); + + StrongNameUtils.GetAssemblyStrongNameLevel(kernel32).ShouldBe(StrongNameLevel.Unknown); + } + + [Fact] + public void GetAssemblyStrongNameLevel_NonExistentFile_ReturnsUnknown() + { + using TestEnvironment env = TestEnvironment.Create(); + TransientTestFile missing = env.GetTempFile(".dll"); + File.Exists(missing.Path).ShouldBeFalse(); + + StrongNameUtils.GetAssemblyStrongNameLevel(missing.Path).ShouldBe(StrongNameLevel.Unknown); + } + + [Fact] + public void GetAssemblyStrongNameLevel_GarbageFile_ReturnsUnknown() + { + using TestEnvironment env = TestEnvironment.Create(); + TransientTestFile garbage = env.CreateFile(); + File.WriteAllBytes(garbage.Path, [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]); + + StrongNameUtils.GetAssemblyStrongNameLevel(garbage.Path).ShouldBe(StrongNameLevel.Unknown); + } + + [Fact] + public void GetAssemblyStrongNameLevel_EmptyFile_ReturnsUnknown() + { + using TestEnvironment env = TestEnvironment.Create(); + TransientTestFile empty = env.CreateFile(); + + StrongNameUtils.GetAssemblyStrongNameLevel(empty.Path).ShouldBe(StrongNameLevel.Unknown); + } + + [Fact] + public void GetAssemblyStrongNameLevel_NullPath_Throws() + { + Should.Throw(() => StrongNameUtils.GetAssemblyStrongNameLevel(null)); + } + } +} + +#endif + diff --git a/src/Tasks/NativeMethods.cs b/src/Tasks/NativeMethods.cs index b97f5cb9817..c53c535254b 100644 --- a/src/Tasks/NativeMethods.cs +++ b/src/Tasks/NativeMethods.cs @@ -462,7 +462,6 @@ internal static partial class NativeMethods #region Constants internal static readonly IntPtr NullPtr = IntPtr.Zero; - internal static readonly IntPtr InvalidIntPtr = new IntPtr(-1); internal const int ERROR_SUCCESS = 0; @@ -507,14 +506,6 @@ internal enum REGKIND internal const UInt16 IMAGE_FILE_MACHINE_ARM64 = 0xAA64; // ARM64 Little-Endian internal const UInt16 IMAGE_FILE_MACHINE_R4000 = 0x166; // Used to test a architecture we do not expect to reference - internal const uint GENERIC_READ = 0x80000000; - - internal const uint PAGE_READONLY = 0x02; - - internal const uint FILE_MAP_READ = 0x04; - - internal const uint FILE_TYPE_DISK = 0x01; - internal const int SE_ERR_ACCESSDENIED = 5; [Flags] @@ -528,152 +519,6 @@ internal enum MoveFileFlags MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020 } - #endregion - - #region NT header stuff - - internal const uint IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b; - internal const uint IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b; - - internal const uint IMAGE_DIRECTORY_ENTRY_COMHEADER = 14; - - internal const uint COMIMAGE_FLAGS_STRONGNAMESIGNED = 0x08; - - [StructLayout(LayoutKind.Sequential)] - internal struct IMAGE_FILE_HEADER - { - internal ushort Machine; - internal ushort NumberOfSections; - internal uint TimeDateStamp; - internal uint PointerToSymbolTable; - internal uint NumberOfSymbols; - internal ushort SizeOfOptionalHeader; - internal ushort Characteristics; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct IMAGE_DATA_DIRECTORY - { - internal uint VirtualAddress; - internal uint Size; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct IMAGE_OPTIONAL_HEADER32 - { - internal ushort Magic; - internal byte MajorLinkerVersion; - internal byte MinorLinkerVersion; - internal uint SizeOfCode; - internal uint SizeOfInitializedData; - internal uint SizeOfUninitializedData; - internal uint AddressOfEntryPoint; - internal uint BaseOfCode; - internal uint BaseOfData; - internal uint ImageBase; - internal uint SectionAlignment; - internal uint FileAlignment; - internal ushort MajorOperatingSystemVersion; - internal ushort MinorOperatingSystemVersion; - internal ushort MajorImageVersion; - internal ushort MinorImageVersion; - internal ushort MajorSubsystemVersion; - internal ushort MinorSubsystemVersion; - internal uint Win32VersionValue; - internal uint SizeOfImage; - internal uint SizeOfHeaders; - internal uint CheckSum; - internal ushort Subsystem; - internal ushort DllCharacteristics; - internal uint SizeOfStackReserve; - internal uint SizeOfStackCommit; - internal uint SizeOfHeapReserve; - internal uint SizeOfHeapCommit; - internal uint LoaderFlags; - internal uint NumberOfRvaAndSizes; - - // should be: - // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] internal IMAGE_DATA_DIRECTORY[] DataDirectory; - // but fixed size arrays only work with simple types, so I have to use ulongs and convert them to IMAGE_DATA_DIRECTORY structs - // Fortunately, IMAGE_DATA_DIRECTORY is only 8 bytes long... (whew) - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] - internal ulong[] DataDirectory; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct IMAGE_OPTIONAL_HEADER64 - { - internal ushort Magic; - internal byte MajorLinkerVersion; - internal byte MinorLinkerVersion; - internal uint SizeOfCode; - internal uint SizeOfInitializedData; - internal uint SizeOfUninitializedData; - internal uint AddressOfEntryPoint; - internal uint BaseOfCode; - internal ulong ImageBase; - internal uint SectionAlignment; - internal uint FileAlignment; - internal ushort MajorOperatingSystemVersion; - internal ushort MinorOperatingSystemVersion; - internal ushort MajorImageVersion; - internal ushort MinorImageVersion; - internal ushort MajorSubsystemVersion; - internal ushort MinorSubsystemVersion; - internal uint Win32VersionValue; - internal uint SizeOfImage; - internal uint SizeOfHeaders; - internal uint CheckSum; - internal ushort Subsystem; - internal ushort DllCharacteristics; - internal ulong SizeOfStackReserve; - internal ulong SizeOfStackCommit; - internal ulong SizeOfHeapReserve; - internal ulong SizeOfHeapCommit; - internal uint LoaderFlags; - internal uint NumberOfRvaAndSizes; - - // should be: - // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] internal IMAGE_DATA_DIRECTORY[] DataDirectory; - // but fixed size arrays only work with simple types, so I have to use ulongs and convert them to IMAGE_DATA_DIRECTORY structs - // Fortunately, IMAGE_DATA_DIRECTORY is only 8 bytes long... (whew) - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] - internal ulong[] DataDirectory; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct IMAGE_NT_HEADERS32 - { - internal uint signature; - internal IMAGE_FILE_HEADER fileHeader; - internal IMAGE_OPTIONAL_HEADER32 optionalHeader; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct IMAGE_NT_HEADERS64 - { - internal uint signature; - internal IMAGE_FILE_HEADER fileHeader; - internal IMAGE_OPTIONAL_HEADER64 optionalHeader; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct IMAGE_COR20_HEADER - { - internal uint cb; - internal ushort MajorRuntimeVersion; - internal ushort MinorRuntimeVersion; - internal IMAGE_DATA_DIRECTORY MetaData; - internal uint Flags; - internal uint EntryPointTokenOrEntryPointRVA; - internal IMAGE_DATA_DIRECTORY Resources; - internal IMAGE_DATA_DIRECTORY StrongNameSignature; - internal IMAGE_DATA_DIRECTORY CodeManagerTable; - internal IMAGE_DATA_DIRECTORY VTableFixups; - internal IMAGE_DATA_DIRECTORY ExportAddressTableJumps; - internal IMAGE_DATA_DIRECTORY ManagedNativeHeader; - } - [StructLayout(LayoutKind.Sequential)] internal struct CRYPTOAPI_BLOB { @@ -809,58 +654,6 @@ internal static extern void UnregisterTypeLib( [return: MarshalAs(UnmanagedType.BStr)] internal static extern string QueryPathOfRegTypeLib([In] ref Guid clsid, [In] short majorVersion, [In] short minorVersion, [In] int lcid); - //------------------------------------------------------------------------------ - // CreateFile - //------------------------------------------------------------------------------ - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, FileShare dwShareMode, - IntPtr lpSecurityAttributes, FileMode dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); - - //------------------------------------------------------------------------------ - // GetFileType - //------------------------------------------------------------------------------ - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern uint GetFileType(IntPtr hFile); - - //------------------------------------------------------------------------------ - // CloseHandle - //------------------------------------------------------------------------------ - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool CloseHandle(IntPtr hObject); - - //------------------------------------------------------------------------------ - // CreateFileMapping - //------------------------------------------------------------------------------ - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern IntPtr CreateFileMapping(IntPtr hFile, IntPtr lpFileMappingAttributes, uint flProtect, - uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName); - - //------------------------------------------------------------------------------ - // MapViewOfFile - //------------------------------------------------------------------------------ - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern IntPtr MapViewOfFile(IntPtr hFileMapping, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, IntPtr dwNumberOfBytesToMap); - - //------------------------------------------------------------------------------ - // UnmapViewOfFile - //------------------------------------------------------------------------------ - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool UnmapViewOfFile(IntPtr lpBaseAddress); - - //------------------------------------------------------------------------------ - // ImageNtHeader - //------------------------------------------------------------------------------ - [DllImport("dbghelp.dll", SetLastError = true)] - internal static extern IntPtr ImageNtHeader(IntPtr imageBase); - - //------------------------------------------------------------------------------ - // ImageRvaToVa - //------------------------------------------------------------------------------ - [DllImport("dbghelp.dll", SetLastError = true)] - internal static extern IntPtr ImageRvaToVa(IntPtr ntHeaders, IntPtr imageBase, uint Rva, out IntPtr LastRvaSection); - internal static bool AllDrivesMapped() { #if FEATURE_WINDOWSINTEROP diff --git a/src/Tasks/StrongNameUtils.cs b/src/Tasks/StrongNameUtils.cs index f1aef67e8c0..ea3e0ab1a87 100644 --- a/src/Tasks/StrongNameUtils.cs +++ b/src/Tasks/StrongNameUtils.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics; using System.IO; using System.Reflection; -using System.Runtime.InteropServices; +using System.Reflection.PortableExecutable; using System.Security; using Microsoft.Build.Shared; using Microsoft.Build.Utilities; @@ -128,167 +127,47 @@ internal static void GetStrongNameKey(TaskLoggingHelper log, string keyFile, str } /// - /// Given an assembly path, determine if the assembly is [delay] signed or not. This code is based on similar unmanaged - /// routines in vsproject and sn.exe (ndp tools) codebases. + /// Given an assembly path, determine if the assembly is [delay] signed or not. /// - /// - /// internal static StrongNameLevel GetAssemblyStrongNameLevel(string assemblyPath) { ErrorUtilities.VerifyThrowArgumentNull(assemblyPath); - StrongNameLevel snLevel = StrongNameLevel.Unknown; - IntPtr fileHandle = NativeMethods.InvalidIntPtr; - try { - // open the assembly - fileHandle = NativeMethods.CreateFile(assemblyPath, NativeMethods.GENERIC_READ, FileShare.Read, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero); - if (fileHandle == NativeMethods.InvalidIntPtr) - { - return snLevel; - } + using FileStream stream = File.OpenRead(assemblyPath); + using PEReader peReader = new PEReader(stream); + CorHeader corHeader = peReader.PEHeaders.CorHeader; - // if it's not a disk file, exit - if (NativeMethods.GetFileType(fileHandle) != NativeMethods.FILE_TYPE_DISK) + // No COR20 header means this isn't a managed PE; preserve the historical + // "we don't know" answer rather than claiming the image is unsigned. + if (corHeader is null) { - return snLevel; + return StrongNameLevel.Unknown; } - IntPtr fileMappingHandle = IntPtr.Zero; - - try + DirectoryEntry signature = corHeader.StrongNameSignatureDirectory; + if (signature.RelativeVirtualAddress == 0 || signature.Size == 0) { - fileMappingHandle = NativeMethods.CreateFileMapping(fileHandle, IntPtr.Zero, NativeMethods.PAGE_READONLY, 0, 0, null); - if (fileMappingHandle == IntPtr.Zero) - { - return snLevel; - } - - IntPtr fileMappingBase = IntPtr.Zero; - - try - { - fileMappingBase = NativeMethods.MapViewOfFile(fileMappingHandle, NativeMethods.FILE_MAP_READ, 0, 0, IntPtr.Zero); - if (fileMappingBase == IntPtr.Zero) - { - return snLevel; - } - - // retrieve NT headers pointer from the file - IntPtr ntHeader = NativeMethods.ImageNtHeader(fileMappingBase); - if (ntHeader == IntPtr.Zero) - { - return snLevel; - } - - // get relative virtual address of the COR20 header - uint cor20HeaderRva = GetCor20HeaderRva(ntHeader); - if (cor20HeaderRva == 0) - { - return snLevel; - } - - // get the pointer to the COR20 header structure - IntPtr cor20HeaderPtr = NativeMethods.ImageRvaToVa(ntHeader, fileMappingBase, cor20HeaderRva, out _); - if (cor20HeaderPtr == IntPtr.Zero) - { - return snLevel; - } - - // get the COR20 structure itself - NativeMethods.IMAGE_COR20_HEADER cor20Header = (NativeMethods.IMAGE_COR20_HEADER)Marshal.PtrToStructure(cor20HeaderPtr, typeof(NativeMethods.IMAGE_COR20_HEADER)); - - // and finally, examine it. If no space is allocated for strong name signature, assembly is not signed. - if ((cor20Header.StrongNameSignature.VirtualAddress == 0) || (cor20Header.StrongNameSignature.Size == 0)) - { - snLevel = StrongNameLevel.None; - } - else - { - // if there's allocated space and strong name flag is set, assembly is fully signed, or delay signed otherwise - if ((cor20Header.Flags & NativeMethods.COMIMAGE_FLAGS_STRONGNAMESIGNED) != 0) - { - snLevel = StrongNameLevel.FullySigned; - } - else - { - snLevel = StrongNameLevel.DelaySigned; - } - } - } - finally - { - if (fileMappingBase != IntPtr.Zero) - { - NativeMethods.UnmapViewOfFile(fileMappingBase); - fileMappingBase = IntPtr.Zero; - } - } + return StrongNameLevel.None; } - finally - { - if (fileMappingHandle != IntPtr.Zero) - { - NativeMethods.CloseHandle(fileMappingHandle); - fileMappingHandle = IntPtr.Zero; - } - } - } - finally - { - if (fileHandle != NativeMethods.InvalidIntPtr) - { - NativeMethods.CloseHandle(fileHandle); - } - } - - return snLevel; - } - - /// - /// Retrieves the relative virtual address of the COR20 header, given the address of the NT headers structure. The catch - /// here is that the NT headers struct can be either 32 or 64 bit version, and some fields have different sizes there. We - /// need to see if we're dealing with a 32bit header or a 64bit one first. - /// - /// - /// - private static uint GetCor20HeaderRva(IntPtr ntHeadersPtr) - { - // read the first ushort in the optional header - we have an uint and IMAGE_FILE_HEADER preceding it - ushort optionalHeaderMagic = (ushort)Marshal.ReadInt16(ntHeadersPtr, Marshal.SizeOf() + Marshal.SizeOf()); - - // this should really be a structure, but NDP can't marshal fixed size struct arrays in a struct... ugh. - // this ulong corresponds to a IMAGE_DATA_DIRECTORY structure - ulong cor20DataDirectoryLong; - // see if we have a 32bit header or a 64bit header - if (optionalHeaderMagic == NativeMethods.IMAGE_NT_OPTIONAL_HDR32_MAGIC) + return (corHeader.Flags & CorFlags.StrongNameSigned) != 0 + ? StrongNameLevel.FullySigned + : StrongNameLevel.DelaySigned; + } + catch (IOException) { - // marshal data into the appropriate structure - NativeMethods.IMAGE_NT_HEADERS32 ntHeader32 = (NativeMethods.IMAGE_NT_HEADERS32)Marshal.PtrToStructure(ntHeadersPtr, typeof(NativeMethods.IMAGE_NT_HEADERS32)); - cor20DataDirectoryLong = ntHeader32.optionalHeader.DataDirectory[NativeMethods.IMAGE_DIRECTORY_ENTRY_COMHEADER]; + return StrongNameLevel.Unknown; } - else if (optionalHeaderMagic == NativeMethods.IMAGE_NT_OPTIONAL_HDR64_MAGIC) + catch (BadImageFormatException) { - // marshal data into the appropriate structure - NativeMethods.IMAGE_NT_HEADERS64 ntHeader64 = (NativeMethods.IMAGE_NT_HEADERS64)Marshal.PtrToStructure(ntHeadersPtr, typeof(NativeMethods.IMAGE_NT_HEADERS64)); - cor20DataDirectoryLong = ntHeader64.optionalHeader.DataDirectory[NativeMethods.IMAGE_DIRECTORY_ENTRY_COMHEADER]; + return StrongNameLevel.Unknown; } - else + catch (UnauthorizedAccessException) { - Debug.Assert(false, "invalid file type!"); - return 0; + return StrongNameLevel.Unknown; } - - // cor20DataDirectoryLong is really a IMAGE_DATA_DIRECTORY structure which I had to pack into an ulong - // (see comments for IMAGE_OPTIONAL_HEADER32/64 in NativeMethods.cs) - // this code extracts the virtualAddress (uint) and size (uint) fields from the ulong by doing simple - // bit masking/shifting ops - uint virtualAddress = (uint)(cor20DataDirectoryLong & 0x00000000ffffffff); - // uint size = (uint)(cor20DataDirectoryLong >> 32); - - return virtualAddress; } } }