libarchive: Uninitialized Heap Read en CAB/LZX Cazado con Patch-Diffing

Mismo archivo CAB, dos binarios: uno extrae tranquilamente leyendo memoria que nadie inicializó; el otro, con el parche aplicado, rechaza el archivo. Este hallazgo del sistema autónomo Sherlock es doblemente interesante: por el bug en sí —un read no inicializado del history guard del descompresor LZX (CWE-457)— y por cómo se encontró: comparando un snapshot objetivo contra upstream y midiendo exactamente dónde divergen.

/índice_del_análisis +

00. Resumen Técnico

Objetivolibarchive / bsdtar 3.8.9 — path de extracción CAB con compresión LZX
VulnerabilidadUninitialized heap read del history guard del decoder LZX (lzx_read_blocks) durante la extracción
CWECWE-457 (Use of Uninitialized Variable) · VRT: MCR (read)
SeveridadLOW — solo lectura de valores no controlados directamente; sin escalada a escritura
DetecciónDivergencia post-snapshot: fix upstream del 2026-08-08 ausente en el snapshot del target
MétodoPatch-diffing + comparación A/B con el mismo input CAB sobre dos builds de bsdtar
Resultado A/BBuild vulnerable: rc=0, extrae (con lectura no inicializada). Build parcheado: rc=1 (-25, LZX_CORRUPT)
ValidaciónASAN (use-of-uninitialized-value) + valgrind --track-origins=yes
ClasificaciónDUP-RACE — el fix llegó por otra vía dentro de la ventana de análisis; se documenta como caso metodológico

01. Contexto: Snapshots que Envejecen Mal

Analizar un target serio exige construirlo: compilar la versión concreta, instrumentarla, fijar su entorno. Esa construcción es un snapshot —y desde el momento en que existe, empieza a envejecer. Upstream sigue vivo: commits, hardening, fixes. Cada día que pasa, tu snapshot fiel deja de serlo un poco más.

Esta historia nace de esa tensión. El sistema Sherlock mantiene un snapshot de libarchive 3.8.9 para sus campañas de fuzzing sobre formatos de archivo. Durante la revisión periódica contra upstream apareció un commit del 2026-08-08 que tocaba justo el camino CAB/LZX que el snapshot ejercita: un hardening frente a lectura de memoria no inicializada en el history guard del decoder LZX. Pregunta inmediata: ¿el snapshot contiene ese fix? Respuesta corta: no. Pregunta larga: ¿qué significa eso en la práctica? El resto del artículo es la respuesta.

02. El Método: Patch-Diffing como Radar

El patch-diffing suele asociarse a martes de parches de grandes vendors: comparar binarios antes/después para localizar la vulnerabilidad corregida. Pero tiene un uso inverso igual de potente para quien caza con targets propios: usar los commits upstream como radar de zonas calientes. Si el proyecto acaba de blindar una ruta de código, esa ruta era frágil ayer —y cualquier versión viva sin el parche lo sigue siendo hoy.

Snapshot 3.8.9 build ASAN propio · target de fuzzing sin el hardening futuro Fix upstream · 08-08 history guard LZX validado path CAB/LZX endurecido Revisión · 15-08 diff snapshot ↔ upstream: ¡el fix NO está en el snapshot! Experimento A/B mismo CAB LZX → bsdtar 3.8.9 vs bsdtar parcheado ASAN + valgrind como jueces Divergencia confirmada vulnerable extrae (rc=0) · parcheado rechaza (rc=1)
Figura 1 — Cronología del hallazgo: el commit upstream del 08-08 funciona como aviso; la revisión semanal del snapshot lo convierte en hipótesis; el A/B la transforma en hecho medible.

03. Anatomía: El History Guard sin Inicializar

El algoritmo LZX —el que comprimían los instaladores de Windows y los ficheros CAB antiguos— mantiene un historial de ventana: los últimos kilobytes de salida descomprimida, que las instrucciones de match reutilizan como diccionario. Para acelerar la decodificación, la implementación de libarchive lleva controles auxiliares sobre ese historial; uno de ellos es el history guard, que decide cuándo el estado del historial es válido.

El problema: en ciertas secuencias de bloques CAB malformadas, el guard se consulta antes de haber sido escrito. El heap entrega lo que había en esas direcciones —basura de usos anteriores— y el decoder toma decisiones de decodificación basadas en ella. Formalmente es CWE-457: comportamiento dependiente de valores no inicializados. En la práctica observada, la consecuencia visible era que archivos que deberían ser rechazados como corruptos se «decodificaban» hasta el final, consumiendo la basura como si fuera estado legítimo.

# Forma del defecto (semántica del fix upstream del 08-08) # ANTES (snapshot 3.8.9): if (guard && ...) # guard puede llegar aquí SIN inicializar ...procesar bloque... # decisión tomada sobre basura del heap # DESPUÉS (upstream parcheado): if (!history_valid) # estado explícito + inicialización garantizada return -25; # LZX_CORRUPT: rechazo limpio del stream

Los sanitarios lo confirman con precisión quirúrgica: AddressSanitizer reporta use-of-uninitialized-value en el camino del guard durante lzx_read_blocks, y valgrind con --track-origins=yes remonta el valor hasta su origen: una asignación de heap cuyo contenido nunca se escribió antes de leerse.

04. El Experimento A/B

La validación mínima de una divergencia es comparar los dos mundos con todo lo demás constante: mismo input CAB que recorre el path del history guard, misma invocación de extracción, única variable = el parche.

Terminal mostrando la comparación A/B: bsdtar 3.8.9 extrae el CAB con rc=0 mientras el build parcheado devuelve rc=1 con error LZX_CORRUPT, seguido de la confirmación de valgrind
Evidencia A/B — El mismo CAB ante dos builds: el snapshot vulnerable extrae silenciosamente (con lectura no inicializada bajo ASAN/valgrind); el parcheado corta con «corrupt history guard». Captura regenerada a partir del log original de validación con rutas de entorno redactadas.
# Resumen del protocolo A/B (2026-08-15) # A — snapshot vulnerable (bsdtar ASAN 3.8.9) $ bsdtar-asan-3.8.9 -xvf poc_cab_lzx_history.cab ; echo rc=$? poc_lzx.bin extraído rc=0 # éxito aparente... # stderr ASAN: use-of-uninitialized-value (history guard) # B — build parcheado (fix upstream 08-08) $ bsdtar-patched -xvf poc_cab_lzx_history.cab ; echo rc=$? bsdtar: LZX decoder error: corrupt history guard rc=1 # (-25, LZX_CORRUPT): rechazo correcto # Origen confirmado de forma independiente: $ valgrind --track-origins=yes bsdtar-asan-3.8.9 -xf poc_cab_lzx_history.cab ==== Conditional jump or move depends on uninitialised value(s) ==== Uninitialised value was created by a heap allocation

La asimetría de veredictos es el resultado completo: un archivo que el código correcto considera corrupto atraviesa el código vulnerable sin levantar sospechas. Ese es exactamente el tipo de silencio que un read no inicializado produce —y el tipo de señal que un A/B bien montado vuelve audible.

05. La Clasificación Honesta: DUP-RACE

Aquí el sistema aplicó una regla que vale tanto para humanos como para agentes: verificar la fecha del fix antes de reclamar prioridad. El hardening upstream databa del 08-08 —una semana antes de la detección—, lo que sitúa el descubrimiento dentro de una carrera perdida: alguien reportó el problema primero y upstream ya lo había cerrado. El hallazgo se archiva como DUP-RACE: real, reproducible, pero duplicado respecto a la línea temporal oficial.

Por qué publicarlo aun siendo dup-race

Tres razones. Primera: el valor metodológico —el patch-diffing como radar— sobrevive al destino del bug concreto. Segunda: millones de sistemas siguen ejecutando builds sin el fix; saber que el path existe ayuda a operadores a priorizar actualizaciones de libarchive. Tercera: la honestidad taxonómica es parte del contrato de esta serie; un dup-race documentado enseña más que un hallazgo inflado.

06. Acotación: Por Qué es LOW

07. Lecciones Metodológicas

Más allá del CWE, este caso dejó tres reglas operativas al sistema:

  1. La fecha del fix importa tanto como el fix: una divergencia detectada días después del commit upstream casi siempre es carrera perdida; reconocerlo rápido ahorra semanas de maduración inútil y protege la credibilidad del programa.
  2. El A/B con build ASAN es la validación mínima: una divergencia solo afirmada por diff de código es una hipótesis; con dos binarios y un input, es un experimento. La diferencia entre ambos estados es la diferencia entre creer y saber.
  3. Los commits upstream son inteligencia gratuita: cada hardening señala una debilidad histórica y delimita las versiones vivas que aún la padecen. Revisarlos periódicamente convierte el repositorio ajeno en sensor propio.

08. Conclusión

No todos los hallazgos terminan en CVE propio, y está bien que sea así. Este caso demuestra que un sistema de caza maduro también sabe perder carreras con elegancia: detectar la divergencia, medirla, clasificarla honestamente y extraer de ella el activo duradero —el método—. El history guard de LZX ya está protegido upstream; el patch-diffing como radar, en cambio, apenas empieza a dar sus primeros resultados.

Para operadores, el recordatorio práctico: si vuestra distribución todavía sirve builds de libarchive sin el hardening de agosto, los CAB malformados pueden atravesar vuestro extractor con éxito aparente. Actualizar no es burocracia; es cerrar puertas que otros ya saben dónde están.

/Más hallazgos de la serie Sherlock

Siete vulnerabilidades confirmadas en un mes de caza autónoma con modelos locales de IA. Todos los análisis con evidencia y diagramas.

09. Referencias