b3:46ff6fcf67bd23dece4d130c92855908520200f88ca85981317f81fcc56bb834
Construida con flock -o en dev.gioser.net (el LXC prestado, coste 0). El
artefacto trae dos ficheros: usr/bin/simi y su .hammer/recipe.toml.
Medido sobre el ARTEFACTO, no sobre el build del hub:
- ELF x86-64 estático, stripped, 498.736 bytes
- scripts/static-audit.sh simi: «estáticos de verdad: 1 · MIENTEN: 0», o sea que
el link=static no es una declaración sino un hecho (ese guardián existe porque
libtool lo ignora en silencio en otras recetas)
- banco diferencial, 159 casos contra el ash de busybox: 0 divergencias
(bash: 8, brush: 35)
- latencia 468 us contra 449 us del ash: 1,04x. brush: 1569 us, 3,5x
Y la lección que vale más que el número: el build NATIVO del hub (glibc,
dinámico) daba 865 us, 1,4x el ash. El estático musl baja a 1,04x. Medir el
binario que VIAJA, no el que se compila a mano — la diferencia fue del 35 % y
habría quedado escrita como si fuera del shell.
Sigue sin estar en ningún perfil de targets.toml a propósito: declararla es
meterla en ATTEST_PATHS y eso es frente C, su propia unidad de trabajo.
recipes/simi.toml sigue el patrón de hammerd/netup: crate del workspace a un
commit fijado, zig cc, musl estático, strip_debug. SIN [build.phases]: el
BuildSys::Cargo instala el binario, y el symlink `sh` NO lo pone la receta a
propósito — quién gana como /bin/sh lo decide USERLAND_COMPONENTS en
takana-bootstrap, que es frente C y su propia unidad de trabajo.
El SDD queda con el veredicto (simi 0 divergencias, bash 8, brush 35), el alcance
por consumidor, los siete bugs del camino y las seis cosas que faltan. Con dos
avisos explícitos: lo medido es el build NATIVO del hub (glibc, dinámico) y el
estático musl no se construyó, así que simi no entra en ningún perfil de
targets.toml; y la shell interactiva de getty no está cubierta.
Pregunta distinta a la de las cards, y el doc lo dice: scripts/ corre en el hub
bajo el bash del anfitrión, así que su superficie sólo obliga el día del
auto-alojamiento. Lo que importa hoy es si algún guion se declara POSIX y no lo
es, porque ése se rompe en la imagen, donde /bin/sh es el ash de busybox.
- 81 POSIX (10.167 líneas), 76 bash (10.269), 21 EMPOTRADOS por heredoc (los
/init que viajan a la imagen, y que hoy nadie parsea), 6 que se hacen source
- los 81 POSIX y los 21 empotrados los parsea el ash: CERO roto
- 48 de los 76 bash parsean tal cual bajo ash; de los 28 que no, 26 lo son por
arrays y nada más. Ése es el presupuesto entero de un sh propio para correr la
herramienta del proyecto
- 27 builtins, 257 órdenes por nombre desnudo y 35 por ruta absoluta; 0 applets
retirados, o sea que este inventario y busybox-vigia.py ahora coinciden
El escáner se extrajo a scripts/lib/sh_analisis.py para no tener dos copias que
divergan (ADR 0019 un piso más abajo). Comprobado que la extracción no cambió la
salida de sh-superficie-cards.py, byte a byte, y que el orden de los empates es
determinista para que sea diffable.
Van anotados los SIETE bugs del instrumento, que es la parte que enseña: los
comentarios sin quitar (89 backticks de prosa en cosecha-cron.sh), shlex
desincronizándose sobre el fichero entero, los cuerpos de heredoc (uno era
Python y aportaba la orden 'p' — sacarlos bajó las externas de 680 a 257), los
patrones de case dando 'init', los cuerpos de $(( )) dando 'i+1', los
descriptores dando '2', y la ruta absoluta confundida con applet. Ninguno se
veía en la salida: los siete daban números plausibles.
Tercer bug de la misma card, de la misma familia y el de peor diente: con un
directorio llamado «juan perez» en /home, el $u desnudo se partía en dos
campos, id -u juan perez fallaba, y el || continue saltaba a ese usuario EN
SILENCIO — se quedaba sin su /run/user/<uid>.
- u=$(basename "$h"), chown "$u", "/run/user/$uid"
- id -u izado a uid=$(id -u "$u" 2>/dev/null) || continue: se llamaba cuatro
veces por usuario (una para el test y tres para las órdenes), ahora una. No
era el bug, era la forma limpia de entrecomillarlo — y se ve en la medición,
que pasó de id×4 a id×1
- los dos echo de error, ahora entre comillas. Inocuo hoy (ningún glob en el
texto) y se comprobó que el mensaje y el rc=78 salen idénticos
Medido con el mismo banco de stubs sobre un /home de prueba con «juan perez»,
root y nadie: antes recibía chown sólo root, ahora root y «juan perez». «nadie»
no recibe en ninguno, que es lo correcto — no está en passwd. Parsea en
busybox, bash y brush.
Los dos los encontró scripts/sh-superficie-cards.py al inventariar las órdenes
externas de las cards. La card nació así en su único commit (3b305642): no fue
un accidente de edición posterior.
- awk NR==2{print } → awk 'NR==2{print $4}'
Sin comillas, awk daba error de sintaxis SIEMPRE y el echo final imprimía
«montar-trabajo: /vvv libres», sin el número, en cada arranque.
- grep -q /vvv /proc/mounts → grep -q " /vvv " /proc/mounts (y lo mismo
para /work/sergio)
El patrón desnudo matchea cualquier línea que CONTENGA la subcadena, así que
la comprobación «¿ya está montado?» podía dar verdadero por un /vvv/algo y
saltarse el mount en silencio. Latente: hace falta otra línea con esa
subcadena para que muerda.
Probado corriendo el fragmento entero con /proc/mounts sustituido por un
fichero de prueba y mkdir/chmod/chown/mount/blkid stubbeados — nada mutó la
máquina. Antes: en el escenario del falso positivo NO montaba /vvv. Después:
monta, y el echo trae el número. Parsea en busybox, bash y brush.
Contesta qué tendría que saber hacer un /bin/sh propio para que el producto
arranque, contado sobre el árbol y no sobre POSIX.
- 33 cards en cuatro poblaciones (recetas, bootstrap, servidor, mudanza); 23
ejecutan shell, 10 son exec directo
- el idioma es uno y se repite 18 veces: test … || { echo >&2; exit 78; }; exec …
- NO aparece nunca: if, funciones, ${…}, posicionales, $?, here-docs, subshells,
segundo plano — ni comillas invertidas, que es lo que tumbó a brush en el sandbox
- 8 builtins y 14 órdenes externas; cruzadas con busybox-applets.tsv, CERO
retiradas por el recorte 401 → 277: es el guardián que el plan pedía, y ahora
corre en un segundo
- las 23 parsean idéntico en busybox, bash y brush (banco diferencial, --files)
Hallazgo que hacía falta para medir bien: en una Card el argv va SIN argv[0] —
arje lo pone desde exec (lo dice la card de hammerd). Sin eso, el idioma de la
mudanza (exec=/bin/sh, argv=[-c, …]) desaparecía de la cuenta.
La detección respeta comillas con un escáner de estados: la primera versión
contaba 11 backticks que eran prosa dentro de mensajes de error.
brush#1396 abierto. Antes de escribirlo apareció que reubeno/brush ya marcaba
el fallo como known_failure en trap.yaml:226 (y :269, :323 para las otras dos
variantes), sin issue que lo siguiera.
Queda anotado porque es método, no anécdota: un known_failure en la suite del
candidato es evidencia de primera y cuesta un gh api mirarla. Buscar en las
PRUEBAS del candidato, no sólo en sus issues, antes de decir «hallazgo nuevo».
Lo que el reporte sí aporta sobre esos TODOs: que las tres variantes son
probablemente un mismo bug (on_exit() sólo lo llaman los puntos de entrada de
nivel superior; el subshell de interp.rs:662-683 clona el shell y devuelve el
exit code sin pasar por ahí), que también pega en pipeline y en segundo plano,
y que falla en silencio con rc intacto.
El banco POSIX de 57 casos lo pasa brush 56/57 y aun así no construye una
receta autotools: mide lo que a uno se le ocurrió preguntar. Este pregunta si
el candidato hace LO MISMO que el control sobre el material que hay.
- compara (rc, stdout, árbol de ficheros producido); stderr es aviso, no
divergencia — el texto de error diverge legítimamente
- el árbol es la columna que faltaba: el caso 10 diverge SÓLO ahí, que es la
clase de bug del libtool mal escapado (rc=0, stdout idéntico)
- toda divergencia se confirma re-corriendo control y candidato: separa caso
no determinista de shell inestable de divergencia real
- fuentes: fixtures de regresión, generador de tortura de comillas, las 1130
fases del corpus y configure/libtool reales en sh -n
Medido, 1273 casos en 5,5 s: brush 35 duras, bash 8, y CERO en los 1156 casos
de parseo. El generador saca el #1394 entero sin conocerlo. Hallazgo nuevo:
brush no ejecuta nunca el trap EXIT de un subshell — y eso golpea el frente
del PRODUCTO, donde el veredicto del paso 3 decía que no había evidencia en
contra.
Se ensamblaron dos Stage 1 completos y se arrancaron los dos, para que cualquier
diferencia se atribuya al recorte y no al azar:
base b3:1a3823b4... 400 applets qemu rc=0
recortado b3:3aa8cd2d... 278 applets qemu rc=0
Los dos dan shell y se apagan limpio. El diff de las dos consolas son 35 líneas y
ninguna es funcional: TSC, direcciones del RAMDISK, BogoMIPS, hora del RTC. La única
diferencia real es el conteo de applets desde dentro de la imagen: 410 contra 288.
Verificado en el recortado: arje-zero como PID 1, la seed card carga (la getty llegó
a incarnar), getty -l /bin/sh da prompt y responde, y poweroff -f apaga de verdad
-- uno de los cinco indultados, probado en vivo.
Corrido en el worker con QEMU en TCG: el LXC no expone /dev/kvm. Hubo que instalar
qemu-system-x86-core y reingerir la semilla de Stage 0, que no estaba en ese store y
salió con el mismo hash que el manifiesto ya anotaba.
Es Stage 1, no el producto: product-boot-test.sh sigue pendiente porque al store del
worker le faltan netup, openssh, uutils, findutils-xargs, diffutils y ripgrep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paso 1 de docs/plan-botar-busybox.md. 124 applets fuera del defconfig: los 105 que
no se reemplazan (dpkg, rpm, httpd, telnetd, ubi*, nand*, i2c*) y 19 cuya función ya
hace otro componente (init/runit -> arje-zero, syslogd/klogd/logread -> hammerd).
Binario de 1.230.976 a 977.096 bytes.
CORRIGE UN ERROR DEL PLAN: arje-zero y hammerd reemplazan la FUNCIÓN, no el COMANDO
-- son un binario cada uno. Por eso 5 indultos: setuidgid (la card de gitea lo
ejecuta), switch_root (el /init del instalador, antes de que arje exista) y
poweroff/halt/reboot (nada más en el corpus apaga la máquina).
scripts/busybox-vigia.py falla si alguien invoca un applet retirado; lee la lista de
la propia receta para que no haya dos que se desincronicen. Su primera versión daba
91 hits y ninguno real: las cuatro reglas que lo bajaron a 0 están documentadas.
El mapa applet->símbolo sale de las líneas //applet: del fuente, no de poner el
nombre en mayúsculas: eso falla en 6 casos medidos, dos de ellos en la lista.
Verificado: 277 applets exactos, los 5 indultados presentes, y zlib-ng, jq y
logrotate reconstruidas y selladas con el busybox recortado -- el sandbox sigue en
pie. Son 3 de las 24 dependientes, no las 24, y el boot de la imagen no se probó.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bajando al config.status que genera el libtool apareció el mecanismo, y es de una
línea: brush aplica a `...` las reglas de $( ). POSIX manda que dentro de comillas
invertidas la barra se ELIMINE cuando precede a $, ` o \ (2.6.3). Que el caso con
$( ) sí esté bien es lo que delata la causa.
Por eso rompe libtool: el case que decide si cada variable necesita escapado evalúa
a cadena vacía bajo brush, así que todas caen en la rama sin escapar. El configure
termina bien; el fichero sale mal escrito.
Se construyó main de hoy (737dd57e, cuatro meses y medio sobre nuestro pin) y
reproduce idéntico los cuatro casos ⇒ adelantar el pin no arregla nada.
Reportado: https://github.com/reubeno/brush/issues/1394
Y la recomendación: no pelear el shell. Los pasos 1, 2 y 5 del plan no dependen de
él y valen 129 applets y dos piezas de userland.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Medido en el worker, con el único cambio de /bin/sh en una copia por hardlinks del
lab (sed/grep/awk siguen siendo busybox) y stores tirables. Control busybox: 3/3
recetas selladas. brush: 0/3.
Lo que pasa: 56/57 del banco POSIX (igual que busybox), 0 rechazos sobre las 1130
fases de shell del corpus, parsea el configure de ffmpeg, funciona como argv[0]=sh
y con el patrón de las cards de arje.
Lo que falla: el configure corre entero pero el libtool que GENERA sale con un
nivel de escapado de menos en 25 líneas, y ese fichero ya no lo parsea ningún
shell. El parser de brush está bien — parsea sin quejarse el libtool del control.
Mecanismo exacto no determinado: echo, sustitución de comando y el propio
func_quote_for_eval de libtool dan idéntico en ambos shells aislados.
Y un dato independiente del bug: 813us de arranque contra 91us de busybox (8,9x),
sobre decenas de miles de shells por configure. El binario son 7,25 MB contra los
1,23 MB del busybox con sus 401 applets.
De paso: brush selló con el mismo hash que traía build-state del otro hub.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Medido contra el artefacto sellado (401 symlinks) cruzado con build-state.json:
148 applets ya tienen dueño sellado Y en perfil, 125 tienen dueño sellado que no
viaja en ninguna imagen (uutils, brush, findutils, diffutils, arje-zero, hammerd),
y de los 128 sin dueño 105 son basura que se borra.
Dos hallazgos que cambian el plan: busybox entra a los 7 perfiles como dep de BUILD
de 24 recetas, no como raíz de producto en targets.toml; y brush (shell Rust) ya
está sealed, sin validar y sin perfil.
docs/state/busybox-applets.tsv es la tabla applet-por-applet (generada), con 24
parejas marcadas NO-drop-in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>