Aikido

Les 10 règles de codage de la Nasa pour le code critique pour la sécurité

Introduction

Les logiciels critiques pour la sécurité, tels que ceux utilisés dans les engins spatiaux ou les systèmes automobiles, nécessitent un code extrêmement fiable. Pour y remédier, le Jet Propulsion Laboratory de la NASA a créé les règles de codage
"Power of 10" en 2006. Ces directives concises éliminent les constructions C complexes difficiles à analyser et garantissent que le code reste simple, vérifiable et fiable.

Aujourd'hui, des outils tels que le Aikido de qualité Aikido peuvent être configurés avec des vérifications personnalisées afin d'appliquer les dix règles à chaque nouvelle demande d'extraction. Dans cet article, nous expliquons chaque règle, son importance, et fournissons des exemples de code illustrant les approches incorrectes et correctes.

Pourquoi ces règles sont importantes

Les règles de la NASA se concentrent sur la lisibilité, l'analysabilité et la fiabilité, des aspects essentiels pour les applications critiques telles que le contrôle des engins spatiaux et les logiciels de vol. En interdisant les constructions C obscures et en imposant des contrôles défensifs, ces directives facilitent la révision du code et la preuve de sa correction. Elles complètent des standards comme MISRA C en abordant des schémas que les analyseurs statiques manquent souvent. Par exemple, éviter la récursion et la mémoire dynamique maintient une utilisation prévisible des ressources, tandis que l'application de contrôles de valeurs de retour aide à détecter de nombreux bugs à la compilation.

En fait, l'étude de la NASA sur un système embarqué grand public, tel que le firmware de l'accélérateur électronique de Toyota, a révélé des centaines de violations de règles. Cela montre que les projets réels rencontrent souvent les mêmes problèmes que ces règles sont conçues pour prévenir. Chaque règle de la liste prévient une classe d'erreurs courantes (boucles incontrôlées, déréférencements de pointeurs nuls, effets secondaires invisibles, etc.). Les ignorer peut entraîner des défaillances subtiles au runtime, des failles de sécurité ou un comportement non déterministe. En revanche, l'adhésion à l'ensemble des dix règles rend la vérification statique beaucoup plus gérable.

Les outils automatisés sont importants. Les plateformes de qualité du code peuvent être configurées pour détecter les constructions ou les modèles interdits. Ces règles s'exécutent automatiquement à chaque demande d'extraction, détectant les problèmes avant que le code ne soit fusionné.

Relier le contexte aux règles

Avant d'aborder les règles individuelles, il est important de comprendre le contexte :

  • Langage cible : Les règles « Power of 10 » de la NASA ont été écrites pour le C, un langage bénéficiant d'un support d'outils étendu (compilateurs, analyseurs, débogueurs) mais également réputé pour ses comportements indéfinis. Elles supposent aucune collecte de déchets (garbage collection) ni gestion avancée de la mémoire. En utilisant uniquement un C simple et bien structuré, on peut exploiter l'analyse statique pour prouver les propriétés du programme.
  • Analyse Statique : De nombreuses règles existent pour faciliter les vérifications automatisées. Par exemple, interdire la récursion (Règle 1) et exiger des bornes de boucle (Règle 2) permet aux outils de prouver le nombre d'itérations ou l'utilisation de la pile qu'une fonction peut avoir. De même, interdire les macros complexes et limiter les pointeurs (Règles 8-9) rend les modèles de code explicites plutôt que cachés dans la magie du préprocesseur ou des indirections multiples.
  • Workflow de développement : dans DevSecOps modernes, ces règles font partie intégrante des contrôles CI. Les outils de qualité du code peuvent s'intégrer à GitHub, GitLab ou Bitbucket pour examiner chaque pull request et détecter à la fois les problèmes simples et les schémas plus complexes. Vous pouvez créer une règle personnalisée pour chaque directive de la NASA, telle que « signaler toute utilisation de goto ou d'appels de fonctions récursives » ou « s'assurer que chaque boucle a une limite littérale ». Une fois configurées, ces règles sont automatiquement appliquées à chaque analyse de code ultérieure, ce qui permet de détecter rapidement les violations et de fournir des conseils sur la manière de les corriger.

En bref, les 10 règles de la NASA incarnent une programmation C défensive et analysable. Ci-dessous, nous listons chaque règle, montrons à quoi ressemblent un bon et un mauvais code, et expliquons pourquoi la règle existe et quels risques elle atténue.

Les 10 règles de codage de la NASA

1. Évitez les flux de contrôle complexes.

Ne pas utiliser goto, setjmp, ou longjmp, éviter d'écrire des fonctions récursives dans n'importe quelle partie du code.

Exemple non conforme

// Non-compliant: recursive function call
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n-1);   // recursion (direct)
}

Exemple conforme (utilise une boucle)

// Compliant: uses an explicit loop instead of recursion
int factorial(int n) {
    int result = 1;
    for (int i = n; i > 1; --i) {
        result *= i;
    }
    return result;
}

Pourquoi c'est important: La récursivité et les 'goto' créent un flux de contrôle non linéaire difficile à appréhender.  Les appels récursifs rendent le graphe d'appels cyclique et la profondeur de pile illimitée ; les 'goto' créent du code spaghetti. En utilisant des boucles simples et du code linéaire, un analyseur statique peut facilement vérifier l'utilisation de la pile et les chemins d'exécution du programme. Violer cette règle pourrait entraîner des débordements de pile inattendus ou des chemins logiques difficiles à réviser manuellement.

2. Les boucles doivent avoir des bornes supérieures fixes.

Chaque boucle devrait avoir une limite vérifiable à la compilation.

Exemple non conforme (boucle non bornée) :

// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
    doSomething(array[i]);
    i++;
}

Exemple conforme (boucle à borne fixe) :

// Compliant: loop with explicit fixed upper bound and assert
#define MAX_LEN 100
for (int i = 0; i < MAX_LEN; i++) {
    if (array[i] == 0) break;
    doSomething(array[i]);
}

Pourquoi c'est important : Les boucles non bornées peuvent s'exécuter indéfiniment ou dépasser les limites de ressources.  Avec une borne fixe, les outils peuvent prouver statiquement le nombre maximal d'itérations. Dans les systèmes critiques pour la sécurité, une borne manquante pourrait provoquer une boucle incontrôlable. En imposant une limite explicite (ou une taille de tableau statique), nous nous assurons que les boucles se terminent de manière prévisible.  Sans cette règle, une erreur dans la logique de la boucle pourrait ne pas être détectée avant le déploiement (par exemple, une erreur de décalage qui provoque une boucle infinie).

3. Pas de mémoire dynamique après l'initialisation.

Évitez malloc/free ou toute utilisation du heap dans le code en exécution ; utilisez uniquement l'allocation fixe ou sur le stack.

Exemple non conforme (utilise malloc)

// Non-compliant: dynamic allocation inside the code
void storeData(int size) {
    int *buffer = malloc(size * sizeof(int));
    if (buffer == NULL) return;
    // ... use buffer ...
    free(buffer);
}

Exemple conforme (allocation statique)

// Compliant: fixed-size array on stack or global
#define MAX_SIZE 256
void storeData() {
    int buffer[MAX_SIZE];
    // ... use buffer without dynamic alloc ...
}

Pourquoi c'est important : L'allocation dynamique de mémoire pendant l'exécution (runtime) peut entraîner un comportement imprévisible, une fragmentation de la mémoire ou des échecs d'allocation, en particulier dans les systèmes à ressources limitées comme les engins spatiaux ou les contrôleurs embarqués. Si malloc ou free échoue en cours de mission, le logiciel pourrait planter ou se comporter de manière imprévisible. L'utilisation de mémoire de taille fixe ou allouée sur la pile garantit un comportement déterministe, simplifie la validation et prévient les fuites de mémoire runtime.

4. Les fonctions tiennent sur une seule page (~60 lignes).

Gardez chaque fonction courte (environ ≤ 60 lignes).

Exemple non conforme

// Non-compliant: hundreds of lines in one function (not shown)
void processAllData() {
    // ... imagine 100+ lines of code doing many tasks ...
}

Exemple conforme (fonctions modulaires)

// Compliant: break the task into clear sub-functions
void processAllData() {
    preprocessData();
    analyzeData();
    postprocessData();
}
void preprocessData() { /* ... */ }
void analyzeData()   { /* ... */ }
void postprocessData(){ /* ... */ }

Pourquoi c'est important : Les fonctions extrêmement longues sont difficiles à comprendre, à tester et à vérifier en tant qu'unité. En limitant chaque fonction à une seule tâche conceptuelle (et à une page imprimée), les revues de code et les vérifications statiques deviennent gérables. Si une fonction s'étend sur trop de lignes, des erreurs logiques ou des conditions limites peuvent être manquées. Diviser le code en fonctions plus petites améliore la clarté et facilite l'application d'autres règles (comme la densité d'assertions et les vérifications de retour par fonction).

5. Utiliser au moins deux assertions par fonction.

Chaque fonction devrait effectuer des vérifications défensives.

Exemple non conforme (pas d'assertions) :

int get_element(int *array, size_t size, size_t index) {
return array[index];
}

Exemple conforme (avec assertions) :

int get_element(int *array, size_t size, size_t index) {
    assert(array != NULL);        // Assertion 1: pointer validity
    assert(index < size);          // Assertion 2: bounds check
    
    if (array == NULL) return -1;  // Recovery: return error
    if (index >= size) return -1;  // Recovery: return error
    
    return array[index];
}

Pourquoi c'est important : Les assertions sont la première ligne de défense contre les conditions invalides. La NASA a constaté qu'une densité d'assertions plus élevée augmente significativement les chances de détecter les bugs. Avec au moins deux assertions par fonction (vérifiant les préconditions, les limites, les invariants), le code auto-documente ses hypothèses et signale immédiatement les anomalies pendant les tests. Sans assertions, une valeur inattendue pourrait se propager silencieusement, provoquant une défaillance loin de la source de l'erreur.

6. Déclarer les données avec une portée minimale.

Maintenez les variables aussi locales que possible ; évitez les variables globales.

Exemple non conforme (données globales) :

// Non-compliant: global variable visible everywhere
int statusFlag;
void setStatus(int f) {
    statusFlag = f;
}

Exemple conforme (portée locale) :

// Compliant: local variable inside function
void setStatus(int f) {
    int statusFlag = f;
    // ... use statusFlag only here ...
}

Pourquoi c'est important : Minimiser la portée réduit le couplage et les interactions involontaires.  Si une variable n'est nécessaire qu'au sein d'une fonction, la déclarer globalement risque que d'autres parties du code la modifient de manière inattendue.  En gardant les données locales, chaque fonction devient plus autonome et exempte d'effets secondaires, ce qui simplifie l'analyse et les tests.  Les violations (comme la réutilisation d'un état global) peuvent entraîner des bugs difficiles à trouver en raison d'aliasing ou de modifications inattendues.

7. Vérifier toutes les valeurs de retour et les paramètres des fonctions.

L'appelant doit examiner chaque valeur de retour non-void ; chaque fonction doit valider ses paramètres d'entrée.

❌ Exemple non conforme (ignore la valeur de retour)

int bad_mission_control(int velocity, int time) {
    int distance;
    calculate_trajectory(velocity, time, &distance);  // Didn't check!
    return distance;  // Could be garbage if calculation failed
}

Exemple conforme

int good_mission_control(int velocity, int time) {
    int distance;
    int status = calculate_trajectory(velocity, time, &distance);
    
    if (status != 0) {  // Checked the return value
        return -1;  // Propagate error to caller
    }
    
    return distance;  // Safe to use
}

Pourquoi c'est important : Ignorer les valeurs de retour ou les paramètres invalides est une source majeure de bugs. Par exemple, ne pas vérifier malloc peut entraîner une déréférence de pointeur nul. De même, ne pas valider les entrées (par exemple, les indices de tableau ou les chaînes de format) peut provoquer des dépassements de tampon ou des plantages. La NASA exige que chaque retour soit géré (ou explicitement casté en void pour signaler l'intention), et que chaque argument soit vérifié. Cette approche globale garantit qu'aucune erreur n'est ignorée silencieusement.

8. Limiter le préprocesseur aux includes et aux macros simples.

Évitez les macros complexes ou les astuces de compilation conditionnelle.

Exemple non conforme (macro complexe) :

#define DECLARE_FUNC(name) void func_##name(void)

DECLARE_FUNC(init);  // S'étend à : void func_init(void)

Exemple conforme (macros simples / inline) :

// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256

Pourquoi c'est important : Les macros complexes (en particulier les macros multi-lignes ou de type fonction) peuvent masquer la logique, perturber le flux de contrôle et contrecarrer l'analyse statique. Limiter le préprocesseur aux tâches triviales (par exemple, les constantes et les en-têtes) rend le code explicite. Par exemple, remplacer les macros par des fonctions inline améliore la vérification de type et la débogabilité. Sans cette règle, des bugs subtils d'expansion de macro ou des erreurs de compilation conditionnelle pourraient passer inaperçus lors des revues.

9. Limiter l'utilisation des pointeurs.

Limitez l'indirection à un seul niveau – évitez les int** et les pointeurs de fonction.

Exemple non conforme (indirection multiple) :

// Non conforme : pointeur double et pointeur de fonction
int **doublePtr ;
int (*funcPtr)(int) = someFunction ;

Exemple conforme (pointeur unique) :

// Conforme : pointeur à un niveau, pas de pointeur de fonction
int *singlePtr ;
// Utiliser un appel explicite au lieu d'un pointeur de fonction
int result = someFunction(5) ;

Pourquoi c'est important : Plusieurs niveaux de pointeurs et de pointeurs de fonction compliquent le flux de données et rendent difficile de suivre la mémoire ou le code accédé.  Les analyseurs statiques doivent résoudre chaque indirection, ce qui peut être indécidable en général. En se limitant aux références à pointeur unique, le code reste plus simple et plus sûr.  Violer cette règle peut entraîner un aliasing peu clair (un pointeur modifiant des données via un autre) ou un comportement de callback inattendu, deux situations risquées dans des contextes critiques pour la sécurité.

10. Compiler avec tous les avertissements activés et les corriger.

Activez tous les avertissements du compilateur et corrigez-les avant la publication.

Exemple non conforme (code avec avertissements)

// Non-compliant: code that generates warnings (uninitialized, suspicious assignment)
int x;
if (x = 5) {  // bug: should be '==' or initialize x
    // ...
}
printf("%d\n", x);  // warning: 'x' is used uninitialized

Exemple conforme (compilation propre)

// Compliant: initialize variables and use '==' in condition
int x = 0;
if (x == 5) {
    // ...
}
printf("%d\n", x);

Pourquoi c'est important : Les avertissements du compilateur signalent souvent de véritables bugs (comme des variables non initialisées, des incompatibilités de type ou des affectations involontaires). La règle de la NASA stipule qu'aucun avertissement ne doit être ignoré. Avant toute publication, le code doit compiler sans avertissements avec les paramètres de verbosité maximale. Cette pratique permet de détecter de nombreuses erreurs triviales dès le début. Si un avertissement ne peut être résolu, le code doit être restructuré ou documenté de manière à ce que l'avertissement ne se produise jamais.

Chacune de ces règles élimine une catégorie d'erreurs cachées. Lorsqu'elles sont suivies conjointement, elles rendent le code C beaucoup plus prévisible et vérifiable.

Conclusion

Les 10 règles de la NASA (la « Puissance de 10 ») fournissent une norme de codage claire et efficace pour les logiciels C critiques. En évitant les constructions complexes et en imposant des vérifications, elles réduisent les risques de bugs cachés et rendent l'analyse statique réalisable. Dans le développement moderne, ces directives peuvent être automatisées avec des outils de qualité de code. Des règles personnalisées peuvent être définies pour signaler toute violation des directives de la NASA, et ces règles peuvent s'exécuter sur chaque pull request, fournissant un feedback immédiat aux développeurs.

L'adoption précoce de ces vérifications conduit à un code plus sûr, de meilleure qualité et plus facile à maintenir. Même en dehors de l'aérospatiale, les principes restent valables : fonctions petites et claires, boucles explicites, programmation défensive et pas de gymnastique de pointeurs effrayante. Suivre et automatiser ces règles avec un outil de qualité de code aide votre équipe à détecter les erreurs tôt et à livrer des logiciels plus fiables.

FAQ

Des questions ?

Les règles de la NASA s'appliquent-elles uniquement aux projets spatiaux ou embarqués ?

Pas du tout. Ces règles ont été créées dans un contexte où la sécurité était primordiale, mais elles peuvent être généralisées. Tout projet C qui accorde de l'importance à la maintenabilité et à la fiabilité peut en bénéficier. En fait, ces règles complètent les normes industrielles telles que MISRA C. De nombreux développeurs en dehors de la NASA ont constaté que le simple fait d'appliquer une partie de ces directives améliorait la qualité du code.

Comment appliquer ces règles automatiquement ?

Utilisez un outil d'analyse statique ou de révision de code. L'outil Code Quality Aikido vous permet de créer des règles personnalisées. Vous pouvez rédiger une petite règle pour chaque directive (par exemple, une règle qui signale tout goto ou toute fonction de plus de 60 lignes) et l'enregistrer dans Aikido. Aikido vérifie Aikido chaque nouvelle demande d'extraction par rapport à vos règles personnalisées, bloquant les fusions en cas de violation. Cet outil s'intègre parfaitement à GitHub/GitLab/Bitbucket, etc.

Pourquoi faut-il éviter la mémoire dynamique et la récursion ?

Les allocateurs de mémoire dynamiques (comme malloc) peuvent échouer ou se comporter de manière imprévisible, et la récursion non gérée rend l'utilisation de la pile illimitée. Dans les logiciels critiques, vous devez souvent prouver les limites de ressources et gérer les pires scénarios. En interdisant malloc et la récursion à l'exécution (runtime), vous forcez la connaissance préalable de toute la mémoire et de la profondeur d'appel. Cela prévient les bugs classiques comme les fuites de mémoire, les dépassements (overflow) ou les débordements de pile (stack overflow), qui sont particulièrement dangereux lorsque des vies ou des équipements de plusieurs millions de dollars sont en jeu.

Que se passe-t-il si mon projet doit enfreindre l'une de ces règles ?

Les directives de la NASA sont strictes par nature. Si vous devez vous en écarter (par exemple, en utilisant un petit tampon dynamique), vous devez le faire en toute conscience : documentez l'exception, justifiez-la et ajoutez éventuellement des vérifications d'exécution. Certaines équipes choisissent de traiter certaines règles comme des avertissements plutôt que comme des erreurs, mais l'approche la plus sûre consiste à refactoriser le code pour le rendre conforme. Les règles de la NASA sont conservatrices, mais c'est précisément pour cela qu'elles fonctionnent. Si vous utilisez Aikido un autre outil, vous pouvez marquer une règle comme étant de faible priorité, mais il est tout de même préférable de traiter le problème sous-jacent.

Aikido peut-il Aikido les violations des règles de la NASA des autres problèmes ?

Oui. Les règles Aikidosont personnalisables et peuvent être étiquetées. Vous pouvez nommer vos règles personnalisées « Règle NASA 1 », « Règle NASA 2 », etc., afin que les violations indiquent clairement quelle directive a été enfreinte. Aikido assure Aikido le suivi des analyses au fil du temps, ce qui vous permet de consulter des indicateurs tels que le « taux de conformité aux règles NASA » dans l'ensemble de votre base de code. Cette traçabilité aide les équipes à hiérarchiser les corrections et à démontrer leur conformité lors des audits.

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.