Bestiario de una memoria mal gestionada (III)

David García    4 mayo, 2020
Bestiario de una memoria mal gestionada (III)

Si tuviéramos que elegir una vulnerabilidad especialmente dañina sería, con mucha probabilidad, la ejecución de código arbitrario, y más aún si puede ser explotada en remoto. En la primera entrada introdujimos los problemas que puede acarrear una memoria mal gestionada; en la siguiente hablamos del double free. Ahora veremos más ejemplos.

Dangling pointers o punteros colgantes

La gestión manual de la memoria es compleja y se debe prestar atención al orden de las operaciones, de dónde se obtienen recursos y dónde dejamos de usarlos para poder liberarlos en buenas condiciones.

También se requiere seguir la pista a copias de punteros o referencias que, si son liberadas antes de tiempo, hacen que los punteros se queden “descolgados”, o en su versión en inglés, dangling pointers. Es decir, hacer uso de un recurso que ya ha sido liberado. Un ejemplo:

Ejecutamos:

Esto nos deja con un puntero que está apuntando a una zona de memoria (montículo) que no es válida (observemos que no imprime nada después de “(p2) apunta a…”. Es más, no existe forma de saber si un recurso al que se ha copiado su dirección continúa siendo válido, del mismo modo que no es posible recuperar un leak de memoria si se pierde su referencia (lo veremos más adelante).

Para señalizar que un puntero no es válido, asignamos la macro NULL a ese puntero (en C++ “moderno” asignaríamos nullptr) para advertir de alguna forma que no apunta a nada. Pero si ese NULL no es comprobado, no nos sirve de nada. Por tanto, por cada puntero que utilice un recurso debe comprobarse “su no NULLidad”.

La buena práctica es, por lo tanto: una vez liberamos memoria, asignamos NULL o nullptr (en C++) para señalizar que ese puntero ya no apunta a nada válido. Y además, antes de hacer uso de él, tanto para copiarlo como para desreferenciarlo, comprobamos su validez.

Memory leaks o fugas de memoria

Justo lo contrario a hacer uso de una zona de memoria que ya no es válida es hacer que ningún puntero apunte a una zona de memoria válida. Una vez perdida la referencia, ya no podremos hacer free sobre esa reserva de memoria, y ocupará ese espacio de forma indefinida hasta que el programa termine. Esto es un gran problema si el programa no termina, como por ejemplo, un servidor que normalmente se ejecuta hasta que la máquina se apague u ocurra alguna otra interrupción ineludible.

Un ejemplo (si lo queréis replicar, hacedlo en un sistema virtualizado que se tenga para experimentar):

El código que se ve a la derecha va obteniendo porciones de memoria hasta agotar toda la memoria de montículo. Esto produce que el sistema vaya agotando la RAM, empiece a hacer swapping y finalmente saldrá el OOM-killer a matar el proceso por pasarse de la raya con el consumo de memoria.

¿Qué es el OOM-killer? Es un procedimiento especial del kernel (en sistemas Linux) para eliminar procesos en memoria con el objeto de que el sistema no se desestabilice. En la captura podemos ver la salida del comando ‘dmesg’ en el que se refleja el kill de nuestro proceso por el coste de recursos que le supone al sistema.

Si analizamos el código vemos que entramos en un bucle infinito en el que se va reservando memoria y reasignando el mismo puntero a nuevos bloques de esa memoria. Las referencias anteriores no se liberan y se pierden, lo que produce una incesante fuga de memoria (exactamente igual a una cañería rota) que termina drásticamente.

Esto es evidentemente una dramatización de lo que ocurre en un programa real, pero la realidad es que ocurre así. El problema es que no se controlan las reservas de memoria en un punto, se van acumulando referencias perdidas y termina siendo un problema. Es posible que, en aplicaciones con fugas de memoria que sólo usamos durante unas horas, tan sólo notemos una ralentización (esto era más evidente en tiempos donde la RAM era más limitada) o una acumulación de memoria. Pero en servidores es común que esto conlleve la caída del servicio.

En la próxima entrada veremos el uso de memoria no inicializada.


No te pierdas la serie completa de este artículo:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *