diff --git a/.jules/sentinel.md b/.jules/sentinel.md index f5cf3bd..ad70d47 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** Source Generator `NTypeForge.SourceGenerator` was always emitting `public static class` for its duck-typing extensions. If the type being ducked (`target`) was `internal`, this created a `CS0051: Inconsistent accessibility` compiler error and essentially violated the principle of least privilege, potentially leaking internal types into public scope. **Learning:** Source Generators creating types meant to be used alongside user code should dynamically match the accessibility of their targets (or default to `internal` if appropriate for their use-case) to prevent scope leakage. **Prevention:** Check `DeclaredAccessibility == Accessibility.Public` (or effective accessibility for nested types) on the target type, and emit `internal static class` instead of `public static class` when the target is not public. + +## 2025-06-09 - [Scope Leakage] Robust Checking for Effective Public Accessibility +**Vulnerability:** The source generator's `IsEffectivelyPublic` logic failed to deeply inspect compound types such as arrays, pointers, and generic type arguments. Consequently, a public generic type parameterized with an internal type (e.g. `PublicGeneric`) was incorrectly considered fully public. This could lead to scope leakage by emitting `public` extension classes for types that logically should be treated as `internal`, resulting in CS0050/CS0051 visibility errors when compiling generated code. +**Learning:** Checking type visibility in Roslyn requires more than just checking `DeclaredAccessibility` of a type and its `ContainingType`. Compound types wrap nested element types whose accessibilities must be recursively verified. In addition, array and pointer types report `Accessibility.NotApplicable`, which must be handled. +**Prevention:** `IsEffectivelyPublic` must recursively check `ElementType` for Arrays, `PointedAtType` for Pointers, and `TypeArguments` for Generic Types to ensure no internal types are inadvertently leaked. diff --git a/src/NTypeForge.SourceGenerator/CandidateAnalyzer.cs b/src/NTypeForge.SourceGenerator/CandidateAnalyzer.cs index bed3113..b0860a5 100644 --- a/src/NTypeForge.SourceGenerator/CandidateAnalyzer.cs +++ b/src/NTypeForge.SourceGenerator/CandidateAnalyzer.cs @@ -96,8 +96,25 @@ private static bool IsEffectivelyPublic(ITypeSymbol type) { for (var current = type; current != null; current = current.ContainingType) { - if (current.DeclaredAccessibility != Accessibility.Public) return false; + if (current.DeclaredAccessibility != Accessibility.Public && current.DeclaredAccessibility != Accessibility.NotApplicable) return false; } + + switch (type.TypeKind) + { + case TypeKind.Array: + return IsEffectivelyPublic(((IArrayTypeSymbol)type).ElementType); + case TypeKind.Pointer: + return IsEffectivelyPublic(((IPointerTypeSymbol)type).PointedAtType); + } + + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) + { + foreach (var typeArg in namedType.TypeArguments) + { + if (!IsEffectivelyPublic(typeArg)) return false; + } + } + return true; }