harness: wrapper linker-driver que quita -static en links -shared

Las extensiones Python (python.extension_module → shared_module) son .so que el
intérprete del sandbox debe dlopen; intrínsecamente dinámicas. El harness inyecta
LDFLAGS=-static para link=static, y meson/autotools lo aplican a TODOS los targets
por igual ⇒ el .so sale ET_EXEC y dlopen da "Exec format error" (el muro de
gobject-introspection: _giscanner.cpython-312.so no cargaba).

Fix estructural (elegido sobre flip-a-dynamic): ensure_layout materializa en el
rootfs dos wrappers (hammer-zig-cc/-cxx sobre zig cc/c++) que QUITAN -static SÓLO
cuando el link lleva -shared. -static+-shared es contradictorio ⇒ correcto por
construcción: transparente (byte-idéntico) para todo link normal, arregla TODA
extensión Python futura, no sólo g-i.

Hash-safe: el ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒
no re-hashea ninguno de los 700+ sellados (cache-hit intacto); sólo cambia builds
nuevos con .so dinámicos. Verificado: rota positional params, preserva orden y
espacios; transparente sin -shared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 01:04:23 -04:00
co-authored by Claude Opus 4.8
parent ee2f3b6de3
commit 437d5bb103
+60 -2
View File
@@ -417,8 +417,11 @@ impl Sandbox {
// rompe la reproducibilidad — un no-determinismo que Stage 2 cazó (runbook §8c, SDD 09
// §2). De paso cierra el SIGILL de AVX en qemu64 (runbook §7). Las rutas SIMD que sí
// necesita el runtime (blake3/sha2) son asm con dispatch en runtime: siguen presentes.
("CC", "zig cc -mcpu=baseline"),
("CXX", "zig c++ -mcpu=baseline"),
// Driver vía wrapper (ensure_layout lo materializa en /usr/local/bin): idéntico a
// `zig cc -mcpu=baseline` salvo que quita `-static` en links `-shared` (extensiones
// Python y demás `.so` dlopen-ables). Ver write_cc_wrappers.
("CC", CC_WRAPPER),
("CXX", CXX_WRAPPER),
("AR", "zig ar"),
("LC_ALL", "C"),
("LANG", "C"),
@@ -517,6 +520,61 @@ pub fn ensure_layout(rootfs: &Path, zig_dir: &Path) -> hammer_core::Result<()> {
zig_dir.display()
)));
}
write_cc_wrappers(rootfs)?;
Ok(())
}
/// Nombres (en `/usr/local/bin`, que está en el PATH del sandbox) del driver de compilador que
/// usan CC/CXX por defecto. Ver [`write_cc_wrappers`].
pub const CC_WRAPPER: &str = "hammer-zig-cc";
pub const CXX_WRAPPER: &str = "hammer-zig-cxx";
/// Materializa en el rootfs dos wrappers de linker-driver (`hammer-zig-cc`/`-cxx`) que envuelven a
/// `zig cc`/`zig c++`. Su ÚNICO efecto es QUITAR `-static` cuando el link también lleva `-shared`.
///
/// El harness inyecta `LDFLAGS=-static` para las recetas `link=static` (binario monolítico que pide
/// la hidratación primaria). Pero meson/autotools aplican ese LDFLAGS a TODOS los targets por igual,
/// incluidas las extensiones Python (`python.extension_module` → `shared_module`): un `.so` que el
/// intérprete del sandbox debe `dlopen`. `-static` + `-shared` es contradictorio ⇒ el `.so` sale
/// ET_EXEC y `dlopen` da `Exec format error` (el muro de gobject-introspection: `_giscanner.so`).
///
/// Quitar `-static` SÓLO cuando hay `-shared` es correcto por construcción: un objeto compartido
/// nunca debe ser estático. Para todo link normal (ejecutable/`.a`) el wrapper es transparente ⇒
/// byte-idéntico a `zig cc`. El ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒
/// este cambio NO re-hashea nada sellado; sólo destraba builds nuevos con extensiones dinámicas.
fn write_cc_wrappers(rootfs: &Path) -> hammer_core::Result<()> {
use std::os::unix::fs::PermissionsExt;
let bindir = rootfs.join("usr/local/bin");
std::fs::create_dir_all(&bindir).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude crear {}: {e}", bindir.display()))
})?;
// `case " $* "` detecta `-shared` como palabra completa; el bucle rota los positional params
// (pop del frente, push al fondo salvo `-static`) ⇒ filtra preservando orden y seguro con espacios.
let body = |driver: &str| {
format!(
"#!/bin/sh\n\
case \" $* \" in\n\
\x20 *\" -shared \"*)\n\
\x20 n=$#\n\
\x20 while [ \"$n\" -gt 0 ]; do\n\
\x20 arg=$1; shift\n\
\x20 [ \"$arg\" = \"-static\" ] || set -- \"$@\" \"$arg\"\n\
\x20 n=$((n - 1))\n\
\x20 done\n\
\x20 ;;\n\
esac\n\
exec {driver} -mcpu=baseline \"$@\"\n"
)
};
for (name, driver) in [(CC_WRAPPER, "zig cc"), (CXX_WRAPPER, "zig c++")] {
let path = bindir.join(name);
std::fs::write(&path, body(driver)).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude escribir {}: {e}", path.display()))
})?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display()))
})?;
}
Ok(())
}