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. 6 ARCHIVOS. 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.

  • 6 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
}

🔴 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 se convierten 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).
  • 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?


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


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