Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type parameter constraint in patched class breaks on newer Mono.Cecil version #931

Open
Kalobi opened this issue Jan 11, 2024 · 6 comments

Comments

@Kalobi
Copy link

Kalobi commented Jan 11, 2024

We encountered this issue while working on Everest, the modding API for the video game Celeste. Everest patches the game using MonoMod, which relies heavily on Mono.Cecil. We've been depending on Mono.Cecil version 0.11.4 so far. When changing the dependency to the latest version 0.11.5 (with no other changes to the project), in the patched UserIO class (using this patch), the type parameter constraint of the generic Save<T> method changes from where T : class (what it is in vanilla and should remain) to where T : class, Entity. Entity is a class that is used a lot in Celeste, but appears neither in the vanilla version of the UserIO class nor anywhere in our patch. Since the only change is the Mono.Cecil version, some kind of bug in Mono.Cecil introduced between versions 0.11.4 and 0.11.5 must be causing this type parameter constraint to bleed over from some earlier point where Mono.Cecil interacts with the Entity class.

@jbevain
Copy link
Owner

jbevain commented Jan 11, 2024

Could you extract the offending code in a small dll that we could write a test around? Thanks!

@Kalobi
Copy link
Author

Kalobi commented Jan 11, 2024

I will look into it, but unfortunately this is quite a complex project and I'm not sure I'll be able to craft a small standalone version that exhibits this bug.

@jbevain
Copy link
Owner

jbevain commented Jan 11, 2024

That would however provide the best chances at this being diagnosed and fixed :)

@Kalobi
Copy link
Author

Kalobi commented Jan 12, 2024

I did manage to use git bisect to determine that the issue is introduced by commit c4cfe16.

@Kalobi
Copy link
Author

Kalobi commented Jan 12, 2024

Specifically, readding the metadata.RemoveGenericConstraintMapping (generic_parameter); line to MetadataReader.ReadGenericConstraints (and readding that method of course) fixes the issue.

@RedFlames
Copy link

So, I've found something, or at least how to trigger this or make it go away.

Some of it is part of MonoMod though. Also I'm very much at my wits end, but I'll just write down what it is that I found.

So, Everest's installer patches Celeste.exe with MonoMod.

Within MonoMod you at some point end up in MonoMod.MonoModder.PatchRefsInType(TypeDefinition type) where you got this

            foreach (MethodDefinition method in type.Methods)
                PatchRefsInMethod(method);

And then in PatchRefsInMethod(MethodDefinition method) when on the Celeste.UserIO type's method which looks like this inside Celeste

public static bool Save<T>(string path, byte[] data) where T : class
{
[...]
}

if you now simply just call the getter of method.CustomDebugInformations or even just method.HasCustomDebugInformations -- you will now end up with a new Constraint, specifically I've been checking method.GenericParameters[0].Constraints.Count

But also the extra fun thing is if you touch the getter of Constraints on that first generic parameter first, it will also be fine and not get the extra constraint.

The part within MonoModder that triggers it actually looks like this, but you only need to "get" method.CustomDebugInformations, the collection it return is empty on this method, so this loop is irrelevant.

            foreach (CustomDebugInformation dbgInfo in method.CustomDebugInformations)
                if (dbgInfo is AsyncMethodBodyDebugInformation asyncDbgInfo)
                    for (var i = 0; i < asyncDbgInfo.ResumeMethods.Count; i++)
                        asyncDbgInfo.ResumeMethods[i] = ((MethodReference) asyncDbgInfo.ResumeMethods[i].Relink(Relinker, method)).Resolve();

I just tried to see if I could trigger this without MonoMod, by just putting this in place of the MonoModder execution:

                if (module.Name == "Celeste.exe") {

                    var method = module.Types.First(t => t.FullName.Contains("Celeste.UserIO")).Methods.First(m => m.FullName.Contains("Save("));

                    // if I comment this out, I should somehow get "1" on the second log statement, otherwise "0" on both...
                    //logVerboseCb($"[UserIOLogger] Count = {method.GenericParameters[0].Constraints.Count}");

                    foreach (var cdi in method.CustomDebugInformations)
                        logVerboseCb($"{cdi}");

                    logVerboseCb($"[UserIOLogger] Count = {method.GenericParameters[0].Constraints.Count}");

                }

But it doesn't want to trigger the bug at all. Must be something else that MonoMod is doing.

The place in MonoMod is here
https://github.com/MonoMod/MonoMod/blob/878928553ef775bd7814ae5d208771f0d4ec29be/src/MonoMod.Patcher/MonoModder.cs#L1949

although I was working inside Everest where it subclasses MonoModder and I copied the base implementation of void PatchRefsInMethod(MethodDefinition method) into an override and worked there because that was easier than (re-)building MonoMod all the time. 😅

https://github.com/EverestAPI/Everest/blob/464dea2c34aac90d0cbb4d7c733c88e2f6bf9d68/NETCoreifier/NetFrameworkModder.cs#L13


I suppose I could try chipping away at MonoMod to actually reproduce the bug without MonoMod... anyways this doesn't make any sense :)

PS: What even is Mono.Cecil.Mixin.Read

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants