Aikido

As 10 regras de codificação da NASA para código crítico de segurança

Introdução

Softwares de segurança crítica, como os usados em espaçonaves ou sistemas automotivos, exigem código extremamente confiável. Para resolver isso, o Jet Propulsion Laboratory da NASA criou as regras de codificação
"Power of 10" em 2006. Essas diretrizes concisas removem construções complexas em C que são difíceis de analisar e garantem que o código permaneça simples, verificável e confiável.

Hoje, ferramentas como o Aikido code quality podem ser configuradas com verificações personalizadas para aplicar todas as dez regras em cada novo pull request. Neste artigo, explicamos cada regra, por que ela é importante e fornecemos exemplos de código mostrando abordagens incorretas e corretas.

Por que essas regras são importantes

As regras da NASA focam em legibilidade, analisabilidade e confiabilidade, que são essenciais para aplicações de missão crítica, como controle de espaçonaves e software de voo. Ao proibir construções C obscuras e impor verificações defensivas, as diretrizes facilitam a revisão do código e a prova de sua correção. Elas complementam padrões como MISRA C ao abordar padrões que os analisadores estáticos frequentemente perdem. Por exemplo, evitar recursão e memória dinâmica mantém o uso de recursos previsível, enquanto a imposição de verificações de valor de retorno ajuda a detectar muitos bugs em tempo de compilação.

De fato, o estudo da NASA sobre um sistema embarcado de mercado de massa, como o firmware do acelerador eletrônico da Toyota, encontrou centenas de violações de regras. Isso mostra que projetos do mundo real frequentemente encontram os mesmos problemas que essas regras são projetadas para prevenir. Cada regra na lista previne uma classe de erros comuns (loops descontrolados, desreferenciações de ponteiro nulo, efeitos colaterais invisíveis, etc.). Ignorá-las pode levar a falhas sutis em tempo de execução, falhas de segurança ou comportamento não determinístico. Em contraste, aderir a todas as dez regras torna a verificação estática muito mais tratável.

Ferramentas automatizadas são importantes. Plataformas de qualidade de código podem ser configuradas para detectar construções ou padrões proibidos. Essas regras são executadas automaticamente em cada pull request, identificando problemas antes que o código seja mesclado.

Conectando o contexto às regras

Antes de nos aprofundarmos nas regras individuais, é importante entender o contexto:

  • Linguagem Alvo: As regras “Power of 10” da NASA foram escritas para C, uma linguagem com profundo suporte de ferramentas (compiladores, analisadores, depuradores), mas também notória por comportamentos indefinidos. Elas assumem nenhuma coleta de lixo ou gerenciamento avançado de memória. Ao usar apenas C simples e bem estruturado, pode-se alavancar a análise estática para provar propriedades do programa.
  • Análise Estática: Muitas regras existem para facilitar as verificações automatizadas. Por exemplo, proibir a recursão (Regra 1) e exigir limites de loop (Regra 2) permite que as ferramentas provem quantas iterações ou uso de pilha qualquer função pode ter. Da mesma forma, proibir macros complexas e limitar ponteiros (Regras 8–9) torna os padrões de código explícitos, em vez de ocultos em mágica de pré-processador ou múltiplas indireções.
  • Fluxo de Trabalho de Desenvolvimento: Em pipelines DevSecOps modernos, essas regras se tornam parte das verificações de CI. Ferramentas de qualidade de código podem se integrar com GitHub, GitLab ou Bitbucket para revisar cada pull request e detectar tanto problemas simples quanto padrões mais complexos. Você pode criar uma regra personalizada para cada diretriz da NASA, como “sinalizar qualquer uso de goto ou chamadas de função recursivas” ou “garantir que cada loop tenha um limite literal.” Uma vez configuradas, essas regras são aplicadas automaticamente em cada varredura de código futura, detectando violações precocemente e fornecendo orientação sobre como corrigi-las.

Em resumo, as 10 regras da NASA incorporam programação C defensiva e analisável. Abaixo, listamos cada regra, mostramos como são os códigos bons e ruins, e explicamos por que a regra existe e quais riscos ela mitiga.

As 10 regras de codificação da NASA

1. Evite fluxo de controle complexo.

Não use goto, setjmp ou longjmp, evite escrever funções recursivas em qualquer parte do código.

Exemplo Não Conforme

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

Exemplo em conformidade (usa 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;
}

Por que isso importa: Recursão e `gotos` criam um fluxo de controle não linear que é difícil de raciocinar. Chamadas recursivas tornam o grafo de chamadas cíclico e a profundidade da stack ilimitada; `goto` cria código espaguete. Ao usar loops simples e código linear, um analisador estático pode verificar facilmente o uso da stack e os caminhos do programa. Violar essa regra pode levar a estouros de stack inesperados ou caminhos lógicos difíceis de revisar manualmente.

2. Laços devem ter limites superiores fixos.

Todo loop deve ter um limite verificável em tempo de compilação.

Exemplo não conforme (loop ilimitado):

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

Exemplo em conformidade (loop de limite fixo):

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

Por que isso importa: Loops ilimitados (unbounded loops) podem rodar indefinidamente ou exceder os limites de recursos. Com um limite fixo, as ferramentas podem provar estaticamente o número máximo de iterações. Em sistemas de segurança crítica, um limite ausente pode causar um loop descontrolado. Ao impor um limite explícito (ou um tamanho de array estático), garantimos que os loops terminem de forma previsível. Sem essa regra, um erro na lógica do loop pode não ser detectado até a implantação (por exemplo, um erro de 'off-by-one' que causa um loop infinito).

3. Nenhuma memória dinâmica após a inicialização.

Evite `malloc`/`free` ou qualquer uso de heap em código em execução; use apenas alocação fixa ou de pilha.

Exemplo não conforme (usa 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);
}

Exemplo em conformidade (alocação estática)

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

Por que isso importa: A alocação dinâmica de memória em tempo de execução pode levar a comportamento imprevisível, fragmentação de memória ou falhas de alocação, especialmente em sistemas com recursos limitados, como espaçonaves ou controladores embarcados. Se `malloc` ou `free` falharem no meio de uma missão, o software pode travar ou se comportar de forma imprevisível. Usar apenas memória de tamanho fixo ou alocada na stack garante comportamento determinístico, simplifica a validação e previne vazamentos de memória em tempo de execução.

4. Funções cabem em uma página (~60 linhas).

Mantenha cada função curta (aproximadamente ≤ 60 linhas).

Exemplo não conforme

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

Exemplo em conformidade (funções modulares)

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

Por que isso importa: Funções excessivamente longas são difíceis de entender, testar e verificar como uma unidade. Ao manter cada função limitada a uma tarefa conceitual (e dentro de uma página impressa), revisões de código e verificações estáticas se tornam mais gerenciáveis. Se uma função se estende por muitas linhas, erros lógicos ou condições de contorno podem ser perdidos. Dividir o código em funções menores melhora a clareza e facilita a aplicação de outras regras (como densidade de asserções e verificações de retorno por função).

5. Use pelo menos duas declarações assert por função.

Cada função deve realizar verificações defensivas.

Exemplo não conforme (sem asserções):

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

Exemplo em conformidade (com asserções):

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

Por que isso importa: Asserções são a primeira linha de defesa contra condições inválidas. A NASA descobriu que uma maior densidade de asserções aumenta significativamente a chance de encontrar bugs. Com pelo menos duas asserções por função (verificando pré-condições, limites, invariantes), o código autodocumenta suas suposições e sinaliza imediatamente anomalias durante os testes. Sem asserções, um valor inesperado pode se propagar silenciosamente, causando falhas longe da fonte do erro.

6. Declare dados com escopo mínimo.

Manter as variáveis o mais locais possível; evitar globais.

Exemplo não conforme (dados globais):

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

Exemplo em conformidade (escopo local):

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

Por que isso importa: Minimizar o escopo reduz o acoplamento e interações não intencionais. Se uma variável é necessária apenas dentro de uma função, declará-la globalmente arrisca que outro código a altere inesperadamente. Ao manter os dados locais, cada função se torna mais autocontida e livre de efeitos colaterais, o que simplifica a análise e os testes. Violações (como a reutilização de estado global) podem levar a bugs difíceis de encontrar devido a aliasing ou modificações inesperadas.

7. Verifique todos os valores de retorno de função e parâmetros.

O chamador deve examinar cada valor de retorno não-void; cada função deve validar seus parâmetros de entrada.

❌ Exemplo Não Conforme (ignora valor de retorno)

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
}

Exemplo em conformidade

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
}

Por que isso importa: Ignorar valores de retorno ou parâmetros inválidos é uma grande fonte de bugs. Por exemplo, não verificar `malloc` pode levar a uma desreferência de ponteiro nulo. Da mesma forma, não validar entradas (por exemplo, índices de array ou strings de formato) pode causar estouros de buffer ou falhas. A NASA exige que todo retorno seja tratado (ou explicitamente convertido para `void` para sinalizar a intenção), e todo argumento seja verificado. Essa abordagem abrangente garante que nenhum erro seja ignorado silenciosamente.

8. Limite o pré-processador a includes e macros simples.

Evite macros complexas ou truques de compilação condicional.

Exemplo não conforme (marco complexo):

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

DECLARE_FUNC(init);  // Expande para: void func_init(void)

Exemplo em conformidade (macros simples / inline):

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

Por que isso importa: Macros complexas (especialmente macros de várias linhas ou tipo função) podem ocultar lógica, confundir o fluxo de controle e frustrar a análise estática. Limitar o pré-processador a tarefas triviais (por exemplo, constantes e cabeçalhos) mantém o código explícito. Por exemplo, substituir macros por funções inline melhora a verificação de tipo e a depurabilidade. Sem essa regra, bugs sutis de expansão de macro ou erros de compilação condicional podem passar despercebidos nas revisões.

9. Limite o uso de ponteiros.

Limite a indireção a um único nível — evite int** e ponteiros de função.

Exemplo não conforme (indireção múltipla):

// Não conforme: ponteiro duplo e ponteiro de função
int **doublePtr;
int (*funcPtr)(int) = someFunction;

Exemplo em conformidade (ponteiro único):

// Conforme: ponteiro de nível único, sem ponteiros de função
int *singlePtr;
// Use chamada explícita em vez de ponteiro de função
int result = someFunction(5);

Por que isso importa: Múltiplos níveis de ponteiros e ponteiros de função complicam o fluxo de dados e dificultam o rastreamento de qual memória ou código está sendo acessado. Analisadores estáticos devem resolver cada indireção, o que pode ser indecidível em geral. Ao restringir a referências de ponteiro único, o código permanece mais simples e seguro. Violar isso pode levar a um aliasing pouco claro (um ponteiro modificando dados através de outro) ou comportamento de callback inesperado, ambos arriscados em contextos de segurança crítica.

10. Compile com todos os avisos ativados e corrija-os.

Habilite todos os avisos do compilador e resolva-os antes do lançamento.

Exemplo não conforme (código com avisos)

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

Exemplo em conformidade (compilação limpa)

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

Por que isso importa: Avisos do compilador frequentemente sinalizam bugs genuínos (como variáveis não inicializadas, incompatibilidades de tipo ou atribuições não intencionais). A regra da NASA exige que nenhum aviso seja ignorado. Antes de qualquer lançamento, o código deve compilar sem avisos sob configurações de verbosidade máxima. Essa prática detecta muitos erros triviais precocemente. Se um aviso não puder ser resolvido, o código deve ser reestruturado ou documentado para que o aviso nunca ocorra em primeiro lugar.

Cada uma dessas regras elimina uma categoria de erros ocultos. Quando seguidas em conjunto, elas tornam o código C muito mais previsível e verificável.

Conclusão

As 10 regras da NASA (o “Poder de 10”) fornecem um padrão de codificação claro e eficaz para software C crítico. Ao evitar construções complexas e impor verificações, elas reduzem a chance de bugs ocultos e tornam a análise estática viável. No desenvolvimento moderno, essas diretrizes podem ser automatizadas com ferramentas de qualidade de código. Regras personalizadas podem ser definidas para sinalizar qualquer violação das diretrizes da NASA, e essas regras podem ser executadas em cada pull request, fornecendo feedback imediato aos desenvolvedores.

Adotar essas verificações precocemente leva a um código mais seguro, de maior qualidade e mais fácil de manter. Mesmo fora da indústria aeroespacial, os princípios se mantêm: funções pequenas e claras, loops explícitos, programação defensiva e sem malabarismos perigosos com ponteiros. Seguir e automatizar essas regras com uma ferramenta de qualidade de código ajuda sua equipe a identificar erros precocemente e a entregar software mais confiável.

FAQs

Dúvidas?

As regras da NASA são apenas para projetos espaciais ou embarcados?

De forma alguma. Essas regras se originaram em um contexto de segurança crítica, mas se generalizam bem. Qualquer projeto C que valorize a manutenibilidade e a confiabilidade pode se beneficiar. Na verdade, as regras complementam padrões da indústria como o MISRA C. Muitos desenvolvedores fora da NASA descobriram que aplicar mesmo um subconjunto dessas diretrizes melhora a qualidade do código.

Como aplicar essas regras automaticamente?

Use uma ferramenta de análise estática ou revisão de código. A ferramenta de Qualidade de Código da Aikido Security permite criar regras personalizadas. Você pode escrever uma pequena regra para cada diretriz – por exemplo, uma que sinalize qualquer goto ou qualquer função com mais de 60 linhas – e salvá-la no Aikido. O Aikido então verifica cada novo pull request em relação às suas regras personalizadas, bloqueando merges se houver uma violação. Isso se integra perfeitamente com GitHub/GitLab/Bitbucket etc.

Por que devo evitar memória dinâmica e recursão?

Alocadores de memória dinâmica (como malloc) podem falhar ou se comportar de forma imprevisível, e a recursão não gerenciada torna o uso da pilha ilimitado. Em software crítico, você frequentemente deve provar os limites de recursos e lidar com os piores cenários. Ao desabilitar malloc e recursão em tempo de execução (runtime), você força que toda a memória e profundidade de chamada sejam conhecidas antecipadamente. Isso evita bugs clássicos como vazamentos de memória, overflow ou stack overflow, que são particularmente perigosos quando vidas ou equipamentos multimilionários estão em jogo.

E se meu projeto precisar violar uma dessas regras?

As diretrizes da NASA são rigorosas por design. Se você precisar desviar (por exemplo, usando um pequeno buffer dinâmico), você deve fazê-lo conscientemente: documentar a exceção, justificá-la e, possivelmente, adicionar verificações em tempo de execução. Algumas equipes optam por tratar algumas regras como avisos em vez de erros, mas a abordagem mais segura é refatorar o código para conformidade. As regras da NASA são conservadoras, mas é exatamente por isso que funcionam. Se você usar o Aikido ou outra ferramenta, você poderia marcar uma regra como de baixa prioridade, mas ainda assim é melhor abordar o problema subjacente.

O Aikido pode distinguir violações das regras da NASA de outros problemas?

Sim. As regras do Aikido são personalizáveis e podem ser marcadas com tags. Você pode rotular suas regras personalizadas como “Regra NASA 1”, “Regra NASA 2”, etc., para que as violações mostrem claramente qual diretriz foi quebrada. O Aikido também rastreia análises ao longo do tempo, assim você pode ver métricas como a “taxa de conformidade com as regras da NASA” em sua base de código. Essa rastreabilidade ajuda as equipes a priorizar correções e demonstrar conformidade durante auditorias.

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.