Desafiar Android desde el sofá y un smartphone

Friday, July 17, 2026

¿Se puede desafiar la seguridad de Android usando SOLO un teléfono móvil?

La respuesta es sí.
Y esta es mi historia.


Soy Manuel García Peña (Lostmon), investigador independiente de seguridad y presidente de BojosXtu, una asociación de apoyo y empoderamiento en salud mental.

Durante los últimos cuatro años he perseguido un mismo patrón dentro de Android. Lo que comenzó como una intuición técnica terminó convirtiéndose en una investigación donde documento 31 vectores de ataque relacionados con un mismo fenómeno al que he llamado Structured Text Amplification (STA), un patrón que implica componentes críticos de Android como libminikin, SystemUI, el pipeline de renderizado de texto y distintos mecanismos internos de procesamiento y restauración del estado del sistema.

📅 La investigación completa verá la luz el próximo 30 de julio.

Pero lo más importante no es el hallazgo técnico.
Lo importante es cómo llegué hasta él.


🧒 Antes del diagnóstico: perseguir bugs era mi forma de entender el mundo

Mucho antes de recibir un diagnóstico de salud mental, ya era el típico chaval que desmontaba todo para descubrir cómo funcionaba.

Mientras otros jugaban, yo abría aplicaciones, observaba comportamientos extraños y me hacía siempre la misma pregunta:

"¿Y si esto se puede romper?"

No sabía lo que era un Bug Bounty. No sabía lo que era AOSP. Ni siquiera imaginaba que algún día investigaría vulnerabilidades.

Simplemente tenía una curiosidad casi obsesiva por comprender cómo funcionaban los sistemas.

Con los años empecé a encontrar fallos reales, a reportarlos y a aprender de forma completamente autodidacta.

Antes de tener que retirarme por problemas de salud mental, llegué a descubrir y documentar, más de 400 vulnerabilidades.


🖤 El momento en que tuve que dejarlo

Después llegó el diagnóstico.

Y con él, una realidad que me costó mucho aceptar.

Ya no podía pasar horas delante de un ordenador como antes.

Mi mente empezó a jugarme malas pasadas. Cuando llega. los Fantasmas, cada uno debe librar sus batallas con sus propios demonios.

Cuanto más tiempo permanecía frente a una pantalla grande, más me absorbía el mundo que había dentro de ella. Perdía la noción del tiempo, perdía el entorno y, a veces, sentía que me perdía yo mismo entre miles de líneas de código.

Tuve que dejar de investigar y alejarme de la tecnología que tanto me apasiona. ¿Como borras algo de tu vida que ha estado siempre?

Y fue durísimo.

Porque no estaba perdiendo simplemente un hobby.
Sentía que estaba perdiendo una parte de mí.

Pensé que todo había terminado.


📱 Mi laboratorio: un Xiaomi Redmi 14 5G

Pero la curiosidad seguía ahí.

Y encontré otra forma de poder de vivir, sin estar rodeado de tanta tecnología.

Gran parte de esta investigación se ha realizado desde un Xiaomi Redmi 14 5G.

Desde el sofá.

En mis días buenos.
Cuando la cabeza me dejaba concentrarme un rato.

Sin un laboratorio.
Sin una estación de trabajo.
Sin varios monitores.
Sin ADB.
Sin Android Studio.

Solo una pantalla pequeña, AOSP abierto en el navegador y miles de líneas de código leídas función a función, siguiendo llamadas entre clases para intentar comprender cómo pensaba Android.

Durante cuatro años fui reconstruyendo ese enorme puzle.

Había días en los que solo podía revisar unas pocas funciones.

Otros encontraba una pista y pasaba horas siguiendo llamadas entre TextView, StaticLayout, LineBreaker, libminikin, SystemUI y muchos otros componentes del framework.

Así comenzó la persecución de un patrón que aparecía una y otra vez.


🔍 Cuatro años persiguiendo el mismo fantasma

La primera vez que observé un comportamiento extraño fue hace años, en Android 9.

Lo reproduje.
Tomé notas.
Y seguí investigando.

Con el tiempo aparecieron nuevos síntomas:

  • • bloqueos en navegadores;
  • ANRs provocados por determinadas estructuras de texto;
  • • problemas durante la impresión;
  • • corrupción de estado en aplicaciones;
  • crash loops persistentes de SystemUI;
  • • comportamientos aparentemente distintos que, una y otra vez, me llevaban al mismo lugar.

La mayoría habría pensado que eran errores independientes.

Yo no conseguía quitarme de la cabeza la sensación de que todos compartían un mismo origen.

Cada vez que encontraba un síntoma nuevo volvía al mismo sitio del código.

Fue entonces cuando empecé a sospechar que no estaba persiguiendo treinta y un errores distintos.
Estaba persiguiendo un único patrón que se manifestaba de formas diferentes.


🧠 La neurodivergencia también puede aportar otra forma de mirar

Durante mucho tiempo pensé que mi diagnóstico era un obstáculo para investigar.

Hoy lo veo de otra manera.

Con el tiempo comprendí que mi forma de procesar la información también tenía fortalezas: una enorme capacidad para detectar patrones, mantener el foco durante semanas sobre un mismo problema y conectar detalles que parecían no tener relación.

No romantizo la salud mental.
Hay días malos.
Hay agotamiento.
Hay frustración.
Hay momentos en los que la cabeza simplemente no coopera.

Pero también he aprendido que cada persona observa el mundo de una manera distinta.

Y, a veces, esa mirada diferente permite encontrar conexiones que habían pasado desapercibidas.


🤝 La comunidad hizo posible continuar

Yo solo no habría podido validar todo esto.

Compañeros y amigos me prestaron dispositivos para reproducir los distintos vectores en fabricantes y versiones diferentes de Android.

Gracias a ellos pude comprobar comportamientos en Xiaomi, Samsung, OPPO, OnePlus y otros terminales.

La investigación independiente también se construye así.

Con personas que comparten tiempo, hardware y confianza sin esperar nada a cambio.


💰 Y un día llegaron dos mensajes que jamás olvidaré

En medio de todo este proceso llegaron dos validaciones que nunca habría imaginado cuando empecé leyendo AOSP desde la pantalla de un teléfono móvil.

Google confirmó una vulnerabilidad relacionada con Binder y me concedió una recompensa a través de su programa de vulnerabilidades.

Y hace apenas unas horas Xiaomi confirmó otra vulnerabilidad y me concedió una recompensa mediante HackerOne.

No es una cuestión de dinero.

Lo verdaderamente importante fue leer una frase muy sencilla:

"This vulnerability is confirmed."

Después de tantos años investigando en silencio, esa frase significó mucho más que una recompensa económica.

Fue la confirmación de que todo aquel esfuerzo, todas aquellas horas leyendo código y persiguiendo un patrón que casi nadie veía, estaban dando fruto.


🚀 La investigación continúa

El próximo 30 de julio publicaré el whitepaper completo con la metodología, el marco teórico de Structured Text Amplification (STA), los 31 vectores documentados y todas las evidencias recogidas durante estos años.

No sé cuál será su impacto.
No sé cuántas personas lo leerán.

Pero sí sé una cosa.

La curiosidad no depende del hardware.

No depende de tener el mejor laboratorio.
Ni de disponer de cuatro monitores.

Depende de seguir haciéndote preguntas.

Depende de no dejar de preguntarte:

"¿Y si nadie ha mirado aquí?"

Porque, al final, investigar nunca ha consistido en tener el mejor equipo.

Ha consistido en no perder nunca la capacidad de hacerse preguntas.

Y después de todo este tiempo sigo creyendo exactamente lo mismo que cuando era un chaval persiguiendo bugs sin saber siquiera cómo se llamaban.

"La curiosidad es lo que hace mover la mente." 🛡️

La investigación continúa.
Y yo también.


#Lostmon #Android #AOSP #Ciberseguridad #MobileSecurity #SystemUI #libminikin #STA #StructuredTextAmplification #BugBounty #GoogleVRP #Xiaomi #HackerOne #SaludMental #InvestigaciónIndependiente #BojosXtu
Manuel García Peña (Lostmon)
lostmon.blogspot.com · lostmon@gmail.com

Timeline de STA Structured Text Amplification

Sunday, July 12, 2026
⏳ Timeline de la investigación STA
De 2022 al 30 de julio de 2026 · 31 vectores · 15 archivos · 13 años de código vulnerable
Actualizado: 12 de julio de 2026
📌 Referencias: Los tres artículos de la saga
📖 Estos tres posts forman la base de la investigación que culminará en el whitepaper del 30 de julio de 2026.

📌 Esta línea temporal resume los hitos clave de la investigación — desde los primeros hallazgos en 2022 hasta la publicación del whitepaper el 30 de julio de 2026. En el camino, 31 vectores, 15 archivos de AOSP, 9 empresas, 12 navegadores y una recompensa de $250 que se convirtió en símbolo de la contradicción de Google.

2022 · El origen

2022 28 ago
🔍 Descubrimiento original
Lostmon encuentra TransactionTooLargeException y DeadSystemException en Firefox Focus, Firefox Nightly mediante clipboard, share y open-in-app.
2022 12 oct
📝 Publicación del advisory
Se publica el primer advisory público: "Mozilla Firefox Focus and Nightly for Android Remote Crash DoS" en lostmon.blogspot.com.
2022 Nov
📋 Reporte a Chromium · Issue 40879254
Se abre Chromium Issue 40879254. Se eliminan las etiquetas de seguridad (comment #9). Se aplica un parche UX parcial (comment #22). El issue sigue abierto y asignado.
2022 Nov
📋 Reporte a Mozilla · Bugzilla #1802594
Se abre Mozilla Bugzilla #1802594 (Severidad S3). El issue sigue en estado NEW (sin resolver) en 2026.

2026 · La investigación formal

2026 20 ene
📋 Reporte al Chrome VRP · Issue 477202817
Se envía el reporte al Chrome VRP con 27 vectores. Google lo cierra el mismo día como "Won't Fix (Intended Behavior)" argumentando que el DoS "no es un bug de seguridad en Chrome".
2026 20 ene
📋 Reporte al Android VRP y MSRC
Se envía el reporte formal a Google Android VRP (A-477279924) y a Microsoft MSRC.
2026 Ene
💰 Recompensa de Google · $250
Google paga 250 dólares por la primera parte de la investigación (caso Binder/IPC).
2026 Feb
📄 Whitepaper v4
Se distribuye el whitepaper v4 con 27 vectores. Se añaden STA-015-DL (CVSS 8.6) y STA-015b.
2026 Mar
🔍 Reporte a Meta
Se notifica a Meta (WhatsApp/Threads) a través de su bug platform. No hay respuesta.

Mayo–Junio 2026 · La evidencia de campo

2026 24 may
📱 Bugreport de Chrome · STA-017 → Tier A
Se capturan 2 ANR completos de Chrome con trazas nativas de libminikin.so en un Xiaomi Redmi Note 14 5G (Android 16, HyperOS 3.0).
2026 26 may
📱 Bugreport de SystemUI · 85 crashes
Se capturan 85 SystemUI crashes en 4 días en el mismo dispositivo. Primera confirmación en Android 16.
2026 31 may
📱 STA-015-DL · Ataque remoto confirmado
Se confirma el ataque remoto a través de Google Drive: callingPackage=com.google.android.apps.docs. Afecta a 6 navegadores.
2026 10 jun
📋 Notificaciones a vendors
Se envían reportes a Mozilla, Opera, DuckDuckGo (HackerOne), Brave (HackerOne), Tor Project, Xiaomi (HackerOne).
2026 10 jun
📱 Bugreport de Firefox · 16.006 ms
Se captura ANR de Firefox con 16.006 ms de duración. Trigger: enlace real de Gmail. STA-017 Firefox → Tier A.
2026 14 jun
📋 Respuesta de MSRC
Microsoft MSRC responde, deriva el caso a Google y solicita casos separados.
2026 15 jun
❌ Google cierra libminikin · "out of scope"
Google cierra el reporte específico de libminikin (524288518) como "out of scope" — meses después de pagar los $250 por el caso Binder/IPC.
2026 16 jun
📋 Apelación a Google
Se envía apelación para STA-015-DL (CVSS 8.6), citando precedentes CVE-2023-21167 y A-259942964.
2026 16 jun
📋 Respuesta de Opera · P5 (Informational)
Opera clasifica STA-016/017 como P5 (Informational). Afecta a Opera y Opera Mini.
2026 19 jun
📋 Rechazo de Brave
Brave rechaza el reporte como "upstream/DoS out of scope".
2026 20 jun
📱 Segunda confirmación SystemUI · Edge ANR
Segundo bugreport espontáneo de SystemUI. 3 crashes en 1.7s → Edge ANR 25s después. STA-017 Edge → Tier A.
2026 24 jun
📋 MSRC · Casos separados
Se envían 3 casos separados a MSRC (STA-013, STA-017/STA-020, STA-015-DL).
2026 24 jun
📝 Publicación: Structured Text Amplification (STA)
Se publica el primer artículo en el blog: "Structured Text Amplification (STA)" — el marco teórico.
2026 24 jun
📝 Publicación: Algorithmic DoS en libminikin.so
Se publica el segundo artículo en el blog: "Algorithmic DoS en libminikin.so" — la prueba de concepto.
2026 28 jun
📱 Bugreport de Google App · 5 ANR + 3 TransactionTooLargeException
Se capturan 5 ANR + 3 TransactionTooLargeException en 14 minutos. breakLineGreedy también es vulnerable. STA-017 Google App → Tier A. Primera evidencia de Clase A + Clase B simultáneas.
2026 30 jun
📋 Confirmación sin parche
No hay commits en AOSP, Chromium o AndroidX que aborden los vectores documentados. El boletín de junio no contiene CVEs relacionados.

Julio 2026 · La cuenta atrás

2026 5 jul
📋 Nuevo issue de Google · #531319203
Se abre issue #531319203: system_server boot loop por imagen grande en BitmapCache. El mismo patrón arquitectónico.
2026 6 jul
📋 Boletín de julio sin parche
Google publica el boletín de seguridad de julio de 2026. No contiene ningún parche para los 31 vectores.
2026 8 jul
📋 Xiaomi sin respuesta · 35+ días
Xiaomi no responde tras 35+ días. Se envía seguimiento con deadline del 20 de julio.
2026 10 jul
📝 Publicación: 10 años de vulnerabilidad
Se publica el tercer artículo en el blog: "libminikin: 10 años de vulnerabilidad en el núcleo de Android" — el análisis forense del código de AOSP.
2026 10 jul
📋 Meta sin respuesta · 113+ días
Meta no responde tras 113+ días.
2026 10 jul
📱 Actualización de seguridad · Sigue vulnerable
El dispositivo Xiaomi Redmi Note 14 5G recibe el parche de junio de 2026. Sigue siendo vulnerable.
2026 11 jul
📝 Nota de prensa · BojosXtu
Se prepara la nota de prensa para BojosXtu. Se añade la contradicción de los $250.
2026 12 jul
📋 Issue 477202817 · Confirmado
Se confirma que Chromium Issue 477202817 (reporte al Chrome VRP) sigue cerrado como "Won't Fix (Intended Behavior)".

📅 Próximos pasos

2026 20 jul
⏰ Último día para vendors
Fecha límite para que los vendors (Xiaomi, Meta, etc.) respondan.
2026 30 jul
🚀 PUBLICACIÓN DEL WHITEPAPER
Se publica el whitepaper completo con 31 vectores, trazas nativas, código AOSP y cronología.

31
vectores documentados
15+
archivos de AOSP
13
años de código vulnerable
2.5B
dispositivos afectados

🔴 250 dólares por 2.500 millones de dispositivos: 0,0000001 $ por dispositivo. Menos de lo que vale un átomo de polvo.
Si Google hubiera pagado 1 céntimo por dispositivo, la recompensa habría sido de 25 millones de dólares. Pagaron 250.


Manuel Garcia Peña (Lostmon) · lostmon@gmail.com · lostmon.blogspot.com
Whitepaper completo: 30 de julio de 2026

libminikin: 10 años de vulnerabilidad en el núcleo de Android

Friday, July 10, 2026
libminikin: "Un comportamiento heredado del motor de composición de texto de Android"
Análisis forense del código fuente de AOSP · 6 archivos · 5 capas vulnerables · Una década de deuda técnica

📌 Este artículo es una extensión de mi análisis original: "Algorithmic DoS en libminikin.so – Cómo un texto de 70 KB puede congelar casi cualquier app Android" (24 de junio de 2026).

🔥 10 AÑOS. 10 ARCHIVOS. MAS DE 5 CAPAS VULNERABLES.

El análisis del código fuente de Android (AOSP) confirma que libminikin.so tiene un fallo de diseño sistémico presente desde 2013.

  • 10 archivos vulnerables en el repositorio de AOSP
  • 5 capas del pipeline de texto sin validación de entrada
  • 10 años de deuda técnica documentada (2013 → 2026)
  • 2.500 millones de dispositivos en riesgo
  • Google lo sabe desde 2016 (CVE-2016-2414) y no lo ha arreglado

1. Resumen ejecutivo

Desde la publicación del artículo original sobre el Algorithmic DoS en libminikin.so, he continuado investigando el código fuente de Android (AOSP) para comprender la magnitud real del problema.

Lo que he encontrado es más grave de lo que imaginaba. No se trata de un bug aislado en una función concreta. Es un fallo de diseño sistémico presente en 6 archivos diferentes, distribuido en 5 capas del pipeline de texto, y que ha estado en el repositorio de AOSP desde 2013.


2. Issues públicos de Google (2020–2026)

La siguiente tabla recopila los issues públicos del Google Issue Tracker que documentan problemas en libminikin y el pipeline de texto de Android. Todos ellos fueron cerrados sin una solución estructural.

Issue Año Descripción Estado Enlace
#161830416 2020 Crash en LayoutCache::getOrCreate con traza completa (FreeType → Minikin → HarfBuzz) Won't Fix Ver issue
#167014931 2020 Crash por Float.POSITIVE_INFINITY en LayoutPiece::LayoutPiece Fixed Ver issue
#188985643 2021 SIGSEGV en FontFamily::getClosestMatch en Samsung Galaxy J6+ con fuente personalizada Won't Fix Ver issue
#40268980 (Chromium 1447465) 2023 ANR en Chrome por LayoutCache::getOrCreate reportado por ingeniero de Samsung Won't Fix Ver issue
#477202817 2026 Reporte formal al Chrome VRP — DoS persistente por URL larga. Cerrado como "Intended Behavior" Won't Fix Ver issue
#524288518 2026 Reporte específico de libminikin al Android VRP — cerrado como "out of scope" Out of Scope (interno de Google)
#531319203 Jul 2026 system_server boot loop por imagen grande en BitmapCache — mismo patrón Assigned Ver issue

🔴 Patrón común: Google ha cerrado sistemáticamente estos reportes sin abordar la causa raíz. Solo ha arreglado el caso trivial (#167014931) porque el trigger era evidente (Float.POSITIVE_INFINITY).


3. Análisis del código fuente: 6 archivos vulnerables

He analizado el código fuente de Minikin en el repositorio de AOSP (frameworks/minikin/) y he identificado seis archivos clave que contienen vulnerabilidades de diseño.

Archivo Año Problema principal Función vulnerable
FontFamily.cpp 2013 Punteros nulos en getClosestMatch getClosestMatch()
Layout.cpp 2013 Procesa directamente textos largos sin caché doLayoutWord()
OptimalLineBreaker.cpp 2015 Knuth-Plass O(n²) sin límites computeBreaks()
GreedyLineBreaker.cpp 2017 Algoritmo voraz O(n²) en el peor caso processLineBreak()
LineBreaker.cpp 2018 Decisión entre algoritmos sin validar longitud breakIntoLines()
LayoutCache.h 2018 Caché sin límite de tamaño por entrada getOrCreate()

4. Funciones vulnerables y código exacto

4.1 LineBreaker::breakIntoLines — Sin validación de longitud

Archivo: frameworks/minikin/libs/minikin/LineBreaker.cpp (2018)

LineBreakResult breakIntoLines(const U16StringPiece& textBuffer, BreakStrategy strategy,
                               HyphenationFrequency frequency, bool justified,
                               const MeasuredText& measuredText, const LineWidth& lineWidth,
                               const TabStops& tabStops, bool useBoundsForWidth) {
    if (strategy == BreakStrategy::Greedy || textBuffer.hasChar(CHAR_TAB)) {
        return breakLineGreedy(textBuffer, measuredText, lineWidth, tabStops,
                               frequency != HyphenationFrequency::None, useBoundsForWidth);
    } else {
        return breakLineOptimal(textBuffer, measuredText, lineWidth, strategy, frequency, justified,
                                useBoundsForWidth);
    }
}

🔴 Vulnerabilidad: No hay comprobación de textBuffer.size(). Si el texto es de 70KB, se ejecuta breakLineOptimal o breakLineGreedy sin límite.

4.2 OptimalLineBreaker::computeBreaks — O(n²) sin límites

Archivo: frameworks/minikin/libs/minikin/OptimalLineBreaker.cpp (2015)

LineBreakResult LineBreakOptimizer::computeBreaks(const OptimizeContext& context,
                                                  const U16StringPiece& textBuf,
                                                  const MeasuredText& measuredText,
                                                  const LineWidth& lineWidth,
                                                  BreakStrategy strategy, bool justified,
                                                  bool useBoundsForWidth) {
    // ... algoritmo de programación dinámica Knuth-Plass
    // SIN validación de longitud de entrada
    // Complejidad O(n²) en el peor caso
}

Pero hay un matiz que el código revela: hay una poda activa (active = j + 1) y una optimización bestHope explícitamente comentada como tal: Cpp Esto significa que el algoritmo no es un O(n²) puro sin ninguna mitigación — tiene un mecanismo de poda que en la práctica reduce trabajo cuando delta mayor que 0 (la línea se desborda), avanzando el puntero active. El peor caso teórico sigue siendo O(n²) (cuando casi todos los candidatos permanecen "activos" simultáneamente, como pasaría con una URL sin espacios donde casi cualquier punto de ruptura es candidato).

if (jScore + bestHope >= best) continue;

🔴 Vulnerabilidad: El algoritmo Knuth-Plass tiene complejidad O(n²). Para 70.000 caracteres, son ~4.9 mil millones de operaciones.

4.3 GreedyLineBreaker::processLineBreak — Bucles anidados

Archivo: frameworks/minikin/libs/minikin/GreedyLineBreaker.cpp (2017)

void GreedyLineBreaker::processLineBreak(uint32_t offset, WordBreaker* breaker,
                                         bool doHyphenation) {
    while (isWidthExceeded() || overhangExceedLineLimit(Range(getPrevLineBreakOffset(), offset))) {
        if (tryLineBreakWithWordBreak()) {
            continue;  // El bucle puede ejecutarse muchas veces
        }
        if (doHyphenation && tryLineBreakWithHyphenation(...)) {
            // ...
        }
    }
}

🔴 Vulnerabilidad: Bucle while con llamadas a funciones que recorren el texto. Complejidad O(n²) en el peor caso.

4.4 LayoutCache::getOrCreate — Procesamiento directo sin caché

Archivo: frameworks/minikin/include/minikin/LayoutCache.h (2018)

template <typename F>
void getOrCreate(const U16StringPiece& text, const Range& range, const MinikinPaint& paint,
                 bool dir, StartHyphenEdit startHyphen, EndHyphenEdit endHyphen,
                 bool boundsCalculation, F& f) {
    LayoutCacheKey key(text, range, paint, dir, startHyphen, endHyphen);
    if (range.getLength() >= CHAR_LIMIT_FOR_CACHE) {
        // ¡PROCESAMIENTO DIRECTO! SIN CACHÉ → BLOQUEO UI
        LayoutPiece piece(text, range, dir, paint, startHyphen, endHyphen);
        // ...
        return;
    }
    // ...
}

🔴 Vulnerabilidad: Los textos largos no se almacenan en caché. Se procesan desde cero cada vez que se renderizan.


5. El pipeline completo: 5 capas de fragilidad

1. LineBreaker.cpp (2018) — breakIntoLines()
   └── Decide entre Greedy y Optimal SIN validar longitud
         │
2. OptimalLineBreaker.cpp (2015) / GreedyLineBreaker.cpp (2017)
   └── Algoritmo O(n²) SIN límites de entrada
         │
3. Layout.cpp (2013) — doLayoutWord()
   └── Llama a LayoutCache SIN verificar tamaño
         │
4. LayoutCache.h (2018) — getOrCreate()
   └── Si texto > CHAR_LIMIT_FOR_CACHE → procesa DIRECTAMENTE
         │
5. HarfBuzz (hb_shape)
   └── Renderizado de glifos → O(n²) o peor
         │
         ▼
   UI BLOQUEADA (5-16 segundos)

🔴 Todas las capas son vulnerables. Ninguna valida la longitud del texto de entrada.


🧠 El Pipeline de Amplificación de Minikin: Anatomía de un Fallo de Diseño de 13 Años

Aquí profundizo en el pipeline completo y en cómo cada capa amplifica el payload hasta convertir 70KB en un bloqueo de 16 segundos.


El motor de layout de texto de Android (Minikin) contiene un fallo de diseño sistémico que permite que un texto de tan solo 70.000 caracteres (aproximadamente 70KB) bloquee el hilo de interfaz de usuario durante 5-16 segundos, provocando ANR en cualquier aplicación que muestre texto sin truncar.

El problema no reside en una única función, sino en más de 10 archivos y 15 funciones que conforman el pipeline de procesamiento de texto. Cada capa del pipeline amplifica el coste computacional sin aplicar ningún tipo de validación de longitud o límite de entrada.

🔴 Dato clave: El código más antiguo data de 2013. Google ha tenido más de 13 años para arreglar esto.


🧠 El esquema de amplificación: 10 capas de fragilidad

Un texto de 70KB con una estructura específica (/code> repetido) activa una cascada de amplificación en la que cada capa del pipeline multiplica el coste computacional:

70KB de entrada
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 1: LineBreaker.cpp (2018)                              │
 │ breakIntoLines() decide entre Greedy y Optimal             │
 │ SIN validar longitud → pasa 70KB al siguiente paso          │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 2: OptimalLineBreaker.cpp (2015) / GreedyLineBreaker  │
 │ computeBreaks() / processLineBreak()                       │
 │ Algoritmo O(n²) → 70.000² = 4.900.000.000 iteraciones      │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 3: Layout.cpp (2013)                                   │
 │ doLayoutWord() llama a LayoutCache SIN verificar tamaño    │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 4: LayoutCache.h (2018)                                │
 │ getOrCreate(): si texto > CHAR_LIMIT_FOR_CACHE             │
 │ → procesa DIRECTAMENTE, sin caché                         │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 5: LayoutCore.cpp (2018)                               │
 │ LayoutPiece() constructor → hb_shape (HarfBuzz)            │
 │ HarfBuzz tiene complejidad O(n²) o peor                    │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 6: Hyphenator.cpp (2015)                               │
 │ hyphenateFromCodes() → bucles anidados O(n²)               │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 7: Measurement.cpp (2015)                              │
 │ getOffsetForAdvance() / distributeAdvances() → O(n²)       │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 8: WordBreaker.cpp (2015)                              │
 │ detectEmailOrUrl() / findNextBreakInEmailOrUrl() → O(n)   │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 9: FontCollection.cpp (2013)                           │
 │ getGlyphScore() → llama a hb_shape() nuevamente           │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ CAPA 10: FontFamily.cpp (2013)                              │
 │ getClosestMatch() → punteros nulos → SIGSEGV               │
 └─────────────────────────────────────────────────────────────┘
      │
      ▼
   🔥 UI BLOQUEADA (5-16 segundos)

🔴 10 capas. 10 archivos. Ninguna valida la longitud del texto de entrada.


📂 Tabla completa de archivos y funciones vulnerables

Archivo Año Función Complejidad Problema
LineBreaker.cpp2018breakIntoLines()O(1)No valida longitud de entrada
OptimalLineBreaker.cpp2015computeBreaks()O(n²)Knuth-Plass sin límites
GreedyLineBreaker.cpp2017processLineBreak()O(n²)Bucles anidados sin límites
Layout.cpp2013doLayoutWord()O(n)Llama a LayoutCache sin verificar tamaño
LayoutCache.h2018getOrCreate()O(n)CHAR_LIMIT_FOR_CACHE → procesa directamente
LayoutCore.cpp2018LayoutPiece()O(n²)Llama a hb_shape (HarfBuzz) sin límites
Hyphenator.cpp2015hyphenateFromCodes()O(n²)Bucles anidados sin límites
Measurement.cpp2015getOffsetForAdvance()O(n²)Bucles anidados sin límites
Measurement.cpp2015distributeAdvances()O(n²)Bucles anidados sin límites
WordBreaker.cpp2015detectEmailOrUrl()O(n)Recorre texto sin límites
WordBreaker.cpp2015findNextBreakInEmailOrUrl()O(n)Recorre texto sin límites
FontCollection.cpp2013getGlyphScore()O(n²)Llama a hb_shape sin límites
FontCollection.cpp2013itemize()O(n)Recorre texto sin límites
FontFamily.cpp2013getClosestMatch()O(n)Puntero nulo → SIGSEGV

📊 10 archivos. 15 funciones. 10 años de código vulnerable. 13 años desde el primer archivo.


🔬 Amplificación paso a paso: Cómo 70KB pieden convertirse en 5 mil millones de operaciones

Paso 1: Entrada (70KB)

El payload es una URL de 70.000 caracteres con el patrón [PAYLOAD] repetido:

https://example.com/[PAYLOAD]

Paso 2: LineBreaker::breakIntoLines (2018)

if (strategy == BreakStrategy::Greedy || textBuffer.hasChar(CHAR_TAB)) {
    return breakLineGreedy(...);   // ← No hay validación de longitud
} else {
    return breakLineOptimal(...);  // ← Tampoco hay validación
}

Amplificación: Decide el algoritmo sin comprobar la longitud del texto.

Paso 3: OptimalLineBreaker::computeBreaks (2015) — O(n²)

LineBreakResult LineBreakOptimizer::computeBreaks(...) {
    // ... algoritmo de programación dinámica Knuth-Plass
    // SIN validación de longitud de entrada
    // Complejidad O(n²) en el peor caso
}

Amplificación: Para n=70.000 → 4.900.000.000 iteraciones.

Paso 4: Layout::doLayoutWord (2013)

float Layout::doLayoutWord(...) {
    // ...
    LayoutCache::getInstance().getOrCreate(textBuf, range, paint, isRtl, startHyphen, endHyphen,
                                           boundsCalculation, f);
    // ...
}

Amplificación: Llama a LayoutCache sin verificar el tamaño del texto.

Paso 5: LayoutCache::getOrCreate (2018)

if (range.getLength() >= CHAR_LIMIT_FOR_CACHE) {
    LayoutPiece piece(text, range, dir, paint, startHyphen, endHyphen);
    // PROCESAMIENTO DIRECTO → SIN CACHÉ → BLOQUEO UI
    return;
}

Amplificación: Los textos largos no se almacenan en caché. Se procesan desde cero cada vez.

Paso 6: LayoutPiece constructor (2018)

LayoutPiece::LayoutPiece(...) {
    // ...
    hb_shape(hbFont.get(), buffer.get(), features.empty() ? NULL : &features[0],
             features.size());
    // ...
}

Amplificación: hb_shape (HarfBuzz) puede tener complejidad O(n²) o peor.

Paso 7: Hyphenator::hyphenateFromCodes (2015)

void HyphenatorCXX::hyphenateFromCodes(...) const {
    for (size_t i = 0; i < len - 1; i++) {
        for (size_t j = i; j < len; j++) {
            // BUCLES ANIDADOS SIN LÍMITES
        }
    }
}

Amplificación: Bucles anidados O(n²) que se ejecutan para cada palabra.

Paso 8: Measurement::getOffsetForAdvance / distributeAdvances (2015)

size_t getOffsetForAdvance(...) {
    for (size_t i = start; i < max; i++) { /* ... */ }
    for (size_t i = searchStart; i <= max; i++) { /* ... */ }
}

Amplificación: Bucles anidados O(n²) en el peor caso.

Paso 9: WordBreaker::detectEmailOrUrl (2015)

void WordBreaker::detectEmailOrUrl() {
    for (i = mLast; i < mTextSize; i++) { /* ... */ }
}

Amplificación: Bucle O(n) que recorre el texto sin límites.

Paso 10: FontCollection::getGlyphScore (2013)

uint32_t getGlyphScore(...) {
    hb_shape(font.get(), buffer.get(), nullptr, 0);  // O(n²)
}

Amplificación: Llama a hb_shape para cada fuente evaluada.

🔴 Resultado: 70KB → 5 mil millones de iteraciones → 16 segundos de bloqueo → ANR.


⚡ El payload mínimo

Para activar todo el pipeline de amplificación, se necesita un texto con:

  • Longitud: > 70.000 caracteres
  • Estructura: Caracteres que multipliquen los puntos de decisión:
    • # → duplica decisiones de salto
    • / → cada barra añade un punto de ruptura
    • % → expansión UTF-8 → UTF-16
    • → propiedades de salto ambiguas

Payload óptimo:

[PAYLOAD]

Repetido hasta alcanzar 70.000 caracteres.


📅 Cronología (2013–2026)

Año Evento Lo que demuestra
2013FontCollection.cpp, FontFamily.cpp, Layout.cpp creadosEl código vulnerable existe desde hace 13 años
2015OptimalLineBreaker.cpp, Hyphenator.cpp, Measurement.cpp, WordBreaker.cpp creadosEl pipeline de amplificación se completa
2016CVE-2016-2414 — DoS en MinikinGoogle sabía que Minikin era vulnerable y lo arregló
2017GreedyLineBreaker.cpp creadoSe añade una capa más de amplificación
2018LineBreaker.cpp, LayoutCore.cpp, LayoutCache.h creadosEl pipeline alcanza 10 capas
2020Issue #161830416 — crash en LayoutCacheGoogle recibió evidencia, la ignoró
2021Issue #188985643 — SIGSEGV en SamsungGoogle lo ignoró porque no se reproducía en Pixel
2023Chromium 1447465 — ANR en Chrome reportado por SamsungGoogle lo cerró con "who knows"
2026Reporte VRP — 31 vectores documentadosGoogle lo cerró "out of scope"
2026Boletín de julio — sin parcheGoogle sigue ignorando el problema

🔴 13 años de código vulnerable. 10 archivos. 15 funciones. Google no lo ha arreglado.


El pipeline de layout de texto de Android está compuesto por más de 10 archivos y 15 funciones que, en conjunto, forman un sistema de amplificación de DoS. Cada capa multiplica el coste computacional sin aplicar validación de longitud, permitiendo que un texto de 70KB bloquee la UI durante 5-16 segundos.

Google ha tenido 13 años para arreglar esto. Los archivos vulnerables datan de 2013 (FontCollection.cpp, FontFamily.cpp, Layout.cpp) y se han ido añadiendo más capas a lo largo de los años (2015, 2017, 2018). Ninguna de ellas ha sido parcheada para abordar el problema de la validación de entrada.

6. FontFamily.cpp: el origen del SIGSEGV en Samsung

El archivo FontFamily.cpp (creado en 2013) contiene la función getClosestMatch, responsable del SIGSEGV documentado en issue #188985643 (2021, Samsung Galaxy J6+).

FakedFont FontFamily::getClosestMatch(FontStyle style, const VariationSettings& axes) const {
    if (features::typeface_redesign_readonly()) {
        int bestIndex = 0;
        Font* bestFont = mFonts[bestIndex].get();  // ← ¡POSIBLE PUNTERO NULO!
        // ...
    }
    // ...
}

🔴 Problema: mFonts puede estar vacío. En producción, MINIKIN_ASSERT no se activa y el programa accede a memoria inválida → SIGSEGV.

Google respondió:

"I haven't receive any crash report at this function on Pixel devices, and likely it is not actionable to me only with this stack trace."


7. LayoutCache.h: el amplificador silencioso

El archivo LayoutCache.h (creado en 2018) es donde la vulnerabilidad se vuelve persistente.

if (range.getLength() >= CHAR_LIMIT_FOR_CACHE) {
    LayoutPiece piece(text, range, dir, paint, startHyphen, endHyphen);
    // PROCESAMIENTO DIRECTO → SIN CACHÉ → BLOQUEO UI
    return;
}

🔴 Esto significa que:

  1. Los textos largos no se almacenan en caché.
  2. Cada vez que se renderizan, se procesan desde cero.
  3. El bloqueo de UI ocurre cada vez que se muestra el texto.

8. Evidencia forense: los números no mienten

Longitud del texto Iteraciones aprox. Tiempo de bloqueo
1.000 caracteres ~1.000² = 1.000.000 < 100 ms
10.000 caracteres ~10.000² = 100.000.000 ~500 ms - 1 s
70.000 caracteres ~70.000² = 4.900.000.000 5 - 16 segundos

🔴 Un texto de 70KB genera casi 5 mil millones de operaciones en el hilo UI.


9. La cronología de la libminikin (2013–2026)

Año Evento Lo que demuestra
2013 FontFamily.cpp y Layout.cpp creados El código vulnerable existe desde hace 11 años
2016 CVE-2016-2414 — DoS en Minikin Google sabía que Minikin era vulnerable y lo arregló
2020 Issue #161830416 — crash en LayoutCache Google recibió evidencia, la ignoró
2021 Issue #188985643 — SIGSEGV en Samsung Google lo ignoró porque no se reproducía en Pixel
2023 Chromium 1447465 — ANR en Chrome reportado por Samsung Google lo cerró con "who knows"
2026 Reporte VRP — libminikin ANR y crash loop Google lo cerró "out of scope"
Jul 2026 Boletín de seguridad sin parche Google sigue ignorando el problema

🔴 Google ha tenido más de 10 años para arreglar esto. Ha arreglado los casos triviales e ignorado los complejos.


10. La contradicción del VRP: $250 y "out of scope"

El timeline:

  • 20 de enero de 2026: Reporte formal al Android VRP (27 vectores).
  • Google paga $250 por el caso Binder/IPC (A-477279924).
  • 15 de junio de 2026: Google cierra el reporte específico de libminikin como "out of scope" (524288518).
  • 16 de junio de 2026: Apelación enviada para STA-015-DL (CVSS 8.6) Google cierra el reporte de apelación específico de libminikin como "NoT reproducible"
  • 6 de julio de 2026: Boletín de seguridad de julio — sin parche para ningún vector.

💡 La contradicción: Google pagó una recompensa por la investigación, pero cerró el reporte más grave como "out of scope". Si está fuera de alcance, ¿por qué pagan?

Por primera vez, después de más de 400 vulnerabilidades descubiertas y documentadas a lo largo de estos años, he sido recompensado con 250 dólares por la primera parte de mi trabajo. Ni la cantidad, ni la forma en que se ha gestionado la segunda parte de la investigación reflejan el alcance ni la gravedad de las vulnerabilidades documentadas

Matemáticamente, teniendo en cuenta que pueden ser 2.500 millones de dispositivos afectados, eso es 0,0000001 dólares por dispositivo. Es decir, menos de una diezmilésima parte de un céntimo por cada teléfono vulnerable.


11. Conclusión: 10 años de código vulnerable

El análisis del código fuente de AOSP confirma que la vulnerabilidad en libminikin no es un bug aislado, sino un fallo de diseño sistémico presente en el núcleo de Android desde 2013.

🔴 Lo que sabemos ahora:

  • 6 archivos vulnerables en AOSP.
  • 5 capas del pipeline de texto sin validación.
  • 10 años de evidencia documentada (2013 → 2026).
  • Google ha tenido más de 10 años para arreglar esto.
  • Ha arreglado los casos triviales (CVE-2016-2414) e ignorado los complejos.

El 30 de julio de 2026 publicaré el whitepaper completo con los 31 vectores documentados, las trazas nativas completas y el análisis forense del código fuente. Si tu aplicación usa TextView, ya estás avisado.

📌 Para desarrolladores (mitigación inmediata):

private static final int MAX_SAFE_LENGTH = 8192;
String safe = text.length() > MAX_SAFE_LENGTH 
    ? text.substring(0, MAX_SAFE_LENGTH) + "…" 
    : text;
textView.setText(safe);

12. Referencias y enlaces

Artículos del investigador

Issues públicos de Google

Código fuente de AOSP

🔥 1.6 Minikin es un síntoma de una brecha de resiliencia sistémica

La vulnerabilidad de libminikin no es un bug aislado. Es un síntoma de una brecha de resiliencia sistémica en el framework de Android: la ausencia de un mecanismo estandarizado de "safe fallback" para manejar datos que superan los límites del sistema.

El mismo patrón aparece en al menos diez componentes distintos del sistema operativo Android:

Componente Modo de fallo Impacto
FragmentManager No captura TransactionTooLargeException Bucle de crash permanente
TaskPersister Persiste el estado corrupto en disco El bucle de crash sobrevive a reinicios
SystemUI Lee TaskPersister sin validación Bucle de crash → pantalla de bloqueo
Print Service Sin validación del tamaño del documento SystemUI crash
ClipboardManager Serializa todo el contenido del portapapeles sin límites SystemUI crash
Binder Límite de 1MB sin safe fallback Crash de la app
libminikin.so Sin validación de longitud de entrada ANR (5–16s)
LayoutCache CHAR_LIMIT_FOR_CACHE fuerza el procesamiento directo Amplifica el DoS
BitmapCache Sin validación del tamaño de imagen (issue #531319203) Crash de system_server → bucle de arranque
MediaSession Sin validación del tamaño de la carátula Crash de system_server → bucle de arranque

🔴 Diez componentes. Diez modos de fallo diferentes. El mismo patrón subyacente: sin validación de entrada antes del procesamiento, y sin safe fallback cuando se superan los límites.

Esto no es un bug de Minikin. Es un fallo de diseño del framework de Android que ha estado presente durante más de una década, afectando a componentes que van desde la capa de aplicación hasta el servidor central del sistema. Google ha añadido capas de complejidad a Minikin sin abordar la causa raíz, y ha descartado los informes de estos problemas como "fuera de alcance" o "comportamiento intencionado" durante años.

Minikin no es la enfermedad. Es un síntoma.



Manuel Garcia Peña (Lostmon) · lostmon@gmail.com · lostmon.blogspot.com
Whitepaper completo: 30 de julio de 2026
 

Browse

About:Me

My blog:http://lostmon.blogspot.com
Mail:Lostmon@gmail.com
Lostmon Google group
Lostmon@googlegroups.com

La curiosidad es lo que hace
mover la mente...

Friends