gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ main scripts/qorpa/qorpa-preflight.sh Shell · 220 líneas · 13.2 KB Historia crudo

qorpa: el mapeo por rango — subuid + --userns FD, y el impuesto se paga 89da9c822 · Sergio ·

1#!/usr/bin/env bash
2# qorpa-preflight.sh — ¿puede ESTA máquina alojar un huésped?
3#
4# ── POR QUÉ EXISTE ──────────────────────────────────────────────────────────────────────────────
5# El ADR 0015 dice, en su §Orden de trabajo 1, que lo PRIMERO es provisionar subuid «porque es lo
6# primero que va a fallar y define si el resto es fácil o difícil». Este script es esa medición,
7# hecha como guardián y no a mano: un huésped depende de cinco capacidades del ENTORNO que no
8# están en ningún grafo (user namespaces sin privilegios, mapeo multi-id, overlayfs sin root, los
9# nodos del borde, y disco), y las cinco fallan de formas que se parecen entre sí desde arriba
10# —«bwrap: Operation not permitted»— pero se arreglan de formas distintas.
11#
12# Es deliberadamente el patrón `yupana preflight`: PREDECIR el hueco de entorno antes de quemar
13# la campaña, en vez de descubrirlo con medio Fedora bajado. Y es deliberadamente RUIDOSO ante la
14# ausencia (CLAUDE.md regla 3): cada prueba dice qué faltó y cómo se repara, nunca sólo "no".
15#
16# ── LOS TRES NIVELES ────────────────────────────────────────────────────────────────────────────
17#   BLOQUEA  — ninguna instancia arranca en esta máquina.
18#   LIMITA   — arranca, pero una CLASE concreta de uso falla (se nombra cuál).
19#   NOTA     — informativo; no afecta a arrancar.
20# Salida: 0 todo verde · 1 hay BLOQUEA · 2 sólo LIMITA. El 2 es una respuesta legítima, no un error:
21# una instancia de sólo-correr-un-binario no necesita subuid.
22#
23# Uso:  scripts/qorpa/qorpa-preflight.sh [--raiz /var/lib/hammer/qorpa]
24set -uo pipefail   # -e NO: queremos correr TODAS las pruebas y reportar el cuadro entero.
25
26RAIZ="${QORPA_RAIZ:-/var/lib/hammer/qorpa}"
27[ "${1:-}" = "--raiz" ] && { RAIZ="$2"; shift 2; }
28
29BLOQUEA=0; LIMITA=0
30c_ok=$'\033[1;32m'; c_mal=$'\033[1;31m'; c_lim=$'\033[1;33m'; c_dim=$'\033[2m'; c_off=$'\033[0m'
31titulo() { printf '\n%s── %s %s\n' $'\033[1;34m' "$*" "$c_off"; }
32ok()     { printf '  %s✓%s %s\n' "$c_ok" "$c_off" "$*"; }
33nota()   { printf '  %s·%s %s\n' "$c_dim" "$c_off" "$*"; }
34mal()    { printf '  %s✗ BLOQUEA%s %s\n' "$c_mal" "$c_off" "$*"; BLOQUEA=$((BLOQUEA+1)); }
35lim()    { printf '  %s! LIMITA%s  %s\n' "$c_lim" "$c_off" "$*"; LIMITA=$((LIMITA+1)); }
36comofix(){ printf '      %s↳ %s%s\n' "$c_dim" "$*" "$c_off"; }
37
38echo "qorpa preflight — $(hostname) · kernel $(uname -r) · usuario $(id -un) ($(id -u))"
39
40# ────────────────────────────────────────────────────────────────────────────────────────────────
41titulo "1. el kernel trae lo necesario"
42CONFIG=""
43if [ -r /proc/config.gz ]; then CONFIG="zcat /proc/config.gz"
44elif [ -r "/boot/config-$(uname -r)" ]; then CONFIG="cat /boot/config-$(uname -r)"
45fi
46if [ -z "$CONFIG" ]; then
47  nota "sin .config legible (ni /proc/config.gz ni /boot/config-\$(uname -r)) — se mide por runtime"
48else
49  # Lo que el ADR §Lo que ya está construido enumera de linux-metal.toml:111-114.
50  for sym in USER_NS OVERLAY_FS SECCOMP SECURITY_LANDLOCK; do
51    v="$($CONFIG | grep -E "^CONFIG_${sym}=" | head -1)"
52    case "$v" in
53      *=y) ok "CONFIG_${sym}=y" ;;
54      *=m) nota "CONFIG_${sym}=m — módulo. NO es limitación por sí sola: quien manda es la prueba
55       funcional de más abajo, que lo monta de verdad o no." ;;
56      *)   if [ "$sym" = SECURITY_LANDLOCK ]; then
57             lim "CONFIG_${sym} apagado — la jaula cae a seccomp+namespaces, sin política de grano fino"
58           else
59             mal "CONFIG_${sym} apagado — sin esto no hay instancia"
60           fi ;;
61    esac
62  done
63fi
64
65max_ns="$(cat /proc/sys/user/max_user_namespaces 2>/dev/null || echo '?')"
66if [ "$max_ns" = 0 ]; then
67  mal "user.max_user_namespaces = 0 — los userns están apagados por sysctl"
68  comofix "sysctl -w user.max_user_namespaces=15000"
69else
70  ok "user.max_user_namespaces = $max_ns"
71fi
72if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then
73  if [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" = 1 ]; then
74    ok "kernel.unprivileged_userns_clone = 1"
75  else
76    mal "kernel.unprivileged_userns_clone = 0 — userns sin privilegios prohibidos"
77    comofix "sysctl -w kernel.unprivileged_userns_clone=1"
78  fi
79fi
80
81# ────────────────────────────────────────────────────────────────────────────────────────────────
82titulo "2. user namespaces sin privilegios"
83if timeout 10 unshare -Ur true 2>/dev/null; then
84  ok "unshare -Ur — userns con mapeo de un solo id"
85else
86  mal "unshare -Ur falla — sin esto NADA de qorpa funciona"
87fi
88# D6: pressure-vessel crea un userns DESDE DENTRO del nuestro. El ADR lo marca como «verificar, no
89# asumir», y es barato verificarlo acá en vez de con un juego real.
90if timeout 10 unshare -Ur unshare -Ur true 2>/dev/null; then
91  ok "userns ANIDADO — pressure-vessel de Valve puede crear el suyo adentro (ADR D6)"
92else
93  lim "userns anidado falla — Steam/pressure-vessel (ADR D6) no arrancaría; el resto sí"
94fi
95if command -v bwrap >/dev/null 2>&1; then
96  if timeout 10 bwrap --unshare-user --ro-bind / / /bin/true 2>/dev/null; then
97    ok "bwrap --unshare-user (la pieza que ya es dep del lab de build)"
98  else
99    mal "bwrap presente pero no puede crear el userns"
100  fi
101else
102  lim "no hay bwrap en el PATH del host — presente en el corpus (recipes/bwrap.toml), falta hidratarlo"
103fi
104
105# ────────────────────────────────────────────────────────────────────────────────────────────────
106titulo "3. subuid — el mapeo multi-id (ADR §NO-resuelve 2, «la primera cosa que va a fallar»)"
107USUARIO="$(id -un)"
108leer_rango() {  # $1 = fichero, $2 = usuario → imprime "inicio cantidad" o nada
109  [ -r "$1" ] || return 1
110  awk -F: -v u="$2" '$1==u {print $2, $3; exit}' "$1"
111}
112RU="$(leer_rango /etc/subuid "$USUARIO")"
113RG="$(leer_rango /etc/subgid "$USUARIO")"
114if [ -z "$RU" ] || [ -z "$RG" ]; then
115  lim "sin rango en /etc/subuid|/etc/subgid para '$USUARIO' — dnf/rpm rompen al hacer chown (tipo 1)"
116  comofix "usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USUARIO"
117else
118  ok "rango declarado: subuid=[$RU] subgid=[$RG]"
119fi
120
121herramienta_ok=1
122for h in newuidmap newgidmap; do
123  ruta="$(command -v $h 2>/dev/null)"
124  if [ -z "$ruta" ]; then
125    lim "$h no está — sin él el rango declarado es papel mojado"
126    comofix "receta shadow.toml ya lo instala; en el host, paquete 'shadow'"
127    herramienta_ok=0; continue
128  fi
129  caps="$(getcap "$ruta" 2>/dev/null)"
130  perm="$(stat -c %a "$ruta")"
131  if printf '%s' "$caps" | grep -q 'cap_set[ug]id'; then
132    ok "$ruta con $(printf '%s' "$caps" | sed 's/.* //')"
133  elif [ "${perm:0:1}" = 4 ] || [ "$(( 8#$perm & 04000 ))" -ne 0 ] 2>/dev/null; then
134    ok "$ruta setuid-root (modo $perm)"
135  else
136    cap=cap_setuid; mapa=uid_map
137    [ "$h" = newgidmap ] && { cap=cap_setgid; mapa=gid_map; }
138    lim "$ruta SIN setuid y SIN capability (modo $perm) — no puede escribir el $mapa"
139    comofix "sudo setcap ${cap}+ep $ruta"
140    herramienta_ok=0
141  fi
142done
143
144# La prueba FUNCIONAL. Las tres de arriba son condiciones necesarias declaradas; ésta es la que
145# realmente responde «¿puedo mapear el rango?». Se hace con un hijo en un userns SIN mapa que
146# espera en un fifo mientras el padre le escribe el mapa desde fuera — que es exactamente la
147# coreografía que hace un runtime de contenedores.
148if [ -n "$RU" ] && [ -n "$RG" ] && [ "$herramienta_ok" = 1 ]; then
149  d="$(mktemp -d)"; mkfifo "$d/listo" "$d/adelante"
150  set -- $RU; ui=$1; uc=$2
151  set -- $RG; gi=$1; gc=$2
152  # SIN `timeout` acá, y no es un descuido: `timeout` FORKEA, así que `$!` sería SU pid y no el
153  # del proceso que entró al namespace ⇒ newuidmap apuntaría al proceso equivocado y el kernel
154  # respondería "Operation not permitted" — un falso negativo que se lee igual que un fallo real.
155  # (Costó una medición: el guardián decía que subuid no andaba cuando a mano andaba.)
156  # El corte por tiempo lo ponen los `timeout` de los fifos de abajo.
157  unshare -U sh -c "echo r > $d/listo; read _ < $d/adelante; id -u > $d/uid; wc -l < /proc/self/uid_map > $d/lineas" &
158  hijo=$!
159  if timeout 10 head -c 1 "$d/listo" >/dev/null 2>&1; then
160    err="$( { newuidmap "$hijo" 0 "$(id -u)" 1 1 "$ui" "$uc" && \
161              newgidmap "$hijo" 0 "$(id -g)" 1 1 "$gi" "$gc"; } 2>&1 )"
162    rc=$?
163    timeout 5 sh -c "echo g > $d/adelante" 2>/dev/null
164    wait "$hijo" 2>/dev/null
165    if [ "$rc" = 0 ] && [ "$(cat "$d/uid" 2>/dev/null)" = 0 ] && [ "$(cat "$d/lineas" 2>/dev/null)" -ge 2 ] 2>/dev/null; then
166      ok "PRUEBA FUNCIONAL: el rango se mapeó ($uc uids) — dnf/rpm pueden chownear"
167    else
168      lim "PRUEBA FUNCIONAL falla: ${err:-mapa no aplicado} — root-adentro sí, multi-id no (tipo 1 cojo)"
169    fi
170  else
171    kill "$hijo" 2>/dev/null; lim "PRUEBA FUNCIONAL: el hijo no llegó al fifo (userns no se creó)"
172  fi
173  rm -rf "$d"
174else
175  nota "prueba funcional omitida — falta una condición previa de arriba"
176fi
177
178# ────────────────────────────────────────────────────────────────────────────────────────────────
179titulo "4. overlayfs sin privilegios (la capa mutable de D3)"
180if timeout 15 unshare -Urm sh -c '
181  mkdir -p /tmp/.qorpa-ovl 2>/dev/null || exit 90
182  mount -t tmpfs none /tmp/.qorpa-ovl || exit 91
183  mkdir -p /tmp/.qorpa-ovl/lower /tmp/.qorpa-ovl/upper /tmp/.qorpa-ovl/work /tmp/.qorpa-ovl/merged
184  mount -t overlay overlay -o lowerdir=/tmp/.qorpa-ovl/lower,upperdir=/tmp/.qorpa-ovl/upper,workdir=/tmp/.qorpa-ovl/work /tmp/.qorpa-ovl/merged
185' 2>/dev/null; then
186  ok "overlay montado dentro del userns — el upper/ de la instancia es viable sin root"
187else
188  lim "overlayfs no monta sin privilegios — la instancia queda de sólo lectura (o pide fuse-overlayfs)"
189  comofix "kernel ≥5.11 con CONFIG_OVERLAY_FS y userns; si no, fuse-overlayfs como plan B"
190fi
191
192# ────────────────────────────────────────────────────────────────────────────────────────────────
193titulo "5. el borde (D2) — qué hay para conceder"
194soc_wl="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/${WAYLAND_DISPLAY:-wayland-0}"
195[ -S "$soc_wl" ] && ok "wayland: $soc_wl" || nota "wayland: sin socket (no hay sesión gráfica acá)"
196soc_pw="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pipewire-0"
197[ -S "$soc_pw" ] && ok "pipewire: $soc_pw" || nota "pipewire: sin socket"
198if compgen -G "/dev/dri/*" >/dev/null; then ok "GPU: $(echo /dev/dri/* | tr ' ' ' ')"; else nota "GPU: sin /dev/dri (sin aceleración adentro)"; fi
199[ -e /dev/ntsync ] && ok "/dev/ntsync (juegos)" || nota "/dev/ntsync ausente — Proton cae a esync/fsync"
200[ -r /etc/resolv.conf ] && ok "/etc/resolv.conf legible (segundo fallo clásico de la 1ª corrida)" \
201                        || lim "/etc/resolv.conf ilegible — dnf no resuelve nombres adentro"
202
203# ────────────────────────────────────────────────────────────────────────────────────────────────
204titulo "6. disco (§NO-resuelve 5: un rootfs son 1-2 G y no lo alcanza store-gc)"
205destino="$RAIZ"; while [ ! -d "$destino" ] && [ "$destino" != / ]; do destino="$(dirname "$destino")"; done
206libre_k="$(df -Pk "$destino" | awk 'NR==2{print $4}')"
207libre_g=$(( libre_k / 1024 / 1024 ))
208if [ "$libre_g" -ge 8 ]; then ok "$destino: ${libre_g} G libres (raíz qorpa: $RAIZ)"
209elif [ "$libre_g" -ge 3 ]; then lim "$destino: sólo ${libre_g} G libres — alcanza para UNA imagen y nada más"
210else mal "$destino: ${libre_g} G libres — no entra ni un rootfs base"; fi
211
212# ────────────────────────────────────────────────────────────────────────────────────────────────
213titulo "veredicto"
214if [ "$BLOQUEA" -gt 0 ]; then
215  printf '  %sBLOQUEA %d · LIMITA %d%s — esta máquina NO puede alojar una instancia todavía.\n' "$c_mal" "$BLOQUEA" "$LIMITA" "$c_off"; exit 1
216elif [ "$LIMITA" -gt 0 ]; then
217  printf '  %sLIMITA %d%s — arranca, pero con las clases de uso marcadas arriba recortadas.\n' "$c_lim" "$LIMITA" "$c_off"; exit 2
218else
219  printf '  %stodo verde%s — esta máquina puede alojar una instancia qorpa.\n' "$c_ok" "$c_off"; exit 0
220fi