
Fuzzea las implementaciones de CPU generando entradas de prueba a partir de proxies de software, para luego ejecutarlas en hardware real y detectar defectos y erratas de microarquitectura.
SiliFuzz es un sistema que encuentra defectos en CPUs fuzzing de proxies de software, como simuladores de CPU o desensambladores, y luego ejecuta las entradas de prueba acumuladas (conocidas como el corpus) en CPUs reales a gran escala. SiliFuzz es un trabajo en curso; consulte el paper para más detalles.
El fuzzing es una técnica para probar un objetivo (una aplicación o una API) con una gran cantidad de entradas de prueba generadas sobre la marcha. El objetivo es hacer que estas entradas sean lo más interesantes y diversas posible para provocar casos límite. En otras palabras, el fuzzing busca maximizar la cobertura de código combinada. La cobertura de código puede tener diferentes significados, por ejemplo, qué bloques básicos se ejecutan o qué rutas se toman en el programa.
Para los propósitos de SiliFuzz, un proxy es cualquier sistema de software o hardware que se comporta de manera similar a algunos aspectos de la CPU objetivo. Por ejemplo, un emulador de CPU o un desensamblador. Se necesita un proxy cuando no podemos recopilar información de cobertura directamente del objetivo.
Al aplicar técnicas de fuzzing a un proxy, podemos generar una serie de entradas de prueba (corpus) que producen un comportamiento interesante en el proxy. Nuestra suposición subyacente es que esto se traduce en un comportamiento igualmente interesante en el objetivo. Lea más en los .
Una colección de entradas utilizadas para probar el objetivo se conoce como corpus.
Un corpus razonablemente grande contiene millones de entradas y generalmente se divide en múltiples fragmentos no superpuestos llamados shards.
Un snapshot de SiliFuzz describe una secuencia corta de instrucciones de CPU más un estado inicial de registros y memoria de la CPU para ejecutar esa secuencia de manera determinista. Un snapshot típico contiene menos de 100 bytes de código y se ejecuta en microsegundos, pero puede ser arbitrariamente grande. Los snapshots se almacenan como protocol buffers silifuzz.proto.Snapshot.
Los snapshots se crean típicamente a partir de las entradas generadas por un motor de fuzzing. Para propósitos de prueba de CPU, estas entradas se filtran para eliminar todos los snapshots no deterministas. Lea más en los docs.
Un estado final describe el contenido de los registros y la memoria que se espera que existan al final de la ejecución de un Snapshot. Si el snapshot se ejecuta de manera diferente en distintas microarquitecturas de CPU, tendrá múltiples estados finales esperados.
Un Snap es una representación en memoria de un Snapshot que puede cargarse y ejecutarse fácilmente mediante el Runner. Los Snaps se cargan típicamente desde el disco mediante un lector runner. El formato en disco de Snap es esencialmente el mismo que el formato en memoria, excepto que los punteros nativos se reemplazan por offsets. Consulte este header para más detalles. Este formato a menudo se denomina relocatable. Cada Snap contiene exactamente un estado final esperado, es decir, los Snaps son específicos de la microarquitectura. Lea más en los docs.
Runner es un binario para probar un solo núcleo de CPU. Un runner consume un fragmento de corpus, ejecuta repetidamente Snaps aleatorios en él y verifica que se alcanza el estado final esperado. Runner es un proceso de un solo hilo.
El orchestrator es un proceso que controla múltiples runners. En una configuración típica, el orchestrator ejecutará continuamente un runner por núcleo de CPU lógico, y acumulará y reportará cualquier fallo producido por los procesos runner individuales.
Consulte este archivo para la lista de microarquitecturas soportadas.
SiliFuzz se ejecuta en sistemas Linux x86_64 y aarch64. Ha sido probado con las versiones 5.x y 6.x del kernel de Linux. No hay garantía de que sea compatible con versiones anteriores del kernel. El ABI legacy vsyscall debe estar desactivado para evitar falsos positivos.
Una lista no exhaustiva de bugs y defectos que SiliFuzz ha encontrado.
Un bug lógico es un comportamiento inválido de la CPU inherente a una microarquitectura o stepping particular de una CPU. SiliFuzz ha identificado los siguientes bugs:
Un defecto (eléctrico) es un comportamiento inválido de la CPU que ocurre solo en uno o varios chips. SiliFuzz ha encontrado los siguientes defectos que describimos en el paper
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}"
NOTA: Puede usar un contenedor Docker para evitar contaminar el sistema anfitrión: 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 ..."
Para Bazel, use los siguientes comandos.
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
NOTA: Consulte la documentación de Centipede sobre cómo ejecutar el motor de fuzzing de manera eficiente.
Esta herramienta auxiliar sirve para comprobar que la máquina en la que se está ejecutando es compatible.
$ ${SILIFUZZ_BIN_DIR}/tools/silifuzz_platform_id --short
intel-skylake
NOTA: La lógica de detección de CPU de SiliFuzz no tiene en cuenta ciertas variantes de escritorio de CPUs que de otro modo son compatibles. La herramienta reportará "Unsupported platform" en tales casos.
La fuzz_filter_tool convierte instrucciones sin procesar en Snapshots compatibles con Snap. Devuelve 0 cuando la conversión es posible y 1 en caso contrario. Esta interfaz es compatible con el input_filter de Centipede.
fuzz_filter_tool raw_input_sequence
El archivo raw_input_sequence contiene instrucciones sin procesar que se convertirán al formato Snapshot usando
InstructionsToSnapshot
Ejemplo de uso:
# INC EAX
echo -en '\xFF\xC0' > /tmp/inc_eax && ./tools/fuzz_filter_tool /tmp/inc_eax
echo $?
0
snap_tool examina y manipula protos binarios de Snapshot. Opcionalmente puede cargar instrucciones sin procesar y convertirlas a 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
....
La simple fix tool toma los resultados de fuzzing de Centipede, convierte las instrucciones sin procesar en snapshots sin estados finales, agrega estados finales a los snapshots y finalmente empaqueta los snapshots en un corpus de snaps relocatable fragmentado.
Actualmente se ejecuta como un proceso no reiniciable en un solo host y todo se carga en memoria, por lo que el tamaño del corpus que se puede manejar está limitado por la memoria disponible del host. Dado que los estados finales se generan en el host, el corpus resultante es de una sola arquitectura.
Experimental: las hash tests son pruebas estructuradas aleatorizadas que inyectan entropía en instrucciones generadas aleatoriamente y capturan las salidas resultantes de la manera más eficiente posible. Este enfoque se basa en la observación de que un porcentaje no trivial de defectos puede detectarse invocando la instrucción correcta con la entrada correcta. Las hash tests apuntan agresivamente a esta clase simple de defecto para proporcionar un punto de comparación experimental para Silifuzz. Actualmente solo se admite x86_64.
Si desea generar, por ejemplo, 30k snapshots de hash test que contengan instrucciones compatibles con procesadores Skylake en el directorio /tmp/hashtest, puede ejecutar este comando.
mkdir -p /tmp/hashtest && bazel run -c opt @silifuzz//fuzzer/hashtest:hashtest_generator -- --platform=intel-skylake -n 30000 --outdir /tmp/hashtest
El resto del documento está organizado en forma de How-to, donde cada pregunta describe un caso de uso típico. La progresión de las preguntas representa la complejidad creciente de la tarea que se intenta realizar. Cada paso típicamente requiere comprender o los artefactos (a veces ambos) obtenidos en el paso anterior.
NOTA: El documento asume una CPU anfitriona/objetivo x86_64. La salida exacta puede variar según el fabricante/stepping/etc. de la CPU y el entorno (por ejemplo, Docker/KVM).
ADVERTENCIA: muchas de las instrucciones a continuación ejecutan código binario arbitrario con los privilegios del usuario actual. La herramienta hace todo lo posible por aislar el código con seccomp(2). Úsela bajo su propio riesgo.
# 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
NOTA: Para evitar resultados no deterministas, varias partes de SiliFuzz excluyen ciertas clases de instrucciones, p. ej. CPUID anteriormente.
$ ./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>
Observe cómo el valor del estado final del registro RAX es 0x20000001
(0x20000000+1, que es exactamente lo que hace INC EAX). Observe también que el valor RIP es el original +2, que es el tamaño de la instrucción INC EAX.
$ ./tools/snap_tool play /tmp/inc_eax.pb
Snapshot played successfully.
Nota: Necesitará especificar una plataforma objetivo para generar un corpus. En este ejemplo, el corpus resultante tiene como objetivo la plataforma en la que se generó.
$ 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
Puede inspeccionar el proceso con 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
NOTA: Este paso depende del paso "fuzzing del objetivo Unicorn" descrito anteriormente.
Convierta el corpus de resultados de fuzzing corpus.* en un corpus ejecutable de 10 fragmentos para la arquitectura actual.
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.*
Los fragmentos del corpus estarán en /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
NOTA: Esta es una herramienta muy básica por ahora que ofrece solo unos pocos comandos.
# 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
El orchestrator recorrerá todos los fragmentos listados en el archivo pasado en el argumento --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
NOTA: El orchestrator también puede cargar fragmentos de corpus comprimidos con XZ desde archivos que terminan en .xz