Journal C23: un memset_explicit() qui carbure

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
11
21
jan.
2025

Hello nal,

Parmi les propositions mal-aimées de la norme C23, j'invoque memset_explicit().

Bon d'accord, c'est pas si nouveau : pour C11 on avait déjà memset_s(), dont ce n'est que l'évolution à un paramètre près.
Dans les deux cas, le support s'en est trouvé relégué à l'annexe K, c'est-à-dire le morceau de la norme qu'on est "invité à, mais pas obligé" d'implémenter. Concrètement et ironiquement, seuls deux célèbres compilateurs propriétaires l'implémentent ici et  ; on y revient…

La raison fondamentale est qu'on touche ici à une zone grise : le rôle d'un développeur est d'exprimer une intention, le compilateur d'interpréter et éventuellement optimiser. Mais que se passe-t-il quand les deux se mêlent, voire se brouillent ?

Prenons ce code par exemple:

#define PW_LEN 16

char password[PW_LEN+1];
scanf("%PW_LENs", password);
 [...]
memset(password, 0, PW_LEN);
return;

On saisit un mot de passe qu'on chiffre éventuellement, puis on l'utilise (non montré), enfin on le nettoie avec memset() pour qu'il n'en reste pas trace dans la mémoire de la machine hôte. Et après on n'y accède plus jamais. Logique.

Que se dit le compilateur, avec un niveau d'optimisation de type "release" (= dés "-O1" pour GCC) ? "Pas la peine de garder la dernière instruction, elle ne sert à rien et on y gagne !"
Et c'est comme ça qu'on passe par exemple de cet assembleur x86 :

pxor %xmm0, %xmm0
movups %xmm0, (%rsp)

ou alors :

rep stos %al, (%edi)

à… rien du tout.
Si si, tu peux essayer avec cet exemple ; pas une trace. Autrement dit, paie ta mesure de sécurité !

Le problème est ici que l'intention du développeur est déjouée par le compilateur faute de contexte. Et comme le contexte en C c'est inexistant, on espère qu'il soit véhiculé par une fonction… explicite, dont l'implémentation concrète sera malheureusement très variable car hyper-dépendante de la plate-forme matérielle ET logicielle.

Bon après, concrètement en son absence ? Alors presque chacun a sa solution maison qui marche bien tant qu'on reste… à la maison justement. Entre autres :

  • Linux/GlibC a explicit_bzero(), comme FreeBSD d'ailleurs -sauf que ce dernier ne le déclare pas dans le même header, sacré lui !
  • macOS est semi-pro, il implémente la version de C11 mais pas (encore ?) celle de C23 ;
  • un certain OS répandu en a une qui marche bien tant qu'on utilise son compilateur maison. A sa décharge, c'est plutôt l'implémentation OSS de l'équipe d'en face qui laisse à désirer ;
  • Android n'a… juste rien ?

Face à ce chaos ambiant, je te présente :

Mon implémentation multi-plateforme de memset_explicit()

(qui s'appuie en partie sur le bon travail préalable d'un monsieur)

Elle n'est forcément pas parfaite, mais je te garantis que je l'ai essayée partout où j'ai raisonnablement pu.
Elle fait appel à l'annexe K dans les rares cas où c'est dispo, à défaut tente la solution maison, se replie sur un code assembleur x86/ARM si ça convient, et en dernier recours tente de forcer la main du compilateur.

Tu peux essayer les étapes du README, et me dire si tout fonctionne dans ton cas particulier.
Je t'en serai très reconnaissant :-) !

  • # volatile ?

    Posté par  (Mastodon) . Évalué à 7 (+4/-0). Dernière modification le 21 janvier 2025 à 09:00.

    Moi j'aurais utilisé le mot clé volatile, mais peut-être ai-je raté quelque chose ?

    En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

    • [^] # Re: volatile ?

      Posté par  (site web personnel) . Évalué à 4 (+1/-0).

      Bonne question.

      "La première sécurité est la liberté"

    • [^] # Re: volatile ?

      Posté par  (site web personnel) . Évalué à 3 (+1/-0). Dernière modification le 21 janvier 2025 à 09:30.

      Effectivement !
      Et en effet si tu regardes, c'est ce qui est fait ici dans le pire des cas :

      // ici on gère toutes les architectures connues
      #else
      
      static void* (*volatile memset_explicit_unk)(void*, int, size_t) = memset;
      
      #endif
      

      Je dis le "pire", car cela fait une indirection qui tape sur le pointeur de fonction de la lib C.

      Très suffisant bien sûr ! Mais juste potentiellement moins efficace que d'avoir soit :
      - une implémentation locale qui tirerait un assembleur adapté ;
      - son propre assembleur adapté :-)
      et le fait que ça verrouille une petite zone mémoire pour le besoin.

      (tu le sais bien sûr, mais je précise pour les non-initiés : il ne faut pas appliquer "volatile" sur la variable de la zone à effacer -ici "password"- mais sur l'appel de fonction comme ci-dessus)

      • [^] # Re: volatile ?

        Posté par  (Mastodon) . Évalué à 5 (+2/-0). Dernière modification le 21 janvier 2025 à 10:25.

        tu le sais bien sûr […] : il ne faut pas appliquer "volatile" sur la variable de la zone à effacer

        Ah non je le sais pas, j'aurais utilisé volatile sur la variable, et ensuite un memset() des familles. C'est quoi le soucis du coup ?

        Bon évidemment j'aurais vérifié tout ça, mais c'est ce que j'aurais fait en première intention. Apparemment ça ne marche pas ?

        EDIT : En effet memset() ça marche pas, mais du coup j'aurais fait différemment

        En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

        • [^] # Re: volatile ?

          Posté par  (site web personnel) . Évalué à 2 (+0/-0). Dernière modification le 21 janvier 2025 à 10:33.

          En fait, comme le dit @David Demelier ci-dessous (et si j'ai bien interprété sa réponse qui avait l'air en rapport),
          appliquer "volatile" à la variable ne va pas forcément empêcher le compilo de la sortir, comme cité ici (même si je reconnais que c'est capillotracté, et perso je n'avais même pas réussi à le reproduire).

          C'est pour cette raison que, sur le conseil d'un ami qui se reconnaîtra -cf les logs de commit ;-), j'ai appliqué la même logique que la bibliothèque Botan en le mettant plutôt sur le pointeur de fonction.

    • [^] # Re: volatile ?

      Posté par  (site web personnel) . Évalué à 3 (+1/-0).

      La variable volatile n'est pas normalement autorisée à être passée à memset et ne changerait pas forcément le résultat. Le compilateur retire l'appel à memset car il voit qu'il y a rien derrière qui utilise la variable. C'est un mot clé utilisé plutôt en relation avec des accès registres pour éviter des optimisations sur des variables modifiées « à l'extérieur ».

      Je crois qu'en C standard pur (si on a pas memset_explicit) le mieux est de passer la variable à une fonction externe qui fait un memset derrière. Dans ce cas il est possible que le compilateur ne prenne plus trop d'initiatives sans savoir ce que la fonction fait.

      Exemple même avec volatile
      Exemple avec une fonction vite fait

      git is great because linus did it, mercurial is better because he didn't

      • [^] # Re: volatile ?

        Posté par  (Mastodon) . Évalué à 3 (+0/-0).

        Ça marche pas avec un memset, mais tu peux faire la boucle explicite : https://godbolt.org/z/rhzaGTTGj

        En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

      • [^] # Re: volatile ?

        Posté par  (site web personnel) . Évalué à 2 (+0/-0). Dernière modification le 21 janvier 2025 à 10:50.

        Exact David !
        D'ailleurs, et sans rapport direct… au début, la fin du code ressemblait à ça :

            puts("Your password is still in memory... Inspect it now! (press any key...)");
            getchar();
        
            memset(password, 0, 16);
        
            puts("Your password is now cleared! (press any key...)");
            getchar();
            return 0;  
        }
        

        C'est-à-dire qu'il y avait une séquence "puts()/getchar()" supplémentaire entre le memset() et le return final.
        Eh bien dans ce cas-là… le compilateur n'optimise pas non plus ! Car il s'agit de fonctions d'I/O et il considère que ça peut interférer avec les buffers -;). (dit autrement : c'est sensible !)

      • [^] # Re: volatile ?

        Posté par  . Évalué à 3 (+1/-0).

        Euh personnellement je pense qu'il y a un bug du compilateur là..
        Si le compilateur se met a retirer des écritures sur de la mémoire volatile, ça va induire des bugs sur les pilotes!

        • [^] # Re: volatile ?

          Posté par  (site web personnel) . Évalué à 3 (+1/-0).

          // https://en.cppreference.com/w/c/string/byte/memset
          // void *memset( void *dest, int ch, size_t count );
          
          
          int
          main(void)
          {
              volatile char tmp[16];
          
              scanf("%15s", tmp);
              memset(tmp, 0, sizeof (tmp));
          }

          Ben justement. Dans le prototype de memset, dest n’est pas déclaré en volatile et toi même tu ne fais pas d’I/O ou de modification de la variable après le scanf. Du coup, ça ne me parait pas aberrant que le compilateur supprime un appel de fonction qui manifestement ne fait rien.

          Mais ça fait longtemps que j’ai pas fait de C et je suis preneur de tout pointeur vers la standard qui permettrai de trancher le débat dans un sens ou l’autre.

          • [^] # Re: volatile ?

            Posté par  (site web personnel) . Évalué à 2 (+0/-0). Dernière modification le 21 janvier 2025 à 11:45.

            Mes pièces rouges : memset() casterait "volatile char*" en "char*", ce qui retirerait l'effet du "volatile".
            C'est tout simple et cité plusieurs fois sur Stack Overflow entre autres.

            Je viens d'essayer ça :

            memset((volatile char*)password, 0, 16);
            

            et ça m'a affiché :

            memset_explicit.c:90:12: warning: passing argument 1 of ‘memset’ discards ‘volatile’ qualifier from pointer target type [-Wdiscarded-qualifiers]

            Un avis là-dessus ?

            • [^] # Re: volatile ?

              Posté par  (Mastodon) . Évalué à 4 (+1/-0).

              Un avis là-dessus ?

              Utilise pas memset() et fais-le à la main.

              Si vraiment c'est pour quelques octets, je chercherais pas du tout à optimiser quoi que ce soit.

              En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

              • [^] # Re: volatile ?

                Posté par  (site web personnel) . Évalué à 2 (+0/-0).

                Non mais d'accord :-D.
                En ce qui me concerne c'est bien plus une affaire de sécurité !

                Après là j'avoue, c'est surtout un "jeu" de comprendre le détail des choses. L'existant marche quoi qu'il arrive !

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.