Skip to content

Ensure that internal code is never exposed through InternalsVisibleTo - #170

Merged
dennisdoomen merged 2 commits into
mainfrom
harden-internals-with-embedded-attribute
Aug 10, 2026
Merged

Ensure that internal code is never exposed through InternalsVisibleTo#170
dennisdoomen merged 2 commits into
mainfrom
harden-internals-with-embedded-attribute

Conversation

@dennisdoomen

@dennisdoomen dennisdoomen commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Ports dennisdoomen/pathy#54 to Reflectify.

The problem

Reflectify is a source-only package, so its types are compiled straight into the consuming assembly as plain internal types. Anything the consumer lists in InternalsVisibleTo can therefore see and use Reflectify.MemberKind, Reflectify.Reflector, and all the extension classes. Reflectify silently becomes part of the consumer's internal API surface, and consumers can start depending on it across assembly boundaries without ever asking for it.

I proved this against main with a throwaway two-project solution: a ConsumerLib that compiles Reflectify's sources and grants InternalsVisibleTo("Friend"), plus a Friend project that references it. On main, Friend compiles fine while referencing Reflectify.MemberKind.

The fix

Every top-level type now gets a conditional declaration:

#if REFLECTIFY_COMPILE
public static class TypeExtensions
#else
[global::Microsoft.CodeAnalysis.Embedded]
[global::System.Diagnostics.DebuggerNonUserCode]
internal static class TypeExtensions
#endif

Roslyn hides types marked [Microsoft.CodeAnalysis.Embedded] from every other assembly, friend assemblies included. EmbeddedAttribute.cs declares the attribute itself and is picked up by the package automatically, since the .nuspec globs *.cs.

REFLECTIFY_COMPILE is only defined in Reflectify.csproj, so inside our own build the types are public instead. That lets Reflectify.Specs keep its plain ProjectReference and let me remove the InternalsVisibleTo entry entirely — which is the whole point of the change. Making the types public is safe because the Reflectify assembly is never shipped; the .nuspec publishes only contentFiles and Reflectify.props, no lib/.

Coexisting with Pathy

Pathy is a source-only package too, and it declares the same Microsoft.CodeAnalysis.EmbeddedAttribute. A consumer using both — FluentAssertions already consumes Reflectify, so this is realistic — compiles two copies into one assembly and gets CS0101 plus two CS0579.

The attribute is declared partial, so the two declarations merge into a single type rather than colliding. Roslyn matches the attribute by name and doesn't care how many parts it came from. Nothing is required of the consumer, and Pathy only has to add partial to its own copy for the pair to work together.

The one constraint is that a type-level attribute cannot be repeated across parts, so [AttributeUsage] and [ExcludeFromCodeCoverage] had to go. Dropping [AttributeUsage] is free: AttributeTargets.All is already the default. Three analyzers (RCS1203, MA0010, CA1018) ask for it back, so they're suppressed on that declaration with the reason inline.

Verification

Built a harness that compiles Reflectify's sources the way a consumer does, alongside a second file mimicking Pathy's attribute, plus a friend assembly:

main this branch
Consumer compiles yes yes
Consumer compiles with Pathy's attribute present yes yes
Friend sees the consumer's own internals yes yes
Friend sees Reflectify.MemberKind yes no (CS0234)

dotnet test passes: 187 on net6.0, 187 on net8.0, 182 on net472. (The netcoreapp3.0 run aborts on my machine because that runtime isn't installed — unrelated to this change.)

Two other things worth knowing

[DebuggerNonUserCode] is not valid on an enum. MemberKind gets [Embedded] only. This one is easy to miss: the error (CS0592) never shows up in Reflectify's own build, where the enum is public and neither attribute is applied. It only appears when you compile the sources the way a consumer does.

CA1852. Sealing the nested OrderedPropertyCollection inside Reflector. The analyzer only fires once Reflector itself becomes externally visible.

<NoWarn>1591;1573</NoWarn> in Reflectify.csproj, since the now-public types would otherwise want XML doc comments. Same as Pathy does.

Reflectify is a source-only package, so its types are compiled directly
into the consuming assembly. Until now those types were plain internal
types, which means any assembly listed in the consumer's
InternalsVisibleTo could see and use them. That makes Reflectify part of
the consumer's internal API surface without the consumer asking for it.

Mark every type with [Microsoft.CodeAnalysis.Embedded] when compiled into
a consumer. Roslyn hides embedded types from all other assemblies,
including friend assemblies. Inside Reflectify's own build the types stay
public instead, which lets the spec project reference them normally and
allowed the InternalsVisibleTo entry to be dropped.

This ports dennisdoomen/pathy#54.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@dennisdoomen dennisdoomen added the enhancement New feature or request label Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Test Results

  4 files  ±0    4 suites  ±0   4m 3s ⏱️ +39s
187 tests ±0  187 ✅ ±0  0 💤 ±0  0 ❌ ±0 
743 runs  ±0  743 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 8e46954. ± Comparison against base commit d63731a.

♻️ This comment has been updated with latest results.

Pathy is also a source-only package and declares the same
Microsoft.CodeAnalysis.EmbeddedAttribute. A consumer using both compiles
two copies into one assembly, which is a duplicate type error.

Declaring the attribute as partial makes the two declarations merge into
a single type. This needs no cooperation from the consumer, so the
REFLECTIFY_EXCLUDE_EMBEDDED_ATTRIBUTE opt-out is no longer needed.

Partial types cannot repeat a type-level attribute across parts, so
[AttributeUsage] and [ExcludeFromCodeCoverage] had to go. Dropping
[AttributeUsage] changes nothing, because AttributeTargets.All is already
the default. Three analyzers ask for it back, so suppress them here.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@dennisdoomen
dennisdoomen merged commit 1a9ca26 into main Aug 10, 2026
4 checks passed
@dennisdoomen
dennisdoomen deleted the harden-internals-with-embedded-attribute branch August 10, 2026 14:42
dennisdoomen added a commit to dennisdoomen/pathy that referenced this pull request Aug 10, 2026
…s copy (#153)

Pathy ships as source and compiles into the consuming assembly. It declares
Microsoft.CodeAnalysis.EmbeddedAttribute so its own types can be marked
[Embedded], which tells Roslyn to hide them from every other assembly,
including InternalsVisibleTo friends.

Reflectify is another source-only package by the same author and declares the
same attribute for the same reason. A project that uses both packages therefore
ended up with two declarations of one type in a single compilation, which failed
to build with CS0101.

Declaring the type as partial fixes this. Two partial declarations of the same
type merge into one type instead of colliding, and Roslyn matches [Embedded] by
name, so it does not care how many parts the type came from. Reflectify made the
same change in dennisdoomen/reflectify#170.

For the parts to merge they must have an identical shape, so the type-level
attributes had to go. C# does not allow a non-AllowMultiple attribute to be
repeated across parts of a partial type, so leaving [AttributeUsage] in place
would fail with CS0579 and defeat the purpose. Dropping it costs nothing because
AttributeTargets.All is already the default when an attribute carries no
[AttributeUsage]. [ExcludeFromCodeCoverage] was removed for the same reason.

Verified with a scratch consumer project that compiles Pathy's sources next to
Reflectify's declaration: the library builds, a friend assembly can still reach
the library's own internal types, and Pathy types remain hidden from it.

Co-authored-by: Dennis Doomen <dennis.doomen@greenchoice.nl>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant