Un artículo que presenta el asignador de memoria Scudo.

1. Antecedentes

En los primeros días de Android, jemalloc se usaba como el asignador de memoria nativo predeterminado, pero a partir de R, Scudo reemplazó a jemalloc como el asignador de memoria predeterminado en el modo de configuración no esbelto (el asignador de memoria predeterminado en el modo esbelto sigue siendo jemalloc).

Con la popularidad de las máquinas de 64 bits y la gran RAM, los cuellos de botella de la memoria virtual y la memoria física se relajan constantemente, dando así al sistema más opciones para tener en cuenta otras características dentro de un rango razonable de rendimiento. Entre todos los problemas de seguridad, las intrusiones causadas por vulnerabilidades de memoria representan más de la mitad, por lo que si se puede resistir las intrusiones en Allocator, se reducirá en gran medida la cantidad de problemas de seguridad y también se introdujo Scudo.

2. Diseño e implementación

Scudo está diseñado pensando en la seguridad, pero pretende lograr un buen equilibrio entre seguridad y rendimiento. Solo desde una perspectiva de rendimiento, es posible que Scudo no supere a jemalloc. Aunque su estrategia de asignación es más simple, algunas estrategias implementadas por seguridad harán que pierda algo de rendimiento.

1. Componente del escudo

El asignador de Scudo se compone principalmente de cuatro componentes: primario, secundario, TSD y cuarentena.

Asignador primario: asigna bloques de memoria más pequeños de manera más rápida y eficiente al dividir el área de memoria reservada en bloques del mismo tamaño. Actualmente, se implementan dos asignadores primarios, dirigidos a arquitecturas de 32 y 64 bits, respectivamente. Se puede configurar mediante opciones de tiempo de compilación.

Para Android R/S de 64 bits, el asignador primario se muestra en la Figura 1. Durante la inicialización, se asignará un espacio de tamaño 256M*33, que se dividirá en 33 regiones, marcadas respectivamente con classid 0~32. El tamaño de cada región es 256M y, por razones de seguridad, se dejarán aleatoriamente de 1 a 16 páginas vacías en el encabezado. Además, cada región se divide en bloques de memoria de un tamaño específico, como la región de clase 1 se divide en 32 Bytes, la región de clase 2 se divide en 48 Bytes, la región de clase 32 es 64 K, etc. (la región de clase 0 se usa para almacenar memoria metadatos de gestión). De esta manera, al asignar memoria pequeña, primero verificará si hay un bloque de memoria libre en una región del tamaño apropiado. De lo contrario, se asignará en una región de nivel superior. Cuando no haya uno adecuado en la región más alta región de nivel, será asignada por el asignador secundario.

Además, Primary Allocator proporciona un mecanismo de caché para acelerar la asignación de memoria. Cuando un subproceso asigna memoria, encontrará memoria libre adecuada a través de la matriz de fragmentos del objeto SizeClassAllocatorLocalCache en el TSD del subproceso. Pero el tamaño de la matriz de fragmentos es limitado (el valor predeterminado es 28) y, cuando se agotan, es necesario reponer los bloques de memoria libres. El suplemento se obtiene directamente de la lista libre de la región (los datos detallados de la lista libre se almacenan en regioninfo). Cuando no haya suficientes objetos libres en la lista libre, se ampliará el área libre de la región.

Asignador secundario: más lento que el primario, asigna mayor memoria a través del mapeo de memoria del sistema operativo subyacente. Y los bloques de memoria asignados a través de Secundaria están rodeados por páginas de protección en ambos extremos.

Para Android R/S de 64 bits, el asignador secundario se utiliza principalmente para asignar memoria de más de 64 K. Utiliza directamente mmap para asignar un nuevo VMA. Para acelerar la asignación, también se diseña un caché correspondiente (MapAllocatorCache), que puede almacenar en caché hasta 32 VMA de no más de 2 M internamente.

TSD: Define cómo opera el caché local de cada hilo. Actualmente existen dos implementaciones de modelos: el modelo exclusivo, en el que cada subproceso tiene su propio caché; o el modelo compartido, en el que los subprocesos comparten un grupo de caché de tamaño fijo.

Para andoridR/S de 64 bits, se utiliza el modelo compartido y el grupo TSD tiene solo dos objetos TSD, cada objeto TSD contiene un objeto SizeClassAllocatorLocalCache y QuarantineCache respectivamente.

Cuarentena : proporciona un método para retrasar la liberación de memoria para evitar la reasignación inmediata de bloques de memoria. Una vez que se alcanza un cierto criterio de tamaño, se recuperará el bloque de memoria. Se trata esencialmente de una lista enlazada gratuita retrasada, que puede ayudar a aliviar algunas situaciones de uso después de la liberación. Esta característica es bastante costosa en términos de rendimiento y uso de memoria, se controla principalmente mediante opciones de tiempo de ejecución y está deshabilitada de forma predeterminada.

Si se configura Cuarentena, los bloques que cumplan con el límite de tamaño se aislarán temporalmente cuando se libere la memoria y el estado se establecerá en Cuarentena en lugar de Disponible, lo que permitirá detectar UAF. Primero intente colocarlo en el QuarantineCache correspondiente al hilo TSD. Si el tamaño del caché de cuarentena local excede el estándar, coloque todos los bloques de memoria en el caché de cuarentena local en el caché de cuarentena global en el front-end. Si la caché de cuarentena global también excede el límite, el reciclaje se libera a TSD->SizeClassAllocatorLocalCache.

2. Encabezado de fragmento

La siguiente es la información detallada del encabezado del fragmento, los siguientes números representan los bits ocupados por cada campo, que suman 64 bits, que son 8 bytes.

ClassId: indica que el bloque de memoria se asigna desde la identificación de la región en el primario. Un ClassId de 0 indica que se asigna desde el secundario.

Estado: Indica el estado actual del bloque de memoria, 0 Disponible, 1 Asignado, 2 En Cuarentena.

OriginOrWasZeroed: cuando se asigna Estado, indica el método mediante el cual se produce la asignación, como nuevo o malloc.

SizeOrUnusedBytes: cuando ClassId es un número positivo, indica el tamaño asignado. Cuando ClassId es 0, indica el tamaño de bytes no utilizados.

Desplazamiento: el desplazamiento del encabezado del fragmento en el bloque de memoria.

Suma de comprobación: suma de comprobación, utilizada para detectar si el encabezado del fragmento está dañado.

Cuando una dirección de memoria se libera mediante liberación/eliminación, la dirección debe pasar por varias comprobaciones para garantizar que no se destruya durante el uso. Las pruebas por las que debe pasar un fragmento se enumeran a continuación en orden cronológico.

1) Detección de alineación: la dirección debe estar alineada en 16 bytes. Si se utiliza un número largo no alineado como puntero, aquí se puede detectar un error de puntero desalineado.

2) Detección de suma de verificación: el número de suma de verificación se calculará nuevamente durante la desasignación y se comparará con la suma de verificación guardada en el encabezado del fragmento. Si los dos no son iguales, se informará un error de encabezado de fragmento dañado.

3) Detección de estado: si el estado del encabezado del fragmento no es Asignado, indica que esta memoria no debe liberarse en este momento. Es probable que se trate de una doble liberación y se informará un error de estado del fragmento no válido.

4) Detección de tipo: si el método de asignación no coincide con el método de liberación, se informará un error de discrepancia de tipo de asignación (siempre que la opción DeallocTypeMismatch esté activada).

5) Detección de tamaño: si el tamaño durante el lanzamiento no es igual al tamaño en el encabezado del fragmento, se informará un error de eliminación de tamaño no válido (siempre que la opción DeleteSizeMismatch esté activada).

Los pasos de detección anteriores durante la liberación de memoria pueden corresponder a los errores típicos en la documentación oficial de Scudo de Android. El siguiente es un análisis de los mensajes de error típicos en la documentación oficial de Scudo de Android: 1) encabezado de fragmento dañado: la verificación de la suma de verificación del encabezado del bloque fallido. Hay dos posibles razones: el encabezado del bloque está parcial o completamente cubierto, o el puntero pasado a la función no es un bloque.

2) carrera en encabezado de bloque: dos subprocesos diferentes intentarán manipular el mismo encabezado de bloque al mismo tiempo. Este síntoma generalmente es causado por una condición de carrera o, en general, por falta de bloqueo mientras se realizan operaciones en el bloque.

3) estado de fragmento no válido: el fragmento no está en el estado esperado para la operación especificada, por ejemplo, está en el estado no asignado cuando se intenta liberar el fragmento, o no está en el estado de aislamiento cuando se intenta reclamar el fragmento. Double free es una causa típica de este error.

4) puntero desalineado: aplique requisitos de alineación básicos: 8 bytes en plataformas de 32 bits y 16 bytes en plataformas de 64 bits. Si el puntero pasado a la función no encaja en esas funciones, el puntero pasado a una de las funciones no estará alineado.

5) No coincide el tipo de asignación: cuando esta opción está habilitada, la función de desasignación llamada en el bloque debe ser del mismo tipo que la función llamada para asignar el bloque. Los tipos incoherentes pueden provocar problemas de seguridad.

6) eliminación de tamaño no válido: si utiliza el operador de eliminación que cumple con el estándar C++14, después de habilitar la verificación opcional, el tamaño pasado al desasignar el bloque será inconsistente con el tamaño solicitado al asignar el bloque. Esto generalmente se debe a un problema del compilador o a una confusión de tipos en el objeto que se está desasignando. 7) Límite de RSS agotado: se ha excedido el límite de tamaño de RSS especificado opcionalmente.

3. Proceso de asignación de memoria

El proceso de asignación de memoria en Scudo se muestra en la siguiente figura:

1. Calcule el tamaño necesario que realmente debe asignarse desde el asignador en función del tamaño del parámetro de entrada (el tamaño del parámetro de entrada está alineado con la alineación más el tamaño mayor entre la alineación y el tamaño del encabezado del fragmento). Si NeededSize excede la clase de tamaño más grande del asignador primario , asígnelo desde el asignador secundario. Vaya al paso 2; de lo contrario, vaya al paso 4.

2. Asigne desde el asignador secundario: alinee el tamaño necesario + el tamaño del encabezado del bloque grande de acuerdo con el tamaño de PageSize para obtener RoundedSize. Si RoundedSize se puede obtener correctamente de MapAllocatorCache, obtenga directamente el bloque de memoria en MapAllocatorCache y vaya al paso 6. De lo contrario vaya al paso 3

3. Se requiere una máquina virtual con un tamaño de mmap de RoundedSize + 2Page, con una página antes y después, similar a RedZone. Tenga en cuenta que el permiso de mmap esta vez es PROT_NONE. Después de eso, se omitirá un tamaño de página y se volverá a asignar en modo RW con el tamaño de RoundedSize. Finalmente, omita el encabezado LargeBlock, obtenga la dirección del bloque de memoria y continúe con el paso 6.

4. Asignar desde el asignador primario: el primario administra la región de cada clase de tamaño. Al realizar la asignación, primero obtenga el objeto SizeClassAllocatorLocalCache en el TSD correspondiente al hilo e intente obtener el bloque de memoria libre correspondiente a la clase de tamaño de SizeClassAllocatorLocalCache. Si el La clase de tamaño especificada existe en SizeClassAllocatorLocalCache. Si hay un bloque de memoria libre, obtenga la dirección del bloque de memoria y vaya al paso 6.

5. Si SizeClassAllocatorLocalCache en TSD no especifica el bloque de memoria libre de la clase de tamaño, regrese al Primario y use RW de la Región (aunque el proceso de inicialización de la Región se ha asignado, es PROT_NONE) y asigne la memoria de nuevamente el tamaño apropiado, llenándolo en la lista libre correspondiente de la región (con TranserBatch como nodo), tome un TransferBatch de la lista libre y complételo en SizeClassAllocatorLocalCache. Después de llenar SizeClassAllocatorLocalCache, puede tomar una memoria libre bloquear y continuar con el siguiente paso. Si todos los bloques de memoria libres en la región actual se han agotado y no hay bloques de memoria libres, se asignarán a una región de nivel superior. Si no hay bloques de memoria adecuados en el nivel más alto, se asignarán desde la región actual. Asignador secundario.

6. Después de obtener el bloque de memoria libre, continúe completando el encabezado del fragmento, omita el encabezado del fragmento y devuelva la dirección de memoria al usuario.

4. Proceso de liberación de memoria

El proceso de liberación de memoria en Scudo se muestra en la siguiente figura:

1. Obtenga el encabezado del fragmento del bloque de memoria y realice verificaciones relevantes, como: verificación de suma de verificación y si el tipo de asignación coincide: malloc/free, eliminar/nuevo, y luego obtenga el tamaño asignado por el usuario según el parámetro ingresado ptr para eliminar el tamaño que no coincide, verificar, etc.

2. Hay dos formas de liberar la memoria, es decir, si es necesario ponerla en cuarentena. Si es así, vaya al paso 3 para ponerla en cuarentena. De lo contrario, libérela nuevamente a Primaria o Secundaria y vaya al Paso 4.

3. Si se configura Cuarentena y los bloques de memoria que cumplen con el límite de tamaño cuando se liberan se aislarán temporalmente y el estado se establece en Cuarentena en lugar de Disponible, se puede detectar UAF. Primero intente colocarlo en el objeto QuarantineCache (tamaño de caché de cuarentena local) correspondiente al hilo TSD. Si el tamaño de la caché de cuarentena local excede el estándar, coloque todos los bloques de memoria en la caché de cuarentena local en la caché de cuarentena global. Si el caché de cuarentena global también excede el límite, se reciclará y se liberará al primario.

4. Determine si publicarlo en Primario o Secundario según el classid del encabezado del fragmento. ID de clase = 0 significa que se asigna desde el asignador secundario.

5. Para el asignador secundario, si el bloque de memoria liberado es inferior a 2 M, primero intentará colocarlo en la matriz CachedBlock de MapAllocatorCache. Si se coloca con éxito en la matriz, la hora actual se utilizará como la hora en que esto se libera el bloque de memoria, y también se basará en el tiempo de gc configurado. El intervalo libera los bloques de memoria en la matriz que han envejecido más que el intervalo de gc (a través de madvise (MADV_DONTNEED) en lugar de desasignar). En otro caso, si se encuentra que la matriz está llena cuando se vuelve a colocar la matriz CachedBlock, todos los bloques de memoria en la matriz se desasignarán (la matriz se borrará solo cuando la matriz esté llena 4 veces en total), y la matriz actual El bloque liberado también se desasignará.

6. Para el asignador primario, primero obtenga el TSD correspondiente al hilo actual y obtenga el objeto SizeClassAllocatorLocalCache en el TSD. Si SizeClassAllocatorLocalCache no está satisfecho, colóquelo directamente en el caché y libre termina aquí. Si SizeClassAllocatorLocalCache está lleno, la mitad de los bloques de memoria caché en el caché se devolverán a la lista libre de la región primaria correspondiente utilizando TransferBatch como portador y luego se juzgará si se necesita madvise para liberar el pss ocupado por el libre. bloques de memoria en la lista libre. La base principal para juzgar es: si el intervalo entre la hora actual y la última versión de pss es suficiente, y si el tamaño del bloque de memoria en la lista libre es lo suficientemente grande, al menos una página, etc. Finalmente, coloque el bloque de memoria actualmente liberado en SizeClassAllocatorLocalCache.

  Information Direct: ruta de aprendizaje de tecnología del código fuente del kernel de Linux + video tutorial sobre el código fuente del kernel

Learning Express: Código fuente del kernel de Linux Ajuste de memoria Sistema de archivos Gestión de procesos Controlador de dispositivo/Pila de protocolo de red

3. Configuraciones comunes de Scudo

Scudo está diseñado para ser altamente ajustable y configurable y, si bien se proporcionan algunas configuraciones predeterminadas, se anima a los usuarios a crear los parámetros que mejor se adapten a su caso de uso.

Como se describe en la documentación oficial de Android Scudo, algunos parámetros del asignador se pueden definir para cada proceso de las siguientes maneras: a. En tiempo de compilación, definiendo SCUDO_DEFAULT_OPTIONS como la cadena de opción predeterminada.

b.Estático: define la función __scudo_default_options en el programa (devuelve la cadena de opción a analizar). La función debe tener el siguiente prototipo: extern "C" constchar *__scudo_default_options(). Las opciones definidas de esta manera reemplazan las opciones definidas en el momento de la compilación. Ejemplo: extern "C" const char *__scudo_default_options(){return "delete_size_mismatch=false:release_to_os_interval_ms=-1"; }c. Dinámico: utilice la variable de entorno SCUDO_OPTIONS (contiene la cadena de opción que se va a analizar). Las opciones definidas de esta manera reemplazan las opciones definidas a través de __scudo_default_options. Ejemplo: SCUDO_OPTIONS="delete_size_mismatch=false:release_to_os_interval_ms=-1"./a.outd, utilizando parámetros específicos de Scudo a través de la API estándar de mallopt.

Principalmente están disponibles las siguientes opciones:

Las siguientes son las opciones de " mallopt " disponibles:

Autor original: Kernel Craftsman

 

Supongo que te gusta

Origin blog.csdn.net/youzhangjing_/article/details/132563936
Recomendado
Clasificación