IgnoresAccessChecksTo - KirillOsenkov/Bliki GitHub Wiki
There is a relatively little known partner attribute to InternalsVisibleToAttribute
called IgnoreAccessChecksToAttribute
. While IVT allows the library author to grant internal access to a specified set of consumers, IACT allows a consumer to bypass accessibility checks on internal APIs without needing an IVT entry. This attribute is already fully supported by the runtime, however there is no language support for this.
Here's a good overview: https://www.strathweb.com/2018/10/no-internalvisibleto-no-problem-bypassing-c-visibility-rules-with-roslyn/
Sample usage:
[assembly: IgnoresAccessChecksTo("Calculator")]
Historical background
There was a request in 2017 from the community, and internal teams, to formally support this in the C# compiler itself. This came in the form of a PR and LDM meeting:
- https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-11-06.md#roslyn-20870
- https://github.com/dotnet/roslyn/pull/20870
Community has already written some tooling:
- https://github.com/aelij/IgnoresAccessChecksToGenerator
- https://github.com/0xd4d/dnSpy/tree/master/Build/MakeEverythingPublic
The LDM decision was to not support this in the language. Recently this came up again via a blog post and twitter discussion:
- https://twitter.com/KirillOsenkov/status/1049723201008201729
- https://www.strathweb.com/2018/10/no-internalvisibleto-no-problem-bypassing-c-visibility-rules-with-roslyn/
- https://twitter.com/praeclarum/status/1309871513412149248
Goal:
Reduce the friction and pain for consumers of libraries and make them more efficient when using C# and our ecosystem, while ensuring that there are guardrails to prevent this feature from causing harm. It needs to be auditable.
Customer painpoints:
https://twitter.com/PammerSiegfried/status/1310119637351313408
internal and sealed are the reason why ILSpy cannot use Roslyn. All the "meat" is internal... Would love to use the Binder, but nope - have to roll our own... Well... at least we can read the source code.
Most anecdotal evidence is around the internal APIs of Roslyn itself.
Arguments for IACT:
- The CLR supports it and C# traditionally wants to be true to the runtime. Case in point: static interface members were allowed because the CLR supports it. Same as private protected etc.
- There needs to be a mechanism of declaring "currently usable but unstable API surface", to completent the public "stable API surface". We want to unlock the value to consumers before stabilizing the API and give them a preview to solicit feedback before the API stabilizes. Public API is not good for previews and feedback as it by definition stabilizes before it can be consumed.
- InternalsVisibleTo already exists. All arguments against IACT can also be applied against IVT itself. Still, IVT exists and is used liberally. There's no evidence that discouraging internal API consumption is effective.
- Huge amount of pain and friction in the ecosystem due to useful APIs being locked. Most often there is no other way and people resort to workarounds (see below).
- IVT is unfair - it prefers blessed consumers, acknowledging that the public API surface is inadequate, but only exposing the valuable internals to a select few. Everyone else has to resort to workarounds. Example: Roslyn editor layer IVT into Visual Studio. Third-party hosts of Roslyn are not privileged.
- The duality is broken. IVT and IACT are dual to each other and the CLR supports both. However C# only exposes one of them.
- IACT is a waiver, akin to consumers signing a contract: I acknowledge that I'm consuming internal APIs and willing to be broken and deal with the consequences. I prefer that to using workarounds such as Reflection etc. and then still being broken anyway.
Arguments against IACT:
-
With IACT available, library authors are not incentivized to design and expose adequate public API.
-
Implication that contacting the API owner has a positive effect. The evidence seems to be to the contrary. For example: Roslyn. We get loads of requests to make APIs public and we act on very, very few of them. Pretty much only when we have no other choice. This is equally true for internal and external customers. The only difference is that if internal customers escalate enough we eventually add them to the sacred IVT list.
-
It should be difficult to consume internal APIs. Consumers should be incentivized to work with library authors to only consume public APIs.
- Counterpoint: there's no evidence it works, as IVT was supposed to be used judiciously.
-
We don't want to investigate breakages resulting from changing internal APIs and breaking consumers we didn't know about
- Counterpoint: we already do, we are already breaking consumers who use workarounds such as Reflection etc.
-
Should anyone be able to see anyone else's internal APIs without their permission?
- Counterpoint: it's already the case, see workarounds.
-
Just because users are doing bad things today doesn't mean we should make it the bad thing a first class feature with zero guard rails around it. Take that argument one step further and we should just let users avoid type checks in C#, after all u can do that in unsafe
-
The customers at the other end have essentially the following concerns:
- How can I disable this in my code base?
- How can I understand my current debt?
- How can I assign engineer X to make yes / no decisions here?
- How can I understand the debt / risk of libraries I depend on?
-
I also worry about team projects; it’s one thing if the sole developer on a project opts in and says “I know what I’m doing”, but what if one developer opts-in, and then every other developer inherits that.
-
Would also need this for F#, VB
-
Ref assemblies don't contain internals
-
New compat burden: as a popular library author uses IACT to see Microsoft internal APIs. Microsoft breaks things, library is broken, users of the library are broken. We Microsoft don’t like doing that so we find ourselves with a new compat burden.
Design considerations
If the idea is user says "grant IACT to type X" and compiler magically allows access to all members then really you're granting IACT X, Y, Z, etc ... every type reachable in the members of X or it's base types. At that point why not just allow access to everything?
What happens if you allow access checks to a sub-type but not the base type? Or a method but not all the param types? Basically inconsistent accessibility which isn't incorrectly constructed metadata.
Explicit opt-in be transitive. That is it is good that you have to throw some switch like /usingInternals on the command line, but I want that to be true not only if you directly use internals, but if you use an assembly that did this.
- Thus assembly A has an IgnoresAccessChecksToAttribute in it. Thus when it is compiled it needs to use /usingInternals command line switch
- If assembly B uses assembly A, because assembly A used IgnoresAccessChecksToAttribute, assembly B also has to ‘mark’ that it understand that it could be broken by future updates. My recommendation is that we require assembly B to also have a IgnoresAccessChecksToAttribute attribute with an empty target.
This ensures that the complete transitive closure of A has ‘opted in’ and understands that they could be broken. I think this transitive nature is important part of insuring that ‘internal’ does not degenerate to ‘public’.
I expect that they would be more likely on “this package requires this exact version of the other package” plan. These kind of hard blocks tend to produce ecosystem of workarounds that makes everybody’s life more difficult at the end. In this case, folks would use reference assemblies to circumvent the transitive closure check; or they would hide the offending code in assembly that they load as stream in memory. I think these blocks should be warnings that can be suppressed.
Implementation:
- requires a new command line option to load reference assemblies with internals
- difference between what the user is allowed to access in their program (designed feature) and what the compiler imports (implementation detail)
Workarounds:
- adding IVT to libraries source
- requires authors permission and turnaround time to ship and consume the change
- forking the libraries and adding IVT in the fork
- various issues with not shipping the original library (signing, assembly conflicts, keeping the fork up-to-date, security servicing etc)
- using reflection
- doesn't work to implement inaccessible interfaces, for example
- no compile time verification
- very cumbersome, slow and error prone
- custom build of the Roslyn compiler that supports IACT (see https://github.com/aelij/IgnoresAccessChecksToGenerator)
- difficulty with shipping a custom toolset, keeping up with upstream, etc.
- IL rewriting of reference assemblies, "make everything public" (see https://github.com/0xd4d/dnSpy/tree/master/Build/MakeEverythingPublic)
- builds are slower and more fragile, IL rewriting can go wrong
All the workaround above are heavily used already despite the associated disadvantages and friction.
Guardrails:
- A tool that generates a report of all internal APIs used
- A message or a warning about every internal API usage without IVT
- I also think some of the "knobs" actually turn into rather nice "promos" from teams. The ability to audit is little different than declaring the API surface area u depend on. Imagine if http://ASP.NET sent out a "internal experiment API" decl file on release?
=====
This attribute does not enable much extra that you cannot do with private reflection already. Private reflection is an ugly compatibility burden, but it is not a complete disaster because of it has a barrier to use and it is documented as dangerous. This attribute should try to be on par with private reflection: There should be barrier to use it and it should be documented as dangerous.
Right now, the barrier to use is “add reference to 3rd party NuGet package”, the attribute is not in the public surface (you have to define it locally) and there is no official documentation. BTW: The lack of official documentation is actually related to the fact that it is not in public surface. We have the documentation system attached to the public surface, and so there is a straightforward way to create documentation for this. We would need to be creative and thus it did not get done.
This proposal is basically about lowering barrier to use, but not make it too close to zero. Allowing this only under a special switch (similar to /unsafe) sounds good to me. Maybe the switch should not have UI, so that you actually need to edit .csproj file to get to this.
Current proposal:
- The attribute names stays the same (“IgnoreAccessChecksToAttribute”)
- Because Jan is right that “scary” names tend not to stick, and
- It is not worth it to remap an attribute name for this
- But the attribute does not come predeclared in the Fx or by the compiler, the user has to know to declare it
- Because there needs to be some impedance, it can’t just feel like “business as usual”
- The tooling around this should err on the side of
- Pri 1: Easiest to implement – we don’t want to do a whole lot of work for people using this feature
- Pri 2: Hardest to discover – all else equal we would refrain from showing these members in completion etc.
- The presence of this feature should not affect other aspects of language design or implementation
- For instance it’s fine if the semantics of a feature relies on the assumption that internals are only accessed from the same compilation
- The risks here should be borne by the IACT user, not everyone else