Comment puis-je implémenter ISerializable dans .NET 4+ sans violer les règles de sécurité d'héritage?


109

Contexte: Noda Time contient de nombreuses structures sérialisables. Bien que je n'aime pas la sérialisation binaire, nous avons reçu de nombreuses demandes pour la prendre en charge, dans la chronologie 1.x. Nous le soutenons en implémentant l' ISerializableinterface.

Nous avons reçu un rapport récent sur l' échec de Noda Time 2.x dans .NET Fiddle . Le même code utilisant Noda Time 1.x fonctionne correctement. L'exception levée est la suivante:

Règles de sécurité d'héritage violées lors du remplacement du membre: «NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData (System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)». L'accessibilité de sécurité de la méthode de substitution doit correspondre à l'accessibilité de sécurité de la méthode remplacée.

J'ai réduit cela au cadre ciblé: 1.x cibles .NET 3.5 (profil client); 2.x cible .NET 4.5. Ils présentent de grandes différences en termes de prise en charge de PCL par rapport à .NET Core et de la structure des fichiers de projet, mais il semble que cela ne soit pas pertinent.

J'ai réussi à reproduire cela dans un projet local, mais je n'ai pas trouvé de solution.

Étapes à suivre pour reproduire dans VS2017:

  • Créer une nouvelle solution
  • Créez une nouvelle application console Windows classique ciblant .NET 4.5.1. Je l'ai appelé "CodeRunner".
  • Dans les propriétés du projet, accédez à Signature et signez l'assembly avec une nouvelle clé. Décochez l'exigence de mot de passe et utilisez n'importe quel nom de fichier de clé.
  • Collez le code suivant pour remplacer Program.cs. Il s'agit d'une version abrégée du code de cet exemple Microsoft . J'ai gardé tous les chemins identiques, donc si vous voulez revenir au code plus complet, vous ne devriez rien changer d'autre.

Code:

using System;
using System.Security;
using System.Security.Permissions;

class Sandboxer : MarshalByRefObject  
{  
    static void Main()  
    {  
        var adSetup = new AppDomainSetup();  
        adSetup.ApplicationBase = System.IO.Path.GetFullPath(@"..\..\..\UntrustedCode\bin\Debug");  
        var permSet = new PermissionSet(PermissionState.None);  
        permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));  
        var fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<System.Security.Policy.StrongName>();  
        var newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);  
        var handle = Activator.CreateInstanceFrom(  
            newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,  
            typeof(Sandboxer).FullName  
            );  
        Sandboxer newDomainInstance = (Sandboxer) handle.Unwrap();  
        newDomainInstance.ExecuteUntrustedCode("UntrustedCode", "UntrustedCode.UntrustedClass", "IsFibonacci", new object[] { 45 });  
    }  

    public void ExecuteUntrustedCode(string assemblyName, string typeName, string entryPoint, Object[] parameters)  
    {  
        var target = System.Reflection.Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint);
        target.Invoke(null, parameters);
    }  
}
  • Créez un autre projet appelé "UntrustedCode". Il doit s'agir d'un projet de bibliothèque de classes de bureau classique.
  • Signez l'assemblée; vous pouvez utiliser une nouvelle clé ou la même que pour CodeRunner. (Ceci est en partie pour imiter la situation Noda Time, et en partie pour garder l'analyse de code heureuse.)
  • Collez le code suivant dans Class1.cs(en écrasant ce qu'il y a):

Code:

using System;
using System.Runtime.Serialization;
using System.Security;
using System.Security.Permissions;

// [assembly: AllowPartiallyTrustedCallers]

namespace UntrustedCode
{
    public class UntrustedClass
    {
        // Method named oddly (given the content) in order to allow MSDN
        // sample to run unchanged.
        public static bool IsFibonacci(int number)
        {
            Console.WriteLine(new CustomStruct());
            return true;
        }
    }

    [Serializable]
    public struct CustomStruct : ISerializable
    {
        private CustomStruct(SerializationInfo info, StreamingContext context) { }

        //[SecuritySafeCritical]
        //[SecurityCritical]
        //[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            throw new NotImplementedException();
        }
    }
}

L'exécution du projet CodeRunner donne l'exception suivante (reformatée pour plus de lisibilité):

Exception non gérée: System.Reflection.TargetInvocationException: une
exception a été levée par la cible d'un appel.
--->
System.TypeLoadException:
règles de sécurité d'héritage violées lors
du remplacement du membre: 'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData (...).
L'accessibilité de sécurité
de la méthode de substitution doit correspondre à l' accessibilité de sécurité de la méthode remplacée.

Les attributs commentés montrent des choses que j'ai essayées:

  • SecurityPermissionest recommandé par deux articles MS différents ( premier , deuxième ), bien qu'il soit intéressant de noter qu'ils font des choses différentes autour de l'implémentation d'interface explicite / implicite
  • SecurityCriticalest ce que Noda Time a actuellement, et c'est ce que la réponse à cette question suggère
  • SecuritySafeCritical est quelque peu suggéré par les messages de règle d'analyse du code
  • Sans aucun attributs, code des règles d' analyse sont heureux - soit avec SecurityPermissionou SecurityCritical présent, les règles que vous disent de supprimer les attributs - à moins que vous n'avez . Suivre les suggestions dans les deux cas n'aide pas.AllowPartiallyTrustedCallers
  • Noda Time s'y est AllowPartiallyTrustedCallersappliqué; l'exemple ici ne fonctionne pas avec ou sans l'attribut appliqué.

Le code fonctionne sans exception si j'ajoute [assembly: SecurityRules(SecurityRuleSet.Level1)]à l' UntrustedCodeassembly (et décommente l' AllowPartiallyTrustedCallersattribut), mais je pense que c'est une mauvaise solution au problème qui pourrait gêner d'autres codes.

J'avoue pleinement être assez perdu en ce qui concerne ce genre d'aspect de sécurité de .NET. Alors, que puis- je faire pour cibler .NET 4.5 tout en autorisant mes types à implémenter ISerializableet à être encore utilisés dans des environnements tels que .NET Fiddle?

(Bien que je cible .NET 4.5, je pense que ce sont les changements de politique de sécurité .NET 4.0 qui ont causé le problème, d'où la balise.)


Fait intéressant, cette explication des changements apportés au modèle de sécurité dans la version 4.0 suggère que la simple suppression AllowPartiallyTrustedCallersdevrait faire l'affaire, mais cela ne semble pas faire de différence
Mathias R. Jessen

Réponses:


56

Selon le MSDN , dans .NET 4.0, vous ne devez essentiellement pas utiliser ISerializablepour du code partiellement approuvé, mais à la place, vous devez utiliser ISafeSerializationData

Citant de https://docs.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization

Important

Dans les versions antérieures à .NET Framework 4.0, la sérialisation des données utilisateur personnalisées dans un assembly partiellement approuvé était effectuée à l'aide de GetObjectData. À compter de la version 4.0, cette méthode est marquée avec l'attribut SecurityCriticalAttribute qui empêche l'exécution dans des assemblys partiellement approuvés. Pour contourner cette condition, implémentez l'interface ISafeSerializationData.

Donc probablement pas ce que vous vouliez entendre si vous en avez besoin, mais je ne pense pas qu'il y ait un moyen de contourner cela tout en ISerializablecontinuant à utiliser (à part revenir à la Level1sécurité, ce que vous avez dit que vous ne voulez pas).

PS: la ISafeSerializationDatadocumentation indique que ce n'est que pour des exceptions, mais cela ne semble pas si spécifique, vous voudrez peut-être essayer ... Je ne peux fondamentalement pas le tester avec votre exemple de code (à part supprimer des ISerializableœuvres, mais tu le savais déjà) ... tu devras voir si ISafeSerializationDataça te convient assez.

PS2: l' SecurityCriticalattribut ne fonctionne pas car il est ignoré lorsque l'assembly est chargé en mode confiance partielle ( sur la sécurité Level2 ). Vous pouvez le voir sur votre exemple de code, si vous déboguer la targetvariable ExecuteUntrustedCodejuste avant d' invoquer, il aurez IsSecurityTransparentà trueet IsSecurityCriticalde falsemême si vous marquez la méthode avec l' SecurityCriticalattribut)


Aha - merci pour l'explication. Dommage que l'exception soit si trompeuse ici. Aura besoin de savoir quoi faire ...
Jon Skeet

@JonSkeet Honnêtement, j'abandonnerais complètement la sérialisation binaire ... mais je comprends que votre base d'utilisateurs ne l'aimera peut-être pas
Jcl

Je pense que nous devrons faire cela - ce qui signifie passer à la v3.0. Il a cependant d'autres avantages ... Je vais devoir consulter la communauté Noda Time.
Jon Skeet

12
@JonSkeet btw, si vous êtes intéressé, cet article explique les différences entre la sécurité de niveau 1 et de niveau 2 (et POURQUOI cela ne fonctionne pas)
Jcl

8

La réponse acceptée est si convaincante que j'ai presque cru que ce n'était pas un bug. Mais après avoir fait quelques expériences maintenant, je peux dire que la sécurité Level2 est un désordre complet; au moins, quelque chose est vraiment louche.

Il y a quelques jours, je suis tombé sur le même problème avec mes bibliothèques. J'ai rapidement créé un test unitaire; cependant, je n'ai pas pu reproduire le problème que j'ai rencontré dans .NET Fiddle, alors que le même code "avec succès" a jeté l'exception dans une application console. En fin de compte, j'ai trouvé deux façons étranges de surmonter le problème.

TL; DR : Il s'avère que si vous utilisez un type interne de la bibliothèque utilisée dans votre projet consommateur, alors le code partiellement fiable fonctionne comme prévu: il est capable d'instancier une ISerializableimplémentation (et un code critique de sécurité ne peut pas être appelé directement, mais voir ci-dessous). Ou, ce qui est encore plus ridicule, vous pouvez essayer de créer à nouveau le bac à sable s'il n'a pas fonctionné pour la première fois ...

Mais voyons du code.

ClassLibrary.dll:

Séparons deux cas: un pour une classe régulière avec un contenu critique pour la sécurité et une ISerializableimplémentation:

public class CriticalClass
{
    public void SafeCode() { }

    [SecurityCritical]
    public void CriticalCode() { }

    [SecuritySafeCritical]
    public void SafeEntryForCriticalCode() => CriticalCode();
}

[Serializable]
public class SerializableCriticalClass : CriticalClass, ISerializable
{
    public SerializableCriticalClass() { }

    private SerializableCriticalClass(SerializationInfo info, StreamingContext context) { }

    [SecurityCritical]
    public void GetObjectData(SerializationInfo info, StreamingContext context) { }
}

Une façon de résoudre le problème consiste à utiliser un type interne de l'assemblage consommateur. N'importe quel type le fera; maintenant je définis un attribut:

[AttributeUsage(AttributeTargets.All)]
internal class InternalTypeReferenceAttribute : Attribute
{
    public InternalTypeReferenceAttribute() { }
}

Et les attributs pertinents appliqués à l'assemblage:

[assembly: InternalsVisibleTo("UnitTest, PublicKey=<your public key>")]
[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityRules(SecurityRuleSet.Level2, SkipVerificationInFullTrust = true)]

Signez l'assemblage, appliquez la clé à l' InternalsVisibleToattribut et préparez-vous au projet de test:

UnitTest.dll (utilise NUnit et ClassLibrary):

Pour utiliser l'astuce interne, l'assembly de test doit également être signé. Attributs d'assemblage:

// Just to make the tests security transparent by default. This helps to test the full trust behavior.
[assembly: AllowPartiallyTrustedCallers] 

// !!! Comment this line out and the partial trust test cases may fail for the fist time !!!
[assembly: InternalTypeReference]

Remarque : l'attribut peut être appliqué n'importe où. Dans mon cas, c'était sur une méthode dans une classe de test aléatoire qui m'a pris quelques jours à trouver.

Remarque 2 : Si vous exécutez toutes les méthodes de test ensemble, il peut arriver que les tests réussissent.

Le squelette de la classe de test:

[TestFixture]
public class SecurityCriticalAccessTest
{
    private partial class Sandbox : MarshalByRefObject
    {
    }

    private static AppDomain CreateSandboxDomain(params IPermission[] permissions)
    {
        var evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
        var permissionSet = GetPermissionSet(permissions);
        var setup = new AppDomainSetup
        {
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
        };

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        var strongNames = new List<StrongName>();
        foreach (Assembly asm in assemblies)
        {
            AssemblyName asmName = asm.GetName();
            strongNames.Add(new StrongName(new StrongNamePublicKeyBlob(asmName.GetPublicKey()), asmName.Name, asmName.Version));
        }

        return AppDomain.CreateDomain("SandboxDomain", evidence, setup, permissionSet, strongNames.ToArray());
    }

    private static PermissionSet GetPermissionSet(IPermission[] permissions)
    {
        var evidence = new Evidence();
        evidence.AddHostEvidence(new Zone(SecurityZone.Internet));
        var result = SecurityManager.GetStandardSandbox(evidence);
        foreach (var permission in permissions)
            result.AddPermission(permission);
        return result;
    }
}

Et voyons les cas de test un par un

Cas 1: implémentation ISerializable

Le même problème que dans la question. Le test réussit si

  • InternalTypeReferenceAttribute est appliqué
  • sandbox est essayé d'être créé plusieurs fois (voir le code)
  • ou, si tous les cas de test sont exécutés en même temps et que ce n'est pas le premier

Sinon, il y a une Inheritance security rules violated while overriding member...exception totalement inappropriée lorsque vous instanciez SerializableCriticalClass.

[Test]
[SecuritySafeCritical] // for Activator.CreateInstance
public void SerializableCriticalClass_PartialTrustAccess()
{
    var domain = CreateSandboxDomain(
        new SecurityPermission(SecurityPermissionFlag.SerializationFormatter), // BinaryFormatter
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    var handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    var sandbox = (Sandbox)handle.Unwrap();
    try
    {
        sandbox.TestSerializableCriticalClass();
        return;
    }
    catch (Exception e)
    {
        // without [InternalTypeReference] it may fail for the first time
        Console.WriteLine($"1st try failed: {e.Message}");
    }

    domain = CreateSandboxDomain(
        new SecurityPermission(SecurityPermissionFlag.SerializationFormatter), // BinaryFormatter
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    sandbox = (Sandbox)handle.Unwrap();
    sandbox.TestSerializableCriticalClass();

    Assert.Inconclusive("Meh... succeeded only for the 2nd try");
}

private partial class Sandbox
{
    public void TestSerializableCriticalClass()
    {
        Assert.IsFalse(AppDomain.CurrentDomain.IsFullyTrusted);

        // ISerializable implementer can be created.
        // !!! May fail for the first try if the test does not use any internal type of the library. !!!
        var critical = new SerializableCriticalClass();

        // Critical method can be called via a safe method
        critical.SafeEntryForCriticalCode();

        // Critical method cannot be called directly by a transparent method
        Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
        Assert.Throws<MethodAccessException>(() => critical.GetObjectData(null, new StreamingContext()));

        // BinaryFormatter calls the critical method via a safe route (SerializationFormatter permission is required, though)
        new BinaryFormatter().Serialize(new MemoryStream(), critical);
    }

}

Cas 2: cours régulier avec des membres critiques pour la sécurité

Le test passe dans les mêmes conditions que le premier. Cependant, le problème est complètement différent ici: un code partiellement approuvé peut accéder directement à un membre critique de sécurité .

[Test]
[SecuritySafeCritical] // for Activator.CreateInstance
public void CriticalClass_PartialTrustAccess()
{
    var domain = CreateSandboxDomain(
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess), // Assert.IsFalse
        new EnvironmentPermission(PermissionState.Unrestricted)); // Assert.Throws (if fails)
    var handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    var sandbox = (Sandbox)handle.Unwrap();
    try
    {
        sandbox.TestCriticalClass();
        return;
    }
    catch (Exception e)
    {
        // without [InternalTypeReference] it may fail for the first time
        Console.WriteLine($"1st try failed: {e.Message}");
    }

    domain = CreateSandboxDomain(
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    sandbox = (Sandbox)handle.Unwrap();
    sandbox.TestCriticalClass();

    Assert.Inconclusive("Meh... succeeded only for the 2nd try");
}

private partial class Sandbox
{
    public void TestCriticalClass()
    {
        Assert.IsFalse(AppDomain.CurrentDomain.IsFullyTrusted);

        // A type containing critical methods can be created
        var critical = new CriticalClass();

        // Critical method can be called via a safe method
        critical.SafeEntryForCriticalCode();

        // Critical method cannot be called directly by a transparent method
        // !!! May fail for the first time if the test does not use any internal type of the library. !!!
        // !!! Meaning, a partially trusted code has more right than a fully trusted one and is       !!!
        // !!! able to call security critical method directly.                                        !!!
        Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
    }
}

Cas 3-4: Versions de confiance totale du cas 1-2

Par souci d'exhaustivité, voici les mêmes cas que ceux ci-dessus exécutés dans un domaine entièrement fiable. Si vous supprimez [assembly: AllowPartiallyTrustedCallers]les tests échouent car vous pouvez accéder directement au code critique (car les méthodes ne sont plus transparentes par défaut).

[Test]
public void CriticalClass_FullTrustAccess()
{
    Assert.IsTrue(AppDomain.CurrentDomain.IsFullyTrusted);

    // A type containing critical methods can be created
    var critical = new CriticalClass();

    // Critical method cannot be called directly by a transparent method
    Assert.Throws<MethodAccessException>(() => critical.CriticalCode());

    // Critical method can be called via a safe method
    critical.SafeEntryForCriticalCode();
}

[Test]
public void SerializableCriticalClass_FullTrustAccess()
{
    Assert.IsTrue(AppDomain.CurrentDomain.IsFullyTrusted);

    // ISerializable implementer can be created
    var critical = new SerializableCriticalClass();

    // Critical method cannot be called directly by a transparent method (see also AllowPartiallyTrustedCallersAttribute)
    Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
    Assert.Throws<MethodAccessException>(() => critical.GetObjectData(null, default(StreamingContext)));

    // Critical method can be called via a safe method
    critical.SafeEntryForCriticalCode();

    // BinaryFormatter calls the critical method via a safe route
    new BinaryFormatter().Serialize(new MemoryStream(), critical);
}

Épilogue:

Bien sûr, cela ne résoudra pas votre problème avec .NET Fiddle. Mais maintenant, je serais très surpris si ce n'était pas un bug dans le framework.

La plus grande question pour moi maintenant est la partie citée dans la réponse acceptée. Comment en sont-ils sortis avec ce non-sens? Le ISafeSerializationDatan'est clairement pas une solution pour quoi que ce soit: il est utilisé exclusivement par la Exceptionclasse de base et si vous souscrivez l' SerializeObjectStateévénement (pourquoi n'est-ce pas une méthode remplaçable?), Alors l'état sera également consommé par le Exception.GetObjectDataà la fin.

Le triumvirat AllowPartiallyTrustedCallers/ SecurityCritical/ SecuritySafeCriticaldes attributs a été conçu exactement pour l'usage indiqué ci-dessus. Il me semble totalement absurde qu'un code partiellement fiable ne puisse même pas instancier un type quelle que soit la tentative utilisant ses membres critiques pour la sécurité. Mais il est encore plus absurde (une faille de sécurité en fait) qu'un code partiellement fiable puisse accéder directement à une méthode critique de sécurité (voir le cas 2 ) alors que cela est interdit pour les méthodes transparentes même à partir d'un domaine entièrement fiable.

Donc, si votre projet consommateur est un test ou un autre assemblage bien connu, alors l'astuce interne peut être parfaitement utilisée. Pour .NET Fiddle et d'autres environnements en bac à sable réels, la seule solution est de revenir SecurityRuleSet.Level1jusqu'à ce que cela soit résolu par Microsoft.


Mise à jour: un ticket de la communauté des développeurs a été créé pour le problème.


2

Selon le MSDN voir:

Comment corriger les violations?

Pour corriger une violation de cette règle, rendez la méthode GetObjectData visible et remplaçable et assurez-vous que tous les champs d'instance sont inclus dans le processus de sérialisation ou explicitement marqués avec l' attribut NonSerializedAttribute .

L' exemple suivant corrige les deux violations précédentes en fournissant une implémentation remplaçable de ISerializable.GetObjectData sur la classe Book et en fournissant une implémentation de ISerializable.GetObjectData sur la classe Library.

using System;
using System.Security.Permissions;
using System.Runtime.Serialization;

namespace Samples2
{
    [Serializable]
    public class Book : ISerializable
    {
        private readonly string _Title;

        public Book(string title)
        {
            if (title == null)
                throw new ArgumentNullException("title");

            _Title = title;
        }

        protected Book(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            _Title = info.GetString("Title");
        }

        public string Title
        {
            get { return _Title; }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        protected virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Title", _Title);
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            GetObjectData(info, context);
        }
    }

    [Serializable]
    public class LibraryBook : Book
    {
        private readonly DateTime _CheckedOut;

        public LibraryBook(string title, DateTime checkedOut)
            : base(title)
        {
            _CheckedOut = checkedOut;
        }

        protected LibraryBook(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            _CheckedOut = info.GetDateTime("CheckedOut");
        }

        public DateTime CheckedOut
        {
            get { return _CheckedOut; }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        protected override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            base.GetObjectData(info, context);

            info.AddValue("CheckedOut", _CheckedOut);
        }
    }
}

2
L'article auquel vous avez lié est pour CA2240, qui n'est pas déclenché - le code ne le viole pas. C'est une structure, donc c'est effectivement scellé; il n'a aucun champ; il implémente GetObjectDataexplicitement, mais le faire implicitement n'aide pas.
Jon Skeet

15
Bien sûr, et merci d'avoir essayé - mais j'explique pourquoi cela ne fonctionne pas . (Et à titre de recommandation - pour quelque chose de délicat comme celui-ci, où la question comprend un exemple vérifiable, c'est une bonne idée d'essayer d'appliquer la solution suggérée et de voir si cela aide réellement .)
Jon Skeet
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.