tawasuyu

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 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ main crates/takana-cli/src/fs_label.rs Rust · 150 líneas · 6.7 KB Historia Quién tocó qué crudo

feat(instalador): la etiqueta sin e2fsprogs, que es lo que le faltaba a adoptar para arrancar f1e662831 · sergio ·

1//! Ponerle la **etiqueta** a un filesystem ext2/3/4 sin e2fsprogs.
2//!
3//! # Por qué existe
4//!
5//! El initramfs de pivote encuentra la raíz con `findfs LABEL=hammer-root`, así que una partición
6//! sin esa etiqueta tiene el sistema adentro y **nadie lo encuentra al arrancar**. Normalmente la
7//! pone `mke2fs -L` al formatear — pero `--adopt` no formatea, y en el initramfs del instalador no
8//! viaja ni `e2label` ni `tune2fs`: busybox no los trae y traerlos obligaría a rearmar el paquete
9//! contra el store, que es justo lo que no se puede hacer desde una máquina de desarrollo.
10//!
11//! El binario `takana` **sí** viaja ahí. Esto es lo mínimo que hace falta, hecho bien.
12//!
13//! # Lo que «bien» quiere decir acá
14//!
15//! La etiqueta son 16 bytes en el offset `0x78` del superbloque, y escribirlos es trivial. Lo que
16//! no es trivial —y lo que convierte un cambio inocente en un filesystem que el kernel monta de
17//! sólo lectura— es que un ext4 moderno trae `metadata_csum`: el superbloque lleva su propio
18//! crc32c en `0x3FC`, y tocarlo sin recalcularlo lo deja inválido. Por eso acá se mira el flag, y
19//! si está, se recalcula.
20
21use std::fs::OpenOptions;
22use std::io::{Read, Seek, SeekFrom, Write};
23use std::path::Path;
24
25const SB_OFFSET: u64 = 1024;
26const SB_LEN: usize = 1024;
27const MAGIC: u16 = 0xEF53;
28/// `RO_COMPAT_METADATA_CSUM`, el bit que dice que el superbloque lleva su propio crc32c.
29const RO_COMPAT_METADATA_CSUM: u32 = 0x0400;
30
31/// crc32c (Castagnoli), que es el que usa ext4. Sin tabla: 16 bytes de superbloque por instalación
32/// no justifican 1 KiB de tabla estática, y así no hay un segundo sitio donde equivocarse.
33fn crc32c(mut crc: u32, datos: &[u8]) -> u32 {
34    const POLY: u32 = 0x82F6_3B78; // el reflejado de 0x1EDC6F41
35    for b in datos {
36        crc ^= *b as u32;
37        for _ in 0..8 {
38            crc = if crc & 1 != 0 { (crc >> 1) ^ POLY } else { crc >> 1 };
39        }
40    }
41    crc
42}
43
44/// Le pone `etiqueta` al ext2/3/4 que empieza en `offset` dentro de `dispositivo`.
45///
46/// Devuelve la etiqueta que había antes, que es lo que permite decir en el informe qué se
47/// reemplazó en vez de sólo que se escribió algo.
48pub fn poner_etiqueta(
49    dispositivo: &Path,
50    offset: u64,
51    etiqueta: &str,
52) -> std::io::Result<String> {
53    let bytes = etiqueta.as_bytes();
54    if bytes.len() > 16 {
55        return Err(std::io::Error::new(
56            std::io::ErrorKind::InvalidInput,
57            format!("la etiqueta no puede pasar de 16 bytes (pediste {})", bytes.len()),
58        ));
59    }
60    let mut f = OpenOptions::new().read(true).write(true).open(dispositivo)?;
61    let mut sb = [0u8; SB_LEN];
62    f.seek(SeekFrom::Start(offset + SB_OFFSET))?;
63    f.read_exact(&mut sb)?;
64    if u16::from_le_bytes([sb[0x38], sb[0x39]]) != MAGIC {
65        return Err(std::io::Error::new(
66            std::io::ErrorKind::InvalidData,
67            "ahí no hay un superbloque ext2/3/4 (la magia no casa)",
68        ));
69    }
70    let antes = String::from_utf8_lossy(
71        &sb[0x78..0x88].split(|b| *b == 0).next().unwrap_or(&[]),
72    )
73    .to_string();
74
75    sb[0x78..0x88].fill(0);
76    sb[0x78..0x78 + bytes.len()].copy_from_slice(bytes);
77
78    // Y el checksum, si este filesystem lo lleva. Sin esto el kernel monta de sólo lectura y el
79    // síntoma —un escritorio que no puede escribir nada— no menciona la etiqueta por ningún lado.
80    let ro_compat = u32::from_le_bytes([sb[0x64], sb[0x65], sb[0x66], sb[0x67]]);
81    if ro_compat & RO_COMPAT_METADATA_CSUM != 0 {
82        // El checksum cubre el superbloque entero MENOS sus propios cuatro bytes finales.
83        //
84        // ⚠ **Y va SIN el complemento final.** La convención de «el crc32c de una cadena» lo lleva
85        // —es lo que hace que `"123456789"` dé `0xE3069283`— pero ext4 guarda el valor CRUDO con
86        // semilla `~0` (`ext4_superblock_csum`: `ext4_chksum(~0, es, offset)`, sin invertir).
87        // Medido: con el complemento, `e2fsck` dice «Superblock checksum does not match» sobre un
88        // filesystem perfecto, y el test de unidad no lo veía porque su superbloque sintético no
89        // traía el flag. Por eso hay un banco contra un ext4 de verdad.
90        let crc = crc32c(!0u32, &sb[..0x3FC]);
91        sb[0x3FC..0x400].copy_from_slice(&crc.to_le_bytes());
92    }
93    f.seek(SeekFrom::Start(offset + SB_OFFSET))?;
94    f.write_all(&sb)?;
95    f.sync_all()?;
96    Ok(antes)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    /// El vector de prueba estándar de crc32c: `"123456789"` da `0xE3069283`.
104    ///
105    /// Está acá porque una implementación de crc con el polinomio al derecho en vez de reflejado
106    /// compila, corre y devuelve un número distinto en cada byte — y el síntoma no sería «el crc
107    /// está mal» sino «el filesystem se monta de sólo lectura», tres capas más allá.
108    #[test]
109    fn el_crc32c_es_el_de_castagnoli() {
110        assert_eq!(!crc32c(!0u32, b"123456789"), 0xE306_9283);
111    }
112
113    /// Y la etiqueta se escribe donde la busca todo el mundo, sin tocar nada más.
114    #[test]
115    fn la_etiqueta_va_en_su_sitio_y_no_pisa_al_vecino() {
116        let dir = std::env::temp_dir().join(format!("takana-label-{}", std::process::id()));
117        std::fs::create_dir_all(&dir).unwrap();
118        let p = dir.join("fs.img");
119        let mut sb = vec![0u8; 2048];
120        sb[1024 + 0x38] = 0x53;
121        sb[1024 + 0x39] = 0xEF;
122        sb[1024 + 0x78..1024 + 0x80].copy_from_slice(b"anterior");
123        // Un vecino justo después de la etiqueta: si se escribieran 17 bytes, se nota.
124        sb[1024 + 0x88] = 0xAB;
125        std::fs::write(&p, &sb).unwrap();
126
127        let antes = poner_etiqueta(&p, 0, "hammer-root").unwrap();
128        assert_eq!(antes, "anterior");
129        let leido = std::fs::read(&p).unwrap();
130        assert_eq!(&leido[1024 + 0x78..1024 + 0x83], b"hammer-root");
131        assert_eq!(leido[1024 + 0x83], 0, "lo que sobra queda en cero");
132        assert_eq!(leido[1024 + 0x88], 0xAB, "no se pisó al vecino");
133        // Sin el flag de metadata_csum, el checksum NO se toca: escribir uno donde no lo hay es
134        // tan dañino como no escribirlo donde sí.
135        assert_eq!(&leido[1024 + 0x3FC..1024 + 0x400], &[0, 0, 0, 0]);
136        let _ = std::fs::remove_dir_all(&dir);
137    }
138
139    /// Y donde no hay superbloque, no se escribe nada.
140    #[test]
141    fn sin_superbloque_no_escribe() {
142        let dir = std::env::temp_dir().join(format!("takana-label-no-{}", std::process::id()));
143        std::fs::create_dir_all(&dir).unwrap();
144        let p = dir.join("basura.img");
145        std::fs::write(&p, vec![0u8; 2048]).unwrap();
146        assert!(poner_etiqueta(&p, 0, "hammer-root").is_err());
147        assert_eq!(std::fs::read(&p).unwrap(), vec![0u8; 2048]);
148        let _ = std::fs::remove_dir_all(&dir);
149    }
150}