Un texto de 70.000 caracteres puede congelar casi cualquier app de Android durante 10–16 segundos. No es corrupción de memoria. Es un ataque algorítmico que explota la complejidad O(n²) del motor de diseño de texto nativo de Android. Y lo peor: cualquier aplicación con un TextView es vulnerable.
He documentado 31 vectores en 9 empresas, incluyendo Google, Meta, Microsoft, Mozilla, Opera, DuckDuckGo, Brave, Tor Project y Xiaomi. El fallo reside en libminikin.so, el motor de diseño de texto de Android, y afecta a todos los dispositivos con Android 9 a 16.
1. El problema: Algoritmos que se vuelven en tu contra
libminikin.so implementa el algoritmo de salto de línea óptimo (Knuth-Plass) en la función LineBreakOptimizer::computeBreaks. Este algoritmo tiene complejidad O(n²) en el peor caso: si duplicas la longitud del texto, el tiempo de procesamiento se multiplica por cuatro.
Para un texto normal de 200 caracteres, el tiempo es imperceptible. Pero para uno de 70.000 caracteres con la estructura adecuada, el algoritmo ejecuta millones de operaciones y bloquea el hilo de la interfaz durante más de 10 segundos.
#, /, % y € multiplica el número de "candidatos a salto de línea", y el algoritmo se dispara.
2. La estructura del ataque: Caracteres que multiplican el caos
| Carácter | Función en la URL | Efecto en libminikin |
|---|---|---|
# |
Fragmento (separador lógico) | Duplica las decisiones de salto de línea |
/ |
Separador de rutas | Cada barra es un punto de ruptura → O(n²) con el número de barras |
% |
Codificación porcentual | Expansión UTF-8 → UTF-16, fragmenta la cadena |
€ |
Carácter multibyte | Propiedades de salto de línea ambiguas → el algoritmo explora ambas opciones |
El patrón óptimo repite miles de veces. Cada 9 bytes generan múltiples puntos de decisión y el algoritmo explota.
Resultado: La UI se congela durante 10–16 segundos. El stack trace nativo confirma que libminikin es el único responsable.
4. Vectores de ataque: Más allá de los navegadores
El fallo no se limita a los navegadores. Cualquier aplicación que muestre texto en un TextView sin truncar es un vector de ataque:
| Componente / Aplicación | ¿Muestra URL larga? | Vulnerabilidad observada |
|---|---|---|
| Chrome (menú contextual) | Sí | ✅ ANR de 5+ segundos |
| Firefox (diálogo externo) | Sí | ✅ ANR de 16 segundos (enlace real de Gmail) |
| Opera / Opera Mini | Sí | ✅ Crash con TransactionTooLargeException |
| DuckDuckGo | Sí (historial) | ✅ Bucle de cierre permanente |
| Gmail (vista previa de enlace) | Sí | ⚠️ Potencial |
| WhatsApp (vista previa de enlace) | Sí | ⚠️ Potencial |
| Cualquier app con TextView | Sí | ✅ Confirmado con PoC |
5. El vector crítico: STA-015-DL (SystemUI crash loop)
La cadena de ataque STA-015-DL demuestra que el problema trasciende las aplicaciones y afecta a SystemUI, la interfaz del sistema:
- La víctima hace clic en un archivo HTML alojado en Google Drive.
- Google Drive lanza un
Intent.ACTION_VIEWcon una URL malformada. - La URL bypasea todas las validaciones del navegador porque llega a través de
callingPackage=com.google.android.apps.docs(certificado por Android). - La URL corrompe el estado de
TaskPersisteren disco. - SystemUI intenta leer el estado →
DeadObjectException→ bucle de cierre. - Recuperación: reinicio forzado del dispositivo.
Evidencia de campo: Un dispositivo en producción (Xiaomi Redmi Note 14 5G, Android 16) registró 85 SystemUI crashes en 4 días, con un tiempo mínimo de supervivencia de 374ms.
6. Stack traces nativos (evidencia forense)
Chrome – Menú contextual (24 de mayo de 2026)
Firefox – Diálogo de aplicación externa (10 de junio de 2026)
7. Mitigación
7.1 A nivel de framework (Google/AOSP)
Google debería implementar un límite de entrada antes de ejecutar el algoritmo O(n²):
7.2 A nivel de aplicación (inmediato)
Los desarrolladores pueden truncar el texto antes de mostrarlo en un TextView:
8. Conclusión
La complejidad O(n²) en libminikin convierte una URL de 70 KB en un arma de denegación de servicio que puede congelar cualquier app Android que la muestre en un TextView sin truncar. No es corrupción de memoria, es explotación algorítmica — usar la propia complejidad del sistema en su contra.
Google ha empezado a moverse (LargePayloadSupport, savedstate 1.5.0), pero no hay parche público para libminikin a fecha de junio de 2026. La comunidad y los desarrolladores pueden aplicar mitigaciones inmediatas truncando el texto antes de mostrarlo.
libminikin.so fue presentado el 15 de junio de 2026.
El reporte fue cerrado con la clasificación "Out of Scope". Posteriormente, el 21 de junio de 2026, se presentó una apelación acompañada de evidencia adicional, incluyendo informes ANR, trazas de pila nativas y resultados obtenidos durante la investigación. La apelación fue finalmente cerrada con la clasificación "Not Reproducible".
El análisis técnico y la evidencia presentada en este artículo reflejan los resultados obtenidos durante la investigación independiente realizada por el autor y se publican con fines de investigación, documentación y mejora de la seguridad.
Apéndice: Nuevo caso reproducible (28 de junio de 2026) – Google App / Android Translate
Desde la publicación inicial de este análisis, he seguido monitorizando el comportamiento del sistema. El pasado 28 de junio de 2026 apareció un segundo caso, completamente independiente del anterior, que no hace sino reforzar la hipótesis planteada.
Mientras que el primer vector documentado en la sección 4 utilizaba StaticLayout y breakLineOptimal(), este nuevo caso utiliza DynamicLayout y breakLineGreedy() durante el procesamiento de texto iniciado por Android System Intelligence mediante un android.intent.action.TRANSLATE.
Secuencia observada
Usuario selecciona texto ▼ Android System Intelligence ▼ Google App (ImplicitTranslateSearchEntrypointInternal) ▼ SpannableStringBuilder.replace() ▼ DynamicLayout.reflow() ▼ libminikin::breakLineGreedy() ▼ getPrevWordBreakForCache() ▼ ANR (Application Not Responding)
Cinco ANR consecutivos
El mismo texto produjo cinco ANR consecutivos en aproximadamente catorce minutos. Todos compartían el mismo patrón:
- Main thread bloqueado durante más de 6 segundos.
- Entrada por
DynamicLayout. - Ejecución dentro de
libminikin. - Funciones
getPrevWordBreakForCache()ogetNextWordBreakForCache().
Después de varios ANR, el proceso también comenzó a finalizar con TransactionTooLargeException durante la restauración del estado de la actividad, un efecto secundario posterior al bloqueo del hilo principal que conecta directamente con el fenómeno de amplificación estructural descrito en la sección 5.
Comparación con el caso de Edge
| Aspecto | Microsoft Edge | Google App |
|---|---|---|
| Layout | StaticLayout | DynamicLayout |
| Algoritmo | breakLineOptimal() | breakLineGreedy() |
| Desencadenante | Re‑layout tras cambio del sistema | Reflow tras modificar texto |
| Entrada | Omnibox | Traductor integrado |
| ANR | 1 ANR | 5 ANR consecutivos |
Aunque las rutas son diferentes, ambas convergen en el mismo componente del framework:
StaticLayout
│
├──► breakLineOptimal()
│
DynamicLayout
│
├──► breakLineGreedy()
│
▼
libminikin
│
├──► getPrevWordBreakForCache()
└──► getNextWordBreakForCache()
Implicaciones
Este segundo caso reduce significativamente la probabilidad de que el problema sea específico de una aplicación concreta. Ahora existen al menos dos aplicaciones independientes, con dos flujos de ejecución distintos y dos algoritmos de line breaking diferentes, que terminan bloqueando el hilo principal dentro de libminikin sobre la misma versión de Android 16 (BuildId 4fabe53671b5ead88314c00a1fd6d67d).
En otras palabras, la evidencia ya no apunta únicamente a un problema asociado a Microsoft Edge, sino a un comportamiento común del framework de composición de texto de Android cuando procesa determinadas entradas.
Este segundo caso demuestra que la Amplificación de Texto Estructurado no es un fenómeno limitado a un flujo de ejecución concreto, sino que puede manifestarse en distintos puntos del framework siempre que el texto pase por las capas de composición y medida de libminikin.
— Añadido el 28 de junio de 2026
Análisis interno del pipeline de layout de Minikin
Tras correlacionar los múltiples ANR obtenidos con el código fuente de AOSP,
la investigación apunta a que las funciones
getPrevWordBreakForCache() y
getNextWordBreakForCache()
probablemente no constituyen la causa raíz del problema, sino que forman
parte de un pipeline de composición de texto mucho más amplio.
DynamicLayout / StaticLayout
│
▼
MeasuredText
│
▼
LayoutSplitter
│
┌────────┴────────┐
▼ ▼
getPrevWordBreakForCache()
getNextWordBreakForCache()
│
▼
LayoutCache
│
▼
LayoutPiece
│
▼
WordBreaker / Hyphenation
│
▼
breakLineGreedy() / breakLineOptimal()
El análisis del código fuente de AOSP muestra que
LayoutSplitter utiliza
getPrevWordBreakForCache() y
getNextWordBreakForCache()
para determinar los límites del fragmento de texto que será almacenado
dentro de LayoutCache.
Además, la implementación de LayoutCache indica que cuando
el texto supera un determinado tamaño (CHAR_LIMIT_FOR_CACHE),
la caché deja de reutilizarse y el sistema debe reconstruir un nuevo
LayoutPiece, recalculando el layout completo.
LayoutCache::getOrCreate()
│
├── Cache hit
│ │
│ ▼
│ Reutiliza LayoutPiece
│
└── Cache miss
│
▼
Construcción de LayoutPiece
│
▼
Glyph shaping
│
▼
Word breaking
│
▼
Cálculo del layout
Esto conduce a una nueva hipótesis de trabajo.
Más que encontrarnos ante un fallo localizado en
getPrevWordBreakForCache(),
es posible que los ANR sean consecuencia de la reconstrucción repetitiva
del pipeline completo de composición de texto cuando la reutilización de
la caché deja de ser efectiva.
Esta hipótesis encaja con los casos observados durante la investigación:
- Microsoft Edge alcanza este pipeline mediante
StaticLayoutybreakLineOptimal(). - Google Translate lo hace mediante
DynamicLayoutybreakLineGreedy(). - Ambas rutas convergen finalmente en la misma infraestructura interna de Minikin encargada de la caché y del cálculo de límites de palabra.
Un aspecto especialmente relevante es que Google ha realizado múltiples modificaciones en esta parte del código fuente de Minikin a lo largo de los últimos años, incluyendo optimizaciones específicas del sistema de caché y del rendimiento del motor de layout. Esto sugiere que se trata de una zona considerada crítica desde el punto de vista del rendimiento.
En el momento de redactar este artículo, esta explicación constituye una hipótesis fundamentada en la correlación entre los stack traces de los ANR y el análisis del código fuente de AOSP. Será necesario un análisis más profundo —mediante perfilado o reproducción controlada— para determinar si la causa raíz corresponde a una invalidación de la caché, un problema de complejidad algorítmica o cualquier otro cuello de botella interno del motor de composición de texto de Minikin.
El 30 de julio de 2026 publicaré el whitepaper completo con los 31 vectores documentados y la evidencia forense completa. Si tu aplicación usa TextView, ya estás avisado.
Investigador independiente · lostmon@gmail.com
Blog: lostmon.blogspot.com