Skip to content

Fuzzing ruamel yaml (Python) project with sydr fuzz (rus)

lehatrutenb edited this page Sep 29, 2025 · 1 revision

Введение

В этой статье я бы хотел поделиться своим опытом фаззинга проектов на языке Python. Для этих целей я буду использовать sydr-fuzz с поддержкой Atheris и python-afl. Sydr-fuzz изначально разрабатывался как гибридный фаззер, который комбинирует инструмент динамического символьного выполнения Sydr и современные инструменты фаззинга AFLplusplus и libFuzzer. Также sydr-fuzz поддерживает несколько полезных фич, таких как сортировка аварийных завершений с помощью casr, возможность проверки предикатов безопасности, удобные команды для минимизации корпуса и сбора покрытия по исходному коду. Atheris и python-afl — это движки фаззинга с обратной связью по покрытию для Python языка. Они поддерживает фаззинг кода на языке Python, кроме того и фаззинг нативных расширений для CPython. Atheris основан на libFuzzer, a python-afl на AFL++. Они выглядит и работают как libFuzzer и AFL++, поэтому мы решили поддержать возможность работы с ним через sydr-fuzz, почему нет? Хотя у нас нет динамического символьного выполнения для кода на языке Python, но мы можем фаззить, делать сортировку аварийных завершений, минимизировать корпус и собирать покрытие по исходному коду через удобный интерфейс sydr-fuzz.

Подготовка фаззинг-цели

Страница Atheris на GitHub предоставляет отличную инструкцию о том, как его установить и использовать. Мы будем фаззить yaml проект из его примеров. У нас уже есть подготовленный к сборке docker контейнер с необходимым окружением для фаззинга. Я буду его использовать в своих экспериментах, но сейчас давайте рассмотрим фаззинг цель и скрипт сборки.

#!/usr/bin/env python3
import atheris

with atheris.instrument_imports():
  from ruamel import yaml as ruamel_yaml
  import sys
  import warnings

# Suppress all warnings.
warnings.simplefilter("ignore")

ryaml = ruamel_yaml.YAML(typ="safe", pure=True)
ryaml.allow_duplicate_keys = True


@atheris.instrument_func
def TestOneInput(input_bytes):
  fdp = atheris.FuzzedDataProvider(input_bytes)
  data = fdp.ConsumeUnicode(sys.maxsize)

  try:
    iterator = ryaml.load_all(data)
    for _ in iterator:
      pass
  except ruamel_yaml.error.YAMLError:
    return

  except Exception:
    input_type = str(type(data))
    codepoints = [hex(ord(x)) for x in data]
    sys.stderr.write(
        "Input was {input_type}: {data}\nCodepoints: {codepoints}".format(
            input_type=input_type, data=data, codepoints=codepoints))
    raise


def main():
  atheris.Setup(sys.argv, TestOneInput)
  atheris.Fuzz()


if __name__ == "__main__":
  main()

В случае Atheris требуется определить модули, которые требуется проинструментировать для сбора метрик фаззинга внутри них. Это подробно описано в (https://github.com/google/atheris/tree/master). Мы воспользуемся with atheris.instrument_imports(): для инструментации перечисленных модулей, а также @atheris.instrument_func для инструментации инструкций внутри функции. Также имеется возможность инструментировать все модули: atheris.instrument_all(). Это может быть полезным, если у нас большой проект с множеством зависимостей.

Затем мы должны реализовать функцию def TestOneInput(input_bytes):, подобно тому, как это делается для int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) на языках C/C++. Важно обрабатывать исключения, которые выкидывает целевая функция. Но мы должны ловить только те исключения, которые указаны разработчиками или их явно выкидывает эта функция. Например, IndexError, не нужно обрабатывать, если об этом явно не сказано в документации. Atheris и python-afl сами поймают это исключение и сохранят, как аварийное завершение.

Для фаззинга при помощи python-afl я внесу необходимые правки, о которых можно подробнее прочитать в литературе по python-afl

#!/usr/bin/python3
import afl, sys, os
from ruamel import yaml as ruamel_yaml
import warnings

# Suppress all warnings.
warnings.simplefilter("ignore")

def _ConsumeString(res_len, data):
  ...

def TestOneInput(input_bytes):
  ryaml = ruamel_yaml.YAML(typ="safe", pure=True)
  ryaml.allow_duplicate_keys = True
  data = _ConsumeString(sys.maxsize, input_bytes)

  try:
    iterator = ryaml.load_all(data)
    for _ in iterator:
      pass
  except ruamel_yaml.error.YAMLError:
    return

  except Exception:
    input_type = str(type(data))
    codepoints = [hex(ord(x)) for x in data]
    sys.stderr.write(
        "Input was {input_type}: {data}\nCodepoints: {codepoints}".format(
            input_type=input_type, data=data, codepoints=codepoints))
    raise


def main():
  # выбираем откуда считывать stdin
  try:
    # Python 3:
    stdin_compat = sys.stdin.buffer
  except AttributeError:
    # There is no buffer attribute in Python 2:
    stdin_compat = sys.stdin

  while afl.loop(10000): 
    TestOneInput(stdin_compat.read())
    sys.stdin.seek(0) # очистка stdin между запусками
  os._exit(0)

if __name__ == "__main__":
  main()

Может возникнуть вопрос — почему shebang (#!/usr/bin/python3) был изменён — это не случайность, у python-afl c asan при использовании run/cmin есть вероятность столкнуться с множеством ошибок вида "[CMIN] du: cannot access ..." или же "Fork server handshake failed ...", и исправлением (при условии, что всё остальное настроено правильно) обнаруженных мной причин является использование shebang без env — так что будьте аккуратны при использовании python-afl.

(но подобные ошибки могут и просто означать, что таргет некорректен (не компилится/...))

Аналогично, пренеприятная проблема с артефактами вида зависающего cmin/сборки покрытия может произойти в очень небольшом проценте систем при использовании LD_PRELOAD — у неё есть решение (https://github.com/actions/runner-images/issues/9524), но простой путь — перейти на python-afl.

Наконец, мы должны написать ещё пару строчек кода, чтобы запустить процесс фаззинга:

В случае Atheris:

atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()

В случае python-afl:

while afl.loop(10000):
  TestOneInput(stdin_compat.read())
  sys.stdin.seek(0) # очистка stdin между запусками
os._exit(0)

Выше мы инициализируем python-afl в рекомендованном Persistent mode — он работает на порядки быстрее инициализации приведённой ниже, но глобальные/статические переменные могут повлиять на успешность фаззинга. Важно знать, что он будет вызывать ошибки при работе с целями, интрументированными afl (например msgspec с Cython внутри, который можно дополнительно инструментировать). В таком случае можно использовать стандартный режим:

afl.init()
TestOneInput(stdin_compat.read())
os._exit(0)

Либо, при желании, можно применить ухищрение и импортировать интрументированные библиотеки внутри TestOneInput - таким образом мы останемся в Persistent mode, но уменьшим stability (и возможно фаззинг будет не совсем корректным), но будем работать значительно быстрее стандартного режима.

В нашем случае этого не требуется (так как нет внутренностей, которые мы могли бы дополнительно проинструментировать), но хочется продемонстрировать вышесказанное ухищрение:

def TestOneInput(input_bytes):
  # тот самый импорт, о котором шла речь
  from ruamel import yaml as ruamel_yaml
  ryaml = ruamel_yaml.YAML(typ="safe", pure=True)
  ryaml.allow_duplicate_keys = True
  # ... идентичный код
while afl.loop(10000):
  TestOneInput(stdin_compat.read())
  sys.stdin.seek(0)
os._exit(0)

Так же мы добавили строчки получения stdin для различных версий Python и _ConsumeString принимающую байты от фаззинга и конвертирующую их в строки Python(так как для python-afl на данный момент нет готовых удобных конвертаций - можно использовать конвертации от atheris, но, возможно фаззинг немного замедлится), но они нам не так интересны.

Что касается сборки, то мы можем просто выполнить команду pip install . в директории и установить инструментированный проект в наше фаззинг окружение. Хорошо, давайте соберём docker контейнер и начнём фаззинг!

В случае сборки для python-afl библиотек содержащих C/C++ могут возникнуть определённые трудности — возможно вам помогут примеры сборок из нами поддержанных target-ов в (https://github.com/ispras/oss-sydr-fuzz). На момент написания wiki это msgspec и ultrajson.

Конфиги

Перед тем как мы начнём, давайте посмотрим на yaml_fuzzer-atheris.toml:

exit-on-time = 3600

[atheris]
path = "/yaml_fuzzer-atheris.py"
args = "/corpus -dict=yaml.dict -jobs=1000 -workers=4"

А так же на yaml_fuzzer-pyafl.toml:

exit-on-time = 3600
[pyafl]
args = "-i /corpus"
target = "/fuzz/yaml_fuzzer_pyafl.py"
jobs = 4

Они очень простые.

exit-on-time — опциональный параметр, принимающий время в секундах. Если в течение этого времени (1 час в нашем случае) покрытие не увеличилось, то фаззинг завершается автоматически.

Фаззинг в Atheris

Я буду использовать 4 потока для фаззинга пока фаззер не найдёт 1000 аварийных завершений или не сработает exit-on-time. Начинаем фаззинг с помощью Atheris следующей командой:

# sydr-fuzz -c yaml_fuzzer-atheris.toml run
[2023-01-11 17:22:47] [INFO] #3582      RELOAD cov: 1178 ft: 5252 corp: 478/64Kb lim: 487 exec/s: 275 rss: 713Mb                                                     
[2023-01-11 17:22:48] [INFO] Uncaught Python exception: KeyError: (0, 1) /fuzz/yaml_fuzzer-out/crashes/crash-a0acd109aef7675ce2268eec4e0901759f4e1edc                      
[2023-01-11 17:22:50] [INFO] #17540     REDUCE cov: 1178 ft: 5257 corp: 511/86Kb lim: 481 exec/s: 343 rss: 677Mb L: 13/481 MS: 2 CrossOver-EraseBytes-                                                     
[2023-01-11 17:22:50] [INFO] #17573     REDUCE cov: 1178 ft: 5257 corp: 511/86Kb lim: 481 exec/s: 344 rss: 677Mb L: 58/481 MS: 3 ChangeBit-ManualDict-EraseBytes- DE: "'"- 
[2023-01-11 17:22:50] [INFO] Uncaught Python exception: KeyError: (1, 5) /fuzz/yaml_fuzzer-out/crashes/crash-4230d57dcf9dce49804ffd9abbc43a751068c6a2                                                          
[2023-01-11 17:22:55] [INFO] #1024      pulse  cov: 1171 ft: 4926 corp: 413/36Kb exec/s: 204 rss: 710Mb                                                                                                            
[2023-01-11 17:22:55] [INFO] [ATHERIS]         run time : 0 days, 0 hrs, 0 min, 57 sec                                                                                                                             
[2023-01-11 17:22:55] [INFO] [ATHERIS]    last new find : 0 days, 0 hrs, 0 min, 8 sec                                                                                                                              
[2023-01-11 17:22:57] [INFO] #1268      INITED cov: 1178 ft: 5265 corp: 477/60Kb exec/s: 181 rss: 710Mb

Через некоторое время мы нашли несколько аварийных завершений. Давайте подождём, когда фаззинг закончится.

[2023-01-11 19:21:27] [INFO] Uncaught Python exception: KeyError: (2, 1) /fuzz/yaml_fuzzer-out/crashes/crash-1f71bdb8cbba856923a45f50c7873bdb7ef64e2d
[2023-01-11 19:21:28] [INFO] Uncaught Python exception: KeyError: (1, 5) /fuzz/yaml_fuzzer-out/crashes/crash-c3152f019e73c4c01925ae1533f47583fe3006df
[2023-01-11 19:21:28] [INFO] Uncaught Python exception: KeyError: (2, 1) /fuzz/yaml_fuzzer-out/crashes/crash-fed9747bec6197c9c8cbc0cf10c051c8f807d407
[2023-01-11 19:21:28] [INFO] Uncaught Python exception: KeyError: (1, 0) /fuzz/yaml_fuzzer-out/crashes/crash-01ae8831870d95a5be5898dd17457235b851bfdf
[2023-01-11 19:21:41] [INFO] EXIT_ON_TIME: No new coverage (cov) for 3600 secs.
[2023-01-11 19:21:42] [INFO] EXIT_ON_TIME: No new coverage (cov) for 3600 secs.
[2023-01-11 19:21:42] [INFO] [RESULTS] Fuzzing corpus is saved in /fuzz/yaml_fuzzer-out/corpus
[2023-01-11 19:21:42] [INFO] [RESULTS] oom/leak/timeout/crash: 0/0/0/407
[2023-01-11 19:21:42] [INFO] [RESULTS] Fuzzing results are saved in /fuzz/yaml_fuzzer-out/crashes

Отлично, наш эксперимент закончился по exit-on-time. Итого мы получили 407 аварийных завершений. Это работа для casr.

Но сперва давайте минимизируем корпус:

# sydr-fuzz -c yaml_fuzzer-atheris.toml cmin
[2023-01-11 20:30:08] [INFO] Original fuzzing corpus saved as /fuzz/yaml_fuzzer-out/corpus-old
[2023-01-11 20:30:08] [INFO] Minimizing corpus /fuzz/yaml_fuzzer-out/corpus
[2023-01-11 20:30:08] [INFO] Using LD_PRELOAD="/usr/local/lib/python3.8/dist-packages/asan_with_fuzzer.so"
[2023-01-11 20:30:08] [INFO] ASAN_OPTIONS="abort_on_error=1,detect_leaks=0,malloc_context_size=0,symbolize=0,allocator_may_return_null=1"
[2023-01-11 20:30:08] [INFO] Launching atheris: "/yaml_fuzzer-atheris.py" "-merge=1" "-artifact_prefix=/fuzz/yaml_fuzzer-out/crashes/" "-close_fd_mask=2" "-verbosity=2" "-detect_leaks=0" "-dict=/fuzz/yaml.dict" "/fuzz/yaml_fuzzer-out/corpus" "/fuzz/yaml_fuzzer-out/corpus-old"
[2023-01-11 20:30:10] [INFO] MERGE-OUTER: 8719 files, 0 in the initial corpus, 0 processed earlier
[2023-01-11 20:30:10] [INFO] MERGE-OUTER: attempt 1
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: successful in 1 attempt(s)
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: the control file has 982127 bytes
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: consumed 0Mb (120Mb rss) to parse the control file
[2023-01-11 20:31:04] [INFO] MERGE-OUTER: 913 new files with 7301 new features added; 1249 new coverage edges

Мы смогли сократить выходной корпус с 8719 файлов до 913 файлов, неплохо. Давайте соберём покрытие по исходному коду!

Покрытие в Atheris

Для сбора покрытия мы будем использовать известный модуль coverage и инструкцию из репозитория Atheris. Конечно, это всё будет обёрнуто в удобную команду sydr-fuzz pycov. Давайте получим html отчёт о покрытии:

# sydr-fuzz -c yaml_fuzzer-atheris.toml pycov html
[2023-01-11 20:37:47] [INFO] Running pycov html "/fuzz/yaml_fuzzer-atheris.toml"
[2023-01-11 20:37:47] [INFO] Collecting coverage data for each file in corpus: /fuzz/yaml_fuzzer-out/corpus
[2023-01-11 20:37:47] [INFO] Saving coverage data to /fuzz/yaml_fuzzer-out/coverage/html/.coverage
[2023-01-11 20:37:47] [INFO] Using LD_PRELOAD="/usr/local/lib/python3.8/dist-packages/asan_with_fuzzer.so"
[2023-01-11 20:37:47] [INFO] ASAN_OPTIONS="abort_on_error=1,detect_leaks=0,malloc_context_size=0,symbolize=0,allocator_may_return_null=1"
[2023-01-11 20:37:47] [INFO] Collecting coverage: "coverage" "run" "/yaml_fuzzer-atheris.py" "-atheris_runs=914"
[2023-01-11 20:37:51] [INFO] Running coverage html: "coverage" "html" "-d" "/fuzz/yaml_fuzzer-out/coverage/html" "--data-file=/fuzz/yaml_fuzzer-out/coverage/html/.coverage"
Wrote HTML report to /fuzz/yaml_fuzzer-out/coverage/html/index.html

Прекрасно, теперь у нас есть покрытие по исходному коду, давайте на него посмотрим и двинемся дальше!

cov-html

Сортировка аварийных завершений в Atheris

Как я говорил ранее, я буду использовать casr через команду sydr-fuzz casr для сортировки аварийных завершений:

# sydr-fuzz -c yaml_fuzzer-atheris.toml casr

Вы можете узнать больше о casr из репозитория casr или из другого моего гайда.

Давайте посмотрим, что нам выдал casr:

[2023-01-11 20:47:14] [INFO] Casr-cluster: deduplication of casr reports...
[2023-01-11 20:47:16] [INFO] Reports before deduplication: 407; after: 16
[2023-01-11 20:47:16] [INFO] Casr-cluster: clustering casr reports...
[2023-01-11 20:47:16] [INFO] Reports before clustering: 16. Clusters: 8
[2023-01-11 20:47:16] [INFO] Copying inputs...
[2023-01-11 20:47:16] [INFO] Done!
[2023-01-11 20:47:16] [INFO] ==> <cl1>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl1/crash-bf5829959ccf0211640314bb30de19bc9bafdeb3
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 1
[2023-01-11 20:47:16] [INFO] ==> <cl2>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl2/crash-e126eb63b0bc1aefac72c3f56dea8484577f1007
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: RecursionError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/events.py:78
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> RecursionError: 1
[2023-01-11 20:47:16] [INFO] ==> <cl3>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl3/crash-017ee5d1bb2bee51263f083eb12a60711a3c84f1
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 4
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 4
[2023-01-11 20:47:16] [INFO] ==> <cl4>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl4/crash-3637416d80df3c5961e05b0bd459b79009e2a182
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 2
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 2
[2023-01-11 20:47:16] [INFO] ==> <cl5>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl5/crash-01ae8831870d95a5be5898dd17457235b851bfdf
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: KeyError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/resolver.py:361
[2023-01-11 20:47:16] [INFO]   Similar crashes: 4
[2023-01-11 20:47:16] [INFO] Cluster summary -> KeyError: 4
[2023-01-11 20:47:16] [INFO] ==> <cl6>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl6/crash-0ea90a02b95f99e850b036e49419a43103a54149
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: ValueError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:533
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl6/crash-988f305721849b6a75af3b3f424b4593901630c3
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: ValueError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:498
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> ValueError: 2
[2023-01-11 20:47:16] [INFO] ==> <cl7>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl7/crash-3f369c580ac61eded9d05eb06bc1ad6d0e90bfe1
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: ValueError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:498
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> ValueError: 1
[2023-01-11 20:47:16] [INFO] ==> <cl8>
[2023-01-11 20:47:16] [INFO] Crash: /fuzz/yaml_fuzzer-out/casr/cl8/crash-05451dc00f42aa97a064d2e08153bb84af113717
[2023-01-11 20:47:16] [INFO]   casr-python: UNDEFINED: TypeError: /usr/local/lib/python3.8/dist-packages/ruamel/yaml/constructor.py:273
[2023-01-11 20:47:16] [INFO]   Similar crashes: 1
[2023-01-11 20:47:16] [INFO] Cluster summary -> TypeError: 1
[2023-01-11 20:47:16] [INFO] SUMMARY -> RecursionError: 1 KeyError: 11 ValueError: 3 TypeError: 1
[2023-01-11 20:47:16] [INFO] Crashes and Casr reports are saved in /fuzz/yaml_fuzzer-out/casr

После дедупликации у нас 16 аварийных завершений разбитых на 8 кластеров. Отлично, приемлемо для ручного анализа. Давайте посмотрим на какой-нибудь отчёт, например из 6ого кластера (cl6): casrep Необработанное исключение, вызванное ошибкой конвертации строки в число. Выглядит как потенциальная ошибка:).

Фаззинг в python-afl:

Теперь сделаем аналогичные шаги для python-afl.

Я буду использовать 4 потока для фаззинга пока не сработает exit-on-time. Начинаем фаззинг с помощью python-afl следующей командой:

# sydr-fuzz -c yaml_fuzzer-pyafl.toml run
[2025-08-15 14:05:47] [INFO] [AFL++] [*] Attempting dry run with 'id:000009,time:0,execs:0,orig:yaml-version.yaml'...
[2025-08-15 14:05:47] [INFO] [AFL++]     len = 24, map size = 1570, exec speed = 5423 us, hash = 26807c104c8188ff
[2025-08-15 14:05:47] [INFO] [AFL++] [!] WARNING: Instrumentation output varies across runs.
[2025-08-15 14:05:47] [INFO] [AFL++] [+] All test cases processed.
[2025-08-15 14:05:47] [INFO] [AFL++] [!] WARNING: The target binary is pretty slow! See /usr/local/share/doc/afl/fuzzing_in_depth.md#i-improve-the-speed
[2025-08-15 14:05:47] [INFO] [AFL++] [+] Here are some useful stats:
[2025-08-15 14:05:47] [INFO] [AFL++]     Test case count : 8 favored, 4 variable, 0 ignored, 10 total
[2025-08-15 14:05:47] [INFO] [AFL++]        Bitmap range : 1160 to 2057 bits (average: 1525.50 bits)
[2025-08-15 14:05:47] [INFO] [AFL++]         Exec timing : 2685 to 48.2k us (average: 12.7k us)
[2025-08-15 14:05:47] [INFO] [AFL++] 
[2025-08-15 14:05:47] [INFO] [AFL++] [*] -t option specified. We'll use an exec timeout of 2000 ms.
[2025-08-15 14:05:47] [INFO] [AFL++] [+] All set and ready to roll!
[2025-08-15 14:05:47] [INFO] [AFL++] [*] Entering queue cycle 1
[2025-08-15 14:05:47] [INFO] [AFL++] 
[2025-08-15 14:05:47] [INFO] [AFL++] [*] Fuzzing test case #1 (10 total, 0 crashes saved, state: started :-), mode=explore, perf_score=100, weight=1, favorite=1, was_fuzzed=0, exec_us=2685, hits=0, map=1360, ascii=0, run_time=0:00:00:00)...
[2025-08-15 14:06:16] [INFO] Found crash /fuzz/yaml_fuzzer_pyafl-out/crashes/crash-bb4d05f035d758b66070c9250124988e81a168d7

Через некоторое время мы нашли несколько аварийных завершений. Давайте подождём, когда фаззинг закончится.

[2025-08-17 09:45:51] [INFO] [AFL++] +++ Testing aborted programmatically +++
[2025-08-17 09:45:51] [INFO] [AFL++] [!] 
[2025-08-17 09:45:51] [INFO] [AFL++] Performing final sync, this make take some time ...
[2025-08-17 09:45:51] [INFO] [AFL++] [!] Done!
[2025-08-17 09:45:51] [INFO] [AFL++] [*] Writing /fuzz/yaml_fuzzer_pyafl-out/aflplusplus/afl_main-worker/fastresume.bin ...
[2025-08-17 09:45:51] [INFO] [AFL++] [+] Written fastresume.bin with 2431192 bytes!
[2025-08-17 09:45:51] [INFO] [AFL++] [+] We're done here. Have a nice day!
[2025-08-17 09:45:51] [INFO] [AFL++] 
[2025-08-17 09:45:51] [INFO] [RESULTS] Fuzzing corpuses are saved in workers queue directories. Run sydr-fuzz cmin subcommand to gather full corpus at "/fuzz/yaml_fuzzer_pyafl-out/corpus-old" and minimized corpus at "/fuzz/yaml_fuzzer_pyafl-out/corpus".
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_main] 1675 new corpus items found, 9.46% coverage achieved, 197 crashes saved, 131 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 53 sec
[2025-08-17 09:45:51] [INFO] [RESULTS]            execs done: 27544322, execs/s: 193.14, edges found: 6202/65536, stability: 66.01%
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_s01] 1422 new corpus items found, 9.35% coverage achieved, 168 crashes saved, 128 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 57 sec
[2025-08-17 09:45:51] [INFO] [RESULTS]           execs done: 15183330, execs/s: 106.46, edges found: 6126/65536, stability: 67.37%
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_s02] 1470 new corpus items found, 9.50% coverage achieved, 218 crashes saved, 111 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 58 sec
[2025-08-17 09:45:51] [INFO] [RESULTS]           execs done: 16792546, execs/s: 117.74, edges found: 6226/65536, stability: 83.99%
[2025-08-17 09:45:51] [INFO] [RESULTS] [afl_s03] 1359 new corpus items found, 9.41% coverage achieved, 203 crashes saved, 118 timeouts saved, total runtime 1 days, 15 hrs, 36 min, 55 sec
[2025-08-17 09:45:51] [INFO] [RESULTS]           execs done: 16465514, execs/s: 115.45, edges found: 6168/65536, stability: 66.97%
[2025-08-17 09:45:51] [INFO] [RESULTS] timeout/crash: 488/785

Перед сбором покрытия давайте минимизируем корпус:

# sydr-fuzz -c yaml_fuzzer-pyafl.toml cmin
[2025-08-18 11:42:24] [INFO] [CMIN] [+] Found 28239 unique tuples across 10970 files.
[2025-08-18 11:42:24] [INFO] [CMIN] [+] Narrowed down to 734 files, saved in '/fuzz/yaml_fuzzer_pyafl-out/corpus'.

Покрытие в python-afl

Для сбора покрытия мы будем использовать известный модуль [coverage] аналогично Atheris, но так как покрытие в формате html уже получили , то посмотрим в формате консольного вывода:

# sydr-fuzz -c yaml_fuzzer-pyafl.toml pycov report
[2025-09-29 16:04:48] [INFO] Configured to run with AFL_PRELOAD SET
[2025-09-29 16:04:48] [INFO] Running pycov report "/fuzz/yaml_fuzzer_pyafl.toml"
[2025-09-29 16:04:48] [INFO] Collecting coverage data for each file in corpus: /fuzz/yaml_fuzzer_pyafl-out/corpus
[2025-09-29 16:04:48] [INFO] Saving coverage data to /fuzz/yaml_fuzzer_pyafl-out/coverage/report/.coverage
[2025-09-29 16:04:48] [INFO] pycov environment: AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 AFL_SYNC_TIME=1 ASAN_OPTIONS=abort_on_error=1,allocator_may_return_null=1,detect_leaks=0,hard_rss_limit_mb=2048,malloc_context_size=0,symbolize=0,verify_asan_link_order=0 PYTHON_AFL_PERSISTENT=1 UBSAN_OPTIONS=abort_on_error=0,allocator_may_return_null=1,halt_on_error=0,malloc_context_size=0
[2025-09-29 16:04:48] [INFO] Collecting coverage: cd "/fuzz/yaml_fuzzer_pyafl-out/coverage/report" && AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES="1" AFL_SYNC_TIME="1" ASAN_OPTIONS="abort_on_error=1,allocator_may_return_null=1,detect_leaks=0,hard_rss_limit_mb=2048,malloc_context_size=0,symbolize=0,verify_asan_link_order=0" PYTHON_AFL_PERSISTENT="1" UBSAN_OPTIONS="abort_on_error=0,allocator_may_return_null=1,halt_on_error=0,malloc_context_size=0" "/bin/bash" "-c" "coverage run --include=*/site-packages/*,*/dist-packages/*,/fuzz/yaml_fuzzer_pyafl.py --omit=*/coverage/* /fuzz/yaml_fuzzer_pyafl-out/coverage/pyAflCovWrapper.py /fuzz/yaml_fuzzer_pyafl-out/corpus"
[2025-09-29 16:04:49] [INFO] Running coverage report: AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES="1" AFL_SYNC_TIME="1" ASAN_OPTIONS="abort_on_error=1,allocator_may_return_null=1,detect_leaks=0,hard_rss_limit_mb=2048,malloc_context_size=0,symbolize=0,verify_asan_link_order=0" PYTHON_AFL_PERSISTENT="1" UBSAN_OPTIONS="abort_on_error=0,allocator_may_return_null=1,halt_on_error=0,malloc_context_size=0" "coverage" "report" "--data-file=/fuzz/yaml_fuzzer_pyafl-out/coverage/report/.coverage" "--ignore-errors"
Name                                                                 Stmts   Miss  Cover
----------------------------------------------------------------------------------------
/usr/local/lib/python3.9/dist-packages/_distutils_hack/__init__.py     101     96     5%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/__init__.py           9      2    78%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/anchor.py            10      4    60%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/comments.py         777    552    29%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/compat.py           154     87    44%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/composer.py         125     23    82%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py     1062    767    28%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/cyaml.py             42     25    40%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/dumper.py            29     16    45%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/emitter.py         1136   1046     8%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/error.py            163    103    37%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/events.py            81      4    95%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/loader.py            43     28    35%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/main.py             828    609    26%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/nodes.py             52     18    65%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/parser.py           466    161    65%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/reader.py           180     89    51%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/representer.py      778    634    19%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py         220    122    45%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarbool.py        23     15    35%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarfloat.py       69     50    28%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarint.py         67     44    34%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scalarstring.py      75     42    44%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/scanner.py         1327    613    54%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/serializer.py       141    118    16%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/timestamp.py         33     26    21%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/tokens.py           236    117    50%
/usr/local/lib/python3.9/dist-packages/ruamel/yaml/util.py             142    121    15%
yaml_fuzzer_pyafl.py                                                    64     26    59%
----------------------------------------------------------------------------------------
TOTAL                                                                 8433   5558    34%

Сортировка аварийных завершений в python-afl

Снова воспользуемся sydr-fuzz casr для сортировки аварийных завершений:

# sydr-fuzz -c yaml_fuzzer-pyafl.toml casr

Давайте посмотрим, что нам выдал casr:

[2025-08-18 13:08:49] [INFO] [CASR] Progress: 785/786
[2025-08-18 13:08:50] [INFO] [CASR] Deduplicating CASR reports...
[2025-08-18 13:08:53] [INFO] [CASR] Number of reports before deduplication: 586. Number of reports after deduplication: 32
[2025-08-18 13:08:53] [INFO] [CASR] Clustering CASR reports...
[2025-08-18 13:08:54] [INFO] [CASR] Number of clusters: 11
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl1>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl1/id:000140,sig:10,src:000473+001776,time:97076090,execs:8677884,op:splice,rep:1
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: OverflowError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/scanner.py:1471
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 1
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> OverflowError: 1
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl2>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl2/id:000094,sig:10,src:000199+002204,time:28614928,execs:2898551,op:splice,rep:4
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 2
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl3>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl3/id:000017,sig:10,src:001539,time:1026063,execs:292379,op:havoc,rep:3
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 2
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl4>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl4/id:000017,sig:10,src:001221+000530,time:517518,execs:247659,op:splice,rep:1
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 3
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 3
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl5>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl5/id:000006,sig:10,src:000827,time:221156,execs:128170,op:havoc,rep:1
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: KeyError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py:361
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 4
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> KeyError: 4
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl6>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl6/id:000043,sig:10,src:001293,time:1646571,execs:573887,op:havoc,rep:1
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:266
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 1
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl6/id:000000,sig:10,src:000004,time:59104,execs:44738,op:havoc,rep:2
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 1
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl7>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl7/id:000000,sig:10,src:000009,time:38807,execs:31142,op:havoc,rep:1
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: KeyError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py:361
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 4
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> KeyError: 4
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl8>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl8/id:000006,sig:10,src:000534+000049,time:354776,execs:170563,op:splice,rep:7
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: TypeError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:273
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 4
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> TypeError: 4
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl9>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl9/id:000085,sig:10,src:001266,time:12587845,execs:3097022,op:havoc,rep:2
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: OverflowError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/scanner.py:1471
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 2
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> OverflowError: 2
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl10>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl10/id:000021,sig:10,src:000128+000228,time:1422306,execs:403476,op:splice,rep:2
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: ValueError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/constructor.py:498
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 3
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> ValueError: 3
[2025-08-18 13:08:54] [INFO] [CASR] ==> <cl11>
[2025-08-18 13:08:54] [INFO] [CASR] Crash: /fuzz/yaml_fuzzer_pyafl-out/casr/cl11/id:000005,sig:10,src:000833,time:218012,execs:126600,op:havoc,rep:2
[2025-08-18 13:08:54] [INFO] [CASR]   casrep: NOT_EXPLOITABLE: KeyError: /usr/local/lib/python3.9/dist-packages/ruamel/yaml/resolver.py:361
[2025-08-18 13:08:54] [INFO] [CASR]   Similar crashes: 5
[2025-08-18 13:08:54] [INFO] [CASR] Cluster summary -> KeyError: 5
[2025-08-18 13:08:54] [INFO] [CASR] SUMMARY -> KeyError: 13 OverflowError: 3 TypeError: 13 ValueError: 3
[2025-08-18 13:08:54] [INFO] [CASR] 200 timeout seeds are saved to "/fuzz/yaml_fuzzer_pyafl-out/casr/timeout"
[2025-08-18 13:08:54] [INFO] Crashes and Casr reports are saved in /fuzz/yaml_fuzzer_pyafl-out/casr

Виртуальные окружения

Так же хочется обратить внимание на недавно добавленную поддержку виртуальных окружений в Atheris и python-afl — работа с ними демонстрируется в фаззинге msgspec.

С ними возможно фаззить Python таргеты при помощи Atheris и python-afl без ребилда инструментируемых библиотек. build скрипт в msgspec на момент написания wiki с небольшими упрощениями выглядит так:

... # do some msgspec scpefic job 

python3 -m venv --system-site-packages /atherisVenv
python3 -m venv --system-site-packages /pyAflVenv

# prepare python-afl venv

source /pyAflVenv/bin/activate

pip install python-afl --ignore-installed
pip install coverage --ignore-installed

... # do some msgspec scpefic job 

cd /msgspec

MSGSPEC_DEBUG=1 CC=afl-clang-fast CFLAGS="-fsanitize=address -Wl,-rpath=/usr/lib/clang/14.0.6/lib/linux/" LDFLAGS="/usr/local/lib/afl/afl-compiler-rt.o /usr/lib/clang/14.0.6/lib/linux/libclang_rt.asan-x86_64.so" LDSHARED="clang -shared" pip3 install --ignore-installed .
rm -rf build

deactivate


# Prepare Atheris venv
source /atherisVenv/bin/activate

pip install atheris --ignore-installed
pip install coverage --ignore-installed

...  # do some msgspec scpefic job 

cd /msgspec
MSGSPEC_DEBUG=1 CC=clang CFLAGS="-g -fsanitize=fuzzer-no-link,address" LDSHARED="clang -shared" pip3 install --ignore-installed .

deactivate

А конфигурационные toml файлы для таргетов дополнительно содержат поле

venv = "/atherisVenv/" 

или

venv = "/pyAflVenv/"

Заключение

В заключении я бы хотел сказать, что Atheris и python-afl — хорошие фаззеры для Python кода. Интерфейс sydr-fuzz делает фаззинг более удобным и приятным. Ну и конечно же casr, куда же без него! Сортировка аварийных завершений для Python сильно упрощает процесс работы.


Андрей Федотов и Алексей Маренков

Clone this wiki locally