Extensión de etiquetado de memoria de Arm (MTE)

¿Por qué usar MTE?

Los errores de seguridad de la memoria, que son errores en el manejo de la memoria en lenguajes de programación nativos, son problemas de código comunes. Estos generan vulnerabilidades de seguridad y problemas de estabilidad.

Armv9 introdujo la extensión de etiquetado de memoria (MTE) de Arm, una extensión de hardware que te permite detectar errores de uso luego de la liberación y de desbordamiento de búfer en tu código nativo.

Verifica la compatibilidad

A partir de Android 13, ciertos dispositivos son compatibles con MTE. Para verificar si tu dispositivo tiene MTE habilitada, ejecuta el siguiente comando:

adb shell grep mte /proc/cpuinfo

Si el resultado es Features : [...] mte, tu dispositivo tiene MTE habilitada.

Algunos dispositivos no habilitan MTE de forma predeterminada, pero permiten que los desarrolladores los reinicien con MTE habilitada. Esta es una configuración experimental que no se recomienda para el uso normal, ya que puede disminuir el rendimiento o la estabilidad del dispositivo, pero que puede ser útil para el desarrollo de apps. Para acceder a este modo, ve a Opciones para desarrolladores > Ext. de etiquetado de memoria en la app de Configuración. Si esta opción no está presente, el dispositivo no admite la habilitación de MTE de esta manera.

Modos de operación de MTE

MTE admite dos modos: SYNC y ASYNC. El modo SYNC proporciona mejor información de diagnóstico y, por lo tanto, es más adecuado para fines de desarrollo, mientras que el modo ASYNC tiene un alto rendimiento que permite su habilitación para apps que ya se hayan lanzado.

Modo síncrono (SYNC)

Este modo está optimizado para la depuración por sobre el rendimiento y se puede usar como una herramienta de detección de errores precisa; se debe tener en cuenta que la sobrecarga de rendimiento será mayor. Cuando MTE SYNC está habilitado, también actúa como mitigación de seguridad.

En una discrepancia de etiqueta, el procesador finaliza el proceso en la instrucción de carga o almacenamiento incorrecta con SIGSEGV (con si_code SEGV_MTESERR) y la información completa sobre el acceso a la memoria y la dirección con errores.

Este modo resulta útil durante las pruebas como una alternativa más rápida a HWASan que no requiere volver a compilar el código, o bien durante la producción cuando la app representa una superficie vulnerable a ataques. Además, cuando el modo ASYNC (descrito a continuación) encuentra un error, se puede obtener un informe de errores preciso usando las APIs de entorno de ejecución para cambiar la ejecución al modo SYNC.

Por otra parte, cuando se ejecuta en modo SYNC, el asignador de Android registra el seguimiento de pila de cada asignación y desasignación, y lo usa para proporcionar mejores informes de errores que incluyen una explicación de un error de memoria, como usar después de liberar o el desbordamiento de búfer, y los seguimientos de pila de los eventos de memoria relevantes (consulta Cómo interpretar los informes de MTE para obtener más detalles). Esos informes proporcionan más información contextual y hacen que los errores sean más fáciles de rastrear y corregir que en el modo ASYNC.

Modo asíncrono (ASYNC)

Este modo está optimizado para el rendimiento por sobre la precisión de los informes de errores y se puede usar para detectar, con una baja sobrecarga, errores de seguridad de memoria. En una discrepancia de etiqueta, el procesador continúa la ejecución hasta la entrada de kernel más cercana (como una llamada de sistema o interrupción del temporizador), donde finaliza el proceso con SIGSEGV (código SEGV_MTEAERR) sin registrar la dirección o el acceso a la memoria con errores.

Este modo es útil para mitigar las vulnerabilidades de seguridad de la memoria durante la producción en bases de código probadas donde se sabe que la densidad de errores de seguridad de la memoria es baja, lo que se logra usando el modo SYNC durante las pruebas.

Cómo habilitar MTE

Para un solo dispositivo

Durante la experimentación, los cambios de compatibilidad de la app se pueden usar para establecer el valor predeterminado del atributo memtagMode para una aplicación que no especifica ningún valor en el manifiesto (o especifica "default").

Puedes encontrarlos en Sistema > Avanzado > Opciones para desarrolladores > Cambios en la compatibilidad de la app en el menú de configuración global. La configuración de NATIVE_MEMTAG_ASYNC o NATIVE_MEMTAG_SYNC habilita MTE para una aplicación en particular.

Como alternativa, esto se puede configurar usando el comando am de la siguiente manera:

  • Para el modo SYNC: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • Para el modo ASYNC: $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

En Gradle

Puedes habilitar MTE para todas las compilaciones de depuración de tu proyecto de Gradle si colocas

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application android:memtagMode="sync" tools:replace="android:memtagMode"/>
</manifest>

en app/src/debug/AndroidManifest.xml. Esto anulará el memtagMode de tu manifiesto con sincronización para las compilaciones de depuración.

Como alternativa, puedes habilitar MTE para todas las compilaciones de un buildType personalizado. Para ello, crea tu propio buildType y coloca el XML en app/src/<name of buildType>/AndroidManifest.xml.

Para un APK en cualquier dispositivo compatible

MTE está inhabilitada de forma predeterminada. Si quieres que las apps usen MTE, puedes configurar android:memtagMode en la etiqueta <application> o <process> en AndroidManifest.xml.

android:memtagMode=(off|default|sync|async)

Cuando se establece en la etiqueta <application>, el atributo afecta a todos los procesos que usa la aplicación y se puede anular en procesos individuales si configuras la etiqueta <process>.

Ejecuta tu app

Una vez que habilites MTE, usa y prueba tu app con normalidad. Si se detecta un problema de seguridad de la memoria, tu app falla con una tombstone que se ve de la siguiente manera (ten en cuenta SIGSEGV con SEGV_MTESERR para SYNC o SEGV_MTEAERR para ASYNC):

pid: 13935, tid: 13935, name: sanitizer-statu  >>> sanitizer-status <<<
uid: 0
tagged_addr_ctrl: 000000000007fff3
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
x28 0000000000000000  x29 0000007fe8191b70
lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

backtrace:
      #00 pc 00000000000010c0  /system/bin/sanitizer-status (test_crash_malloc_uaf()+40) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #01 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #02 pc 00000000000019cc  /system/bin/sanitizer-status (main+1032) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000487d8  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+96) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)

deallocated by thread 13935:
      #00 pc 000000000004643c  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+688) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 00000000000421e4  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+212) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 00000000000010b8  /system/bin/sanitizer-status (test_crash_malloc_uaf()+32) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

allocated by thread 13935:
      #00 pc 0000000000042020  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::allocate(unsigned long, scudo::Chunk::Origin, unsigned long, bool)+1300) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 0000000000042394  /apex/com.android.runtime/lib64/bionic/libc.so (scudo_malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 000000000003cc9c  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #03 pc 00000000000010ac  /system/bin/sanitizer-status (test_crash_malloc_uaf()+20) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #04 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
Learn more about MTE reports: https://source.android.com/docs/security/test/memory-safety/mte-report

Consulta Cómo interpretar los informes de MTE en la documentación de AOSP para obtener más detalles. También puedes depurar tu app con Android Studio, y el depurador se detendrá en la línea que está provocando el acceso no válido a la memoria.

Usuarios avanzados: Cómo usar MTE en tu propio asignador

Si deseas usar MTE para la memoria no asignada a través de los asignadores normales del sistema, debes modificar tu asignador para etiquetar la memoria y los punteros.

Las páginas de tu asignador deben asignarse usando PROT_MTE en la marca prot de mmap (o mprotect).

Todas las asignaciones etiquetadas deben estar alineadas con 16 bytes, ya que las etiquetas solo se pueden asignar a fragmentos de 16 bytes (también conocidos como gránulos).

Luego, antes de mostrar un puntero, debes usar la instrucción IRG para generar una etiqueta aleatoria y almacenarla en el puntero.

Usa las siguientes instrucciones para etiquetar la memoria subyacente:

  • STG: Etiqueta un solo gránulo de 16 bytes.
  • ST2G: Etiqueta dos gránulos de 16 bytes.
  • DC GVA: Etiqueta la línea de caché con la misma etiqueta.

Como alternativa, las siguientes instrucciones también inicializan la memoria en cero:

  • STZG: Etiqueta e inicializa en cero un solo gránulo de 16 bytes.
  • STZ2G: Etiqueta e inicializa en cero dos gránulos de 16 bytes.
  • DC GZVA: Etiqueta e inicializa en cero la línea de caché con la misma etiqueta.

Ten en cuenta que estas instrucciones no son compatibles con CPUs más antiguas, por lo que debes ejecutarlas de manera condicional cuando MTE esté habilitada. Puedes verificar si MTE está habilitada para tu proceso de la siguiente manera:

#include <sys/prctl.h>

bool runningWithMte() {
      int mode = prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0);
      return mode != -1 && mode & PR_MTE_TCF_MASK;
}

La implementación de Scudo puede resultarte útil como referencia.

Más información

Puedes obtener más información en la MTE User Guide for Android OS (Guía del usuario de MTE para SO Android), escrita por Arm.