Reduciendo el comportamiento indefinido en el lenguaje C
El comportamiento indefinido en C y C++ —operaciones para las que el lenguaje no garantiza nada, como el desbordamiento de enteros con signo o la desreferenciación de null— sigue siendo una fuente central de errores, vulnerabilidades de seguridad y optimizaciones sorprendentes del compilador. Los comentaristas sopesan las razones históricas del UB y la necesidad de optimización agresiva frente a los llamados a convertir más casos en comportamiento definido por la implementación, añadir modos opcionales que provoquen trampas o saniticen el UB, y avanzar en C2y para reducir el “zoológico de UB”. Muchos sostienen que la verdadera robustez vendrá de lenguajes más seguros (como Rust) o de mecanismos a nivel de hardware/ABI (como CHERI y Fil-C), mientras que C debería reservarse para código de sistemas de bajo nivel donde los desarrolladores aceptan conscientemente estas compensaciones.
Alcance y naturaleza del comportamiento indefinido (UB) en C
- UB se describe como “lo que ocurre cuando violas una precondición obligatoria”, lo que da a los compiladores libertad para asumir que esos casos nunca ocurren.
- Varios comentarios sostienen que esto hace extremadamente difícil razonar sobre un programa completo, ya que el compilador puede optimizar basándose en que “UB nunca sucede” y cambiar el comportamiento incluso antes del punto de UB.
- Otros señalan que el UB en C/C++ es inusualmente agresivo en comparación con muchos otros lenguajes, permitiendo esencialmente un comportamiento arbitrario (“demonios nasales”) en lugar de solo “lo que haga el hardware”.
Optimización vs. corrección
- Quienes defienden el UB dicen que es esencial para la optimización: asumir que no hay UB permite la inserción en línea, propagación de constantes, eliminación de código muerto, asignación de registros y eliminación de comprobaciones redundantes.
- Los críticos responden que los compiladores ahora “armas” el UB, lo que lleva a la eliminación sorprendente de comprobaciones de null o ramas y dificulta la depuración.
- Algunos sugieren que los compiladores al menos deberían advertir cuando el razonamiento basado en UB elimina código, mientras que otros argumentan que esto es inviable porque casi todas las optimizaciones dependen de asumir que no hay UB.
Definido por la implementación vs. indefinido
- Varios comentaristas argumentan que muchos comportamientos (por ejemplo, la desreferenciación de null, lecturas no inicializadas) deberían estar definidos por la implementación o garantizar que provoquen una trampa, en lugar de ser totalmente indefinidos.
- Punto de vista opuesto: el hardware exótico, WASM y las limitaciones de rendimiento hacen que un comportamiento totalmente definido sea impracticable; los comités de C son reacios a romper implementaciones existentes.
Desbordamiento de enteros
- Hay un fuerte debate sobre si el desbordamiento con signo debería estar definido (por ejemplo, wrap en complemento a dos, trampas o configurable).
- Lado pro-definición: comprobaciones en tiempo de ejecución más fáciles, menos explotación de UB, comportamiento consistente; flags como
-fwrapv/-ftrapvya existen. - Lado anti-definición: el UB permite a los compiladores advertir sobre errores obvios, usar sanitizers y mantener optimizaciones potentes; el desbordamiento sin signo ya está definido pero sigue siendo propenso a errores.
Memoria no inicializada
- Un ejemplo de código muestra a los compiladores explotando lecturas de memoria
mallocno inicializada para optimizar de formas sorprendentes (por ejemplo, plegado a constante para tomar siempre una rama). - Algunos quieren valores “indeterminados pero estables” o semántica definida por la implementación; otros afirman que C2y no hará esto porque rompería demasiadas implementaciones.
Alternativas y mitigaciones
- Las sugerencias incluyen: usar lenguajes seguros para la memoria (Rust, Go) para aplicaciones; mantener C para kernels de bajo nivel; apoyarse en sanitizers (UBSan, ASan, TSan), Valgrind y analizadores estáticos a pesar de sus límites.
- CHERI y Fil-C se citan como enfoques a nivel de ABI que restringen estrechamente el comportamiento de la memoria y reducen la explotabilidad, con compensaciones en compatibilidad y despliegue.
Evolución del estándar y capas
- Según informes, C2y elimina algunas instancias de UB, pero muchas permanecen por motivos de optimización y portabilidad.
- Hay discusión sobre qué capa (lenguaje, SO, ABI, hardware) debe asumir la complejidad para la seguridad, sin un consenso claro.