1# Plan — botar busybox, MEDIDO
2
3**Fecha:** 2026-09-21 · **Instrumento:** los 401 symlinks de applet del artefacto sellado
4`2a2b1280…-busybox` (contados en el store del worker), cruzados con el estado y la membresía de
5perfil de cada receta en `docs/state/build-state.json` y con `grep '[deps]' recipes/*.toml`.
6**Tabla applet-por-applet:** `docs/state/busybox-applets.tsv` (401 filas, generada, no escrita a mano).
7
8## Por qué existe este documento
9
10La pregunta fue «¿qué hay que hacer para botar busybox?», con la sospecha de que faltaba escribir
11mucho userland en Rust. **El número dice lo contrario, y el hueco está en otro lado:**
12
13> De los 401 applets, **148 ya tienen dueño sellado Y en perfil** (el applet es redundante en la
14> imagen), **125 tienen dueño sellado que NO viaja en ninguna imagen** (trabajo hecho y no cobrado),
15> y de los **128 sin dueño, 105 son basura que se borra**. El trabajo de escritura real son
16> **23 applets** repartidos en 8 piezas.
17
18Es la lección de `foot` y del §D de `plan-barrido-perfiles-y-servicios.md` otra vez: `uutils`,
19`findutils`, `findutils-xargs`, `diffutils` y **`brush`** están todos `sealed` con `perfiles: []`.
20Se construyeron, se sellaron, y ninguna imagen los lleva.
21
22## Son TRES frentes, no uno — y esto es lo que hace que «botar busybox» se lea como imposible
23
24Mezclarlos es el error que cuesta el trabajo. Tienen dificultad, dueño y riesgo distintos:
25
26| frente | qué es busybox ahí | dificultad |
27|---|---|---|
28| **A · Sandbox de build** | `[deps] build = [… "busybox" …]` en **24 recetas** — es el `sh`+coreutils que el sandbox del lab trae (`recipes/jq.toml:5-7`, `recipes/zlib-ng.toml:11`). Más el `/toolchain` mixto de Alpine con el swap de `docs/11-bootstrap.md:234-240` | media — no es runtime, no toca el producto |
29| **B · Runtime del producto** | los applets de `/bin` y `/sbin` de las imágenes. `USERLAND_COMPONENTS` (`crates/takana-bootstrap/src/lib.rs:406`) ya los ensombrece con los binarios Rust y retira el homónimo de los otros dirs (`lib.rs:676`) | **baja — la máquina ya está construida, falta munición** |
30| **C · Stage 1 / self-host / atestación** | `STAGE1_COMPONENTS` (`lib.rs:172`, el 4/4), `ATTEST_PATHS` como raíz-de-confianza (`lib.rs:465`), y el `exec` de las cards de `getty` y `sshd` (`lib.rs:429`, `:240`) | **alta — es el ADR 0008, y toca la evidencia de reproducibilidad `of_tree=9adefb82…`** |
31
32⚠ **La membresía de perfil de busybox es del frente A, no del B.** `busybox` figura en `cli`,
33`servidor`, los cuatro escritorios y `metal-tigerlake`, pero **no está declarado como raíz en
34`docs/state/targets.toml`** (los únicos hits ahí son comentarios). Llega por la clausura, porque 24
35recetas lo declaran build-dep: `abseil-cpp chrony cronie crun dwarves firmware-tigerlake
36ia-modelo-embeddings intel-ucode jq libmnl libnftnl linux linux-firmware llama-cpp logrotate
37nftables opensmtpd popt protobuf qdrant sof-firmware valkey wireless-regdb zlib-ng`. **Sacarlo de
38las imágenes empieza por ahí y no requiere escribir ni una línea de userland.**
39
40## El reparto de los 401
41
42| estado del dueño | n | qué significa |
43|---|---|---|
44| **sellada + en perfil** | 148 | el reemplazo ya viaja en la imagen: el applet es redundante hoy |
45| **sellada, sin perfil** | 125 | construido, sellado, y no viaja: falta declararlo |
46| **sin dueño** | 128 | de los cuales **105 son `BORRAR`** y **23 son trabajo real** |
47
48### 148 · Ya cubiertos y en la imagen — el applet sobra
49
50`util-linux` 48 · `iproute2` 19 · `procps-ng` 12 · `shadow` 14 · `kbd` 12 · `xz` 6 · `iputils` 5 ·
51`e2fsprogs` 5 · `bzip2` 3 · `cronie` 3 · `dhcpcd` 3 · `ncurses` 2 · `dosfstools` 2 · `chrony` 2 ·
52`vim` 2 · `less` `lsof` `pciutils` `usbutils` `mandoc` `socat` `patch` `binutils` `tar` `wget` 1 c/u.
53
54⚠ **24 de estos 148 NO son drop-in**, y están marcados en el TSV. El caso grande es la red:
55`iproute2` da `ip`/`ss`, **no** `ifconfig`/`route`/`arp`/`netstat` — eso es net-tools, que no está en
56el corpus. Igual con `addgroup`/`adduser` (shadow da `groupadd`/`useradd`), `udhcpc` (dhcpcd es otro
57cliente con otro contrato de hooks) y `nc` (socat tiene otra sintaxis). **Cada uno de esos 24 es una
58migración de llamadores, no un borrado.** Mirar quién los invoca en `scripts/` y en las cards antes.
59
60### 125 · Sellados y sin viajar — la munición que ya está fabricada
61
62| dueño | n | qué cubre |
63|---|---|---|
64| `uutils` | 90 | todo el núcleo POSIX: `ls cp mv rm cat head tail sort cut tr wc stat date dd chmod chown …` |
65| `arje-zero` | 20 | `init linuxrc runlevel halt poweroff reboot run-init switch_root` + los 12 de runit (`runsv sv svc svlogd chpst envdir setuidgid softlimit start-stop-daemon …`) |
66| `hammerd` | 4 | `syslogd klogd logger logread` |
67| `brush` | 3 | **`sh ash hush`** |
68| `gzip` | 3 | `gzip gunzip zcat` |
69| `diffutils` | 2 | `diff cmp` |
70| `findutils` / `findutils-xargs` / `libarchive` | 3 | `find` · `xargs` · `cpio` |
71
72⚠ **CORRECCIÓN (2026-09-21, al ejecutar el paso 1): los 24 de `arje-zero` y `hammerd` NO son
73gratis, y esta tabla decía mal.** Esas dos recetas reemplazan la **función**, no el **comando**:
74cada una instala UN binario (`-p arje-zero`, `-p hammerd`), así que no existe un `poweroff`, un
75`logger` ni un `setuidgid` que las respalde. «Cubierto» significaba «nadie necesita correr esto»,
76y para 5 de los 24 era falso. Ver los indultos del paso 1.
77
78### 128 · Sin dueño
79
80**105 se borran** (no se reemplazan, salen del `defconfig`): `dpkg dpkg-deb rpm rpm2cpio httpd ftpd
81ftpget ftpput tftp tftpd telnet telnetd inetd dnsd sendmail popmaildir lpd lpq lpr ubi* nand*
82nbd-client i2c* hdparm fdformat setserial powertop bootchartd fbsplash conspy ether-wake udhcpd
83udpsvd tcpsvd watchdog whois tree which unzip …` (lista completa en el TSV).
84
85**23 son el trabajo real:**
86
87| pieza | applets | qué es |
88|---|---|---|
89| **kmod** | `insmod rmmod lsmod modprobe depmod modinfo` (6) | no hay receta kmod en el corpus. En Rust: `finit_module` + parseo de `modules.dep`. Bien acotado |
90| **psmisc** | `killall killall5 pstree fuser` (4) | **`procps-ng` NO los trae.** Traer receta psmisc (C) o escribir |
91| **grep POSIX** | `grep egrep fgrep` (3) | `grep` GNU está sellado (sin perfil) como puente. El Rust nativo se escribe sobre `grep-matcher`/`grep-regex`/`grep-searcher` |
92| **gestor de /dev** | `mdev makedevs uevent` (3) | decisión: lo absorbe arje, o entra una receta eudev/mdevd |
93| **bc** | `bc dc` (2) | traer receta |
94| **getty** | `getty cttyhack` (2) | hoy la card de getty ejecuta `/bin/busybox` (`lib.rs:429`) |
95| **awk** | `awk` (1) | decisión abierta, ver abajo |
96| **sed** | `sed` (1) | `sed` GNU sellado y en los 7 perfiles; el Rust nativo está por escribir |
97| **dns** | `nslookup` (1) | traer receta bind-tools/doggo |
98
99## Las tres clases de reemplazo — y cuándo forkear (nunca)
100
101La pregunta era si hay que forkear los que «existen pero no son exactos». **No, y hay tres clases
102distintas que conviene no mezclar:**
103
104**1 · Adopción exacta.** `uutils/coreutils`, `uutils/findutils`, `uutils/diffutils`: reimplementación
105madura, MIT, receta Cargo con commit pinneado (ADR 0006), multicall. El patrón está probado y
106documentado en la cabecera de `recipes/uutils.toml`. Lo que falta de esta clase es
107**`uutils/procps`** y **`uutils/util-linux`** — receta calcada, coste bajo. ⚠ **Verificar su estado
108upstream antes de prometer los 60 applets que cubrirían**: `util-linux` en Rust era embrionario.
109
110**2 · «Existe pero no es exacto» — y forkear es la trampa.** `ripgrep` no es `grep` POSIX, `sd` no es
111`sed`, `fd` no es `find` (por eso la receta buena es `uutils/findutils` y no `fd`), `procs` no es
112`ps`, `bottom` no es `top`, `helix` no es `vi`. Todas están selladas y en los 7 perfiles, y **ninguna
113sirve de reemplazo**: cambian las banderas. Forkear ripgrep para que hable POSIX es peor que
114escribir — heredás su arquitectura de recursión+ignore y peleás contra ella para siempre. Lo correcto
115es usarlas **como librerías**: `grep-matcher`/`grep-regex`/`grep-searcher` están publicadas
116justamente para eso, y un `grep` POSIX encima son unos cientos de líneas.
117
118**3 · `awk` es el caso feo, y hay que decidirlo, no resolverlo de paso.** `recipes/goawk.toml` existe
119y está sellada, pero **goawk es Go, no Rust** — no cumple la premisa de soberanía de cadena de la
120Etapa C. `frawk` es Rust pero es otro dialecto (JIT, semántica propia). `gawk` es C con extensiones
121GNU que los `configure` del corpus sí usan. Las tres opciones son legítimas y ninguna es gratis.
122
123## El bloqueante, y la buena noticia
124
125**Sin `sh` POSIX no se bota busybox en ningún frente**: es el `exec` de las cards, el `/bin/sh` del
126producto y el shell del sandbox. Lo esperado era que fuera un proyecto de meses.
127
128**`recipes/brush.toml` (0.4.0, reubeno/brush, MIT, Rust, compat bash) está `sealed`.** Compila
129estático musl con zig cc y produce `/usr/bin/brush`.
130
131⚠ **Y está sin validar como `sh`.** La receta es un import crudo de nixpkgs y lo dice en su primera
132línea: «PUNTO DE PARTIDA, no final». No está en ningún perfil, nadie lo ejecutó como `/bin/sh`, y el
133sandbox de build es el consumidor más duro que existe (`configure` de autotools). **La unidad de
134trabajo #1 es medirlo, no adoptarlo.**
135
136## Orden de ataque
137
138Cada paso es una unidad de trabajo con su criterio de terminado. Los tres primeros **no tocan el
139frente C** y se pueden hacer hoy.
140
1411. ~~**Recortar el `defconfig`.**~~ **HECHO, 2026-09-21 — 401 → 277 applets. Veredicto al final.**
1422. **Declarar la munición que ya existe.** `uutils`, `findutils`, `findutils-xargs`, `diffutils`,
143 `brush`, `gzip`, `libarchive` a los perfiles de `targets.toml`. Terminado cuando `build-state.py`
144 los marca con perfil y la imagen los lleva.
1453. ~~**Medir `brush` como `/bin/sh`.**~~ **HECHO, 2026-09-21 — veredicto abajo: no sirve todavía
146 para el sandbox.** Queda pendiente medirlo como `/bin/sh` del PRODUCTO (cards + `product-boot-test.sh`),
147 que es un consumidor distinto y mucho más blando.
1484. **Las dos recetas de adopción baratas** (`uutils/procps`, `uutils/util-linux`), previa
149 verificación de madurez upstream. Cubren ~60 applets de los 148 «ya cubiertos por C».
1505. **`grep` y `sed` POSIX en Rust.** Es la escritura de mejor relación coste/beneficio y desbloquea
151 el `/toolchain` del frente A.
1526. **Migrar los 24 build-deps.** Cambiar `busybox` por el conjunto real en `[deps] build` de las 24
153 recetas. Terminado cuando `busybox` sale de los 7 perfiles.
1547. **La cola larga:** kmod, psmisc, gestor de /dev, getty, bc, dns. Después del shell, no antes.
1558. **ADR — `awk`**, y **ADR — el frente C** (sustituir busybox en `STAGE1_COMPONENTS` y
156 `ATTEST_PATHS`). No antes de que 1–7 estén cerrados.
157
158## La métrica, y el guardián que falta
159
160`docs/state/busybox-applets.tsv` convierte esto en un número que baja: **401 → 272 → … → 0**.
161Regenerarlo es contar los symlinks del artefacto sellado y cruzarlos con `build-state.json`.
162
163**Falta un guardián**, y sin él el paso 1 se deshace solo: un script que falle si alguna receta,
164script o card invoca un applet que ya salió del `defconfig`. Hoy nada lo detectaría — el fallo
165aparecería en boot, en otra máquina, semanas después.
166
167## Lo que este documento NO midió (y hay que decirlo así)
168
169- **El reparto applet→destino es juicio, no medida.** Lo medido son los 401 symlinks, el estado
170 `sealed`/perfiles de cada receta y las 24 recetas con `busybox` en `[deps] build`. La asignación de
171 dueño la escribí yo y hay que revisarla receta a receta.
172- **No se verificó la equivalencia de banderas de ninguna pareja.** Los 24 marcados `NO-drop-in` son
173 los que sé que difieren; el resto dice `sin-verificar` y eso es literal, no una forma de decir «va
174 a andar».
175- **No se midió qué applets usa el producto de verdad.** El `defconfig` instala 401 porque nadie
176 eligió; cuáles se ejecutan en una imagen viva no está medido. Eso abarataría el paso 1.
177- **`uutils/procps` y `uutils/util-linux` no se verificaron upstream.** Los ~60 applets que les
178 atribuyo son una hipótesis de trabajo.
179
180---
181
182# Veredicto del paso 3 — `brush` como `/bin/sh`, MEDIDO
183
184**Fecha:** 2026-09-21 · **Dónde:** `dev.gioser.net`, todo bajo `flock -o work/.farm-build.lock` ·
185**Contra qué:** busybox 1.36.1 `sh` (ash) como control, en la misma caja y la misma corrida.
186
187> **No sirve todavía como `/bin/sh` del SANDBOX DE BUILD.** Pasa el banco POSIX igual que busybox,
188> parsea todo el corpus, y aun así **0 de 3 recetas autotools construyen** (control: 3/3). Lo que
189> falla no es su parser: es que el `libtool` que su `configure` genera sale con un nivel de escapado
190> de menos y después no lo parsea ningún shell, ni él ni busybox.
191
192## Lo que se construyó
193
194`takana build recipes/brush.toml` en el worker selló **`b3:715237c78edd…`** — **el mismo hash que
195`build-state.json` traía del otro hub.** Dos máquinas distintas, bytes idénticos: reproducibilidad
196confirmada de paso, que no era el objetivo de la prueba.
197
198Binario: ELF x86-64 **estático** (sin loader), `brush 0.4.0`, **7,25 MB** — contra **1,23 MB** del
199busybox que trae los 401 applets. Botar busybox por brush, hoy, **engorda** la imagen.
200
201## Lo que PASA
202
203| prueba | brush | control busybox |
204|---|---|---|
205| **Banco POSIX** — 57 casos (`scripts/sh-banco-posix.sh`) | **56/57** | 56/57 |
206| **Las 1130 fases de shell del corpus** (`sh -n` sobre las fases de las 549 recetas que las tienen) | **0 rechazadas** | 0 rechazadas |
207| **`configure` de ffmpeg** — 8345 líneas, `sh -n` | rc=0 | rc=0 |
208| **Invocado como `sh`** (argv[0]) | ✅ | ✅ |
209| **El patrón de las cards de arje** (`sh -c '…; exec …'`) | ✅ | ✅ |
210
211La única divergencia del banco: **`shift` más allá de `$#` devuelve 2**, y POSIX/busybox/bash
212devuelven 1. Cosmético, pero un `set -e` que haga `shift` de más se comporta distinto.
213
214⚠ **Y el banco NO es prueba de suficiencia** — esto es lo que hay que recordar del ejercicio:
215**brush lo pasa entero y aun así no construye una sola receta autotools.** Un banco de casos
216sintéticos mide lo que a uno se le ocurre preguntar; el consumidor real mide lo que hay.
217
218## Lo que FALLA
219
220**Tres recetas autotools reales, con el único cambio de `/bin/sh` en una copia del lab:**
221
222```sh
223cp -al /opt/takana/.dev-fs/alpine $L/alpine # copia con hardlinks, el lab compartido intacto
224cp …-brush/usr/bin/brush $L/alpine/bin/brush
225ln -sfn /bin/brush $L/alpine/bin/sh # ← lo ÚNICO que cambia; sed/grep/awk siguen siendo busybox
226flock -o work/.farm-build.lock env TAKANA_ROOTFS=$L/alpine \
227 ./target/release/takana --store <store-tirable> build recipes/<r>.toml
228```
229
230| receta | control (busybox sh) | brush |
231|---|---|---|
232| `flex` | ✅ sellada | ❌ `make[1]: Error 2` |
233| `pkgconf` | ✅ sellada | ❌ `./libtool: syntax error at line 368 col 30` |
234| `oniguruma` | ✅ sellada | ❌ `make[1]: Error 2` |
235
236### La causa, hasta donde está medida
237
238**El `configure` corre entero y bien bajo brush.** Lo que sale mal es el `libtool` que ese
239`configure` GENERA. Regenerado el mismo fichero con cada shell y comparado, **25 líneas difieren, y
240todas en el mismo sentido: falta un nivel de escapado.**
241
242```
243 control (busybox) brush
244línea 62 ECHO="printf %s\\n" ECHO="printf %s\n"
245línea 132 file_magic_cmd="\$MAGIC_CMD" file_magic_cmd="$MAGIC_CMD"
246línea 176 …'s/^.*[ ]\\([ABCDGIRSTW]… …'s/^.*[ ]\([ABCDGIRSTW]…
247```
248
249Ese fichero ya no lo parsea **nadie** — ni brush ni busybox lo aceptan (`rc=2` los dos, misma línea
250368). Y el contraprueba cierra el caso: **el `libtool` del control lo parsea brush sin quejarse
251(`rc=0`)**. ⇒ El parser de brush está bien; lo que difiere es lo que su shell PRODUCE.
252
253### El mecanismo, encontrado (2026-09-21, después de escribir lo de arriba)
254
255Primero se escribió acá que la causa no estaba determinada, tras descartar las tres sospechas
256obvias —`echo` con barras invertidas, sustitución de comando preservando `\\`, y el
257`func_quote_for_eval` de libtool aplicado a mano—, que dan **idéntico** en ambos shells. La causa
258apareció bajando al `config.status` que genera el `libtool`. **Es de una línea:**
259
260```sh
261V=hola
262echo "a: [`echo \\$V`]" # POSIX: $V · brush: \hola
263echo "b: [`echo \\\\`]" # POSIX: \ · brush: \\
264echo "c: [`echo \$V`]" # POSIX: hola · brush: $V
265echo "d: [$(echo \\$V)]" # POSIX: \hola · brush: \hola ✅
266```
267
268**brush aplica a las comillas invertidas las reglas de `$( )`.** POSIX manda otra cosa: dentro de
269`` `…` `` la barra invertida conserva su significado literal **salvo** cuando precede a `$`,
270`` ` `` o `\`, donde se ELIMINA antes de parsear el texto del comando (POSIX.1-2024, 2.6.3). Que el
271caso **d** sí esté bien es lo que delata la causa.
272
273Por qué rompe libtool: el `config.status` generado trae este bucle, que decide si cada variable
274necesita escapado antes de escribirla en el `libtool`:
275
276```sh
277for var in SHELL ECHO PATH_SEPARATOR SED GREP …; do
278 case `eval \\$ECHO \\""\\$$var"\\"` in
279 *[\\\`\"\$]*) eval "lt_$var=\\\"\`\$ECHO \"\$$var\" | \$SED \"\$sed_quote_subst\"\`\\\"" ;;
280 *) eval "lt_$var=\\\"\$$var\\\"" ;;
281 esac
282done
283```
284
285Bajo brush ese `case` evalúa a **cadena vacía**, así que **todas** las variables caen en la rama sin
286escapar. De ahí las 25 líneas y de ahí que el `configure` termine bien: no falla nada, sólo sale mal
287escrito.
288
289**Reportado upstream: [reubeno/brush#1394](https://github.com/reubeno/brush/issues/1394).**
290
291## Y el otro dato, que no es un fallo pero decide
292
293**Latencia de arranque: 813 µs contra 91 µs de busybox — 8,9×** (300 invocaciones de `sh -c :`).
294Un `configure` de autotools levanta decenas de miles de shells: a ese factor, el sandbox entero se
295vuelve medible más lento. Es un argumento independiente del bug, y no se arregla arreglando el bug.
296
297## Qué queda de esto
298
2991. **brush NO entra al sandbox de build.** El frente A sigue con busybox hasta que esto se cierre.
3002. **El frente del producto sigue abierto**: las cards, la getty y la shell interactiva no generan
301 libtool. Ahí brush no tiene ninguna evidencia en contra — pero tampoco se probó, y el
302 `product-boot-test.sh` es la prueba que falta.
3033. **Reportado upstream: [reubeno/brush#1394](https://github.com/reubeno/brush/issues/1394)**, con
304 el reproductor de una línea, la tabla de los cuatro casos y el fragmento de `config.status`.
3054. **El paso 5 del plan (grep/sed POSIX en Rust) sube de prioridad**, porque no depende del shell.
3065. **No se tocó el lab compartido**: copia con hardlinks y stores tirables. El worker siguió
307 compilando su cola en paralelo, serializado por el `flock`.
308
309## ¿Parchar, o buscar otra opción?
310
311**Primero lo que cierra la pregunta barata: adelantar el pin NO sirve.** Nuestra receta apunta a
312`96a26d0c` (tag `brush-shell-v0.4.0`, mayo 2026), que sigue siendo el último release. Se construyó
313**`main` de hoy** (`737dd57e`, 2026-09-21 — cuatro meses y medio de trabajo encima) y **reproduce
314idéntico**, los cuatro casos. No hay nada que esperar de la rama.
315
316Lo que queda, en orden de coste:
317
318| opción | qué cuesta | qué deja |
319|---|---|---|
320| **Esperar a #1394** | cero | el upstream es activísimo (issue #1393 de anteayer) y el bug es acotado, pero no es nuestro calendario |
321| **Parchar brush en la receta** | un `.patch` en el árbol, como ya hacen 13 recetas | ⚠ es un parche **al parser de un shell**, que es el componente más cargado de todos. Y el bug apareció en el PRIMER build real: no es prueba de que sea el único |
322| **dash** | receta nueva, C, ~200 KB. Es el `sh` de referencia POSIX | resuelve el sandbox YA — y no avanza nada la soberanía en Rust |
323| **bash como `/bin/sh` del producto** | cero: ya está sellada Y en los 7 perfiles | tapa el frente B sin escribir nada. No sirve para el sandbox sin medirlo antes |
324| **Dejar busybox donde está** | cero | es lo que dice el plan: el shell NO es por donde se empieza |
325
326**La recomendación es la última, y no por conservadurismo.** El plan ya ordenaba los pasos 1, 2 y 5
327—recortar el defconfig, declarar lo sellado, escribir `grep`/`sed` POSIX— **sin depender del shell**,
328y valen 129 applets y dos piezas de userland. Gastar el turno en pelear el shell es empezar por el
329único frente que está bloqueado por terceros.
330
331Lo que sí conviene hacer ya, porque es gratis: **suscribirse a #1394** y **medir brush como `/bin/sh`
332del PRODUCTO** (`product-boot-test.sh` + las cards), que es un consumidor que no genera libtool y
333donde brush no tiene ninguna evidencia en contra.
334
335
336---
337
338# Paso 1 — el recorte, HECHO y MEDIDO
339
340**Fecha:** 2026-09-21 · **Dónde:** `dev.gioser.net`, bajo `flock -o`, contra un store tirable y con
341un directorio de recetas por hardlinks — el `recipes/` del worker no se tocó.
342
343> **401 → 277 applets.** 124 retirados, 5 indultados, binario de **1.230.976 → 977.096 bytes**
344> (−20,6 %). Nuevo hash `b3:a24fdbf6…` (el vigente era `2a2b1280…`). Y **3 de las 24 recetas que
345> declaran busybox build-dep se reconstruyeron y sellaron con el recortado**: el sandbox sigue en pie.
346
347## Lo que el ejercicio corrigió del plan
348
349**Mi tabla decía que los 24 applets de `arje-zero`/`hammerd` eran gratis. Es falso para 5 de ellos.**
350Esas recetas instalan **un binario cada una** (`-p arje-zero`, `-p hammerd`): reemplazan la FUNCIÓN,
351no el COMANDO. No hay un `poweroff` ni un `setuidgid` detrás. Los cinco indultos, con su razón:
352
353| indultado | por qué se queda |
354|---|---|
355| `setuidgid` | la card de **gitea** hace `setuidgid gitea` (`recipes/gitea.toml:89`, «applet de busybox, comprobado presente»). Sin él el servicio arranca, muere y reintenta para siempre |
356| `switch_root` | el `/init` del initramfs del instalador lo ejecuta (`scripts/takana-live-install.sh:375`), **antes** de que arje exista |
357| `poweroff` · `halt` · `reboot` | **nada más en el corpus apaga la máquina.** arje-zero es PID 1, no una CLI. Quitarlos deja un producto que no se puede apagar desde la shell |
358
359Los tres primeros los encontró el vigía; los tres últimos, preguntarse qué instala arje-zero de
360verdad en vez de creerle a mi propia tabla.
361
362## El vigía — y por qué un vigía ingenuo no sirve
363
364`scripts/busybox-vigia.py` falla si alguna receta, script o card invoca un applet retirado. **Lee la
365lista de la propia receta** (`RETIRADOS='…'`) cruzada con la columna `simbolo_kconfig` del TSV, así
366que no hay dos listas que se desincronicen.
367
368Su primera versión daba **91 hits y ninguno real**. Las cuatro reglas que lo bajaron a 0, todas
369medidas contra falsos positivos concretos:
370
371| regla | qué mataba |
372|---|---|
373| quitar comentarios antes de buscar | `# busybox provee sendmail, no mail` (logrotate), `/// los dos fsync no son prolijidad` |
374| en Rust/Python, mirar sólo cadenas **que parezcan una línea de comandos** | `"init"` como nombre de capacidad, `tree` como variable, `rx = re.compile(…)` |
375| ignorar nombres que el propio script define como función | `$(ts)` en los seis scripts de `scripts/farm/` |
376| ignorar patrones de `case` y `command -v X` | `proc\|sys\|dev\|init) continue ;;` · `command -v dpkg` en el censo de la mudanza |
377
378Y `[` / `[[` no se vigilan: son palabras del shell, y todo script bash del hub los usa sin que eso
379sea una llamada a un applet.
380
381⚠ **El vigía mira ESTE repo.** No ve los `configure` de terceros que corren dentro del sandbox, que
382son quienes podrían llamar a `which` o `unzip`. Por eso la verificación de abajo no es opcional.
383
384## El mapa applet → símbolo NO se deriva por mayúsculas
385
386Sale de las líneas `//applet:` del fuente de busybox 1.36.1 y vive en la columna `simbolo_kconfig`
387del TSV. Poner el nombre en mayúsculas **falla en 6 casos medidos**: `[`→`TEST1`, `[[`→`TEST2`,
388`arch`→`BB_ARCH`, `nbd-client`→`NBDCLIENT`, `sh`→`SH_IS_ASH`, `sysctl`→`BB_SYSCTL`. Dos de esos seis
389estaban en la lista a retirar.
390
391## Verificación
392
393| qué | resultado |
394|---|---|
395| applets tras el recorte | **277** (401 − 124), exactamente los previstos |
396| los 5 indultados presentes | ✅ y también `sh` y `getty` |
397| los que debían irse | ✅ `httpd telnetd dpkg rpm init syslogd runsv tree which unzip` fuera |
398| **el sandbox sigue construyendo** | ✅ `zlib-ng`, `jq` y `logrotate` reconstruidas y selladas con el busybox recortado |
399
400⚠ **Son 3 de las 24, no las 24.** Las otras 21 —entre ellas `linux`, `llama-cpp`, `protobuf`,
401`qdrant`— no se probaron: son horas de cómputo en una caja que está con la campaña de KDE. Si alguna
402usa un applet retirado, se verá en su próximo build y el vigía **no** lo habría anticipado, porque el
403llamador estaría en un `configure` de terceros.
404
405## El coste, dicho antes de que sorprenda
406
407**Cambiar esta lista re-hashea busybox, y con él las 24 recetas que lo declaran build-dep**:
408`abseil-cpp chrony cronie crun dwarves firmware-tigerlake ia-modelo-embeddings intel-ucode jq libmnl
409libnftnl linux linux-firmware llama-cpp logrotate nftables opensmtpd popt protobuf qdrant
410sof-firmware valkey wireless-regdb zlib-ng`. **`linux` está en la lista**: el kernel se reconstruye.
411La granja lo hará en su propio ciclo; no se disparó a mano.
412
413## Lo que queda del paso 1
414
415- **Arrancar una imagen.** Lo medido es que el artefacto sella y que el sandbox construye. Que el
416 producto **bootee** con 277 applets no se probó: falta `product-boot-test.sh`.
417- **Las otras 21 dependientes**, cuando la granja llegue.
418- El siguiente corte natural son los `BORRAR` que no se tocaron por prudencia y los 272 que siguen:
419 el número baja a 0 por el paso 2 (declarar la munición sellada) y el 5 (`grep`/`sed` POSIX).
plan-botar-busybox.md
Plan — botar busybox, MEDIDO
Fecha: 2026-09-21 · Instrumento: los 401 symlinks de applet del artefacto sellado
2a2b1280…-busybox (contados en el store del worker), cruzados con el estado y la membresía de
perfil de cada receta en docs/state/build-state.json y con grep '[deps]' recipes/*.toml.
Tabla applet-por-applet: docs/state/busybox-applets.tsv (401 filas, generada, no escrita a mano).
Por qué existe este documento
La pregunta fue «¿qué hay que hacer para botar busybox?», con la sospecha de que faltaba escribir
mucho userland en Rust. El número dice lo contrario, y el hueco está en otro lado:
De los 401 applets, 148 ya tienen dueño sellado Y en perfil (el applet es redundante en la
imagen), 125 tienen dueño sellado que NO viaja en ninguna imagen (trabajo hecho y no cobrado),
y de los 128 sin dueño, 105 son basura que se borra. El trabajo de escritura real son
23 applets repartidos en 8 piezas.
Es la lección de foot y del §D de plan-barrido-perfiles-y-servicios.md otra vez: uutils,
findutils, findutils-xargs, diffutils y brush están todos sealed con perfiles: [].
Se construyeron, se sellaron, y ninguna imagen los lleva.
Son TRES frentes, no uno — y esto es lo que hace que «botar busybox» se lea como imposible
Mezclarlos es el error que cuesta el trabajo. Tienen dificultad, dueño y riesgo distintos:
| frente | qué es busybox ahí | dificultad |
| A · Sandbox de build | [deps] build = [… "busybox" …] en 24 recetas — es el sh+coreutils que el sandbox del lab trae (recipes/jq.toml:5-7, recipes/zlib-ng.toml:11). Más el /toolchain mixto de Alpine con el swap de docs/11-bootstrap.md:234-240 | media — no es runtime, no toca el producto |
| B · Runtime del producto | los applets de /bin y /sbin de las imágenes. USERLAND_COMPONENTS (crates/takana-bootstrap/src/lib.rs:406) ya los ensombrece con los binarios Rust y retira el homónimo de los otros dirs (lib.rs:676) | baja — la máquina ya está construida, falta munición |
| C · Stage 1 / self-host / atestación | STAGE1_COMPONENTS (lib.rs:172, el 4/4), ATTEST_PATHS como raíz-de-confianza (lib.rs:465), y el exec de las cards de getty y sshd (lib.rs:429, :240) | alta — es el ADR 0008, y toca la evidencia de reproducibilidad of_tree=9adefb82… |
⚠ La membresía de perfil de busybox es del frente A, no del B. busybox figura en cli,
servidor, los cuatro escritorios y metal-tigerlake, pero no está declarado como raíz en
docs/state/targets.toml (los únicos hits ahí son comentarios). Llega por la clausura, porque 24
recetas lo declaran build-dep: abseil-cpp chrony cronie crun dwarves firmware-tigerlake ia-modelo-embeddings intel-ucode jq libmnl libnftnl linux linux-firmware llama-cpp logrotate nftables opensmtpd popt protobuf qdrant sof-firmware valkey wireless-regdb zlib-ng. Sacarlo de
las imágenes empieza por ahí y no requiere escribir ni una línea de userland.
El reparto de los 401
| estado del dueño | n | qué significa |
| sellada + en perfil | 148 | el reemplazo ya viaja en la imagen: el applet es redundante hoy |
| sellada, sin perfil | 125 | construido, sellado, y no viaja: falta declararlo |
| sin dueño | 128 | de los cuales 105 son BORRAR y 23 son trabajo real |
148 · Ya cubiertos y en la imagen — el applet sobra
util-linux 48 · iproute2 19 · procps-ng 12 · shadow 14 · kbd 12 · xz 6 · iputils 5 ·
e2fsprogs 5 · bzip2 3 · cronie 3 · dhcpcd 3 · ncurses 2 · dosfstools 2 · chrony 2 ·
vim 2 · less lsof pciutils usbutils mandoc socat patch binutils tar wget 1 c/u.
⚠ 24 de estos 148 NO son drop-in, y están marcados en el TSV. El caso grande es la red:
iproute2 da ip/ss, no ifconfig/route/arp/netstat — eso es net-tools, que no está en
el corpus. Igual con addgroup/adduser (shadow da groupadd/useradd), udhcpc (dhcpcd es otro
cliente con otro contrato de hooks) y nc (socat tiene otra sintaxis). Cada uno de esos 24 es una
migración de llamadores, no un borrado. Mirar quién los invoca en scripts/ y en las cards antes.
125 · Sellados y sin viajar — la munición que ya está fabricada
| dueño | n | qué cubre |
uutils | 90 | todo el núcleo POSIX: ls cp mv rm cat head tail sort cut tr wc stat date dd chmod chown … |
arje-zero | 20 | init linuxrc runlevel halt poweroff reboot run-init switch_root + los 12 de runit (runsv sv svc svlogd chpst envdir setuidgid softlimit start-stop-daemon …) |
hammerd | 4 | syslogd klogd logger logread |
brush | 3 | sh ash hush |
gzip | 3 | gzip gunzip zcat |
diffutils | 2 | diff cmp |
findutils / findutils-xargs / libarchive | 3 | find · xargs · cpio |
⚠ CORRECCIÓN (2026-09-21, al ejecutar el paso 1): los 24 de arje-zero y hammerd NO son
gratis, y esta tabla decía mal. Esas dos recetas reemplazan la función, no el comando:
cada una instala UN binario (-p arje-zero, -p hammerd), así que no existe un poweroff, un
logger ni un setuidgid que las respalde. «Cubierto» significaba «nadie necesita correr esto»,
y para 5 de los 24 era falso. Ver los indultos del paso 1.
128 · Sin dueño
105 se borran (no se reemplazan, salen del defconfig): dpkg dpkg-deb rpm rpm2cpio httpd ftpd ftpget ftpput tftp tftpd telnet telnetd inetd dnsd sendmail popmaildir lpd lpq lpr ubi* nand* nbd-client i2c* hdparm fdformat setserial powertop bootchartd fbsplash conspy ether-wake udhcpd udpsvd tcpsvd watchdog whois tree which unzip … (lista completa en el TSV).
23 son el trabajo real:
| pieza | applets | qué es |
| kmod | insmod rmmod lsmod modprobe depmod modinfo (6) | no hay receta kmod en el corpus. En Rust: finit_module + parseo de modules.dep. Bien acotado |
| psmisc | killall killall5 pstree fuser (4) | procps-ng NO los trae. Traer receta psmisc (C) o escribir |
| grep POSIX | grep egrep fgrep (3) | grep GNU está sellado (sin perfil) como puente. El Rust nativo se escribe sobre grep-matcher/grep-regex/grep-searcher |
| gestor de /dev | mdev makedevs uevent (3) | decisión: lo absorbe arje, o entra una receta eudev/mdevd |
| bc | bc dc (2) | traer receta |
| getty | getty cttyhack (2) | hoy la card de getty ejecuta /bin/busybox (lib.rs:429) |
| awk | awk (1) | decisión abierta, ver abajo |
| sed | sed (1) | sed GNU sellado y en los 7 perfiles; el Rust nativo está por escribir |
| dns | nslookup (1) | traer receta bind-tools/doggo |
Las tres clases de reemplazo — y cuándo forkear (nunca)
La pregunta era si hay que forkear los que «existen pero no son exactos». No, y hay tres clases
distintas que conviene no mezclar:
1 · Adopción exacta. uutils/coreutils, uutils/findutils, uutils/diffutils: reimplementación
madura, MIT, receta Cargo con commit pinneado (ADR 0006), multicall. El patrón está probado y
documentado en la cabecera de recipes/uutils.toml. Lo que falta de esta clase es
uutils/procps y uutils/util-linux — receta calcada, coste bajo. ⚠ Verificar su estado
upstream antes de prometer los 60 applets que cubrirían: util-linux en Rust era embrionario.
2 · «Existe pero no es exacto» — y forkear es la trampa. ripgrep no es grep POSIX, sd no es
sed, fd no es find (por eso la receta buena es uutils/findutils y no fd), procs no es
ps, bottom no es top, helix no es vi. Todas están selladas y en los 7 perfiles, y ninguna
sirve de reemplazo: cambian las banderas. Forkear ripgrep para que hable POSIX es peor que
escribir — heredás su arquitectura de recursión+ignore y peleás contra ella para siempre. Lo correcto
es usarlas como librerías: grep-matcher/grep-regex/grep-searcher están publicadas
justamente para eso, y un grep POSIX encima son unos cientos de líneas.
3 · awk es el caso feo, y hay que decidirlo, no resolverlo de paso. recipes/goawk.toml existe
y está sellada, pero goawk es Go, no Rust — no cumple la premisa de soberanía de cadena de la
Etapa C. frawk es Rust pero es otro dialecto (JIT, semántica propia). gawk es C con extensiones
GNU que los configure del corpus sí usan. Las tres opciones son legítimas y ninguna es gratis.
El bloqueante, y la buena noticia
Sin sh POSIX no se bota busybox en ningún frente: es el exec de las cards, el /bin/sh del
producto y el shell del sandbox. Lo esperado era que fuera un proyecto de meses.
recipes/brush.toml (0.4.0, reubeno/brush, MIT, Rust, compat bash) está sealed. Compila
estático musl con zig cc y produce /usr/bin/brush.
⚠ Y está sin validar como sh. La receta es un import crudo de nixpkgs y lo dice en su primera
línea: «PUNTO DE PARTIDA, no final». No está en ningún perfil, nadie lo ejecutó como /bin/sh, y el
sandbox de build es el consumidor más duro que existe (configure de autotools). La unidad de
trabajo #1 es medirlo, no adoptarlo.
Orden de ataque
Cada paso es una unidad de trabajo con su criterio de terminado. Los tres primeros no tocan el
frente C y se pueden hacer hoy.
Recortar el defconfig. HECHO, 2026-09-21 — 401 → 277 applets. Veredicto al final.
- Declarar la munición que ya existe.
uutils, findutils, findutils-xargs, diffutils,
brush, gzip, libarchive a los perfiles de targets.toml. Terminado cuando build-state.py
los marca con perfil y la imagen los lleva.
Medir brush como /bin/sh. HECHO, 2026-09-21 — veredicto abajo: no sirve todavía
para el sandbox. Queda pendiente medirlo como /bin/sh del PRODUCTO (cards + product-boot-test.sh),
que es un consumidor distinto y mucho más blando.
- Las dos recetas de adopción baratas (
uutils/procps, uutils/util-linux), previa
verificación de madurez upstream. Cubren ~60 applets de los 148 «ya cubiertos por C».
grep y sed POSIX en Rust. Es la escritura de mejor relación coste/beneficio y desbloquea
el /toolchain del frente A.
- Migrar los 24 build-deps. Cambiar
busybox por el conjunto real en [deps] build de las 24
recetas. Terminado cuando busybox sale de los 7 perfiles.
- La cola larga: kmod, psmisc, gestor de /dev, getty, bc, dns. Después del shell, no antes.
- ADR —
awk, y ADR — el frente C (sustituir busybox en STAGE1_COMPONENTS y
ATTEST_PATHS). No antes de que 1–7 estén cerrados.
La métrica, y el guardián que falta
docs/state/busybox-applets.tsv convierte esto en un número que baja: 401 → 272 → … → 0.
Regenerarlo es contar los symlinks del artefacto sellado y cruzarlos con build-state.json.
Falta un guardián, y sin él el paso 1 se deshace solo: un script que falle si alguna receta,
script o card invoca un applet que ya salió del defconfig. Hoy nada lo detectaría — el fallo
aparecería en boot, en otra máquina, semanas después.
Lo que este documento NO midió (y hay que decirlo así)
- El reparto applet→destino es juicio, no medida. Lo medido son los 401 symlinks, el estado
sealed/perfiles de cada receta y las 24 recetas con busybox en [deps] build. La asignación de
dueño la escribí yo y hay que revisarla receta a receta.
- No se verificó la equivalencia de banderas de ninguna pareja. Los 24 marcados
NO-drop-in son
los que sé que difieren; el resto dice sin-verificar y eso es literal, no una forma de decir «va
a andar».
- No se midió qué applets usa el producto de verdad. El
defconfig instala 401 porque nadie
eligió; cuáles se ejecutan en una imagen viva no está medido. Eso abarataría el paso 1.
uutils/procps y uutils/util-linux no se verificaron upstream. Los ~60 applets que les
atribuyo son una hipótesis de trabajo.
Veredicto del paso 3 — brush como /bin/sh, MEDIDO
Fecha: 2026-09-21 · Dónde: dev.gioser.net, todo bajo flock -o work/.farm-build.lock ·
Contra qué: busybox 1.36.1 sh (ash) como control, en la misma caja y la misma corrida.
No sirve todavía como /bin/sh del SANDBOX DE BUILD. Pasa el banco POSIX igual que busybox,
parsea todo el corpus, y aun así 0 de 3 recetas autotools construyen (control: 3/3). Lo que
falla no es su parser: es que el libtool que su configure genera sale con un nivel de escapado
de menos y después no lo parsea ningún shell, ni él ni busybox.
Lo que se construyó
takana build recipes/brush.toml en el worker selló b3:715237c78edd… — el mismo hash que
build-state.json traía del otro hub. Dos máquinas distintas, bytes idénticos: reproducibilidad
confirmada de paso, que no era el objetivo de la prueba.
Binario: ELF x86-64 estático (sin loader), brush 0.4.0, 7,25 MB — contra 1,23 MB del
busybox que trae los 401 applets. Botar busybox por brush, hoy, engorda la imagen.
Lo que PASA
| prueba | brush | control busybox |
Banco POSIX — 57 casos (scripts/sh-banco-posix.sh) | 56/57 | 56/57 |
Las 1130 fases de shell del corpus (sh -n sobre las fases de las 549 recetas que las tienen) | 0 rechazadas | 0 rechazadas |
configure de ffmpeg — 8345 líneas, sh -n | rc=0 | rc=0 |
Invocado como sh (argv[0]) | ✅ | ✅ |
El patrón de las cards de arje (sh -c '…; exec …') | ✅ | ✅ |
La única divergencia del banco: shift más allá de $# devuelve 2, y POSIX/busybox/bash
devuelven 1. Cosmético, pero un set -e que haga shift de más se comporta distinto.
⚠ Y el banco NO es prueba de suficiencia — esto es lo que hay que recordar del ejercicio:
brush lo pasa entero y aun así no construye una sola receta autotools. Un banco de casos
sintéticos mide lo que a uno se le ocurre preguntar; el consumidor real mide lo que hay.
Lo que FALLA
Tres recetas autotools reales, con el único cambio de /bin/sh en una copia del lab:
cp -al /opt/takana/.dev-fs/alpine $L/alpine # copia con hardlinks, el lab compartido intacto
cp …-brush/usr/bin/brush $L/alpine/bin/brush
ln -sfn /bin/brush $L/alpine/bin/sh # ← lo ÚNICO que cambia; sed/grep/awk siguen siendo busybox
flock -o work/.farm-build.lock env TAKANA_ROOTFS=$L/alpine \
./target/release/takana --store <store-tirable> build recipes/<r>.toml
| receta | control (busybox sh) | brush |
flex | ✅ sellada | ❌ make[1]: Error 2 |
pkgconf | ✅ sellada | ❌ ./libtool: syntax error at line 368 col 30 |
oniguruma | ✅ sellada | ❌ make[1]: Error 2 |
La causa, hasta donde está medida
El configure corre entero y bien bajo brush. Lo que sale mal es el libtool que ese
configure GENERA. Regenerado el mismo fichero con cada shell y comparado, 25 líneas difieren, y
todas en el mismo sentido: falta un nivel de escapado.
control (busybox) brush
línea 62 ECHO="printf %s\\n" ECHO="printf %s\n"
línea 132 file_magic_cmd="\$MAGIC_CMD" file_magic_cmd="$MAGIC_CMD"
línea 176 …'s/^.*[ ]\\([ABCDGIRSTW]… …'s/^.*[ ]\([ABCDGIRSTW]…
Ese fichero ya no lo parsea nadie — ni brush ni busybox lo aceptan (rc=2 los dos, misma línea
368). Y el contraprueba cierra el caso: el libtool del control lo parsea brush sin quejarse
(rc=0). ⇒ El parser de brush está bien; lo que difiere es lo que su shell PRODUCE.
El mecanismo, encontrado (2026-09-21, después de escribir lo de arriba)
Primero se escribió acá que la causa no estaba determinada, tras descartar las tres sospechas
obvias —echo con barras invertidas, sustitución de comando preservando \\, y el
func_quote_for_eval de libtool aplicado a mano—, que dan idéntico en ambos shells. La causa
apareció bajando al config.status que genera el libtool. Es de una línea:
V=hola
echo "a: [`echo \\$V`]" # POSIX: $V · brush: \hola
echo "b: [`echo \\\\`]" # POSIX: \ · brush: \\
echo "c: [`echo \$V`]" # POSIX: hola · brush: $V
echo "d: [$(echo \\$V)]" # POSIX: \hola · brush: \hola ✅
brush aplica a las comillas invertidas las reglas de $( ). POSIX manda otra cosa: dentro de
`…` la barra invertida conserva su significado literal salvo cuando precede a $,
` o \, donde se ELIMINA antes de parsear el texto del comando (POSIX.1-2024, 2.6.3). Que el
caso d sí esté bien es lo que delata la causa.
Por qué rompe libtool: el config.status generado trae este bucle, que decide si cada variable
necesita escapado antes de escribirla en el libtool:
for var in SHELL ECHO PATH_SEPARATOR SED GREP …; do
case `eval \\$ECHO \\""\\$$var"\\"` in
*[\\\`\"\$]*) eval "lt_$var=\\\"\`\$ECHO \"\$$var\" | \$SED \"\$sed_quote_subst\"\`\\\"" ;;
*) eval "lt_$var=\\\"\$$var\\\"" ;;
esac
done
Bajo brush ese case evalúa a cadena vacía, así que todas las variables caen en la rama sin
escapar. De ahí las 25 líneas y de ahí que el configure termine bien: no falla nada, sólo sale mal
escrito.
Reportado upstream: reubeno/brush#1394.
Y el otro dato, que no es un fallo pero decide
Latencia de arranque: 813 µs contra 91 µs de busybox — 8,9× (300 invocaciones de sh -c :).
Un configure de autotools levanta decenas de miles de shells: a ese factor, el sandbox entero se
vuelve medible más lento. Es un argumento independiente del bug, y no se arregla arreglando el bug.
Qué queda de esto
- brush NO entra al sandbox de build. El frente A sigue con busybox hasta que esto se cierre.
- El frente del producto sigue abierto: las cards, la getty y la shell interactiva no generan
libtool. Ahí brush no tiene ninguna evidencia en contra — pero tampoco se probó, y el
product-boot-test.sh es la prueba que falta.
- Reportado upstream: reubeno/brush#1394, con
el reproductor de una línea, la tabla de los cuatro casos y el fragmento de
config.status.
- El paso 5 del plan (grep/sed POSIX en Rust) sube de prioridad, porque no depende del shell.
- No se tocó el lab compartido: copia con hardlinks y stores tirables. El worker siguió
compilando su cola en paralelo, serializado por el
flock.
¿Parchar, o buscar otra opción?
Primero lo que cierra la pregunta barata: adelantar el pin NO sirve. Nuestra receta apunta a
96a26d0c (tag brush-shell-v0.4.0, mayo 2026), que sigue siendo el último release. Se construyó
main de hoy (737dd57e, 2026-09-21 — cuatro meses y medio de trabajo encima) y reproduce
idéntico, los cuatro casos. No hay nada que esperar de la rama.
Lo que queda, en orden de coste:
| opción | qué cuesta | qué deja |
| Esperar a #1394 | cero | el upstream es activísimo (issue #1393 de anteayer) y el bug es acotado, pero no es nuestro calendario |
| Parchar brush en la receta | un .patch en el árbol, como ya hacen 13 recetas | ⚠ es un parche al parser de un shell, que es el componente más cargado de todos. Y el bug apareció en el PRIMER build real: no es prueba de que sea el único |
| dash | receta nueva, C, ~200 KB. Es el sh de referencia POSIX | resuelve el sandbox YA — y no avanza nada la soberanía en Rust |
bash como /bin/sh del producto | cero: ya está sellada Y en los 7 perfiles | tapa el frente B sin escribir nada. No sirve para el sandbox sin medirlo antes |
| Dejar busybox donde está | cero | es lo que dice el plan: el shell NO es por donde se empieza |
La recomendación es la última, y no por conservadurismo. El plan ya ordenaba los pasos 1, 2 y 5
—recortar el defconfig, declarar lo sellado, escribir grep/sed POSIX— sin depender del shell,
y valen 129 applets y dos piezas de userland. Gastar el turno en pelear el shell es empezar por el
único frente que está bloqueado por terceros.
Lo que sí conviene hacer ya, porque es gratis: suscribirse a #1394 y medir brush como /bin/sh
del PRODUCTO (product-boot-test.sh + las cards), que es un consumidor que no genera libtool y
donde brush no tiene ninguna evidencia en contra.
Paso 1 — el recorte, HECHO y MEDIDO
Fecha: 2026-09-21 · Dónde: dev.gioser.net, bajo flock -o, contra un store tirable y con
un directorio de recetas por hardlinks — el recipes/ del worker no se tocó.
401 → 277 applets. 124 retirados, 5 indultados, binario de 1.230.976 → 977.096 bytes
(−20,6 %). Nuevo hash b3:a24fdbf6… (el vigente era 2a2b1280…). Y 3 de las 24 recetas que
declaran busybox build-dep se reconstruyeron y sellaron con el recortado: el sandbox sigue en pie.
Lo que el ejercicio corrigió del plan
Mi tabla decía que los 24 applets de arje-zero/hammerd eran gratis. Es falso para 5 de ellos.
Esas recetas instalan un binario cada una (-p arje-zero, -p hammerd): reemplazan la FUNCIÓN,
no el COMANDO. No hay un poweroff ni un setuidgid detrás. Los cinco indultos, con su razón:
| indultado | por qué se queda |
setuidgid | la card de gitea hace setuidgid gitea (recipes/gitea.toml:89, «applet de busybox, comprobado presente»). Sin él el servicio arranca, muere y reintenta para siempre |
switch_root | el /init del initramfs del instalador lo ejecuta (scripts/takana-live-install.sh:375), antes de que arje exista |
poweroff · halt · reboot | nada más en el corpus apaga la máquina. arje-zero es PID 1, no una CLI. Quitarlos deja un producto que no se puede apagar desde la shell |
Los tres primeros los encontró el vigía; los tres últimos, preguntarse qué instala arje-zero de
verdad en vez de creerle a mi propia tabla.
El vigía — y por qué un vigía ingenuo no sirve
scripts/busybox-vigia.py falla si alguna receta, script o card invoca un applet retirado. Lee la
lista de la propia receta (RETIRADOS='…') cruzada con la columna simbolo_kconfig del TSV, así
que no hay dos listas que se desincronicen.
Su primera versión daba 91 hits y ninguno real. Las cuatro reglas que lo bajaron a 0, todas
medidas contra falsos positivos concretos:
| regla | qué mataba |
| quitar comentarios antes de buscar | # busybox provee sendmail, no mail (logrotate), /// los dos fsync no son prolijidad |
| en Rust/Python, mirar sólo cadenas que parezcan una línea de comandos | "init" como nombre de capacidad, tree como variable, rx = re.compile(…) |
| ignorar nombres que el propio script define como función | $(ts) en los seis scripts de scripts/farm/ |
ignorar patrones de case y command -v X | proc|sys|dev|init) continue ;; · command -v dpkg en el censo de la mudanza |
Y [ / [[ no se vigilan: son palabras del shell, y todo script bash del hub los usa sin que eso
sea una llamada a un applet.
⚠ El vigía mira ESTE repo. No ve los configure de terceros que corren dentro del sandbox, que
son quienes podrían llamar a which o unzip. Por eso la verificación de abajo no es opcional.
El mapa applet → símbolo NO se deriva por mayúsculas
Sale de las líneas //applet: del fuente de busybox 1.36.1 y vive en la columna simbolo_kconfig
del TSV. Poner el nombre en mayúsculas falla en 6 casos medidos: [→TEST1, [[→TEST2,
arch→BB_ARCH, nbd-client→NBDCLIENT, sh→SH_IS_ASH, sysctl→BB_SYSCTL. Dos de esos seis
estaban en la lista a retirar.
Verificación
| qué | resultado |
| applets tras el recorte | 277 (401 − 124), exactamente los previstos |
| los 5 indultados presentes | ✅ y también sh y getty |
| los que debían irse | ✅ httpd telnetd dpkg rpm init syslogd runsv tree which unzip fuera |
| el sandbox sigue construyendo | ✅ zlib-ng, jq y logrotate reconstruidas y selladas con el busybox recortado |
⚠ Son 3 de las 24, no las 24. Las otras 21 —entre ellas linux, llama-cpp, protobuf,
qdrant— no se probaron: son horas de cómputo en una caja que está con la campaña de KDE. Si alguna
usa un applet retirado, se verá en su próximo build y el vigía no lo habría anticipado, porque el
llamador estaría en un configure de terceros.
El coste, dicho antes de que sorprenda
Cambiar esta lista re-hashea busybox, y con él las 24 recetas que lo declaran build-dep:
abseil-cpp chrony cronie crun dwarves firmware-tigerlake ia-modelo-embeddings intel-ucode jq libmnl libnftnl linux linux-firmware llama-cpp logrotate nftables opensmtpd popt protobuf qdrant sof-firmware valkey wireless-regdb zlib-ng. linux está en la lista: el kernel se reconstruye.
La granja lo hará en su propio ciclo; no se disparó a mano.
Lo que queda del paso 1
- Arrancar una imagen. Lo medido es que el artefacto sella y que el sandbox construye. Que el
producto bootee con 277 applets no se probó: falta
product-boot-test.sh.
- Las otras 21 dependientes, cuando la granja llegue.
- El siguiente corte natural son los
BORRAR que no se tocaron por prudencia y los 272 que siguen:
el número baja a 0 por el paso 2 (declarar la munición sellada) y el 5 (grep/sed POSIX).