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.
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 versionReferencia CLI
Ocho comandos. Sin archivos de configuracion, sin YAML, sin TOML. Todo se controla mediante manifests JSON pasados a hull run.
| Comando | Descripcion |
| 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 ps | Lista 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 version | Imprime la version de hull e informacion de compilacion. |
Codigos de Salida
| 0 | Exito |
| 1 | Error de uso (argumentos invalidos, manifest faltante) |
| 2 | Error de runtime (configuracion de namespace, escritura de cgroup, red) |
| 3 | Error de manifest (JSON invalido, campos requeridos faltantes) |
| 127 | Exec 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
| Campo | Tipo | Descripcion |
| name | string | Nombre del contenedor. 1-64 caracteres, alfanumerico mas guiones. |
| rootfs | string | Ruta absoluta al directorio rootfs o archivo .tar.gz. |
| argv | string[] | Comando y argumentos. El primer elemento es la ruta del ejecutable. |
Campos Opcionales
| Campo | Tipo | Default | Descripcion |
| env | string[] | [] | Variables de entorno en formato KEY=VALUE. |
| profile | string | "default" | Perfil seccomp: default, webapp, node, dotnet, beam, java. |
| network | string | "none" | Modo de red: none, host, bridge. |
| hostname | string | (name) | Hostname dentro del contenedor. Por defecto usa el nombre del contenedor. |
| cwd | string | "/" | Directorio de trabajo para el proceso dentro del contenedor. |
| limits.memory_mb | number | 256 | Limite de memoria en megabytes. Aplicado por el kernel via cgroups v2. |
| limits.cpu | number | 1.0 | Limite de CPU como fraccion de un core (ej. 0.5, 2.0). |
| limits.pids | number | 128 | Numero maximo de procesos. Previene fork bombs. |
| mounts[] | object[] | [] | Bind mounts. Cada objeto tiene campos host, container y readonly. |
| mounts[].host | string | - | Ruta absoluta en el host para el bind mount. |
| mounts[].container | string | - | Ruta absoluta dentro del contenedor para el punto de montaje. |
| mounts[].readonly | boolean | false | Si el montaje es de solo lectura. |
| bridge.name | string | "hull0" | Nombre del dispositivo bridge en el host. |
| bridge.subnet | string | "10.88.0.0/24" | Subred para asignacion de IP. |
| bridge.ip | string | (auto) | IP estatica para el contenedor. Se asigna automaticamente si se omite. |
| bridge.mtu | number | 1500 | MTU 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.
{
"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.
{
"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.
{
"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.
| Perfil | Syscalls | Runtimes Objetivo | Notas |
| default | 122 | Rust musl, Zig, Go, shell scripts | I/O + red + gestion de procesos + pipelines de shell. La linea base para servidores de binario unico. Excluye io_uring intencionalmente (historial de CVEs). |
| webapp | default + 3 | Node.js 22+, Next.js SSR, runtimes userspace modernos | Default 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). |
| node | 32 | Node.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. |
| dotnet | 36 | .NET 8/9 (CoreCLR, NativeAOT) | select/pselect6 + signalfd4 + memfd_create (staging de codigo JIT) + tgkill (signals de pthread). Minimo. |
| beam | 177 | Elixir, Erlang, Phoenix (BEAM VM) | Default + 55 extras: timerfd, signalfd, inotify, memfd_create, epoll_create, operaciones de archivo legacy (mkdir/unlink/chmod/chown). |
| java | permisivo | OpenJDK 8/11/17/21, Mirth Connect, apps JVM empaquetadas con install4j | Tabla 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_moduleSe 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.
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:
$ 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 lossModo 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:
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:
| Prioridad | Fuente | Ruta Tipica |
| 1 | HULL_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
$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 mismoEl 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:
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: