> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
tasas H4: un test hace regla lo que era suerte — cero pre_exec en el workspace
pre_exec apaga el camino posix_spawn de Rust y devuelve el spawn a fork+exec: 231 µs contra
3 949 µs con 256 MB tocados en el padre (SDD 25 T1). hammer no usa ninguno hoy, pero nadie lo
comprobaba.
El test se comprobó en los dos sentidos: inyectando un pre_exec en sandbox.rs se pone rojo, y
si el barrido no encuentra fuentes también falla — un guardián que aprueba por vacío es el
fallo de CLAUDE.md §3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
diff --git a/crates/hammer-build/tests/sin_pre_exec.rs b/crates/hammer-build/tests/sin_pre_exec.rs
new file mode 100644
index 00000000..e376ba62
--- /dev/null
+++ b/crates/hammer-build/tests/sin_pre_exec.rs
@@ -0,0 +1,73 @@
+//! Guardián de SDD 25 §7-H4: **ningún camino de spawn de hammer usa `pre_exec`**.
+//!
+//! POR QUÉ. `std::process::Command` de Rust usa `posix_spawn` cuando puede, y `posix_spawn` es
+//! PLANO respecto al tamaño del padre: a 256 MB tocados, `fork+exec` costó 3 949 µs contra 231 µs
+//! (SDD 25 T1, **17×**). En cuanto se le pone un `pre_exec`, Rust apaga ese camino y cae a
+//! `fork+exec`, que copia el espacio de direcciones del padre. hammer lanza builds desde un
+//! proceso que puede tener cientos de MB mapeados.
+//!
+//! Hoy `hammer-build::sandbox` lanza `bwrap` sin `pre_exec` — y hasta ahora eso era SUERTE, no una
+//! regla: nadie lo comprobaba. Esto lo vuelve una regla.
+//!
+//! CUÁNDO SE PUEDE APAGAR. Si algún día hace falta de verdad (un `setsid`, un `unshare` que bwrap
+//! no cubra), la respuesta no es borrar el test: es escribir acá por qué ese caso paga los 17× y
+//! ponerlo en la lista de excepciones, para que el precio quede a la vista de quien lo lea.
+
+use std::path::{Path, PathBuf};
+
+fn raiz_del_workspace() -> PathBuf {
+ // CARGO_MANIFEST_DIR = crates/hammer-build
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(Path::parent)
+ .expect("crates/<crate> tiene dos ancestros")
+ .to_path_buf()
+}
+
+fn rs_de(dir: &Path, out: &mut Vec<PathBuf>) {
+ let Ok(rd) = std::fs::read_dir(dir) else { return };
+ for e in rd.flatten() {
+ let p = e.path();
+ if p.is_dir() {
+ rs_de(&p, out);
+ } else if p.extension().is_some_and(|x| x == "rs") {
+ out.push(p);
+ }
+ }
+}
+
+#[test]
+fn ningun_spawn_de_hammer_usa_pre_exec() {
+ let raiz = raiz_del_workspace();
+ let crates = raiz.join("crates");
+ assert!(crates.is_dir(), "no encontré {} — ¿se movió el workspace?", crates.display());
+
+ let mut fuentes = Vec::new();
+ rs_de(&crates, &mut fuentes);
+ // Un barrido que no encuentra fuentes aprueba por vacío, que es el fallo de CLAUDE.md §3.
+ assert!(fuentes.len() > 20, "sólo {} fuentes: el barrido no miró nada", fuentes.len());
+
+ let mut culpables = Vec::new();
+ for f in &fuentes {
+ if f == Path::new(file!()) || f.ends_with("tests/sin_pre_exec.rs") {
+ continue;
+ }
+ let Ok(txt) = std::fs::read_to_string(f) else { continue };
+ for (i, l) in txt.lines().enumerate() {
+ let limpia = l.trim_start();
+ if limpia.starts_with("//") || limpia.starts_with("*") {
+ continue; // documentarlo no es usarlo
+ }
+ if l.contains("pre_exec") {
+ culpables.push(format!("{}:{}: {}", f.display(), i + 1, l.trim()));
+ }
+ }
+ }
+
+ assert!(
+ culpables.is_empty(),
+ "SDD 25 §7-H4: `pre_exec` apaga el camino `posix_spawn` de Rust y devuelve el spawn a \
+ `fork+exec` (17× más caro con 256 MB en el padre, SDD 25 T1). Aparece en:\n {}",
+ culpables.join("\n ")
+ );
+}
diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md
index 417632d6..5cb52330 100644
--- a/docs/25-tasas-del-kernel.md
+++ b/docs/25-tasas-del-kernel.md
@@ -403,9 +403,13 @@ Su precio, concreto:
parcheado en la receta. Queda escrito para no rediscutirlo.
- **H3 · `harkaq` usa `openat2 RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS`** donde hoy valida rutas
a mano (T9, ~gratis).
-- **H4 · Auditar los `Command` de hammer que ponen `pre_exec`**: Rust apaga el camino
- `posix_spawn` en cuanto hay `pre_exec`, y cae a `fork+exec` (T1). `hammer-build/sandbox.rs`
- lanza `bwrap` sin `pre_exec` ⇒ está bien hoy; el punto es que sea una regla, no una suerte.
+- **H4 · Ningún `Command` de hammer pone `pre_exec`** — Rust apaga el camino `posix_spawn` en
+ cuanto hay uno, y cae a `fork+exec` (T1: 17× a 256 MB tocados). `hammer-build/sandbox.rs` lanza
+ `bwrap` sin `pre_exec`, o sea que estaba bien **por suerte**: nadie lo comprobaba. **HECHO**: el
+ test `crates/hammer-build/tests/sin_pre_exec.rs` barre las fuentes del workspace y falla si
+ aparece uno (comprobado en los dos sentidos: inyectando un `pre_exec` el test se pone rojo, y un
+ barrido que no encuentra fuentes también, para que no apruebe por vacío). Si algún día hace falta
+ de verdad, la salida es escribir en el test por qué ese caso paga los 17×, no borrarlo.
- **H5 · La materialización de dependencias se queda en hardlink** (T10) y el experimento de
reflink (§9) alimenta SDD 18 (wawafs), donde el store como FS vivo cambiaría el cociente.
- **H6 · No `madvise(MADV_HUGEPAGE)` en el camino de build** mientras `defrag=madvise` y la