Hull Runtime
Hull es un runtime de contenedores Linux daemonless. Un solo binario Zig static-musl de ~3 MB orquesta namespaces, cgroups v2, seccomp-bpf, Landlock y pivot_root — sin daemon, sin containerd, sin shim. Cada hull run forkea el workload, escribe estado a disco y sale. Comandos como ps, stop e inspect leen ese directorio de estado.
Referencia CLI
| hull run [--rootless] <manifest> | Iniciar contenedor desde manifest JSON. --rootless fuerza NEWUSER incluso como root |
| hull ps | Listar contenedores: nombre, PID, uptime, argv[0] |
| hull stop <name> | Parar gracioso (SIGTERM) |
| hull kill <name> | Parar inmediato (SIGKILL) |
| hull logs <name> | Imprimir stdout/stderr capturado |
| hull inspect <name> | Mostrar cgroup, namespaces, mount points |
| hull version | Imprimir version |
| hull help | Mostrar uso |
Codigos de salida: 0 exito, 1 error de uso, 2 error de runtime, 3 error de manifest, 127 execve fallo.
Especificacion del Manifest
Un manifest hull es un archivo JSON que describe el contenedor. Tres campos son requeridos; todo lo demas tiene defaults razonables.
Campos Requeridos
| name | string | — | Nombre del contenedor (1-64 chars, alfanumerico + guion + underscore) |
| rootfs | string | — | Ruta a directorio rootfs o archivo .tar.gz. Los archivos se extraen y cachean en /var/lib/hull/rootfs/<name>/ |
| argv | string[] | — | Comando a ejecutar dentro del contenedor |
Campos Opcionales
| env | string[] | [] | Variables de entorno (e.g. ["PORT=4500"]) |
| profile | string | "default" | Perfil seccomp: default, beam, dotnet o node |
| network | string | "none" | Modo de red: none (loopback), host (compartida) o bridge (veth) |
| bridge.name | string | "hull0" | Nombre del bridge (solo si network=bridge) |
| bridge.subnet | string | "10.88.0.0/24" | CIDR del subnet (solo /24 soportado) |
| bridge.ip | string | "" (auto) | IP del contenedor. Vacio = auto-asignar via lease dir |
| bridge.mtu | number | 0 | MTU del par veth. 0 = default del kernel (1500) |
| hostname | string | name | Hostname del contenedor (namespace UTS) |
| cwd | string | "/" | Directorio de trabajo dentro del contenedor antes del execve |
| limits.memory_mb | number | 0 | Limite de memoria en MB. 0 = sin limite |
| limits.cpu | number | 0 | Fraccion de CPU (1.0 = un core). 0 = sin limite |
| limits.pids | number | 0 | Max procesos. 0 = sin limite |
| mounts[].host | string | — | Ruta fuente en el host para bind mount |
| mounts[].container | string | — | Ruta destino en el contenedor |
| mounts[].readonly | boolean | false | Montar como solo lectura |
Ejemplo: Titan ESB (Elixir/Phoenix)
{
"name": "titan",
"rootfs": "/var/lib/hull/rootfs/titan",
"argv": ["/opt/titan/bin/titan", "start"],
"env": [
"PHX_SERVER=true",
"PORT=4500",
"PHX_HOST=www.titan-bus.com",
"LANG=en_US.UTF-8",
"HOME=/tmp",
"RELEASE_TMP=/tmp"
],
"profile": "beam",
"network": "host",
"hostname": "titan",
"cwd": "/opt/titan",
"mounts": [
{ "host": "/dev/null", "container": "/dev/null" },
{ "host": "/dev/urandom", "container": "/dev/urandom" },
{ "host": "/etc/resolv.conf", "container": "/etc/resolv.conf", "readonly": true },
{ "host": "/etc/ssl/certs", "container": "/etc/ssl/certs", "readonly": true }
],
"limits": { "memory_mb": 1024, "cpu": 2.0, "pids": 4096 }
}Ejemplo: Contenedor con Bridge
{
"name": "webapp",
"rootfs": "/var/lib/hull/rootfs/webapp",
"argv": ["/usr/local/bin/node", "server.js"],
"env": ["PORT=3000", "NODE_ENV=production"],
"profile": "node",
"network": "bridge",
"bridge": { "subnet": "10.88.0.0/24" },
"hostname": "webapp",
"cwd": "/app",
"limits": { "memory_mb": 256, "cpu": 1.0, "pids": 128 }
}Perfiles Seccomp
Hull trae cuatro allowlists de syscalls curadas. El perfil se selecciona via el campo profile del manifest y se instala justo antes del execve — incluso el primer syscall del workload esta filtrado. Cualquier syscall fuera de la lista gatilla KILL_PROCESS (no EPERM como el default de Docker).
| Perfil | Syscalls | Uso | Extras notables |
| default | 122 | Binarios estaticos Rust musl, Zig, Go; scripts shell | execve/clone/clone3 para pipelines shell, copy_file_range para coreutils |
| beam | 177 | Elixir, Erlang, Phoenix — la BEAM VM | +55 extras: timerfd, signalfd, inotify, memfd_create, legacy mkdir/rmdir/unlink/rename/chmod/chown |
| node | 32 | Node.js, Deno, Bun — event loop libuv | epoll_create1, epoll_wait, eventfd, signalfd, timerfd |
| dotnet | 36 | .NET 8/9 (CoreCLR, NativeAOT) | select, pselect6, signalfd4, memfd_create (staging JIT), tgkill (pthreads) |
Bloqueados en todos los perfiles: ptrace, process_vm_readv/writev, bpf, add_key/keyctl, userfaultfd, kexec_load, init_module.
Capas de Seguridad
Hull aplica siete capas de aislamiento, de la mas externa a la mas interna. Cada capa es independiente — el fallo de una no deshabilita las otras.
| 1 | User namespace | El proceso cree que es root; el host ve uid no privilegiado. Habilitado con --rootless |
| 2 | PID namespace | Arbol PID aislado. PID 1 del contenedor = tu workload. No puede ver ni signalear procesos del host |
| 3 | Network namespace | Stack de red aislado. Tres modos: none (solo loopback), host (compartido), bridge (par veth + NAT) |
| 4 | Mount namespace | pivot_root a rootfs dedicado. Filesystem del host completamente invisible (no chroot — enforcement del kernel) |
| 5 | cgroups v2 | Limites duros de CPU, memoria y PIDs. Enforced por kernel. El contenedor no puede fork-bomb ni agotar RAM del host |
| 6 | Landlock LSM | Allowlist de filesystem. Default: rootfs read+exec, /tmp read+write. Ni uid 0 dentro del contenedor puede bypassear. Skip gracioso en kernels < 5.13 |
| 7 | seccomp-bpf | Allowlist de syscalls por perfil de workload. KILL_PROCESS en violacion. Instalado justo antes del execve |
Networking Bridge
Pon "network": "bridge" y hull crea un stack de networking veth-bridge completo por contenedor:
- Crea el bridge
hull0(idempotente) con IP gateway10.88.0.1/24 - Habilita
ip_forwarde instala regla masquerade nftables para el subnet - Inserta
iptables -I FORWARD 1 -i hull0 -j ACCEPTpara bypasear elpolicy DROPde Docker - Asigna la siguiente IP libre via lockfiles atomicos
O_EXCLen~/.hull/leases/ - Crea par veth; conecta host end al bridge, mueve container end al NEWNET del child
- Configura container side via
nsenter -t <pid> -n ip addr add ... - Al salir: lease liberado, veth auto-limpiado por kernel al terminar netns
$ sudo nsenter -t <pid> -n ip -br addr
lo UNKNOWN 127.0.0.1/8
eth0@if36548 UP 10.88.0.2/24
$ sudo 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
$ sudo nsenter -t <pid> -n ping -c 3 8.8.8.8
3 packets transmitted, 3 received, 0% packet loss
rtt min/avg/max = 0.256/0.389/0.523 msModo Rootless
Pasa --rootless (o ejecuta como usuario no-root) y hull usa un dance de tres procesos con pipes para montar el namespace NEWUSER:
orig_parent (uid del host)
│ fork
▼
userns_setup
│ unshare(NEWUSER) → señal parent "listo"
│ bloquea en pipe → parent escribe uid_map/gid_map
│ unshare(NEWPID|NEWNET|NEWNS|NEWUTS|NEWIPC)
│ fork
▼
workload (PID 1 en NEWPID, uid 0 en contenedor)
dup2 log_fd → pivot_root → landlock → seccomp → execveEstado y Logs
Hull no tiene daemon — el estado vive en disco. Precedencia de ubicacion:
| Estado | $HULL_STATE_DIR → $HOME/.hull/state → /var/run/hull/state → /tmp/.hull/state |
| Logs | $HULL_LOGS_DIR → $HOME/.hull/logs → /var/run/hull/logs → /tmp/.hull/logs |
| Leases | $HULL_LEASE_DIR → $HOME/.hull/leases → /var/run/hull/leases |
| Rootfs cache | /var/lib/hull/rootfs/<name>/ (extraido de archivos tar.gz) |
Integracion con Mentat
Cuando se usa como driver Mentat, hull es invocado por mentat-agent via la implementacion del trait HullDriver. El YAML config.driver: hull dispara al agent a ejecutar hull run <manifest>, registrar el endpoint en el registry de servicios, y auto-generar el bloque reverse proxy de Caddy.
services:
- name: titan
replicas: 1
config:
driver: hull
manifest: /etc/hull/titan.json
binary: /usr/local/bin/hull-sudo
port: 4500
ingress:
host: www.titan-bus.com
aliases:
- titan.getmentat.run
path: /
tls: true
security:
profile: hullLimitaciones Conocidas
- Solo x86_64 y aarch64 (tablas seccomp son especificas por arquitectura)
- Kernel ≥ 5.13 requerido para Landlock (fallback gracioso en kernels mas viejos)
- Solo cgroups v2 jerarquia unificada (sin fallback v1)
- Bridge mode solo soporta subnets /24 (254 IPs usables)
- Bridge mode omite NEWPID — el workload comparte PID namespace del host
- Sin registry de contenedores — rootfs debe ser path local o archivo
- Sin filesystem por capas — el rootfs es una copia completa, no capas overlayfs
- Reglas Landlock custom via manifest aun no implementadas (solo politica default)
- Modo rootless: cgroups son best-effort (la mayoria de hosts no delegan a usuarios no privilegiados)
Build y Deploy
cd /path/to/hull
zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseFast
# Binario: zig-out/bin/hull (~3.1 MB)
scp zig-out/bin/hull user@host:/usr/local/bin/hull
ssh user@host "hull version"
# hull 0.2.0 (zig 0.15.2)