Artículos en profundidad sobre la tecnología que da forma al futuro.

Sandboxes de VM en sub-milisegundos con Copy-on-Write

Cómo el forking de memoria copy-on-write permite sandboxes de VM que arrancan en menos de un milisegundo y cambian el serverless y el aislamiento de seguridad.

Una pompa de jabón que se divide en muchas pequeñas pompas, cada una con una diminuta habitación de cristal

Iniciar un contenedor de Docker tarda unos 500 milisegundos. Una microVM de Firecracker tarda unos 125 milisegundos. Un isolate de V8 tarda unos 5 milisegundos. Pero una nueva generación de sandboxes ligeros, que usan forking de memoria copy-on-write, puede levantar un entorno de ejecución aislado en menos de 1 milisegundo, a menudo en el rango de 50 a 200 microsegundos. Es lo bastante rápido como para crear un sandbox nuevo para cada llamada a una función.

Esto no es solo una mejora incremental. Es un cambio cualitativo en lo que el sandboxing puede hacer. Cuando crear un sandbox cuesta 500 ms, se crean con cuentagotas y se reutilizan. Cuando cuesta 50 μs, se crean para cada entrada no confiable, cada invocación de un plugin y cada petición de usuario. El modelo de seguridad pasa de «aislar inquilinos» a «aislar cada operación individual».

Qué significa realmente Copy-on-Write

Copy-on-write (CoW) es una técnica del sistema operativo en la que creas una “copia” de una región de memoria sin copiar realmente ningún dato. Tanto el original como la copia apuntan a las mismas páginas físicas de memoria, marcadas como de solo lectura. Son indistinguibles: ambos ven exactamente los mismos datos. La copia real solo ocurre cuando uno de ellos intenta escribir en una página. En ese momento, el kernel intercepta la escritura, copia únicamente esa página y deja que la escritura continúe en la copia.

La llamada al sistema fork() de Unix usa esto desde los años noventa. Cuando haces fork de un proceso, el hijo obtiene una copia completa de la memoria del padre, pero gracias a CoW no se copia realmente ningún dato. Si el hijo llama inmediatamente a exec() (lo habitual), reemplaza toda su memoria y las páginas CoW simplemente se liberan. El fork era prácticamente gratis.

// Classic fork() — copy-on-write in action
pid_t pid = fork();
// At this point:
// - Parent and child share ALL physical memory pages
// - Both have identical virtual address spaces
// - Pages are marked read-only in both
// - Zero data has been copied
if (pid == 0) {
// Child process
// Reading shared_data is free — same physical page
int value = shared_data[0];
// Writing triggers CoW — only THIS page gets copied
shared_data[0] = 42;
// Now child has its own copy of this one page
// Parent's page is unchanged
}

De fork a sandbox

La idea clave de los sandboxes CoW es esta: en lugar de arrancar una VM o un contenedor desde cero, preinicias un entorno «plantilla» (con el runtime, las librerías y el estado inicial ya cargados) y luego le haces fork usando CoW para crear copias instantáneas. Cada copia arranca exactamente en el estado en el que quedó la plantilla, completamente inicializada y lista para ejecutar, pero corre en su propio espacio de memoria aislado.

La diferencia de rendimiento es enorme. El arranque tradicional de una VM implica cargar un kernel, inicializar el hardware, montar los sistemas de archivos, arrancar el sistema init, cargar el código de la aplicación e inicializar el runtime. Incluso con optimizaciones agresivas (Firecracker lo reduce mucho), sigues haciendo cientos de milisegundos de trabajo de inicialización.

El forking CoW se salta todo eso. La plantilla ya hizo la inicialización. El fork crea una copia preinicializada en microsegundos. El «coste de arranque» es solo el trabajo interno del kernel para crear un nuevo espacio de direcciones y duplicar las entradas de la tabla de páginas: unas pocas miles de operaciones, sin importar cuánta memoria use la plantilla.

Dónde cambia el juego

Funciones serverless

Los arranques en frío son la pesadilla del serverless. AWS Lambda tarda entre 100 y 500 ms en un arranque en frío, y más en runtimes basados en JVM. Esto es inaceptable para cargas sensibles a la latencia, lo que obliga a mantener instancias calientes (anulando el propósito del serverless) o a aceptar una latencia impredecible.

Con sandboxes CoW, los arranques en frío bajan a menos de un milisegundo. Cada invocación puede ser un «arranque en frío», porque en realidad son prácticamente gratuitos. No hacen falta pools de instancias calientes, no se desperdicia memoria en instancias ociosas y no hay estado residual entre invocaciones. Cada ejecución de función recibe un entorno aislado y limpio sin pagar el coste de inicialización.

Sistemas de plugins y extensiones

Ejecutar plugins no confiables de forma segura es uno de los problemas más difíciles del software. Los navegadores lo resolvieron para JavaScript con los isolates de V8. Pero para código arbitrario (extensiones compiladas, lenguajes de scripting, plugins binarios), las opciones de aislamiento se han limitado a contenedores (demasiado lentos para aislar por petición) o WebAssembly (ecosistema y soporte de lenguajes limitados).

Los sandboxes CoW ofrecen un término medio: ejecutar código arbitrario en una copia aislada del entorno del host, con preparación y destrucción en menos de un milisegundo. El plugin ve un entorno de SO completo (sistema de archivos, red, librerías), pero sus modificaciones quedan contenidas: al salir del sandbox, todos los cambios desaparecen. Es ideal para código enviado por usuarios en sistemas de CI, entornos de notebooks y herramientas de build.

Aislamiento de seguridad

Al procesar entradas no confiables (analizar un PDF subido, renderizar HTML proporcionado por el usuario, ejecutar una consulta a la base de datos), ejecutar la operación en un sandbox aislado limita el radio de impacto de cualquier exploit. Si el parser de PDF tiene un desbordamiento de búfer, el atacante toma el control de un sandbox desechable que está a punto de destruirse, no del servidor de aplicaciones.

Este enfoque (aislamiento de procesos para cada operación no confiable) ha sido poco práctico con el sandboxing tradicional, porque el overhead superaba el tiempo de procesamiento. Si analizar un PDF tarda 10 ms, gastar 500 ms en crear un contenedor no tiene sentido. Pero gastar 100 μs en crear un sandbox CoW es trivialmente barato.

Los detalles de implementación

Construir un sistema práctico de sandboxes CoW exige resolver varios problemas más allá de llamar a fork().

  • Contabilidad de memoria. CoW hace ambigua la medición del uso de memoria. Si una plantilla usa 1 GB y haces fork de 100 copias que modifican cada una 10 MB, el uso físico de memoria es de unos 2 GB (1 GB compartido + 100 × 10 MB únicos), no 100 GB. El kernel distingue páginas compartidas y privadas, pero para obtener el uso exacto por sandbox hay que analizar /proc/[pid]/smaps.
  • Aislamiento del sistema de archivos. CoW resuelve la memoria, pero las escrituras en disco necesitan su propio aislamiento. Los sistemas de archivos overlay (overlayfs) ofrecen semántica CoW para archivos: el sandbox ve el sistema de archivos de la plantilla, pero las escrituras van a una capa separada. Al salir, el overlay se descarta.
  • Aislamiento de red. Cada sandbox necesita su propio namespace de red para evitar interferencias. Los namespaces de Linux lo proporcionan, pero crearlos tiene un overhead medible. Algunos sistemas reutilizan un pool de namespaces ya creados.
  • Límites de recursos. Un sandbox que reserva memoria sin límite o consume CPU sin tope es un vector de denegación de servicio. Los cgroups permiten limitar memoria, CPU y E/S, pero crear y destruir cgroups añade overhead. De nuevo, el pooling ayuda.
  • Limpieza determinista. Cuando un sandbox termina, todos sus recursos (memoria, descriptores de archivo, conexiones de red, objetos IPC) deben liberarse de forma fiable. Los PID namespaces ayudan: al matar el proceso init del namespace, se matan todos sus descendientes.

CoW frente a sandboxes WebAssembly

WebAssembly (Wasm) es la otra gran tecnología de sandboxing ligero. Vale la pena compararlas porque hacen compromisos fundamentalmente distintos.

Los sandboxes Wasm ejecutan código en una máquina virtual con seguridad de memoria y un modelo de memoria lineal. El sandbox no puede acceder a nada fuera de su memoria lineal: ni sistema de archivos, ni red, ni llamadas al sistema (salvo que se proporcionen explícitamente mediante WASI). Es muy seguro, pero restrictivo: el código existente debe recompilarse a Wasm, y no todos los lenguajes compilan bien a Wasm.

Los sandboxes CoW ejecutan código nativo en un entorno de SO aislado. El sandbox tiene acceso a una interfaz de SO completa (posiblemente restringida con filtros seccomp), puede ejecutar cualquier binario y usa las librerías del sistema habituales. Es menos restrictivo, pero también menos seguro: la frontera de aislamiento es el modelo de procesos del SO, que tiene una superficie de ataque mayor que la VM mínima de Wasm.

Elige Wasm cuando controlas el código que vas a aislar, tu carga compila limpiamente a Wasm y necesitas el aislamiento más fuerte posible. Elige sandboxes CoW cuando necesitas ejecutar binarios existentes arbitrarios, tu carga requiere capacidades de SO (sistema de archivos, red, procesos hijos) y priorizas la compatibilidad sobre una superficie de ataque mínima.

La trampa: fork en programas multihilo

Hay un escollo muy conocido con fork(): solo copia el hilo que lo invoca. Si el padre tiene 20 hilos, el hijo tiene uno. Los mutex que tenían bloqueados los otros 19 hilos siguen marcados como bloqueados en la memoria del hijo, pero los hilos que los poseían ya no existen. El hijo se bloqueará la primera vez que intente adquirir uno de esos mutex.

Los sistemas de sandboxes CoW evitan esto asegurándose de que el proceso plantilla tenga un solo hilo en el momento del fork. Normalmente esto significa inicializar todo en la plantilla (cargar librerías, configurar el runtime, preparar el estado inicial), luego detener todos los hilos excepto el principal, hacer fork y dejar que cada hijo recree hilos según los necesite. El coste de inicialización se paga una sola vez; el fork evita el peligro de los hilos.

Algunos enfoques más nuevos usan userfaultfd o manejadores de fallos de página personalizados para implementar semántica tipo CoW sin depender de fork(). Evitan el problema del multihilo, pero añaden complejidad y requieren más coordinación a nivel de kernel.

Qué vigilar

El sandboxing de sub-milisegundos todavía está en sus primeras etapas, pero los bloques de construcción son sólidos (fork, namespaces, cgroups y overlayfs son tecnologías maduras). Los sistemas construidos sobre ellos, para serverless, CI/CD y ejecución segura de código, demuestran que el aislamiento por operación es práctico a escala. A medida que estas herramientas maduren, la suposición de que el sandboxing es caro quedará tan obsoleta como la de que el garbage collection es demasiado lento para aplicaciones en tiempo real. El overhead se está desvaneciendo, y los beneficios de seguridad de «sandboxear todo» son cada vez más difíciles de ignorar.