hull
← Volver al inicio
Documentacion

Hull Documentacion

Referencia completa del runtime de contenedores hull. Comandos CLI, especificacion del manifest, modelo de seguridad, networking y guias de integracion.

Resumen

Hull es un runtime de contenedores Linux sin daemon escrito en Zig. Se compila a un unico binario estatico-musl de ~3 MB sin dependencias en tiempo de ejecucion. Sin dockerd, sin containerd, sin proceso shim. Cada hull run ejecuta la carga de trabajo directamente y termina.

Hull aplica 7 capas de seguridad independientes: user namespaces, PID namespaces, network namespaces, mount namespaces con pivot_root, cgroups v2, Landlock LSM y seccomp-bpf. La falla de una capa no desactiva las demas. Una carga de trabajo que escape de seccomp aun encuentra Landlock. Una carga de trabajo que evite Landlock aun ve un arbol PID aislado y un /proc vacio.

Hull incluye 6 perfiles seccomp curados (default, webapp, node, dotnet, beam, java), 3 modos de red (none, host, bridge), y soporta ejecucion rootless via --rootless. Los manifests son JSON plano con 3 campos requeridos.


Instalacion

Hull es un unico binario. Descargalo y corre.

Instalar
curl -fsSL https://hull.getmentat.run/install.sh | sh

# o manualmente:
curl -fsSL https://hull.getmentat.run/releases/hull-x86_64 -o /usr/local/bin/hull
chmod +x /usr/local/bin/hull
hull version

Referencia CLI

Ocho comandos. Sin archivos de configuracion, sin YAML, sin TOML. Todo se controla mediante manifests JSON pasados a hull run.

ComandoDescripcion
hull run [--rootless] <manifest>Inicia un contenedor desde un manifest JSON. Ejecuta la carga de trabajo y termina. Con --rootless, mapea el uid del host en el user namespace.
hull psLista contenedores en ejecucion con PID, tiempo activo (segundos) y argv.
hull stop <name>Apagado graceful. Envia SIGTERM, espera la salida.
hull kill <name>Kill inmediato. Envia SIGKILL, sin periodo de gracia.
hull exec <name> <cmd...>Ejecuta un comando dentro de los namespaces de un contenedor en ejecucion (nsenter).
hull logs <name>Imprime stdout y stderr capturados del contenedor.
hull inspect <name>Muestra estado del contenedor, limites y uso de cgroup, inodes de namespaces, puntos de montaje.
hull versionImprime la version de hull e informacion de compilacion.

Codigos de Salida

0Exito
1Error de uso (argumentos invalidos, manifest faltante)
2Error de runtime (configuracion de namespace, escritura de cgroup, red)
3Error de manifest (JSON invalido, campos requeridos faltantes)
127Exec fallido (binario no encontrado en rootfs o permiso denegado)

En una violacion de seccomp (SIGSYS), hull lee dmesg e imprime el numero de syscall bloqueada para que puedas identificar cual syscall necesita ser agregada al perfil.


Especificacion del Manifest

Un manifest de hull es un unico archivo JSON. Tres campos son requeridos; todo lo demas tiene valores por defecto razonables.

Campos Requeridos

CampoTipoDescripcion
namestringNombre del contenedor. 1-64 caracteres, alfanumerico mas guiones.
rootfsstringRuta absoluta al directorio rootfs o archivo .tar.gz.
argvstring[]Comando y argumentos. El primer elemento es la ruta del ejecutable.

Campos Opcionales

CampoTipoDefaultDescripcion
envstring[][]Variables de entorno en formato KEY=VALUE.
profilestring"default"Perfil seccomp: default, webapp, node, dotnet, beam, java.
networkstring"none"Modo de red: none, host, bridge.
hostnamestring(name)Hostname dentro del contenedor. Por defecto usa el nombre del contenedor.
cwdstring"/"Directorio de trabajo para el proceso dentro del contenedor.
limits.memory_mbnumber256Limite de memoria en megabytes. Aplicado por el kernel via cgroups v2.
limits.cpunumber1.0Limite de CPU como fraccion de un core (ej. 0.5, 2.0).
limits.pidsnumber128Numero maximo de procesos. Previene fork bombs.
mounts[]object[][]Bind mounts. Cada objeto tiene campos host, container y readonly.
mounts[].hoststring-Ruta absoluta en el host para el bind mount.
mounts[].containerstring-Ruta absoluta dentro del contenedor para el punto de montaje.
mounts[].readonlybooleanfalseSi el montaje es de solo lectura.
bridge.namestring"hull0"Nombre del dispositivo bridge en el host.
bridge.subnetstring"10.88.0.0/24"Subred para asignacion de IP.
bridge.ipstring(auto)IP estatica para el contenedor. Se asigna automaticamente si se omite.
bridge.mtunumber1500MTU para el par veth.

Ejemplos de Manifest

Servidor Web (perfil node, red bridge)

Una aplicacion web tipica con networking bridge para acceso a internet y el perfil seccomp node para un runtime basado en libuv.

webapp.json
{
  "name": "webapp",
  "rootfs": "/var/lib/hull/rootfs/webapp",
  "argv": ["/app/server", "--port", "3000"],
  "env": [
    "PORT=3000",
    "NODE_ENV=production",
    "HOST=app.example.com"
  ],
  "profile": "node",
  "network": "bridge",
  "hostname": "webapp",
  "cwd": "/app",
  "limits": {
    "memory_mb": 512,
    "cpu": 2.0,
    "pids": 256
  }
}

Servidor API (perfil default, red host)

Un servidor API compilado estaticamente que se enlaza directamente a los puertos del host. Networking host para cero overhead. Perfil seccomp default para un binario tipico de Rust/Go/Zig.

api-server.json
{
  "name": "api-server",
  "rootfs": "/var/lib/hull/rootfs/api-server",
  "argv": ["/usr/local/bin/myapp", "--bind", "0.0.0.0:8080"],
  "env": [
    "RUST_LOG=info",
    "DATABASE_URL=postgres://localhost:5432/mydb"
  ],
  "profile": "default",
  "network": "host",
  "hostname": "api-server",
  "limits": {
    "memory_mb": 1024,
    "cpu": 4.0,
    "pids": 64
  }
}

Worker en Segundo Plano (perfil default, sin red)

Un worker de computo puro sin acceso a red. Solo loopback. Lee trabajo de un directorio montado con bind y escribe resultados de vuelta.

worker.json
{
  "name": "worker",
  "rootfs": "/var/lib/hull/rootfs/worker",
  "argv": ["/app/worker", "--queue", "/data/jobs"],
  "env": [
    "WORKER_THREADS=4",
    "LOG_LEVEL=warn"
  ],
  "profile": "default",
  "network": "none",
  "hostname": "worker",
  "cwd": "/app",
  "limits": {
    "memory_mb": 2048,
    "cpu": 2.0,
    "pids": 32
  },
  "mounts": [
    {
      "host": "/srv/data/jobs",
      "container": "/data/jobs",
      "readonly": false
    },
    {
      "host": "/srv/data/config",
      "container": "/etc/worker",
      "readonly": true
    }
  ]
}

Perfiles Seccomp

Hull incluye 6 perfiles seccomp-bpf curados. Cada uno es una lista de permitidos: las syscalls que no estan en la lista disparan KILL_PROCESS (no EPERM como Docker). La violacion se registra y hull lee dmesg para reportar el numero de syscall bloqueada.

PerfilSyscallsRuntimes ObjetivoNotas
default122Rust musl, Zig, Go, shell scriptsI/O + red + gestion de procesos + pipelines de shell. La linea base para servidores de binario unico. Excluye io_uring intencionalmente (historial de CVEs).
webappdefault + 3Node.js 22+, Next.js SSR, runtimes userspace modernosDefault mas el trio io_uring (io_uring_setup/enter/register). Opt-in por las CVEs recientes de io_uring (2023-21400, 2024-0582, 2024-1085).
node32Node.js, Deno, Bun (libuv)Perfil ajustado: epoll, eventfd, signalfd, timerfd -- el nucleo de libuv. Sin syscalls legacy, sin creacion de archivos mas alla de openat.
dotnet36.NET 8/9 (CoreCLR, NativeAOT)select/pselect6 + signalfd4 + memfd_create (staging de codigo JIT) + tgkill (signals de pthread). Minimo.
beam177Elixir, Erlang, Phoenix (BEAM VM)Default + 55 extras: timerfd, signalfd, inotify, memfd_create, epoll_create, operaciones de archivo legacy (mkdir/unlink/chmod/chown).
javapermisivoOpenJDK 8/11/17/21, Mirth Connect, apps JVM empaquetadas con install4jTabla completa curada -- los workloads JVM ejercitan una superficie amplia (GC coordinado por signals, JNI, JIT, IPC hsperfdata). Iterar syscall por syscall es O(N) fallos, asi que este profile permite la tabla; seccomp sigue bloqueando todo lo que esta fuera (operaciones de modulos kernel, ptrace, kexec).

Bloqueadas en Todos los Perfiles

Las siguientes syscalls peligrosas estan bloqueadas en todos los perfiles, independientemente del tipo de carga de trabajo:

ptraceprocess_vm_readvprocess_vm_writevbpfadd_keykeyctlrequest_keyuserfaultfdkexec_loadkexec_file_loadinit_modulefinit_moduledelete_module

Se soportan ambas arquitecturas x86_64 y aarch64.


Capas de Seguridad

Hull aplica 7 capas de seguridad independientes. Cada capa opera de forma independiente -- la falla de una no desactiva las demas.

1
User namespace (CLONE_NEWUSER)
El proceso del contenedor cree que ejecuta como uid 0, pero el host ve un uid sin privilegios. Se habilita via --rootless. El proceso padre escribe uid_map y gid_map mediante el fork-pipe dance antes de que el hijo llame a execve.
2
PID namespace (CLONE_NEWPID)
Arbol PID aislado. La carga de trabajo ejecuta como PID 1 dentro de su namespace. No puede ver ni enviar signals a ningun proceso del host. El modo bridge usa un double-fork para preservar el aislamiento PID durante la configuracion de red.
3
Network namespace (CLONE_NEWNET)
Tres modos: none (solo loopback, sin ruta de salida), host (stack de red compartido con el host), bridge (par veth conectado al bridge hull0 con masquerade de nftables para acceso a internet).
4
Mount namespace (CLONE_NEWNS) + pivot_root
pivot_root (no chroot) hacia un rootfs dedicado. El sistema de archivos del host es completamente invisible para el contenedor. /proc se remonta para mostrar solo la vista del PID namespace.
5
cgroups v2
Limites duros aplicados por el kernel en CPU (cpu.max), memoria (memory.max) y PIDs (pids.max). El contenedor no puede hacer fork-bomb al host ni agotar la RAM del host. Hull crea un cgroup slice dedicado por contenedor.
6
Landlock LSM
Lista de permisos del sistema de archivos: rootfs obtiene lectura+ejecucion, /tmp obtiene lectura+escritura, todo lo demas es denegado. Ni siquiera uid 0 dentro del contenedor puede evadir Landlock. Se omite gracefully en kernels anteriores a 5.13.
7
seccomp-bpf
Lista de syscalls permitidas por perfil de carga de trabajo. La violacion dispara KILL_PROCESS (no el EPERM de Docker). El filtro BPF se instala inmediatamente antes de execve. Al kill, hull lee dmesg y reporta el numero de syscall bloqueada.

Networking Bridge

Cuando "network": "bridge" esta configurado, hull crea un stack de red completo basado en veth para el contenedor. Este es el proceso paso a paso:

1. Crear bridge hull0
Si el dispositivo bridge hull0 no existe en el host, hull lo crea y asigna 10.88.0.1/24 como direccion gateway. El bridge se levanta (UP).
2. Asignar IP via archivos de lease
Hull escanea archivos de lease en el directorio de estado para encontrar la siguiente IP disponible en la subred 10.88.0.0/24. Se escribe un archivo de lease atomicamente para el nuevo contenedor.
3. Crear par veth
Se crea un par veth: un extremo (vethXXXX) permanece en el namespace del host y se adjunta a hull0; el otro extremo se mueve al network namespace del contenedor y se renombra a eth0.
4. Configurar interfaz del contenedor
Dentro del namespace del contenedor, a eth0 se le asigna la IP asignada (ej. 10.88.0.2/24), se levanta (UP), y se agrega una ruta por defecto via 10.88.0.1.
5. Masquerade de nftables
Hull agrega una regla de masquerade de nftables para la subred 10.88.0.0/24 en la interfaz de salida por defecto del host. Esto permite que los contenedores lleguen a internet via la conexion de red del host.
6. FORWARD de iptables
Se agrega una regla FORWARD de iptables para permitir trafico entre hull0 y la interfaz de salida del host. Se habilita IP forwarding via /proc/sys/net/ipv4/ip_forward.
Salida verificada desde dentro de un contenedor bridge
$ nsenter -t <pid> -n ip -br addr
lo               UNKNOWN        127.0.0.1/8
eth0@if36548     UP             10.88.0.2/24

$ nsenter -t <pid> -n ip route
default via 10.88.0.1 dev eth0
10.88.0.0/24 dev eth0 proto kernel scope link src 10.88.0.2

$ nsenter -t <pid> -n ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=0.523 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=0.389 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=117 time=0.256 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss

Modo Rootless

Con hull run --rootless, hull ejecuta el contenedor completo sin privilegios reales de root. Este es el modo recomendado para cargas de trabajo no confiables.

Fork-Pipe Dance

Linux requiere un proceso fuera del nuevo user namespace para escribir uid_map y gid_map. Hull usa un patron fork-pipe para resolver esto:

Padre
sin privilegios, en el host
Hijo
en namespaces nuevos
1
fork() con CLONE_NEWUSER
+ CLONE_NEWPID + CLONE_NEWNS
+ CLONE_NEWNET
creado, bloqueado en pipe read
2
write /proc/<hijo>/uid_map
write /proc/<hijo>/gid_map
write "ok" al pipe
pipe read retorna "ok"
3
mount setup
pivot_root al rootfs
aplica seccomp + landlock
execve(argv[0])
4
exit (sin daemon)
workload corriendo

Mapeo NEWUSER

El uid_map mapea el uid del host (ej. 1000) al uid 0 del contenedor. Dentro del contenedor, el proceso cree que es root y puede montar sistemas de archivos, crear dispositivos y establecer capabilities -- pero el kernel conoce el uid real sin privilegios y lo aplica en cada limite.

Defensa en Profundidad

Incluso en modo rootless, las otras 6 capas permanecen activas: PID namespace, network namespace, mount namespace con pivot_root, cgroups v2, Landlock y seccomp-bpf. El user namespace agrega una capa adicional encima, no un reemplazo.


Estado y Logs

Hull almacena todo el estado como archivos JSON planos en disco. Sin base de datos, sin socket, sin daemon. El directorio de estado se resuelve en orden:

PrioridadFuenteRuta Tipica
1HULL_STATE_DIR env var/custom/path/hull/state
2$HOME/.hull/state/home/user/.hull/state
3/var/lib/hull/state (root)/var/lib/hull/state

Estructura del Directorio

Estructura del directorio de estado
$STATE_DIR/
  containers/
    myapp/
      state.json        # PID, status, hora de inicio, snapshot del manifest
      stdout.log        # stdout capturado
      stderr.log        # stderr capturado
  leases/
    10.88.0.2.lease     # lease de IP bridge (nombre del contenedor + timestamp)
    10.88.0.3.lease
  hull.pid              # opcional: archivo PID para el proceso hull mismo

El comando hull logs lee directamente de stdout.log y stderr.log. El comando hull inspect lee state.json y consulta el sistema de archivos cgroup en vivo para el uso actual de recursos.


Integracion con Mentat

Hull fue disenado como driver de cargas de trabajo para Mentat, una plataforma de computo auto-alojada. Mentat usa hull como uno de sus backends de ejecucion de contenedores junto con microVMs Firecracker y exec directo.

Como Funciona

El scheduler de Mentat genera manifests de hull dinamicamente basandose en definiciones de servicio. La integracion es directa porque hull no tiene daemon:

Generacion de manifest
Mentat traduce configuraciones de servicio en manifests JSON de hull, estableciendo ruta rootfs, limites de recursos, modo de red y perfil seccomp.
Spawn
Mentat llama a hull run <manifest.json>. Hull ejecuta la carga de trabajo y termina. Mentat registra el PID retornado.
Monitoreo de salud
Mentat llama periodicamente a hull inspect <name> para verificar el estado del contenedor y el uso de recursos.
Ciclo de vida
Mentat usa hull stop o hull kill para apagado graceful/forzado durante despliegues y eventos de escalado.
Logs
Mentat lee la salida de hull logs <name> para agregacion centralizada de logs.

Debido a que hull es un binario estatico sin daemon, Mentat no necesita gestionar un servicio de contenedores de larga ejecucion. No hay socket al cual conectarse, no hay API contra la cual autenticarse, y no hay estado que sincronizar. El sistema de archivos es la API.


Limitaciones Conocidas

Hull esta construido con un proposito especifico e intencionalmente limitado en alcance. Estas son las restricciones conocidas:

Solo Linux
Hull usa syscalls especificas de Linux (clone3, pivot_root, seccomp, landlock). No ejecuta en macOS, Windows o BSDs. Compila cruzado desde macOS para despliegue.
Sin compatibilidad OCI
Hull no implementa la especificacion OCI runtime. Usa su propio formato de manifest JSON. Los bundles OCI no pueden usarse directamente (convertir con Docker export).
Sin capas de imagen
Hull usa directorios rootfs planos o tarballs. No hay deduplicacion de capas, no hay almacenamiento direccionable por contenido, no hay grafo de imagenes.
Sin filesystem overlay
Cada contenedor obtiene su propia copia del rootfs. No hay copy-on-write. El uso de disco escala linealmente con el numero de contenedores usando la misma imagen base.
Sin port forwarding
El modo bridge provee acceso de salida a internet pero hull no configura port forwarding de entrada (DNAT). Usa modo de red host o configura iptables manualmente.
Sin DNS contenedor-a-contenedor
Los contenedores en la red bridge pueden alcanzarse entre si por IP pero no hay resolucion DNS integrada para nombres de contenedor.
Solo host unico
Hull gestiona contenedores en una sola maquina. No hay orquestacion multi-host, no hay overlay networking entre nodos, no hay descubrimiento de servicios integrado.
Requiere cgroups v2
Hull requiere cgroups v2 (jerarquia unificada). Sistemas ejecutando cgroups v1 o modo hibrido no son soportados.
Landlock requiere kernel 5.13+
Landlock LSM se omite gracefully en kernels anteriores, pero la capa de aislamiento del sistema de archivos estara ausente.
Sin passthrough de GPU
No hay soporte para pasar dispositivos GPU a los contenedores. Solo cargas de trabajo de computo.