la imagen del servidor, armada y booteada — tres fallos que sólo aparecen ahí
Se armó la imagen del perfil `servidor` con gitea y se arrancó en QEMU. Los tres hallazgos, en el orden en que aparecieron, son los que justifican probar la imagen en vez de dar por buena la receta. 1) ⚠⚠ ESCRIBIR EN EL ROOTFS HIDRATADO ES ESCRIBIR DENTRO DEL STORE `takana users --merge` hacía `fs::write` sobre `<rootfs>/etc/passwd`. Un rootfs hidratado se arma con HARDLINKS contra el store: medido, ese fichero y el del artefacto `product-rootfs` eran **el mismo inode (1225824, 2 links, modo 444)**. Un `write` habría modificado el artefacto SELLADO, y todas las imágenes futuras habrían salido con la cuenta metida dentro del producto. Acá se salvó porque el store es de sólo lectura y salió `Permission denied` — confiar en eso es confiar en un permiso. Ahora `escribir_rompiendo_hardlink()`: temporal + `rename`. Con su control, que afirma lo que importa: tras escribir, el fichero del store **conserva su contenido**, baja a 1 link y el del rootfs tiene otro inode. Verificado también sobre la imagen real. 2) LA IMAGEN TRAÍA EL BINARIO, LA CUENTA… Y NADIE LO ARRANCABA Primera imagen: `gitea` instalado, `gitea:x:916` en `/etc/passwd`, y en el `genesis` de la seed sólo `sshd`, `console-getty`, `hammerd`, `hammer-product`. `servidor-image.sh` no inyectaba las Cards — eso sólo estaba en el camino de las imágenes de escritorio. Y ninguna métrica lo dice: `--services` responde que el perfil lo habilita, y lo habilita; lo que faltaba era el paso que lleva esa declaración a la imagen. Ahora llama a `service-cards` + `inyectar-cards.py` (idempotente por label). 3) ⚡ EL BINARIO MORÍA CON `trap invalid opcode` — Y EL BUG ESTABA EN EL BUILDER Con la card en el genesis y la config puesta, gitea arrancaba y moría al instante: traps: gitea[91] trap invalid opcode ip:79ea992 ... in gitea[...] El sandbox exporta `CC` apuntando a un wrapper que pone `-mcpu=baseline` (`sandbox.rs` ya avisaba: «de paso cierra el SIGILL de AVX en qemu64»), y la fase Go del propio builder lo PISABA con `CC="zig cc"` a secas — que por defecto es `-mcpu=native` y hornea la ISA del que compila. El binario corría perfecto en el worker y moría en QEMU-TCG. No falla al compilar ni al sellar: falla al EJECUTAR en otra CPU. Y el `ArtifactHash` no lo puede cazar, porque la CPU del builder no entra en `hash_inputs` — dos workers distintos sellan bytes distintos bajo la misma dirección. Arreglado en el builder y en la receta; re-hashea las CINCO recetas `cgo = true` (gitea, usql, sq, gocryptfs, naabu), que es correcto: lo que había sellado no es portable. gitea: `b3:391a613e…` → `b3:5edf9c16…`. Lo verificado en la VM: PID 1 = arje-zero · la cuenta de `[[user]]` en `/etc/passwd` y `/etc/group` de la imagen · la card de gitea en el `genesis` · y arje encarnándola, con la guarda saliendo 78 y `/var/log/arje/ente-gitea.log` diciendo exactamente «falta /etc/gitea/app.ini — es config del SITIO». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
This commit is contained in:
@@ -213,6 +213,35 @@ pub fn fusionar(contenido: &str, lineas: &[String]) -> (String, Vec<String>, Vec
|
||||
(out, anadidas, conflictos)
|
||||
}
|
||||
|
||||
/// Escribe `contenido` en `p` **rompiendo el hardlink**: temporal en el mismo directorio + `rename`.
|
||||
///
|
||||
/// ⚠ Esto no es higiene, es lo único correcto, y se descubrió al armar la primera imagen: un rootfs
|
||||
/// hidratado se arma con **hardlinks contra el store**, así que su `/etc/passwd` y el del artefacto
|
||||
/// `product-rootfs` **son el mismo inode** (medido: mismo número de inode, `2 links`, modo `444`).
|
||||
/// Un `fs::write` sobre él abre ESE inode en truncate y **modifica el artefacto sellado** — todas
|
||||
/// las imágenes futuras saldrían con la cuenta metida dentro del producto, y el store mentiría
|
||||
/// sobre lo que contiene su propio hash. Acá se salvó porque el store es de sólo lectura y el
|
||||
/// primer intento murió con `Permission denied`; confiar en eso sería confiar en un permiso.
|
||||
///
|
||||
/// `rename` sobre el mismo directorio es atómico y deja el fichero del store intacto con un link
|
||||
/// menos. El modo sale 0644: el del store es 0444 y un `passwd` de sólo lectura rompe a cualquiera
|
||||
/// que después añada una cuenta.
|
||||
pub fn escribir_rompiendo_hardlink(p: &std::path::Path, contenido: &str) -> crate::Result<()> {
|
||||
let dir = p.parent().unwrap_or(std::path::Path::new("."));
|
||||
let tmp = dir.join(format!(
|
||||
".{}.users-tmp",
|
||||
p.file_name().and_then(|s| s.to_str()).unwrap_or("fichero")
|
||||
));
|
||||
std::fs::write(&tmp, contenido)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o644))?;
|
||||
}
|
||||
std::fs::rename(&tmp, p)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -272,6 +301,42 @@ mod tests {
|
||||
assert!(validate_all(&[a.clone(), a]).unwrap_err().to_string().contains("name duplicado"));
|
||||
}
|
||||
|
||||
/// El control del fallo que casi envenena el store: escribir el `passwd` de un rootfs
|
||||
/// hidratado NO puede tocar el fichero del artefacto con el que comparte inode.
|
||||
#[test]
|
||||
fn escribir_no_toca_el_fichero_hardlinkeado() {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let dir = std::env::temp_dir().join(format!("takana-user-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join("store")).unwrap();
|
||||
std::fs::create_dir_all(dir.join("rootfs")).unwrap();
|
||||
let en_store = dir.join("store/passwd");
|
||||
let en_rootfs = dir.join("rootfs/passwd");
|
||||
std::fs::write(&en_store, "root:x:0:0:root:/root:/bin/sh\n").unwrap();
|
||||
std::fs::hard_link(&en_store, &en_rootfs).unwrap();
|
||||
assert_eq!(
|
||||
std::fs::metadata(&en_store).unwrap().ino(),
|
||||
std::fs::metadata(&en_rootfs).unwrap().ino(),
|
||||
"el montaje del test tiene que hardlinkear para que el control valga"
|
||||
);
|
||||
|
||||
escribir_rompiendo_hardlink(&en_rootfs, "root:x:0:0:root:/root:/bin/sh\ngitea:x:916:916:gitea:/var/lib/gitea:/bin/false\n").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&en_store).unwrap(),
|
||||
"root:x:0:0:root:/root:/bin/sh\n",
|
||||
"SE MODIFICÓ EL ARTEFACTO DEL STORE"
|
||||
);
|
||||
assert!(std::fs::read_to_string(&en_rootfs).unwrap().contains("gitea:"));
|
||||
assert_ne!(
|
||||
std::fs::metadata(&en_store).unwrap().ino(),
|
||||
std::fs::metadata(&en_rootfs).unwrap().ino(),
|
||||
"el hardlink no se rompió"
|
||||
);
|
||||
assert_eq!(std::fs::metadata(&en_store).unwrap().nlink(), 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fusionar_es_idempotente_y_no_pisa() {
|
||||
let base = "root:x:0:0:root:/root:/bin/sh\nsshd:x:74:74:sshd privsep:/var/empty:/bin/false\n";
|
||||
|
||||
Reference in New Issue
Block a user