Introducción
El software de seguridad crítica, como el que se utiliza en naves espaciales o sistemas de automoción, requiere un código extremadamente fiable. Para solucionar este problema, el Laboratorio de Propulsión a Chorro de la NASA creó en 2006 las reglas de codificación
"Power of 10". Estas concisas directrices eliminan las complejas construcciones en C que son difíciles de analizar y garantizan que el código siga siendo sencillo, verificable y fiable.
Hoy en día, herramientas como la calidad de código Aikido se pueden configurar con controles personalizados para hacer cumplir las diez reglas en cada nuevo pull request. 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é son importantes estas normas
Las normas de la NASA se centran en la legibilidad, la analizabilidad y la fiabilidad, aspectos esenciales para aplicaciones de misión crítica como el software de control y vuelo de naves espaciales. Al prohibir las construcciones oscuras en C e imponer comprobaciones defensivas, las directrices facilitan la revisión del código y la comprobación de su corrección. Complementan normas como MISRA C al abordar patrones que los analizadores estáticos suelen pasar por alto. Por ejemplo, al evitar la recursividad y la memoria dinámica se mantiene la previsibilidad en el uso de los recursos, mientras que las comprobaciones de los valores de retorno ayudan a detectar muchos errores en tiempo de compilación.
De hecho, el estudio de la NASA sobre un sistema integrado de gran consumo, como el firmware del acelerador electrónico de Toyota, detectó cientos de infracciones de las normas. Esto demuestra que los proyectos del mundo real suelen tropezar con los mismos problemas que estas reglas pretenden evitar. Cada regla de la lista previene una clase de errores comunes (bucles incontrolados, desviaciones de puntero nulo, efectos secundarios invisibles, etc.). Ignorarlas puede provocar fallos sutiles en tiempo de ejecución, agujeros de seguridad o comportamientos no deterministas. Por el contrario, el cumplimiento de las diez reglas hace que la verificación estática sea mucho más sencilla.
Las herramientas automatizadas son importantes. Las plataformas de calidad del código pueden configurarse para detectar construcciones o patrones prohibidos. Estas reglas se ejecutan automáticamente en cada pull request, detectando problemas antes de que se fusione el código.
Unir el contexto a las normas
Antes de entrar en cada una de las normas, es importante comprender el contexto:
- Lenguaje objetivo: Las reglas "Power of 10" de la NASA fueron escritas para C, un lenguaje con un gran soporte de herramientas (compiladores, analizadores, depuradores) pero también notorio por sus comportamientos indefinidos. No suponen recolección de basura ni gestión avanzada de la memoria. Utilizando sólo un C sencillo y bien estructurado, se puede aprovechar el análisis estático para demostrar las propiedades del programa.
- Análisis estático: Existen muchas reglas para facilitar las comprobaciones automáticas. Por ejemplo, prohibir la recursividad (Regla 1) y exigir límites de bucle (Regla 2) permite a las herramientas demostrar cuántas iteraciones o uso de la pila puede tener una función. Del mismo modo, la prohibición de macros complejas y la limitación de punteros (reglas 8 y 9) hacen que los patrones de código sean explícitos y no queden ocultos en la magia del preprocesador o en múltiples indirecciones.
- Flujo de trabajo de desarrollo: En los procesos DevSecOps modernos, estas reglas forman parte de las comprobaciones CI. Las herramientas de calidad del código pueden integrarse con GitHub, GitLab o Bitbucket para revisar cada pull request y detectar tanto problemas sencillos como patrones más complejos. Puede crear una regla personalizada para cada pauta de la NASA, como "marcar cualquier uso de goto o llamadas a funciones recursivas" o "asegurarse de que cada bucle tiene un límite literal". Una vez configuradas, estas reglas se aplican automáticamente en cada escaneo de código futuro, detectando infracciones a tiempo y proporcionando orientación sobre cómo solucionarlas.
En resumen, las 10 reglas de la NASA encarnan la programación C defensiva y analizable. A continuación enumeramos cada regla, mostramos qué aspecto tiene el código bueno y el malo, y explicamos por qué existe la regla y qué riesgos mitiga.
Las 10 reglas de codificación de la NASA
1. Evite flujos de control complejos.
No utilice goto, setjmp, o longjmp, evite escribir funciones recursivas en cualquier parte del código.
❌ Ejemplo de no conformidad
// Non-compliant: recursive function call
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n-1); // recursion (direct)
}✅ Ejemplo conforme (utiliza 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 sobre el que es difícil razonar. Las llamadas recursivas hacen que el gráfico de llamadas sea cíclico y la profundidad de la pila ilimitada; goto crea código espagueti. Al utilizar bucles simples y código en línea recta, un analizador estático puede verificar fácilmente el uso de la pila y las rutas del programa. Infringir esta regla podría provocar desbordamientos inesperados de la pila o rutas lógicas difíciles de revisar manualmente.
2. Los bucles deben tener límites superiores fijos.
Cada bucle debe tener un límite verificable en tiempo de compilación.
❌ Ejemplo no conforme (bucle no delimitado):
// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
doSomething(array[i]);
i++;
}✅ Ejemplo conforme (bucle de conexión fija):
// 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 eternamente o superar los límites de recursos. Con un límite fijo, las herramientas pueden probar estáticamente las iteraciones máximas. En sistemas críticos para la seguridad, un límite omitido podría provocar un bucle fuera de control. Al imponer un límite explícito (o un tamaño de matriz estático), nos aseguramos de que los bucles terminen de forma predecible. Sin esta regla, un error en la lógica del bucle podría no detectarse hasta su despliegue (por ejemplo, un error de uno en uno que provoque un bucle infinito).
3. No hay memoria dinámica después de la inicialización.
Evite malloc/free o cualquier uso de heap en código en ejecución; utilice sólo asignación fija o de pila.
❌ Ejemplo no conforme (utiliza 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é es importante: La asignación dinámica de memoria durante el tiempo de ejecución puede provocar comportamientos impredecibles, fragmentación de la memoria o fallos en la asignación, especialmente en sistemas con recursos limitados como naves espaciales o controladores integrados. Si malloc o free fallan a mitad de la misión, el software puede bloquearse o comportarse de forma impredecible. Utilizar únicamente memoria de tamaño fijo o asignada en pila garantiza un comportamiento determinista, simplifica la validación y evita fugas de memoria en tiempo de ejecución.
4. Las funciones caben en una página (~60 líneas).
Que cada función sea corta (aproximadamente ≤ 60 líneas).
❌ Ejemplo de no conformidad
// 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é es importante: Las funciones extremadamente largas son difíciles de entender, probar y verificar como una unidad. Si cada función se limita a una tarea conceptual (y a una página impresa), las revisiones del código y las comprobaciones estáticas resultan más sencillas. Si una función abarca demasiadas líneas, pueden pasarse por alto errores lógicos o condiciones límite. Dividir el código en funciones más pequeñas mejora la claridad y facilita la aplicación de otras normas (como la densidad de aserciones y las comprobaciones de retorno por función).
5. Utilice al menos dos sentencias assert por función.
Cada función debe realizar comprobaciones defensivas.
❌ Ejemplo no conforme (sin asecrciones):
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é es importante: Las aserciones son la primera línea de defensa contra las condiciones no válidas. La NASA descubrió que una mayor densidad de aserciones aumenta significativamente las posibilidades de detectar errores. Con al menos dos aserciones por función (comprobación de condiciones previas, límites, invariantes), el código documenta por sí mismo sus suposiciones y señala inmediatamente las anomalías durante las pruebas. Sin aserciones, un valor inesperado podría propagarse silenciosamente, causando fallos lejos de la fuente 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 gloabl):
// 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 alcance reduce el acoplamiento y las interacciones no deseadas. Si una variable sólo se necesita dentro de una función, al declararla globalmente se corre el riesgo de que otro código la altere inesperadamente. Al mantener los datos locales, cada función se vuelve más autónoma y libre de efectos secundarios, lo que simplifica el análisis y las pruebas. Las infracciones (como la reutilización del estado global) pueden dar lugar a errores difíciles de detectar debido al aliasing o a modificaciones inesperadas.
7. Compruebe todos los valores de retorno de las funciones y los parámetros.
La persona que llama debe examinar cada valor de retorno no vacío; 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 de conformidad
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é es importante: Ignorar valores de retorno o parámetros inválidos es una fuente importante de errores. Por ejemplo, no comprobar malloc puede provocar una desviación de puntero nulo. Del mismo modo, no validar las entradas (por ejemplo, índices de matrices o cadenas de formato) puede causar desbordamientos de búfer o fallos. La NASA requiere que cada retorno sea manejado (o explícitamente convertido a void para señalar la intención), y que cada argumento sea verificado. Este enfoque "catch-all" garantiza que no se ignore ningún error.
8. Limite el preprocesador a includes y macros simples.
Evite macros complejas o trucos de compilación condicional.
❌ Ejemplo no conforme (marco complejo):
#define DECLARE_FUNC(nombre) void func_##nombre(void)
DECLARE_FUNC(init); // Se expande a: void func_init(void)✅ Ejemplo conforme (macros simples / inline):
// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256Por qué es importante: Las macros complejas (especialmente las de varias líneas o tipo función) pueden ocultar la 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, sustituir las macros por funciones en línea mejora la comprobación de tipos y la depurabilidad. Sin esta regla, los 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 int** y punteros a funciones.
❌ Ejemplo no conforme (inderección múltiple):
// 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 de función complican el flujo de datos y hacen difícil seguir a qué memoria o código se está accediendo. Los analizadores estáticos deben resolver cada indirección, lo que puede ser indecidible en general. Al restringir las referencias a un único puntero, el código es más sencillo y seguro. Violar esto puede llevar a un aliasing poco claro (un puntero modificando datos a través de otro) o a un comportamiento de callback inesperado, ambos arriesgados en contextos de seguridad crítica.
10. Compilar con todas las advertencias activadas y corregirlas.
Active todas las advertencias del compilador y resuélvalas antes de la publicación.
❌ 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é es importante: Las advertencias del compilador a menudo señalan errores genuinos (como variables no inicializadas, desajustes de tipo o asignaciones involuntarias). La norma de la NASA exige que no se ignore ninguna advertencia. Antes de cualquier publicación, el código debe compilarse sin advertencias con los ajustes de máxima verbosidad. Esta práctica permite detectar a tiempo muchos errores triviales. Si una advertencia no puede resolverse, el código debe reestructurarse o documentarse para que la advertencia nunca se produzca en primer lugar.
Cada una de estas reglas elimina una categoría de errores ocultos. Cuando se siguen juntas, 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 eficaz para el software C crítico. Al evitar construcciones complejas e imponer comprobaciones, reducen la posibilidad de errores ocultos y hacen factible el análisis estático. En el desarrollo moderno, estas directrices pueden automatizarse con herramientas de calidad del código. Pueden definirse reglas personalizadas para señalar cualquier violación de las directrices de la NASA, y estas reglas pueden ejecutarse en cada solicitud de extracción, proporcionando informació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 sector aeroespacial, los principios se mantienen: funciones pequeñas y claras, bucles explícitos, programación defensiva y nada de gimnasia con punteros. Seguir y automatizar estas reglas con una herramienta de calidad del código ayuda a su equipo a detectar errores en una fase temprana y a ofrecer un software más fiable.
.avif)
