Skip to content

Commit c1d83c9

Browse files
authored
Warn when accessing DAM annotated method via reflection (#2145)
* Warn when accessing DAM annotated method via reflection Any access to DAM annotated method must be validated by the trimmer. Normal calls and such are handled already, but direct reflection access or other indirect accesses can lead to trimmer allowing potential invocation of DAM annotated method without validating the annotations. This change will warn whenever such potential "leak" happens. This applies to method parameter, method return value and field annotations. Uses the same infra as RUC already just adds additional checks. Tests for the new cases. * Formatting * Only warn on virtual methods with annotate return value Annotated return value is mostly not problematic (the caller only "Reads" from it, which is always safe). The only case where it's problematic is if something would override the method at runtime (ref emit) in which case the trimmer can't validate the new code that it fulfills the return value requirements. But that can only happen if the method is virtual. * PR feedback * PR feedback * Remove test code.
1 parent 0cb9250 commit c1d83c9

File tree

6 files changed

+710
-55
lines changed

6 files changed

+710
-55
lines changed

docs/error-codes.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1689,7 +1689,7 @@ The only scopes supported on global unconditional suppressions are 'module', 'ty
16891689
it is assumed that the suppression is put on the module. Global unconditional suppressions using invalid scopes are ignored.
16901690

16911691
```C#
1692-
// Invalid scope 'method' used in 'UnconditionalSuppressMessageAttribute' on module 'Warning' with target 'MyTarget'.
1692+
// IL2108: Invalid scope 'method' used in 'UnconditionalSuppressMessageAttribute' on module 'Warning' with target 'MyTarget'.
16931693
[module: UnconditionalSuppressMessage ("Test suppression with invalid scope", "IL2026", Scope = "method", Target = "MyTarget")]
16941694

16951695
class Warning
@@ -1722,6 +1722,37 @@ class Warning
17221722
class Derived : UnsafeClass {}
17231723
```
17241724

1725+
#### `IL2110`: Trim analysis: Field 'field' with 'DynamicallyAccessedMembersAttribute' is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field.
1726+
1727+
- Trimmer currently can't guarantee that all requirements of the `DynamicallyAccessedMembersAttribute` are fulfilled if the field is accessed via reflection.
1728+
1729+
```C#
1730+
[DynamicallyAccessedMembers(DynamicallyAccessedMemeberTypes.PublicMethods)]
1731+
Type _field;
1732+
1733+
void TestMethod()
1734+
{
1735+
// IL2110: Field '_field' with 'DynamicallyAccessedMembersAttribute' is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field.
1736+
typeof(Test).GetField("_field");
1737+
}
1738+
```
1739+
1740+
#### `IL2111`: Trim analysis: Method 'method' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field.
1741+
1742+
- Trimmer currently can't guarantee that all requirements of the `DynamicallyAccessedMembersAttribute` are fulfilled if the method is accessed via reflection.
1743+
1744+
```C#
1745+
void MethodWithRequirements([DynamicallyAccessedMembers(DynamicallyAccessedMemeberTypes.PublicMethods)] Type type)
1746+
{
1747+
}
1748+
1749+
void TestMethod()
1750+
{
1751+
// IL2111: Method 'MethodWithRequirements' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field.
1752+
typeof(Test).GetMethod("MethodWithRequirements");
1753+
}
1754+
```
1755+
17251756
## Single-File Warning Codes
17261757

17271758
#### `IL3000`: 'member' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'

src/linker/Linker.Dataflow/FlowAnnotations.cs

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,14 @@ public FlowAnnotations (LinkContext context)
2323
_hierarchyInfo = new TypeHierarchyCache (context);
2424
}
2525

26-
public bool RequiresDataFlowAnalysis (MethodDefinition method)
27-
{
28-
return GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out _);
29-
}
26+
public bool RequiresDataFlowAnalysis (MethodDefinition method) =>
27+
GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out _);
3028

31-
public bool RequiresDataFlowAnalysis (FieldDefinition field)
32-
{
33-
return GetAnnotations (field.DeclaringType).TryGetAnnotation (field, out _);
34-
}
29+
public bool RequiresDataFlowAnalysis (FieldDefinition field) =>
30+
GetAnnotations (field.DeclaringType).TryGetAnnotation (field, out _);
3531

36-
public bool RequiresDataFlowAnalysis (GenericParameter genericParameter)
37-
{
38-
return GetGenericParameterAnnotation (genericParameter) != DynamicallyAccessedMemberTypes.None;
39-
}
32+
public bool RequiresDataFlowAnalysis (GenericParameter genericParameter) =>
33+
GetGenericParameterAnnotation (genericParameter) != DynamicallyAccessedMemberTypes.None;
4034

4135
/// <summary>
4236
/// Retrieves the annotations for the given parameter.
@@ -45,35 +39,31 @@ public bool RequiresDataFlowAnalysis (GenericParameter genericParameter)
4539
/// <returns></returns>
4640
public DynamicallyAccessedMemberTypes GetParameterAnnotation (MethodDefinition method, int parameterIndex)
4741
{
48-
if (GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out var annotation) && annotation.ParameterAnnotations != null) {
42+
if (GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out var annotation) &&
43+
annotation.ParameterAnnotations != null)
4944
return annotation.ParameterAnnotations[parameterIndex];
50-
}
5145

5246
return DynamicallyAccessedMemberTypes.None;
5347
}
5448

5549
public DynamicallyAccessedMemberTypes GetReturnParameterAnnotation (MethodDefinition method)
5650
{
57-
if (GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out var annotation)) {
51+
if (GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out var annotation))
5852
return annotation.ReturnParameterAnnotation;
59-
}
6053

6154
return DynamicallyAccessedMemberTypes.None;
6255
}
6356

6457
public DynamicallyAccessedMemberTypes GetFieldAnnotation (FieldDefinition field)
6558
{
66-
if (GetAnnotations (field.DeclaringType).TryGetAnnotation (field, out var annotation)) {
59+
if (GetAnnotations (field.DeclaringType).TryGetAnnotation (field, out var annotation))
6760
return annotation.Annotation;
68-
}
6961

7062
return DynamicallyAccessedMemberTypes.None;
7163
}
7264

73-
public DynamicallyAccessedMemberTypes GetTypeAnnotation (TypeDefinition type)
74-
{
75-
return GetAnnotations (type).TypeAnnotation;
76-
}
65+
public DynamicallyAccessedMemberTypes GetTypeAnnotation (TypeDefinition type) =>
66+
GetAnnotations (type).TypeAnnotation;
7767

7868
public DynamicallyAccessedMemberTypes GetGenericParameterAnnotation (GenericParameter genericParameter)
7969
{
@@ -93,6 +83,17 @@ public DynamicallyAccessedMemberTypes GetGenericParameterAnnotation (GenericPara
9383
return DynamicallyAccessedMemberTypes.None;
9484
}
9585

86+
public bool ShouldWarnWhenAccessedForReflection (MethodDefinition method) =>
87+
GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out var annotation) &&
88+
(annotation.ParameterAnnotations != null || annotation.ReturnParameterAnnotation != DynamicallyAccessedMemberTypes.None);
89+
90+
public bool MethodHasNoAnnotatedParameters (MethodDefinition method) =>
91+
GetAnnotations (method.DeclaringType).TryGetAnnotation (method, out var annotation) &&
92+
annotation.ParameterAnnotations == null;
93+
94+
public bool ShouldWarnWhenAccessedForReflection (FieldDefinition field) =>
95+
GetAnnotations (field.DeclaringType).TryGetAnnotation (field, out _);
96+
9697
TypeAnnotations GetAnnotations (TypeDefinition type)
9798
{
9899
if (!_annotations.TryGetValue (type, out TypeAnnotations value)) {

src/linker/Linker.Steps/MarkStep.cs

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,23 @@ void MarkField (FieldDefinition field, in DependencyInfo reason)
15531553
Annotations.Mark (field, reason);
15541554
}
15551555

1556+
switch (reason.Kind) {
1557+
case DependencyKind.AccessedViaReflection:
1558+
case DependencyKind.DynamicDependency:
1559+
case DependencyKind.DynamicallyAccessedMember:
1560+
case DependencyKind.InteropMethodDependency:
1561+
if (_context.Annotations.FlowAnnotations.ShouldWarnWhenAccessedForReflection (field) &&
1562+
!ShouldSuppressAnalysisWarningsForRequiresUnreferencedCode ())
1563+
_context.LogWarning (
1564+
$"Field '{field.GetDisplayName ()}' with 'DynamicallyAccessedMembersAttribute' is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field.",
1565+
2110,
1566+
_scopeStack.CurrentScope.Origin,
1567+
MessageSubCategory.TrimAnalysis);
1568+
1569+
break;
1570+
}
1571+
1572+
15561573
if (CheckProcessed (field))
15571574
return;
15581575

@@ -2705,12 +2722,12 @@ protected virtual MethodDefinition MarkMethod (MethodReference reference, Depend
27052722

27062723
// Use the original reason as it's important to correctly generate warnings
27072724
// the updated reason is only useful for better tracking of dependencies.
2708-
ProcessRequiresUnreferencedCode (method, originalReasonKind);
2725+
ProcessAnalysisAnnotationsForMethod (method, originalReasonKind);
27092726

27102727
return method;
27112728
}
27122729

2713-
void ProcessRequiresUnreferencedCode (MethodDefinition method, DependencyKind dependencyKind)
2730+
void ProcessAnalysisAnnotationsForMethod (MethodDefinition method, DependencyKind dependencyKind)
27142731
{
27152732
switch (dependencyKind) {
27162733
// DirectCall, VirtualCall and NewObj are handled by ReflectionMethodBodyScanner
@@ -2767,24 +2784,85 @@ void ProcessRequiresUnreferencedCode (MethodDefinition method, DependencyKind de
27672784
return;
27682785

27692786
default:
2770-
// DirectCall, VirtualCall and NewObj are handled by ReflectionMethodBodyScanner
2771-
// This is necessary since the ReflectionMethodBodyScanner has intrinsic handling for some
2772-
// of the annotated methods annotated (for example Type.GetType)
2773-
// and it knows when it's OK and when it needs a warning. In this place we don't know
2774-
// and would have to warn every time
2775-
27762787
// All other cases have the potential of us missing a warning if we don't report it
27772788
// It is possible that in some cases we may report the same warning twice, but that's better than not reporting it.
27782789
break;
27792790
}
27802791

2781-
// All override methods should have the same annotations as their base methods (else we will produce warning IL2046.)
2782-
// When marking override methods with RequiresUnreferencedCode on a type annotated with DynamicallyAccessedMembers,
2783-
// we should only issue a warning for the base method.
2784-
if (dependencyKind != DependencyKind.DynamicallyAccessedMember ||
2785-
!method.IsVirtual ||
2786-
Annotations.GetBaseMethods (method) == null)
2787-
CheckAndReportRequiresUnreferencedCode (method);
2792+
// All override methods should have the same annotations as their base methods
2793+
// (else we will produce warning IL2046 or IL2092 or some other warning).
2794+
// When marking override methods via DynamicallyAccessedMembers, we should only issue a warning for the base method.
2795+
if (dependencyKind == DependencyKind.DynamicallyAccessedMember &&
2796+
method.IsVirtual &&
2797+
Annotations.GetBaseMethods (method) != null)
2798+
return;
2799+
2800+
CheckAndReportRequiresUnreferencedCode (method);
2801+
2802+
if (_context.Annotations.FlowAnnotations.ShouldWarnWhenAccessedForReflection (method)) {
2803+
// If the current scope has analysis warnings suppressed, don't generate any
2804+
if (ShouldSuppressAnalysisWarningsForRequiresUnreferencedCode ())
2805+
return;
2806+
2807+
// ReflectionMethodBodyScanner handles more cases for data flow annotations
2808+
// so don't warn for those.
2809+
switch (dependencyKind) {
2810+
case DependencyKind.AttributeConstructor:
2811+
case DependencyKind.AttributeProperty:
2812+
return;
2813+
2814+
default:
2815+
break;
2816+
}
2817+
2818+
// If the method only has annotation on the return value and it's not virtual avoid warning.
2819+
// Return value annotations are "consumed" by the caller of a method, and as such there is nothing
2820+
// wrong calling these dynamically. The only problem can happen if something overrides a virtual
2821+
// method with annotated return value at runtime - in this case the trimmer can't validate
2822+
// that the method will return only types which fulfill the annotation's requirements.
2823+
// For example:
2824+
// class BaseWithAnnotation
2825+
// {
2826+
// [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)]
2827+
// public abstract Type GetTypeWithFields();
2828+
// }
2829+
//
2830+
// class UsingTheBase
2831+
// {
2832+
// public void PrintFields(Base base)
2833+
// {
2834+
// // No warning here - GetTypeWithFields is correctly annotated to allow GetFields on the return value.
2835+
// Console.WriteLine(string.Join(" ", base.GetTypeWithFields().GetFields().Select(f => f.Name)));
2836+
// }
2837+
// }
2838+
//
2839+
// If at runtime (through ref emit) something generates code like this:
2840+
// class DerivedAtRuntimeFromBase
2841+
// {
2842+
// // No point in adding annotation on the return value - nothing will look at it anyway
2843+
// // Linker will not see this code, so there are no checks
2844+
// public override Type GetTypeWithFields() { return typeof(TestType); }
2845+
// }
2846+
//
2847+
// If TestType from above is trimmed, it may note have all its fields, and there would be no warnings generated.
2848+
// But there has to be code like this somewhere in the app, in order to generate the override:
2849+
// class RuntimeTypeGenerator
2850+
// {
2851+
// public MethodInfo GetBaseMethod()
2852+
// {
2853+
// // This must warn - that the GetTypeWithFields has annotation on the return value
2854+
// return typeof(BaseWithAnnotation).GetMethod("GetTypeWithFields");
2855+
// }
2856+
// }
2857+
if (!method.IsVirtual && _context.Annotations.FlowAnnotations.MethodHasNoAnnotatedParameters (method))
2858+
return;
2859+
2860+
_context.LogWarning (
2861+
$"Method '{method.GetDisplayName ()}' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field.",
2862+
2111,
2863+
_scopeStack.CurrentScope.Origin,
2864+
MessageSubCategory.TrimAnalysis);
2865+
}
27882866
}
27892867

27902868
internal bool ShouldSuppressAnalysisWarningsForRequiresUnreferencedCode ()

0 commit comments

Comments
 (0)