fix(instalador): el bucle de la contraseña no terminaba nunca, y llenó 1,5 G de RAM del anfitrión
Los dos `while :;` que piden contraseña reintentaban SIN TOPE, y los `read_secret` —el de la
librería y el que traen los dos instaladores— se comen el fallo del `read` (`|| _sec=""`). Juntos:
con un stdin en EOF —un arnés, un `< /dev/null`, un pipe cerrado— el `read` falla, la variable sale
vacía, el `[ -n ]` no pasa, y el bucle imprime «no coinciden (o vacía) — reintentá» para siempre,
sin dormir ni un milisegundo.
ENCONTRADO EN VIVO, no leyendo el código: la raíz del anfitrión estaba al 96 % y un
`takana-live-install.sh` quedó HUÉRFANO colgado de PID 1 escribiendo **1,6 G en cuatro minutos** de
esas tres líneas. Dos cosas lo hacían invisible: su propio script ya estaba borrado del disco, y
nada en su línea de comandos nombraba el fichero que estaba llenando — se encontró recorriendo
`/proc/*/fd`. Y `/tmp` es **tmpfs**, así que eso no era disco: era **RAM**, en una caja de 7,7 G que
ya tenía 1,8 G en swap.
El tope va en el BUCLE y no sólo en el lector a propósito: los instaladores traen su propio
`read_secret` y la librería no manda sobre él, así que un bucle que se defiende solo es lo único que
protege en los tres casos. `CUENTA_EOF` corta en el primer intento cuando el lector sí es el
nuestro; el tope de `CUENTA_INTENTOS` (3) corta de todas formas.
Control en los cuatro casos, con stdin en EOF y 15 s de techo:
actual · lector de la librería SE COLGÓ 202 KB en 15 s
actual · lector del instalador SE COLGÓ 15,3 MB en 15 s
arreglado · lector de la librería terminó 704 bytes (corta en el primer EOF)
arreglado · lector del instalador terminó 998 bytes (corta por el tope)
Y lo que dice al cortar nombra la causa y la consecuencia, que es lo que faltaría para diagnosticarlo
desde un log: «⚠ no pude leer la contraseña de root (¿stdin sin terminal?) — root queda CERRADO».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Semantic delta
No definition changed: this change is text, documentation or data.
1 files · +35 −2
- scripts/lib/cuenta.sh +35 −2
diff --git a/scripts/lib/cuenta.sh b/scripts/lib/cuenta.sh
index 29be5e0c..cfad81a5 100644
--- a/scripts/lib/cuenta.sh
+++ b/scripts/lib/cuenta.sh
@@ -60,16 +60,35 @@ CUENTA_ROOT_HASH="${TAKANA_ROOT_HASH:-${HAMMER_ROOT_HASH:-}}"
command -v bb >/dev/null 2>&1 || bb() { busybox "$@" 2>/dev/null || command "$@"; }
command -v ask >/dev/null 2>&1 || ask() {
_a_v="$1"; printf '%s%s: ' "$2" "$([ -n "${3:-}" ] && printf ' [%s]' "$3")"
- IFS= read -r _a_r || _a_r=""
+ IFS= read -r _a_r || { _a_r=""; CUENTA_EOF=1; }
[ -n "$_a_r" ] || _a_r="${3:-}"
eval "$_a_v=\$_a_r"
}
command -v read_secret >/dev/null 2>&1 || read_secret() {
printf '%s: ' "$2"; stty -echo 2>/dev/null || true
- IFS= read -r _rs || _rs=""; stty echo 2>/dev/null || true; echo
+ IFS= read -r _rs || { _rs=""; CUENTA_EOF=1; }; stty echo 2>/dev/null || true; echo
eval "$1=\$_rs"
}
+# ── ⚠ POR QUÉ LOS BUCLES DE CONTRASEÑA SON ACOTADOS, Y NO ES UN GUSTO (2026-10-15) ─────────────
+# Los dos `while :;` de abajo reintentaban SIN TOPE, y los `read_secret` —el de acá y el que traen
+# los dos instaladores— se comen el fallo del `read` (`|| _sec=""`). Juntos: con un stdin en EOF
+# —un arnés, un `< /dev/null`, un pipe que se cerró— el `read` falla, la variable sale vacía, el
+# `[ -n ]` no pasa, y el bucle imprime «no coinciden (o vacía) — reintentá» para siempre, sin
+# dormir ni un milisegundo.
+#
+# MEDIDO en el anfitrión: un `takana-live-install.sh` quedó huérfano colgado de PID 1 y en CUATRO
+# MINUTOS escribió **1,5 G** de esas tres líneas. Y `/tmp` es **tmpfs**, así que eso no era disco:
+# era RAM, en una caja de 7,7 G que ya tenía 1,8 G en swap. El proceso no se veía: su propio script
+# ya estaba borrado y nada en su línea de comandos nombraba el fichero que estaba llenando.
+#
+# El tope va en el BUCLE y no sólo en el lector a propósito: los instaladores traen su propio
+# `read_secret` y la librería no manda sobre él, así que un bucle que se defiende solo es lo único
+# que protege en los tres casos. `CUENTA_EOF` corta en el primer intento cuando el lector SÍ es el
+# nuestro; el tope corta de todas formas.
+CUENTA_EOF=0
+CUENTA_INTENTOS="${CUENTA_INTENTOS:-3}"
+
# cuenta_hash <contraseña> — SHA-512 por `cryptpw`. Devuelve vacío si este busybox no lo trae, y el
# llamador lo dice: un hash vacío escribiría una cuenta SIN CONTRASEÑA, que es peor que no tenerla.
cuenta_hash() {
@@ -122,9 +141,16 @@ cuenta_preguntar() {
if [ -n "$CUENTA_USER" ]; then
if [ -z "$CUENTA_HASH" ]; then
if [ -n "$(cuenta_hash x)" ]; then
+ _cp_n=0
while :; do
+ _cp_n=$((_cp_n+1))
read_secret _cp_p1 "Contraseña para $CUENTA_USER"
read_secret _cp_p2 "Repetir"
+ if [ "$CUENTA_EOF" = 1 ] || [ "$_cp_n" -ge "$CUENTA_INTENTOS" ]; then
+ [ "$_cp_p1" = "$_cp_p2" ] && [ -n "$_cp_p1" ] || {
+ echo " ⚠ no pude leer una contraseña para $CUENTA_USER (¿stdin sin terminal?) — su cuenta queda CERRADA"
+ break; }
+ fi
[ "$_cp_p1" = "$_cp_p2" ] && [ -n "$_cp_p1" ] || { echo " no coinciden (o vacía) — reintentá"; continue; }
CUENTA_HASH=$(cuenta_hash "$_cp_p1")
[ -n "$CUENTA_HASH" ] || { echo " ⚠ no pude generar el hash"; break; }
@@ -150,9 +176,16 @@ cuenta_preguntar() {
echo
echo " Nadie puede escalar todavía ⇒ root necesita contraseña, o la máquina queda inadministrable."
if [ -n "$(cuenta_hash x)" ]; then
+ _cp_m=0
while :; do
+ _cp_m=$((_cp_m+1))
read_secret _cp_r1 "Contraseña de root"
read_secret _cp_r2 "Repetir"
+ if [ "$CUENTA_EOF" = 1 ] || [ "$_cp_m" -ge "$CUENTA_INTENTOS" ]; then
+ [ "$_cp_r1" = "$_cp_r2" ] && [ -n "$_cp_r1" ] || {
+ echo " ⚠ no pude leer la contraseña de root (¿stdin sin terminal?) — root queda CERRADO"
+ break; }
+ fi
[ "$_cp_r1" = "$_cp_r2" ] && [ -n "$_cp_r1" ] || { echo " no coinciden (o vacía) — reintentá"; continue; }
CUENTA_ROOT_HASH=$(cuenta_hash "$_cp_r1"); break
done