tawasuyu

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 Sutichaykuna

⎇ 739b7423ea2ed4e42dbf30b292f4fa27d426db7e scripts/lib/cuenta.sh Shell · 385 siq'ikuna · 19.8 KB Ñawpa willay ch'uya

fix(instalador): el bucle de la contraseña no terminaba nunca, y llenó 1,5 G de RAM del anfitrión 739b7423e · Sergio ·

1#!/bin/sh
2# lib/cuenta.sh — LA CUENTA DE QUIEN VA A USAR LA MÁQUINA: se pregunta antes de instalar y se
3# escribe al terminar. Es la sección que faltaba en los dos instaladores.
4#
5# ── El defecto, medido en metal el 2026-10-14/15 ─────────────────────────────────────────────────
6# El operador instaló, arrancó, y **no podía hacer nada**: ni entrar como root, ni escalar. Lo dijo
7# así: *«al correr su me dice que debe ser suid, así que se cagó ese boot»* y *«¿de dónde salió el
8# usuario sergio? me di cuenta que debe ser una sección antes de instalar»*.
9#
10# Las dos preguntas tienen la misma respuesta, y es ésta: **nadie preguntó nunca por una cuenta.**
11#
12#   · `takana-live-install.sh` (disco entero) pregunta hostname, zona, teclado y la credencial de
13#     ROOT — y ningún usuario. En una imagen de escritorio eso es una máquina donde el greeter no
14#     puede autenticar a nadie, porque no hay nadie.
15#   · `takana-install-partition.sh` (el camino de `--keep`/`--adopt`, el que usó él) **no pregunta
16#     NADA**. Los usuarios salen de `lib/usuarios-del-home.sh`, que los cosecha del `/home` que se
17#     conserva —un usuario por directorio, con el uid de sus archivos— y les pone `!` en
18#     `/etc/shadow`, que es «cuenta sin contraseña posible». Correcto de su parte: inventar una
19#     contraseña sería peor. Pero nadie, después, la pedía.
20#
21# Resultado medido: `sergio:!` + `root:!` + ningún `doas.conf` = una máquina instalada, arrancada y
22# **inadministrable**. El encabezado de `takana-core/src/setuid.rs` lo había previsto textualmente
23# («hoy es LATENTE: el instalador sólo pone contraseña de root, así que no hay un humano no-root que
24# lo choque») y el 14 de octubre lo hubo.
25#
26# ── Por qué una librería y no dos secciones ──────────────────────────────────────────────────────
27# El mismo argumento que `lib/usuarios-del-home.sh` escribe para sí: son DOS caminos y UNA pregunta,
28# y dos implementaciones divergen en el caso raro. Acá el caso raro es justo el del operador —el
29# usuario YA existe, cosechado del `/home` conservado, y lo único que falta es su contraseña— y una
30# de las dos copias se lo habría perdido.
31#
32# ── La regla de root, sin criterio ───────────────────────────────────────────────────────────────
33# Root queda **cerrado** si el usuario puede escalar (hay `doas` en la imagen y se pidió que
34# administre): es lo correcto, y es lo que hacen las distros desde hace una década. Si NO puede
35# escalar, entonces se pide contraseña de root **obligatoriamente** — porque una máquina sin
36# ninguna de las dos no se administra, y eso es exactamente lo que acabamos de medir.
37#
38# Se usa así (los dos instaladores):
39#
40#     . "$(dirname "$0")/lib/cuenta.sh"
41#     cuenta_preguntar "$RAIZ_DEL_MEDIO"   # TUI; no pregunta si no hay tty o si es desatendido
42#     ...
43#     cuenta_aplicar "$ARBOL"              # escribe passwd/group/shadow/home/doas.conf
44#
45# Desatendido (arneses y `--unattended`): `TAKANA_USER`, `TAKANA_USER_HASH` (un hash, NO una
46# contraseña), `TAKANA_USER_PW` (sólo donde no haya más remedio), `TAKANA_ADMIN=1|0`,
47# `TAKANA_ROOT_PW` / `TAKANA_ROOT_HASH` como ya existían.
48#
49# ⚠ **La contraseña NO viaja por la cmdline del kernel.** `/proc/cmdline` lo lee cualquiera, así que
50# un hash ahí es un ataque de diccionario offline servido en bandeja. Por eso esto pregunta en el
51# instalador —que tiene tty— en vez de recibirlo del que hizo el kexec.
52
53CUENTA_USER="${TAKANA_USER:-}"
54CUENTA_HASH="${TAKANA_USER_HASH:-}"
55CUENTA_ADMIN="${TAKANA_ADMIN:-1}"
56CUENTA_ROOT_HASH="${TAKANA_ROOT_HASH:-${HAMMER_ROOT_HASH:-}}"
57
58# Los dos instaladores ya traen sus propios `ask`/`read_secret`/`bb`; si falta alguno (al usar la
59# librería desde un arnés) se definen acá. Un `command -v` sobre una función de shell la encuentra.
60command -v bb >/dev/null 2>&1 || bb() { busybox "$@" 2>/dev/null || command "$@"; }
61command -v ask >/dev/null 2>&1 || ask() {
62	_a_v="$1"; printf '%s%s: ' "$2" "$([ -n "${3:-}" ] && printf ' [%s]' "$3")"
63	IFS= read -r _a_r || { _a_r=""; CUENTA_EOF=1; }
64	[ -n "$_a_r" ] || _a_r="${3:-}"
65	eval "$_a_v=\$_a_r"
66}
67command -v read_secret >/dev/null 2>&1 || read_secret() {
68	printf '%s: ' "$2"; stty -echo 2>/dev/null || true
69	IFS= read -r _rs || { _rs=""; CUENTA_EOF=1; }; stty echo 2>/dev/null || true; echo
70	eval "$1=\$_rs"
71}
72
73# ── ⚠ POR QUÉ LOS BUCLES DE CONTRASEÑA SON ACOTADOS, Y NO ES UN GUSTO (2026-10-15) ─────────────
74# Los dos `while :;` de abajo reintentaban SIN TOPE, y los `read_secret` —el de acá y el que traen
75# los dos instaladores— se comen el fallo del `read` (`|| _sec=""`). Juntos: con un stdin en EOF
76# —un arnés, un `< /dev/null`, un pipe que se cerró— el `read` falla, la variable sale vacía, el
77# `[ -n ]` no pasa, y el bucle imprime «no coinciden (o vacía) — reintentá» para siempre, sin
78# dormir ni un milisegundo.
79#
80# MEDIDO en el anfitrión: un `takana-live-install.sh` quedó huérfano colgado de PID 1 y en CUATRO
81# MINUTOS escribió **1,5 G** de esas tres líneas. Y `/tmp` es **tmpfs**, así que eso no era disco:
82# 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
83# ya estaba borrado y nada en su línea de comandos nombraba el fichero que estaba llenando.
84#
85# El tope va en el BUCLE y no sólo en el lector a propósito: los instaladores traen su propio
86# `read_secret` y la librería no manda sobre él, así que un bucle que se defiende solo es lo único
87# que protege en los tres casos. `CUENTA_EOF` corta en el primer intento cuando el lector SÍ es el
88# nuestro; el tope corta de todas formas.
89CUENTA_EOF=0
90CUENTA_INTENTOS="${CUENTA_INTENTOS:-3}"
91
92# cuenta_hash <contraseña> — SHA-512 por `cryptpw`. Devuelve vacío si este busybox no lo trae, y el
93# llamador lo dice: un hash vacío escribiría una cuenta SIN CONTRASEÑA, que es peor que no tenerla.
94cuenta_hash() {
95	printf '%s' "$1" | bb cryptpw -m sha512 2>/dev/null || true
96}
97
98# cuenta_login_valido <nombre> — el mismo criterio que `lib/usuarios-del-home.sh` usa al cosechar,
99# porque los dos juegos de usuarios tienen que poder convivir en el mismo `/etc/passwd`.
100cuenta_login_valido() {
101	case "$1" in
102		"" | -* | *[!a-z0-9_-]*) return 1 ;;
103		root | sshd | bin | daemon | nobody | messagebus) return 1 ;;
104		*) [ "${#1}" -le 32 ] ;;
105	esac
106}
107
108# cuenta_preguntar [raíz-del-medio] — la sección. No pregunta si no hay tty, si es desatendido, o si
109# ya vino todo por el ambiente.
110cuenta_preguntar() {
111	_cp_raiz="${1:-/}"
112	[ "${TAKANA_UNATTENDED:-0}" = 1 ] && return 0
113	# ⚠ La condición es la MISMA que usa el instalador para decidir si es interactivo, y no sólo
114	# `[ -t 0 ]`: con stdin alimentado por un heredoc —que es como se prueba una TUI sin una persona
115	# delante— no hay tty, y la sección se saltaba entera dejando el arnés en verde sobre un
116	# instalador que no preguntaba nada. Medido escribiéndolo.
117	[ -t 0 ] || [ "${TAKANA_INSTALL_INTERACTIVE:-${HAMMER_INSTALL_INTERACTIVE:-}}" = 1 ] || return 0
118	[ -n "$CUENTA_USER" ] && [ -n "$CUENTA_HASH" ] && return 0
119
120	# ¿Hay con qué escalar en la imagen que se está instalando? La respuesta cambia la regla de root,
121	# así que se MIRA y no se supone.
122	_cp_doas=no
123	for _d in usr/bin/doas bin/doas usr/local/bin/doas usr/bin/sudo; do
124		[ -e "$_cp_raiz/$_d" ] && { _cp_doas=sí; break; }
125	done
126
127	echo
128	echo "Tu cuenta en esta máquina"
129	echo "  ─────────────────────────"
130	echo "  Es la que vas a usar para entrar al escritorio. Sin una cuenta con contraseña,"
131	echo "  el escritorio arranca y no deja entrar a nadie."
132	while :; do
133		ask CUENTA_USER "Nombre de usuario" "${CUENTA_USER:-}"
134		[ -z "$CUENTA_USER" ] && { echo "  (sin usuario: la máquina queda sólo con root)"; break; }
135		cuenta_login_valido "$CUENTA_USER" && break
136		echo "  ✗ «$CUENTA_USER» no sirve de login: minúsculas, dígitos, «_» y «-», sin empezar con «-»,"
137		echo "    y no puede ser un usuario de sistema (root, sshd, nobody…)."
138		CUENTA_USER=""
139	done
140
141	if [ -n "$CUENTA_USER" ]; then
142		if [ -z "$CUENTA_HASH" ]; then
143			if [ -n "$(cuenta_hash x)" ]; then
144				_cp_n=0
145				while :; do
146					_cp_n=$((_cp_n+1))
147					read_secret _cp_p1 "Contraseña para $CUENTA_USER"
148					read_secret _cp_p2 "Repetir"
149					if [ "$CUENTA_EOF" = 1 ] || [ "$_cp_n" -ge "$CUENTA_INTENTOS" ]; then
150						[ "$_cp_p1" = "$_cp_p2" ] && [ -n "$_cp_p1" ] || {
151							echo "  ⚠ no pude leer una contraseña para $CUENTA_USER (¿stdin sin terminal?) — su cuenta queda CERRADA"
152							break; }
153					fi
154					[ "$_cp_p1" = "$_cp_p2" ] && [ -n "$_cp_p1" ] || { echo "  no coinciden (o vacía) — reintentá"; continue; }
155					CUENTA_HASH=$(cuenta_hash "$_cp_p1")
156					[ -n "$CUENTA_HASH" ] || { echo "  ⚠ no pude generar el hash"; break; }
157					break
158				done
159				_cp_p1=; _cp_p2=
160			else
161				echo "  ⚠ este busybox no trae cryptpw: no puedo poner contraseña y la cuenta queda cerrada."
162			fi
163		fi
164		if [ "$_cp_doas" = sí ]; then
165			ask CUENTA_ADMIN "¿$CUENTA_USER administra la máquina (doas)? 1=sí 0=no" "1"
166		else
167			echo "  ⚠ esta imagen no trae doas ni sudo: nadie puede escalar, así que root necesita contraseña."
168			CUENTA_ADMIN=0
169		fi
170	fi
171
172	# La regla de root, aplicada: cerrada si hay quien escale, obligatoria si no.
173	if [ -n "$CUENTA_USER" ] && [ "$CUENTA_ADMIN" = 1 ] && [ -n "$CUENTA_HASH" ]; then
174		[ -n "$CUENTA_ROOT_HASH" ] || echo "  · root queda CERRADO: se administra con «doas» desde $CUENTA_USER."
175	elif [ -z "$CUENTA_ROOT_HASH" ]; then
176		echo
177		echo "  Nadie puede escalar todavía ⇒ root necesita contraseña, o la máquina queda inadministrable."
178		if [ -n "$(cuenta_hash x)" ]; then
179			_cp_m=0
180			while :; do
181				_cp_m=$((_cp_m+1))
182				read_secret _cp_r1 "Contraseña de root"
183				read_secret _cp_r2 "Repetir"
184				if [ "$CUENTA_EOF" = 1 ] || [ "$_cp_m" -ge "$CUENTA_INTENTOS" ]; then
185					[ "$_cp_r1" = "$_cp_r2" ] && [ -n "$_cp_r1" ] || {
186						echo "  ⚠ no pude leer la contraseña de root (¿stdin sin terminal?) — root queda CERRADO"
187						break; }
188				fi
189				[ "$_cp_r1" = "$_cp_r2" ] && [ -n "$_cp_r1" ] || { echo "  no coinciden (o vacía) — reintentá"; continue; }
190				CUENTA_ROOT_HASH=$(cuenta_hash "$_cp_r1"); break
191			done
192			_cp_r1=; _cp_r2=
193		fi
194	fi
195}
196
197# cuenta_resumen — una línea para el resumen de confirmación del instalador.
198cuenta_resumen() {
199	if [ -z "$CUENTA_USER" ]; then printf 'sin usuario (sólo root)'; return; fi
200	printf '%s' "$CUENTA_USER"
201	[ -n "$CUENTA_HASH" ] && printf ' con contraseña' || printf ' SIN contraseña (cuenta cerrada)'
202	[ "$CUENTA_ADMIN" = 1 ] && printf ', administra (doas)'
203}
204
205# _cuenta_romper_hardlink <fichero> — el árbol del stage son HARDLINKS del rootfs sellado (ADR
206# 0005): un `>>` sobre `etc/passwd` escribiría DENTRO del artefacto del store y lo envenenaría para
207# todas las imágenes, sin que nada falle. Es la misma precaución que toma `usuarios_aplicar`, y la
208# razón de que acá no haya un solo `>>` directo.
209_cuenta_romper_hardlink() {
210	[ -e "$1" ] || return 0
211	cp "$1" "$1.cuenta-tmp" && mv "$1.cuenta-tmp" "$1"
212}
213
214# cuenta_aplicar <árbol> — escribe la cuenta en la raíz nueva. Idempotente: si el usuario ya existe
215# (cosechado del `/home` conservado) NO lo vuelve a crear y sólo le pone su contraseña, que es
216# exactamente el caso del operador.
217cuenta_aplicar() {
218	_ca_t="$1"
219	bb mkdir -p "$_ca_t/etc"
220	for _f in etc/passwd etc/group etc/shadow; do _cuenta_romper_hardlink "$_ca_t/$_f"; done
221	[ -e "$_ca_t/etc/passwd" ] || : > "$_ca_t/etc/passwd"
222	[ -e "$_ca_t/etc/group" ]  || : > "$_ca_t/etc/group"
223	[ -e "$_ca_t/etc/shadow" ] || { : > "$_ca_t/etc/shadow"; bb chmod 600 "$_ca_t/etc/shadow"; }
224
225	if [ -n "$CUENTA_USER" ]; then
226		if bb awk -F: -v n="$CUENTA_USER" '$1==n{f=1} END{exit !f}' "$_ca_t/etc/passwd"; then
227			_ca_uid=$(bb awk -F: -v n="$CUENTA_USER" '$1==n{print $3; exit}' "$_ca_t/etc/passwd")
228			echo "    · «$CUENTA_USER» ya existe con uid $_ca_uid (del /home conservado): le pongo su contraseña"
229		else
230			# El primer uid libre desde 1000. Se cuenta sobre el passwd que YA tiene los usuarios
231			# cosechados, así que no puede chocar con ellos.
232			_ca_uid=1000
233			while bb awk -F: -v u="$_ca_uid" '$3==u{f=1} END{exit !f}' "$_ca_t/etc/passwd"; do
234				_ca_uid=$((_ca_uid + 1))
235			done
236			printf '%s:x:%s:%s:%s:/home/%s:/bin/sh\n' \
237				"$CUENTA_USER" "$_ca_uid" "$_ca_uid" "$CUENTA_USER" "$CUENTA_USER" >> "$_ca_t/etc/passwd"
238			bb awk -F: -v g="$_ca_uid" '$3==g{f=1} END{exit !f}' "$_ca_t/etc/group" \
239				|| printf '%s:x:%s:\n' "$CUENTA_USER" "$_ca_uid" >> "$_ca_t/etc/group"
240			echo "    · usuario «$CUENTA_USER» creado con uid $_ca_uid"
241		fi
242		# El hogar: sólo si NO existe. Con `--keep` o `--adopt` ya está ahí con sus archivos y sus
243		# dueños, y pisarlo sería el único daño irreparable que puede hacer un instalador.
244		if [ ! -e "$_ca_t/home/$CUENTA_USER" ]; then
245			bb mkdir -p "$_ca_t/home/$CUENTA_USER"
246			bb chown "$_ca_uid:$_ca_uid" "$_ca_t/home/$CUENTA_USER" 2>/dev/null || true
247			bb chmod 700 "$_ca_t/home/$CUENTA_USER"
248			echo "    · /home/$CUENTA_USER creado (uid $_ca_uid)"
249		else
250			echo "    · /home/$CUENTA_USER ya existe: NO lo toco"
251		fi
252		# Los grupos del hardware: seatd exige `video` POR NOMBRE, y sin `input` libinput no abre los
253		# dispositivos. Sólo si el grupo existe en la imagen — inventar un gid acá sería adivinar.
254		for _g in video input audio seat; do
255			bb awk -F: -v g="$_g" -v u="$CUENTA_USER" '
256				BEGIN{OFS=":"}
257				$1==g { if ($4=="") $4=u; else if ($4 !~ ("(^|,)" u "(,|$)")) $4=$4","u; f=1 }
258				{print}
259				END{exit !f}' "$_ca_t/etc/group" > "$_ca_t/etc/group.cuenta-tmp" \
260				&& mv "$_ca_t/etc/group.cuenta-tmp" "$_ca_t/etc/group" \
261				|| rm -f "$_ca_t/etc/group.cuenta-tmp"
262		done
263		if [ -n "$CUENTA_HASH" ]; then
264			_cuenta_set_shadow "$CUENTA_USER" "$CUENTA_HASH" "$_ca_t"
265		else
266			_cuenta_set_shadow "$CUENTA_USER" '!' "$_ca_t"
267			echo "    ⚠ «$CUENTA_USER» queda SIN contraseña posible: nadie va a poder entrar con esa cuenta"
268		fi
269		# La regla de escalada. Sin esto doas RECHAZA TODO («no rule matched») aunque esté instalado y
270		# con su bit: quién escala no lo puede decidir el paquete, lo decide esta máquina.
271		if [ "$CUENTA_ADMIN" = 1 ]; then
272			if [ -e "$_ca_t/usr/bin/doas" ] || [ -e "$_ca_t/bin/doas" ]; then
273				if [ -e "$_ca_t/etc/doas.conf" ]; then
274					echo "    · /etc/doas.conf ya existe: no lo toco"
275				else
276					printf '# Quién puede ser root en esta máquina. La ÚLTIMA regla que casa es la que vale.\npermit persist %s\n' \
277						"$CUENTA_USER" > "$_ca_t/etc/doas.conf"
278					bb chown 0:0 "$_ca_t/etc/doas.conf" 2>/dev/null || true
279					bb chmod 640 "$_ca_t/etc/doas.conf"
280					echo "    · /etc/doas.conf: permit persist $CUENTA_USER"
281				fi
282			else
283				echo "    ⚠ se pidió que «$CUENTA_USER» administre y esta imagen NO trae doas: no hay escalada"
284			fi
285		fi
286	fi
287
288	if [ -n "$CUENTA_ROOT_HASH" ]; then
289		_cuenta_set_shadow root "$CUENTA_ROOT_HASH" "$_ca_t"
290		echo "    · root con contraseña"
291	fi
292
293	# El entorno de login de esa cuenta. Va acá y no en otra sección porque una cuenta cuyo shell no
294	# encuentra `/usr/local/bin` es media cuenta: es lo que dejó al operador con `claude` instalado y
295	# `claude: not found` en la consola.
296	cuenta_perfil_de_login "$_ca_t"
297
298	# ── El brazo de control, ACÁ y no en un test aparte ───────────────────────────────────────────
299	# Una cuenta mal escrita no se nota hasta el primer arranque, y arreglarla cuesta montar el disco
300	# desde otro sistema. Se comprueba lo que se acaba de escribir, y se dice en voz alta.
301	_ca_mal=0
302	if [ -n "$CUENTA_USER" ]; then
303		bb awk -F: -v n="$CUENTA_USER" '$1==n{f=1} END{exit !f}' "$_ca_t/etc/passwd" \
304			|| { echo "    ✗ «$CUENTA_USER» NO quedó en /etc/passwd"; _ca_mal=$((_ca_mal+1)); }
305		if [ -n "$CUENTA_HASH" ]; then
306			bb awk -F: -v n="$CUENTA_USER" '$1==n && $2 ~ /^\$/{f=1} END{exit !f}' "$_ca_t/etc/shadow" \
307				|| { echo "    ✗ «$CUENTA_USER» no quedó con un hash en /etc/shadow"; _ca_mal=$((_ca_mal+1)); }
308		fi
309	fi
310	# El aviso cubre las TRES formas de quedar sin nadie que administre: sin contraseña de root y
311	# (sin usuario | con usuario que no escala | con usuario sin contraseña). La primera versión de
312	# esta línea sólo miraba `ADMIN != 1`, y con el default `ADMIN=1` y usuario VACÍO se callaba —
313	# que es exactamente la combinación con la que arrancó la máquina del operador.
314	if [ -z "$CUENTA_ROOT_HASH" ] \
315		&& { [ -z "$CUENTA_USER" ] || [ "$CUENTA_ADMIN" != 1 ] || [ -z "$CUENTA_HASH" ]; }; then
316		echo "    ⚠ root cerrado y nadie escala: esta máquina va a arrancar INADMINISTRABLE"
317	fi
318	[ "$_ca_mal" = 0 ]
319}
320
321# cuenta_perfil_de_login <árbol> — `/etc/profile`, que es lo que lee el shell de esa cuenta.
322#
323# ── Por qué está acá y no en una receta ──────────────────────────────────────────────────────────
324# Medido en la máquina del operador: **`/etc/profile` NO EXISTÍA**. Y como el `ash` de busybox
325# sourcea `/etc/profile.d/*` sólo si ese archivo lo hace, `/etc/profile.d/tawasuyu-dev.sh` —que
326# exportaba el PATH del toolchain— estuvo inerte desde el primer arranque: `claude` instalado en
327# `/usr/local/bin` y la consola contestando `claude: not found`. Las recetas no tienen mecanismo
328# para aportar un fichero de `/etc` (no hay `[[file]]`: lo que entra sale del `install` de la
329# receta, y tocar eso re-hashea), así que el sitio donde nace un `/etc` de ESTA máquina es el
330# instalador — el mismo que escribe `hostname`, `hosts`, `localtime` y `shadow`.
331#
332# **Los seis directorios se garantizan uno por uno** en vez de agregar sólo `/usr/local`: un PATH
333# heredado puede traer unos y no otros, y entonces `su` o `wpa_supplicant` «no existen» porque
334# faltaba `/bin` o `/usr/sbin`. Medido dentro de un chroot de la imagen: con el PATH de la otra
335# máquina, `claude` se encontraba y `su` no.
336#
337# Y los del hogar van con `$HOME`, no con una ruta escrita: este fichero viaja a la máquina de
338# cualquiera. La primera versión de este PATH decía `/home/sergio/...` y eso es justo lo que no
339# puede quedar en algo que instala un desconocido.
340cuenta_perfil_de_login() {
341	_cp_t="$1"
342	[ -e "$_cp_t/etc/profile" ] && { echo "    · /etc/profile ya existe: no lo toco"; return 0; }
343	bb mkdir -p "$_cp_t/etc"
344	cat > "$_cp_t/etc/profile" <<'PERFIL'
345# /etc/profile — lo que lee un login shell.
346#
347# El `ash` de busybox sourcea ESTE fichero y nada más: `/etc/profile.d/*` no se lee si acá no se
348# lee. Sin esto, la consola de rescate tampoco tiene PATH.
349for dir in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin; do
350	case ":${PATH:-}:" in
351	*":$dir:"*) ;;
352	*) PATH="${PATH:+$PATH:}$dir" ;;
353	esac
354done
355# Y los del hogar, al final: lo de la persona no debe tapar lo del sistema.
356for dir in "$HOME/.local/bin" "$HOME/bin"; do
357	[ -d "$dir" ] || continue
358	case ":$PATH:" in *":$dir:"*) ;; *) PATH="$PATH:$dir" ;; esac
359done
360export PATH
361umask 022
362
363for perfil in /etc/profile.d/*.sh; do
364	[ -r "$perfil" ] && . "$perfil"
365done
366unset perfil dir
367PERFIL
368	bb chmod 644 "$_cp_t/etc/profile"
369	echo "    · /etc/profile escrito (los seis directorios + los del hogar al final)"
370}
371
372# El mismo `_set_shadow` que ya tenía `takana-live-install.sh`, con otro nombre para no pisárselo.
373_cuenta_set_shadow() {
374	_cs_u="$1"; _cs_h="$2"; _cs_f="$3/etc/shadow"
375	if [ ! -f "$_cs_f" ]; then
376		printf '%s:%s:20000:0:99999:7:::\n' "$_cs_u" "$_cs_h" > "$_cs_f"
377		bb chmod 600 "$_cs_f"
378		return
379	fi
380	bb awk -F: -v u="$_cs_u" -v h="$_cs_h" 'BEGIN{OFS=":"}
381		{ if ($1==u){ $2=h; seen=1 } print }
382		END{ if(!seen) print u,h,"20000","0","99999","7","","","" }' "$_cs_f" > "$_cs_f.cuenta-tmp" \
383		&& mv "$_cs_f.cuenta-tmp" "$_cs_f"
384	bb chmod 600 "$_cs_f"
385}