diff --git a/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs b/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs index 12ae3e4..0668317 100644 --- a/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs +++ b/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs @@ -6,7 +6,13 @@ using System.Linq; using System.Reflection; using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Threading.Tasks; +using ILVerify; +using UnitystationLauncher.Constants; using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Infrastructure; +using UnitystationLauncher.Models.ConfigFile; using UnitystationLauncher.Models.ContentScanning; using UnitystationLauncher.Models.ContentScanning.ScanningTypes; @@ -24,419 +30,38 @@ namespace UnitystationLauncher.ContentScanning; /// internal static class AssemblyTypeCheckerHelpers { - private static readonly bool _parallelReferencedMembersScanning = true; - internal static Resolver CreateResolver(DirectoryInfo managedPath) { return new(managedPath); } - internal static string FormatMethodName(MetadataReader reader, MethodDefinition method) - { - MethodSignature methodSig = method.DecodeSignature(new TypeProvider(), 0); - MTypeDefined type = GetTypeFromDefinition(reader, method.GetDeclaringType()); - - return - $"{type}.{reader.GetString(method.Name)}({string.Join(", ", methodSig.ParameterTypes)}) Returns {methodSig.ReturnType} "; - } - - internal static void CheckNoUnmanagedMethodDefs(MetadataReader reader, ConcurrentBag errors) - { - foreach (MethodDefinitionHandle methodDefHandle in reader.MethodDefinitions) - { - MethodDefinition methodDef = reader.GetMethodDefinition(methodDefHandle); - MethodImplAttributes implAttr = methodDef.ImplAttributes; - MethodAttributes attr = methodDef.Attributes; - - if ((implAttr & MethodImplAttributes.Unmanaged) != 0 || - (implAttr & MethodImplAttributes.CodeTypeMask) is not (MethodImplAttributes.IL - or MethodImplAttributes.Runtime)) - { - string err = $"Method has illegal MethodImplAttributes: {FormatMethodName(reader, methodDef)}"; - errors.Add(new(err)); - } - - if ((attr & (MethodAttributes.PinvokeImpl | MethodAttributes.UnmanagedExport)) != 0) - { - string err = $"Method has illegal MethodAttributes: {FormatMethodName(reader, methodDef)}"; - errors.Add(new(err)); - } - } - } - - internal static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag errors) - { - foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) - { - TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); - if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) != 0) - { - // The C# compiler emits explicit layout types for some array init logic. These have no fields. - // Only ban explicit layout if it has fields. - - MTypeDefined type = GetTypeFromDefinition(reader, typeDefHandle); - - if (typeDef.GetFields().Count > 0) - { - string err = $"Explicit layout type {type} may not have fields."; - errors.Add(new(err)); - } - } - } - } - - internal static List GetReferencedTypes(MetadataReader reader, ConcurrentBag errors) - { - return reader.TypeReferences.Select(typeRefHandle => - { - try - { - return ParseTypeReference(reader, typeRefHandle); - } - catch (UnsupportedMetadataException e) - { - errors.Add(new(e)); - return null; - } - }) - .Where(p => p != null) - .ToList()!; - } - - internal static List GetReferencedMembers(MetadataReader reader, ConcurrentBag errors) + internal static bool CheckVerificationResult(SandboxConfig loadedCfg, VerificationResult res, string name, MetadataReader reader, Action logErrors) { - if (_parallelReferencedMembersScanning) - { - return reader.MemberReferences.AsParallel() - .Select(memRefHandle => - { - MemberReference memRef = reader.GetMemberReference(memRefHandle); - string memName = reader.GetString(memRef.Name); - MType parent; - switch (memRef.Parent.Kind) - { - // See II.22.25 in ECMA-335. - case HandleKind.TypeReference: - { - // Regular type reference. - try - { - parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new(u)); - return null; - } - - break; - } - case HandleKind.TypeDefinition: - { - try - { - parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new(u)); - return null; - } - - break; - } - case HandleKind.TypeSpecification: - { - TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); - // Generic type reference. - TypeProvider provider = new(); - parent = typeSpec.DecodeSignature(provider, 0); - - if (parent.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics since MethodSpec needs to point to MemberRef. - return null; - } - - break; - } - case HandleKind.ModuleReference: - { - errors.Add(new( - $"Module global variables and methods are unsupported. Name: {memName}")); - return null; - } - case HandleKind.MethodDefinition: - { - errors.Add(new($"Vararg calls are unsupported. Name: {memName}")); - return null; - } - default: - { - errors.Add(new( - $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); - return null; - } - } - - MMemberRef memberRef; - - switch (memRef.GetKind()) - { - case MemberReferenceKind.Method: - { - MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); - - memberRef = new MMemberRefMethod( - parent, - memName, - sig.ReturnType, - sig.GenericParameterCount, - sig.ParameterTypes); - - break; - } - case MemberReferenceKind.Field: - { - MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); - memberRef = new MMemberRefField(parent, memName, fieldType); - break; - } - default: - throw new ArgumentOutOfRangeException(); - } - - return memberRef; - }) - .Where(p => p != null) - .ToList()!; - } - else + if (loadedCfg.AllowedVerifierErrors.Contains(res.Code)) { - return reader.MemberReferences.Select(memRefHandle => - { - MemberReference memRef = reader.GetMemberReference(memRefHandle); - string memName = reader.GetString(memRef.Name); - MType parent; - switch (memRef.Parent.Kind) - { - // See II.22.25 in ECMA-335. - case HandleKind.TypeReference: - { - // Regular type reference. - try - { - parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new(u)); - return null; - } - - break; - } - case HandleKind.TypeDefinition: - { - try - { - parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new(u)); - return null; - } - - break; - } - case HandleKind.TypeSpecification: - { - TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); - // Generic type reference. - TypeProvider provider = new(); - parent = typeSpec.DecodeSignature(provider, 0); - - if (parent.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics since MethodSpec needs to point to MemberRef. - return null; - } - - break; - } - case HandleKind.ModuleReference: - { - errors.Add(new( - $"Module global variables and methods are unsupported. Name: {memName}")); - return null; - } - case HandleKind.MethodDefinition: - { - errors.Add(new($"Vararg calls are unsupported. Name: {memName}")); - return null; - } - default: - { - errors.Add(new( - $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); - return null; - } - } - - MMemberRef memberRef; - - switch (memRef.GetKind()) - { - case MemberReferenceKind.Method: - { - MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); - - memberRef = new MMemberRefMethod( - parent, - memName, - sig.ReturnType, - sig.GenericParameterCount, - sig.ParameterTypes); - - break; - } - case MemberReferenceKind.Field: - { - MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); - memberRef = new MMemberRefField(parent, memName, fieldType); - break; - } - default: - throw new ArgumentOutOfRangeException(); - } - - return memberRef; - }) - .Where(p => p != null) - .ToList()!; + return false; } - } - internal static bool ParseInheritType(MType ownerType, EntityHandle handle, [NotNullWhen(true)] out MType? type, MetadataReader reader, ConcurrentBag errors) - { - type = default; + string formatted = res.Args == null ? res.Message : string.Format(res.Message, res.Args); + string msg = $"{name}: ILVerify: {formatted}"; - switch (handle.Kind) + if (!res.Method.IsNil) { - case HandleKind.TypeDefinition: - // Definition to type in same assembly, allowed without hassle. - return false; - - case HandleKind.TypeReference: - // Regular type reference. - try - { - type = ParseTypeReference(reader, (TypeReferenceHandle)handle); - return true; - } - catch (UnsupportedMetadataException u) - { - errors.Add(new(u)); - return false; - } - - case HandleKind.TypeSpecification: - TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle); - // Generic type reference. - TypeProvider provider = new(); - type = typeSpec.DecodeSignature(provider, 0); - - if (type.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics. - return false; - } - - break; + MethodDefinition method = reader.GetMethodDefinition(res.Method); + string methodName = reader.FormatMethodName(method); - default: - errors.Add(new( - $"Unsupported BaseType of kind {handle.Kind} on type {ownerType}")); - return false; + msg = $"{msg}, method: {methodName}"; } - type = default!; - return false; - } - - /// - /// Thrown if the metadata does something funny we don't "support" like type forwarding. - /// - internal static MTypeReferenced ParseTypeReference(MetadataReader reader, TypeReferenceHandle handle) - { - TypeReference typeRef = reader.GetTypeReference(handle); - string name = reader.GetString(typeRef.Name); - string? nameSpace = NilNullString(reader, typeRef.Namespace); - MResScope resScope; - - // See II.22.38 in ECMA-335 - if (typeRef.ResolutionScope.IsNil) + if (!res.Type.IsNil) { - throw new UnsupportedMetadataException( - $"Null resolution scope on type Name: {nameSpace}.{name}. This indicates exported/forwarded types"); + MTypeDefined type = reader.GetTypeFromDefinition(res.Type); + msg = $"{msg}, type: {type}"; } - switch (typeRef.ResolutionScope.Kind) - { - case HandleKind.AssemblyReference: - { - // Different assembly. - AssemblyReference assemblyRef = - reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); - string assemblyName = reader.GetString(assemblyRef.Name); - resScope = new MResScopeAssembly(assemblyName); - break; - } - case HandleKind.TypeReference: - { - // Nested type. - MTypeReferenced enclosingType = ParseTypeReference(reader, (TypeReferenceHandle)typeRef.ResolutionScope); - resScope = new MResScopeType(enclosingType); - break; - } - case HandleKind.ModuleReference: - { - // Same-assembly-different-module - throw new UnsupportedMetadataException( - $"Cross-module reference to type {nameSpace}.{name}. "); - } - default: - // Edge cases not handled: - // https://github.com/dotnet/runtime/blob/b2e5a89085fcd87e2fa9300b4bb00cd499c5845b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/DisassemblingTypeProvider.cs#L130-L132 - throw new UnsupportedMetadataException( - $"TypeRef to {typeRef.ResolutionScope.Kind} for type {nameSpace}.{name}"); - } - - return new(resScope, name, nameSpace); - } - - internal static string? NilNullString(MetadataReader reader, StringHandle handle) - { - return handle.IsNil ? null : reader.GetString(handle); + logErrors.Invoke(msg); + return true; } - - internal static MTypeDefined GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle) - { - TypeDefinition typeDef = reader.GetTypeDefinition(handle); - string name = reader.GetString(typeDef.Name); - string? ns = NilNullString(reader, typeDef.Namespace); - MTypeDefined? enclosing = null; - if (typeDef.IsNested) - { - enclosing = GetTypeFromDefinition(reader, typeDef.GetDeclaringType()); - } - - return new(name, ns, enclosing); - } } \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/IlScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/IlScanner.cs new file mode 100644 index 0000000..d293206 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/IlScanner.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Threading.Tasks; +using ILVerify; +using UnitystationLauncher.Constants; +using UnitystationLauncher.Models.ConfigFile; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class IlScanner +{ + private static readonly bool _parallelIlScanning = false; + + internal static bool DoVerifyIl(string name, IResolver resolver, PEReader peReader, + MetadataReader reader, Action info, Action logErrors, SandboxConfig loadedCfg) + { + info.Invoke($"{name}: Verifying IL..."); + Stopwatch sw = Stopwatch.StartNew(); + ConcurrentBag bag = new(); + + // TODO: We should probably just pick one of these and remove the other + if (_parallelIlScanning) + { + IlScanner.ParallelIlScanning(reader, resolver, peReader, bag); + } + else + { + IlScanner.NonParallelIlScanning(reader, resolver, peReader, bag); + } + + bool verifyErrors = false; + foreach (VerificationResult res in bag) + { + bool error = AssemblyTypeCheckerHelpers.CheckVerificationResult(loadedCfg, res, name, reader, logErrors); + if (error) + { + verifyErrors = true; + } + } + + info.Invoke($"{name}: Verified IL in {sw.Elapsed.TotalMilliseconds}ms"); + + if (verifyErrors) + { + return false; + } + + return true; + } + + private static void ParallelIlScanning(MetadataReader reader, IResolver resolver, PEReader peReader, ConcurrentBag bag) + { + OrderablePartitioner partitioner = Partitioner.Create(reader.TypeDefinitions); + Parallel.ForEach(partitioner.GetPartitions(Environment.ProcessorCount), handle => + { + Verifier ver = new(resolver); + ver.SetSystemModuleName(new(AssemblyNames.SystemAssemblyName)); + while (handle.MoveNext()) + { + foreach (VerificationResult? result in ver.Verify(peReader, handle.Current, verifyMethods: true)) + { + bag.Add(result); + } + } + }); + } + + private static void NonParallelIlScanning(MetadataReader reader, IResolver resolver, PEReader peReader, ConcurrentBag bag) + { + Verifier ver = new(resolver); + //mscorlib + ver.SetSystemModuleName(new(AssemblyNames.SystemAssemblyName)); + foreach (TypeDefinitionHandle definition in reader.TypeDefinitions) + { + IEnumerable errors = ver.Verify(peReader, definition, verifyMethods: true); + foreach (VerificationResult error in errors) + { + bag.Add(error); + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/InheritanceScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/InheritanceScanner.cs new file mode 100644 index 0000000..20f5d70 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/InheritanceScanner.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using UnitystationLauncher.Models.ConfigFile; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; +using UnitystationLauncher.Models.Enums; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class InheritanceScanner +{ + internal static void CheckInheritance( + SandboxConfig sandboxConfig, + List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited, + ConcurrentBag errors) + { + // This inheritance whitelisting primarily serves to avoid content doing funny stuff + // by e.g. inheriting Type. + foreach ((MType _, MType baseType, ArraySegment interfaces) in inherited) + { + if (CanInherit(baseType) == false) + { + errors.Add(new($"Inheriting of type not allowed: {baseType}")); + } + + foreach (MType @interface in interfaces) + { + if (CanInherit(@interface) == false) + { + errors.Add(new($"Implementing of interface not allowed: {@interface}")); + } + } + + bool CanInherit(MType inheritType) + { + MTypeReferenced realBaseType = inheritType switch + { + MTypeGeneric generic => (MTypeReferenced)generic.GenericType, + MTypeReferenced referenced => referenced, + _ => throw new InvalidOperationException() // Can't happen. + }; + + if (realBaseType.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? cfg) == false) + { + return false; + } + + return cfg.Inherit != InheritMode.Block && (cfg.Inherit == InheritMode.Allow || cfg.All); + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/MemberReferenceScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/MemberReferenceScanner.cs new file mode 100644 index 0000000..1b5c73f --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/MemberReferenceScanner.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnitystationLauncher.Models.ConfigFile; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class MemberReferenceScanner +{ + private static readonly bool _parallelMemberReferencesScanning = true; + + public static void CheckMemberReferences(SandboxConfig sandboxConfig, List members, ConcurrentBag errors) + { + // TODO: We should probably just pick one of these and remove the other + if (_parallelMemberReferencesScanning) + { + ParallelCheckMemberReferences(sandboxConfig, members, errors); + } + else + { + NonParallelCheckMemberReferences(sandboxConfig, members, errors); + } + } + + + private static void ParallelCheckMemberReferences(SandboxConfig sandboxConfig, List members, ConcurrentBag errors) + { + Parallel.ForEach(members, memberRef => + { + MType baseType = memberRef.ParentType; + while (!(baseType is MTypeReferenced)) + { + switch (baseType) + { + case MTypeGeneric generic: + { + baseType = generic.GenericType; + + break; + } + case MTypeWackyArray: + { + // Members on arrays (not to be confused with vectors) are all fine. + // See II.14.2 in ECMA-335. + return; + } + case MTypeDefined: + { + // Valid for this to show up, safe to ignore. + return; + } + default: + { + throw new ArgumentOutOfRangeException(); + } + } + } + + MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; + + if (baseTypeReferenced.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? typeCfg) == false) + { + // Technically this error isn't necessary since we have an earlier pass + // checking all referenced types. That should have caught this + // We still need the typeCfg so that's why we're checking. Might as well. + errors.Add(new($"Access to type not allowed: {baseTypeReferenced}")); + return; + } + + if (typeCfg.All) + { + // Fully whitelisted for the type, we good. + return; + } + + switch (memberRef) + { + case MMemberRefField mMemberRefField: + { + foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) + { + if (field.Name == mMemberRefField.Name && + mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) + { + return; // Found + } + } + + errors.Add(new($"Access to field not allowed: {mMemberRefField}")); + break; + } + case MMemberRefMethod mMemberRefMethod: + foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) + { + bool notParamMismatch = true; + + if (parsed.Name == mMemberRefMethod.Name && + mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && + mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && + mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) + { + for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) + { + MType a = mMemberRefMethod.ParameterTypes[i]; + MType b = parsed.ParameterTypes[i]; + + if (a.WhitelistEquals(b) == false) + { + notParamMismatch = false; + break; + } + } + + if (notParamMismatch) + { + return; // Found + } + } + } + + errors.Add(new($"Access to method not allowed: {mMemberRefMethod}")); + break; + default: + throw new ArgumentOutOfRangeException(nameof(memberRef)); + } + }); + } + + private static void NonParallelCheckMemberReferences(SandboxConfig sandboxConfig, List members, ConcurrentBag errors) + { + foreach (MMemberRef memberRef in members) + { + MType baseType = memberRef.ParentType; + while (!(baseType is MTypeReferenced)) + { + switch (baseType) + { + case MTypeGeneric generic: + { + baseType = generic.GenericType; + + break; + } + case MTypeWackyArray: + { + // Members on arrays (not to be confused with vectors) are all fine. + // See II.14.2 in ECMA-335. + continue; + } + case MTypeDefined: + { + // Valid for this to show up, safe to ignore. + continue; + } + default: + { + throw new ArgumentOutOfRangeException(); + } + } + } + + MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; + + if (baseTypeReferenced.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? typeCfg) == false) + { + // Technically this error isn't necessary since we have an earlier pass + // checking all referenced types. That should have caught this + // We still need the typeCfg so that's why we're checking. Might as well. + errors.Add(new($"Access to type not allowed: {baseTypeReferenced}")); + continue; + } + + if (typeCfg.All) + { + // Fully whitelisted for the type, we good. + continue; + } + + switch (memberRef) + { + case MMemberRefField mMemberRefField: + { + foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) + { + if (field.Name == mMemberRefField.Name && + mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) + { + continue; // Found + } + } + + errors.Add(new($"Access to field not allowed: {mMemberRefField}")); + break; + } + case MMemberRefMethod mMemberRefMethod: + bool notParamMismatch = true; + foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) + { + if (parsed.Name == mMemberRefMethod.Name && + mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && + mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && + mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) + { + for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) + { + MType a = mMemberRefMethod.ParameterTypes[i]; + MType b = parsed.ParameterTypes[i]; + + if (!a.WhitelistEquals(b)) + { + notParamMismatch = false; + break; + + } + } + + if (notParamMismatch) + { + break; // Found + } + break; + } + } + + if (notParamMismatch == false) + { + continue; + } + + errors.Add(new($"Access to method not allowed: {mMemberRefMethod}")); + break; + default: + throw new ArgumentOutOfRangeException(nameof(memberRef)); + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/ReferencedMembersScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/ReferencedMembersScanner.cs new file mode 100644 index 0000000..26e7d3c --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/ReferencedMembersScanner.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Infrastructure; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class ReferencedMembersScanner +{ + internal static List ParallelReferencedMembersCheck(MetadataReader reader, ConcurrentBag errors) + { + return reader.MemberReferences.AsParallel() + .Select(memRefHandle => + { + MemberReference memRef = reader.GetMemberReference(memRefHandle); + string memName = reader.GetString(memRef.Name); + MType parent; + switch (memRef.Parent.Kind) + { + // See II.22.25 in ECMA-335. + case HandleKind.TypeReference: + { + // Regular type reference. + try + { + parent = reader.ParseTypeReference((TypeReferenceHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return null; + } + + break; + } + case HandleKind.TypeDefinition: + { + try + { + parent = reader.GetTypeFromDefinition((TypeDefinitionHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return null; + } + + break; + } + case HandleKind.TypeSpecification: + { + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); + // Generic type reference. + TypeProvider provider = new(); + parent = typeSpec.DecodeSignature(provider, 0); + + if (parent.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics since MethodSpec needs to point to MemberRef. + return null; + } + + break; + } + case HandleKind.ModuleReference: + { + errors.Add(new( + $"Module global variables and methods are unsupported. Name: {memName}")); + return null; + } + case HandleKind.MethodDefinition: + { + errors.Add(new($"Vararg calls are unsupported. Name: {memName}")); + return null; + } + default: + { + errors.Add(new( + $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); + return null; + } + } + + MMemberRef memberRef; + + switch (memRef.GetKind()) + { + case MemberReferenceKind.Method: + { + MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); + + memberRef = new MMemberRefMethod( + parent, + memName, + sig.ReturnType, + sig.GenericParameterCount, + sig.ParameterTypes); + + break; + } + case MemberReferenceKind.Field: + { + MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); + memberRef = new MMemberRefField(parent, memName, fieldType); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + return memberRef; + }) + .Where(p => p != null) + .ToList()!; + } + + internal static List NonParallelReferencedMembersCheck(MetadataReader reader, ConcurrentBag errors) + { + return reader.MemberReferences.Select(memRefHandle => + { + MemberReference memRef = reader.GetMemberReference(memRefHandle); + string memName = reader.GetString(memRef.Name); + MType parent; + switch (memRef.Parent.Kind) + { + // See II.22.25 in ECMA-335. + case HandleKind.TypeReference: + { + // Regular type reference. + try + { + parent = reader.ParseTypeReference((TypeReferenceHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return null; + } + + break; + } + case HandleKind.TypeDefinition: + { + try + { + parent = reader.GetTypeFromDefinition((TypeDefinitionHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return null; + } + + break; + } + case HandleKind.TypeSpecification: + { + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); + // Generic type reference. + TypeProvider provider = new(); + parent = typeSpec.DecodeSignature(provider, 0); + + if (parent.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics since MethodSpec needs to point to MemberRef. + return null; + } + + break; + } + case HandleKind.ModuleReference: + { + errors.Add(new( + $"Module global variables and methods are unsupported. Name: {memName}")); + return null; + } + case HandleKind.MethodDefinition: + { + errors.Add(new($"Vararg calls are unsupported. Name: {memName}")); + return null; + } + default: + { + errors.Add(new( + $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); + return null; + } + } + + MMemberRef memberRef; + + switch (memRef.GetKind()) + { + case MemberReferenceKind.Method: + { + MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); + + memberRef = new MMemberRefMethod( + parent, + memName, + sig.ReturnType, + sig.GenericParameterCount, + sig.ParameterTypes); + + break; + } + case MemberReferenceKind.Field: + { + MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); + memberRef = new MMemberRefField(parent, memName, fieldType); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + return memberRef; + }) + .Where(p => p != null) + .ToList()!; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/TypeAbuseScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/TypeAbuseScanner.cs new file mode 100644 index 0000000..d43c214 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/TypeAbuseScanner.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Metadata; +using UnitystationLauncher.Infrastructure; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class TypeAbuseScanner +{ + internal static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag errors) + { + foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); + if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) != 0) + { + // The C# compiler emits explicit layout types for some array init logic. These have no fields. + // Only ban explicit layout if it has fields. + + MTypeDefined type = reader.GetTypeFromDefinition(typeDefHandle); + + if (typeDef.GetFields().Count > 0) + { + string err = $"Explicit layout type {type} may not have fields."; + errors.Add(new(err)); + } + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Scanners/UnmanagedMethodScanner.cs b/UnitystationLauncher/ContentScanning/Scanners/UnmanagedMethodScanner.cs new file mode 100644 index 0000000..f11173a --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Scanners/UnmanagedMethodScanner.cs @@ -0,0 +1,33 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Metadata; +using UnitystationLauncher.Infrastructure; + +namespace UnitystationLauncher.ContentScanning.Scanners; + +internal static class UnmanagedMethodScanner +{ + internal static void CheckNoUnmanagedMethodDefs(MetadataReader reader, ConcurrentBag errors) + { + foreach (MethodDefinitionHandle methodDefHandle in reader.MethodDefinitions) + { + MethodDefinition methodDef = reader.GetMethodDefinition(methodDefHandle); + MethodImplAttributes implAttr = methodDef.ImplAttributes; + MethodAttributes attr = methodDef.Attributes; + + if ((implAttr & MethodImplAttributes.Unmanaged) != 0 || + (implAttr & MethodImplAttributes.CodeTypeMask) is not (MethodImplAttributes.IL + or MethodImplAttributes.Runtime)) + { + string err = $"Method has illegal MethodImplAttributes: {reader.FormatMethodName(methodDef)}"; + errors.Add(new(err)); + } + + if ((attr & (MethodAttributes.PinvokeImpl | MethodAttributes.UnmanagedExport)) != 0) + { + string err = $"Method has illegal MethodAttributes: {reader.FormatMethodName(methodDef)}"; + errors.Add(new(err)); + } + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/TypeProvider.cs b/UnitystationLauncher/ContentScanning/TypeProvider.cs index 6e42692..b0618f6 100644 --- a/UnitystationLauncher/ContentScanning/TypeProvider.cs +++ b/UnitystationLauncher/ContentScanning/TypeProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Immutable; using System.Reflection.Metadata; +using UnitystationLauncher.Infrastructure; using UnitystationLauncher.Models.ContentScanning; using UnitystationLauncher.Models.ContentScanning.ScanningTypes; @@ -40,12 +41,12 @@ public MType GetPrimitiveType(PrimitiveTypeCode typeCode) public MType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) { - return AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, handle); + return reader.GetTypeFromDefinition(handle); } public MType GetTypeFromReference(MetadataReader inReader, TypeReferenceHandle inHandle, byte inRawTypeKind) { - return AssemblyTypeCheckerHelpers.ParseTypeReference(inReader, inHandle); + return inReader.ParseTypeReference(inHandle); } public MType GetFunctionPointerType(MethodSignature signature) diff --git a/UnitystationLauncher/Exceptions/CodeScanningException.cs b/UnitystationLauncher/Exceptions/CodeScanningException.cs new file mode 100644 index 0000000..9377699 --- /dev/null +++ b/UnitystationLauncher/Exceptions/CodeScanningException.cs @@ -0,0 +1,18 @@ +using System; + +namespace UnitystationLauncher.Exceptions; + +public sealed class CodeScanningException : Exception +{ + public CodeScanningException() + { + } + + public CodeScanningException(string message) : base(message) + { + } + + public CodeScanningException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Exceptions/ContentLengthNullException.cs b/UnitystationLauncher/Exceptions/ContentLengthNullException.cs index e3c02e1..da4c20f 100644 --- a/UnitystationLauncher/Exceptions/ContentLengthNullException.cs +++ b/UnitystationLauncher/Exceptions/ContentLengthNullException.cs @@ -1,28 +1,27 @@ using System; -namespace UnitystationLauncher.Exceptions +namespace UnitystationLauncher.Exceptions; + +public class ContentLengthNullException : Exception { - public class ContentLengthNullException : Exception + public ContentLengthNullException(string url) { - public ContentLengthNullException(string url) - { - Url = url; - } + Url = url; + } - public ContentLengthNullException(string url, string message) - : base(message) - { - Url = url; - } + public ContentLengthNullException(string url, string message) + : base(message) + { + Url = url; + } - public ContentLengthNullException(string url, string message, Exception inner) - : base(message, inner) - { - Url = url; - } + public ContentLengthNullException(string url, string message, Exception inner) + : base(message, inner) + { + Url = url; + } - public string Url { get; } + public string Url { get; } - public override String Message => $"{base.Message} Url='{Url}"; - } -} \ No newline at end of file + public override String Message => $"{base.Message} Url='{Url}"; +} diff --git a/UnitystationLauncher/Infrastructure/EntityHandleExtensions.cs b/UnitystationLauncher/Infrastructure/EntityHandleExtensions.cs new file mode 100644 index 0000000..298abe6 --- /dev/null +++ b/UnitystationLauncher/Infrastructure/EntityHandleExtensions.cs @@ -0,0 +1,13 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Models.ContentScanning; + +namespace UnitystationLauncher.Infrastructure; + +internal static class EntityHandleExtensions +{ + +} \ No newline at end of file diff --git a/UnitystationLauncher/Infrastructure/MetadataReaderExtensions.cs b/UnitystationLauncher/Infrastructure/MetadataReaderExtensions.cs new file mode 100644 index 0000000..8200ad8 --- /dev/null +++ b/UnitystationLauncher/Infrastructure/MetadataReaderExtensions.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.ContentScanning.Scanners; +using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.Infrastructure; + +internal static class MetadataReaderExtensions +{ + private static readonly bool _parallelReferencedMembersScanning = true; + + internal static List<(MType type, MType parent, ArraySegment interfaceImpls)> GetExternalInheritedTypes( + this MetadataReader reader, + ConcurrentBag errors) + { + List<(MType, MType, ArraySegment)> list = new(); + foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); + ArraySegment interfaceImpls; + MTypeDefined type = reader.GetTypeFromDefinition(typeDefHandle); + + if (!type.ParseInheritType(typeDef.BaseType, out MType? parent, reader, errors)) + { + continue; + } + + InterfaceImplementationHandleCollection interfaceImplsCollection = typeDef.GetInterfaceImplementations(); + if (interfaceImplsCollection.Count == 0) + { + interfaceImpls = Array.Empty(); + } + else + { + interfaceImpls = new MType[interfaceImplsCollection.Count]; + int i = 0; + foreach (InterfaceImplementationHandle implHandle in interfaceImplsCollection) + { + InterfaceImplementation interfaceImpl = reader.GetInterfaceImplementation(implHandle); + + if (type.ParseInheritType(interfaceImpl.Interface, out MType? implemented, reader, errors)) + { + interfaceImpls[i++] = implemented; + } + } + + interfaceImpls = interfaceImpls[..i]; + } + + list.Add((type, parent, interfaceImpls)); + } + + return list; + } + + internal static List GetReferencedTypes(this MetadataReader reader, ConcurrentBag errors) + { + return reader.TypeReferences.Select(typeRefHandle => + { + try + { + return reader.ParseTypeReference(typeRefHandle); + } + catch (UnsupportedMetadataException e) + { + errors.Add(new(e)); + return null; + } + }) + .Where(p => p != null) + .ToList()!; + } + + /// + /// Thrown if the metadata does something funny we don't "support" like type forwarding. + /// + internal static MTypeReferenced ParseTypeReference(this MetadataReader reader, TypeReferenceHandle handle) + { + TypeReference typeRef = reader.GetTypeReference(handle); + string name = reader.GetString(typeRef.Name); + string? nameSpace = typeRef.Namespace.NilNullString(reader); + MResScope resScope; + + // See II.22.38 in ECMA-335 + if (typeRef.ResolutionScope.IsNil) + { + throw new UnsupportedMetadataException( + $"Null resolution scope on type Name: {nameSpace}.{name}. This indicates exported/forwarded types"); + } + + switch (typeRef.ResolutionScope.Kind) + { + case HandleKind.AssemblyReference: + { + // Different assembly. + AssemblyReference assemblyRef = + reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); + string assemblyName = reader.GetString(assemblyRef.Name); + resScope = new MResScopeAssembly(assemblyName); + break; + } + case HandleKind.TypeReference: + { + // Nested type. + MTypeReferenced enclosingType = ParseTypeReference(reader, (TypeReferenceHandle)typeRef.ResolutionScope); + resScope = new MResScopeType(enclosingType); + break; + } + case HandleKind.ModuleReference: + { + // Same-assembly-different-module + throw new UnsupportedMetadataException( + $"Cross-module reference to type {nameSpace}.{name}. "); + } + default: + // Edge cases not handled: + // https://github.com/dotnet/runtime/blob/b2e5a89085fcd87e2fa9300b4bb00cd499c5845b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/DisassemblingTypeProvider.cs#L130-L132 + throw new UnsupportedMetadataException( + $"TypeRef to {typeRef.ResolutionScope.Kind} for type {nameSpace}.{name}"); + } + + return new(resScope, name, nameSpace); + } + + internal static List GetReferencedMembers(this MetadataReader reader, ConcurrentBag errors) + { + if (_parallelReferencedMembersScanning) + { + return ReferencedMembersScanner.ParallelReferencedMembersCheck(reader, errors); + } + else + { + return ReferencedMembersScanner.NonParallelReferencedMembersCheck(reader, errors); + } + } + + internal static MTypeDefined GetTypeFromDefinition(this MetadataReader reader, TypeDefinitionHandle handle) + { + TypeDefinition typeDef = reader.GetTypeDefinition(handle); + string name = reader.GetString(typeDef.Name); + string? ns = typeDef.Namespace.NilNullString(reader); + MTypeDefined? enclosing = null; + if (typeDef.IsNested) + { + enclosing = reader.GetTypeFromDefinition(typeDef.GetDeclaringType()); + } + + return new(name, ns, enclosing); + } + + internal static string FormatMethodName(this MetadataReader reader, MethodDefinition method) + { + MethodSignature methodSig = method.DecodeSignature(new TypeProvider(), 0); + MTypeDefined type = reader.GetTypeFromDefinition(method.GetDeclaringType()); + + return + $"{type}.{reader.GetString(method.Name)}({string.Join(", ", methodSig.ParameterTypes)}) Returns {methodSig.ReturnType} "; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Infrastructure/StringHandleExtensions.cs b/UnitystationLauncher/Infrastructure/StringHandleExtensions.cs new file mode 100644 index 0000000..6c4fd0f --- /dev/null +++ b/UnitystationLauncher/Infrastructure/StringHandleExtensions.cs @@ -0,0 +1,11 @@ +using System.Reflection.Metadata; + +namespace UnitystationLauncher.Infrastructure; + +internal static class StringHandleExtensions +{ + internal static string? NilNullString(this StringHandle handle, MetadataReader reader) + { + return handle.IsNil ? null : reader.GetString(handle); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Infrastructure/TypeExtensions.cs b/UnitystationLauncher/Infrastructure/TypeExtensions.cs index fe282e1..2b31ab3 100644 --- a/UnitystationLauncher/Infrastructure/TypeExtensions.cs +++ b/UnitystationLauncher/Infrastructure/TypeExtensions.cs @@ -31,7 +31,7 @@ public static IEnumerable DumpMetaMembers(this Type type) { TypeDefinition tempTypeDef = metaReader.GetTypeDefinition(typeDefHandle); string name = metaReader.GetString(tempTypeDef.Name); - string? @namespace = AssemblyTypeCheckerHelpers.NilNullString(metaReader, tempTypeDef.Namespace); + string? @namespace = tempTypeDef.Namespace.NilNullString(metaReader); if (name == type.Name && @namespace == type.Namespace) { typeDef = tempTypeDef; diff --git a/UnitystationLauncher/Models/ContentScanning/MType.cs b/UnitystationLauncher/Models/ContentScanning/MType.cs index b56b42f..004eee6 100644 --- a/UnitystationLauncher/Models/ContentScanning/MType.cs +++ b/UnitystationLauncher/Models/ContentScanning/MType.cs @@ -1,4 +1,11 @@ -namespace UnitystationLauncher.Models.ContentScanning; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Infrastructure; + +namespace UnitystationLauncher.Models.ContentScanning; public record MType @@ -20,5 +27,53 @@ public virtual bool IsCoreTypeDefined() { return ToString(); } + + internal bool ParseInheritType(EntityHandle handle, [NotNullWhen(true)] out MType? type, MetadataReader reader, ConcurrentBag errors) + { + type = default; + + switch (handle.Kind) + { + case HandleKind.TypeDefinition: + // Definition to type in same assembly, allowed without hassle. + return false; + + case HandleKind.TypeReference: + // Regular type reference. + try + { + type = reader.ParseTypeReference((TypeReferenceHandle)handle); + return true; + } + catch (UnsupportedMetadataException u) + { + errors.Add(new(u)); + return false; + } + + case HandleKind.TypeSpecification: + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle); + // Generic type reference. + TypeProvider provider = new(); + type = typeSpec.DecodeSignature(provider, 0); + + if (type.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics. + return false; + } + + break; + + default: + errors.Add(new( + $"Unsupported BaseType of kind {handle.Kind} on type {this}")); + return false; + } + + type = default!; + return false; + } } diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs index d244824..a725a98 100644 --- a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using UnitystationLauncher.Models.ConfigFile; + namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; internal sealed record MTypeReferenced(MResScope ResolutionScope, string Name, string? Namespace) : MType @@ -34,4 +38,76 @@ public override bool WhitelistEquals(MType other) _ => false }; } + + public bool IsTypeAccessAllowed(SandboxConfig sandboxConfig, [NotNullWhen(true)] out TypeConfig? cfg) + { + if (Namespace == null) + { + if (ResolutionScope is MResScopeType parentType) + { + if (parentType.Type is MTypeReferenced parentReferencedType) + { + if (parentReferencedType.IsTypeAccessAllowed(sandboxConfig, out TypeConfig? parentCfg) == false) + { + cfg = null; + return false; + } + + if (parentCfg.All) + { + // Enclosing type is namespace-whitelisted so we don't have to check anything else. + cfg = TypeConfig.DefaultAll; + return true; + } + + // Found enclosing type, checking if we are allowed to access this nested type. + // Also pass it up in case of multiple nested types. + if (parentCfg.NestedTypes != null && parentCfg.NestedTypes.TryGetValue(Name, out cfg)) + { + return true; + } + + cfg = null; + return false; + } + + if (ResolutionScope is MResScopeAssembly mResScopeAssembly && + sandboxConfig.MultiAssemblyOtherReferences.Contains(mResScopeAssembly.Name)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + + // Types without namespaces or nesting parent are not allowed at all. + cfg = null; + return false; + } + } + + // Check if in whitelisted namespaces. + foreach (string whNamespace in sandboxConfig.WhitelistedNamespaces) + { + if (Namespace?.StartsWith(whNamespace) ?? false) + { + cfg = TypeConfig.DefaultAll; + return true; + } + } + + if (ResolutionScope is MResScopeAssembly resScopeAssembly && + sandboxConfig.MultiAssemblyOtherReferences.Contains(resScopeAssembly.Name)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + + + if (Namespace == null || sandboxConfig.Types.TryGetValue(Namespace, out Dictionary? nsDict) == false) + { + cfg = null; + return false; + } + + return nsDict.TryGetValue(Name, out cfg); + } } \ No newline at end of file diff --git a/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs b/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs index ea4ecc7..c5f06a8 100644 --- a/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs +++ b/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs @@ -2,19 +2,19 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Threading.Tasks; using ILVerify; -using UnitystationLauncher.Constants; using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.ContentScanning.Scanners; +using UnitystationLauncher.Infrastructure; using UnitystationLauncher.Models.ConfigFile; using UnitystationLauncher.Models.ContentScanning; using UnitystationLauncher.Models.ContentScanning.ScanningTypes; -using UnitystationLauncher.Models.Enums; + using UnitystationLauncher.Services.Interface; // psst @@ -31,8 +31,6 @@ namespace UnitystationLauncher.Services; /// public sealed class AssemblyTypeCheckerService : IAssemblyTypeCheckerService { - private readonly bool _parallelIlScanning = false; - private readonly bool _parallelMemberReferencesScanning = true; private readonly Task _config; public AssemblyTypeCheckerService(ICodeScanConfigService codeScanConfigService) @@ -68,7 +66,7 @@ public async Task CheckAssemblyTypesAsync(FileInfo diskPath, DirectoryInfo return false; } - if (DoVerifyIl(asmName, resolver, peReader, reader, infoAction, errorsAction) == false) + if (IlScanner.DoVerifyIl(asmName, resolver, peReader, reader, infoAction, errorsAction, await _config) == false) { errorsAction.Invoke($"Assembly {asmName} Has invalid IL code"); return false; @@ -76,9 +74,9 @@ public async Task CheckAssemblyTypesAsync(FileInfo diskPath, DirectoryInfo ConcurrentBag errors = new(); - List types = AssemblyTypeCheckerHelpers.GetReferencedTypes(reader, errors); - List members = AssemblyTypeCheckerHelpers.GetReferencedMembers(reader, errors); - List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited = GetExternalInheritedTypes(reader, errors); + List types = reader.GetReferencedTypes(errors); + List members = reader.GetReferencedMembers(errors); + List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited = reader.GetExternalInheritedTypes(errors); infoAction.Invoke($"References loaded... {fullStopwatch.ElapsedMilliseconds}ms"); SandboxConfig loadedConfig = _config.Result; @@ -91,7 +89,7 @@ public async Task CheckAssemblyTypesAsync(FileInfo diskPath, DirectoryInfo // we won't have to check that any types in their type arguments are whitelisted. foreach (MTypeReferenced type in types) { - if (IsTypeAccessAllowed(loadedConfig, type, out _) == false) + if (type.IsTypeAccessAllowed(loadedConfig, out _) == false) { errors.Add(new($"Access to type not allowed: {type} asmName {asmName}")); } @@ -99,19 +97,19 @@ public async Task CheckAssemblyTypesAsync(FileInfo diskPath, DirectoryInfo infoAction.Invoke($"Types... {fullStopwatch.ElapsedMilliseconds}ms"); - CheckInheritance(loadedConfig, inherited, errors); + InheritanceScanner.CheckInheritance(loadedConfig, inherited, errors); infoAction.Invoke($"Inheritance... {fullStopwatch.ElapsedMilliseconds}ms"); - AssemblyTypeCheckerHelpers.CheckNoUnmanagedMethodDefs(reader, errors); + UnmanagedMethodScanner.CheckNoUnmanagedMethodDefs(reader, errors); infoAction.Invoke($"Unmanaged methods... {fullStopwatch.ElapsedMilliseconds}ms"); - AssemblyTypeCheckerHelpers.CheckNoTypeAbuse(reader, errors); + TypeAbuseScanner.CheckNoTypeAbuse(reader, errors); infoAction.Invoke($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms"); - CheckMemberReferences(loadedConfig, members, errors); + MemberReferenceScanner.CheckMemberReferences(loadedConfig, members, errors); errors = new(errors.OrderBy(x => x.Message)); @@ -125,458 +123,4 @@ public async Task CheckAssemblyTypesAsync(FileInfo diskPath, DirectoryInfo peReader.Dispose(); return errors.IsEmpty; } - - private bool DoVerifyIl( - string name, - IResolver resolver, - PEReader peReader, - MetadataReader reader, - Action info, - Action logErrors) - { - info.Invoke($"{name}: Verifying IL..."); - Stopwatch sw = Stopwatch.StartNew(); - ConcurrentBag bag = new(); - - if (_parallelIlScanning) - { - OrderablePartitioner partitioner = Partitioner.Create(reader.TypeDefinitions); - Parallel.ForEach(partitioner.GetPartitions(Environment.ProcessorCount), handle => - { - Verifier ver = new(resolver); - ver.SetSystemModuleName(new(AssemblyNames.SystemAssemblyName)); - while (handle.MoveNext()) - { - foreach (VerificationResult? result in ver.Verify(peReader, handle.Current, verifyMethods: true)) - { - bag.Add(result); - } - } - }); - } - else - { - Verifier ver = new(resolver); - //mscorlib - ver.SetSystemModuleName(new(AssemblyNames.SystemAssemblyName)); - foreach (TypeDefinitionHandle definition in reader.TypeDefinitions) - { - IEnumerable errors = ver.Verify(peReader, definition, verifyMethods: true); - foreach (VerificationResult? error in errors) - { - bag.Add(error); - } - } - } - - SandboxConfig loadedCfg = _config.Result; - - bool verifyErrors = false; - foreach (VerificationResult res in bag) - { - if (loadedCfg.AllowedVerifierErrors.Contains(res.Code)) - { - continue; - } - - string formatted = res.Args == null ? res.Message : string.Format(res.Message, res.Args); - string msg = $"{name}: ILVerify: {formatted}"; - - if (!res.Method.IsNil) - { - MethodDefinition method = reader.GetMethodDefinition(res.Method); - string methodName = AssemblyTypeCheckerHelpers.FormatMethodName(reader, method); - - msg = $"{msg}, method: {methodName}"; - } - - if (!res.Type.IsNil) - { - MTypeDefined type = AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, res.Type); - msg = $"{msg}, type: {type}"; - } - - - verifyErrors = true; - logErrors.Invoke(msg); - } - - info.Invoke($"{name}: Verified IL in {sw.Elapsed.TotalMilliseconds}ms"); - - if (verifyErrors) - { - return false; - } - - return true; - } - - private void CheckMemberReferences(SandboxConfig sandboxConfig, List members, ConcurrentBag errors) - { - if (_parallelMemberReferencesScanning) - { - Parallel.ForEach(members, memberRef => - { - MType baseType = memberRef.ParentType; - while (!(baseType is MTypeReferenced)) - { - switch (baseType) - { - case MTypeGeneric generic: - { - baseType = generic.GenericType; - - break; - } - case MTypeWackyArray: - { - // Members on arrays (not to be confused with vectors) are all fine. - // See II.14.2 in ECMA-335. - return; - } - case MTypeDefined: - { - // Valid for this to show up, safe to ignore. - return; - } - default: - { - throw new ArgumentOutOfRangeException(); - } - } - } - - MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; - - if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out TypeConfig? typeCfg) == false) - { - // Technically this error isn't necessary since we have an earlier pass - // checking all referenced types. That should have caught this - // We still need the typeCfg so that's why we're checking. Might as well. - errors.Add(new($"Access to type not allowed: {baseTypeReferenced}")); - return; - } - - if (typeCfg.All) - { - // Fully whitelisted for the type, we good. - return; - } - - switch (memberRef) - { - case MMemberRefField mMemberRefField: - { - foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) - { - if (field.Name == mMemberRefField.Name && - mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) - { - return; // Found - } - } - - errors.Add(new($"Access to field not allowed: {mMemberRefField}")); - break; - } - case MMemberRefMethod mMemberRefMethod: - foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) - { - bool notParamMismatch = true; - - if (parsed.Name == mMemberRefMethod.Name && - mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && - mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && - mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) - { - for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) - { - MType a = mMemberRefMethod.ParameterTypes[i]; - MType b = parsed.ParameterTypes[i]; - - if (a.WhitelistEquals(b) == false) - { - notParamMismatch = false; - break; - } - } - - if (notParamMismatch) - { - return; // Found - } - } - } - - errors.Add(new($"Access to method not allowed: {mMemberRefMethod}")); - break; - default: - throw new ArgumentOutOfRangeException(nameof(memberRef)); - } - }); - } - else - { - foreach (MMemberRef memberRef in members) - { - MType baseType = memberRef.ParentType; - while (!(baseType is MTypeReferenced)) - { - switch (baseType) - { - case MTypeGeneric generic: - { - baseType = generic.GenericType; - - break; - } - case MTypeWackyArray: - { - // Members on arrays (not to be confused with vectors) are all fine. - // See II.14.2 in ECMA-335. - continue; - } - case MTypeDefined: - { - // Valid for this to show up, safe to ignore. - continue; - } - default: - { - throw new ArgumentOutOfRangeException(); - } - } - } - - MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; - - if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out TypeConfig? typeCfg) == false) - { - // Technically this error isn't necessary since we have an earlier pass - // checking all referenced types. That should have caught this - // We still need the typeCfg so that's why we're checking. Might as well. - errors.Add(new($"Access to type not allowed: {baseTypeReferenced}")); - continue; - } - - if (typeCfg.All) - { - // Fully whitelisted for the type, we good. - continue; - } - - switch (memberRef) - { - case MMemberRefField mMemberRefField: - { - foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) - { - if (field.Name == mMemberRefField.Name && - mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) - { - continue; // Found - } - } - - errors.Add(new($"Access to field not allowed: {mMemberRefField}")); - break; - } - case MMemberRefMethod mMemberRefMethod: - bool notParamMismatch = true; - foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) - { - if (parsed.Name == mMemberRefMethod.Name && - mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && - mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && - mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) - { - for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) - { - MType a = mMemberRefMethod.ParameterTypes[i]; - MType b = parsed.ParameterTypes[i]; - - if (!a.WhitelistEquals(b)) - { - notParamMismatch = false; - break; - - } - } - - if (notParamMismatch) - { - break; // Found - } - break; - } - } - - if (notParamMismatch == false) - { - continue; - } - - errors.Add(new($"Access to method not allowed: {mMemberRefMethod}")); - break; - default: - throw new ArgumentOutOfRangeException(nameof(memberRef)); - } - } - } - } - - private void CheckInheritance( - SandboxConfig sandboxConfig, - List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited, - ConcurrentBag errors) - { - // This inheritance whitelisting primarily serves to avoid content doing funny stuff - // by e.g. inheriting Type. - foreach ((MType _, MType baseType, ArraySegment interfaces) in inherited) - { - if (CanInherit(baseType) == false) - { - errors.Add(new($"Inheriting of type not allowed: {baseType}")); - } - - foreach (MType @interface in interfaces) - { - if (CanInherit(@interface) == false) - { - errors.Add(new($"Implementing of interface not allowed: {@interface}")); - } - } - - bool CanInherit(MType inheritType) - { - MTypeReferenced realBaseType = inheritType switch - { - MTypeGeneric generic => (MTypeReferenced)generic.GenericType, - MTypeReferenced referenced => referenced, - _ => throw new InvalidOperationException() // Can't happen. - }; - - if (IsTypeAccessAllowed(sandboxConfig, realBaseType, out TypeConfig? cfg) == false) - { - return false; - } - - return cfg.Inherit != InheritMode.Block && (cfg.Inherit == InheritMode.Allow || cfg.All); - } - } - } - - private bool IsTypeAccessAllowed(SandboxConfig sandboxConfig, MTypeReferenced type, - [NotNullWhen(true)] out TypeConfig? cfg) - { - if (type.Namespace == null) - { - if (type.ResolutionScope is MResScopeType parentType) - { - if (IsTypeAccessAllowed(sandboxConfig, (MTypeReferenced)parentType.Type, out TypeConfig? parentCfg) == false) - { - cfg = null; - return false; - } - - if (parentCfg.All) - { - // Enclosing type is namespace-whitelisted so we don't have to check anything else. - cfg = TypeConfig.DefaultAll; - return true; - } - - // Found enclosing type, checking if we are allowed to access this nested type. - // Also pass it up in case of multiple nested types. - if (parentCfg.NestedTypes != null && parentCfg.NestedTypes.TryGetValue(type.Name, out cfg)) - { - return true; - } - - cfg = null; - return false; - } - - if (type.ResolutionScope is MResScopeAssembly mResScopeAssembly && - sandboxConfig.MultiAssemblyOtherReferences.Contains(mResScopeAssembly.Name)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - - // Types without namespaces or nesting parent are not allowed at all. - cfg = null; - return false; - } - - // Check if in whitelisted namespaces. - foreach (string whNamespace in sandboxConfig.WhitelistedNamespaces) - { - if (type.Namespace.StartsWith(whNamespace)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - } - - if (type.ResolutionScope is MResScopeAssembly resScopeAssembly && - sandboxConfig.MultiAssemblyOtherReferences.Contains(resScopeAssembly.Name)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - - - if (sandboxConfig.Types.TryGetValue(type.Namespace, out Dictionary? nsDict) == false) - { - cfg = null; - return false; - } - - return nsDict.TryGetValue(type.Name, out cfg); - } - - private List<(MType type, MType parent, ArraySegment interfaceImpls)> GetExternalInheritedTypes( - MetadataReader reader, - ConcurrentBag errors) - { - List<(MType, MType, ArraySegment)> list = new(); - foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) - { - TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); - ArraySegment interfaceImpls; - MTypeDefined type = AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, typeDefHandle); - - if (!AssemblyTypeCheckerHelpers.ParseInheritType(type, typeDef.BaseType, out MType? parent, reader, errors)) - { - continue; - } - - InterfaceImplementationHandleCollection interfaceImplsCollection = typeDef.GetInterfaceImplementations(); - if (interfaceImplsCollection.Count == 0) - { - interfaceImpls = Array.Empty(); - } - else - { - interfaceImpls = new MType[interfaceImplsCollection.Count]; - int i = 0; - foreach (InterfaceImplementationHandle implHandle in interfaceImplsCollection) - { - InterfaceImplementation interfaceImpl = reader.GetInterfaceImplementation(implHandle); - - if (AssemblyTypeCheckerHelpers.ParseInheritType(type, interfaceImpl.Interface, out MType? implemented, reader, errors)) - { - interfaceImpls[i++] = implemented; - } - } - - interfaceImpls = interfaceImpls[..i]; - } - - list.Add((type, parent, interfaceImpls)); - } - - return list; - } } \ No newline at end of file diff --git a/UnitystationLauncher/Services/CodeScanService.cs b/UnitystationLauncher/Services/CodeScanService.cs index 6cced34..f64313b 100644 --- a/UnitystationLauncher/Services/CodeScanService.cs +++ b/UnitystationLauncher/Services/CodeScanService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Serilog; using UnitystationLauncher.Constants; +using UnitystationLauncher.Exceptions; using UnitystationLauncher.Models.Enums; using UnitystationLauncher.Services.Interface; @@ -56,8 +57,6 @@ public async Task OnScanAsync(ZipArchive archive, string targetDirectory, DeleteFilesWithExtension(processingDirectory.ToString(), ".bundle", exceptionDirectory: Path.Combine(processingDirectory.ToString(), @"Unitystation.app/Contents/Resources/Data/StreamingAssets")); } - - DirectoryInfo? stagingManaged = null; if (_environmentService.GetCurrentEnvironment() != CurrentEnvironment.MacOsStandalone) { @@ -124,8 +123,6 @@ public async Task OnScanAsync(ZipArchive archive, string targetDirectory, return false; } - - CopyFilesRecursively(goodFilePath, processingDirectory.ToString()); if (dataPath.Name != FolderNames.UnitystationData && _environmentService.GetCurrentEnvironment() != CurrentEnvironment.MacOsStandalone) //I know Cases and to file systems but F { @@ -134,42 +131,17 @@ public async Task OnScanAsync(ZipArchive archive, string targetDirectory, Directory.Delete(oldPath, true); } - switch (_environmentService.GetCurrentEnvironment()) { case CurrentEnvironment.WindowsStandalone: - FileInfo? exeRename = processingDirectory.GetFiles() - .FirstOrDefault(x => x.Extension == ".exe" && x.Name != "UnityCrashHandler64.exe"); //TODO OS - - - if (exeRename == null || exeRename.Directory == null) - { - errors.Invoke("no Executable found "); - DeleteContentsOfDirectory(processingDirectory); - DeleteContentsOfDirectory(stagingDirectory); - return false; - } - info.Invoke($"Found exeRename {exeRename}"); - exeRename.MoveTo(Path.Combine(exeRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + ".exe")); + LocateWindowsExecutable(processingDirectory, stagingDirectory, dataPath, info, errors); break; case CurrentEnvironment.LinuxFlatpak: case CurrentEnvironment.LinuxStandalone: - FileInfo? executableRename = processingDirectory.GetFiles() - .FirstOrDefault(x => x.Extension == ""); - - if (executableRename == null || executableRename.Directory == null) - { - errors.Invoke("no Executable found "); - DeleteContentsOfDirectory(processingDirectory); - DeleteContentsOfDirectory(stagingDirectory); - return false; - } - info.Invoke($"Found ExecutableRename {executableRename}"); - executableRename.MoveTo(Path.Combine(executableRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + "")); + LocateLinuxExecutable(processingDirectory, stagingDirectory, dataPath, info, errors); break; } - DirectoryInfo targetDirectoryInfo = new(targetDirectory); if (targetDirectoryInfo.Exists) { @@ -191,6 +163,41 @@ public async Task OnScanAsync(ZipArchive archive, string targetDirectory, return true; } + private static void LocateWindowsExecutable(DirectoryInfo processingDirectory, DirectoryInfo stagingDirectory, DirectoryInfo dataPath, Action info, Action errors) + { + FileInfo? exeRename = processingDirectory.GetFiles() + .FirstOrDefault(x => x.Extension == ".exe" && x.Name != "UnityCrashHandler64.exe"); //TODO OS + + + if (exeRename?.Directory == null) + { + errors.Invoke("no Executable found "); + DeleteContentsOfDirectory(processingDirectory); + DeleteContentsOfDirectory(stagingDirectory); + throw new CodeScanningException("No Windows executable found"); + } + + info.Invoke($"Found exeRename {exeRename}"); + exeRename.MoveTo(Path.Combine(exeRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + ".exe")); + } + + private static void LocateLinuxExecutable(DirectoryInfo processingDirectory, DirectoryInfo stagingDirectory, DirectoryInfo dataPath, Action info, Action errors) + { + FileInfo? executableRename = processingDirectory.GetFiles() + .FirstOrDefault(x => x.Extension == ""); + + if (executableRename?.Directory == null) + { + errors.Invoke("no Executable found "); + DeleteContentsOfDirectory(processingDirectory); + DeleteContentsOfDirectory(stagingDirectory); + throw new CodeScanningException("No Linux executable found"); + } + + info.Invoke($"Found ExecutableRename {executableRename}"); + executableRename.MoveTo(Path.Combine(executableRename.Directory.ToString(), dataPath.Name.Replace("_Data", "") + "")); + } + private string GetManagedOnOS(string goodFiles) { CurrentEnvironment os = _environmentService.GetCurrentEnvironment(); diff --git a/UnitystationLauncher/Services/InstallationService.cs b/UnitystationLauncher/Services/InstallationService.cs index c0fd585..9004018 100644 --- a/UnitystationLauncher/Services/InstallationService.cs +++ b/UnitystationLauncher/Services/InstallationService.cs @@ -414,8 +414,7 @@ private static string GetArguments(string? server, long? port) return arguments; } - public static List InfoList = new(); - public static List ErrorList = new(); + private async Task StartDownloadAsync(Download download) { Log.Information("Download requested, Installation Path '{Path}', Url '{Url}'", download.InstallPath, download.DownloadUrl); @@ -455,6 +454,9 @@ private async Task StartDownloadAsync(Download download) private async Task ExtractAndScan(Download download, ProgressStream progressStream) { + // TODO: Display infoList and errorList in the UI. + List infoList = new(); + List errorList = new(); Log.Information("Extracting..."); try { @@ -464,13 +466,13 @@ private async Task ExtractAndScan(Download download, ProgressStream progressStre void Info(string log) { Log.Information(log); - InfoList.Add(log); + infoList.Add(log); } void Errors(string log) { Log.Error(log); - ErrorList.Add(log); + errorList.Add(log); } download.DownloadState = DownloadState.Scanning; @@ -496,7 +498,7 @@ void Errors(string log) } else { - string jsonString = JsonSerializer.Serialize(ErrorList); + string jsonString = JsonSerializer.Serialize(errorList); string filePath = Path.Combine(_preferencesService.GetPreferences().InstallationPath, "CodeScanErrors.json"); await File.WriteAllTextAsync(filePath, jsonString); diff --git a/UnitystationLauncher/ViewModels/ServerViewModel.cs b/UnitystationLauncher/ViewModels/ServerViewModel.cs index efbdc89..d553cde 100644 --- a/UnitystationLauncher/ViewModels/ServerViewModel.cs +++ b/UnitystationLauncher/ViewModels/ServerViewModel.cs @@ -103,7 +103,7 @@ private void RefreshDownloadingStatus() this.RaisePropertyChanged(nameof(ShowDownloadFailed)); // Refresh the UI for this server more often while it is downloading. - if (ShowDownloadProgress) + if (ShowDownloadProgress || ShowScanningProgress) { RxApp.MainThreadScheduler.Schedule(DateTimeOffset.Now.AddMilliseconds(200), RefreshDownloadingStatus); } @@ -112,6 +112,12 @@ private void RefreshDownloadingStatus() this.RaisePropertyChanged(nameof(Installation)); this.RaisePropertyChanged(nameof(ShowStartButton)); } + + // Clear out the old Download object once we have an Installation. We won't need it anymore. + if (Download != null && Installation != null) + { + Download = null; + } } }