Why Custom Attributes in .NET give me Nightmares | Washi
Post
Cancel
` and `<br>-->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
` is wrapped by `` -->
Why Custom Attributes in .NET give me Nightmares
Contents
Why Custom Attributes in .NET give me Nightmares
Some people may think I am a shill for .NET.
With my previous post, they may be right.
However, as much as I like .NET, there are some things that just really do not make any sense to me, and they frustrate me to no end.
Given that I maintain a PE parsing library and thus am deeply familiar with the anatomy of .NET binaries, I feel I am qualified to complain about some of the design choices Microsoft made for this file format :).
In this post, I will rant about Custom Attributes and why their underlying storage mechanism is among the poorest design choices Microsoft has ever made in .NET.<br>It has caused me so much grief over the past few years, it has become a meme in the AsmResolver core maintainers group.<br>I am truly convinced that custom attributes are the source of all evil.
I literally have nightmares about custom attributes.
What are Custom Attributes?
For the unfamiliar, custom attributes are extra pieces of metadata you can attach to classes, methods, fields, parameters, etc.<br>They are typically used to instruct the C# compiler to do something extra.
A classic example is the ObsoleteAttribute, which lets the compiler produce a warning if an object marked with this attribute is used in some user code:
[Obsolete] // Custom attribute that marks MyClass obsolete.<br>public class MyClass { /* ... */ }
var x = new MyClass(); //
You can define your own custom attributes, and they can also define parameters:
public class MyAttribute(int x, string y) : Attribute { /* ... */ }
[MyAttribute(0x1337, "Hello, world!")]<br>public class MyClass { /* ... */ }
Custom attributes are a great way to extend the normal metadata that exists around a function, variable, or type.<br>It is used mainly by analyzers, source generators, or dynamic initialization/inspection and is great for meta-programming and use cases like automatic serialization and deserialization of objects.
Anatomy of a Custom Attribute
In the .NET file format, everything is stored in a database of metadata tables.<br>Types, fields, methods, parameters, etc. will all reside in their own table.<br>This allows for each object to be referenced and looked up efficiently by a metadata token, i.e., a table + row index.
You can view the raw contents of these metadata tables in a .NET binary with a tool like CFF Explorer:
Metadata tables in a .NET binary.
Custom attributes are no exception to this.<br>Each row in the CustomAttribute table represents one instantiation of an attribute.<br>It contains a reference to the member the attribute is attached to, a reference to the attribute’s constructor, and an index into the blob stream referencing an array of arguments to call this constructor with.
A single custom attribute row
The blob signature is the interesting part for this post.
The Custom Attribute Signature
All arguments in a custom attribute signature are serialized to their binary representation and concatenated in sequence.<br>It is important to note that this binary representation is fully implied by the parameter types of the attribute’s constructor.<br>For example, if the first argument’s type is int, the first four bytes encode an integer.<br>If the second argument is a string, you follow it up by reading a length-prefixed array of characters.<br>You keep going until you read all the arguments, and if you did everything right, you would be at the end of the blob signature.
00000000 37 13 00 00 44 48 65 6c 6c 6f 2c 20 77 6f 72 6c |....DHello, worl|<br>00000010 64 21 |d!|
This is intuitive; there are no problems here.<br>Things go downhill very fast after this, though.
Custom Attributes with Enum Values
The vast majority of attributes do not actually take in primitive arguments like int or string, but often include parameters defined as an enum type.<br>The specification stipulates that enum values are serialized in the same way as their underlying type.
ECMA specification on enum values.
Consider the following example:
10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>public enum MyIntEnum // `int` is the default in C#<br>Value1 = 1,<br>Value2 = 2,<br>Value3 = 3,<br>// ...
public enum MyShortEnum : short // explicit underlying type `short`<br>Value1 = 1,<br>Value2 = 2,<br>Value3 = 3,<br>// ...
public class FooAttribute(MyIntEnum a, MyShortEnum b) : Attribute { /* ... */ }
[Foo(MyIntEnum.Value2, MyShortEnum.Value3)]<br>public class SomeClass;
Because MyIntEnum...