From 5e489e974ef0429d90b13a2a4939ec1347616fb6 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 5 Sep 2026 17:37:30 +0000 Subject: [PATCH] =?UTF-8?q?firefox:=20BuildID=20determinista=20=E2=80=94?= =?UTF-8?q?=20se=20cierra=20la=20deuda=20de=20reproducibilidad,=20con=20gu?= =?UTF-8?q?ardi=C3=A1n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Medido y probado hoy: `atuq` REPRODUCE bit a bit (`why-differs`: 85 entradas idénticas, 0 divergen), lo que valida el re-empaque determinista del omni.ja, el XPI, los iconos copiados y el BuildID derivado del contenido. El que NO reproducía era la BASE: el `BuildID` de firefox era la fecha y hora del build (`20260905035306` en el artefacto sellado), así que dos construcciones con el mismo ArtifactHash daban bytes distintos. Es el agujero exacto que el invariante de reproducibilidad existe para cerrar, y `build-state.json` no lo puede ver: el hash es input-addressed y no se mueve por esto. Verde y mintiendo. El valor sale de `SOURCE_DATE_EPOCH`, que el sandbox de hammer YA fija a 1 para todas las recetas — mejor que una constante escrita a mano, porque usa el mecanismo de hermeticidad que ya existe en vez de inventar un segundo. Alpine hace lo mismo en su APKBUILD. BuildID esperado: 19700101000001. Va en LAS TRES FASES, por la lección que costó 50 minutos esta misma tarde con MOZ_REQUIRE_SIGNING: son shells distintos y `mach build` re-ejecuta el configure cuando le parece. Y con su GUARDIÁN, hermano del de la firma: la fase install compara el BuildID del artefacto contra el que SOURCE_DATE_EPOCH obliga, y falla el build si no coinciden. Los dos fallos de esta familia —flag que el configure acepta y el artefacto ignora— son mudos y viajan lejos; la única defensa es comprobarlos donde importa, que es dentro del artefacto. --- recipes/firefox.toml | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/recipes/firefox.toml b/recipes/firefox.toml index a46d7e04..4d16baf5 100644 --- a/recipes/firefox.toml +++ b/recipes/firefox.toml @@ -133,16 +133,16 @@ flags = [] # Entra en `hash_inputs` A PROPÓSITO (cambia el contenido del artefacto), y usa `zig objcopy`, que # siempre está en el sandbox ⇒ no agrega una dep de build. strip_debug = true -# ⚠ DEUDA MEDIDA EL 2026-09-05: ESTE ARTEFACTO NO REPRODUCE BIT A BIT ═══════════════════════════ -# `application.ini` del artefacto sellado trae `BuildID=20260905035306`: la FECHA Y HORA del build. -# Dos construcciones de esta misma receta, con el mismo hash, producen bytes distintos — que es -# exactamente el agujero que el invariante de reproducibilidad existe para cerrar, y no se ve en -# `build-state.json` porque el ArtifactHash es input-addressed y no se mueve. -# El arreglo es el que ya usa Alpine: exportar `MOZ_BUILD_DATE` con un valor determinista en las -# fases (su APKBUILD lo deriva de `SOURCE_DATE_EPOCH`; acá el candidato natural es un valor fijo, -# que al vivir en el texto de la fase entra en `hash_inputs` y queda declarado). -# NO se aplica de paso: cambiar una fase re-hashea la receta y cuesta otro build de cuatro horas -# más el re-sellado de `atuq`, que cuelga de ella. Es su propia unidad de trabajo. +# ══ `MOZ_BUILD_DATE`: SIN ESTO EL ARTEFACTO NO REPRODUCE ══════════════════════════════════════ +# El `BuildID` de `application.ini` es, por defecto, la FECHA Y HORA del build. Medido en el +# artefacto sellado del 2026-09-05: `BuildID=20260905035306`. Dos construcciones de esta misma +# receta, con el mismo ArtifactHash, producían bytes distintos — justo el agujero que el invariante +# de reproducibilidad existe para cerrar, y que `build-state.json` no puede ver porque el hash es +# input-addressed y no se mueve por esto. +# +# El valor sale de `SOURCE_DATE_EPOCH`, que el sandbox de hammer YA fija a 1 para todas las recetas. +# Es mejor que una constante escrita a mano: usa el mecanismo de hermeticidad que ya existe, en vez +# de inventar un segundo. Alpine hace exactamente lo mismo en su APKBUILD. [build.phases] configure = ''' @@ -183,6 +183,7 @@ export RUST_TARGET=x86_64-alpine-linux-musl # El `export` con valor vacío es lo que hace llegar la variable PRESENTE y vacía; desexportarla la # dejaría sin definir y volvería al default, que acá es «exigir firma». export MOZ_REQUIRE_SIGNING= +export MOZ_BUILD_DATE="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)" # ── LOS PARCHES ROMPEN EL CHECKSUM DE LOS CRATES VENDORIZADOS ───────────────────────────────── # `error: the listed checksum of third_party/rust/zeitstempel/src/unix.rs has changed`. Los crates @@ -275,6 +276,7 @@ export RUST_TARGET=x86_64-alpine-linux-musl # ver la nota en la fase configure # aceptó — 50 minutos de build para descubrirlo al abrir el navegador. Es exactamente la razón por # la que `RUST_TARGET` se repite en las tres fases. export MOZ_REQUIRE_SIGNING= +export MOZ_BUILD_DATE="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)" # ídem: ver configure export MOZBUILD_STATE_PATH="$PWD/.mozbuild" export MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system export MOZ_NOSPAM=1 @@ -287,6 +289,7 @@ echo "mach build -j$j (MemTotal ${gib} GiB, nproc $n)" install = ''' export RUST_TARGET=x86_64-alpine-linux-musl # ver la nota en la fase configure export MOZ_REQUIRE_SIGNING= # ídem +export MOZ_BUILD_DATE="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)" # ídem export MOZBUILD_STATE_PATH="$PWD/.mozbuild" export MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system DESTDIR=/out ./mach install @@ -310,6 +313,20 @@ if "MOZ_REQUIRE_SIGNING: false" not in t: "las fases (son shells distintos y mach re-ejecuta configure).") print("guardián: MOZ_REQUIRE_SIGNING = false en el artefacto") PY + +# ── GUARDIÁN: QUE EL BuildID NO SEA UNA FECHA ──────────────────────────────────────────────── +# Mismo razonamiento que el de arriba, y el mismo modo de fallo: si `MOZ_BUILD_DATE` no llega, el +# BuildID vuelve a ser la hora del build y el artefacto deja de reproducir SIN QUE NADA LO DIGA — +# el ArtifactHash es input-addressed y no se mueve por esto, así que `build-state.json` seguiría en +# verde. Se comprueba contra el valor que `SOURCE_DATE_EPOCH=1` obliga. +esperado="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)" +real="$(sed -n 's/^BuildID=//p' /out/usr/lib/firefox/application.ini)" +if [ "$real" != "$esperado" ]; then + echo "guardián: BuildID=$real y esperaba $esperado — el artefacto NO reproduce." >&2 + echo " Revisá que MOZ_BUILD_DATE se exporte en TODAS las fases." >&2 + exit 1 +fi +echo "guardián: BuildID = $real (determinista, de SOURCE_DATE_EPOCH)" ''' [deps]