hammer-bootstrap: userland Rust en el producto + adelgazar busybox (Etapa C)

Extiende la capa de producto con el userland Rust-nativo ADOPTADO, fuera del
núcleo blindado (sigue sin tocar STAGE1_COMPONENTS ni el of_tree).

- USERLAND_COMPONENTS = [uutils, findutils, findutils-xargs, diffutils,
  ripgrep]: se hidratan sobre el 4/4 DESPUÉS de busybox ⇒ sus symlinks en
  /usr/bin ensombrecen los applets busybox.
- ADELGAZAR BUSYBOX (determinista, sin depender del PATH del shell): por cada
  nombre que el userland Rust provee en /usr/bin, se RETIRA el symlink
  homónimo de busybox en /bin, /sbin, /usr/sbin (busybox suele dejar `ls` en
  /bin, fuera del /usr/bin ya ensombrecido). La tool Rust queda ÚNICA en PATH.
  El binario busybox y lo no reemplazado (sh/ash, tar, mount) se preservan.
- product_rootfs_hash v2 (incluye userland); build_components() helper;
  assemble_product_rootfs hidrata base→userland→servicios.
- 36 tests verde (nuevo: ensombrecido + retiro de applet + sh/busybox quedan).

Validado in-VM (product-boot-test.sh sobre el product-rootfs de la ruta real):
ls = "uutils coreutils 0.9.0" (/usr/bin/ls), find = "find (Rust) 0.9.1",
rg = "ripgrep 14.1.1"; /bin/ls y /bin/cat retirados, /bin/sh + busybox
preservados. SSH sigue verde. "Adelgazar busybox" cerrado en el producto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 17:47:20 -04:00
co-authored by Claude Opus 4.8
parent 7baaf4afc5
commit 20936e1f96
2 changed files with 126 additions and 40 deletions
+118 -38
View File
@@ -396,9 +396,17 @@ fn assemble_rootfs(
// Así, meter más servicios mañana no toca jamás el núcleo verificado. // Así, meter más servicios mañana no toca jamás el núcleo verificado.
/// Componentes de servicio inyectados sobre el 4/4 verificado: `netup` (red) + `openssh` (sshd/ssh). /// Componentes de servicio inyectados sobre el 4/4 verificado: `netup` (red) + `openssh` (sshd/ssh).
/// Crecer esta lista NO toca `STAGE1_COMPONENTS` ni el selfhost-verify. /// Daemons ⇒ llevan card en la seed de producto. Crecer esta lista NO toca `STAGE1_COMPONENTS`.
const SERVICE_COMPONENTS: &[&str] = &["netup", "openssh"]; const SERVICE_COMPONENTS: &[&str] = &["netup", "openssh"];
/// Userland Rust-nativo ADOPTADO (Etapa C): multicall `uutils`/coreutils + `find`/`xargs` + `diff`/`cmp`
/// + `rg`. Se hidrata sobre la base DESPUÉS de busybox, así que sus symlinks en `/usr/bin` ENSOMBRECEN
/// los applets de busybox (con `PATH` `/usr/bin:/bin` gana la tool Rust). Es el "adelgazar busybox" del
/// producto SIN tocar el núcleo: busybox sigue en el 4/4 (self-host) y queda de fallback para lo aún no
/// reemplazado (`sh`/ash, `tar`, …). NO son daemons ⇒ no llevan card en la seed (sólo ficheros).
const USERLAND_COMPONENTS: &[&str] =
&["uutils", "findutils", "findutils-xargs", "diffutils", "ripgrep"];
/// Card de servicio `sshd` (genesis `Native`/`Restart`): el comando levanta la red (`netup`, DHCP), /// Card de servicio `sshd` (genesis `Native`/`Restart`): el comando levanta la red (`netup`, DHCP),
/// genera host keys al primer boot (`ssh-keygen -A`) y `exec`uta `sshd -D`. Todo dentro del card para /// genera host keys al primer boot (`ssh-keygen -A`) y `exec`uta `sshd -D`. Todo dentro del card para
/// no asumir orden de genesis. Mismo esquema validado de [`STAGE1_SEED_CARD`]; ULID propio. Se inyecta /// no asumir orden de genesis. Mismo esquema validado de [`STAGE1_SEED_CARD`]; ULID propio. Se inyecta
@@ -454,17 +462,19 @@ fn product_seed_card() -> Result<String> {
} }
/// Identidad del `product-rootfs`: tag + el `RootfsHash` del 4/4 base + `(nombre,hash)` de cada /// Identidad del `product-rootfs`: tag + el `RootfsHash` del 4/4 base + `(nombre,hash)` de cada
/// servicio + la seed de producto + las configs. Cambiar cualquiera re-hashea (reproducible). /// componente userland y de servicio (en orden) + la seed de producto + las configs. Cambiar
/// cualquiera re-hashea (reproducible). El tag `v2` marca el corte respecto al producto sólo-servicios.
fn product_rootfs_hash( fn product_rootfs_hash(
base: &RootfsHash, base: &RootfsHash,
userland: &[(String, ArtifactHash)],
services: &[(String, ArtifactHash)], services: &[(String, ArtifactHash)],
seed: &str, seed: &str,
) -> RootfsHash { ) -> RootfsHash {
let mut inputs: Vec<Vec<u8>> = vec![ let mut inputs: Vec<Vec<u8>> = vec![
b"hammer-product-rootfs-v1".to_vec(), b"hammer-product-rootfs-v2".to_vec(),
base.as_str().as_bytes().to_vec(), base.as_str().as_bytes().to_vec(),
]; ];
for (name, h) in services { for (name, h) in userland.iter().chain(services.iter()) {
inputs.push(name.as_bytes().to_vec()); inputs.push(name.as_bytes().to_vec());
inputs.push(h.as_str().as_bytes().to_vec()); inputs.push(h.as_str().as_bytes().to_vec());
} }
@@ -476,13 +486,14 @@ fn product_rootfs_hash(
ArtifactHash::of_inputs(&refs) ArtifactHash::of_inputs(&refs)
} }
/// Ensambla el árbol de producto en `staging`: hidrata el rootfs base sellado (4/4) y luego los /// Ensambla el árbol de producto en `staging`: hidrata el rootfs base sellado (4/4), luego el userland
/// componentes de servicio ENCIMA (hidratación tardía); sobrescribe la seed con la de producto y /// Rust (ENSOMBRECE los applets de busybox en `/usr/bin`) y los componentes de servicio ENCIMA
/// escribe las configs de servicio (passwd/group/sshd_config + `/var/empty` 0711, `/etc/ssh`, /// (hidratación tardía); sobrescribe la seed con la de producto y escribe las configs de servicio
/// `/root/.ssh` 0700). Pieza testeable sin build real. /// (passwd/group/sshd_config + `/var/empty` 0711, `/etc/ssh`, `/root/.ssh` 0700). Testeable sin build.
fn assemble_product_rootfs( fn assemble_product_rootfs(
store: &Store, store: &Store,
base: &RootfsHash, base: &RootfsHash,
userland: &[(String, ArtifactHash)],
services: &[(String, ArtifactHash)], services: &[(String, ArtifactHash)],
seed: &str, seed: &str,
staging: &Path, staging: &Path,
@@ -500,13 +511,38 @@ fn assemble_product_rootfs(
} }
hammer_build::run_hydrate(&base_dir, staging, hammer_core::LinkMode::Static, None)?; hammer_build::run_hydrate(&base_dir, staging, hammer_core::LinkMode::Static, None)?;
// 2) servicios encima (openssh, netup): mismo proyectado por hardlink. // 2) userland Rust + servicios ENCIMA (hardlink). El userland va DESPUÉS de la base: sus symlinks
for (name, h) in services { // en /usr/bin reemplazan a los de busybox (run_hydrate sustituye el symlink existente) ⇒ el
// producto corre las tools Rust. Orden userland→servicios (sin colisiones entre sí).
for (name, h) in userland.iter().chain(services.iter()) {
let dir = store.path_of(h, name); let dir = store.path_of(h, name);
hammer_build::run_hydrate(&dir, staging, hammer_core::LinkMode::Static, None)?; hammer_build::run_hydrate(&dir, staging, hammer_core::LinkMode::Static, None)?;
} }
let _ = std::fs::remove_dir_all(staging.join(".hammer")); let _ = std::fs::remove_dir_all(staging.join(".hammer"));
// ADELGAZAR BUSYBOX (determinista, sin depender del orden de PATH del shell): por cada nombre que
// el userland Rust provee en `/usr/bin`, retirar el symlink homónimo de busybox en los OTROS dirs
// (`/bin`, `/sbin`, `/usr/sbin`) — busybox suele instalar `ls` en `/bin`, fuera del `/usr/bin` que
// la hidratación ya ensombreció. Así la tool Rust es la ÚNICA en PATH. El binario busybox queda
// (sh/ash, tar, mount, … lo no reemplazado); sólo se quitan los applets ya cubiertos por Rust.
let mut applet_names: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for (name, h) in userland {
let ub = store.path_of(h, name).join("usr/bin");
if let Ok(rd) = std::fs::read_dir(&ub) {
for e in rd.flatten() {
applet_names.insert(e.file_name().to_string_lossy().into_owned());
}
}
}
for applet in &applet_names {
for d in ["bin", "sbin", "usr/sbin"] {
let p = staging.join(d).join(applet);
if std::fs::symlink_metadata(&p).is_ok() {
let _ = std::fs::remove_file(&p);
}
}
}
// 3) seed de producto (sobrescribe la base) + configs + dirs de runtime de sshd. // 3) seed de producto (sobrescribe la base) + configs + dirs de runtime de sshd.
// Los ficheros hidratados son HARDLINKS al store sellado (read-only); escribir encima daría // Los ficheros hidratados son HARDLINKS al store sellado (read-only); escribir encima daría
// EACCES y `chmod` mutaría el inodo del store. Hay que ROMPER el hardlink: remove + write nuevo. // EACCES y `chmod` mutaría el inodo del store. Hay que ROMPER el hardlink: remove + write nuevo.
@@ -532,31 +568,44 @@ fn assemble_product_rootfs(
Ok(()) Ok(())
} }
/// **Producto** — toma el rootfs base verificado (`base`, el `stage1-rootfs` del 4/4) y le inyecta la /// Construye/cachea (idempotente vía el lab) una lista de componentes nombrados, devolviendo sus
/// capa de servicios por hidratación tardía, sellando un `product-rootfs`. El mecanismo base queda /// `(nombre, hash)` sellados. Cada receta usa su propia config de build (p.ej. `zig_version`).
/// intacto: este árbol NO es el que reconstruye el selfhost-verify (su `of_tree` sigue blindado). fn build_components(
/// Idempotente. `base_cfg` apunta el zig del lab (no la semilla): los servicios usan su `zig_version` names: &[&str],
/// propia (openssh ⇒ 0.13.0), y si ya están sellados, `build` los devuelve cacheados sin recompilar. recipes_dir: &Path,
cfg: &hammer_build::BuildConfig,
store: &Store,
) -> Result<Vec<(String, ArtifactHash)>> {
let mut out = Vec::with_capacity(names.len());
for name in names {
let recipe_path = recipes_dir.join(format!("{name}.toml"));
let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| {
Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display()))
})?;
let h = hammer_build::build(&recipe, cfg, store)?;
out.push((name.to_string(), h));
}
Ok(out)
}
/// **Producto** — toma el rootfs base verificado (`base`, el `stage1-rootfs` del 4/4) y le inyecta por
/// hidratación tardía el userland Rust (adelgaza busybox) + la capa de servicios, sellando un
/// `product-rootfs`. El mecanismo base queda intacto: este árbol NO es el que reconstruye el
/// selfhost-verify (su `of_tree` sigue blindado). Idempotente. `base_cfg` apunta el zig del lab (no la
/// semilla): cada componente usa su `zig_version` propia, y si ya está sellado sale cacheado.
pub fn product( pub fn product(
base: &RootfsHash, base: &RootfsHash,
base_cfg: &hammer_build::BuildConfig, base_cfg: &hammer_build::BuildConfig,
recipes_dir: &Path, recipes_dir: &Path,
store: &Store, store: &Store,
) -> Result<RootfsHash> { ) -> Result<RootfsHash> {
// 1) construir/cachear los componentes de servicio (idempotente vía el lab). // 1) construir/cachear el userland Rust + los servicios.
let mut services: Vec<(String, ArtifactHash)> = Vec::with_capacity(SERVICE_COMPONENTS.len()); let userland = build_components(USERLAND_COMPONENTS, recipes_dir, base_cfg, store)?;
for name in SERVICE_COMPONENTS { let services = build_components(SERVICE_COMPONENTS, recipes_dir, base_cfg, store)?;
let recipe_path = recipes_dir.join(format!("{name}.toml"));
let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| {
Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display()))
})?;
let h = hammer_build::build(&recipe, base_cfg, store)?;
services.push((name.to_string(), h));
}
// 2) seed de producto + hash de identidad. // 2) seed de producto + hash de identidad.
let seed = product_seed_card()?; let seed = product_seed_card()?;
let phash = product_rootfs_hash(base, &services, &seed); let phash = product_rootfs_hash(base, &userland, &services, &seed);
let store_name = "product-rootfs"; let store_name = "product-rootfs";
// 3) ensamblar + sellar (si no estaba ya). // 3) ensamblar + sellar (si no estaba ya).
@@ -568,13 +617,13 @@ pub fn product(
let _ = std::fs::remove_dir_all(&staging); let _ = std::fs::remove_dir_all(&staging);
std::fs::create_dir_all(&staging)?; std::fs::create_dir_all(&staging)?;
let result = (|| -> Result<()> { let result = (|| -> Result<()> {
assemble_product_rootfs(store, base, &services, &seed, &staging)?; assemble_product_rootfs(store, base, &userland, &services, &seed, &staging)?;
store.seal(&staging, &phash, store_name)?; store.seal(&staging, &phash, store_name)?;
Ok(()) Ok(())
})(); })();
let _ = std::fs::remove_dir_all(&staging); let _ = std::fs::remove_dir_all(&staging);
result?; result?;
tracing::info!(hash = %phash, "product: rootfs de servicios sellado"); tracing::info!(hash = %phash, "product: rootfs (userland Rust + servicios) sellado");
} else { } else {
tracing::info!(hash = %phash, "product: rootfs ya sellado (idempotente)"); tracing::info!(hash = %phash, "product: rootfs ya sellado (idempotente)");
} }
@@ -1526,14 +1575,17 @@ mod tests {
#[test] #[test]
fn product_rootfs_hash_is_deterministic_and_sensitive() { fn product_rootfs_hash_is_deterministic_and_sensitive() {
let base = ArtifactHash::from_hex("ab"); let base = ArtifactHash::from_hex("ab");
let userland = [("uutils".to_string(), ArtifactHash::from_hex("11"))];
let svcs = [("openssh".to_string(), ArtifactHash::from_hex("cd"))]; let svcs = [("openssh".to_string(), ArtifactHash::from_hex("cd"))];
let seed = product_seed_card().unwrap(); let seed = product_seed_card().unwrap();
let a = product_rootfs_hash(&base, &svcs, &seed); let a = product_rootfs_hash(&base, &userland, &svcs, &seed);
assert_eq!(a, product_rootfs_hash(&base, &svcs, &seed), "determinista"); assert_eq!(a, product_rootfs_hash(&base, &userland, &svcs, &seed), "determinista");
let other_base = ArtifactHash::from_hex("ef"); let other_base = ArtifactHash::from_hex("ef");
assert_ne!(a, product_rootfs_hash(&other_base, &svcs, &seed), "cambiar la base re-hashea"); assert_ne!(a, product_rootfs_hash(&other_base, &userland, &svcs, &seed), "cambiar la base re-hashea");
let other_userland = [("uutils".to_string(), ArtifactHash::from_hex("22"))];
assert_ne!(a, product_rootfs_hash(&base, &other_userland, &svcs, &seed), "cambiar el userland re-hashea");
let other_svc = [("openssh".to_string(), ArtifactHash::from_hex("99"))]; let other_svc = [("openssh".to_string(), ArtifactHash::from_hex("99"))];
assert_ne!(a, product_rootfs_hash(&base, &other_svc, &seed), "cambiar un servicio re-hashea"); assert_ne!(a, product_rootfs_hash(&base, &userland, &other_svc, &seed), "cambiar un servicio re-hashea");
} }
#[test] #[test]
@@ -1542,17 +1594,31 @@ mod tests {
let store = Store::open(tmp.path().join("store")).unwrap(); let store = Store::open(tmp.path().join("store")).unwrap();
// Un rootfs base sintético sellado como `stage1-rootfs` (el 4/4 ya verificado): trae la seed // Un rootfs base sintético sellado como `stage1-rootfs` (el 4/4 ya verificado): trae la seed
// base, /sbin/init, un mountpoint vacío y un binario del núcleo. // base, /sbin/init, un mountpoint vacío, un binario del núcleo y un applet busybox `ls`.
let base = seal_component(&store, "stage1-rootfs", "ba5e", |w| { let base = seal_component(&store, "stage1-rootfs", "ba5e", |w| {
std::fs::create_dir_all(w.join("usr/bin")).unwrap(); std::fs::create_dir_all(w.join("usr/bin")).unwrap();
std::fs::create_dir_all(w.join("bin")).unwrap();
std::fs::create_dir_all(w.join("ente")).unwrap(); std::fs::create_dir_all(w.join("ente")).unwrap();
std::fs::create_dir_all(w.join("proc")).unwrap(); std::fs::create_dir_all(w.join("proc")).unwrap();
std::fs::create_dir_all(w.join("var")).unwrap(); std::fs::create_dir_all(w.join("var")).unwrap();
std::fs::create_dir_all(w.join("root")).unwrap(); std::fs::create_dir_all(w.join("root")).unwrap();
std::fs::write(w.join("usr/bin/hammerd"), b"\x7fELFfake").unwrap(); std::fs::write(w.join("usr/bin/hammerd"), b"\x7fELFfake").unwrap();
std::fs::write(w.join("bin/busybox"), b"\x7fELFbb").unwrap();
// busybox instala el applet `ls` como symlink en /usr/bin (lo ensombrece la hidratación) y
// `cat` en /bin (lo debe RETIRAR el adelgazamiento). `sh` en /bin NO se reemplaza (queda).
std::os::unix::fs::symlink("../../bin/busybox", w.join("usr/bin/ls")).unwrap();
std::os::unix::fs::symlink("busybox", w.join("bin/cat")).unwrap();
std::os::unix::fs::symlink("busybox", w.join("bin/sh")).unwrap();
std::fs::write(w.join("ente/seed.card.json"), STAGE1_SEED_CARD).unwrap(); std::fs::write(w.join("ente/seed.card.json"), STAGE1_SEED_CARD).unwrap();
std::os::unix::fs::symlink("/usr/bin/arje-zero", w.join("sbin_init_tmp")).unwrap(); std::os::unix::fs::symlink("/usr/bin/arje-zero", w.join("sbin_init_tmp")).unwrap();
}); });
// Userland Rust sintético (uutils): coreutils multicall + symlink `ls`→coreutils.
let huu = seal_component(&store, "uutils", "c0re", |w| {
std::fs::create_dir_all(w.join("usr/bin")).unwrap();
std::fs::write(w.join("usr/bin/coreutils"), b"\x7fELFuu").unwrap();
std::os::unix::fs::symlink("coreutils", w.join("usr/bin/ls")).unwrap();
std::os::unix::fs::symlink("coreutils", w.join("usr/bin/cat")).unwrap();
});
// Componentes de servicio sintéticos. // Componentes de servicio sintéticos.
let hossh = seal_component(&store, "openssh", "0551", |w| { let hossh = seal_component(&store, "openssh", "0551", |w| {
std::fs::create_dir_all(w.join("usr/sbin")).unwrap(); std::fs::create_dir_all(w.join("usr/sbin")).unwrap();
@@ -1564,16 +1630,29 @@ mod tests {
}); });
let seed = product_seed_card().unwrap(); let seed = product_seed_card().unwrap();
let userland = [("uutils".to_string(), huu)];
let services = [("openssh".to_string(), hossh), ("netup".to_string(), hnet)]; let services = [("openssh".to_string(), hossh), ("netup".to_string(), hnet)];
let staging = store.root().join("prod-staging"); let staging = store.root().join("prod-staging");
std::fs::create_dir_all(&staging).unwrap(); std::fs::create_dir_all(&staging).unwrap();
assemble_product_rootfs(&store, &base, &services, &seed, &staging).unwrap(); assemble_product_rootfs(&store, &base, &userland, &services, &seed, &staging).unwrap();
// base hidratada + servicios inyectados. // base hidratada + userland + servicios inyectados.
assert!(staging.join("usr/bin/hammerd").is_file(), "núcleo presente"); assert!(staging.join("usr/bin/hammerd").is_file(), "núcleo presente");
assert!(staging.join("proc").is_dir(), "mountpoint vacío recreado"); assert!(staging.join("proc").is_dir(), "mountpoint vacío recreado");
assert!(staging.join("usr/bin/coreutils").is_file(), "uutils inyectado");
assert!(staging.join("usr/sbin/sshd").is_file(), "openssh inyectado"); assert!(staging.join("usr/sbin/sshd").is_file(), "openssh inyectado");
assert!(staging.join("usr/bin/netup").is_file(), "netup inyectado"); assert!(staging.join("usr/bin/netup").is_file(), "netup inyectado");
// ADELGAZAR BUSYBOX: /usr/bin/ls apunta ahora a coreutils (Rust), no a busybox.
assert_eq!(
std::fs::read_link(staging.join("usr/bin/ls")).unwrap(),
std::path::PathBuf::from("coreutils"),
"el userland Rust ensombrece el applet busybox en /usr/bin"
);
// El applet busybox `cat` en /bin fue RETIRADO (uutils provee cat) ⇒ no compite en PATH.
assert!(!staging.join("bin/cat").exists(), "el applet busybox /bin/cat reemplazado fue retirado");
// Lo NO reemplazado por Rust (sh) y el binario busybox quedan intactos.
assert!(staging.join("bin/sh").exists(), "/bin/sh (ash) interino se preserva");
assert!(staging.join("bin/busybox").is_file(), "el binario busybox queda (fallback)");
// la seed fue sobrescrita con la de producto (3 cards). // la seed fue sobrescrita con la de producto (3 cards).
let got = std::fs::read_to_string(staging.join("ente/seed.card.json")).unwrap(); let got = std::fs::read_to_string(staging.join("ente/seed.card.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&got).unwrap(); let v: serde_json::Value = serde_json::from_str(&got).unwrap();
@@ -1590,12 +1669,13 @@ mod tests {
} }
#[test] #[test]
fn shipped_service_recipes_parse_and_hash() { fn shipped_product_recipes_parse_and_hash() {
// Las recetas de servicio reales (netup, openssh) cargan y hashean — no contaminan el núcleo. // Las recetas de producto reales (userland Rust + servicios) cargan y hashean — viven fuera
// del núcleo (no entran a STAGE1_COMPONENTS ni al selfhost-verify).
let recipes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../recipes"); let recipes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../recipes");
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let store = Store::open(tmp.path().join("store")).unwrap(); let store = Store::open(tmp.path().join("store")).unwrap();
for name in SERVICE_COMPONENTS { for name in USERLAND_COMPONENTS.iter().chain(SERVICE_COMPONENTS.iter()) {
let path = recipes_dir.join(format!("{name}.toml")); let path = recipes_dir.join(format!("{name}.toml"));
let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}")); let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}"));
assert_eq!(&r.name, name); assert_eq!(&r.name, name);
+8 -2
View File
@@ -42,10 +42,13 @@ chmod 0600 "$RFS/root/.ssh/authorized_keys"
# sanity: el árbol de producto trae lo que esperamos (sin ensamblarlo acá) # sanity: el árbol de producto trae lo que esperamos (sin ensamblarlo acá)
echo "==> chequeo de contenido del artefacto:" echo "==> chequeo de contenido del artefacto:"
for f in usr/sbin/sshd usr/bin/netup etc/ssh/sshd_config etc/passwd ente/seed.card.json; do for f in usr/sbin/sshd usr/bin/netup usr/bin/coreutils usr/bin/find usr/bin/rg etc/ssh/sshd_config etc/passwd ente/seed.card.json; do
[ -e "$RFS/$f" ] && echo " ok $f" || { echo " FALTA $f"; exit 1; } [ -e "$RFS/$f" ] && echo " ok $f" || { echo " FALTA $f"; exit 1; }
done done
grep -q '"label": "sshd"' "$RFS/ente/seed.card.json" && echo " ok card sshd en la seed" || { echo " FALTA card sshd"; exit 1; } grep -q '"label": "sshd"' "$RFS/ente/seed.card.json" && echo " ok card sshd en la seed" || { echo " FALTA card sshd"; exit 1; }
# adelgazar busybox: /usr/bin/ls debe apuntar a coreutils (Rust), no a busybox
lstgt=$(readlink "$RFS/usr/bin/ls" 2>/dev/null || true)
[ "$lstgt" = coreutils ] && echo " ok /usr/bin/ls → coreutils (ensombrece busybox)" || echo " !! /usr/bin/ls → ${lstgt:-?}"
# empaquetar (root-owned) y bootear # empaquetar (root-owned) y bootear
( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz" ( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
@@ -63,7 +66,10 @@ start=$(date +%s); ok=0
while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do
kill -0 $QPID 2>/dev/null || { echo "!! QEMU murió"; break; } kill -0 $QPID 2>/dev/null || { echo "!! QEMU murió"; break; }
if out=$(ssh "${SSHOPTS[@]}" root@localhost \ if out=$(ssh "${SSHOPTS[@]}" root@localhost \
'echo PRODUCT_SSH_OK uid=$(id -u); cat /ente/seed.card.json | grep -o "\"label\": \"[a-z-]*\"" | tr "\n" " "; echo; uname -sr' 2>/dev/null); then 'echo PRODUCT_SSH_OK uid=$(id -u); uname -sr;
printf "ls: "; command -v ls; ls --version 2>&1 | head -1;
printf "find: "; find --version 2>&1 | head -1;
printf "rg: "; rg --version 2>&1 | head -1' 2>/dev/null); then
echo "============== RESPUESTA DEL GUEST =============="; echo "$out"; echo "================================================" echo "============== RESPUESTA DEL GUEST =============="; echo "$out"; echo "================================================"
echo "$out" | grep -q PRODUCT_SSH_OK && ok=1; break echo "$out" | grep -q PRODUCT_SSH_OK && ok=1; break
fi fi