sandbox: forzar archivos ar deterministas — dos builds sellaban bytes distintos
CÓMO SE VIO. Construir zlib en gioser y en el worker nuevo dio el MISMO ArtifactHash y BYTES DISTINTOS. `hammer why-differs` lo nombró exacto: 13 entradas idénticas, 1 diverge — `usr/lib/libz.a`, cabecera `ar` del miembro `/` (la tabla de símbolos), mtime 1787326910 vs 1787950875. Es la hora de pared de cada build. CAUSA, AISLADA POR BISECCIÓN EN EL LAB (no por lectura de código): · `zig ar` (llvm-ar) → mtime 0 ✅ · `ranlib` del rootfs Alpine → mtime 0 ✅ (Alpine compila binutils con --enable-deterministic-archives) · `ar` y `ranlib` de NUESTRO recipes/binutils.toml → hora de pared ❌ · los mismos con `-D` → mtime 0 ✅ La receta de binutils no pasa --enable-deterministic-archives, así que toda receta que lo materialice como build-dep y llame a ar/ranlib hereda la hora. Y el comentario de SOURCE_DATE_EPOCH en este mismo fichero AFIRMABA cubrir `ar`. Era falso: binutils no lee esa variable. Corregido, porque una suposición escrita en un comentario es la que impide que alguien vuelva a medir. ARREGLO. Wrappers `ar`/`ranlib` en /usr/local/bin (que gana al /usr/bin del PATH) que anteponen `-D`. Mismo patrón que ya usa el sandbox para arreglar esta CLASE de bug sin re-hashear: -mcpu=baseline, CARGO_PROFILE_RELEASE_CODEGEN_UNITS, CARGO_BUILD_JOBS. `-D` antepuesto y no como modificador de la cadena de operación, porque la operación la pone el llamador en "$@" y no se puede reescribir. Verificado: la secuencia exacta de zlib (`ar rc` + `ranlib`) da mtime 0 y el índice del archivo sigue siendo legible (`ar t`, `nm -s`). POR QUÉ AQUÍ Y NO EN LA RECETA. Arreglar recipes/binutils.toml es la cura de raíz, pero `yupana radio binutils` da 421 dependientes transitivos y 365 sellados a deuda, en las siete imágenes — desproporcionado frente a los 9 artefactos realmente contaminados (medidos barriendo los 185 artefactos con .a del store: elfutils, elfutils-libdw ×4, zlib, musl-fts, musl-obstack, argp-standalone). La cura de raíz se agenda para el próximo rebuild del corpus que ya haya que hacer por otro motivo. PENDIENTE Y DICHO EN VOZ ALTA: esto cambia los BYTES sin cambiar la DIRECCIÓN de esos 9. Hay que re-sellarlos a propósito — es el mismo peligro que la huella del lab existe para eliminar, y no se cierra solo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
This commit is contained in:
@@ -426,7 +426,11 @@ impl Sandbox {
|
||||
("LC_ALL", "C"),
|
||||
("LANG", "C"),
|
||||
// Reproducibilidad (pre-Stage 2): fija los timestamps que muchas herramientas embeben
|
||||
// (tar, ar, gzip, __DATE__/__TIME__ vía SOURCE_DATE_EPOCH). Valor fijo y arbitrario
|
||||
// (tar, gzip, __DATE__/__TIME__ vía SOURCE_DATE_EPOCH). Valor fijo y arbitrario
|
||||
// ⚠ NO cubre `ar`/`ranlib`: binutils NO lee SOURCE_DATE_EPOCH — la determinismo de
|
||||
// archivos se decide en `configure` (`--enable-deterministic-archives`) o por opción
|
||||
// `-D` en cada invocación. Este comentario decía que sí lo cubría y era FALSO; lo
|
||||
// desmintió comparar dos builds de zlib (ver AR_WRAPPER/RANLIB_WRAPPER abajo).
|
||||
// (1970-01-01 00:00:01). Las rutas ya son deterministas: el source se bindea en `/src`
|
||||
// (constante entre rebuilds, sea cual sea el work dir del host). Ver SDD 11 / runbook.
|
||||
("SOURCE_DATE_EPOCH", "1"),
|
||||
@@ -529,6 +533,33 @@ pub fn ensure_layout(rootfs: &Path, zig_dir: &Path) -> hammer_core::Result<()> {
|
||||
pub const CC_WRAPPER: &str = "hammer-zig-cc";
|
||||
pub const CXX_WRAPPER: &str = "hammer-zig-cxx";
|
||||
|
||||
/// Herramientas de archivo que se interponen en `/usr/local/bin` para forzar **modo determinista**.
|
||||
///
|
||||
/// ── POR QUÉ (medido 2026-08-28) ─────────────────────────────────────────────────────────────
|
||||
/// Construir `zlib` en dos máquinas dio el MISMO `ArtifactHash` y **bytes distintos**:
|
||||
/// `usr/lib/libz.a` divergía sólo en la cabecera `ar` del miembro `/` (la tabla de símbolos),
|
||||
/// con la hora de pared del build. `hammer why-differs` lo nombró exacto.
|
||||
///
|
||||
/// La causa NO es `zig ar` (llvm-ar sella mtime 0) ni el binutils del rootfs Alpine (que Alpine
|
||||
/// compila con `--enable-deterministic-archives`): es **nuestro propio `recipes/binutils.toml`**,
|
||||
/// que no pasa ese flag. Toda receta que lo materialice como build-dep y llame a `ar`/`ranlib`
|
||||
/// —directamente o vía el `$(RANLIB)` de su Makefile— hereda la hora de pared.
|
||||
///
|
||||
/// ── POR QUÉ AQUÍ Y NO EN LA RECETA ──────────────────────────────────────────────────────────
|
||||
/// Arreglar `recipes/binutils.toml` es la cura de raíz, pero su radio son **421 recetas
|
||||
/// transitivas y 365 sellados a deuda** (medido con `yupana radio binutils`) — desproporcionado
|
||||
/// frente a los 9 artefactos realmente contaminados. El sandbox es donde este proyecto ya
|
||||
/// arregla esta CLASE de bug sin re-hashear: `-mcpu=baseline`, `CARGO_PROFILE_RELEASE_CODEGEN_UNITS`
|
||||
/// y `CARGO_BUILD_JOBS` son exactamente el mismo patrón. La cura de raíz queda pendiente para
|
||||
/// cuando toque un rebuild del corpus por otro motivo (SDD 23), no como campaña propia.
|
||||
///
|
||||
/// ⚠ CONSECUENCIA QUE HAY QUE EJECUTAR, NO SÓLO DECLARAR: esto cambia los BYTES sin cambiar la
|
||||
/// DIRECCIÓN de los 9 artefactos ya contaminados. Un artefacto viejo y uno nuevo son distintos en
|
||||
/// el mismo hash — el peligro exacto que la huella del lab existe para eliminar. Por eso la
|
||||
/// adopción incluye **re-sellar esos 9 a propósito**, no esperar a que caduquen solos.
|
||||
pub const AR_WRAPPER: &str = "ar";
|
||||
pub const RANLIB_WRAPPER: &str = "ranlib";
|
||||
|
||||
/// 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 REQUIERE enlace dinámico.
|
||||
///
|
||||
@@ -594,6 +625,28 @@ fn write_cc_wrappers(rootfs: &Path) -> hammer_core::Result<()> {
|
||||
hammer_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display()))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Archivadores en modo determinista. Ver la doc de AR_WRAPPER.
|
||||
//
|
||||
// `-D` ANTEPUESTO y no como modificador de la cadena de operación (`Drc`): las dos formas
|
||||
// funcionan en binutils (verificado), pero anteponerlo es lo único que compone con una línea
|
||||
// de comando ajena — el llamador pone su propia operación (`rc`, `t`, `x`…) en "$@" y no la
|
||||
// podemos reescribir. `-D` es inocuo en las operaciones de lectura.
|
||||
//
|
||||
// Ruta ABSOLUTA `/usr/bin/<tool>` a propósito: el wrapper vive en `/usr/local/bin`, que va
|
||||
// ANTES en el PATH, así que un `exec ar` se llamaría a sí mismo para siempre.
|
||||
for tool in [AR_WRAPPER, RANLIB_WRAPPER] {
|
||||
let path = bindir.join(tool);
|
||||
let body = format!(
|
||||
"#!/bin/sh\n # Interpuesto por hammer: fuerza archivos deterministas (sin hora de pared en la\n # cabecera `ar`). Sin esto dos builds idénticos sellan bytes distintos. Ver\n # sandbox.rs::AR_WRAPPER.\n [ -x /usr/bin/{tool} ] || exit 127\n exec /usr/bin/{tool} -D \"$@\"\n"
|
||||
);
|
||||
std::fs::write(&path, body).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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user