Skip to content

Unsafe evolution: avoid checking unreferenced assemblies#82300

Merged
jjonescz merged 7 commits into
dotnet:features/UnsafeEvolutionfrom
jjonescz:Unsafe-15-IvtAssert
Feb 18, 2026
Merged

Unsafe evolution: avoid checking unreferenced assemblies#82300
jjonescz merged 7 commits into
dotnet:features/UnsafeEvolutionfrom
jjonescz:Unsafe-15-IvtAssert

Conversation

@jjonescz

@jjonescz jjonescz commented Feb 5, 2026

Copy link
Copy Markdown
Member

Test plan: #81207

@dotnet-policy-service dotnet-policy-service Bot added VSCode Needs API Review Needs to be reviewed by the API review council labels Feb 5, 2026
@jjonescz jjonescz closed this Feb 11, 2026
@jjonescz jjonescz reopened this Feb 11, 2026
@jjonescz jjonescz removed Needs API Review Needs to be reviewed by the API review council VSCode labels Feb 11, 2026
@jjonescz jjonescz marked this pull request as ready for review February 11, 2026 19:44
@jjonescz jjonescz requested a review from a team as a code owner February 11, 2026 19:44
@jjonescz jjonescz requested review from 333fred and jcouv February 11, 2026 19:44
333fred
333fred previously approved these changes Feb 12, 2026
@jcouv jcouv self-assigned this Feb 12, 2026
return;
}

overriddenMember = overriddenMember.OriginalDefinition;

@jcouv jcouv Feb 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't quite understand exactly what the problem was. How did the logic below trigger an IVT check and why was that check inappropriate? It seems fair game to ask questions about overriddenMember and leastOverriddenMember since we're referencing them. #Closed

@jjonescz jjonescz Feb 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did the logic below trigger an IVT check and why was that check inappropriate?

Consider test InternalsVisibleToAndStrongNameTests.Issue81820_01 which was failing. The GetLeastOverriddenMember when called on C3.M() performs an IVT check on C1<C2>.M() and the assert fails since C2 is from an "unrelated" assembly (it's not referenced by the assembly that C1 comes from).

It seems fair game to ask questions about overriddenMember and leastOverriddenMember since we're referencing them.

I'm also not sure why this shouldn't be allowed in general (cc @AlekseyTs who added the assert). But in this case it seems fine to narrow the check to OriginalDefinition.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without doing a deep analysis, I find this change suspicious. I think that the argument to the GetLeastOverriddenMember is wrong. The within type is the type that overrides, not the type where the overridden member is declared. On the other subject, I am surprised that I was not proactively consulted on the issue and instead we had to rely on reviewers to ask questions about the change.

@jjonescz jjonescz Feb 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other subject, I am surprised that I was not proactively consulted on the issue and instead we had to rely on reviewers to ask questions about the change.

Good point, sorry, I will try to be more proactive next time. I thought I understood what the issue is here until I started thinking more about Julien's question and that's when I summoned you :)

@jcouv jcouv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with review pass (commit 1)

}

overriddenMember = overriddenMember.OriginalDefinition;
var leastOverriddenMember = overriddenMember.GetLeastOverriddenMember(overriddenMember.ContainingType);

@AlekseyTs AlekseyTs Feb 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var leastOverriddenMember = overriddenMember.GetLeastOverriddenMember(overriddenMember.ContainingType);

If we are looking for a least overridden member for an override in source, then I think we should start with that override, not from some intermediate override. Then, we wouldn't make a mistake with the context type. The way it looks to me, to implement the current logic, we don't even need the overriddenMember parameter. Then there is also a question whether the check should be performed against the least overridden member, rather than the immediate overridden member, like the other similar methods do. #Closed

@jjonescz jjonescz Feb 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then there is also a question whether the check should be performed against the least overridden member, rather than the immediate overridden member, like the other similar methods do.

You're right, I can simplify this. I originally wanted this to be more similar to Obsolete (which is similar in other ways too; and it checks against least overridden member) but that doesn't seem necessary, wouldn't match the speclet either I guess, and consistency with other modifiers like scoped seems better.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take that back, as Julien pointed out offline, it makes sense to look at the least overridden member. In case we have unsafe overrides safe overrides unsafe, there should be no error.

@AlekseyTs AlekseyTs Feb 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you perhaps give an example? Also, what does the feature spec explicitly say about the situation? Consider also adding a test that observes the difference.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D.M3() in Member_Method_Override is interesting scenario

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you perhaps give an example?
Consider also adding a test that observes the difference.

As Julien says, D2.M3() in Member_Method_Override is both an example and a test for this.

Also, what does the feature spec explicitly say about the situation?

The speclet currently only has this simple paragraph for OHI:

It is a memory safety error to add RequiresUnsafe at the member level in any override or implementation of a member that is not requires-unsafe originally, because callers may be using the base
definition and not see any addition of RequiresUnsafe by a derived implementation.

We can probably clarify but I think the intent is clear.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be checking all methods in the chain? Do we assume all code is produced by C# compiler?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be checking all methods in the chain? Do we assume all code is produced by C# compiler?

I guess we could add an open question for this. But it feels like we shouldn't report declaration errors for methods from metadata - if someone marks a method as RequriesUnsafe using a compiler that doesn't support that feature, that's their fault. Similarly we don't check if someone marks a class as RequiresUnsafe in other languages.

@333fred 333fred dismissed their stale review February 12, 2026 23:36

After comments from Aleksey, it seems like we should have a different fix.

}

var leastOverriddenMember = overriddenMember.GetLeastOverriddenMember(overriddenMember.ContainingType);
var leastOverriddenMember = implementedMember ?? overridingMember.GetLeastOverriddenMember(overridingMember.ContainingType);

@AlekseyTs AlekseyTs Feb 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overridingMember.ContainingType

this? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inside a static helper.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inside a static helper.

Can we assert that overridingMember is definition?

@jjonescz jjonescz Feb 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we can.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this won't hold in some complex cases, adding test Member_Method_Implementation_Synthesized to demonstrate one such case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we assert that it is a definition when implementedMember is null? It is important for the situation when GetLeastOverriddenMember is called.


CheckCallerUnsafeMismatch(
overriddenEvent,
implementedMember: null,

@AlekseyTs AlekseyTs Feb 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null

Are there call-sites that pass non-null? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for interface implementations.

@jcouv jcouv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (commit 6)

@jjonescz

Copy link
Copy Markdown
Member Author

@333fred or @AlekseyTs for another look, thanks

@AlekseyTs

Copy link
Copy Markdown
Contributor

Done with review pass (commit 6)

@jjonescz jjonescz requested a review from AlekseyTs February 17, 2026 17:41

@AlekseyTs AlekseyTs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (commit 7)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants