1. De una firma reconocible a una lectura verificable
El tema 37 introdujo un resumen de la familia ext. Ahora profundizaremos en la representación del superbloque y en cómo escribir un lector revisable: cada acceso debe corresponder a un campo documentado y cada cálculo debe tener una justificación de rango.
Decodificar: convertir bytes de una representación externa en valores de la aplicación. Validar: comprobar las relaciones que exige el subconjunto interpretado. Verificar un checksum: contrastar una función de integridad con el valor almacenado. Son comprobaciones complementarias.
Encontrar 0xEF53 no acredita que el volumen esté sano ni distingue por sí solo ext2, ext3 y ext4. Un encabezado también puede tener campos numéricamente razonables y un checksum incorrecto. Tampoco un CRC correcto revisa todas las tablas, directorios y datos del volumen.
2. Posición de volumen, superbloque y bloque
El superbloque primario comienza a 1024 bytes del inicio del sistema de archivos y ocupa 1024 bytes. Si el volumen empieza en el desplazamiento V de un archivo contenedor, la lectura comienza en V + 1024.
Con bloques de 1 KiB, el superbloque ocupa el bloque 1. Con bloques de 4 KiB, está dentro del bloque 0, a 1024 bytes de su comienzo. «Leer siempre el bloque 1» sería incorrecto en el segundo caso. Véase Block Groups.
El programa admite un desplazamiento decimal opcional. No lo obtiene de MBR o GPT ni busca firmas por toda la imagen. El laboratorio crea un prefijo artificial conocido: no lo presenta como una tabla de particiones real.
3. Contrato del lector y subconjunto admitido
- Solo abre archivos regulares en lectura y rechaza el enlace simbólico final.
- La imagen permanece estable y desmontada durante las consultas.
- Interpreta revisión dinámica 1 y geometría ordinaria sin bigalloc.
- Admite las banderas incompatibles declaradas en el código; otras producen un estado de no soportado.
- No recorre descriptores, bitmaps, inodos ni el journal.
- No repara, monta ni escribe imágenes.
Un valor no soportado no demuestra corrupción. Por ejemplo, un volumen válido con bigalloc queda fuera del contrato de este cálculo de bitmaps. El programa distingue ese resultado de un encabezado incoherente o una imagen truncada.
O_NOFOLLOW se aplica al componente final, no a todos los directorios de una ruta. El ensayo usa rutas propias y confiables. Un descriptor abierto conserva una referencia, pero no congela los bytes si otro proceso modifica el archivo.
4. Una estructura que representa bytes, no enteros nativos
Copiar el disco directamente a una estructura con miembros uint32_t no resuelve endianess, alineación ni relleno. Una directiva packed puede modificar el relleno del compilador, pero no convierte little-endian y puede introducir accesos desalineados.
struct sb_disco:
arreglos de unsigned char con tamaños y posiciones documentados
→ representa 1024 bytes del formato
struct sb_valores:
enteros nativos ya decodificados y validados
→ representa las cantidades que usa el programaEl ejemplo utiliza arreglos de bytes y comprueba sizeof y offsetof con _Static_assert. No necesita extensiones packed. Las aserciones hacen fallar la compilación si la estructura no tiene las posiciones exigidas.
Los intervalos que no interpretamos se conservan como arreglos de relleno explícitos. Leerlos no equivale a implementar todos sus campos. La referencia de las posiciones es Super Block.
5. Decodificar little-endian y la firma 0xEF53
Bytes del disco: 53 EF
Valor little-endian: 0x53 + (0xEF << 8) = 0xEF53
Bytes del disco: EF 53
Valor little-endian: 0xEF + (0x53 << 8) = 0x53EFLos desplazamientos se aplican después de convertir a un tipo sin signo suficientemente ancho. Así se evita que la promoción a int y un desplazamiento alto produzcan resultados que no representan el entero deseado.
No se obtiene un uint32_t desreferenciando un puntero convertido desde una dirección arbitraria del buffer. Las funciones le16 y le32 leen bytes independientes y funcionan sin depender del orden nativo de la máquina.
6. Completar una lectura con pread
pread solicita bytes desde una posición sin modificar la posición de la descripción abierta. Puede devolver una lectura corta; un retorno positivo no significa que se hayan entregado los 1024 bytes pedidos. Véase pread(2).
usados = 0
mientras usados < 1024:
n = pread(fd, buffer + usados, 1024 - usados, inicio + usados)
n > 0: avanzar usados
n == 0: informar fin prematuro
n == -1 y EINTR: reintentar
otro error: detener la interpretaciónAntes de iniciar se comprueba el tamaño con fstat. La forma segura del límite es V ≤ tamaño y 2048 ≤ tamaño − V; evita desbordar V + 2048 al validar. El descriptor usa offsets de al menos 64 bits en este ejemplo.
Un cambio concurrente puede invalidar lo observado por fstat: por eso también se maneja EOF durante el bucle. La comprobación previa y el tratamiento del retorno son necesarios por razones diferentes.
7. Validar geometría sin desbordar cálculos
El tamaño de bloque se obtiene como 1024 << s_log_block_size. Primero se acota el exponente; desplazar por un valor arbitrario del disco sería inseguro. El subconjunto admite exponentes de 0 a 6, sin prometer que el sistema pueda montar todos esos tamaños.
bloques_del_volumen ≤ bytes_disponibles / bytes_por_bloque
libres ≤ total
reservados ≤ total
0 < bloques_por_grupo ≤ 8 × bytes_por_bloque (sin bigalloc)
0 < inodos_por_grupo ≤ 8 × bytes_por_bloque
tamaño_inodo: potencia de dos, desde 128 y hasta el bloque
grupos = (bloques - primer_bloque - 1) / bloques_por_grupo + 1La división evita formar primero un producto que podría desbordar. Una vez demostrado que el volumen cabe en el archivo, calcular sus bytes es seguro dentro del tipo usado. La fórmula de grupos evita sumar bloques_por_grupo − 1 al numerador.
Los bloques reservados forman parte de los bloques del volumen; no se suman como otra región externa. Estos contadores tampoco equivalen sin más a f_bavail observado desde un montaje, que responde otra pregunta.
8. Revisión y características que cambian la interpretación
Las categorías compat, incompat y ro_compat tienen contratos de compatibilidad específicos. Un lector didáctico que conoce algunos campos no debe presentarse como un controlador que puede montar el volumen.
El ejemplo admite FILETYPE, RECOVER, EXTENTS, 64BIT, FLEX_BG y CSUM_SEED en la máscara incompat. Puede resumir sus campos escalares sin localizar las estructuras externas. Si aparece otra bandera incompatible, devuelve 3 y explica la limitación.
En ro_compat se comprueba expresamente bigalloc, que se rechaza para esta geometría. Otras máscaras se muestran sin interpretarlas como permiso para recorrer toda estructura. metadata_csum indica que el segundo programa debe comprobar el checksum cuando su algoritmo esté soportado.
RECOVER indica recuperación pendiente. El resumen puede mostrar campos, pero sus contadores no se convierten por ello en un diagnóstico de un estado recuperado. No se reproduce el journal ni se modifica el estado en este tema.
9. Contadores bajos, altos y unidades
blocks = blocks_lo | ((uint64_t)blocks_hi << 32)
free = free_lo | ((uint64_t)free_hi << 32)
rsvd = rsvd_lo | ((uint64_t)rsvd_hi << 32)
Ejemplo: hi=1, lo=0xffffffff
contador = 8589934591, no 4294967295
Sin la característica 64bit:
no se usan los campos altos para formar estos contadores.La conversión a uint64_t debe ocurrir antes del desplazamiento. Desplazar un entero de 32 bits por 32 no es una forma válida de construir la parte alta.
La salida usa macros PRIu64 de inttypes.h. No se supone que uint64_t sea siempre unsigned long ni se imprime con un formato que corresponda a otro tipo.
10. UUID, etiqueta y campos de longitud fija
El UUID ocupa 16 bytes. El ejemplo los imprime en hexadecimal con separadores y sin reinterpretarlos como enteros nativos. Esto permite contrastarlo con el identificador que muestra dumpe2fs.
La etiqueta también tiene un ancho fijo y puede no terminar en cero. Pasarla directamente a printf con %s podría leer más allá del campo. El lector imprime LABEL_HEX con exactamente sus 16 bytes, evitando además ejecutar caracteres de control que pudieran aparecer en una imagen ajena.
Representar bytes de forma segura no resuelve por sí solo su codificación como texto. Una herramienta que deba mostrar nombres legibles necesita decidir y validar la conversión correspondiente.
11. Qué comprueba el CRC32C del superbloque
Con metadata_csum, el superbloque almacena un checksum al final. El programa de comprobación calcula CRC32C sobre los bytes anteriores a ese campo, incluidos los bytes del UUID. Usa la convención de ext4: semilla 0xffffffff y sin XOR final adicional.
CRC32C no es el mismo polinomio que el CRC32 habitual de otras bibliotecas. Usar zlib.crc32 sin comprobar el algoritmo produciría otra función. La implementación bit a bit del ejemplo usa el polinomio reflejado 0x82F63B78.
El checksum del superbloque no utiliza de forma idéntica todos los ingredientes de los checksums de otras estructuras; no se generaliza su fórmula a inodos o bitmaps. Véase Checksums.
Una coincidencia detecta determinadas alteraciones bajo este mecanismo, pero no autentica al autor de una imagen ni revisa todos sus objetos. Si metadata_csum no está habilitado o el tipo de algoritmo no se reconoce, se informa «no aplicable o no soportado», sin fabricar un éxito.
12. Programa en C: estructura de disco y resumen validado
Guarda leer-super-ext4.c. Recibe imagen y, opcionalmente, desplazamiento decimal en bytes del volumen. Devuelve 0 al completar su resumen parcial, 1 ante error de lectura o incoherencia, 2 por uso incorrecto y 3 para características fuera del subconjunto. No comprueba CRC: ese paso se realiza con el programa siguiente.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
#include <stdint.h>
#include <inttypes.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
struct sb_disco {
unsigned char inodos[4], bloques_lo[4], reservados_lo[4], libres_lo[4];
unsigned char inodos_libres[4], primero[4], log_bloque[4], log_cluster[4];
unsigned char bpg[4], cpg[4], ipg[4];
unsigned char hasta_magic[0x38 - 0x2c];
unsigned char magic[2], estado[2];
unsigned char hasta_revision[0x4c - 0x3c];
unsigned char revision[4];
unsigned char hasta_inode_size[0x58 - 0x50];
unsigned char inode_size[2], group_nr[2];
unsigned char compat[4], incompat[4], ro_compat[4];
unsigned char uuid[16], etiqueta[16];
unsigned char hasta_desc[0xfe - 0x88];
unsigned char desc_size[2];
unsigned char hasta_altos[0x150 - 0x100];
unsigned char bloques_hi[4], reservados_hi[4], libres_hi[4];
unsigned char hasta_tipo_crc[0x175 - 0x15c];
unsigned char tipo_crc[1];
unsigned char hasta_crc[0x3fc - 0x176];
unsigned char crc[4];
};
struct sb_valores {
uint64_t bloque, bloques, reservados, libres, grupos;
uint32_t inodos, primero, bpg, ipg, inode_size, desc_size;
uint32_t compat, incompat, ro;
};
_Static_assert(CHAR_BIT == 8, "Se requieren bytes de ocho bits");
_Static_assert(sizeof(off_t) >= 8, "Se requieren offsets de al menos 64 bits");
_Static_assert(sizeof(struct sb_disco) == 1024, "Tamano de superbloque incorrecto");
_Static_assert(offsetof(struct sb_disco, magic) == 0x38, "Offset magic incorrecto");
_Static_assert(offsetof(struct sb_disco, uuid) == 0x68, "Offset UUID incorrecto");
_Static_assert(offsetof(struct sb_disco, desc_size) == 0xfe, "Offset descriptor incorrecto");
_Static_assert(offsetof(struct sb_disco, bloques_hi) == 0x150, "Offset alto incorrecto");
_Static_assert(offsetof(struct sb_disco, crc) == 0x3fc, "Offset CRC incorrecto");
static uint16_t le16(const unsigned char *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t le32(const unsigned char *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static int decimal(const char *s, uint64_t *n)
{
if (!*s) return -1;
for (const char *p = s; *p; p++) if (*p < '0' || *p > '9') return -1;
errno = 0;
uintmax_t v = strtoumax(s, NULL, 10);
if (errno || v > INT64_MAX) return -1;
*n = (uint64_t)v;
return 0;
}
static int leer(int fd, struct sb_disco *s, uint64_t inicio)
{
unsigned char *p = (unsigned char *)s;
size_t usados = 0;
while (usados < sizeof *s) {
ssize_t n = pread(fd, p + usados, sizeof *s - usados, (off_t)(inicio + usados));
if (n > 0) usados += (size_t)n;
else if (n == -1 && errno == EINTR) continue;
else { if (n == -1) perror("pread"); return -1; }
}
return 0;
}
static uint64_t contador(const unsigned char *lo, const unsigned char *hi, int alto)
{
uint64_t n = le32(lo);
return alto ? n | ((uint64_t)le32(hi) << 32) : n;
}
static int decodificar(const struct sb_disco *s, uint64_t disponibles, struct sb_valores *v)
{
if (le16(s->magic) != 0xef53) { fprintf(stderr, "Firma incorrecta.\n"); return 1; }
if (le32(s->revision) != 1) { fprintf(stderr, "Revision no soportada.\n"); return 3; }
v->compat = le32(s->compat); v->incompat = le32(s->incompat); v->ro = le32(s->ro_compat);
const uint32_t admitidas = UINT32_C(0x000022c6);
if ((v->incompat & ~admitidas) || (v->ro & 0x200)) {
fprintf(stderr, "Caracteristica incompatible no soportada o bigalloc.\n"); return 3;
}
uint32_t log = le32(s->log_bloque);
if (log > 6) { fprintf(stderr, "Exponente de bloque fuera de rango.\n"); return 1; }
v->bloque = UINT64_C(1024) << log;
int alto = (v->incompat & 0x80) != 0;
v->bloques = contador(s->bloques_lo, s->bloques_hi, alto);
v->reservados = contador(s->reservados_lo, s->reservados_hi, alto);
v->libres = contador(s->libres_lo, s->libres_hi, alto);
v->primero = le32(s->primero); v->bpg = le32(s->bpg); v->ipg = le32(s->ipg);
v->inodos = le32(s->inodos); v->inode_size = le16(s->inode_size);
v->desc_size = alto ? le16(s->desc_size) : 32;
if (v->primero != (v->bloque == 1024 ? 1u : 0u) ||
v->bloques <= v->primero || v->bloques > disponibles / v->bloque ||
v->libres > v->bloques || v->reservados > v->bloques ||
!v->bpg || v->bpg > 8 * v->bloque || le32(s->cpg) != v->bpg ||
le32(s->log_cluster) != log || !v->ipg || v->ipg > 8 * v->bloque ||
!v->inodos || le32(s->inodos_libres) > v->inodos ||
v->inode_size < 128 || v->inode_size > v->bloque ||
(v->inode_size & (v->inode_size - 1)) || v->desc_size < (alto ? 64u : 32u) ||
v->desc_size > v->bloque || (v->desc_size & (v->desc_size - 1))) {
fprintf(stderr, "Geometria incoherente o volumen fuera de la imagen.\n"); return 1;
}
v->grupos = (v->bloques - v->primero - 1) / v->bpg + 1;
if (((uint64_t)v->inodos - 1) / v->ipg + 1 > v->grupos) {
fprintf(stderr, "Cantidad de inodos incompatible con los grupos.\n"); return 1;
}
return 0;
}
static void mostrar(const struct sb_disco *s, const struct sb_valores *v, uint64_t base)
{
printf("VOLUME_OFFSET=%" PRIu64 "\nSUPER_OFFSET=%" PRIu64 "\n", base, base + 1024);
printf("MAGIC=0x%04x\nBLOCK_SIZE=%" PRIu64 "\n", (unsigned)le16(s->magic), v->bloque);
printf("BLOCKS=%" PRIu64 "\nFREE_BLOCKS=%" PRIu64 "\nRESERVED_BLOCKS=%" PRIu64 "\n",
v->bloques, v->libres, v->reservados);
printf("VOLUME_BYTES=%" PRIu64 "\nGROUPS=%" PRIu64 "\n", v->bloques * v->bloque, v->grupos);
printf("BLOCKS_PER_GROUP=%" PRIu32 "\nINODES=%" PRIu32 "\nINODES_PER_GROUP=%" PRIu32
"\nINODE_SIZE=%" PRIu32 "\nDESC_SIZE=%" PRIu32 "\n",
v->bpg, v->inodos, v->ipg, v->inode_size, v->desc_size);
printf("COMPAT=%08" PRIx32 "\nINCOMPAT=%08" PRIx32 "\nRO_COMPAT=%08" PRIx32 "\n",
v->compat, v->incompat, v->ro);
printf("NEEDS_RECOVERY=%s\n", (v->incompat & 4) ? "si" : "no");
printf("UUID=");
for (unsigned i = 0; i < 16; i++) {
if (i == 4 || i == 6 || i == 8 || i == 10) putchar('-');
printf("%02x", (unsigned)s->uuid[i]);
}
printf("\nLABEL_HEX=");
for (unsigned i = 0; i < 16; i++) printf("%02x", (unsigned)s->etiqueta[i]);
printf("\nCHECKSUM_TYPE=%u\nCHECKSUM_STORED=%08" PRIx32 "\n",
(unsigned)s->tipo_crc[0], le32(s->crc));
puts("Resumen parcial de la familia ext; CRC y estructuras externas no verificados.");
}
int main(int argc, char **argv)
{
uint64_t base = 0;
if (argc < 2 || argc > 3 || (argc == 3 && decimal(argv[2], &base))) {
fprintf(stderr, "Uso: %s imagen [offset-decimal-en-bytes]\n", argv[0]); return 2;
}
int fd = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd == -1) { perror("open"); return 1; }
struct stat st;
struct sb_disco s;
struct sb_valores v;
int estado = 1;
if (fstat(fd, &st) == -1) perror("fstat");
else if (!S_ISREG(st.st_mode) || st.st_size < 0 || base > (uint64_t)st.st_size ||
(uint64_t)st.st_size - base < 2048)
fprintf(stderr, "Imagen regular insuficiente para el desplazamiento.\n");
else if (leer(fd, &s, base + 1024) == -1) fprintf(stderr, "Superbloque incompleto.\n");
else {
estado = decodificar(&s, (uint64_t)st.st_size - base, &v);
if (!estado) mostrar(&s, &v, base);
}
if (close(fd) == -1) { perror("close"); estado = 1; }
return estado;
}La comparación con el tamaño real impide imprimir como válido un volumen cuyos bloques declarados no caben en la imagen. Aun con estado 0, el resultado es un resumen del subconjunto, no un permiso para montar ni una declaración de integridad global.
13. Programa en C: comprobar un superbloque extraído
Guarda crc-super.c. Recibe exactamente los 1024 bytes extraídos del superbloque. Devuelve 0 si coincide CRC32C, 1 por error o discrepancia, 2 por uso incorrecto y 3 si no hay metadata_csum o el tipo no está soportado. No acepta una imagen completa como si fuera el superbloque.
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <errno.h>
static uint32_t le32(const unsigned char *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static uint32_t crc32c(const unsigned char *p, size_t n)
{
uint32_t crc = UINT32_MAX;
for (size_t i = 0; i < n; i++) {
crc ^= p[i];
for (unsigned bit = 0; bit < 8; bit++)
crc = (crc >> 1) ^ ((0u - (crc & 1u)) & UINT32_C(0x82f63b78));
}
return crc;
}
static int comprobar(const unsigned char *s)
{
if (s[0x38] != 0x53 || s[0x39] != 0xef) {
fprintf(stderr, "Firma incorrecta.\n"); return 1;
}
if (!(le32(s + 0x64) & 0x400) || s[0x175] != 1) {
fprintf(stderr, "metadata_csum no habilitado o tipo no soportado.\n"); return 3;
}
uint32_t guardado = le32(s + 0x3fc), calculado = crc32c(s, 0x3fc);
printf("STORED=%08" PRIx32 "\nCOMPUTED=%08" PRIx32 "\n", guardado, calculado);
if (guardado != calculado) { fprintf(stderr, "Checksum diferente.\n"); return 1; }
puts("CHECKSUM=coincide");
return 0;
}
int main(int argc, char **argv)
{
if (argc != 2) { fprintf(stderr, "Uso: %s superbloque-1024.bin\n", argv[0]); return 2; }
int fd = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd == -1) { perror("open"); return 1; }
struct stat st;
unsigned char s[1024];
int estado = 1;
if (fstat(fd, &st) == -1) perror("fstat");
else if (!S_ISREG(st.st_mode) || st.st_size != 1024)
fprintf(stderr, "Se requiere un archivo regular de exactamente 1024 bytes.\n");
else {
size_t usados = 0;
while (usados < sizeof s) {
ssize_t n = pread(fd, s + usados, sizeof s - usados, (off_t)usados);
if (n > 0) usados += (size_t)n;
else if (n == -1 && errno == EINTR) continue;
else { if (n == -1) perror("pread"); break; }
}
if (usados == sizeof s) estado = comprobar(s);
else fprintf(stderr, "Lectura incompleta.\n");
}
if (close(fd) == -1) { perror("close"); estado = 1; }
return estado;
}El intervalo calculado tiene 1020 bytes: excluye los cuatro bytes del checksum almacenado, no todo el último bloque físico. CSUM_SEED no reemplaza la semilla de este cálculo particular del superbloque. El laboratorio lo contrasta con volúmenes creados por e2fsprogs.
14. Laboratorio con dos geometrías y un desplazamiento
Usa una sesión Bash en Linux con compilador C11, Python 3 y e2fsprogs. No se requiere sudo ni montar imágenes. Detén la secuencia ante cualquier fallo y trabaja con los nombres nuevos del directorio exclusivo.
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 leer-super-ext4.c -o leer-super-ext4
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 crc-super.c -o crc-super
$ BIN40="$PWD"
$ LAB40=$(mktemp -d "$HOME/super40.XXXXXX")
$ cd -- "$LAB40"
$ truncate -s 128M ext4-4k.img
$ truncate -s 64M ext4-1k.img
$ mkfs.ext4 -b 4096 -I 256 -m 0 -L CURSO40 -O metadata_csum,64bit -E lazy_itable_init=0,lazy_journal_init=0 ext4-4k.img
$ mkfs.ext4 -b 1024 -I 256 -m 0 -L CURSO40 -O metadata_csum,^64bit -E lazy_itable_init=0,lazy_journal_init=0 ext4-1k.img
$ "$BIN40/leer-super-ext4" ext4-4k.img
$ "$BIN40/leer-super-ext4" ext4-1k.img
$ dumpe2fs -h ext4-4k.img
$ dumpe2fs -h ext4-1k.imgEl primer volumen tiene 32768 bloques de 4096 bytes; el segundo, 65536 bloques de 1024. En ambos casos el superbloque empieza en el byte 1024. Se desactiva la inicialización diferida de metadatos para facilitar una inspección estable, sin modificar las políticas de un volumen del equipo.
Extraer el superbloque y preparar un contenedor con prefijo
$ python3 - <<'PY'
from pathlib import Path
import shutil
for nombre in ('ext4-4k', 'ext4-1k'):
with Path(nombre + '.img').open('rb') as src:
src.seek(1024)
datos = src.read(1024)
if len(datos) != 1024:
raise SystemExit('Superbloque incompleto')
with Path(nombre + '-super.bin').open('xb') as dst:
dst.write(datos)
with Path('contenedor.img').open('xb') as dst:
dst.write(b'\x00' * 4096)
with Path('ext4-4k.img').open('rb') as src:
shutil.copyfileobj(src, dst)
print('Superbloques extraidos; volumen del contenedor en byte 4096')
PY
$ "$BIN40/crc-super" ext4-4k-super.bin
$ "$BIN40/crc-super" ext4-1k-super.bin
$ "$BIN40/leer-super-ext4" contenedor.img 4096El lector del contenedor debe informar VOLUME_OFFSET=4096 y SUPER_OFFSET=5120. UUID, contadores y tamaño de bloque deben coincidir con ext4-4k.img. Leer el contenedor con base cero no encuentra el volumen en la posición esperada.
dumpe2fs se aplica a las imágenes de volumen, sin el prefijo artificial. Su información puede ser antigua o inconsistente sobre sistemas activos: consulta dumpe2fs(8). Este laboratorio conserva los archivos desmontados durante todas las consultas.
15. Verificador reproducible y casos negativos
El siguiente programa compara ambos resúmenes con dumpe2fs, verifica los CRC, prueba el desplazamiento del contenedor y altera solo una copia extraída para comprobar la detección de un cambio. También rechaza una base inválida y una entrada demasiado corta.
$ python3 - "$BIN40/leer-super-ext4" "$BIN40/crc-super" <<'PY'
from pathlib import Path
import hashlib
import os
import re
import subprocess
import sys
lector, crc = (str(Path(s).resolve()) for s in sys.argv[1:])
env = dict(os.environ, LC_ALL='C')
def ejecutar(args, esperado=0):
r = subprocess.run(args, capture_output=True, text=True, env=env)
if r.returncode != esperado:
raise SystemExit(f'Estado {r.returncode}, esperado {esperado}: {r.stderr}')
return r.stdout
def resumen(args):
return dict(re.findall(r'^([A-Z_]+)=([^\n]+)$', ejecutar(args), re.M))
def sha(ruta):
h = hashlib.sha256()
with ruta.open('rb') as f:
for b in iter(lambda: f.read(1024 * 1024), b''):
h.update(b)
return h.digest()
archivos = [Path(n) for n in ('ext4-4k.img', 'ext4-1k.img', 'contenedor.img')]
antes = {p: sha(p) for p in archivos}
for nombre in ('ext4-4k', 'ext4-1k'):
campos = resumen([lector, nombre + '.img'])
externo = ejecutar(['dumpe2fs', '-h', nombre + '.img'])
for clave, etiqueta in [('BLOCK_SIZE', 'Block size'), ('BLOCKS', 'Block count'),
('FREE_BLOCKS', 'Free blocks'), ('RESERVED_BLOCKS', 'Reserved block count'),
('INODES', 'Inode count'), ('INODE_SIZE', 'Inode size')]:
m = re.search(r'^' + re.escape(etiqueta) + r':\s+(\d+)', externo, re.M)
if not m or campos[clave] != m.group(1):
raise SystemExit('Diferencia en ' + nombre + ': ' + clave)
m = re.search(r'^Filesystem UUID:\s+(\S+)', externo, re.M)
if not m or campos['UUID'] != m.group(1):
raise SystemExit('UUID diferente')
ejecutar([crc, nombre + '-super.bin'])
print('OK resumen y CRC:', nombre)
original = resumen([lector, 'ext4-4k.img'])
con_prefijo = resumen([lector, 'contenedor.img', '4096'])
for clave in ('UUID', 'BLOCK_SIZE', 'BLOCKS', 'FREE_BLOCKS', 'INODES'):
if original[clave] != con_prefijo[clave]:
raise SystemExit('Desplazamiento mal interpretado')
if con_prefijo['SUPER_OFFSET'] != '5120':
raise SystemExit('Posicion absoluta inesperada')
alterado = bytearray(Path('ext4-4k-super.bin').read_bytes())
alterado[0x78] ^= 1
with Path('super-alterado.bin').open('xb') as f:
f.write(alterado)
ejecutar([crc, 'super-alterado.bin'], 1)
ejecutar([lector, 'contenedor.img'], 1)
ejecutar([lector, 'ext4-4k.img', '-1'], 2)
ejecutar([lector, 'ext4-4k-super.bin'], 1)
for p in archivos:
if sha(p) != antes[p]:
raise SystemExit('La imagen cambio: ' + str(p))
print('OK: desplazamiento, rechazos y consultas sin modificar imagenes')
PYLa copia alterada modifica un byte de etiqueta, sin cambiar la firma ni crear un error de tamaño. Eso demuestra por qué una firma correcta no reemplaza la comprobación de integridad. Para repetir el verificador, prepara otro laboratorio o usa otro nombre para la copia exclusiva.
Los estados negativos se capturan como valores esperados, sin detener la prueba por tratarlos como éxitos. El conjunto no incluye una reparación ni un corte de energía y no acredita integridad de todos los archivos del volumen.
16. Simuladores de posiciones y contadores
Una posición fija dentro de bloques de distinto tamaño
El cálculo usa un desplazamiento de volumen conocido, no descubre particiones. El número de bloque se refiere al volumen; la posición absoluta se refiere al archivo contenedor.
¿Dónde leer el superbloque primario?
Combinar contadores sin perder precisión
Se usan enteros BigInt para conservar los 64 bits. El modelo calcula también el producto matemático en bytes y advierte si excede uint64_t. Ese resultado no demuestra que exista una imagen de ese tamaño ni que el lector la admita.
Parte baja, parte alta y característica 64bit
17. Diagnóstico y resumen
| Observación | Interpretación a comprobar |
|---|---|
| La firma aparece como 0x53EF. | Puede haberse invertido la interpretación little-endian. |
| Funciona con bloques de 1 KiB, pero no de 4 KiB. | Revisar si se leyó bloque 1 en vez del offset fijo 1024. |
| El contenedor falla y la imagen de volumen funciona. | Revisar la base del volumen y las unidades del desplazamiento. |
| Los contadores grandes pierden la parte alta. | Revisar 64bit y convertir antes de desplazar. |
| El lector devuelve 3. | Hay una revisión o característica fuera de su subconjunto; no equivale a corrupción. |
| La geometría parece razonable, pero el CRC difiere. | Comprobar algoritmo, bytes cubiertos, estado estable y posible alteración. |
| CRC coincide, pero falla otro objeto del volumen. | La comprobación cubre solo el superbloque, no todas las estructuras. |
- El superbloque primario empieza 1024 bytes después del inicio del volumen.
- La estructura externa usa bytes y comprueba posiciones; la estructura nativa contiene valores decodificados.
- Las lecturas cortas, EOF y EINTR se manejan antes de interpretar el buffer.
- Los límites se validan antes de sumar, multiplicar o desplazar.
- Revisión y características forman parte del contrato del lector.
- Los campos altos se combinan solo cuando su característica los habilita.
- Firma, geometría y CRC aportan evidencias distintas.
- El contraste externo y los casos negativos permiten revisar lo que realmente verifica cada programa.
18. Preguntas de repaso y práctica
1. ¿En qué offset comienza el superbloque primario?
A 1024 bytes del inicio del volumen, no necesariamente del archivo contenedor.
2. ¿Qué cambia si el volumen empieza en 4096?
La lectura absoluta del superbloque empieza en 5120 y la firma está en 5176.
3. ¿En qué bloque está con bloques de 4 KiB?
Dentro del bloque 0 del volumen, a 1024 bytes de su comienzo.
4. ¿Por qué no leer directamente una estructura de uint32_t?
Su representación nativa puede diferir en orden de bytes, relleno o alineación.
5. ¿Qué verifican las aserciones estáticas?
El ancho de byte y offsets, tamaños y condiciones de compilación necesarias para la representación externa.
6. ¿Qué bytes representan 0xEF53 en little-endian?
0x53 seguido de 0xEF.
7. ¿La firma distingue inequívocamente ext4 de ext2?
No. Es una firma compartida de la familia ext y no certifica integridad.
8. ¿Una lectura positiva de pread completa siempre el buffer?
No. Hay que avanzar por lo entregado y continuar hasta completar o detectar un fallo.
9. ¿Qué ventaja tiene pread para esta lectura?
Solicita datos desde un offset explícito sin cambiar la posición de la descripción abierta.
10. ¿Por qué validar con tamaño menos base?
Para evitar desbordar una suma durante la comprobación de que la región cabe.
11. ¿Qué significa estado 3 del lector?
El volumen usa una revisión o característica fuera del subconjunto soportado.
12. ¿Cuándo se interpreta blocks_hi?
Cuando la característica 64bit habilita ese campo para el contador correspondiente.
13. ¿Por qué convertir a uint64_t antes de desplazar?
Porque desplazar un operando de 32 bits por 32 no construye válidamente la parte alta.
14. ¿La etiqueta siempre termina en cero?
No. Es un campo de longitud fija y debe manejarse con límites explícitos.
15. ¿Qué algoritmo usa el comprobador?
CRC32C con la convención del superbloque ext4: semilla 0xffffffff y sin XOR final adicional.
16. ¿Se incluyen los cuatro bytes del checksum almacenado?
No. Se calculan los 1020 bytes anteriores a ese campo.
17. ¿CSUM_SEED sustituye esa semilla del superbloque?
No. El superbloque tiene su cálculo particular; no se aplica indiscriminadamente la fórmula de otras estructuras.
18. ¿Qué demuestra la alteración de un byte de etiqueta?
Que puede mantenerse la firma y, aun así, fallar la comprobación de integridad.
19. ¿CRC correcto significa volumen íntegro?
No. Solo se verificó esa estructura con ese checksum.
20. ¿El contenedor del laboratorio tiene una GPT?
No. Contiene un prefijo artificial de longitud conocida seguido de una imagen de volumen.
Para practicar
- Calcula los offsets absolutos del superbloque y de su firma para bases 0, 4096 y 1048576.
- Relaciona cada miembro interpretado de sb_disco con su offset documentado.
- Comprueba qué aserción falla si agregas un byte antes de magic en una copia del fuente.
- Compila ambos programas con todas las advertencias propuestas.
- Contrasta contadores, UUID y tamaño de inodo con dumpe2fs para las dos imágenes.
- Ejecuta el verificador y explica el resultado esperado de cada caso negativo.
- Prueba offsets vacíos, negativos y fuera del archivo sin modificar la imagen.
- En el simulador de contadores, deshabilita 64bit y observa qué ocurre con la parte alta.
- Prueba hi=4294967295 y lo=4294967295 y explica por qué el producto en bytes puede exceder uint64_t.
- Describe qué validaciones adicionales necesitaría un lector de grupos e inodos.
- Explica por qué un estado RECOVER no se resuelve cambiando un bit en una copia del encabezado.
- Resume qué evidencia aportan firma, geometría, CRC y comprobación global del formato.
En el tema 41 comenzaremos el bloque de planificación de disco: búsqueda, latencia rotacional y transferencia en la anatomía de un acceso.