
Fuzz les implémentations de CPU en générant des entrées de test à partir de proxies logiciels, puis les exécute sur du matériel réel pour détecter les défauts et errata de la microarchitecture.
SiliFuzz est un système qui détecte les défauts des CPU en appliquant le fuzzing à des proxies logiciels, comme les simulateurs de CPU ou les désassembleurs, puis en exécutant les entrées de test accumulées (appelées le corpus) sur de véritables CPU à grande échelle. SiliFuzz est un travail en cours, veuillez consulter l'article pour plus de détails.
Le fuzzing est une technique qui consiste à tester une cible (une application ou une API) avec un grand nombre d'entrées de test générées à la volée. L'objectif est de rendre ces entrées aussi intéressantes et aussi diverses que possible afin de déclencher des cas limites. En d'autres termes, le fuzzing vise à maximiser la couverture de code combinée. La couverture de code peut avoir différentes significations, par exemple quels blocs de base sont exécutés ou quels chemins sont pris dans le programme.
Dans le cadre de SiliFuzz, un proxy est tout système logiciel ou matériel qui se comporte de manière similaire à certains aspects du CPU cible. Par exemple, un émulateur de CPU ou un désassembleur. Un proxy est nécessaire lorsque nous ne pouvons pas collecter directement les informations de couverture de la cible.
En appliquant les techniques de fuzzing à un proxy, nous pouvons générer une série d'entrées de test (corpus) qui produisent un comportement intéressant dans le proxy. Notre hypothèse sous-jacente est que cela se traduit par un comportement tout aussi intéressant dans la cible. En savoir plus dans la documentation.
Un ensemble d'entrées utilisé pour tester la cible est appelé corpus.
Un corpus raisonnablement grand contient des millions d'entrées et est généralement divisé en plusieurs morceaux non chevauchants appelés shards.
Un snapshot SiliFuzz décrit une courte séquence d'instructions CPU ainsi qu'un état initial des registres du CPU et de la mémoire pour exécuter cette séquence de manière déterministe. Un snapshot typique contient moins de 100 octets de code et s'exécute en microsecondes, mais il peut être arbitrairement grand. Les snapshots sont stockés sous forme de protobufs silifuzz.proto.Snapshot.
Les snapshots sont généralement créés à partir des entrées générées par un moteur de fuzzing. À des fins de test de CPU, ces entrées sont filtrées pour éliminer tous les snapshots non déterministes. En savoir plus dans la documentation.
Un état final décrit le contenu des registres et de la mémoire censé exister à la fin de l'exécution d'un snapshot. Si le snapshot s'exécute différemment sur différentes microarchitectures de CPU, il aura plusieurs états finaux attendus.
Un Snap est une représentation en mémoire d'un Snapshot qui peut être facilement chargée et exécutée par le Runner. Les Snaps sont généralement chargés depuis le disque par un reading runner. Le format sur disque d'un Snap est essentiellement le même que le format en mémoire, à l'exception du fait que les pointeurs natifs sont remplacés par des offsets. Voir ce fichier d'en-tête pour plus de détails. Ce format est souvent appelé relocatable. Chaque Snap contient exactement un état final attendu, c'est-à-dire que les Snaps sont spécifiques à une microarchitecture. En savoir plus dans la documentation.
Runner est un binaire permettant de tester un seul cœur de CPU. Un runner consomme un shard de corpus, exécute à plusieurs reprises des Snaps aléatoires qui s'y trouvent et vérifie que l'état final attendu est atteint. Runner est un processus monothread.
L'orchestrateur est un processus qui pilote plusieurs runners. Dans une configuration typique, l'orchestrateur exécute en continu un runner par cœur de CPU logique, et accumule et signale toute défaillance produite par les processus runners individuels.
Voir ce fichier pour la liste des microarchitectures prises en charge.
SiliFuzz fonctionne sur les systèmes Linux x86_64 et aarch64. Il a été testé avec les versions 5.x et 6.x du noyau Linux. Rien ne garantit qu'il soit compatible avec les versions plus anciennes du noyau. L'ABI legacy vsyscall doit être désactivée pour éviter les faux positifs.
Une liste non exhaustive des bogues et défauts découverts par SiliFuzz.
Un bogue logique est un comportement CPU invalide inhérent à une microarchitecture ou à un stepping particulier du CPU. SiliFuzz a identifié les bogues suivants :
Un défaut (électrique) est un comportement CPU invalide qui ne se produit que sur une ou plusieurs puces. SiliFuzz a trouvé les défauts suivants, décrits dans l'article
git clone https://github.com/google/silifuzz.git && cd silifuzz
SILIFUZZ_SRC_DIR=`pwd`
./install_build_dependencies.sh # Currently, works for the latest Ubuntu only.
bazel build -c opt @silifuzz//tools:{snap_corpus_tool,fuzz_filter_tool,snap_tool,silifuzz_platform_id,simple_fix_tool_main} \
@silifuzz//runner:reading_runner_main_nolibc \
@silifuzz//orchestrator:silifuzz_orchestrator_main
SILIFUZZ_BIN_DIR=`pwd`/bazel-bin
cd "${SILIFUZZ_BIN_DIR}"
REMARQUE : vous pouvez utiliser un conteneur Docker pour éviter de polluer le système hôte : docker run -it --tty --security-opt seccomp=unconfined --mount type=bind,source=${SILIFUZZ_SRC_DIR},target=/app ubuntu:noble /bin/bash -c "cd /app && ./install_build_dependencies.sh && bazel build ... && bazel test ..."
Pour Bazel, utilisez les commandes suivantes.
cd "${SILIFUZZ_SRC_DIR}"
COV_FLAGS_FILE="$(bazel info output_base)/external/fuzztest+/centipede/clang-flags.txt"
bazel build -c opt --copt=-UNDEBUG --dynamic_mode=off \
--per_file_copt=unicorn/.*@$(xargs < "${COV_FLAGS_FILE}" |sed -e 's/,/\\,/g' -e 's/ /,/g') @//proxies:unicorn_x86_64
bazel build -c opt @fuzztest//centipede:centipede
mkdir -p /tmp/wd
# Fuzz the Unicorn proxy under Centipede 1000 times with parallelism of 30.
"${SILIFUZZ_BIN_DIR}/external/fuzztest+/centipede/centipede" \
--binary="${SILIFUZZ_BIN_DIR}/proxies/unicorn_x86_64" \
--workdir=/tmp/wd \
-j=30 --num_runs=1000
REMARQUE : veuillez vous référer à la documentation Centipede pour savoir comment exécuter efficacement le moteur de fuzzing.
Cet outil utilitaire permet de vérifier que la machine sur laquelle vous travaillez est prise en charge.
$ ${SILIFUZZ_BIN_DIR}/tools/silifuzz_platform_id --short
intel-skylake
REMARQUE : la logique de détection du CPU de SiliFuzz ne prend pas en compte certaines variantes de bureau de CPU par ailleurs pris en charge. L'outil indiquera "Unsupported platform" dans de tels cas.
Le fuzz_filter_tool convertit des instructions brutes en snapshots compatibles Snap. Il renvoie 0 lorsque la conversion est possible et 1 sinon. Cette interface est compatible avec l'input_filter de Centipede.
fuzz_filter_tool raw_input_sequence
Le fichier raw_input_sequence contient des instructions brutes qui seront converties au format Snapshot à l'aide de
InstructionsToSnapshot
Exemple d'utilisation :
# INC EAX
echo -en '\xFF\xC0' > /tmp/inc_eax && ./tools/fuzz_filter_tool /tmp/inc_eax
echo $?
0
snap_tool examine et manipule des protos Snapshot binaires. Il peut éventuellement charger des instructions brutes et les convertir en Snapshot.
echo -en '\xFF\xC0' > /tmp/inc_eax
./tools/snap_tool --raw print /tmp/inc_eax
Metadata:
Id: inc_eax
Architecture: x86_64 Linux
Completeness: complete
Registers:
gregs (non-0 only)
rax = 0x20000000
....
L'outil simple fix prend les résultats de fuzzing de Centipede, convertit les instructions brutes en snapshots sans états finaux, ajoute des états finaux aux snapshots et, enfin, regroupe les snapshots en un corpus de snaps relocalisables, fragmenté en shards.
Actuellement, il s'exécute comme un processus non redémarrable sur un seul hôte et tout est chargé en mémoire, de sorte que la taille du corpus pouvant être traitée est limitée par la mémoire disponible de l'hôte. Comme les états finaux sont générés sur l'hôte, le corpus résultant est à architecture unique.
Expérimental : les hash tests sont des tests structurés aléatoires qui injectent de l'entropie dans des instructions générées aléatoirement et capturent les sorties résultantes aussi efficacement que possible. Cette approche repose sur le constat qu'un pourcentage non négligeable de défauts peut être détecté en appelant la bonne instruction avec la bonne entrée. Les hash tests ciblent agressivement cette classe simple de défauts afin de fournir un point de comparaison expérimental pour Silifuzz. Actuellement, seul x86_64 est pris en charge.
Si vous souhaitez générer, par exemple, 30 000 snapshots de hash tests contenant des instructions prises en charge par les processeurs Skylake dans le répertoire /tmp/hashtest, vous pouvez exécuter cette commande.
mkdir -p /tmp/hashtest && bazel run -c opt @silifuzz//fuzzer/hashtest:hashtest_generator -- --platform=intel-skylake -n 30000 --outdir /tmp/hashtest
Le reste du document est organisé sous forme de procédures (How-to), chaque question décrivant un cas d'utilisation typique. La progression des questions reflète la complexité croissante de la tâche à accomplir. Chaque étape nécessite généralement de comprendre ou d'utiliser les artefacts (parfois les deux) obtenus à l'étape précédente.
REMARQUE : ce document suppose un CPU hôte/cible x86_64. La sortie exacte peut varier en fonction du fournisseur/stepping du CPU, etc., et de l'environnement (par ex. Docker/KVM).
AVERTISSEMENT : bon nombre des instructions ci-dessous exécutent du code binaire arbitraire avec les privilèges de l'utilisateur courant. L'outil fait de son mieux pour isoler le code avec seccomp(2). À utiliser à vos risques et périls.
# INC EAX
$ echo -en '\xFF\xC0' > /tmp/inc_eax
$ ./tools/snap_tool --raw --out=/tmp/inc_eax.pb make /tmp/inc_eax
# CPUID
$ echo -en '\x0F\xA2' > /tmp/cpuid
$ ./tools/snap_tool --raw --out=/tmp/cpuid.pb make /tmp/cpuid
<error log omitted>
Could not load snapshot: INTERNAL: Tracing failed: Banned instruction: CPUID
REMARQUE : pour éviter des résultats non déterministes, diverses parties de SiliFuzz excluent certaines classes d'instructions, par ex. CPUID ci-dessus.
$ ./tools/snap_tool print /tmp/inc_eax.pb
Metadata:
Id: inc_eax
Architecture: x86_64 Linux
Completeness: complete
Registers:
gregs (non-0 only):
rax = 0x20000000
rip = 0xeb85c12b000
<omitted>
End states (1):
Endpoint:
Instruction address: 0xeb85c12b002
Platforms:
intel-skylake
Registers (diff vs snapshot's initial values):
gregs (modified only):
rax = 0x20000001
rip = 0xeb85c12b002
<omitted>
Remarquez que la valeur de l'état final du registre RAX est 0x20000001 (0x20000000+1, ce qui correspond exactement à ce que fait INC EAX). Remarquez également que la valeur de RIP est celle d'origine +2, soit la taille de l'instruction INC EAX.
$ ./tools/snap_tool play /tmp/inc_eax.pb
Snapshot played successfully.
Remarque : vous devrez spécifier une plateforme cible afin de générer un corpus. Dans cet exemple, le corpus résultant cible la plateforme sur laquelle il a été généré.
$ cd "${SILIFUZZ_BIN_DIR}"
$ PLATFORM_ID=$(./tools/silifuzz_platform_id --short)
$ ./tools/snap_tool generate_corpus /tmp/inc_eax.pb \
--target_platform="${PLATFORM_ID}" > /tmp/inc_eax.corpus
# Will play the same "INC EAX" snapshot 1M times
$ ./runner/reading_runner_main_nolibc /tmp/inc_eax.corpus
Vous pouvez inspecter le processus avec gdb :
$ gdb ./runner/reading_runner_main_nolibc
(gdb) b RestoreUContextNoSyscalls
(gdb) run /tmp/inc_eax.corpus
Starting program: .../reading_runner_main_nolibc /tmp/inc_eax.corpus
Breakpoint 1, 0x0000456700010598 in RestoreUContextNoSyscalls ()
(gdb) x/i 0xeb85c12b000 # same as the rip value produced by snap_tool print above
0xeb85c12b000: inc %eax
REMARQUE : cette étape repose sur l'étape "fuzzing de la cible Unicorn" décrite précédemment.
Convertissez le résultat de fuzzing corpus.* en un corpus exécutable de 10 shards pour l'architecture actuelle.
cd "${SILIFUZZ_BIN_DIR}"
"${SILIFUZZ_BIN_DIR}/tools/simple_fix_tool_main" \
--num_output_shards=10 \
--output_path_prefix=/tmp/wd/runnable-corpus \
--runner="${SILIFUZZ_BIN_DIR}/runner/reading_runner_main_nolibc" \
/tmp/wd/corpus.*
Les shards du corpus se trouveront dans /tmp/wd/runnable-corpus.*
$ ./tools/snap_corpus_tool list_snaps /tmp/inc_eax.corpus
...
I0000 00:00:1661887744.019079 4074672 snap_corpus_tool.cc:155] inc_eax
REMARQUE : il s'agit pour l'instant d'un outil très basique n'offrant que quelques commandes.
# Will play the same "INC EAX" snapshot on CPU#1 10k times.
$ ./runner/reading_runner_main_nolibc \
--cpu=1 --num_iterations=10000 /tmp/inc_eax.corpus
L'orchestrateur parcourra tous les shards listés dans le fichier passé à l'argument --shard_list_file.
$ ls -1 /tmp/wd/runnable-corpus.* > /tmp/wd/shard_list
$ echo 'version: "local_corpus"' > /tmp/wd/corpus_metadata
# Will repeatedly run the corpus on all available CPU cores for 30s using
# /tmp/wd/runnable-corpus.* selected randomly.
$ ${SILIFUZZ_BIN_DIR}/orchestrator/silifuzz_orchestrator_main --duration=30s \
--runner=${SILIFUZZ_BIN_DIR}/runner/reading_runner_main_nolibc \
--shard_list_file=/tmp/wd/shard_list \
--corpus_metadata_file=/tmp/wd/corpus_metadata
REMARQUE : l'orchestrateur peut également charger des shards de corpus compressés en XZ à partir de fichiers se terminant par .xz.