Votre téléphone peut détecter la corruption de la mémoire en production. Pas avec l’instrumentation complète que vous exécutez en CI, et pas à chaque allocation. Mais le matériel dans votre poche intègre depuis quelques années déjà les primitives nécessaires, et un nombre croissant d’applications en production les activent discrètement.

La version courte : AddressSanitizer est trop coûteux pour la production. ARM Memory Tagging Extension ne l’est pas. Et si vous ne pouvez pas encore compter sur MTE, GWP-ASan vous offre une couverture probabiliste avec presque aucun overhead. Ensemble, ils répondent à une question qui avait autrefois une réponse déprimante.

La corruption de la mémoire est le bug que vous livrez

Use-after-free, buffer overflow et heap corruption sont les bugs qui passent toute suite de tests. Ils vivent dans du code natif, se reproduisent mal et plantent souvent loin de l’erreur réelle. Au moment où un utilisateur signale un plantage, la corruption originale s’est déjà produite, et votre seule preuve est une stack trace déformée ou un SIGSEGV à une adresse qui semble aléatoire.

Les outils traditionnels les attrapent tôt. Valgrind est précis mais lent. ASan est plus rapide mais ajoute encore environ 2-3x d’overhead mémoire et 2x d’overhead CPU. C’est acceptable pour le fuzzing ou les tests unitaires. Ce n’est pas acceptable pour un appareil alimenté par batterie exécutant Instagram.

La production a historiquement été un angle mort. Soit vous reproduisiez le bug localement, soit vous deviniez.

Comment ARM MTE tagge la mémoire en matériel

ARM Memory Tagging Extension, introduite dans ARMv8.5-A, donne au CPU un moyen peu coûteux de vérifier la validité des pointeurs à chaque accès mémoire. Cela fonctionne en assignant un tag de 4 bits à chaque granule de 16 octets de mémoire et en stockant un tag correspondant dans l’octet supérieur du pointeur.

À chaque load ou store, le CPU compare le tag du pointeur avec le tag mémoire. S’ils diffèrent, le matériel lève une faute. Ce n’est pas une vérification logicielle. Cela se produit dans le sous-système mémoire, et l’overhead est typiquement inférieur à 5 %.

L’octet supérieur d’un pointeur 64 bits était déjà ignoré par le matériel. MTE réutilise ces bits. Un pointeur comme 0xb7f0_0000_1234_5678 porte le tag 0xb7. La mémoire à cette adresse doit porter le même tag, sinon l’accès déclenche une faute.

MTE est disponible sur les appareils avec ARMv8.5-A ou ultérieur. Cela inclut le Google Pixel 8 et ultérieur, l’iPhone 15 Pro et ultérieur, et une part croissante d’appareils Android milieu de gamme. Ce n’est pas universel, mais ce n’est plus exotique.

Le noyau expose MTE via des flags prctl. Une application peut demander une vérification synchrone ou asynchrone. Le mode synchrone déclenche immédiatement une faute en cas de mismatch de tag. Le mode asynchrone met la faute en file d’attente et la livre plus tard, ce qui est moins coûteux mais retarde légèrement la détection.

GWP-ASan : quand vous ne pouvez pas tout tagger

MTE est excellent, mais il nécessite un matériel compatible et des allocations taggées dans toute votre codebase. Si vous livrez sur des appareils plus anciens, ou si vous voulez une protection ciblée, GWP-ASan est l’alternative pragmatique.

GWP-ASan signifie Guarded Write Protection AddressSanitizer. C’est un allocateur d’échantillonnage qui place une petite fraction d’allocations de tas dans des pages protégées entourées de redzones empoisonnées. Si un use-after-free ou un buffer overflow touche ces redzones, le MMU matériel déclenche immédiatement une faute.

L’idée clé est la probabilité. GWP-ASan pourrait protéger 1 allocation sur 10 000. Cela semble inutile jusqu’à ce que vous réalisiez qu’une application boguée corrompra la mémoire des milliers de fois avant de planter. Avec suffisamment de sessions utilisateur, même un taux d’échantillonnage de 0,01 % attrape de vrais bugs en production.

Google fait tourner GWP-ASan dans Chrome et les services système Android depuis des années. Il a trouvé des centaines de bugs use-after-free qui ont échappé aux tests et au fuzzing. L’overhead CPU est négligeable car seule une infime fraction d’allocations passe par le chemin protégé. L’overhead mémoire est borné car le pool protégé est petit et les allocations sont finalement recyclées.

Les compromis dont personne ne veut parler

MTE et GWP-ASan ne sont pas gratuits. Ils sont juste assez bon marché pour en valoir la peine.

MTE nécessite l’octet supérieur de chaque pointeur, ce qui signifie que votre codebase doit être compilée avec -fsanitize=memtag ou -march=armv8.5-a+memtag. Si vous avez du code qui masque des pointeurs, les hashe ou les passe à travers JNI sans tagging correct, vous obtiendrez des faux positifs. L’Android NDK et les implémentations modernes de libc gèrent cela correctement, mais les custom allocators ou les schémas de packing de pointeurs vont casser.

MTE n’attrape également que les bugs dans les régions de tas et de pile que vous taggez explicitement. Un wild pointer qui saute vers une région mmap non taggée ne sera pas attrapé. C’est mieux que rien, mais ce n’est pas une couverture totale.

GWP-ASan a le problème opposé. Il n’attrape que les allocations qui se trouvent par hasard dans le pool protégé. Un bug qui corrompt une allocation non protégée passera inaperçu. Vous payez également un petit coût de latence à l’allocation et à la désallocation pour l’ensemble échantillonné, et vous devez gérer les plantages avec élégance dans votre pipeline de télémétrie.

Aucun des deux outils ne remplace ASan dans votre pipeline CI. Ils le complètent. ASan vous donne une détection déterministe à haute couverture pendant les tests. MTE et GWP-ASan vous donnent un filet de sécurité sur le terrain.

Activer MTE sur un appareil Android moderne

Si vous avez un Pixel 8 ou plus récent, vous pouvez tester MTE dès aujourd’hui. L’Android NDK le prend en charge avec un seul flag du compilateur.

D’abord, vérifiez si votre appareil prend en charge MTE :

#include <sys/prctl.h>
#include <linux/prctl.h>
#include <sys/auxv.h>
#include <asm/hwcap.h>

bool mte_supported() {
    unsigned long hwcap2 = getauxval(AT_HWCAP2);
    return (hwcap2 & HWCAP2_MTE) != 0;
}

Puis compilez votre bibliothèque native avec MTE activé dans votre CMakeLists.txt :

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=memtag")

Si vous voulez un faulting synchrone, ce que vous voulez typiquement en production, demandez-le au démarrage :

#include <sys/prctl.h>

void enable_mte() {
    if (mte_supported()) {
        prctl(PR_SET_TAGGED_ADDR_CTRL,
              PR_TAGGED_ADDR_ENABLE | PR_MTE_TCF_SYNC | (0xfffe << PR_MTE_TAG_SHIFT),
              0, 0, 0);
    }
}

Le flag PR_MTE_TCF_SYNC dit au noyau de planter immédiatement en cas de mismatch de tag. Le masque de tag 0xfffe exclut le tag zéro, que du code système utilise encore.

Configurer GWP-ASan pour la production

GWP-ASan est plus facile à activer car il ne nécessite pas de matériel spécial. Sur Android, vous pouvez vous lier contre le wrapper de l’allocateur GWP-ASan ou l’activer via l’allocateur du système sur les niveaux d’API plus récents.

Pour une intégration minimale, enveloppez vos points d’entrée de l’allocateur :

extern "C" void* malloc(size_t size) {
    if (__gwp_asan_sample()) {
        return __gwp_asan_guarded_malloc(size);
    }
    return __libc_malloc(size);
}

En pratique, vous utiliserez le runtime Android GWP-ASan ou l’implémentation LLVM compiler-rt. Le paramètre de configuration clé est le taux d’échantillonnage. Commencez de manière conservatrice :

// Une allocation protégée par 10 000
__gwp_asan_set_sample_rate(10000);

Collectez les plantages résultants via votre télémétrie existante. Un plantage GWP-ASan ressemble à un SIGSEGV standard, mais l’adresse fautive tombera dans une page protégée. Votre rapporteur de plantages peut détecter cela en vérifiant si l’adresse fautive se trouve dans le pool GWP-ASan.

Ce que vous devriez vraiment faire

Si vous livrez du code natif sur mobile, vous devriez déjà exécuter ASan en CI et en fuzzing. La question est ce qui se passe après la livraison.

Si votre appareil minimum pris en charge inclut des puces ARMv8.5-A, activez MTE en mode synchrone. L’overhead est suffisamment faible pour que les utilisateurs ne le remarquent pas, et les plantages que vous obtiendrez auront des informations précises de mismatch de tag au lieu d’une corruption de tas aléatoire.

Si vous prenez en charge des appareils plus anciens, activez GWP-ASan avec un taux d’échantillonnage conservateur. Il n’attrapera pas chaque bug, mais il attrapera des bugs que rien d’autre n’attrape. Sur des millions de sessions, ce n’est pas théorique. C’est ainsi que Chrome a trouvé un use-after-free dans son décodeur d’images l’année dernière.

Le matériel de votre téléphone est déjà capable d’attraper les bugs de sécurité mémoire dont vous avez le plus peur. La seule question est de savoir si vous l’avez activé.