gioser

sergio/takana

> 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…

TOML 1389Shell 137Markdown 80Rust 802602 Definiciones

selfhost-verify: pieza 3 (linux-headers swap) — SWAP_LINUX_HEADERS=1, byte-idéntico a Alpine en host

Tercera pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): los headers
UAPI del kernel 6.16.12 que musl/busybox #include, hoy tomados del paquete linux-headers
de Alpine.

- recipes/linux-headers.toml: `make headers` (no `headers_install` — su rsync final falta
  en el toolchain hermético) + unifdef vía HOSTCC=zig cc; .tar.gz (busybox-tar lo
  descomprime sin xz). Sella los 13 subdirs kernel-owned de usr/include.
- assemble_builder: el swap ahora soporta rel_path de DIRECTORIO (reemplaza el árbol
  entero: remove + copy), no sólo binarios. Cubierto por test nuevo
  (builder_rootfs_swaps_toolchain_header_tree_from_source, incl. borrado de huérfanos).
- recipes/linux-headers-alpine-compat.patch: el paquete de Alpine no es el `make headers`
  crudo. Aporta content-pinned 5 archivos que difieren de la salida vainilla; 3 son
  REQUERIDOS (scsi/{scsi,scsi_ioctl,sg}.h — legacy userspace, NO UAPI) porque el applet
  `eject` de busybox los incluye y sin ellos no compila. install los pisa sobre /out y
  limpia el junk (.cmd/Makefile/headers_check.pl/drm) que `cp -a` arrastra.
- scripts/selfhost-verify.sh: SWAP_LINUX_HEADERS=1 construye la receta y emite un --swap
  por subdir kernel-owned (auto-derivado del artefacto sellado).

Criterio de éxito fuerte: `diff -r` del header-tree de hammer contra el de Alpine = VACÍO
⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ reproducibilidad por construcción.
Ensamblado end-to-end en host OK (13 swaps, musl bits/sys intactos). 133 tests verdes.
Pendiente: corrida in-VM para el sello ✓ REPRODUCIBLE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

b05badee231d3f5f3a8ad808397aa9ece272dbc4 · por sergio · (hace 4 meses) · padre 26b10bd75 · ver el árbol en este commit

Delta semántico · 3

crates/hammer-bootstrap/src/lib.rs

5 archivos · +693 −16

diff --git a/crates/hammer-bootstrap/src/lib.rs b/crates/hammer-bootstrap/src/lib.rs
index 581cb28c..9ba82bd2 100644
--- a/crates/hammer-bootstrap/src/lib.rs
+++ b/crates/hammer-bootstrap/src/lib.rs
@@ -730,29 +730,37 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
     }
     link_or_copy_tree(&spec.toolchain_src, &staging.join("toolchain"), None)?;
 
-    // 3b) Swaps de variante (b): herramientas construidas por hammer desde fuente pisan su versión
-    //     Alpine en /toolchain. El binario sellado es estático musl ⇒ corre sin el shim del loader.
+    // 3b) Swaps de variante (b): piezas construidas por hammer desde fuente pisan su versión Alpine
+    //     en /toolchain. Un `rel_path` puede apuntar a un BINARIO (estático musl ⇒ corre sin el shim
+    //     del loader; p.ej. make, busybox) o a un DIRECTORIO (un árbol de headers; p.ej. los
+    //     `usr/include/{linux,asm,…}` de linux-headers). En el caso directorio se reemplaza el árbol
+    //     entero (remove + copy) para no dejar archivos Alpine huérfanos.
     for swap in &spec.swaps {
         let art_dir = store.path_of(&swap.artifact, &swap.name);
         let src = art_dir.join(&swap.rel_path);
-        if !src.is_file() {
+        let dst = staging.join("toolchain").join(&swap.rel_path);
+        if src.is_dir() {
+            let _ = std::fs::remove_dir_all(&dst);
+            std::fs::create_dir_all(&dst)?;
+            link_or_copy_tree(&src, &dst, None)?;
+        } else if src.is_file() {
+            if let Some(parent) = dst.parent() {
+                std::fs::create_dir_all(parent)?;
+            }
+            let _ = std::fs::remove_file(&dst);
+            if std::fs::hard_link(&src, &dst).is_err() {
+                std::fs::copy(&src, &dst)?;
+            }
+            set_executable(&dst)?;
+        } else {
             return Err(Error::Other(format!(
-                "swap '{}': '{}' no existe en el artefacto sellado {} ({})",
+                "swap '{}': '{}' no existe (ni archivo ni directorio) en el artefacto sellado {} ({})",
                 swap.name,
                 swap.rel_path,
                 swap.artifact,
                 src.display(),
             )));
         }
-        let dst = staging.join("toolchain").join(&swap.rel_path);
-        if let Some(parent) = dst.parent() {
-            std::fs::create_dir_all(parent)?;
-        }
-        let _ = std::fs::remove_file(&dst);
-        if std::fs::hard_link(&src, &dst).is_err() {
-            std::fs::copy(&src, &dst)?;
-        }
-        set_executable(&dst)?;
     }
 
     // 4) Semilla → /store/<hash>-seed-<kind>, tal cual está sellada afuera, para que el `hammer` de
@@ -1474,7 +1482,42 @@ mod tests {
         }];
         let out = tmp.path().join("builder-out");
         let err = builder_rootfs(&spec, &store, &out).unwrap_err().to_string();
-        assert!(err.contains("no existe en el artefacto sellado"), "{err}");
+        assert!(err.contains("no existe") && err.contains("artefacto sellado"), "{err}");
+    }
+
+    #[test]
+    fn builder_rootfs_swaps_toolchain_header_tree_from_source() {
+        let tmp = tempfile::tempdir().unwrap();
+        let (store, mut spec) = builder_fixture(tmp.path());
+
+        // Artefacto sellado de un `linux-headers` con un árbol `usr/include/linux` propio.
+        let headers = seal_component(&store, "linux-headers", "abcd", |w| {
+            std::fs::create_dir_all(w.join("usr/include/linux")).unwrap();
+            std::fs::write(w.join("usr/include/linux/version.h"), b"#define HAMMER 1\n").unwrap();
+            std::fs::write(w.join("usr/include/linux/stat.h"), b"struct hammer_stat;\n").unwrap();
+        });
+        // El toolchain Alpine tiene un linux/ con un archivo huérfano que el swap debe borrar.
+        let tc_linux = spec.toolchain_src.join("usr/include/linux");
+        std::fs::create_dir_all(&tc_linux).unwrap();
+        std::fs::write(tc_linux.join("version.h"), b"#define ALPINE 1\n").unwrap();
+        std::fs::write(tc_linux.join("orphan.h"), b"alpine-only\n").unwrap();
+
+        spec.swaps = vec![ToolchainSwap {
+            name: "linux-headers".into(),
+            artifact: headers,
+            rel_path: "usr/include/linux".into(),
+        }];
+
+        let out = tmp.path().join("builder-out");
+        builder_rootfs(&spec, &store, &out).unwrap();
+
+        let v = std::fs::read(out.join("toolchain/usr/include/linux/version.h")).unwrap();
+        assert_eq!(v, b"#define HAMMER 1\n", "el swap pisó el version.h de Alpine");
+        assert!(out.join("toolchain/usr/include/linux/stat.h").is_file(), "copió el árbol entero");
+        assert!(
+            !out.join("toolchain/usr/include/linux/orphan.h").exists(),
+            "el swap-directorio borró el huérfano de Alpine"
+        );
     }
 
     #[test]
diff --git a/docs/10-roadmap.md b/docs/10-roadmap.md
index 522f5eb3..1cd0bf29 100644
--- a/docs/10-roadmap.md
+++ b/docs/10-roadmap.md
@@ -256,8 +256,22 @@ Diseño completo en [SDD 11 — Bootstrap from-scratch](11-bootstrap.md). Resume
   reconstruyó los 4/4 dentro de la VM con el `/toolchain` hammerizado (make `fbad44ac…` + busybox
   `56664d70…` pisando los de Alpine) y el `of_tree(stage1')` **igualó** la referencia `9adefb82…` ⇒
   `✓ REPRODUCIBLE: stage1' == stage1`, `DRIVER_RC=0` (~54 min, arje-zero cu=1 in-VM). **La procedencia
-  del builder ya no es "todo Alpine" y el auto-alojamiento sigue bit-a-bit.** **Pendiente:** seguir
-  reemplazando piezas — linux-headers → bwrap → rust/llvm (cada una swapeada y re-verificada igual).
+  del builder ya no es "todo Alpine" y el auto-alojamiento sigue bit-a-bit.** **Pieza 3 — linux-headers
+  (`recipes/linux-headers.toml`, kernel 6.16.12, validada en host):** los headers UAPI que musl/busybox
+  `#include`. Construidos con `make headers` (NO `headers_install`: su `rsync` final falta en el toolchain
+  hermético) + `unifdef` vía HOSTCC=zig cc. El swap es de **directorio**, no de binario: se extendió
+  `assemble_builder` para reemplazar árboles enteros (los 13 subdirs kernel-owned `usr/include/{linux,asm,
+  asm-generic,cxl,fwctl,misc,mtd,rdma,regulator,scsi,sound,video,xen}`), dejando intactos los de musl
+  (bits/sys/net…). Hallazgo clave: el paquete de Alpine NO es el `make headers` crudo — trae 5 archivos
+  distintos a la salida vainilla; 3 de ellos (`scsi/{scsi,scsi_ioctl,sg}.h`, legacy userspace, NO UAPI)
+  son **requeridos** porque el applet `eject` de busybox los incluye y sin ellos no compila. Se aportan
+  content-pinned vía `recipes/linux-headers-alpine-compat.patch` (+ 2 cosméticos: kernel.h/if_tunnel.h) y
+  se pisan sobre la salida. Criterio de éxito fuerte alcanzado: **`diff -r` del header-tree hammer contra
+  el de Alpine = vacío** ⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ todo lo que compile abajo
+  reproduce de por sí (garantía por construcción, más fuerte que un rebuild). Expuesto con
+  `SWAP_LINUX_HEADERS=1`; ensamblado end-to-end en host OK. **Pendiente:** la corrida in-VM
+  (`KVM=1 MEM=24576 SWAP_LINUX_HEADERS=1 ./scripts/selfhost-verify.sh`) para el sello `✓ REPRODUCIBLE`,
+  y seguir con bwrap → rust/llvm (cada una swapeada y re-verificada igual).
 - ⏭️ **También pendiente (Stage 1):** bus único (B.2: exponer el `CRASHED` a la capa de IA) y
   atestación arje (A1/A2).
 
diff --git a/recipes/linux-headers-alpine-compat.patch b/recipes/linux-headers-alpine-compat.patch
new file mode 100644
index 00000000..345f4c39
--- /dev/null
+++ b/recipes/linux-headers-alpine-compat.patch
@@ -0,0 +1,531 @@
+--- /dev/null
++++ b/alpine-overlay/linux/kernel.h
+@@ -0,0 +1,10 @@
++/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
++#ifndef _LINUX_KERNEL_H
++#define _LINUX_KERNEL_H
++
++#ifdef __GLIBC__
++#include <linux/sysinfo.h>
++#endif
++#include <linux/const.h>
++
++#endif /* _LINUX_KERNEL_H */
+--- /dev/null
++++ b/alpine-overlay/linux/if_tunnel.h
+@@ -0,0 +1,216 @@
++/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
++#ifndef _IF_TUNNEL_H_
++#define _IF_TUNNEL_H_
++
++#include <linux/types.h>
++#include <asm/byteorder.h>
++
++
++#define SIOCGETTUNNEL   (SIOCDEVPRIVATE + 0)
++#define SIOCADDTUNNEL   (SIOCDEVPRIVATE + 1)
++#define SIOCDELTUNNEL   (SIOCDEVPRIVATE + 2)
++#define SIOCCHGTUNNEL   (SIOCDEVPRIVATE + 3)
++#define SIOCGETPRL      (SIOCDEVPRIVATE + 4)
++#define SIOCADDPRL      (SIOCDEVPRIVATE + 5)
++#define SIOCDELPRL      (SIOCDEVPRIVATE + 6)
++#define SIOCCHGPRL      (SIOCDEVPRIVATE + 7)
++#define SIOCGET6RD      (SIOCDEVPRIVATE + 8)
++#define SIOCADD6RD      (SIOCDEVPRIVATE + 9)
++#define SIOCDEL6RD      (SIOCDEVPRIVATE + 10)
++#define SIOCCHG6RD      (SIOCDEVPRIVATE + 11)
++
++#define GRE_CSUM	__cpu_to_be16(0x8000)
++#define GRE_ROUTING	__cpu_to_be16(0x4000)
++#define GRE_KEY		__cpu_to_be16(0x2000)
++#define GRE_SEQ		__cpu_to_be16(0x1000)
++#define GRE_STRICT	__cpu_to_be16(0x0800)
++#define GRE_REC		__cpu_to_be16(0x0700)
++#define GRE_ACK		__cpu_to_be16(0x0080)
++#define GRE_FLAGS	__cpu_to_be16(0x0078)
++#define GRE_VERSION	__cpu_to_be16(0x0007)
++
++#define GRE_IS_CSUM(f)		((f) & GRE_CSUM)
++#define GRE_IS_ROUTING(f)	((f) & GRE_ROUTING)
++#define GRE_IS_KEY(f)		((f) & GRE_KEY)
++#define GRE_IS_SEQ(f)		((f) & GRE_SEQ)
++#define GRE_IS_STRICT(f)	((f) & GRE_STRICT)
++#define GRE_IS_REC(f)		((f) & GRE_REC)
++#define GRE_IS_ACK(f)		((f) & GRE_ACK)
++
++#define GRE_VERSION_0		__cpu_to_be16(0x0000)
++#define GRE_VERSION_1		__cpu_to_be16(0x0001)
++#define GRE_PROTO_PPP		__cpu_to_be16(0x880b)
++#define GRE_PPTP_KEY_MASK	__cpu_to_be32(0xffff)
++
++struct ip_tunnel_parm {
++	char			name[IFNAMSIZ];
++	int			link;
++	__be16			i_flags;
++	__be16			o_flags;
++	__be32			i_key;
++	__be32			o_key;
++	struct iphdr		iph;
++};
++
++enum {
++	IFLA_IPTUN_UNSPEC,
++	IFLA_IPTUN_LINK,
++	IFLA_IPTUN_LOCAL,
++	IFLA_IPTUN_REMOTE,
++	IFLA_IPTUN_TTL,
++	IFLA_IPTUN_TOS,
++	IFLA_IPTUN_ENCAP_LIMIT,
++	IFLA_IPTUN_FLOWINFO,
++	IFLA_IPTUN_FLAGS,
++	IFLA_IPTUN_PROTO,
++	IFLA_IPTUN_PMTUDISC,
++	IFLA_IPTUN_6RD_PREFIX,
++	IFLA_IPTUN_6RD_RELAY_PREFIX,
++	IFLA_IPTUN_6RD_PREFIXLEN,
++	IFLA_IPTUN_6RD_RELAY_PREFIXLEN,
++	IFLA_IPTUN_ENCAP_TYPE,
++	IFLA_IPTUN_ENCAP_FLAGS,
++	IFLA_IPTUN_ENCAP_SPORT,
++	IFLA_IPTUN_ENCAP_DPORT,
++	IFLA_IPTUN_COLLECT_METADATA,
++	IFLA_IPTUN_FWMARK,
++	__IFLA_IPTUN_MAX,
++};
++#define IFLA_IPTUN_MAX	(__IFLA_IPTUN_MAX - 1)
++
++enum tunnel_encap_types {
++	TUNNEL_ENCAP_NONE,
++	TUNNEL_ENCAP_FOU,
++	TUNNEL_ENCAP_GUE,
++	TUNNEL_ENCAP_MPLS,
++};
++
++#define TUNNEL_ENCAP_FLAG_CSUM		(1<<0)
++#define TUNNEL_ENCAP_FLAG_CSUM6		(1<<1)
++#define TUNNEL_ENCAP_FLAG_REMCSUM	(1<<2)
++
++/* SIT-mode i_flags */
++#define	SIT_ISATAP	0x0001
++
++struct ip_tunnel_prl {
++	__be32			addr;
++	__u16			flags;
++	__u16			__reserved;
++	__u32			datalen;
++	__u32			__reserved2;
++	/* data follows */
++};
++
++/* PRL flags */
++#define	PRL_DEFAULT		0x0001
++
++struct ip_tunnel_6rd {
++	struct in6_addr		prefix;
++	__be32			relay_prefix;
++	__u16			prefixlen;
++	__u16			relay_prefixlen;
++};
++
++enum {
++	IFLA_GRE_UNSPEC,
++	IFLA_GRE_LINK,
++	IFLA_GRE_IFLAGS,
++	IFLA_GRE_OFLAGS,
++	IFLA_GRE_IKEY,
++	IFLA_GRE_OKEY,
++	IFLA_GRE_LOCAL,
++	IFLA_GRE_REMOTE,
++	IFLA_GRE_TTL,
++	IFLA_GRE_TOS,
++	IFLA_GRE_PMTUDISC,
++	IFLA_GRE_ENCAP_LIMIT,
++	IFLA_GRE_FLOWINFO,
++	IFLA_GRE_FLAGS,
++	IFLA_GRE_ENCAP_TYPE,
++	IFLA_GRE_ENCAP_FLAGS,
++	IFLA_GRE_ENCAP_SPORT,
++	IFLA_GRE_ENCAP_DPORT,
++	IFLA_GRE_COLLECT_METADATA,
++	IFLA_GRE_IGNORE_DF,
++	IFLA_GRE_FWMARK,
++	IFLA_GRE_ERSPAN_INDEX,
++	IFLA_GRE_ERSPAN_VER,
++	IFLA_GRE_ERSPAN_DIR,
++	IFLA_GRE_ERSPAN_HWID,
++	__IFLA_GRE_MAX,
++};
++
++#define IFLA_GRE_MAX	(__IFLA_GRE_MAX - 1)
++
++/* VTI-mode i_flags */
++#define VTI_ISVTI ((__be16)0x0001)
++
++enum {
++	IFLA_VTI_UNSPEC,
++	IFLA_VTI_LINK,
++	IFLA_VTI_IKEY,
++	IFLA_VTI_OKEY,
++	IFLA_VTI_LOCAL,
++	IFLA_VTI_REMOTE,
++	IFLA_VTI_FWMARK,
++	__IFLA_VTI_MAX,
++};
++
++#define IFLA_VTI_MAX	(__IFLA_VTI_MAX - 1)
++
++/* Historically, tunnel flags have been defined as __be16 and now there are
++ * no free bits left. It is strongly advised to switch the already existing
++ * userspace code to the new *_BIT definitions from down below, as __be16
++ * can't be simply cast to a wider type on LE systems. All new flags and
++ * code must use *_BIT only.
++ */
++
++#define TUNNEL_CSUM		__cpu_to_be16(0x01)
++#define TUNNEL_ROUTING		__cpu_to_be16(0x02)
++#define TUNNEL_KEY		__cpu_to_be16(0x04)
++#define TUNNEL_SEQ		__cpu_to_be16(0x08)
++#define TUNNEL_STRICT		__cpu_to_be16(0x10)
++#define TUNNEL_REC		__cpu_to_be16(0x20)
++#define TUNNEL_VERSION		__cpu_to_be16(0x40)
++#define TUNNEL_NO_KEY		__cpu_to_be16(0x80)
++#define TUNNEL_DONT_FRAGMENT    __cpu_to_be16(0x0100)
++#define TUNNEL_OAM		__cpu_to_be16(0x0200)
++#define TUNNEL_CRIT_OPT		__cpu_to_be16(0x0400)
++#define TUNNEL_GENEVE_OPT	__cpu_to_be16(0x0800)
++#define TUNNEL_VXLAN_OPT	__cpu_to_be16(0x1000)
++#define TUNNEL_NOCACHE		__cpu_to_be16(0x2000)
++#define TUNNEL_ERSPAN_OPT	__cpu_to_be16(0x4000)
++#define TUNNEL_GTP_OPT		__cpu_to_be16(0x8000)
++
++#define TUNNEL_OPTIONS_PRESENT \
++		(TUNNEL_GENEVE_OPT | TUNNEL_VXLAN_OPT | TUNNEL_ERSPAN_OPT | \
++		TUNNEL_GTP_OPT)
++
++enum {
++	IP_TUNNEL_CSUM_BIT		= 0U,
++	IP_TUNNEL_ROUTING_BIT,
++	IP_TUNNEL_KEY_BIT,
++	IP_TUNNEL_SEQ_BIT,
++	IP_TUNNEL_STRICT_BIT,
++	IP_TUNNEL_REC_BIT,
++	IP_TUNNEL_VERSION_BIT,
++	IP_TUNNEL_NO_KEY_BIT,
++	IP_TUNNEL_DONT_FRAGMENT_BIT,
++	IP_TUNNEL_OAM_BIT,
++	IP_TUNNEL_CRIT_OPT_BIT,
++	IP_TUNNEL_GENEVE_OPT_BIT,	/* OPTIONS_PRESENT */
++	IP_TUNNEL_VXLAN_OPT_BIT,	/* OPTIONS_PRESENT */
++	IP_TUNNEL_NOCACHE_BIT,
++	IP_TUNNEL_ERSPAN_OPT_BIT,	/* OPTIONS_PRESENT */
++	IP_TUNNEL_GTP_OPT_BIT,		/* OPTIONS_PRESENT */
++
++	IP_TUNNEL_VTI_BIT,
++	IP_TUNNEL_SIT_ISATAP_BIT	= IP_TUNNEL_VTI_BIT,
++
++	/* Flags starting from here are not available via the old UAPI */
++	IP_TUNNEL_PFCP_OPT_BIT,		/* OPTIONS_PRESENT */
++
++	__IP_TUNNEL_FLAG_NUM,
++};
++
++#endif /* _IF_TUNNEL_H_ */
+--- /dev/null
++++ b/alpine-overlay/scsi/scsi.h
+@@ -0,0 +1,150 @@
++#ifndef _SCSI_SCSI_H
++#define _SCSI_SCSI_H
++
++#define TEST_UNIT_READY 0x00
++#define REZERO_UNIT 0x01
++#define REQUEST_SENSE 0x03
++#define FORMAT_UNIT 0x04
++#define READ_BLOCK_LIMITS 0x05
++#define REASSIGN_BLOCKS 0x07
++#define READ_6 0x08
++#define WRITE_6 0x0a
++#define SEEK_6 0x0b
++#define READ_REVERSE 0x0f
++#define WRITE_FILEMARKS 0x10
++#define SPACE 0x11
++#define INQUIRY 0x12
++#define RECOVER_BUFFERED_DATA 0x14
++#define MODE_SELECT 0x15
++#define RESERVE 0x16
++#define RELEASE 0x17
++#define COPY 0x18
++#define ERASE 0x19
++#define MODE_SENSE 0x1a
++#define START_STOP 0x1b
++#define RECEIVE_DIAGNOSTIC 0x1c
++#define SEND_DIAGNOSTIC 0x1d
++#define ALLOW_MEDIUM_REMOVAL 0x1e
++#define SET_WINDOW 0x24
++#define READ_CAPACITY 0x25
++#define READ_10 0x28
++#define WRITE_10 0x2a
++#define SEEK_10 0x2b
++#define WRITE_VERIFY 0x2e
++#define VERIFY 0x2f
++#define SEARCH_HIGH 0x30
++#define SEARCH_EQUAL 0x31
++#define SEARCH_LOW 0x32
++#define SET_LIMITS 0x33
++#define PRE_FETCH 0x34
++#define READ_POSITION 0x34
++#define SYNCHRONIZE_CACHE 0x35
++#define LOCK_UNLOCK_CACHE 0x36
++#define READ_DEFECT_DATA 0x37
++#define MEDIUM_SCAN 0x38
++#define COMPARE 0x39
++#define COPY_VERIFY 0x3a
++#define WRITE_BUFFER 0x3b
++#define READ_BUFFER 0x3c
++#define UPDATE_BLOCK 0x3d
++#define READ_LONG 0x3e
++#define WRITE_LONG 0x3f
++#define CHANGE_DEFINITION 0x40
++#define WRITE_SAME 0x41
++#define READ_TOC 0x43
++#define LOG_SELECT 0x4c
++#define LOG_SENSE 0x4d
++#define MODE_SELECT_10 0x55
++#define RESERVE_10 0x56
++#define RELEASE_10 0x57
++#define MODE_SENSE_10 0x5a
++#define PERSISTENT_RESERVE_IN 0x5e
++#define PERSISTENT_RESERVE_OUT 0x5f
++#define MOVE_MEDIUM 0xa5
++#define READ_12 0xa8
++#define WRITE_12 0xaa
++#define WRITE_VERIFY_12 0xae
++#define SEARCH_HIGH_12 0xb0
++#define SEARCH_EQUAL_12 0xb1
++#define SEARCH_LOW_12 0xb2
++#define READ_ELEMENT_STATUS 0xb8
++#define SEND_VOLUME_TAG 0xb6
++#define WRITE_LONG_2 0xea
++#define GOOD 0x00
++#define CHECK_CONDITION 0x01
++#define CONDITION_GOOD 0x02
++#define BUSY 0x04
++#define INTERMEDIATE_GOOD 0x08
++#define INTERMEDIATE_C_GOOD 0x0a
++#define RESERVATION_CONFLICT 0x0c
++#define COMMAND_TERMINATED 0x11
++#define QUEUE_FULL 0x14
++#define STATUS_MASK 0x3e
++#define NO_SENSE 0x00
++#define RECOVERED_ERROR 0x01
++#define NOT_READY 0x02
++#define MEDIUM_ERROR 0x03
++#define HARDWARE_ERROR 0x04
++#define ILLEGAL_REQUEST 0x05
++#define UNIT_ATTENTION 0x06
++#define DATA_PROTECT 0x07
++#define BLANK_CHECK 0x08
++#define COPY_ABORTED 0x0a
++#define ABORTED_COMMAND 0x0b
++#define VOLUME_OVERFLOW 0x0d
++#define MISCOMPARE 0x0e
++#define TYPE_DISK 0x00
++#define TYPE_TAPE 0x01
++#define TYPE_PROCESSOR 0x03
++#define TYPE_WORM 0x04
++#define TYPE_ROM 0x05
++#define TYPE_SCANNER 0x06
++#define TYPE_MOD 0x07
++#define TYPE_MEDIUM_CHANGER 0x08
++#define TYPE_ENCLOSURE 0x0d
++#define TYPE_NO_LUN 0x7f
++#define COMMAND_COMPLETE 0x00
++#define EXTENDED_MESSAGE 0x01
++#define EXTENDED_MODIFY_DATA_POINTER 0x00
++#define EXTENDED_SDTR 0x01
++#define EXTENDED_EXTENDED_IDENTIFY 0x02
++#define EXTENDED_WDTR 0x03
++#define SAVE_POINTERS 0x02
++#define RESTORE_POINTERS 0x03
++#define DISCONNECT 0x04
++#define INITIATOR_ERROR 0x05
++#define ABORT 0x06
++#define MESSAGE_REJECT 0x07
++#define NOP 0x08
++#define MSG_PARITY_ERROR 0x09
++#define LINKED_CMD_COMPLETE 0x0a
++#define LINKED_FLG_CMD_COMPLETE 0x0b
++#define BUS_DEVICE_RESET 0x0c
++#define INITIATE_RECOVERY 0x0f
++#define RELEASE_RECOVERY 0x10
++#define SIMPLE_QUEUE_TAG 0x20
++#define HEAD_OF_QUEUE_TAG 0x21
++#define ORDERED_QUEUE_TAG 0x22
++#define SCSI_IOCTL_GET_IDLUN 0x5382
++#define SCSI_IOCTL_TAGGED_ENABLE 0x5383
++#define SCSI_IOCTL_TAGGED_DISABLE 0x5384
++#define SCSI_IOCTL_PROBE_HOST 0x5385
++#define SCSI_IOCTL_GET_BUS_NUMBER 0x5386
++
++struct ccs_modesel_head {
++	unsigned char _r1;
++	unsigned char medium;
++	unsigned char _r2;
++	unsigned char block_desc_length;
++	unsigned char density;
++	unsigned char number_blocks_hi;
++	unsigned char number_blocks_med;
++	unsigned char number_blocks_lo;
++	unsigned char _r3;
++	unsigned char block_length_hi;
++	unsigned char block_length_med;
++	unsigned char block_length_lo;
++};
++
++#endif
++
+--- /dev/null
++++ b/alpine-overlay/scsi/scsi_ioctl.h
+@@ -0,0 +1,11 @@
++#ifndef _SCSI_IOCTL_H
++#define _SCSI_IOCTL_H
++#define SCSI_IOCTL_SEND_COMMAND 1
++#define SCSI_IOCTL_TEST_UNIT_READY 2
++#define SCSI_IOCTL_BENCHMARK_COMMAND 3
++#define SCSI_IOCTL_SYNC 4
++#define SCSI_IOCTL_START_UNIT 5
++#define SCSI_IOCTL_STOP_UNIT 6
++#define SCSI_IOCTL_DOORLOCK 0x5380
++#define SCSI_IOCTL_DOORUNLOCK 0x5381
++#endif
+--- /dev/null
++++ b/alpine-overlay/scsi/sg.h
+@@ -0,0 +1,129 @@
++#ifndef _SCSI_SG_H
++#define _SCSI_SG_H
++
++#define SG_DXFER_NONE -1
++#define SG_DXFER_TO_DEV -2
++#define SG_DXFER_FROM_DEV -3
++#define SG_DXFER_TO_FROM_DEV -4
++#define SG_FLAG_DIRECT_IO 1
++#define SG_FLAG_LUN_INHIBIT 2
++#define SG_FLAG_NO_DXFER 0x10000
++#define SG_INFO_OK_MASK 0x1
++#define SG_INFO_OK 0x0
++#define SG_INFO_CHECK 0x1
++#define SG_INFO_DIRECT_IO_MASK 0x6
++#define SG_INFO_INDIRECT_IO 0x0
++#define SG_INFO_DIRECT_IO 0x2
++#define SG_INFO_MIXED_IO 0x4
++#define SG_EMULATED_HOST 0x2203
++#define SG_SET_TRANSFORM 0x2204
++#define SG_GET_TRANSFORM 0x2205
++#define SG_SET_RESERVED_SIZE 0x2275
++#define SG_GET_RESERVED_SIZE 0x2272
++#define SG_GET_SCSI_ID 0x2276
++#define SG_SET_FORCE_LOW_DMA 0x2279
++#define SG_GET_LOW_DMA 0x227a
++#define SG_SET_FORCE_PACK_ID 0x227b
++#define SG_GET_PACK_ID 0x227c
++#define SG_GET_NUM_WAITING 0x227d
++#define SG_GET_SG_TABLESIZE 0x227F
++#define SG_GET_VERSION_NUM 0x2282
++#define SG_SCSI_RESET 0x2284
++#define SG_SCSI_RESET_NOTHING 0
++#define SG_SCSI_RESET_DEVICE 1
++#define SG_SCSI_RESET_BUS 2
++#define SG_SCSI_RESET_HOST 3
++#define SG_IO 0x2285
++#define SG_GET_REQUEST_TABLE 0x2286
++#define SG_SET_KEEP_ORPHAN 0x2287
++#define SG_GET_KEEP_ORPHAN 0x2288
++#define SG_SCATTER_SZ (8 * 4096)
++#define SG_DEFAULT_RETRIES 1
++#define SG_DEF_FORCE_LOW_DMA 0
++#define SG_DEF_FORCE_PACK_ID 0
++#define SG_DEF_KEEP_ORPHAN 0
++#define SG_DEF_RESERVED_SIZE SG_SCATTER_SZ
++#define SG_MAX_QUEUE 16
++#define SG_BIG_BUFF SG_DEF_RESERVED_SIZE
++#define SG_MAX_SENSE 16
++#define SG_SET_TIMEOUT 0x2201
++#define SG_GET_TIMEOUT 0x2202
++#define SG_GET_COMMAND_Q 0x2270
++#define SG_SET_COMMAND_Q 0x2271
++#define SG_SET_DEBUG 0x227e
++#define SG_NEXT_CMD_LEN 0x2283
++#define SG_DEFAULT_TIMEOUT (60*100) /* 60*HZ */
++#define SG_DEF_COMMAND_Q 0
++#define SG_DEF_UNDERRUN_FLAG 0
++
++typedef struct sg_iovec {
++	void *iov_base;
++	unsigned long iov_len;
++} sg_iovec_t;
++
++typedef struct sg_io_hdr { 
++	int interface_id; 
++	int dxfer_direction; 
++	unsigned char cmd_len;
++	unsigned char mx_sb_len;
++	unsigned short iovec_count;
++	unsigned dxfer_len;
++	void *dxferp;
++	unsigned char *cmdp;
++	unsigned char *sbp;
++	unsigned timeout;
++	unsigned flags;
++	int pack_id;
++	void *usr_ptr;
++	unsigned char status;
++	unsigned char masked_status;
++	unsigned char msg_status;
++	unsigned char sb_len_wr;
++	unsigned short host_status;
++	unsigned short driver_status;
++	int resid; 
++	unsigned int duration;
++	unsigned int info;
++} sg_io_hdr_t;
++
++struct sg_scsi_id {
++	int host_no;
++	int channel;
++	int scsi_id;
++	int lun;
++	int scsi_type;
++	short h_cmd_per_lun;
++	short d_queue_depth;
++	int unused[2];
++};
++
++typedef struct sg_req_info {
++	char req_state;
++	char orphan;
++	char sg_io_owned;
++	char problem;
++	int pack_id;
++	void *usr_ptr;
++	unsigned duration; 
++	int unused; 
++} sg_req_info_t;
++
++typedef struct sg_io_hdr Sg_io_hdr;
++typedef struct sg_io_vec Sg_io_vec;
++typedef struct sg_scsi_id Sg_scsi_id;
++typedef struct sg_req_info Sg_req_info;
++
++struct sg_header {
++	int pack_len;
++	int reply_len;
++	int pack_id;
++	int result;
++	unsigned twelve_byte:1;
++	unsigned target_status:5;
++	unsigned host_status:8;
++	unsigned driver_status:8;
++	unsigned other_flags:10;
++	unsigned char sense_buffer[SG_MAX_SENSE];
++};
++
++#endif
diff --git a/recipes/linux-headers.toml b/recipes/linux-headers.toml
new file mode 100644
index 00000000..aaab2aa3
--- /dev/null
+++ b/recipes/linux-headers.toml
@@ -0,0 +1,72 @@
+# Linux kernel UAPI headers 6.16.12 — tercera pieza del toolchain hammer-from-source (SDD 11 §7.2b).
+#
+# Tras make (pieza 1) y busybox (pieza 2), reemplazamos los headers del kernel que hoy el builder
+# toma del paquete `linux-headers` de Alpine (`/toolchain/usr/include/{linux,asm,asm-generic,…}`).
+# musl y busybox los `#include` al compilar (syscalls, ioctls, structs UAPI), así que son parte real
+# del toolchain, no mero relleno.
+#
+# Versión clavada a la del toolchain Alpine actual: `usr/include/linux/version.h` declara 6.16.12
+# (LINUX_VERSION_CODE 397324). Construir la MISMA versión es lo que mantiene musl/busybox bit-a-bit:
+# un header distinto (un struct/constante que cambió entre versiones) cambiaría el binario resultante.
+#
+# A diferencia de make/busybox esto NO es un binario: `make headers_install` no compila nada del
+# kernel, sólo pasa los headers UAPI por `unifdef` (un toolito host que el propio target construye con
+# HOSTCC) y los copia saneados a INSTALL_HDR_PATH. El árbol resultante (`usr/include/*/`) se sella y
+# el swap-directorio del builder (variante b) lo monta sobre los de Alpine.
+#
+# Tarball .tar.gz (no .xz): `tar` lo autodetecta por cabecera y el busybox-tar de adentro descomprime
+# gzip nativo, sin depender de un `xz` en el rootfs del Stage 1. El sha256 clava el contenido (ADR 0006).
+
+name    = "linux-headers"
+version = "6.16.12"
+
+[source]
+tarball = "https://mirrors.edge.kernel.org/pub/linux/kernel/v6.x/linux-6.16.12.tar.gz"
+sha256  = "ffc6af80b014ddebd55e116aa29a9f7a5256c87a29a8a9dd97270b6d49625109"
+# Overlay de compatibilidad con el paquete linux-headers de Alpine (ADR 0006: contenido clavado en el
+# patch, hasheado al artefacto). El paquete de Alpine NO es el `make headers` crudo: trae 5 archivos
+# que difieren de la salida vainilla y que cualquier consumidor userspace (aquí, busybox) necesita
+# byte-a-byte para reproducir. El patch los CREA bajo `alpine-overlay/` (no toca el árbol del kernel);
+# el `install` los pisa sobre /out tras `make headers`. Por qué overlay y no patch-al-source: `make
+# headers` corre unifdef (renombra los guards `_UAPI…`, borra `__force`/`#ifndef __KERNEL__`), así que
+# parchear el source daría una salida distinta a la de Alpine; pisar la salida final garantiza igualdad.
+#   • linux/kernel.h     — Alpine envuelve `#include <linux/sysinfo.h>` en `#ifdef __GLIBC__` (musl ya
+#                          define `struct sysinfo`). Cosmético aquí (nadie del 4/4 lo incluye), pero lo
+#                          igualamos para que `diff -r` contra Alpine quede limpio.
+#   • linux/if_tunnel.h  — Alpine quita 3 `#include <linux/{if,ip,in6}.h>` (chocan con musl). Cosmético.
+#   • scsi/{scsi,scsi_ioctl,sg}.h — REQUERIDOS: el applet `eject` de busybox hace `#include <scsi/sg.h>`
+#                          y `<scsi/scsi.h>`. NO son UAPI (no salen de `make headers`); los del kernel en
+#                          `include/scsi/` son `__KERNEL__`-only (no compilan en userspace). Alpine trae
+#                          versiones legacy userspace-limpias; sin ellas busybox no compila ⇒ divergiría.
+patches = ["linux-headers-alpine-compat.patch"]
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+
+[build.phases]
+# El árbol del kernel detecta como BuildSys::Make ⇒ el default de compile sería `make -j$(nproc)`,
+# que construiría el kernel ENTERO. No queremos eso: los headers no necesitan compilarse ni config.
+# Override a no-op; todo el trabajo va en `install`.
+compile = "true"
+# El target `headers` (NO `headers_install`) construye `scripts/unifdef` con HOSTCC (zig cc) y satura
+# los headers UAPI por unifdef hacia `./usr/include` (las líneas `HDRINST …`). Evitamos
+# `headers_install` a propósito: su paso final hace un `rsync` de `usr/include` → INSTALL_HDR_PATH, y
+# rsync NO está en el toolchain hermético (ADR 0003) — `headers_install` aborta con "rsync: not found".
+# En su lugar copiamos el árbol ya saturado con `cp -a` (determinista: of_tree ignora timestamps y
+# write-bit, los headers son 0644 sin exec). ARCH=x86 produce los `asm/` de x86_64. Sella
+# `usr/include/{linux,asm,asm-generic,cxl,fwctl,misc,mtd,rdma,regulator,scsi,sound,video,xen}`,
+# exactamente los subdirs que el paquete linux-headers de Alpine posee.
+# 1) `make headers` satura los UAPI en ./usr/include. 2) copiamos a /out. 3) limpiamos lo que el
+# rsync de `headers_install` filtra y que `cp -a` arrastró: los `.*.cmd` de kbuild, el Makefile y
+# headers_check.pl del propio árbol, y drm/ (Alpine no lo empaqueta). 4) pisamos el overlay de Alpine
+# (los 5 archivos del patch) para casar byte-a-byte. Resultado: `diff -r` contra Alpine = vacío.
+install = '''
+make ARCH=x86 HOSTCC='zig cc -mcpu=baseline' headers && \
+mkdir -p /out/usr && cp -a usr/include /out/usr/ && \
+find /out/usr/include -name '.*.cmd' -delete && \
+rm -f /out/usr/include/Makefile /out/usr/include/headers_check.pl && \
+rm -rf /out/usr/include/drm && \
+cp -a alpine-overlay/. /out/usr/include/
+'''
diff --git a/scripts/selfhost-verify.sh b/scripts/selfhost-verify.sh
index 5a58d893..c2345abd 100755
--- a/scripts/selfhost-verify.sh
+++ b/scripts/selfhost-verify.sh
@@ -135,6 +135,23 @@ if [[ "${SWAP_BUSYBOX:-0}" == 1 ]]; then
     say "busybox hammer: $BB_HASH"
     SWAP_ARGS+=(--swap "busybox=$BB_HASH:bin/busybox")
 fi
+if [[ "${SWAP_LINUX_HEADERS:-0}" == 1 ]]; then
+    # Pieza 3: los headers UAPI del kernel. NO es un binario sino un árbol — el swap-directorio
+    # (assemble_builder) reemplaza cada subdir entero. Swapeamos exactamente los subdirs que el
+    # artefacto sellado produce (linux/asm/asm-generic/cxl/fwctl/misc/mtd/rdma/regulator/scsi/sound/
+    # video/xen), que son justo los que el paquete linux-headers de Alpine posee; los de musl
+    # (bits/sys/net/…) quedan intactos.
+    say "variante b — construir linux-headers desde fuente y swapear /toolchain/usr/include/{linux,asm,…}"
+    LH_HASH="$("$HAMMER" --store "$STORE" build recipes/linux-headers.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
+    [[ "$LH_HASH" == b3:* ]] || die "no obtuve el hash sellado de linux-headers"
+    say "linux-headers hammer: $LH_HASH"
+    LH_INC="$(ls -d "$STORE"/*-linux-headers/usr/include 2>/dev/null | head -1)"
+    [[ -d "$LH_INC" ]] || die "no encuentro usr/include en el linux-headers sellado"
+    for d in "$LH_INC"/*/; do
+        sub="$(basename "$d")"
+        SWAP_ARGS+=(--swap "linux-headers=${LH_HASH}:usr/include/$sub")
+    done
+fi
 for s in ${SWAPS:-}; do SWAP_ARGS+=(--swap "$s"); done
 
 # 4) Ensamblar el builder con la referencia embebida.