//! Ponerle la **etiqueta** a un filesystem ext2/3/4 sin e2fsprogs. //! //! # Por qué existe //! //! El initramfs de pivote encuentra la raíz con `findfs LABEL=hammer-root`, así que una partición //! sin esa etiqueta tiene el sistema adentro y **nadie lo encuentra al arrancar**. Normalmente la //! pone `mke2fs -L` al formatear — pero `--adopt` no formatea, y en el initramfs del instalador no //! viaja ni `e2label` ni `tune2fs`: busybox no los trae y traerlos obligaría a rearmar el paquete //! contra el store, que es justo lo que no se puede hacer desde una máquina de desarrollo. //! //! El binario `takana` **sí** viaja ahí. Esto es lo mínimo que hace falta, hecho bien. //! //! # Lo que «bien» quiere decir acá //! //! La etiqueta son 16 bytes en el offset `0x78` del superbloque, y escribirlos es trivial. Lo que //! no es trivial —y lo que convierte un cambio inocente en un filesystem que el kernel monta de //! sólo lectura— es que un ext4 moderno trae `metadata_csum`: el superbloque lleva su propio //! crc32c en `0x3FC`, y tocarlo sin recalcularlo lo deja inválido. Por eso acá se mira el flag, y //! si está, se recalcula. use std::fs::OpenOptions; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::Path; const SB_OFFSET: u64 = 1024; const SB_LEN: usize = 1024; const MAGIC: u16 = 0xEF53; /// `RO_COMPAT_METADATA_CSUM`, el bit que dice que el superbloque lleva su propio crc32c. const RO_COMPAT_METADATA_CSUM: u32 = 0x0400; /// crc32c (Castagnoli), que es el que usa ext4. Sin tabla: 16 bytes de superbloque por instalación /// no justifican 1 KiB de tabla estática, y así no hay un segundo sitio donde equivocarse. fn crc32c(mut crc: u32, datos: &[u8]) -> u32 { const POLY: u32 = 0x82F6_3B78; // el reflejado de 0x1EDC6F41 for b in datos { crc ^= *b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ POLY } else { crc >> 1 }; } } crc } /// Le pone `etiqueta` al ext2/3/4 que empieza en `offset` dentro de `dispositivo`. /// /// Devuelve la etiqueta que había antes, que es lo que permite decir en el informe qué se /// reemplazó en vez de sólo que se escribió algo. pub fn poner_etiqueta( dispositivo: &Path, offset: u64, etiqueta: &str, ) -> std::io::Result { let bytes = etiqueta.as_bytes(); if bytes.len() > 16 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("la etiqueta no puede pasar de 16 bytes (pediste {})", bytes.len()), )); } let mut f = OpenOptions::new().read(true).write(true).open(dispositivo)?; let mut sb = [0u8; SB_LEN]; f.seek(SeekFrom::Start(offset + SB_OFFSET))?; f.read_exact(&mut sb)?; if u16::from_le_bytes([sb[0x38], sb[0x39]]) != MAGIC { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "ahí no hay un superbloque ext2/3/4 (la magia no casa)", )); } let antes = String::from_utf8_lossy( &sb[0x78..0x88].split(|b| *b == 0).next().unwrap_or(&[]), ) .to_string(); sb[0x78..0x88].fill(0); sb[0x78..0x78 + bytes.len()].copy_from_slice(bytes); // Y el checksum, si este filesystem lo lleva. Sin esto el kernel monta de sólo lectura y el // síntoma —un escritorio que no puede escribir nada— no menciona la etiqueta por ningún lado. let ro_compat = u32::from_le_bytes([sb[0x64], sb[0x65], sb[0x66], sb[0x67]]); if ro_compat & RO_COMPAT_METADATA_CSUM != 0 { // El checksum cubre el superbloque entero MENOS sus propios cuatro bytes finales. // // ⚠ **Y va SIN el complemento final.** La convención de «el crc32c de una cadena» lo lleva // —es lo que hace que `"123456789"` dé `0xE3069283`— pero ext4 guarda el valor CRUDO con // semilla `~0` (`ext4_superblock_csum`: `ext4_chksum(~0, es, offset)`, sin invertir). // Medido: con el complemento, `e2fsck` dice «Superblock checksum does not match» sobre un // filesystem perfecto, y el test de unidad no lo veía porque su superbloque sintético no // traía el flag. Por eso hay un banco contra un ext4 de verdad. let crc = crc32c(!0u32, &sb[..0x3FC]); sb[0x3FC..0x400].copy_from_slice(&crc.to_le_bytes()); } f.seek(SeekFrom::Start(offset + SB_OFFSET))?; f.write_all(&sb)?; f.sync_all()?; Ok(antes) } #[cfg(test)] mod tests { use super::*; /// El vector de prueba estándar de crc32c: `"123456789"` da `0xE3069283`. /// /// Está acá porque una implementación de crc con el polinomio al derecho en vez de reflejado /// compila, corre y devuelve un número distinto en cada byte — y el síntoma no sería «el crc /// está mal» sino «el filesystem se monta de sólo lectura», tres capas más allá. #[test] fn el_crc32c_es_el_de_castagnoli() { assert_eq!(!crc32c(!0u32, b"123456789"), 0xE306_9283); } /// Y la etiqueta se escribe donde la busca todo el mundo, sin tocar nada más. #[test] fn la_etiqueta_va_en_su_sitio_y_no_pisa_al_vecino() { let dir = std::env::temp_dir().join(format!("takana-label-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let p = dir.join("fs.img"); let mut sb = vec![0u8; 2048]; sb[1024 + 0x38] = 0x53; sb[1024 + 0x39] = 0xEF; sb[1024 + 0x78..1024 + 0x80].copy_from_slice(b"anterior"); // Un vecino justo después de la etiqueta: si se escribieran 17 bytes, se nota. sb[1024 + 0x88] = 0xAB; std::fs::write(&p, &sb).unwrap(); let antes = poner_etiqueta(&p, 0, "hammer-root").unwrap(); assert_eq!(antes, "anterior"); let leido = std::fs::read(&p).unwrap(); assert_eq!(&leido[1024 + 0x78..1024 + 0x83], b"hammer-root"); assert_eq!(leido[1024 + 0x83], 0, "lo que sobra queda en cero"); assert_eq!(leido[1024 + 0x88], 0xAB, "no se pisó al vecino"); // Sin el flag de metadata_csum, el checksum NO se toca: escribir uno donde no lo hay es // tan dañino como no escribirlo donde sí. assert_eq!(&leido[1024 + 0x3FC..1024 + 0x400], &[0, 0, 0, 0]); let _ = std::fs::remove_dir_all(&dir); } /// Y donde no hay superbloque, no se escribe nada. #[test] fn sin_superbloque_no_escribe() { let dir = std::env::temp_dir().join(format!("takana-label-no-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let p = dir.join("basura.img"); std::fs::write(&p, vec![0u8; 2048]).unwrap(); assert!(poner_etiqueta(&p, 0, "hammer-root").is_err()); assert_eq!(std::fs::read(&p).unwrap(), vec![0u8; 2048]); let _ = std::fs::remove_dir_all(&dir); } }