El modelo simplificado de Fil-C
(corsix.org)- Estructura que rastrea metadatos AllocationRecord junto con los punteros de C/C++ y realiza verificación de límites de memoria al desreferenciarlos
- Método que mueve juntos el valor original del puntero y sus metadatos correspondientes, o los convierte en llamadas específicas de Fil-C, incluso en asignaciones de punteros, aritmética, paso de argumentos a funciones, retornos y llamadas a
malloc/free - Los metadatos de punteros dentro de la memoria del heap se almacenan por separado en invisible_bytes; al cargar o guardar punteros se leen y escriben juntos el valor y los metadatos, y también se aplica una verificación de alineación
filc_freelibera solovisible_byteseinvisible_bytesy mantiene el propio AllocationRecord; la limpieza posterior queda a cargo del recolector de basura, y las variables locales cuya dirección podría escapar se promueven al heap- Aunque siguen existiendo complejidades de implementación real como hilos, punteros a función y optimizaciones de memoria y rendimiento, puede servir para la verificación de seguridad de memoria en código C/C++ a gran escala o como ejemplo concreto de sistema de pointer provenance
Modelo simplificado de Fil-C
- Fil-C usa una estructura que rastrea metadatos AllocationRecord* junto con los punteros para manejar código C/C++ de forma segura en memoria
- La implementación real reescribe LLVM IR, pero el modelo simplificado adopta la forma de una conversión automática del código fuente C/C++
- Se agrega una variable local
AllocationRecord*correspondiente a cada variable local de tipo puntero en cada función - Por ejemplo, a
T1* p1se le agregaAllocationRecord* p1ar = NULL
- Las asignaciones simples y los cálculos sobre variables locales puntero mueven también el AllocationRecord* junto con el valor original del puntero
p1 = p2se transforma enp1 = p2, p1ar = p2arp1 = p2 + 10también va acompañado dep1ar = p2ar- Los casts de entero a puntero establecen los metadatos en
NULL - Los casts de puntero a entero se mantienen tal cual
- En el paso de argumentos y los retornos de funciones también se pasa un AllocationRecord* adicional junto con el puntero, y ciertas llamadas a la biblioteca estándar se sustituyen por funciones específicas de Fil-C
- Las llamadas a
mallocyfreese transforman enfilc_mallocyfilc_free, respectivamente - Por ejemplo,
p1 = malloc(x); free(p1);pasa a{p1, p1ar} = filc_malloc(x); filc_free(p1, p1ar);
- Las llamadas a
filc_mallocno asigna solo la memoria solicitada, sino que realiza tres asignaciones- Asignación del objeto
AllocationRecord - Asignación de
visible_bytespara los datos reales - Asignación con
callocdeinvisible_bytespara almacenar metadatos invisibles AllocationRecordincluye los camposvisible_bytes,invisible_bytesylength
- Asignación del objeto
Desreferenciación y verificación de límites
- Al desreferenciar un puntero se usa el AllocationRecord* asociado para realizar una verificación de límites
- Se comprueba que los metadatos del puntero no sean
NULL - Se calcula la diferencia entre la posición actual del puntero y la dirección inicial de
visible_bytes - Se verifica que el desplazamiento sea menor que la longitud total
- Se verifica que la longitud restante sea suficiente para el tamaño del objeto a desreferenciar
- Se comprueba que los metadatos del puntero no sean
- El mismo procedimiento de verificación se aplica tanto a lectura como a escritura
- También se verifica antes de
x = *p1 - Y se aplica la misma forma de verificación antes de
*p2 = x
- También se verifica antes de
- Con esta estructura se bloquean los accesos en los que el puntero apunta fuera del rango asignado
Punteros en el heap e invisible_bytes
- Los punteros almacenados en memoria del heap no pueden gestionarse mediante variables separadas directamente por el compilador como ocurre con las variables locales, por lo que se usa invisible_bytes
- Si hay un puntero en la posición
visible_bytes + i, suAllocationRecord*correspondiente se almacena eninvisible_bytes + i - Es decir,
invisible_bytesfunciona como un arreglo cuyo tipo de elemento esAllocationRecord*
- Si hay un puntero en la posición
- Al leer o escribir un valor puntero desde memoria, además de la verificación de límites normal se añade una verificación de alineación
- Se comprueba que el desplazamiento
isea múltiplo desizeof(AllocationRecord*) - Solo si se cumple esa condición puede accederse con seguridad a
invisible_bytescomo si fuera un arreglo deAllocationRecord**
- Se comprueba que el desplazamiento
- Al cargar un puntero, se cargan juntos el puntero de datos y sus metadatos
p2 = *p1agrega despuésp2ar = *(AllocationRecord**)(p1ar->invisible_bytes + i)
- Al almacenar un puntero, se guarda no solo el valor del puntero sino también sus metadatos correspondientes
*p1 = p2ejecuta*(AllocationRecord**)(p1ar->invisible_bytes + i) = p2ardespués de almacenar el dato real
filc_free y el recolector de basura
filc_freelibera dos bloques de memoria después de verificar la coherencia con AllocationRecord cuando el puntero no esNULL- Verifica
par != NULL - Verifica
p == par->visible_bytes - Libera
visible_byteseinvisible_bytes - Luego cambia
visible_byteseinvisible_bytesaNULLylengtha 0
- Verifica
- Aunque
filc_mallocrealiza tres asignaciones,filc_freeno libera el propio objeto AllocationRecord- Esa diferencia la maneja el recolector de basura
- En el modelo simplificado basta con un GC stop-the-world, mientras que Fil-C real usa un recolector paralelo, concurrente e incremental
- El GC rastrea siguiendo los objetos
AllocationRecord - Los
AllocationRecordinalcanzables pasan a ser candidatos a liberación
- El GC rastrea siguiendo los objetos
- El GC además realiza dos tareas adicionales
- Llama a
filc_freeal liberarAllocationRecordinalcanzables - Cambia todos los punteros que apuntan a
AllocationRecordconlengthigual a 0 por un únicoAllocationRecordcanónico de longitud 0
- Llama a
- Gracias a este comportamiento, no llamar a
freeno necesariamente provoca una fuga de memoria- El GC la libera automáticamente
- Aun así, llamar a
freepermite liberar memoria antes de que actúe el GC
- Después de
free, eseAllocationRecordacaba volviéndose inalcanzable y puede limpiarse más adelante
Escape de direcciones de variables locales y promoción al heap
- La existencia de un GC amplía el rango en el que puede tratarse de forma segura la dirección de variables locales
- Si se toma la dirección de una variable local y el compilador no puede demostrar que esa dirección no escapa fuera de la vida útil de la variable, se la promueve a una asignación en el heap
- Esas variables locales se asignan con
mallocen lugar de usar la pila- No hace falta insertar un
freecorrespondiente por separado - El GC se encarga de recolectarlas
- No hace falta insertar un
Versión de Fil-C de memmove
memmovede la biblioteca estándar de C maneja memoria arbitraria, por lo que el compilador tiene el problema de no saber si dentro hay punteros- Para ello se aplican heurísticas
- Los punteros dentro de memoria arbitraria deben estar completamente contenidos dentro de ese rango de memoria
- Los punteros deben estar correctamente alineados
- Debido a estas reglas, incluso con el mismo movimiento de 8 bytes puede haber diferencias de comportamiento
- Si se hace un
memmovede 8 bytes alineados de una sola vez, también se mueve la sección correspondiente deinvisible_bytes - Si se hace
memmove8 veces de a 1 byte,invisible_bytesno se mueve
- Si se hace un
Complejidades adicionales en la implementación real
-
Hilos
- La concurrencia aumenta la complejidad del GC
filc_freeno puede liberar memoria de inmediato- Porque puede haber una condición de carrera entre el hilo que libera y otro hilo que accede a la misma memoria
- Las operaciones atómicas sobre punteros también requieren tratamiento adicional
- La reescritura básica convierte la carga/almacenamiento de punteros en dos cargas/almacenamientos, rompiendo la atomicidad
-
Punteros a función
- Metadatos adicionales en
AllocationRecordindican sivisible_bytesno es un dato común sino un puntero a código ejecutable - Una llamada a través del puntero a función
p1compruebap1 == p1ar->visible_bytesy también verifica quep1arrepresente un puntero a función - Para evitar ataques de confusión de tipos sobre punteros a función, también hace falta verificación de firmas de tipo en el ABI de llamada
- Una forma es hacer que todas las funciones tengan la misma firma de tipo
- Tratándolo como si todos los argumentos se empaquetaran en una estructura y se pasaran por memoria
- En el límite del ABI, cada función recibiría solo un
AllocationRecordcorrespondiente a esa estructura
- Metadatos adicionales en
-
Optimización del uso de memoria
- Puede considerarse que
filc_mallocno asigneinvisible_bytesde inmediato, sino que lo haga de forma diferida cuando haga falta - También puede considerarse colocar
AllocationRecordyvisible_bytesjuntos en una sola asignación - Si el
mallocsubyacente adjunta metadatos al inicio de cada asignación, también puede considerarse incorporar esos metadatos enAllocationRecord
- Puede considerarse que
-
Optimización de rendimiento
- La seguridad de memoria de Fil-C conlleva un costo de rendimiento
- Hay margen para aplicar diversas técnicas que recuperen parte del rendimiento perdido
Cuándo usar Fil-C
- Puede usarse cuando un código C/C++ grande parece funcionar, pero no tiene verificación de seguridad de memoria, y se puede tolerar introducir GC y una gran caída de rendimiento para ganar seguridad de memoria
- Se menciona como posible medida temporal antes de reescribirlo en Java, Go o Rust
- También puede ejecutarse Fil-C con el propósito de detectar bugs de memoria, como ASan
- Es posible ejecutar código C/C++ bajo Fil-C para comprobar bugs de memoria
- En lenguajes donde el lenguaje en tiempo de compilación y el lenguaje en tiempo de ejecución son el mismo, y la seguridad en tiempo de compilación es fuerte, puede servir para evaluación segura en tiempo de compilación
- Se menciona Zig como ejemplo
- Aunque la evaluación en tiempo de ejecución no sea segura, la evaluación en tiempo de compilación puede usar una configuración de Fil-C
- También tiene valor como caso de sistema concreto para pointer provenance
- Se plantea la pregunta de si, cuando
p1yp2tienen el mismo tipo, la optimizaciónif (p1 == p2) { f(p1); }→if (p1 == p2) { f(p2); }sería posible - En Fil-C, como el
AllocationRecord*que se pasa afsería distinto, se afirma claramente que la respuesta es no - En este sentido, Fil-C sirve como ejemplo de un sistema concreto que incorpora pointer provenance
- Se plantea la pregunta de si, cuando
1 comentarios
Comentarios en Hacker News
También habría espacio para probar conteo de referencias o variantes del invisible capability system, y da la impresión de que quizá se podría ahorrar memoria a cambio de un pequeño costo de indirección.
Espero que le sirva a quien quiera usar ambos juntos para hacer hermetic builds.
Upon freeing an unreachable AllocationRecord, call filc_free on it.Según yo, lo que intentan decir es que antes de liberar un AR inalcanzable, primero hay que liberar la memoria a la que apuntan los campos
visible_byteseinvisible_bytes.Me parece más interesante que el típico “rewrite it in Rust” por seguridad, porque aquí se plantea poder compilar programas C existentes para que sean completamente memory-safe.
Primero, Fil-C es más lento y más grande. Si eso fuera aceptable, también se podría decir que en los últimos 10 años habría tenido más sentido irse antes a Java o C# que a Rust.
Segundo, sigue siendo C. Para mantener código existente está bien, pero si vas a escribir mucho código nuevo, a mí Rust me parece mucho más cómodo.
Tercero, Fil-C ofrece seguridad en tiempo de ejecución, mientras que Rust puede expresar parte de eso en tiempo de compilación. Y más allá de eso, lenguajes como WUFFS intentan demostrar seguridad en compilación sin checks en runtime, así que aunque el código pueda estar mal en su lógica, el enfoque es evitar crashes o ejecución arbitraria de código.
Han salido hilos como Fil-Qt: A Qt Base build with Fil-C experience, Linux Sandboxes and Fil-C, Ported freetype, fontconfig, harfbuzz, and graphite to Fil-C, A Note on Fil-C, Notes by djb on using Fil-C, Fil-C: A memory-safe C implementation, y Fil's Unbelievable Garbage Collector.
Todavía puedes escribir código que no sea memory-safe, y ahora lo que pasa es más bien que el resultado será un crash garantizado en vez de una vulnerabilidad.
Si estás construyendo algo como una web API que recibe entradas no confiables, este tipo de problema puede terminar convirtiéndose en denial-of-service, así que sí, es mejor, pero no necesariamente lo bastante bueno.
No lo digo para menospreciar el trabajo de Fil-C; solo creo que el enfoque en runtime tiene límites claros.
Pero, siendo justos, Fil-C es bastante más lento que Rust, y también usa más memoria.
A cambio, Fil-C soporta safe dynamic linking y, en ciertos aspectos, incluso podría decirse que es más estricto con la seguridad que Rust.
Al final son trade-offs, así que cada quien puede elegir según su caso.
Por eso siento que, aunque la idea sea interesante técnicamente, en lo emocional no entra con tanta facilidad.
Los valores de capability y pointer pueden quedar rasgados durante una asignación, así que con un mal interleaving entre hilos se podría acceder a un objeto con un puntero incorrecto y producir comportamiento arbitrario.
Esa limitación en sí me parece aceptable, pero me decepciona un poco el ambiente donde hasta los partidarios se lanzan con dureza contra quienes señalan ese problema.
Por desgracia, eso también es una de las grandes fuentes del overhead.
Este tipo de enfoque ya se ha implementado y descartado muchas veces porque no da garantías de seguridad suficientes, porque cuesta cruzar non-fat ABI boundaries, o porque el overhead es alto.
Además, creo que filc no se explica solo como un simple fat pointer.