Aikido

Las 10 reglas de codificación de la NASA para código crítico para la seguridad

Introducción

El software crítico para la seguridad, como el utilizado en naves espaciales o sistemas automotrices, requiere código extremadamente fiable. Para abordar esto, el Laboratorio de Propulsión a Chorro de la NASA creó las
reglas de codificación "Power of 10" en 2006. Estas directrices concisas eliminan construcciones complejas de C que son difíciles de analizar y aseguran que el código siga siendo simple, verificable y fiable.

Hoy en día, herramientas como Aikido code quality pueden configurarse con comprobaciones personalizadas para aplicar las diez reglas en cada nueva solicitud de extracción. En este artículo, explicamos cada regla, por qué es importante, y proporcionamos ejemplos de código que muestran enfoques incorrectos y correctos.

¿Por qué importan estas reglas?

Las reglas de la NASA se centran en la legibilidad, la capacidad de análisis y la fiabilidad, que son esenciales para aplicaciones de misión crítica como el control de naves espaciales y el software de vuelo. Al prohibir construcciones de C oscuras y aplicar verificaciones defensivas, las directrices facilitan la revisión del código y la prueba de su corrección. Complementan estándares como MISRA C al abordar patrones que los analizadores estáticos a menudo pasan por alto. Por ejemplo, evitar la recursión y la memoria dinámica mantiene el uso de recursos predecible, mientras que la aplicación de verificaciones de valores de retorno ayuda a detectar muchos errores en tiempo de compilación.

De hecho, el estudio de la NASA sobre un sistema embebido de consumo masivo, como el firmware del acelerador electrónico de Toyota, encontró cientos de violaciones de reglas. Esto demuestra que los proyectos del mundo real a menudo se encuentran con los mismos problemas que estas reglas están diseñadas para prevenir. Cada regla de la lista previene una clase de errores comunes (bucles incontrolados, desreferencias de punteros nulos, efectos secundarios invisibles, etc.). Ignorarlas puede llevar a fallos sutiles en tiempo de ejecución, brechas de seguridad o comportamiento no determinista. Por el contrario, adherirse a las diez reglas hace que la verificación estática sea mucho más manejable.

Las herramientas automatizadas son importantes. Las plataformas de calidad del código se pueden configurar para detectar construcciones o patrones prohibidos. Estas reglas se ejecutan automáticamente en cada solicitud de extracción, detectando problemas antes de que se fusione el código.

Conectando el contexto con las reglas

Antes de adentrarnos en las reglas individuales, es importante comprender el contexto:

  • Lenguaje Objetivo: Las reglas "Power of 10" de la NASA fueron escritas para C, un lenguaje con un profundo soporte de herramientas (compiladores, analizadores, depuradores) pero también conocido por sus comportamientos indefinidos. Asumen la ausencia de recolección de basura o gestión avanzada de memoria. Utilizando únicamente C simple y bien estructurado, se puede aprovechar el análisis estático para probar propiedades del programa.
  • Análisis estático: Existen muchas reglas para facilitar las comprobaciones automatizadas. Por ejemplo, prohibir la recursividad (Regla 1) y requerir límites de bucle (Regla 2) permite a las herramientas probar cuántas iteraciones o uso de pila puede tener cualquier función. Del mismo modo, prohibir macros complejas y limitar los punteros (Reglas 8-9) hace que los patrones de código sean explícitos en lugar de estar ocultos en la magia del preprocesador o en múltiples indirecciones.
  • Flujo de trabajo de desarrollo: En los pipelines modernos de DevSecOps, estas reglas se convierten en parte de los controles de CI. Las herramientas de calidad de código pueden integrarse con GitHub, GitLab o Bitbucket para revisar cada pull request y detectar tanto problemas simples como patrones más complejos. Se puede crear una regla personalizada para cada directriz de la NASA, como “señalar cualquier uso de goto o llamadas a funciones recursivas” o “asegurar que cada bucle tenga un límite literal.” Una vez configuradas, estas reglas se aplican automáticamente en cada escaneo de código futuro, detectando las infracciones a tiempo y proporcionando orientación sobre cómo solucionarlas.

En resumen, las 10 reglas de la NASA encarnan una programación C defensiva y analizable. A continuación, enumeramos cada regla, mostramos cómo se ve el código bueno y malo, y explicamos por qué existe la regla y qué riesgos mitiga.

Las 10 reglas de codificación de la NASA

1. Evitar flujos de control complejos.

No utilices goto, setjmp o longjmp, evita escribir funciones recursivas en cualquier parte del código.

Ejemplo no conforme

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

Ejemplo conforme (usa bucle)

// 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 qué es importante: La recursividad y los `gotos` crean un flujo de control no lineal que es difícil de razonar. Las llamadas recursivas hacen que el grafo de llamadas sea cíclico y la profundidad de la pila ilimitada; los `gotos` crean código espagueti. Al usar bucles simples y código lineal, un analizador estático puede verificar fácilmente el uso de la pila y las rutas del programa. Violar esta regla podría conducir a desbordamientos de pila inesperados o a rutas lógicas difíciles de revisar manualmente.

2. Los bucles deben tener límites superiores fijos.

Cada bucle debería tener un límite verificable en tiempo de compilación.

Ejemplo no conforme (bucle ilimitado):

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

Ejemplo conforme (bucle de límite fijo):

// 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 qué es importante: Los bucles sin límite pueden ejecutarse indefinidamente o exceder los límites de recursos. Con un límite fijo, las herramientas pueden demostrar estáticamente las iteraciones máximas. En sistemas críticos para la seguridad, un límite ausente podría causar un bucle descontrolado. Al imponer un límite explícito (o un tamaño de array estático), aseguramos que los bucles terminen de forma predecible. Sin esta regla, un error en la lógica del bucle podría no ser detectado hasta el despliegue (por ejemplo, un error por uno que causa un bucle infinito).

3. Sin memoria dinámica después de la inicialización.

Evitar malloc/free o cualquier uso de la memoria heap en el código en ejecución; utilizar únicamente asignación fija o en la pila.

Ejemplo no 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);
}

Ejemplo conforme (asignación 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 qué esto es importante: La asignación dinámica de memoria durante el tiempo de ejecución puede llevar a un comportamiento impredecible, fragmentación de memoria o fallos de asignación, especialmente en sistemas con recursos limitados como naves espaciales o controladores embebidos. Si `malloc` o `free` fallan a mitad de misión, el software podría colapsar o comportarse de manera impredecible. Utilizar solo memoria de tamaño fijo o asignada en la pila asegura un comportamiento determinista, simplifica la validación y previene fugas de memoria en tiempo de ejecución.

4. Las funciones caben en una página (~60 líneas).

Mantén cada función corta (aproximadamente ≤ 60 líneas).

Ejemplo no conforme

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

Ejemplo conforme (funciones modulares)

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

Por qué esto es importante: Las funciones extremadamente largas son difíciles de entender, probar y verificar como una unidad. Al limitar cada función a una tarea conceptual (y dentro de una página impresa), las revisiones de código y las comprobaciones estáticas se vuelven manejables. Si una función abarca demasiadas líneas, se pueden pasar por alto errores lógicos o condiciones de contorno. Dividir el código en funciones más pequeñas mejora la claridad y facilita la aplicación de otras reglas (como la densidad de aserciones y las comprobaciones de retorno por función).

5. Utilizar al menos dos sentencias assert por función.

Cada función debería realizar comprobaciones defensivas.

Ejemplo no conforme (sin aserciones):

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

Ejemplo conforme (con aserciones):

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 qué esto es importante: Las aserciones son la primera línea de defensa contra condiciones inválidas. La NASA descubrió que una mayor densidad de aserciones aumenta significativamente la probabilidad de detectar errores. Con al menos dos aserciones por función (verificando precondiciones, límites, invariantes), el código autodocumenta sus suposiciones y marca inmediatamente las anomalías durante las pruebas. Sin aserciones, un valor inesperado podría propagarse silenciosamente, causando fallos lejos del origen del error.

6. Declarar datos con un alcance mínimo.

Mantenga las variables lo más locales posible; evite las globales.

Ejemplo no conforme (datos globales):

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

Ejemplo conforme (ámbito local):

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

Por qué es importante: Minimizar el ámbito reduce el acoplamiento y las interacciones no deseadas. Si una variable solo se necesita dentro de una función, declararla globalmente arriesga que otro código la altere inesperadamente. Al mantener los datos locales, cada función se vuelve más autocontenida y libre de efectos secundarios, lo que simplifica el análisis y las pruebas. Las violaciones (como la reutilización de estados globales) pueden conducir a errores difíciles de encontrar debido al aliasing o a modificaciones inesperadas.

7. Comprobar todos los valores de retorno y parámetros de función.

El llamador debe examinar cada valor de retorno no nulo; cada función debe validar sus parámetros de entrada.

❌ Ejemplo no conforme (ignora el 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
}

Ejemplo 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
}

Por qué esto es importante: Ignorar los valores de retorno o los parámetros inválidos es una fuente importante de errores. Por ejemplo, no verificar `malloc` podría llevar a una desreferenciación de puntero nulo. Del mismo modo, no validar las entradas (por ejemplo, índices de array o cadenas de formato) puede causar desbordamientos de búfer o fallos. La NASA exige que cada retorno sea manejado (o explícitamente convertido a `void` para señalar la intención), y cada argumento sea verificado. Este enfoque integral asegura que ningún error sea ignorado silenciosamente.

8. Limitar el preprocesador a includes y macros simples.

Evite macros complejas o trucos de compilación condicional.

Ejemplo no conforme (macro compleja):

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

DECLARE_FUNC(init);  // Se expande a: void func_init(void)

Ejemplo conforme (macros simples / en línea):

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

Por qué esto es importante: Las macros complejas (especialmente las de varias líneas o las que se asemejan a funciones) pueden ocultar lógica, confundir el flujo de control y frustrar el análisis estático. Limitar el preprocesador a tareas triviales (por ejemplo, constantes y cabeceras) mantiene el código explícito. Por ejemplo, reemplazar macros con funciones inline mejora la verificación de tipos y la depurabilidad. Sin esta regla, errores sutiles de expansión de macros o de compilación condicional podrían pasar desapercibidos en las revisiones.

9. Limitar el uso de punteros.

Limite la indirección a un solo nivel: evite los punteros a punteros (int**) y los punteros a funciones.

Ejemplo no conforme (múltiple indirección):

// No conforme: puntero doble y puntero de función
int **doblePtr;
int (*funcPtr)(int) = algunaFunción;

Ejemplo conforme (puntero único):

// Conforme: puntero de un nivel, sin punteros de función
int *singlePtr;
// Usar llamada explícita en lugar de puntero a función
int result = algunaFunción(5);

Por qué es importante: Múltiples niveles de punteros y punteros a funciones complican el flujo de datos y dificultan el seguimiento de la memoria o el código al que se accede. Los analizadores estáticos deben resolver cada indirección, lo que puede ser indecidible en general. Al restringir el uso a referencias de puntero único, el código se mantiene más simple y seguro. Violar esto puede llevar a un aliasing poco claro (un puntero que modifica datos a través de otro) o a un comportamiento inesperado de los callbacks, ambos riesgosos en contextos críticos para la seguridad.

10. Compilar con todas las advertencias habilitadas y corregirlas.

Habilite todas las advertencias del compilador y abórdelas antes del lanzamiento.

Ejemplo no conforme (código con advertencias)

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

Ejemplo conforme (compilación limpia)

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

Por qué esto es importante: Las advertencias del compilador a menudo señalan errores genuinos (como variables no inicializadas, incompatibilidades de tipo o asignaciones no intencionadas). La regla de la NASA exige que ninguna advertencia sea ignorada. Antes de cualquier lanzamiento, el código debe compilar sin advertencias bajo la configuración de máxima verbosidad. Esta práctica detecta muchos errores triviales tempranamente. Si una advertencia no puede resolverse, el código debe reestructurarse o documentarse para que la advertencia nunca ocurra en primer lugar.

Cada una de estas reglas elimina una categoría de errores ocultos. Cuando se siguen en conjunto, hacen que el código C sea mucho más predecible y verificable.

Conclusión

Las 10 reglas de la NASA (el «Poder de 10») proporcionan un estándar de codificación claro y efectivo para software crítico en C. Al evitar construcciones complejas y aplicar verificaciones, reducen la probabilidad de errores ocultos y hacen factible el análisis estático. En el desarrollo moderno, estas directrices pueden automatizarse con herramientas de calidad de código. Se pueden definir reglas personalizadas para señalar cualquier violación de las directrices de la NASA, y estas reglas pueden ejecutarse en cada pull request, proporcionando retroalimentación inmediata a los desarrolladores.

La adopción temprana de estas comprobaciones conduce a un código más seguro, de mayor calidad y más fácil de mantener. Incluso fuera del ámbito aeroespacial, los principios se mantienen: funciones pequeñas y claras, bucles explícitos, programación defensiva y sin uso complejo y peligroso de punteros. Seguir y automatizar estas reglas con una herramienta de calidad de código ayuda a tu equipo a detectar errores temprano y a entregar software más fiable.

Preguntas frecuentes

¿Tiene preguntas?

¿Son las reglas de la NASA solo para proyectos espaciales o embebidos?

En absoluto. Estas reglas se originaron en un contexto crítico para la seguridad, pero se pueden generalizar fácilmente. Cualquier proyecto en C que valore la mantenibilidad y la fiabilidad puede beneficiarse de ellas. De hecho, las reglas complementan estándares industriales como MISRA C. Muchos desarrolladores ajenos a la NASA han descubierto que aplicar incluso solo una parte de estas directrices mejora la calidad del código.

¿Cómo aplico estas reglas automáticamente?

Utilice una herramienta de análisis estático o de revisión de código. La herramienta Code Quality de Aikido Security le permite crear reglas personalizadas. Puede escribir una pequeña regla para cada directriz – por ejemplo, una que señale cualquier goto o cualquier función de más de 60 líneas – y guardarla en Aikido. Aikido verifica cada nueva pull request contra sus reglas personalizadas, bloqueando las fusiones si hay una infracción. Esto se integra sin problemas con GitHub/GitLab/Bitbucket, etc.

¿Por qué debo evitar la memoria dinámica y la recursividad?

Los asignadores de memoria dinámicos (como malloc) pueden fallar o comportarse de forma impredecible, y la recursión no gestionada hace que el uso de la pila sea ilimitado. En software crítico, a menudo debe demostrar los límites de recursos y manejar los peores escenarios. Al deshabilitar malloc y la recursión en tiempo de ejecución, obliga a que toda la memoria y la profundidad de llamada se conozcan de antemano  . Esto previene errores clásicos como fugas de memoria, desbordamientos o desbordamientos de pila, que son particularmente peligrosos cuando hay vidas o equipos multimillonarios en juego.

¿Qué ocurre si mi proyecto necesita incumplir una de estas reglas?

Las directrices de la NASA son estrictas por diseño. Si debes desviarte (por ejemplo, usando un pequeño búfer dinámico), debes hacerlo conscientemente: documenta la excepción, justifícala y, si es posible, añade verificaciones en tiempo de ejecución. Algunos equipos eligen tratar algunas reglas como advertencias en lugar de errores, pero el enfoque más seguro es refactorizar el código para cumplir. Las reglas de la NASA son conservadoras, pero esa es exactamente la razón por la que funcionan. Si utilizas Aikido u otra herramienta, podrías marcar una regla como de baja prioridad, pero aun así es mejor abordar el problema subyacente.

¿Puede Aikido distinguir las violaciones de las reglas de la NASA de otros problemas?

Sí. Las reglas de Aikido son personalizables y etiquetables. Puede etiquetar sus reglas personalizadas como «Regla NASA 1», «Regla NASA 2», etc., para que las infracciones muestren claramente qué directriz se incumplió. Aikido también realiza un seguimiento de las analíticas a lo largo del tiempo, lo que le permitiría ver métricas como la «tasa de cumplimiento de las reglas de la NASA» en toda su base de código. Esta trazabilidad ayuda a los equipos a priorizar las correcciones y a demostrar el cumplimiento durante las auditorías.

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.