Décompiler les setters init-only et les membres required : le modreq et les attributs qui les font
init et required sont deux des mots-clés les plus petits d'apparence en C# et deux des plus faciles à perdre pour un décompilateur. Un accesseur init n'est qu'un accesseur set dont la signature porte un modificateur requis — modreq(IsExternalInit) — et un décompilateur qui ne regarde pas le modificateur l'affiche comme un set ordinaire, vous remettant discrètement un type dont les propriétés peuvent être réaffectées après construction. required est pire : ce sont trois attributs distincts travaillant ensemble — RequiredMemberAttribute sur chaque membre, RequiredMemberAttribute sur le type et CompilerFeatureRequired sur le type — plus un SetsRequiredMembers sur tout constructeur qui les satisfait, et un décompilateur qui les laisse tomber produit du code dont les appelants ne sont plus forcés d'affecter quoi que ce soit. Ce billet lit l'IL et les métadonnées des deux fonctionnalités, montre la forme exacte au niveau octet du modreq et des blobs d'attributs, parcourt la façon dont Glass.NET reconstruit init, required et [SetsRequiredMembers], et explique pourquoi sauter l'étape change silencieusement le contrat public d'un type.
init et required sont deux des mots-clés les plus courts du C# moderne, et deux des plus faciles à jeter pour un décompilateur sans s”en apercevoir.
Ce sont tous deux des fonctionnalités uniquement à la compilation. Le runtime n”applique ni l”un ni l”autre — un setter par réflexion réaffectera volontiers une propriété init, et rien ne vous empêche de construire un objet qui saute un membre required si vous contournez le compilateur. Les deux existent entièrement pour que le compilateur puisse tenir les appelants à un contrat : affectez ceci une fois, pendant la construction ; vous devez affecter ceci avant d”avoir un objet valide. Ce contrat est encodé dans des métadonnées que le compilateur lit et que tous les autres ignorent, ce qui signifie qu”un décompilateur doit aller lire les mêmes métadonnées ou il vous remettra une source dont le contrat est silencieusement plus faible que l”original.
Les signaux sont petits. init est un modificateur personnalisé sur un setter. required est trois attributs plus, sur le constructeur qui satisfait, un quatrième. Manquez-les et le code compile encore — c”est exactement ce qui le rend dangereux.
init est un modreq sur le setter
Prenez le plus petit exemple possible et regardez ce quӎmet le compilateur :
public class Person
{
public string Name { get; init; }
}
La propriété compile vers un getter et un setter comme toute autre, mais la signature du setter n”est pas tout à fait ordinaire. En IL :
.property instance string Name()
{
.get instance string Person::get_Name()
.set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) Person::set_Name(string)
}
.method public hidebysig specialname instance void
set_Name(string 'value') cil managed
{
// ... ordinary field store ...
}
Toute la différence entre set et init est le modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) sur le type de retour de set_Name. Un modificateur personnalisé fait partie de la signature — le CLR traite une signature avec un modreq comme distincte d”une sans — et le compilateur utilise celui-ci en particulier comme sentinelle. Quand il résout un appel vers un setter et voit IsExternalInit dans la signature, il n”autorise l”appel que depuis un contexte d”initialisation : le constructeur du type déclarant, un initialiseur d”objet ou une expression with. Partout ailleurs c”est une erreur de compilation. Le runtime n”applique rien ; IsExternalInit n”est qu”un type que le compilateur a convenu de traiter comme un drapeau.
Cela rend le travail du décompilateur précis. Lisez la signature du setter ; si le retour porte modreq(IsExternalInit), rendez init ; sinon, rendez set. Un décompilateur qui parcourt seulement le corps de méthode et la paire get/set de la propriété, sans inspecter les modificateurs personnalisés sur la signature du setter, perd la distinction et émet set. La sortie compile — c”est le piège — mais le Person régénéré laisse désormais tout appelant écrire p.Name = "..." bien après la construction, une mutation que l”init de l”auteur était là spécifiquement pour interdire.
Glass.NET lit le modreq sur la signature du setter et reconstruit le mot-clé :
// Decompiled by Glass.NET — the init is recovered from modreq(IsExternalInit)
public class Person
{
public string Name { get; init; }
}
required est trois attributs qui doivent voyager ensemble
Ajoutez maintenant l”autre mot-clé :
public class Person
{
public required string Name { get; init; }
public required string Email { get; init; }
}
Il n”y a pas de nouvel IL pour les corps des propriétés — required ne génère aucun code. Il génère des attributs, et trois distincts, car trois choses différentes doivent être marquées. Décompilé des métadonnées, le type porte :
.class public auto ansi beforefieldinit Person
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.RequiredMemberAttribute::.ctor() = ( 01 00 00 00 )
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute::.ctor(string) = ( 01 00 0F 52 65 71 75 69 72 65 64 4D 65 6D 62 65 72 73 00 00 )
// "RequiredMembers"
// and on each required property:
.property instance string Name()
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.RequiredMemberAttribute::.ctor() = ( 01 00 00 00 )
// ...
}
}
Le blob de l”attribut sur CompilerFeatureRequired mérite d”être décodé, car c”est la garde d”échec en fermé. 01 00 est le prologue ; 0F est le préfixe de longueur (15) de la chaîne UTF-8 qui suit, 52 65 71 75 69 72 65 64 4D 65 6D 62 65 72 73 = "RequiredMembers" ; 00 00 est le nombre (vide) d”arguments nommés. Les trois attributs font trois travaux :
RequiredMemberAttributesur chaque membre marque quels membres sont requis. Ce sont ceux qu”un compilateur consommateur force les appelants à affecter.RequiredMemberAttributesur le type est un marqueur rapide indiquant que le type a des membres requis, pour qu”un consommateur n”ait pas à scanner chaque membre pour le savoir.CompilerFeatureRequiredAttribute("RequiredMembers")sur le type est la garde. Il dit à un compilateur qui ne comprend pas les membres requis de refuser le type plutôt que d”ignorer l”exigence — l”équivalent, pour une fonctionnalité du langage, d”échouer en fermé. Sans lui, un ancien compilateur laisserait volontiers les appelants sauter des membres que l”auteur exigeait.
Un décompilateur qui filtre ceux-ci comme du « bruit généré par le compilateur » — une valeur par défaut qui sonne raisonnable, puisque vous ne voulez pas [Nullable] ni [IsReadOnly] encombrant votre sortie — supprime le contrat required entier. Le type régénéré a des propriétés optionnelles ordinaires, et chaque appelant que l”original forçait à écrire new Person { Name = ..., Email = ... } est désormais libre d”écrire new Person { }. Le contrat ne s”est pas affaibli bruyamment ; il a simplement disparu.
Glass.NET reconnaît l”appariement RequiredMemberAttribute/CompilerFeatureRequired comme l”encodage de required, consomme les attributs et réémet le mot-clé sur exactement les membres qui portaient l”attribut au niveau du membre :
public class Person
{
public required string Name { get; init; }
public required string Email { get; init; }
}
[SetsRequiredMembers] est porteur sur les constructeurs
Il y a un quatrième attribut, et il vit sur les constructeurs. Considérez un type qui offre un constructeur affectant tout ce qui est requis :
public class Person
{
public required string Name { get; init; }
public required string Email { get; init; }
[SetsRequiredMembers]
public Person(string name, string email)
{
Name = name;
Email = email;
}
}
[SetsRequiredMembers] dit au compilateur que ce constructeur satisfait déjà chaque membre requis, de sorte que les appelants l”utilisant ne sont pas forcés de réaffecter ces membres dans un initialiseur d”objet. Sans lui, new Person("a", "b") exigerait encore { Name = ..., Email = ... }, ce qui serait absurde. Le compilateur l”émet comme System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute sur le constructeur, et il est émis automatiquement derrière le constructeur de copie généré d”un record pour que les expressions with fonctionnent.
Celui-ci est porteur au sens le plus fort : laissez-le tomber et le code régénéré ne compile même pas de la même façon. Un décompilateur qui omet [SetsRequiredMembers] produit un constructeur dont le compilateur juge alors qu”il ne satisfait pas les membres requis, donc tout appelant par initialiseur d”objet est soudain forcé d”affecter des propriétés que la conception d”origine lui laissait omettre — ou, si le constructeur est le seul chemin, le type devient pénible à utiliser exactement de la façon que l”attribut existait pour empêcher. Glass.NET le fait remonter tel quel sur le constructeur, car le type ne fait pas l”aller-retour sans lui.
Lire les deux signaux, indépendamment
La raison pour laquelle les deux fonctionnalités demandent du soin, c”est qu”elles sont orthogonales, et leurs quatre combinaisons sont quatre contrats différents :
public string A { get; set; } // mutable, optional
public string B { get; init; } // set once, optional
public required string C { get; set; } // must be set, then mutable
public required string D { get; init; } // must be set, construction only
Dans les métadonnées, c”est la combinaison indépendante de deux signaux : le modreq IsExternalInit sur le setter (présent → init, absent → set) et le RequiredMemberAttribute sur le membre (présent → required, absent → non). Un décompilateur doit les lire séparément et les combiner, car chacun effondré seul produit une réponse fausse différente : perdez le modreq et D devient réaffectable ; perdez l”attribut et D devient optionnel ; perdez les deux et c”est une propriété mutable simple sans aucune des garanties que l”auteur a écrites.
C”est le fil conducteur de toute cette famille de fonctionnalités C#. Le mot-clé que vous avez tapé n”a laissé presque aucune trace dans le code exécutable — aucun nouvel IL dans les corps, juste un modificateur sur une signature et une poignée d”attributs sur les déclarations. Le compilateur a mis le contrat dans les métadonnées et a fait confiance au compilateur suivant pour le lire. Un décompilateur qui veut vous rendre la source que l”auteur a réellement écrite doit être ce lecteur suivant : inspecter les modificateurs personnalisés, conserver les attributs qui encodent des fonctionnalités du langage plutôt que de les filtrer comme du bruit, et reconstruire init, required et [SetsRequiredMembers] avec précision, pour que le type que vous récupérez impose exactement ce qu”imposait l”original.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.