La gestión manual de memoria en C/C++ es una fuente persistente de vulnerabilidades de seguridad y errores de programación, como desbordamientos de búfer, usos de memoria después de liberada (use-after-free) y fugas de memoria. Fil-C aborda este problema fundamental de la computación, que ha sido un desafío desde los inicios de lenguajes de bajo nivel, al introducir un modelo de seguridad de memoria en tiempo de ejecución. Esto se logra mediante una transformación del código que instrumenta cada acceso a punteros con metadatos de asignación y un recolector de basura, moviendo la responsabilidad de la seguridad de memoria del programador a un sistema automatizado. La relevancia actual de Fil-C radica en la necesidad continua de mantener y evolucionar grandes bases de código C/C++ existentes, donde una reescritura completa a lenguajes memory-safe como Rust o Go no siempre es factible a corto plazo.

Históricamente, la seguridad de memoria se ha abordado con enfoques como la verificación estática (limitada por la indecidibilidad), la instrumentación en tiempo de ejecución (como AddressSanitizer, que detecta errores pero no los previene activamente), o la adopción de lenguajes con recolección de basura o sistemas de tipos más robustos. Fil-C se posiciona como una solución híbrida, aplicando principios de instrumentación y recolección de basura a un lenguaje que tradicionalmente carece de ellos, buscando un equilibrio entre la compatibilidad con el código existente y la mitigación de riesgos de seguridad.

Arquitectura del Sistema

La arquitectura de Fil-C se basa en una transformación a nivel de código fuente (o LLVM IR en la versión de producción) que modifica la representación de los punteros y las operaciones sobre ellos. Cada variable local de tipo puntero T* p1 se expande para incluir un AllocationRecord* p1ar asociado. Este AllocationRecord es una estructura que contiene visible_bytes (el puntero original a la memoria solicitada), invisible_bytes (un área de memoria paralela para almacenar los AllocationRecord* de punteros anidados en el heap) y length (el tamaño de la asignación original).

Las operaciones triviales con punteros, como asignaciones (p1 = p2) o aritmética de punteros (p1 = p2 + 10), se reescriben para también manipular los AllocationRecord* asociados (p1ar = p2ar). Cuando un puntero se desreferencia (*p1), el p1ar se utiliza para realizar verificaciones de límites en tiempo de ejecución, asegurando que el acceso esté dentro del length de la asignación. Para punteros almacenados en el heap, invisible_bytes actúa como un array paralelo donde se guardan los AllocationRecord* correspondientes, manteniendo la trazabilidad de la procedencia de la memoria.

La gestión de memoria se modifica con filc_malloc y filc_free. filc_malloc realiza tres asignaciones: una para el AllocationRecord, una para visible_bytes y otra para invisible_bytes. filc_free libera visible_bytes e invisible_bytes, pero no el AllocationRecord en sí. La liberación del AllocationRecord es responsabilidad de un recolector de basura (GC). Este GC, que en la versión simplificada es 'stop-the-world' y en la de producción es concurrente e incremental, rastrea los AllocationRecord y libera aquellos que son inalcanzables. También se encarga de promover variables locales a asignaciones en el heap si su dirección escapa de su ámbito, eliminando la necesidad de free explícitos para estas.

Funciones como memmove requieren un tratamiento especial, ya que manipulan memoria arbitraria. Fil-C utiliza una heurística para memmove que asume que los punteros dentro de la memoria movida deben estar correctamente alineados y completamente dentro del rango, y mueve los AllocationRecord* correspondientes en invisible_bytes en paralelo. La versión de producción también aborda la complejidad de la concurrencia, los punteros a funciones (añadiendo metadatos al AllocationRecord y verificaciones de ABI) y optimizaciones de uso de memoria y rendimiento.

Flujo de Asignación de Memoria con filc_malloc

  1. 1 filc_malloc(length) Solicita una asignación de memoria de 'length' bytes.
  2. 2 malloc(sizeof(AR)) Asigna memoria para el 'AllocationRecord' (AR).
  3. 3 malloc(length) Asigna memoria para 'visible_bytes' (datos del usuario).
  4. 4 calloc(length, 1) Asigna memoria para 'invisible_bytes' (metadatos de punteros anidados).
  5. 5 Retorna {visible_bytes, AR} Devuelve el puntero visible y su 'AllocationRecord' asociado.

Flujo de Desreferencia de Puntero con Verificación de Límites

  1. 1 Acceso a *p1 Intento de desreferenciar un puntero 'p1'.
  2. 2 Carga p1ar Recupera el 'AllocationRecord' asociado 'p1ar'.
  3. 3 Verifica p1 en p1ar->visible_bytes Comprueba que 'p1' apunte al inicio de 'visible_bytes'.
  4. 4 Verifica offset < p1ar->length Asegura que el desplazamiento esté dentro de los límites de la asignación.
  5. 5 Acceso a memoria Si las verificaciones pasan, se permite el acceso a la memoria.
CapaTecnologíaJustificación
storage malloc/calloc Funciones base para la asignación de memoria en el heap, utilizadas por filc_malloc para obtener los bloques de memoria crudos para visible_bytes, invisible_bytes y AllocationRecord. vs jemalloc, tcmalloc
compute LLVM IR La versión de producción de Fil-C opera a nivel de LLVM IR para transformar el código, lo que permite una instrumentación más profunda y optimizada que la transformación a nivel de código fuente. vs GCC GIMPLE, compilación a nivel de AST
orchestration Garbage Collector Componente crucial para la liberación automática de AllocationRecord y la prevención de fugas de memoria. En la versión de producción, es un GC concurrente e incremental. vs Referencia de conteo, GC generacional

Trade-offs

Ganancias
  • Seguridad de memoria
  • Prevención de fugas de memoria
  • Detección de errores de puntero
Costes
  • Rendimiento
  • Uso de memoria
  • Complejidad del sistema
Original: T* p1;
Transformed: T* p1; AllocationRecord* p1ar;
Transformación de una declaración de puntero local para incluir su AllocationRecord asociado.
void* filc_malloc(size_t length) {
  AllocationRecord* ar = malloc(sizeof(AllocationRecord));
  ar->visible_bytes = malloc(length);
  ar->invisible_bytes = calloc(length, 1);
  ar->length = length;
  return (void*){ar->visible_bytes, ar};
}
Implementación simplificada de filc_malloc que asigna datos visibles, datos invisibles y el AllocationRecord.
Original: *p1 = 10;
Transformed: assert(p1 >= p1ar->visible_bytes && p1 < p1ar->visible_bytes + p1ar->length); *p1 = 10;
Transformación de una desreferencia de puntero para incluir la verificación de límites usando el AllocationRecord.

Fundamentos Teóricos

El problema de la seguridad de memoria en C/C++ ha sido un tema central en la investigación de sistemas desde los años 70. Conceptos como la 'pointer provenance' (procedencia de punteros) son fundamentales para entender cómo los compiladores pueden optimizar el código y cómo estas optimizaciones pueden interactuar con la seguridad de memoria. Fil-C ofrece un ejemplo concreto de un sistema donde la procedencia de punteros es explícitamente rastreada y utilizada para garantizar la seguridad, lo que tiene implicaciones directas en la validez de ciertas optimizaciones de compilador.

La introducción de un recolector de basura en un entorno C/C++ se alinea con décadas de investigación en gestión automática de memoria, desde los primeros algoritmos de Mark-and-Sweep de John McCarthy (1960) hasta los recolectores generacionales, concurrentes e incrementales modernos. La idea de usar un 'shadow memory' o 'metadata' para punteros y asignaciones se ha explorado en sistemas como AddressSanitizer (ASan) de Google, que utiliza una región de memoria para almacenar metadatos de acceso y detectar errores. Fil-C extiende este concepto al integrar los metadatos de forma más profunda en el modelo de punteros y al combinarlo con un GC para una prevención más proactiva y una gestión de memoria más robusta, en lugar de solo detección de errores.