No InternalsVisibleTo, no problem – bypassing C# visibility rules with Roslyn

Β· 1665 words Β· 8 minutes to read

Both the C# compiler and the CLR/CoreCLR runtimes contain a bunch of rules that are in place to save us from ourselves (and to allow us to write code without needing to fully understand ECMA-334 C# Language Specification). That said, there are times where we want to do some things that are normally not allowed, and a good example of that is reaching into reflection to execute some private or internal code.

Today I wanted to show you how to do something quite cool - how to bypass the type/member visibility rules using the Roslyn compiler. In other words, how to get access to internal and private members without needing to use reflection or something like InternalsVisibleToAttribute.

Sample code πŸ”—

To best illustrate today’s discussion and exercise, let’s imagine the following basic code - structured into two assemblies. One will act as an “external library” and the other as an executable that consumes that library.

We’ll call the “external library” a Calculator and add a little code into it, so that we have some stuff to invoke:

namespace Calculator
{
    class Square
    {
        public Square(double side)
        {
            Side = side;
        }

        public double Side { get; }
    }

    class AreaCalculator
    {
        private double Calculate(Square square) => square.Side * square.Side;
    }
}

The consumer of this library will add it as a regular reference and will be called CrazyProgram. It will make use of the Calculator code.

using System;
using Calculator;

namespace CrazyProgram
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var square = new Square(4); // internal type
            var calculator = new AreaCalculator(); // internal type
            var area = calculator.Calculate(square); // private method
            Console.WriteLine($"Square with a side of 4 has an area of {area}");
        }
    }
}

Obviously the above code makes no sense - it will not compile, and it will not run because we are accessing internal classes and their private members, from a different assembly.

You should see compiler errors like these at this points:

Program.cs(12,30,12,36): error CS0122: 'Square' is inaccessible due to its protection level
Program.cs(13,34,13,48): error CS0122: 'AreaCalculator' is inaccessible due to its protection level
Done building project "CrazyProgram.csproj" -- FAILED.

IgnoresAccessChecksTo - a secret attribute πŸ”—

I think pretty much everyone working with .NET is, at least to some degree, familiar with InternalsVisibleToAttribute, and how it can be used to open up the internals of an assembly for another assembly. It is commonly used especially in test projects. It can be extremely useful, but the reality is, in most cases where it would actually be really useful - for third party libraries - it cannot be used. The reason, which I am sure you all well know, is that the attribute would need to be added to the source code of the third party library itself, as it needs to explicitly specify which assemblies get access to the internals. In other words, you have to ideally control all the sides, and you have to do that upfront.

In our case, if we wanted to use InternalsVisibleToAttribute, we’d need to add it to Calculator and explicitly mention CrazyProgram as the one having access to the internals.

Of course this is not a blog post about InternalsVisibleToAttribute because it is known, and boring, and I promised cool things instead.

So bear with me. Turns out, there is a secret (well, FWIW very little documented and very little known) attribute that you can use in your code that acts as reverse of InternalsVisibleToAttribute - and it’s called IgnoresAccessChecksToAttribute. It allows you to explicitly suppress member and type visibility checks against a specific assembly. IgnoresAccessChecksToAttribute is not defined in the Base Class Library, but - and here is even more magic - you can actually simply declare it in your own codebase, use it, and the runtime (both CLR and CoreCLR) will recognize it and respect it.

In our case, we’d need to add it to CrazyProgram (the “consumer” side), and explicitly mention that we want to suppress the visibility checks to Calculator. This works tremendously well, because we only need to control one side of the equation, the other (the “library”) can be anything we want to raid (for the lack of better word) for privates/internals - JSON.NET, ASP.NET Core MVC, Entity Framework Core, anything we wish.

Sounds perfect, no? Our updated CrazyProgram code now looks like this:

using System;
using System.Runtime.CompilerServices;
using Calculator;

[assembly: IgnoresAccessChecksTo("Calculator")]
namespace CrazyProgram
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var square = new Square(4);
            var calculator = new AreaCalculator();
            var area = calculator.Calculate(square);
            Console.WriteLine($"Square with a side of 4 has an area of {area}");
        }
    }
}

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
    public class IgnoresAccessChecksToAttribute : Attribute
    {
        public IgnoresAccessChecksToAttribute(string assemblyName)
        {
            AssemblyName = assemblyName;
        }

        public string AssemblyName { get; }
    }
}

There are no changes need to the Calculator. It just sits there quietly, and doesn’t suspect that someone is just about to start using its internals very soon.

So we have handled the runtime part (or so you have to believe me for now) - and thanks to IgnoresAccessChecksToAttribute we managed to get the runtime visibility checks under control. If we didn’t use it, we’d actually run into MethodAccessException at runtime. However, we still need to tame the compiler - because it surely will not allow us to compile the code with all those private and internal member accesses.

Compiling C# code without visibility checks πŸ”—

In order to do that, we will use Roslyn’s compiler-as-a-service capabilities and compile the CrazyProgram ourselves (instead of relying on csc.exe and/or MsBuild). This is fairly simple though and there are plenty of online resources about that. It also allows us to closer look at what changes are needed in the compilation process to make this work.

What I wanted to highlight, are two special compilation settings (so set on CSharpCompilationOptions) that we need to tweak, to make code compile in a way that visibility checks will not be active and therefore the compilation will indeed succeed.

First is the setting called MetadataImportOptions, which is actually a new thing available in Roslyn since 2.9.0 (it was private before that). We can set it to MetadataImportOptions.All to ensure all symbols are imported from the referenced assemblies (instead of only public ones).

This is shown next, along with a base set of MetadataReferences that we’ll use to compile the code. With regards to setting up the assembly references, we could do something smarter here, like use MsBuildWorkspace to compile the project, but what we have is good enough for a simple demo, plus it works well on .NET Core 2.1. The only difference is we have to manually resolve all references instead of relying on a csproj file.

var metadataReferences = new[]
{
    typeof(object).GetTypeInfo().Assembly, // System.Private.CoreLib.dll
    typeof(Enumerable).GetTypeInfo().Assembly, // System.Linq.dll
    typeof(Console).GetTypeInfo().Assembly, // System.Console.dll
    Assembly.Load(new AssemblyName("System.Runtime")), // System.Runtime.dll
    Assembly.Load(new AssemblyName("Calculator")) // Calculator.dll
}.Select(x => MetadataReference.CreateFromFile(x.Location)).ToList();

var compilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication).
    WithMetadataImportOptions(MetadataImportOptions.All);

Once we have the compilation options equipped with MetadataImportOptions.All, we can proceed to the second step, which is to set the BinderFlags.IgnoreAccessibility on the options too. Ironically (in light of what we are trying to do in this blog post), this part of the compiler is not public, meaning we’ll have to use (again, ironically) reflection. Thankfully it’s not very complicated - we need to get a hold of TopLevelBinderFlags property on CSharpCompilationOptions and set it to a secret uint value (trust me) that resolves to BinderFlags.IgnoreAccessibility. This is shown in the next snippet.

var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
topLevelBinderFlagsProperty.SetValue(compilationOptions, (uint)1 << 22);

Binder controls how the compiler selects members from candidate lists and binder flags allow us to fine tune its behavior. Hopefully the pieces are now slowly falling in place together, because BinderFlags.IgnoreAccessibility definitely sounds very promising, as in, maybe this thing can work after all.

To complete our exercise, we need to add the rest of the compilation code. So far we only dealt with compilation options, so it’s natural to progress to creating a CSharpCompilation. And once we have that, we can emit the IL, load that into an assembly and execute.

Therefore, the rest of the code is shown next (notice how we consume our list of metadata references when creating a CSharpCompilation).

// the path is hardcoded for simplicity
var code = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "CrazyProgram", "Program.cs"));
var compilation = CSharpCompilation.Create("DynamicCrazyProgram", 
    new[] { CSharpSyntaxTree.ParseText(code) }, metadataReferences, 
    compilationOptions);

using (var ms = new MemoryStream())
{
    var cr = compilation.Emit(ms);
    ms.Seek(0, SeekOrigin.Begin);
    var assembly = Assembly.Load(ms.ToArray());
    assembly.EntryPoint.Invoke(null, new object[] { new string[0] });
}

Console.ReadKey();

Instead of writing an assembly to disk, we emit it in memory, but that doesn’t affect anything, except it actually makes the whole thing faster. Once emitted, we can invoke the assembly’s entry point, to execute our CrazyProgram. And lo and behold, it should work.

If you have followed along, and I hope you did, you should now see your prize:

Square with a side of 4 has an area of 16

Which is the expected output of our original program. Pretty cool, isn’t it? Please remember that we only disabled visibility checks in the compiler - it’s not that the compiler is suddenly dumb. In fact, all the other compiler rules still apply, which is even crazier, if you think about it. It means we now have type safety against the privates/internals of an assembly that is not aware about that. This means that the compiler will now help us working against all these privates and internals and actually prevent the compilation from succeeding if i.e. we have type mismatch on a method signature and so on.

Now, don’t blame me if things go wrong, and use at your own risk, but it doesn’t take a lot of imagination to come up with some interesting use cases for this. Let me repeat again - it’s a fully functional C# program, running on CoreCLR, with visibility rules completely suppressed.

All the code from this article is, as usually, on Github.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP