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 1430Shell 145Markdown 87Rust 852973 Definitions

⎇ cf2cf54914e5917613be71ffb49109894c1791c7 crates/hammer-core/src/kernel/hw.rs Rust · 350 lines · 12.8 KB History Who touched what raw

kernel: modo reversa y catálogo de bundles — y las cuatro recetas apagan dos símbolos que ya no existen cf2cf5491 · Sergio ·

1//! Huella de hardware: DMI + PCI + flags de CPU.
2//!
3//! Es la pieza de §2.2 del handoff — «un config que **booteó** en esta huella es un hecho
4//! atestable». Con el §1 del SDD 22 en la mano vale más de lo que el handoff suponía: como el
5//! config ES la identidad del artefacto, atestar por huella no necesita mecanismo nuevo; es un
6//! `ArtifactHash` más una firma.
7//!
8//! ## Qué entra en la huella y qué no
9//! Entran **DMI, PCI y los flags de CPU**: describen la máquina, no lo que hay enchufado hoy. El
10//! USB se **lee y se reporta pero no se hashea** — un pendrive no puede cambiar la clase de
11//! hardware bajo la que se cachea un kernel. Si entrara, la huella sería distinta cada vez y el
12//! CDN cachearía por máquina en vez de por clase, que es justo lo que §2.2 quiere evitar.
13//!
14//! ## Y para qué sirve leerla
15//! **Sólo para contradecir** (handoff §6). Detectar sirve para decirle al usuario «marcaste "no
16//! necesito wifi" y tenés un AX211 activo ahora mismo», nunca para podar el config solo: no se
17//! puede detectar el dock que se enchufa el mes que viene ni el fs del USB de rescate.
18
19use std::collections::{BTreeMap, BTreeSet};
20use std::path::{Path, PathBuf};
21
22use serde::{Deserialize, Serialize};
23
24use crate::hash::ArtifactHash;
25
26/// Campos de `/sys/class/dmi/id` que se leen. Deliberadamente **no** se lee `product_uuid` ni
27/// `product_serial`: identifican al equipo concreto (y son root-only), y la huella tiene que
28/// agrupar máquinas iguales, no distinguirlas.
29const DMI_FIELDS: &[&str] = &[
30    "sys_vendor",
31    "product_name",
32    "product_version",
33    "board_vendor",
34    "board_name",
35    "bios_vendor",
36    "bios_version",
37    "chassis_type",
38];
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct PciDevice {
42    /// `0000:00:1f.3`
43    pub slot: String,
44    /// `0x8086`
45    pub vendor: String,
46    pub device: String,
47    /// Clase de 6 dígitos hex: los 2 primeros son la clase base, los 2 siguientes la subclase.
48    pub class: String,
49    /// Driver bindeado ahora mismo, si hay.
50    pub driver: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct UsbDevice {
55    pub id: String,
56    pub vendor: String,
57    pub product: String,
58    pub driver: Option<String>,
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
62pub struct Hardware {
63    pub dmi: BTreeMap<String, String>,
64    pub pci: Vec<PciDevice>,
65    /// Leído y reportado, pero **fuera** de la huella (ver doc del módulo).
66    pub usb: Vec<UsbDevice>,
67    pub cpu_model: Option<String>,
68    pub cpu_flags: BTreeSet<String>,
69    /// Todos los drivers con al menos un dispositivo bindeado, de cualquier bus.
70    pub bound_drivers: BTreeSet<String>,
71    /// Qué no se pudo leer. Una huella incompleta es un dato, no una excepción: en una VM sin DMI
72    /// o con `/sys` restringido la huella sigue siendo útil, pero quien la lea tiene que saberlo.
73    pub gaps: Vec<String>,
74}
75
76impl Hardware {
77    /// Lee la máquina real. `sys` y `proc` se inyectan para poder testear con un árbol de juguete.
78    pub fn probe(sys: &Path, proc_dir: &Path) -> Hardware {
79        let mut hw = Hardware::default();
80        hw.read_dmi(sys);
81        hw.read_pci(sys);
82        hw.read_usb(sys);
83        hw.read_cpu(proc_dir);
84        hw.read_bound_drivers(sys);
85        hw
86    }
87
88    /// El caso normal: la máquina donde corre esto.
89    pub fn probe_local() -> Hardware {
90        Hardware::probe(Path::new("/sys"), Path::new("/proc"))
91    }
92
93    fn read_dmi(&mut self, sys: &Path) {
94        let dir = sys.join("class/dmi/id");
95        if !dir.is_dir() {
96            self.gaps.push("sin DMI (¿VM o /sys restringido?)".into());
97            return;
98        }
99        for f in DMI_FIELDS {
100            if let Ok(v) = std::fs::read_to_string(dir.join(f)) {
101                let v = v.trim();
102                if !v.is_empty() {
103                    self.dmi.insert((*f).to_string(), v.to_string());
104                }
105            }
106        }
107        if self.dmi.is_empty() {
108            self.gaps.push("DMI presente pero ilegible".into());
109        }
110    }
111
112    fn read_pci(&mut self, sys: &Path) {
113        let dir = sys.join("bus/pci/devices");
114        let Ok(entries) = std::fs::read_dir(&dir) else {
115            self.gaps.push("sin bus PCI".into());
116            return;
117        };
118        let mut devs = Vec::new();
119        for e in entries.flatten() {
120            let p = e.path();
121            let slot = e.file_name().to_string_lossy().into_owned();
122            let g = |f: &str| read_trim(&p.join(f)).unwrap_or_default();
123            devs.push(PciDevice {
124                slot,
125                vendor: strip_0x(&g("vendor")),
126                device: strip_0x(&g("device")),
127                class: strip_0x(&g("class")),
128                driver: link_basename(&p.join("driver")),
129            });
130        }
131        devs.sort_by(|a, b| a.slot.cmp(&b.slot));
132        self.pci = devs;
133    }
134
135    fn read_usb(&mut self, sys: &Path) {
136        let dir = sys.join("bus/usb/devices");
137        let Ok(entries) = std::fs::read_dir(&dir) else {
138            return;
139        };
140        let mut devs = Vec::new();
141        for e in entries.flatten() {
142            let p = e.path();
143            // Las interfaces (`1-1:1.0`) no son dispositivos; sólo los nodos con idVendor.
144            let Some(vendor) = read_trim(&p.join("idVendor")) else {
145                continue;
146            };
147            devs.push(UsbDevice {
148                id: e.file_name().to_string_lossy().into_owned(),
149                vendor,
150                product: read_trim(&p.join("idProduct")).unwrap_or_default(),
151                driver: link_basename(&p.join("driver")),
152            });
153        }
154        devs.sort_by(|a, b| a.id.cmp(&b.id));
155        self.usb = devs;
156    }
157
158    fn read_cpu(&mut self, proc_dir: &Path) {
159        let Ok(text) = std::fs::read_to_string(proc_dir.join("cpuinfo")) else {
160            self.gaps.push("sin /proc/cpuinfo".into());
161            return;
162        };
163        for line in text.lines() {
164            let Some((k, v)) = line.split_once(':') else {
165                continue;
166            };
167            let (k, v) = (k.trim(), v.trim());
168            match k {
169                "model name" if self.cpu_model.is_none() => {
170                    self.cpu_model = Some(v.to_string());
171                }
172                "flags" if self.cpu_flags.is_empty() => {
173                    self.cpu_flags = v.split_whitespace().map(|s| s.to_string()).collect();
174                }
175                _ => {}
176            }
177        }
178    }
179
180    /// Un driver «en uso» es uno con al menos un dispositivo bindeado. Es el dato que necesita el
181    /// gate de no-regresión (#6): lo que hoy funciona tiene que seguir teniendo driver mañana.
182    fn read_bound_drivers(&mut self, sys: &Path) {
183        let Ok(buses) = std::fs::read_dir(sys.join("bus")) else {
184            self.gaps.push("sin /sys/bus".into());
185            return;
186        };
187        for bus in buses.flatten() {
188            let Ok(devs) = std::fs::read_dir(bus.path().join("devices")) else {
189                continue;
190            };
191            for d in devs.flatten() {
192                if let Some(drv) = link_basename(&d.path().join("driver")) {
193                    self.bound_drivers.insert(drv);
194                }
195            }
196        }
197    }
198
199    /// Los bytes canónicos que se hashean. Se exponen para poder auditar una huella sin confiar en
200    /// que el hash se calculó sobre lo que uno cree.
201    pub fn fingerprint_material(&self) -> String {
202        let mut s = String::new();
203        for (k, v) in &self.dmi {
204            s.push_str(&format!("dmi\t{k}\t{v}\n"));
205        }
206        // Sin el slot: mover una tarjeta de ranura no cambia la clase de hardware.
207        let mut pci: Vec<String> = self
208            .pci
209            .iter()
210            .map(|d| format!("pci\t{}\t{}\t{}\n", d.vendor, d.device, d.class))
211            .collect();
212        pci.sort();
213        pci.dedup();
214        s.extend(pci);
215        for f in &self.cpu_flags {
216            s.push_str(&format!("cpuflag\t{f}\n"));
217        }
218        s
219    }
220
221    /// La huella. BLAKE3 sobre [`Hardware::fingerprint_material`], con el mismo tipo que cualquier
222    /// otro identificador por contenido de hammer.
223    pub fn fingerprint(&self) -> ArtifactHash {
224        ArtifactHash::of_bytes(self.fingerprint_material().as_bytes())
225    }
226
227    /// ¿Hay algún dispositivo PCI cuya clase empiece por `prefix`? (`"0280"` = red inalámbrica.)
228    pub fn has_pci_class(&self, prefix: &str) -> bool {
229        self.pci.iter().any(|d| d.class.starts_with(prefix))
230    }
231
232    pub fn has_driver(&self, name: &str) -> bool {
233        self.bound_drivers.contains(name)
234    }
235
236    pub fn has_cpu_flag(&self, flag: &str) -> bool {
237        self.cpu_flags.contains(flag)
238    }
239}
240
241fn read_trim(p: &Path) -> Option<String> {
242    let s = std::fs::read_to_string(p).ok()?;
243    let t = s.trim();
244    if t.is_empty() {
245        None
246    } else {
247        Some(t.to_string())
248    }
249}
250
251fn strip_0x(s: &str) -> String {
252    s.trim().trim_start_matches("0x").to_string()
253}
254
255fn link_basename(p: &Path) -> Option<String> {
256    let target: PathBuf = std::fs::read_link(p).ok()?;
257    Some(target.file_name()?.to_string_lossy().into_owned())
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    fn arbol_de_juguete() -> tempfile::TempDir {
265        let d = tempfile::tempdir().unwrap();
266        let sys = d.path().join("sys");
267        std::fs::create_dir_all(sys.join("class/dmi/id")).unwrap();
268        std::fs::write(sys.join("class/dmi/id/sys_vendor"), "LENOVO\n").unwrap();
269        std::fs::write(sys.join("class/dmi/id/product_name"), "20XW\n").unwrap();
270        // Un serial NO debe entrar aunque exista: la huella agrupa, no identifica.
271        std::fs::write(sys.join("class/dmi/id/product_serial"), "PF3ABCDE\n").unwrap();
272
273        let dev = sys.join("bus/pci/devices/0000:00:14.3");
274        std::fs::create_dir_all(&dev).unwrap();
275        std::fs::write(dev.join("vendor"), "0x8086\n").unwrap();
276        std::fs::write(dev.join("device"), "0xa0f0\n").unwrap();
277        std::fs::write(dev.join("class"), "0x028000\n").unwrap();
278        let drv = sys.join("bus/pci/drivers/iwlwifi");
279        std::fs::create_dir_all(&drv).unwrap();
280        std::os::unix::fs::symlink(&drv, dev.join("driver")).unwrap();
281
282        let proc_dir = d.path().join("proc");
283        std::fs::create_dir_all(&proc_dir).unwrap();
284        std::fs::write(
285            proc_dir.join("cpuinfo"),
286            "model name\t: Intel(R) Core(TM) i7-1165G7\nflags\t\t: fpu vme avx2 aes\n",
287        )
288        .unwrap();
289        d
290    }
291
292    #[test]
293    fn lee_dmi_pci_cpu_y_driver_bindeado() {
294        let d = arbol_de_juguete();
295        let hw = Hardware::probe(&d.path().join("sys"), &d.path().join("proc"));
296        assert_eq!(hw.dmi.get("sys_vendor").map(|s| s.as_str()), Some("LENOVO"));
297        assert_eq!(hw.pci.len(), 1);
298        assert_eq!(hw.pci[0].class, "028000");
299        assert_eq!(hw.pci[0].driver.as_deref(), Some("iwlwifi"));
300        assert!(hw.has_pci_class("0280"));
301        assert!(hw.has_driver("iwlwifi"));
302        assert!(hw.has_cpu_flag("avx2"));
303        assert_eq!(hw.cpu_model.as_deref(), Some("Intel(R) Core(TM) i7-1165G7"));
304    }
305
306    #[test]
307    fn el_serial_no_entra_en_la_huella() {
308        let d = arbol_de_juguete();
309        let hw = Hardware::probe(&d.path().join("sys"), &d.path().join("proc"));
310        let m = hw.fingerprint_material();
311        assert!(!m.contains("PF3ABCDE"), "la huella agrupa máquinas, no las identifica:\n{m}");
312        assert!(m.contains("LENOVO"));
313    }
314
315    #[test]
316    fn mover_la_tarjeta_de_ranura_no_cambia_la_huella() {
317        let d = arbol_de_juguete();
318        let sys = d.path().join("sys");
319        let hw1 = Hardware::probe(&sys, &d.path().join("proc"));
320        // Mismo dispositivo, otra ranura.
321        let viejo = sys.join("bus/pci/devices/0000:00:14.3");
322        let nuevo = sys.join("bus/pci/devices/0000:03:00.0");
323        std::fs::rename(&viejo, &nuevo).unwrap();
324        let hw2 = Hardware::probe(&sys, &d.path().join("proc"));
325        assert_eq!(hw1.fingerprint(), hw2.fingerprint());
326    }
327
328    #[test]
329    fn el_usb_no_mueve_la_huella() {
330        let d = arbol_de_juguete();
331        let sys = d.path().join("sys");
332        let hw1 = Hardware::probe(&sys, &d.path().join("proc"));
333        let usb = sys.join("bus/usb/devices/1-1");
334        std::fs::create_dir_all(&usb).unwrap();
335        std::fs::write(usb.join("idVendor"), "0781\n").unwrap();
336        std::fs::write(usb.join("idProduct"), "5581\n").unwrap();
337        let hw2 = Hardware::probe(&sys, &d.path().join("proc"));
338        assert_eq!(hw2.usb.len(), 1, "se lee");
339        assert_eq!(hw1.fingerprint(), hw2.fingerprint(), "pero no se hashea");
340    }
341
342    #[test]
343    fn maquina_sin_dmi_deja_hueco_en_vez_de_fallar() {
344        let d = tempfile::tempdir().unwrap();
345        let hw = Hardware::probe(&d.path().join("sys"), &d.path().join("proc"));
346        assert!(!hw.gaps.is_empty());
347        // Y aun así da una huella: incompleta, pero declarada.
348        assert!(!hw.fingerprint().as_str().is_empty());
349    }
350}