File: Linker.Dataflow\ReflectionMethodBodyScanner.cs
Web Access
Project: src\src\tools\illink\src\linker\Mono.Linker.csproj (illink)
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ILLink.Shared.TrimAnalysis;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Linker.Steps;
using MultiValue = ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>;
 
namespace Mono.Linker.Dataflow
{
    sealed class ReflectionMethodBodyScanner : MethodBodyScanner
    {
        readonly MarkStep _markStep;
        MessageOrigin _origin;
        readonly FlowAnnotations _annotations;
        readonly ReflectionMarker _reflectionMarker;
        public readonly TrimAnalysisPatternStore TrimAnalysisPatterns;
 
        public static bool RequiresReflectionMethodBodyScannerForCallSite(LinkContext context, MethodReference calledMethod)
        {
            MethodDefinition? methodDefinition = context.TryResolve(calledMethod);
            if (methodDefinition == null)
                return false;
 
            return Intrinsics.GetIntrinsicIdForMethod(methodDefinition) > IntrinsicId.RequiresReflectionBodyScanner_Sentinel ||
                context.Annotations.FlowAnnotations.RequiresDataFlowAnalysis(methodDefinition) ||
                context.Annotations.DoesMethodRequireUnreferencedCode(methodDefinition, out _) ||
                IsPInvokeDangerous(methodDefinition, context, out _);
        }
 
        public static bool RequiresReflectionMethodBodyScannerForMethodBody(LinkContext context, MethodDefinition methodDefinition)
        {
            return Intrinsics.GetIntrinsicIdForMethod(methodDefinition) > IntrinsicId.RequiresReflectionBodyScanner_Sentinel ||
                context.Annotations.FlowAnnotations.RequiresDataFlowAnalysis(methodDefinition);
        }
 
        public static bool RequiresReflectionMethodBodyScannerForAccess(LinkContext context, FieldReference field)
        {
            FieldDefinition? fieldDefinition = context.TryResolve(field);
            if (fieldDefinition == null)
                return false;
 
            return context.Annotations.FlowAnnotations.RequiresDataFlowAnalysis(fieldDefinition);
        }
 
        public ReflectionMethodBodyScanner(LinkContext context, MarkStep parent, MessageOrigin origin)
            : base(context)
        {
            _markStep = parent;
            _origin = origin;
            _annotations = context.Annotations.FlowAnnotations;
            _reflectionMarker = new ReflectionMarker(context, parent, enabled: false);
            TrimAnalysisPatterns = new TrimAnalysisPatternStore(MultiValueLattice, context);
        }
 
        public override void InterproceduralScan(MethodIL methodIL)
        {
            base.InterproceduralScan(methodIL);
 
            var reflectionMarker = new ReflectionMarker(_context, _markStep, enabled: true);
            TrimAnalysisPatterns.MarkAndProduceDiagnostics(reflectionMarker, _markStep);
        }
 
        protected override void Scan(MethodIL methodIL, ref InterproceduralState interproceduralState)
        {
            _origin = new MessageOrigin(methodIL.Method);
            base.Scan(methodIL, ref interproceduralState);
        }
 
        protected override void WarnAboutInvalidILInMethod(MethodBody method, int ilOffset)
        {
            // Serves as a debug helper to make sure valid IL is not considered invalid.
            //
            // The .NET Native compiler used to warn if it detected invalid IL during treeshaking,
            // but the warnings were often triggered in autogenerated dead code of a major game engine
            // and resulted in support calls. No point in warning. If the code gets exercised at runtime,
            // an InvalidProgramException will likely be raised.
            Debug.Fail("Invalid IL or a bug in the scanner");
        }
 
        protected override ValueWithDynamicallyAccessedMembers GetMethodParameterValue(ParameterProxy parameter)
            => GetMethodParameterValue(parameter, _context.Annotations.FlowAnnotations.GetParameterAnnotation(parameter));
 
        MethodParameterValue GetMethodParameterValue(ParameterProxy parameter, DynamicallyAccessedMemberTypes dynamicallyAccessedMemberTypes)
            => _annotations.GetMethodParameterValue(parameter, dynamicallyAccessedMemberTypes);
 
        protected override MultiValue GetFieldValue(FieldReference field) => _annotations.GetFieldValue(field);
 
        protected override MethodReturnValue GetReturnValue(MethodDefinition method) => _annotations.GetMethodReturnValue(method, isNewObj: false);
 
        private void HandleStoreValueWithDynamicallyAccessedMembers(ValueWithDynamicallyAccessedMembers targetValue, Instruction operation, MultiValue sourceValue, int? parameterIndex)
        {
            if (targetValue.DynamicallyAccessedMemberTypes != 0)
            {
                _origin = _origin.WithInstructionOffset(operation.Offset);
                TrimAnalysisPatterns.Add(new TrimAnalysisAssignmentPattern(sourceValue, targetValue, _origin, parameterIndex));
            }
        }
 
        protected override void HandleStoreField(MethodDefinition method, FieldValue field, Instruction operation, MultiValue valueToStore, int? parameterIndex)
            => HandleStoreValueWithDynamicallyAccessedMembers(field, operation, valueToStore, parameterIndex);
 
        protected override void HandleStoreParameter(MethodDefinition method, MethodParameterValue parameter, Instruction operation, MultiValue valueToStore, int? parameterIndex)
            => HandleStoreValueWithDynamicallyAccessedMembers(parameter, operation, valueToStore, parameterIndex);
 
        protected override void HandleReturnValue(MethodDefinition method, MethodReturnValue returnValue, Instruction operation, MultiValue valueToStore)
            => HandleStoreValueWithDynamicallyAccessedMembers(returnValue, operation, valueToStore, null);
 
        public override MultiValue HandleCall(MethodBody callingMethodBody, MethodReference calledMethod, Instruction operation, ValueNodeList methodParams)
        {
            var reflectionProcessed = _markStep.ProcessReflectionDependency(callingMethodBody, operation);
            if (reflectionProcessed)
            {
                return UnknownValue.Instance;
            }
 
            Debug.Assert(callingMethodBody.Method == _origin.Provider);
            var calledMethodDefinition = _context.TryResolve(calledMethod);
            if (calledMethodDefinition == null)
            {
                return UnknownValue.Instance;
            }
 
            _origin = _origin.WithInstructionOffset(operation.Offset);
 
            MultiValue instanceValue;
            ImmutableArray<MultiValue> arguments;
            if (calledMethodDefinition.HasImplicitThis())
            {
                instanceValue = methodParams[0];
                arguments = methodParams.Skip(1).ToImmutableArray();
            }
            else
            {
                instanceValue = MultiValueLattice.Top;
                arguments = methodParams.ToImmutableArray();
            }
 
            TrimAnalysisPatterns.Add(new TrimAnalysisMethodCallPattern(
                operation,
                calledMethod,
                instanceValue,
                arguments,
                _origin
            ));
 
            var diagnosticContext = new DiagnosticContext(_origin, diagnosticsEnabled: false, _context);
            return HandleCall(
                operation,
                calledMethod,
                instanceValue,
                arguments,
                diagnosticContext,
                _reflectionMarker,
                _context,
                _markStep);
        }
 
        public static MultiValue HandleCall(
            Instruction operation,
            MethodReference calledMethod,
            MultiValue instanceValue,
            ImmutableArray<MultiValue> argumentValues,
            DiagnosticContext diagnosticContext,
            ReflectionMarker reflectionMarker,
            LinkContext context,
            MarkStep markStep)
        {
            var origin = diagnosticContext.Origin;
            if (!MethodProxy.TryCreate(calledMethod, context, out MethodProxy? calledMethodProxy))
            {
                Debug.Fail("Should only be called for resolvable methods");
                return UnknownValue.Instance;
            }
            var calledMethodDefinition = calledMethodProxy.Value.Definition;
            var callingMethodDefinition = origin.Provider as MethodDefinition;
            Debug.Assert(callingMethodDefinition != null);
 
            bool requiresDataFlowAnalysis = context.Annotations.FlowAnnotations.RequiresDataFlowAnalysis(calledMethodDefinition);
            bool isNewObj = operation.OpCode.Code == Code.Newobj;
            var annotatedMethodReturnValue = context.Annotations.FlowAnnotations.GetMethodReturnValue(calledMethodProxy.Value, isNewObj);
            Debug.Assert(requiresDataFlowAnalysis || annotatedMethodReturnValue.DynamicallyAccessedMemberTypes == DynamicallyAccessedMemberTypes.None);
 
            var handleCallAction = new HandleCallAction(context, operation, markStep, reflectionMarker, diagnosticContext, callingMethodDefinition);
            var intrinsicId = Intrinsics.GetIntrinsicIdForMethod(calledMethodProxy.Value);
            if (!handleCallAction.Invoke(calledMethodProxy.Value, instanceValue, argumentValues, intrinsicId, out MultiValue methodReturnValue))
                throw new NotImplementedException($"Unhandled intrinsic: {intrinsicId}");
            return methodReturnValue;
        }
 
        static bool IsComInterop(IMarshalInfoProvider marshalInfoProvider, TypeReference parameterType, LinkContext context)
        {
            // This is best effort. One can likely find ways how to get COM without triggering these alarms.
            // AsAny marshalling of a struct with an object-typed field would be one, for example.
 
            // This logic roughly corresponds to MarshalInfo::MarshalInfo in CoreCLR,
            // not trying to handle invalid cases and distinctions that are not interesting wrt
            // "is this COM?" question.
 
            NativeType nativeType = NativeType.None;
            if (marshalInfoProvider.HasMarshalInfo)
            {
                nativeType = marshalInfoProvider.MarshalInfo.NativeType;
            }
 
            if (nativeType == NativeType.IUnknown || nativeType == NativeType.IDispatch || nativeType == NativeType.IntF)
            {
                // This is COM by definition
                return true;
            }
 
            if (nativeType == NativeType.None)
            {
                // Resolve will look at the element type
                var parameterTypeDef = context.TryResolve(parameterType);
 
                if (parameterTypeDef != null)
                {
                    if (parameterTypeDef.IsTypeOf(WellKnownType.System_Array))
                    {
                        // System.Array marshals as IUnknown by default
                        return true;
                    }
                    else if (parameterTypeDef.IsTypeOf(WellKnownType.System_String) ||
                        parameterTypeDef.IsTypeOf("System.Text", "StringBuilder"))
                    {
                        // String and StringBuilder are special cased by interop
                        return false;
                    }
 
                    if (parameterTypeDef.IsValueType)
                    {
                        // Value types don't marshal as COM
                        return false;
                    }
                    else if (parameterTypeDef.IsInterface)
                    {
                        // Interface types marshal as COM by default
                        return true;
                    }
                    else if (parameterTypeDef.IsMulticastDelegate())
                    {
                        // Delegates are special cased by interop
                        return false;
                    }
                    else if (parameterTypeDef.IsSubclassOf("System.Runtime.InteropServices", "CriticalHandle", context))
                    {
                        // Subclasses of CriticalHandle are special cased by interop
                        return false;
                    }
                    else if (parameterTypeDef.IsSubclassOf("System.Runtime.InteropServices", "SafeHandle", context))
                    {
                        // Subclasses of SafeHandle are special cased by interop
                        return false;
                    }
                    else if (!parameterTypeDef.IsSequentialLayout && !parameterTypeDef.IsExplicitLayout)
                    {
                        // Rest of classes that don't have layout marshal as COM
                        return true;
                    }
                }
            }
 
            return false;
        }
 
        internal static bool IsPInvokeDangerous(MethodDefinition methodDefinition, LinkContext context, out bool comDangerousMethod)
        {
            // The method in ILLink only detects one condition - COM Dangerous, but it's structured like this
            // so that the code looks very similar to AOT which has more than one condition.
 
            if (!methodDefinition.IsPInvokeImpl)
            {
                comDangerousMethod = false;
                return false;
            }
 
            comDangerousMethod = IsComInterop(methodDefinition.MethodReturnType, methodDefinition.ReturnType, context);
#pragma warning disable RS0030 // MethodDefinition.Parameters is banned. Here we iterate through the parameters and don't need to worry about the 'this' parameter.
            foreach (ParameterDefinition pd in methodDefinition.Parameters)
            {
                comDangerousMethod |= IsComInterop(pd, pd.ParameterType, context);
            }
#pragma warning restore RS0030
 
            return comDangerousMethod;
        }
    }
}