-ftrivial-auto-var-init : le prix du progrès

Magazine
Marque
MISC
Numéro
127
Mois de parution
mai 2023
Spécialité(s)


Résumé

En C et en C++, les variables déclarées sur la pile sans initialisation n’ont pas de valeur par défaut. Accéder à leur valeur est à la fois une source de comportement indéfini et une source de fuite de données reconnue. De telles situations sont complexes à détecter à la compilation et coûteuses à détecter à l’exécution. Cet article propose d’explorer une alternative disponible dans les compilateurs clang et gcc : -ftrivial-auto-var-init=<value>.


Body

Tout relecteur à l’œil un peu exercé saura trouver (au moins) une erreur dans le code suivant :

#include <stdio.h>
void scan_int(char const* data) {
    int value;
    sscanf(data, "%d", &value);
    printf("value=%d\n", value);
}

Et pourtant ni GCC ni Clang n’affichent d’avertissement à la compilation de cette fonction. Par contre si on l’appelle avec data positionné à "vive misc", on obtient un message d’erreur non équivoque lors d’une exécution dans Valgrind :

==658164== Use of uninitialised value of size 8
==658164==    at 0x48C398B: _itoa_word (_itoa.c:177)
==658164==    by 0x48DF574: __vfprintf_internal (vfprintf-internal.c:1516)
==658164==    by 0x48C9A2E: printf (printf.c:33)
==658164==    by 0x40116F: scan_int (a.c:7)

En effet, on se retrouve à afficher à l’écran une valeur lue depuis la pile, non initialisée, car c’est ce que permet le standard C et que c’est plus rapide de ne rien faire que de positionner une valeur par défaut.

On n’apprendra pas au lecteur de MISC l’impact potentiel d’un tel comportement : la valeur de value va dépendre de ce qui a été écrit sur la pile précédemment, fuitant ainsi de potentielles données importantes (stack cookie, clef ou mot de passe, adresse permettant de deviner la valeur de base utilisée par l’ASLR, etc.).

Ce problème est si fréquent dans les codes C que quelqu’un a eu l’idée assez triviale de forcer une valeur par défaut pour toute variable allouée sur la pile. C’est compatible avec le standard et cela réduit les fuites de données à travers la pile. Mais à quel prix ?

1. Bienvenue -ftrivial-auto-var-init=zero

L’idée parait simple : et si on forçait toutes les allocations sur la pile à zéro ? Après tout c’est déjà le comportement standard pour les variables globales et les variables statiques. Cette approche propose plusieurs avantages : elle est reproductible, elle est rapide et elle est facile à implémenter. En effet, cela revient à écrire dans le code mentionné en introduction :

#include <stdio.h>
void scan_int(char const* data) {
    int value = 0;
    sscanf(data, "%d", &value);
    printf("value=%d\n", value);
}

Et cela suffit à satisfaire Valgrind. Notons néanmoins que le code n’est pas devenu correct (il faudrait vérifier la valeur de retour de sscanf), mais au moins il ne fuite plus d’information.

Cette approche a la bonne idée de parfois avoir un coût nul. Prenons en exemple le code suivant :

#include <string.h>
int len(char const* data) {
    int value = 0;
    if (data) value = strlen(value);
    else value = 0;
    return value;
}

Un compilateur avec les optimisations adéquates (-O1 suffit pour Clang et GCC) saura supprimer l’assignation value = 0, puisque tous les chemins menant à la lecture de value passent par une autre écriture que cette initialisation. Malheureusement, ce n’est pas le cas en général, même quand l’initialisation est inutile (penser par exemple aux conséquences d’un quelconque appel à une fonction définie dans une autre bibliothèque).

Mais cette approche est-elle suffisante ? Il se trouve qu’elle a plusieurs défauts.

Premièrement pour des objets de grande taille alloués sur la pile, l’initialisation s’avère coûteuse. Et de telles initialisations sont plus fréquentes qu’on le penserait : un petit buffer char buffer[256] ou une structure standard de type struct statbuf suffisent.

Deuxièmement, forcer la valeur à zéro introduit un comportement non standard sur lequel on peut avoir envie de se reposer. On pourrait alors voir apparaître une « simplification » du code précédent :

#include <stdio.h>
int len(char const* data) {
    int value;
    if (data) value = strlen(value);
    return value;
}

Qui se comporterait comme la version originale, mais seulement si on utilise le bon drapeau de compilation, ce qui n’est pas franchement une bonne idée.

Troisièmement, la valeur zéro ayant souvent un sens, elle risque de faire en sorte que le programme tombe en marche. Un pointeur à zéro, un flottant à zéro, un entier à zéro, autant de cas qui peuvent être traités normalement par le programme sans déclencher d’alerte.

2. Bienvenue -ftrivial-auto-var-init=pattern

À défaut d’utiliser la valeur zéro, on peut utiliser un motif particulier, qui a de fortes chances de déclencher un comportement inattendu. L’auteur original du patch dans clang pour cette option propose astucieusement le « screaming pattern », à base de 0xAAAAAAAA. C’est une valeur entière incongrue. Si on la préfixe de 0xFFF on obtient un « screaming not a number » flottant (qui a la bonne idée de se propager à travers les opérations arithmétiques, et donc de faire surface plus facilement). Sur 64 bits, cela fait une adresse mémoire généralement non adressable (à moins d’avoir une mémoire de plus de 1e19 octets), etc.

Notons cependant que Linus Torvald préfère l’initialisation à zéro [1].

Niveau implémentation, le compilateur génère systématiquement un appel à memset après chaque allocation, et cet appel est ensuite soit supprimé (meilleur des cas), soit transformé en un ou plusieurs chargements mémoires explicites (cas des petites allocations de taille fixe), soit conservé (cas des grosses allocations ou des allocations à taille dynamique, i.e. appels à alloca ou allocation de variable length array).

3. Bienvenue __attribute__((uninitialized))

Si l’on accepte de passer à -ftrivial-auto-var-init=pattern, on peut vouloir contrôler finement la génération des initialisations, afin d’éviter le surcoût de l’initialisation quand on sait que l’initialisation n’est pas nécessaire, même si le compilateur n’arrive pas à le prouver. Ainsi, un peu à la manière du marqueur unsafe que l’on retrouve en Rust, on peut se contenter d’auditer le code en se concentrant sur les zones marquées explicitement.

Cela se traduit par un attribut porté par des variables initialisées sur la pile : __attribute__((uninitialized)) ou (en C++) [[clang::uninitialized]]. Cette approche est incrémentale, portable et permet d’avoir le meilleur des deux mondes.

Par exemple, dans le cas suivant le compilateur ne sait pas que lstat va correctement initialiser la variable sb si la valeur de retour est différente de -1, mais après une relecture minutieuse du code, le développeur peut se permettre de forcer la non-initialisation de cette variable :

#include <sys/stat.h>
int main(int argc, char ** argv) {
    __attribute__((uninitialized)) struct stat sb;
if (lstat(argv[1], &sb) == -1) {
    perror("lstat");
    exit(EXIT_FAILURE);
}
/* do stuff with sb */
return 0;
}

4. Vers une évolution du standard ?

L’auteur du patch dans clang/LLVM a récemment (novembre 2022) proposé une évolution du standard C++ [2] pour harmoniser l’initialisation des variables allouées sur la pile avec celles allouées de manière globale : tout à zéro. Cette proposition suggère également la normalisation d’une porte de sortie à travers l’attribut [[uninitialized]].

Prévoyant une levée de boucliers pour des raisons de performance, en vertu du principe qu’on ne paie que pour ce que l’on demande, l’auteur de la proposition avance que d’une, les performances des options actuelles sont déjà très bonnes en pratique, et que de deux, les compilateurs ont encore une belle marge de manœuvre dans leur capacité à supprimer les initialisations inutiles, avec nombre patch à l’appui.

On pourrait rétorquer que la situation actuelle est déjà satisfaisante, et que si l’on veut explicitement initialiser une variable à zéro, un appel à memset suffit. En termes de posture, l’auteur défend une approche sécurisée par défaut, dont on peut se désengager localement pour des raisons de performance, alors que l’état actuel est une approche rapide par défaut dont on peut se désengager localement pour des raisons de sécurité.

Conclusion

L’utilisation de -ftrivial-auto-var-init=pattern combinée à __attribute__((uninitialized)) permet au développeur soucieux d’éviter les fuites de données à travers la lecture de variables de pile non initialisées. Le surcoût est généralement négligeable et quand ce n’est pas le cas, on peut le contrôler finement. Cette option est utilisée par défaut dans le noyau Linux.

Références

[0] RFC sur la liste de discussion de clang/llvm :
https://lists.llvm.org/pipermail/cfe-dev/2018-November/060172.html

[1] Fil de discussion sur le sujet sur la liste du noyau : https://lkml.org/lkml/2019/7/28/194

[2] Proposition d’évolution du standard C++ : https://isocpp.org/files/papers/P2723R0.html



Article rédigé par

37 articles

Par le(s) même(s) auteur(s)

Garder ses parties privées

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

Il y a 19 ans, Ulrich Drepper, alors développeur chez Red Hat et un des contributeurs principaux de la glibc, ajoutait au changelog de la glibc la ligne suivante : sysdeps/i386/bsd-_setjmp.S: Use PTR_MANGLE for PC if defined, inaugurant ainsi l’arrivée de la protection des pointeurs de fonction stockés dans des structures internes à la glibc.

Des soucis à la chaîne

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

L’histoire, ou plutôt l’Histoire, est une coquine. Et quand Dennis Ritchie et Ken Thompson inventent le langage C en 1972, ils prennent une décision anodine, une micro-optimisation qui fait gagner quelques octets, mais qui aura un impact important sur la sécurité de nombreux systèmes : en C, les chaînes de caractères sont terminées par un octet positionné à zéro.

NouveauLes derniers articles Premiums

Nouveau Les derniers articles Premium

Bun.js : l’alternative à Node.js pour un développement plus rapide

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Dans l’univers du développement backend, Node.js domine depuis plus de dix ans. Mais un nouveau concurrent fait de plus en plus parler de lui, il s’agit de Bun.js. Ce runtime se distingue par ses performances améliorées, sa grande simplicité et une expérience développeur repensée. Peut-il rivaliser avec Node.js et changer les standards du développement JavaScript ?

PostgreSQL au centre de votre SI avec PostgREST

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Dans un système d’information, il devient de plus en plus important d’avoir la possibilité d’échanger des données entre applications. Ce passage au stade de l’interopérabilité est généralement confié à des services web autorisant la mise en œuvre d’un couplage faible entre composants. C’est justement ce que permet de faire PostgREST pour les bases de données PostgreSQL.

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 70 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous