Aikido

Le 10 regole di codifica della NASA per il codice critico per la sicurezza

Introduzione

Il software critico per la sicurezza, come quello utilizzato nei veicoli spaziali o nei sistemi automobilistici, richiede un codice estremamente affidabile. Per far fronte a questa esigenza, il Jet Propulsion Laboratory della NASA ha creato nel 2006 le regole di codifica
‍"Power of 10". Queste concise linee guida eliminano i costrutti C complessi e difficili da analizzare, garantendo che il codice rimanga semplice, verificabile e affidabile.

Oggi, strumenti come la qualità del codice di Aikido possono essere configurati con controlli personalizzati per applicare tutte e dieci le regole su ogni nuova pull request. In questo articolo spieghiamo ogni regola, perché è importante e forniamo esempi di codice che mostrano sia gli approcci errati che quelli corretti.

Perché queste regole sono importanti

Le regole della NASA si concentrano sulla leggibilità, sull'analizzabilità e sull'affidabilità, elementi essenziali per applicazioni mission-critical come il controllo di veicoli spaziali e il software di volo. Vietando costrutti C oscuri e imponendo controlli difensivi, le linee guida rendono più semplice la revisione del codice e la verifica della sua correttezza. Esse integrano standard come MISRA C affrontando pattern che gli analizzatori statici spesso non colgono. Ad esempio, evitare la ricorsione e la memoria dinamica mantiene prevedibile l'utilizzo delle risorse, mentre l'imposizione di controlli sui valori restituiti aiuta a individuare molti bug in fase di compilazione.

In effetti, lo studio della NASA su un sistema embedded di largo consumo, come il firmware del corpo farfallato elettronico di Toyota, ha rilevato centinaia di violazioni delle regole. Ciò dimostra che i progetti reali incorrono spesso negli stessi problemi che queste regole mirano a prevenire. Ciascuna regola nell'elenco previene una classe di errori comuni (loop non controllati, dereferenziazioni di puntatori a null, effetti collaterali invisibili, ecc.). Ignorarle può portare a lievi guasti in fase di esecuzione, a problemi di sicurezza o a un comportamento non deterministico. Al contrario, aderire a tutte e dieci le regole rende la verifica statica molto più gestibile.

Gli strumenti automatizzati sono importanti. Le piattaforme di qualità del codice possono essere configurate per rilevare costrutti o pattern proibiti. Queste regole vengono eseguite automaticamente su ogni pull request, intercettando i problemi prima che il codice venga unito.

Contestualizzare le regole

Prima di addentrarsi nelle singole regole, è importante comprendere il contesto:

  • Linguaggio di destinazione: Le regole "Power of 10" della NASA sono state scritte per il C, un linguaggio con un forte supporto di strumenti (compilatori, analizzatori, debugger) ma anche noto per i comportamenti indefiniti. Presuppongono l'assenza di garbage collection o di gestione avanzata della memoria. Utilizzando esclusivamente un C semplice e ben strutturato, è possibile sfruttare l'analisi statica per dimostrare le proprietà del programma.
  • Analisi statica: Molte regole esistono per semplificare i controlli automatizzati. Ad esempio, vietare la ricorsione (Regola 1) e richiedere i limiti dei loop (Regola 2) consente agli strumenti di dimostrare il numero di iterazioni o l'utilizzo dello stack di qualsiasi funzione. Allo stesso modo, vietare macro complesse e limitare i puntatori (Regole 8–9) rende i pattern di codice espliciti anziché nascosti nella magia del preprocessore o in indirection multiple.
  • Flusso di lavoro di sviluppo: Nelle moderne pipeline DevSecOps, queste regole diventano parte dei controlli CI. Gli strumenti di qualità del codice possono integrarsi con GitHub, GitLab o Bitbucket per esaminare ogni pull request e rilevare sia problemi semplici che pattern più complessi. È possibile creare una regola personalizzata per ciascuna linea guida della NASA, come "segnala qualsiasi uso di goto o chiamate di funzioni ricorsive" o "assicurati che ogni loop abbia un limite letterale". Una volta configurate, queste regole vengono applicate automaticamente in ogni futura scansione del codice, intercettando le violazioni in anticipo e fornendo indicazioni su come risolverli.

In breve, le 10 regole della NASA incarnano una programmazione C difensiva e analizzabile. Di seguito elenchiamo ciascuna regola, mostriamo esempi di codice corretto e scorretto, e spieghiamo perché la regola esiste e quali rischi mitiga.‍

Le 10 regole di codifica della NASA

1. Evitare un flusso di controllo complesso.

Non utilizzare goto, setjmp o longjmp ed evita di scrivere funzioni ricorsive in qualsiasi parte del codice.

‍❌ Esempio non conforme

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

✅ Esempio conforme (utilizza un loop)

// 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;
}

Perché questo è importante: La ricorsione e i goto creano un flusso di controllo non lineare difficile da analizzare. Le chiamate ricorsive rendono il grafo delle chiamate ciclico e la profondità dello stack illimitata; il comando goto crea codice spaghetti. Utilizzando loop semplici e codice lineare, un analizzatore statico può verificare facilmente l'utilizzo dello stack e i percorsi del programma. La violazione di questa regola potrebbe portare a inaspettati stack overflow o a percorsi logici difficili da verificare manualmente.

2. I loop devono avere limiti superiori fissi.

Ogni loop dovrebbe disporre di un limite verificabile in fase di compilazione.

❌ Esempio non conforme (loop illimitato):

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

✅ Esempio conforme (loop con limite fisso):

// 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]);
}

Perché questo è importante: I loop illimitati possono essere eseguiti all'infinito o superare i limiti delle risorse. Con un limite fisso, gli strumenti possono dimostrare staticamente il numero massimo di iterazioni. Nei sistemi safety-critical, l'assenza di un limite potrebbe causare un loop fuori controllo. Imponendo un limite esplicito (o una dimensione di array statica), ci assicuriamo che i loop terminino in modo prevedibile. Senza questa regola, un errore nella logica del loop potrebbe non essere scoperto fino alla fase di distribuzione (ad esempio, un errore di tipo off-by-one che provoca un loop infinito).

3. Nessuna memoria dinamica dopo l'inizializzazione.

Evita malloc/free o qualsiasi uso dell'heap nel codice in esecuzione; utilizza solo allocazioni fisse o sullo stack.

❌ Esempio non conforme (utilizza 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);
}

✅ Esempio conforme (allocazione statica)

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

Perché è importante: L'allocazione dinamica della memoria a runtime può portare a comportamenti imprevedibili, frammentazione della memoria o errori di allocazione, specialmente in sistemi con risorse limitate come veicoli spaziali o controller embedded. Se malloc o free fallono a metà missione, il software potrebbe bloccarsi o comportarsi in modo anomalo. L'uso esclusivo di memoria a dimensione fissa o allocata sullo stack garantisce un comportamento deterministico, semplifica la convalida e previene i memory leak a runtime.

4. Le funzioni rientrano in una sola pagina (~60 righe).

Mantenere ogni funzione breve (all'incirca ≤ 60 righe).

❌ Esempio non conforme

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

✅ Esempio conforme (funzioni modulari)

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

Perché è importante: Funzioni estremamente lunghe sono difficili da comprendere, testare e verificare come un'unità. Limitando ogni funzione a un singolo compito concettuale (e all'interno di una pagina stampata), le code review e i controlli statici diventano gestibili. Se una funzione si estende su troppe righe, è facile tralasciare errori logici o condizioni limite. Suddividere il codice in funzioni più piccole migliora la leggibilità e facilita l'applicazione di altre regole (come la densità di asserzioni e i controlli sui valori di ritorno per funzione).

5. Utilizzare almeno due istruzioni assert per funzione.

Ogni funzione dovrebbe eseguire controlli difensivi.

❌ Esempio non conforme (senza asserzioni):

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

✅ Esempio conforme (con asserzioni):

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];
}

Perché è importante: Le asserzioni rappresentano la prima linea di difesa contro condizioni non valide. La NASA ha scoperto che una maggiore densità di asserzioni aumenta significativamente la probabilità di individuare bug. Con almeno due assert per funzione (che verificano precondizioni, limiti e invarianti), il codice documenta implicitamente le proprie assunzioni e segnala immediatamente le anomalie durante i test. Senza le asserzioni, un valore imprevisto potrebbe propagarsi silenziosamente, causando guasti lontani dall'origine dell'errore.

6. Dichiarare i dati con uno scope minimo.

Mantenere le variabili il più locali possibile; evitare le variabili globali.

❌ Esempio non conforme (dati globali):

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

✅ Esempio conforme (scope locale):

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

Perché è importante: Ridurre al minimo lo scope diminuisce l'accoppiamento e le interazioni indesiderate. Se una variabile è necessaria solo all'interno di una funzione, dichiararla come globale espone al rischio che altro codice la modifichi inaspettatamente. Mantenendo i dati locali, ciascuna funzione diventa più autonoma e priva di effetti collaterali, semplificando l'analisi e i test. Le violazioni (come il riutilizzo di uno stato globale) possono causare bug difficili da individuare a causa di aliasing o modifiche impreviste.

7. Controllare tutti i valori di ritorno e i parametri delle funzioni.

Il chiamante deve esaminare ogni valore di ritorno non void; ogni funzione deve convalidare i propri parametri di input.

❌ Esempio non conforme (ignora il valore di ritorno)

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
}

✅ Esempio 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
}

Perché è importante: Ignorare i valori di ritorno o i parametri non validi è una delle principali cause di bug. Ad esempio, non controllare malloc potrebbe portare alla dereferenziazione di un puntatore a null. Allo stesso modo, la mancata convalida degli input (come indici di array o stringhe di formato) può causare buffer overflow o arresti anomali. La NASA richiede che ogni valore di ritorno venga gestito (o esplicitamente castato a void per segnalare l'intenzione) e che ogni argomento venga verificato. Questo approccio onnicomprensivo assicura che nessun errore venga ignorato silenziosamente.

8. Limitare il preprocessore a include e macro semplici.

Evitare macro complesse o trucchi di compilazione condizionale.

❌ Esempio non conforme (macro complessa):

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

DECLARE_FUNC(init);  // Espande a: void func_init(void)

✅ Esempio conforme (macro semplici / inline):

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

Perché è importante: Le macro complesse (specialmente quelle multilinea o simili a funzioni) possono nascondere la logica, confondere il flusso di controllo e ostacolare l'analisi statica. Limitare il preprocessore a compiti banali (ad es. costanti e intestazioni) mantiene il codice esplicito. Ad esempio, sostituire le macro con funzioni inline migliora il controllo dei tipi e la manutenibilità in fase di debug. Senza questa regola, sottili bug di espansione delle macro o errori di compilazione condizionale potrebbero sfuggire inosservati durante le revisioni.

9. Limitare l'uso dei puntatori.

Limitare l'indirezione a un solo livello: evita `int**` e puntatori a funzione.

❌ Esempio non conforme (indirezione multipla):

// Non conforme: doppio puntatore e puntatore a funzione
int **doublePtr;
int (*funcPtr)(int) = someFunction;

✅ Esempio conforme (puntatore singolo):

// Conforme: puntatore a livello singolo, nessun puntatore a funzione
int *singlePtr;
// Usa una chiamata esplicita invece di un puntatore a funzione
int result = someFunction(5);

Perché è importante: Livelli multipli di puntatori e puntatori a funzione complicano il flusso dei dati e rendono difficile capire quale memoria o codice venga effettivamente accessibile. Gli analizzatori statici devono risolvere ogni indirezione, operazione che in generale può risultare indecidibile. Limitandosi a riferimenti con un solo puntatore, il codice rimane più semplice e sicuro. La violazione di questa regola può portare a un aliasing poco chiaro (un puntatore che modifica i dati attraverso un altro) o a un comportamento inatteso dei callback, entrambi rischiosi in contesti critici per la sicurezza.

10. Compilare abilitando tutti gli avvisi e risolverli.

Abilitare ogni avviso del compilatore e risolverlo prima del rilascio.

❌ Esempio non conforme (codice con avvisi)

// 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

✅ Esempio conforme (compilazione pulita)

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

Perché è importante: Gli avvisi del compilatore segnalano spesso veri e propri bug (come variabili non inizializzate, disallineamenti di tipo o assegnazioni non intenzionali). La regola della NASA impone che nessun avviso venga ignorato. Prima di qualsiasi rilascio, il codice dovrebbe essere compilato senza avvisi con le impostazioni di massima verbosità. Questa pratica intercetta precocemente molti errori banali. Se un avviso non può essere risolto, il codice deve essere ristrutturato o documentato in modo che l'avviso non si verifichi affatto.

Ognuna di queste regole elimina una categoria di errori nascosti. Se seguite insieme, rendono il codice C notevolmente più prevedibile e verificabile.

Conclusione

Le 10 regole della NASA (il "Power of 10") forniscono uno standard di codifica chiaro ed efficace per il software C critico. Evitando costruttori complessi e imponendo controlli, riducono la probabilità di bug nascosti e rendono fattibile l'analisi statica. Nello sviluppo moderno, queste linee guida possono essere automatizzate con strumenti di qualità del codice. È possibile definire regole personalizzate per segnalare qualsiasi violazione delle linee guida della NASA, eseguendo tali controlli su ogni pull request per fornire un feedback immediato agli sviluppatori.

Adottare precocemente questi controlli porta a un codice più sicuro, di qualità superiore e più facile da mantenere. Anche al di fuori dell'aerospazio, i principi rimangono validi: funzioni piccole e chiare, cicli espliciti, programmazione difensiva e nessun virtuosismo spericolato con i puntatori. Seguire e automatizzare queste regole con uno strumento di qualità del codice aiuta il tuo team a individuare tempestivamente gli errori e a rilasciare software più affidabile.

FAQ

Hai domande?

Le regole della NASA sono dedicate solo allo spazio o ai progetti embedded?

Assolutamente no. Queste regole sono nate in un contesto critico per la sicurezza, ma sono ampiamente generalizzabili. Qualsiasi progetto C che attribuisca valore alla manutenibilità e all'affidabilità può trarne beneficio. Di fatto, le regole integrano standard industriali come MISRA C. Molti sviluppatori esterni alla NASA hanno constatato che applicare anche solo un sottoinsieme di queste linee guida migliora la qualità del codice.

Come posso applicare automaticamente queste regole?

Usa uno strumento di analisi statica o di code-review. Lo strumento Code Quality di Aikido Security ti permette di creare regole personalizzate. Puoi scrivere una piccola regola per ogni linea guida – ad esempio una che segnali l'uso di qualsiasi `goto` o qualsiasi funzione più lunga di 60 righe – e salvarla in Aikido. Aikido verificherà quindi ogni nuova pull request rispetto alle tue regole personalizzate, bloccando i merge in caso di violazione. Questo si integra perfettamente con GitHub, GitLab, Bitbucket e altri.

Perché devo evitare la memoria dinamica e la ricorsione?

Gli allocatori di memoria dinamica (come `malloc`) possono fallire o comportarsi in modo imprevedibile, mentre la ricorsione non gestita rende l'uso dello stack illimitato. Nel software critico, spesso è necessario dimostrare i limiti delle risorse e gestire i casi peggiori. Vietando `malloc` e ricorsione in fase di esecuzione, si obbliga a conoscere preventivamente tutta la memoria e la profondità delle chiamate. Ciò previene bug classici come memory leak, overflow o stack overflow, particolarmente pericolosi quando sono in gioco vite umane o apparecchiature multimilionarie.

Cosa succede se il mio progetto deve violare una di queste regole?

Le linee guida della NASA sono rigorose per definizione. Se devi discostartene (ad esempio, utilizzando un piccolo buffer dinamico), dovresti farlo in modo consapevole: documenta l'eccezione, giustificala e, se possibile, aggiungi controlli a livello di runtime. Alcuni team preferiscono considerare alcune regole come avvisi anziché errori, ma l'approccio più sicuro consiste nel rifattorizzare il codice per renderlo conforme. Le regole della NASA sono conservative, ma è proprio per questo che funzionano. Se usi Aikido o un altro strumento, puoi contrassegnare una regola come a bassa priorità, ma è comunque consigliabile risolvere il problema alla radice.

Aikido è in grado di distinguere le violazioni delle regole NASA dagli altri problemi?

Sì. Le regole di Aikido sono personalizzabili e dotate di tag. Puoi etichettare le tue regole personalizzate come "Regola NASA 1", "Regola NASA 2" e così via, in modo che le violazioni mostrino chiaramente quale linea guida è stata infranta. Aikido tiene inoltre traccia delle metriche nel tempo, consentendoti di visualizzare indicatori come il "tasso di conformità alle regole NASA" nell'intera codebase. Questa tracciabilità aiuta i team a dare priorità alle correzioni e a dimostrare la conformità durante gli audit.

Metti in sicurezza ora

Metti in sicurezza il tuo codice, il cloud e il runtime in un unico sistema centralizzato.
Trova e risolvi le vulnerabilità rapidamente e automaticamente.

Nessuna carta di credito richiesta | Risultati della scansione in 32 secondi.