├── tests ├── __init__.py ├── c_extension.py └── test_mutators_generic.py ├── requirements.txt ├── pyfuzzer ├── mutators │ ├── __init__.py │ └── generic.py ├── version.py ├── __main__.py ├── pyfuzzer_common.h ├── pyfuzzer.c ├── pyfuzzer_print.c ├── pyfuzzer_common.c └── __init__.py ├── MANIFEST.in ├── examples ├── hello_world_cython │ ├── setup.py │ ├── hello_world.pyx │ └── Makefile ├── hello_world_custom_mutator │ ├── hello_world_mutator.py │ └── hello_world.c ├── hello_world │ └── hello_world.c └── hello_world_fatal_error │ └── hello_world.c ├── .travis.yml ├── Makefile ├── LICENSE ├── setup.py ├── .gitignore └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pygments 2 | -------------------------------------------------------------------------------- /pyfuzzer/mutators/__init__.py: -------------------------------------------------------------------------------- 1 | # Package. 2 | -------------------------------------------------------------------------------- /pyfuzzer/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.19.0' 2 | -------------------------------------------------------------------------------- /pyfuzzer/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include Makefile 3 | include pyfuzzer/*.h 4 | include pyfuzzer/*.c 5 | -------------------------------------------------------------------------------- /examples/hello_world_cython/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from Cython.Build import cythonize 3 | 4 | setup(ext_modules=cythonize("hello_world.pyx")) 5 | -------------------------------------------------------------------------------- /examples/hello_world_cython/hello_world.pyx: -------------------------------------------------------------------------------- 1 | def tell(message): 2 | if not isinstance(message, str): 3 | return 4 | 5 | length = len(message) 6 | 7 | if length == 0: 8 | return 5 9 | elif length == 1: 10 | return 1 11 | elif length == 2: 12 | return 'Hello!' 13 | else: 14 | return 0 15 | -------------------------------------------------------------------------------- /examples/hello_world_cython/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python3 setup.py build_ext --inplace 3 | python3 -c "import hello_world ; print('tell(\'Hi!\') =', hello_world.tell('Hi!'))" 4 | PYTHONPATH=../.. python3 -m pyfuzzer clean 5 | PYTHONPATH=../.. python3 -m pyfuzzer run -l max_total_time=1 hello_world.c 6 | PYTHONPATH=../.. python3 -m pyfuzzer print_coverage 7 | PYTHONPATH=../.. python3 -m pyfuzzer print_corpus 8 | -------------------------------------------------------------------------------- /tests/c_extension.py: -------------------------------------------------------------------------------- 1 | class Counter: 2 | 3 | def __init__(self): 4 | self._counter = 0 5 | 6 | def get(self): 7 | return self._counter 8 | 9 | def increment(self, value): 10 | self._counter += value 11 | 12 | def decrement(self, value): 13 | self._counter -= value 14 | 15 | 16 | def add(a, b): 17 | return a + b 18 | 19 | 20 | def func_0(): 21 | return 'func 0' 22 | 23 | 24 | def func_1(): 25 | return 'func 1' 26 | 27 | 28 | def noop(v): 29 | return v 30 | 31 | 32 | def sub(a: int, b: int): 33 | return a - b 34 | -------------------------------------------------------------------------------- /examples/hello_world_custom_mutator/hello_world_mutator.py: -------------------------------------------------------------------------------- 1 | import pyfuzzer.mutators.generic 2 | from pyfuzzer.mutators.generic import print_callable 3 | 4 | 5 | def mutate(data): 6 | if data[0] == 0: 7 | return None 8 | else: 9 | return data[1:] 10 | 11 | 12 | class Mutator(pyfuzzer.mutators.generic.Mutator): 13 | 14 | def test_one_input(self, data): 15 | return self._module.tell(mutate(data)) 16 | 17 | def test_one_input_print(self, data): 18 | print_callable(self._module.tell, [mutate(data)], 4 * ' ') 19 | 20 | 21 | def setup(module): 22 | return Mutator(module) 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.7" 5 | 6 | install: 7 | - pip install coveralls 8 | - pip install -r requirements.txt 9 | 10 | script: 11 | - coverage run --source=pyfuzzer setup.py test 12 | - (cd examples/hello_world && 13 | PYTHONPATH=../.. python -m pyfuzzer clean && 14 | PYTHONPATH=../.. python -m pyfuzzer run 15 | -l max_total_time=1 hello_world.c && 16 | PYTHONPATH=../.. python -m pyfuzzer print_corpus && 17 | PYTHONPATH=../.. python -m pyfuzzer print_crashes) 18 | - (cd examples/hello_world_custom_mutator && 19 | PYTHONPATH=.:../.. 20 | python -m pyfuzzer run 21 | -l max_total_time=1 22 | -m hello_world_mutator.py 23 | hello_world.c) 24 | 25 | after_success: 26 | - coveralls 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python3 setup.py test 3 | cd examples/hello_world && \ 4 | PYTHONPATH=../.. \ 5 | python3 -m pyfuzzer clean && \ 6 | PYTHONPATH=../.. \ 7 | python3 -m pyfuzzer run -l max_total_time=1 hello_world.c && \ 8 | PYTHONPATH=../.. \ 9 | python3 -m pyfuzzer print_coverage && \ 10 | PYTHONPATH=../.. \ 11 | python3 -m pyfuzzer print_corpus && \ 12 | PYTHONPATH=../.. \ 13 | python3 -m pyfuzzer print_crashes 14 | cd examples/hello_world_custom_mutator && \ 15 | PYTHONPATH=.:../.. \ 16 | python3 -m pyfuzzer run \ 17 | -l max_total_time=1 \ 18 | -m hello_world_mutator.py \ 19 | hello_world.c 20 | cd examples/hello_world_fatal_error && \ 21 | ! PYTHONPATH=../.. \ 22 | python3 -m pyfuzzer run hello_world.c && \ 23 | PYTHONPATH=../.. \ 24 | python3 -m pyfuzzer print_coverage 25 | 26 | release-to-pypi: 27 | python setup.py sdist 28 | twine upload dist/* 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Erik Moqvist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | from setuptools import find_packages 5 | import re 6 | 7 | 8 | def find_version(): 9 | return re.search(r"^__version__ = '(.*)'$", 10 | open('pyfuzzer/version.py', 'r').read(), 11 | re.MULTILINE).group(1) 12 | 13 | 14 | setup(name='pyfuzzer', 15 | version=find_version(), 16 | description='Fuzz test Python modules with libFuzzer.', 17 | long_description=open('README.rst', 'r').read(), 18 | author='Erik Moqvist', 19 | author_email='erik.moqvist@gmail.com', 20 | license='MIT', 21 | classifiers=[ 22 | 'License :: OSI Approved :: MIT License', 23 | 'Programming Language :: Python :: 3', 24 | ], 25 | keywords=['fuzz', 'fuzzying', 'test'], 26 | url='https://github.com/eerimoq/pyfuzzer', 27 | packages=find_packages(exclude=['tests']), 28 | install_requires=[ 29 | 'Pygments' 30 | ], 31 | test_suite="tests", 32 | include_package_data=True, 33 | entry_points = { 34 | 'console_scripts': ['pyfuzzer=pyfuzzer.__init__:main'] 35 | }) 36 | -------------------------------------------------------------------------------- /examples/hello_world/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | PyDoc_STRVAR( 4 | tell_doc, 5 | "tell(message)\n" 6 | "--\n" 7 | "Arguments: (message:bytes)\n"); 8 | 9 | static PyObject *m_tell(PyObject *module_p, PyObject *message_p) 10 | { 11 | Py_ssize_t size; 12 | char* buf_p; 13 | int res; 14 | PyObject *res_p; 15 | 16 | res = PyBytes_AsStringAndSize(message_p, &buf_p, &size); 17 | 18 | if (res != -1) { 19 | switch (size) { 20 | 21 | case 0: 22 | res_p = PyLong_FromLong(5); 23 | break; 24 | 25 | case 1: 26 | res_p = PyBool_FromLong(1); 27 | break; 28 | 29 | case 2: 30 | res_p = PyBytes_FromString("Hello!"); 31 | break; 32 | 33 | default: 34 | res_p = PyLong_FromLong(0); 35 | break; 36 | } 37 | } else { 38 | res_p = NULL; 39 | } 40 | 41 | return (res_p); 42 | } 43 | 44 | static struct PyMethodDef methods[] = { 45 | { "tell", m_tell, METH_O, tell_doc}, 46 | { NULL } 47 | }; 48 | 49 | static PyModuleDef module = { 50 | PyModuleDef_HEAD_INIT, 51 | .m_name = "hello_world", 52 | .m_size = -1, 53 | .m_methods = methods 54 | }; 55 | 56 | PyMODINIT_FUNC PyInit_hello_world(void) 57 | { 58 | return (PyModule_Create(&module)); 59 | } 60 | -------------------------------------------------------------------------------- /examples/hello_world_custom_mutator/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | PyDoc_STRVAR( 4 | tell_doc, 5 | "tell(message)\n" 6 | "--\n" 7 | "Arguments: (message:bytes)\n"); 8 | 9 | static PyObject *m_tell(PyObject *module_p, PyObject *message_p) 10 | { 11 | Py_ssize_t size; 12 | char* buf_p; 13 | int res; 14 | PyObject *res_p; 15 | 16 | res = PyBytes_AsStringAndSize(message_p, &buf_p, &size); 17 | 18 | if (res != -1) { 19 | switch (size) { 20 | 21 | case 0: 22 | res_p = PyLong_FromLong(5); 23 | break; 24 | 25 | case 1: 26 | res_p = PyBool_FromLong(1); 27 | break; 28 | 29 | case 2: 30 | res_p = PyBytes_FromString("Hello!"); 31 | break; 32 | 33 | default: 34 | res_p = PyLong_FromLong(0); 35 | break; 36 | } 37 | } else { 38 | res_p = NULL; 39 | } 40 | 41 | return (res_p); 42 | } 43 | 44 | static struct PyMethodDef methods[] = { 45 | { "tell", m_tell, METH_O, tell_doc}, 46 | { NULL } 47 | }; 48 | 49 | static PyModuleDef module = { 50 | PyModuleDef_HEAD_INIT, 51 | .m_name = "hello_world", 52 | .m_size = -1, 53 | .m_methods = methods 54 | }; 55 | 56 | PyMODINIT_FUNC PyInit_hello_world(void) 57 | { 58 | return (PyModule_Create(&module)); 59 | } 60 | -------------------------------------------------------------------------------- /examples/hello_world_fatal_error/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | PyDoc_STRVAR( 4 | tell_doc, 5 | "tell(message)\n" 6 | "--\n" 7 | "Arguments: (message:bytes)\n"); 8 | 9 | static PyObject *m_tell(PyObject *module_p, PyObject *message_p) 10 | { 11 | Py_ssize_t size; 12 | char* buf_p; 13 | int res; 14 | PyObject *res_p; 15 | 16 | res = PyBytes_AsStringAndSize(message_p, &buf_p, &size); 17 | 18 | if (res != -1) { 19 | switch (size) { 20 | 21 | case 0: 22 | res_p = PyLong_FromLong(5); 23 | break; 24 | 25 | case 1: 26 | res_p = PyBool_FromLong(1); 27 | break; 28 | 29 | case 2: 30 | res_p = PyBytes_FromString("Hello!"); 31 | break; 32 | 33 | default: 34 | /* Not incremeting the reference count will result in 35 | "Fatal Python error: deallocating None". */ 36 | res_p = Py_None; 37 | break; 38 | } 39 | } else { 40 | res_p = NULL; 41 | } 42 | 43 | return (res_p); 44 | } 45 | 46 | static struct PyMethodDef methods[] = { 47 | { "tell", m_tell, METH_O, tell_doc}, 48 | { NULL } 49 | }; 50 | 51 | static PyModuleDef module = { 52 | PyModuleDef_HEAD_INIT, 53 | .m_name = "hello_world", 54 | .m_size = -1, 55 | .m_methods = methods 56 | }; 57 | 58 | PyMODINIT_FUNC PyInit_hello_world(void) 59 | { 60 | return (PyModule_Create(&module)); 61 | } 62 | -------------------------------------------------------------------------------- /pyfuzzer/pyfuzzer_common.h: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Erik Moqvist 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, copy, 10 | * modify, merge, publish, distribute, sublicense, and/or sell copies 11 | * of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | #ifndef PYFUZZER_COMMON_H 28 | #define PYFUZZER_COMMON_H 29 | 30 | #include 31 | 32 | void pyfuzzer_init(PyObject **test_one_input_pp, 33 | PyObject **args_pp, 34 | PyObject **test_one_input_print_pp); 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | examples/*/module.c 107 | examples/*/pyfuzzer 108 | examples/*/pyfuzzer_print 109 | examples/*/pyfuzzer_print_corpus 110 | examples/*/mutator.py 111 | examples/hello_world_cython/hello_world.c 112 | *.profdata 113 | *.profraw 114 | crash-* 115 | corpus -------------------------------------------------------------------------------- /pyfuzzer/pyfuzzer.c: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Erik Moqvist 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, copy, 10 | * modify, merge, publish, distribute, sublicense, and/or sell copies 11 | * of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | #include "pyfuzzer_common.h" 28 | 29 | int LLVMFuzzerTestOneInput(const uint8_t *data_p, size_t size) 30 | { 31 | static PyObject *test_one_input_p = NULL; 32 | static PyObject *args_p; 33 | PyObject *res_p; 34 | PyObject *data_obj_p; 35 | 36 | if (test_one_input_p == NULL) { 37 | pyfuzzer_init(&test_one_input_p, &args_p, NULL); 38 | } 39 | 40 | data_obj_p = PyBytes_FromStringAndSize((const char *)data_p, size); 41 | 42 | if (data_obj_p == NULL) { 43 | PyErr_Print(); 44 | exit(1); 45 | } 46 | 47 | PyTuple_SET_ITEM(args_p, 0, data_obj_p); 48 | res_p = PyObject_CallObject(test_one_input_p, args_p); 49 | Py_DECREF(data_obj_p); 50 | 51 | if (res_p != NULL) { 52 | /* printf("res: %s\n", PyUnicode_AsUTF8(PyObject_Str(res_p))); */ 53 | Py_DECREF(res_p); 54 | } 55 | 56 | /* if (PyErr_Occurred()) { */ 57 | /* PyErr_Print(); */ 58 | /* } */ 59 | 60 | PyErr_Clear(); 61 | 62 | return (0); 63 | } 64 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |buildstatus|_ 2 | |coverage|_ 3 | 4 | About 5 | ===== 6 | 7 | Use `libFuzzer`_ to fuzz test Python 3.6+ C extension modules. 8 | 9 | Installation 10 | ============ 11 | 12 | `clang 8` or later is required. 13 | 14 | .. code-block:: text 15 | 16 | $ apt install clang 17 | $ pip install pyfuzzer 18 | 19 | Example Usage 20 | ============= 21 | 22 | Hello world 23 | ----------- 24 | 25 | Use the default mutator ``pyfuzzer.mutators.generic`` when testing the 26 | module ``hello_world``. 27 | 28 | .. code-block:: text 29 | 30 | $ cd examples/hello_world 31 | $ pyfuzzer run -l max_total_time=1 hello_world.c 32 | 33 | 34 | Print the function calls that found new code paths. This information 35 | is usually useful when writing unit tests. 36 | 37 | .. code-block:: text 38 | 39 | $ pyfuzzer print_corpus 40 | corpus/25409981b15b978c9fb5a5a2f4dab0c4b04e295f: 41 | tell(b'') = 5 42 | corpus/a8a4e6c9abfd3c6cba171579190702ddc1317df0: 43 | tell(b'\xfd#') = b'Hello!' 44 | corpus/80f87702ef9fbe4baf17095c79ff928b9fa1ea14: 45 | tell(b'\x00') = True 46 | corpus/be3d1b7df189727b2cecd6526aa8f24abbf6df10: 47 | tell(b'\x00\xfd\x00') = 0 48 | corpus/defd8787d638f271cd83362eafe7fdeed9fa4a8f: 49 | tell(None) raises: 50 | Traceback (most recent call last): 51 | File "/home/erik/workspace/pyfuzzer/pyfuzzer/mutators/utils.py", line 35, in print_callable 52 | res = obj(*args) 53 | TypeError: expected bytes, NoneType found 54 | 55 | See the `hello_world`_ for all files. 56 | 57 | Hello world fatal error 58 | ----------------------- 59 | 60 | Similar to the previous example, but triggers a fatal error when 61 | ``tell()`` is called with a bytes object longer than 2 bytes as its 62 | first argument. 63 | 64 | .. code-block:: text 65 | 66 | $ cd examples/hello_world_fatal_error 67 | $ pyfuzzer run hello_world.c 68 | ... 69 | Fatal Python error: deallocating None 70 | 71 | Current thread 0x00007f7ca99c2780 (most recent call first): 72 | ... 73 | 74 | Print the function call that caused the crash. Just as expected, the 75 | first argument is clearly longer than 2 bytes. 76 | 77 | .. code-block:: text 78 | 79 | $ pyfuzzer print_crashes 80 | crash-1013ed88cd71fd14407b2bdbc17b95d7bc317c21: 81 | tell(b'\n\xbf+') = None 82 | 83 | See the `hello_world_fatal_error`_ for all files. 84 | 85 | Custom mutator 86 | -------------- 87 | 88 | Use the custom mutator ``hello_world_mutator`` when testing the module 89 | ``hello_world``. 90 | 91 | Testing with a custom mutator is often more efficient than using a 92 | generic one. 93 | 94 | .. code-block:: text 95 | 96 | $ cd examples/hello_world_custom_mutator 97 | $ pyfuzzer run -l max_total_time=1 -m hello_world_mutator.py hello_world.c 98 | ... 99 | 100 | See the `hello_world_custom_mutator`_ for all files. 101 | 102 | Mutators 103 | ======== 104 | 105 | A mutator module uses data from `libFuzzer`_ to test a module. A 106 | mutator module must implement the function ``setup(module)``, where 107 | ``module`` is the module under test. It shall return a mutator 108 | instance that implements the methods ``test_one_input(self, data)`` 109 | and ``test_one_input_print(self, data)``, where ``data`` is the data 110 | generated by `libFuzzer`_ (as a bytes object). 111 | 112 | ``test_one_input(self, data)`` performs the actual fuzz testing, while 113 | ``test_one_input_print(self, data)`` prints corpus and crashes. 114 | 115 | A minimal mutator fuzz testing a CRC-32 algorithm could look like 116 | below. It simply calls ``crc_32()`` with ``data`` as its only 117 | argument. 118 | 119 | .. code-block:: python 120 | 121 | from pyfuzzer.mutators.generic import print_callable 122 | 123 | class Mutator: 124 | 125 | def __init__(self, module): 126 | self._module = module 127 | 128 | def test_one_input(self, data): 129 | return self._module.crc_32(data) 130 | 131 | def test_one_input_print(self, data): 132 | print_callable(self._module.crc_32, [data]) 133 | 134 | def setup(module): 135 | return Mutator(module) 136 | 137 | Ideas 138 | ===== 139 | 140 | - Add support to fuzz test pure Python modules by generating C code 141 | using Cython. 142 | 143 | .. |buildstatus| image:: https://travis-ci.org/eerimoq/pyfuzzer.svg 144 | .. _buildstatus: https://travis-ci.org/eerimoq/pyfuzzer 145 | 146 | .. |coverage| image:: https://coveralls.io/repos/github/eerimoq/pyfuzzer/badge.svg?branch=master 147 | .. _coverage: https://coveralls.io/github/eerimoq/pyfuzzer 148 | 149 | .. _libFuzzer: https://llvm.org/docs/LibFuzzer.html 150 | 151 | .. _hello_world: https://github.com/eerimoq/pyfuzzer/tree/master/examples/hello_world 152 | 153 | .. _hello_world_fatal_error: https://github.com/eerimoq/pyfuzzer/tree/master/examples/hello_world_fatal_error 154 | 155 | .. _hello_world_custom_mutator: https://github.com/eerimoq/pyfuzzer/tree/master/examples/hello_world_custom_mutator 156 | -------------------------------------------------------------------------------- /pyfuzzer/pyfuzzer_print.c: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Erik Moqvist 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, copy, 10 | * modify, merge, publish, distribute, sublicense, and/or sell copies 11 | * of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | #include 28 | #include "pyfuzzer_common.h" 29 | 30 | static void *read_unit(const char *unit_path_p, Py_ssize_t *size_p) 31 | { 32 | void *buf_p; 33 | FILE *fin_p; 34 | ssize_t res; 35 | 36 | fin_p = fopen(unit_path_p, "rb"); 37 | 38 | if (fin_p == NULL) { 39 | fprintf(stderr, "Failed to open '%s'.\n", unit_path_p); 40 | exit(1); 41 | } 42 | 43 | res = fseek(fin_p, 0, SEEK_END); 44 | 45 | if (res != 0) { 46 | fprintf(stderr, "fseek() failed.\n"); 47 | exit(1); 48 | } 49 | 50 | *size_p = ftell(fin_p); 51 | 52 | if (*size_p < 0) { 53 | fprintf(stderr, "ftell() failed.\n"); 54 | exit(1); 55 | } 56 | 57 | res = fseek(fin_p, 0, SEEK_SET); 58 | 59 | if (res != 0) { 60 | fprintf(stderr, "fseek() failed.\n"); 61 | exit(1); 62 | } 63 | 64 | buf_p = malloc(*size_p); 65 | 66 | if (buf_p == NULL) { 67 | fprintf(stderr, "malloc() failed.\n"); 68 | exit(1); 69 | } 70 | 71 | res = fread(buf_p, 1, *size_p, fin_p); 72 | 73 | if (res != *size_p) { 74 | fprintf(stderr, 75 | "fread() failed with %d (size: %d).\n", 76 | (int)res, 77 | (int)*size_p); 78 | exit(1); 79 | } 80 | 81 | fclose(fin_p); 82 | 83 | return (buf_p); 84 | } 85 | 86 | static void print_unit(PyObject *test_one_input_print_p, 87 | const char *path_p) 88 | { 89 | PyObject *args_p; 90 | PyObject *res_p; 91 | Py_ssize_t size; 92 | void *buf_p; 93 | 94 | printf("%s:\n", path_p); 95 | 96 | buf_p = read_unit(path_p, &size); 97 | args_p = PyTuple_Pack(1, PyBytes_FromStringAndSize((const char *)buf_p, size)); 98 | 99 | if (args_p == NULL) { 100 | printf("Failed to create arguments.\n"); 101 | exit(1); 102 | } 103 | 104 | res_p = PyObject_CallObject(test_one_input_print_p, args_p); 105 | 106 | if (res_p != NULL) { 107 | Py_DECREF(res_p); 108 | } 109 | 110 | if (PyErr_Occurred()) { 111 | PyErr_Print(); 112 | } 113 | 114 | PyErr_Clear(); 115 | free(buf_p); 116 | } 117 | 118 | static bool char_in_string(char c, const char *str_p) 119 | { 120 | while (*str_p != '\0') { 121 | if (c == *str_p) { 122 | return (true); 123 | } 124 | 125 | str_p++; 126 | } 127 | 128 | return (false); 129 | } 130 | 131 | static char *strip(char *str_p, const char *strip_p) 132 | { 133 | char *begin_p; 134 | size_t length; 135 | 136 | /* Strip whitespace characters by default. */ 137 | if (strip_p == NULL) { 138 | strip_p = "\t\n\x0b\x0c\r "; 139 | } 140 | 141 | /* String leading characters. */ 142 | while ((*str_p != '\0') && char_in_string(*str_p, strip_p)) { 143 | str_p++; 144 | } 145 | 146 | begin_p = str_p; 147 | 148 | /* Strip training characters. */ 149 | length = strlen(str_p); 150 | str_p += (length - 1); 151 | 152 | while ((str_p >= begin_p) && char_in_string(*str_p, strip_p)) { 153 | *str_p = '\0'; 154 | str_p--; 155 | } 156 | 157 | return (begin_p); 158 | } 159 | 160 | int main(int argc, const char *argv[]) 161 | { 162 | PyObject *test_one_input_print_p; 163 | char path[256]; 164 | char *path_p; 165 | 166 | if (argc != 1) { 167 | fprintf(stderr, "Wrong number of arguments %d.\n", argc); 168 | exit(1); 169 | } 170 | 171 | pyfuzzer_init(NULL, NULL, &test_one_input_print_p); 172 | 173 | while (true) { 174 | path_p = fgets(&path[0], sizeof(path), stdin); 175 | 176 | if (path_p == NULL) { 177 | break; 178 | } 179 | 180 | print_unit(test_one_input_print_p, strip(path_p, NULL)); 181 | } 182 | 183 | return (0); 184 | } 185 | -------------------------------------------------------------------------------- /pyfuzzer/pyfuzzer_common.c: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Erik Moqvist 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, copy, 10 | * modify, merge, publish, distribute, sublicense, and/or sell copies 11 | * of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | #include "pyfuzzer_common.h" 28 | 29 | extern PyObject *pyfuzzer_module_init(void); 30 | 31 | static PyObject *import_module_under_test(void) 32 | { 33 | PyObject *module_p; 34 | 35 | printf("Importing module under test... "); 36 | module_p = pyfuzzer_module_init(); 37 | 38 | if (module_p != NULL) { 39 | printf("done.\n"); 40 | } else { 41 | printf("failed.\n"); 42 | PyErr_Print(); 43 | exit(1); 44 | } 45 | 46 | return (module_p); 47 | } 48 | 49 | static PyObject *import_mutator_module(void) 50 | { 51 | PyObject *module_p; 52 | 53 | printf("Importing custom mutator... "); 54 | module_p = PyImport_ImportModule("mutator"); 55 | 56 | if (module_p != NULL) { 57 | printf("done.\n"); 58 | } else { 59 | printf("failed.\n"); 60 | PyErr_Print(); 61 | 62 | printf("Importing mutator 'pyfuzzer.mutators.generic'... "); 63 | module_p = PyImport_ImportModule("pyfuzzer.mutators.generic"); 64 | 65 | if (module_p != NULL) { 66 | printf("done.\n"); 67 | } else { 68 | printf("failed.\n"); 69 | PyErr_Print(); 70 | printf("sys.path: %s\n", 71 | PyUnicode_AsUTF8(PyObject_Str(PySys_GetObject("path")))); 72 | exit(1); 73 | } 74 | } 75 | 76 | return (module_p); 77 | } 78 | 79 | static PyObject *find_setup_function(PyObject *module_p) 80 | { 81 | PyObject *setup_p; 82 | 83 | printf("Finding function 'setup' in mutator module... "); 84 | setup_p = PyObject_GetAttrString(module_p, "setup"); 85 | 86 | if (setup_p != NULL) { 87 | printf("done.\n"); 88 | } else { 89 | printf("failed.\n"); 90 | PyErr_Print(); 91 | exit(1); 92 | } 93 | 94 | return (setup_p); 95 | } 96 | 97 | static PyObject *call_setup_function(PyObject *setup_p, 98 | PyObject *module_under_test_p) 99 | { 100 | PyObject *mutator_p; 101 | PyObject *args_p; 102 | 103 | args_p = PyTuple_Pack(1, module_under_test_p); 104 | 105 | if (args_p == NULL) { 106 | printf("Failed to create arguments.\n"); 107 | exit(1); 108 | } 109 | 110 | PyErr_Clear(); 111 | mutator_p = PyObject_CallObject(setup_p, args_p); 112 | 113 | if (PyErr_Occurred()) { 114 | PyErr_Print(); 115 | exit(1); 116 | } 117 | 118 | return (mutator_p); 119 | } 120 | 121 | static PyObject *find_test_one_input_method(PyObject *mutator_p) 122 | { 123 | PyObject *test_one_input_p; 124 | 125 | printf("Finding function 'test_one_input' in mutator... "); 126 | test_one_input_p = PyObject_GetAttrString(mutator_p, "test_one_input"); 127 | 128 | if (test_one_input_p != NULL) { 129 | printf("done.\n"); 130 | } else { 131 | printf("failed.\n"); 132 | PyErr_Print(); 133 | exit(1); 134 | } 135 | 136 | return (test_one_input_p); 137 | } 138 | 139 | static PyObject *find_test_one_input_print_method(PyObject *mutator_p) 140 | { 141 | PyObject *test_one_input_print_p; 142 | 143 | printf("Finding function 'test_one_input_print' in mutator... "); 144 | test_one_input_print_p = PyObject_GetAttrString(mutator_p, 145 | "test_one_input_print"); 146 | 147 | if (test_one_input_print_p != NULL) { 148 | printf("done.\n"); 149 | } else { 150 | printf("failed.\n"); 151 | PyErr_Print(); 152 | exit(1); 153 | } 154 | 155 | return (test_one_input_print_p); 156 | } 157 | 158 | static PyObject *create_test_one_input_args(void) 159 | { 160 | PyObject *args_p; 161 | 162 | args_p = PyTuple_New(1); 163 | 164 | if (args_p == NULL) { 165 | PyErr_Print(); 166 | exit(1); 167 | } 168 | 169 | return (args_p); 170 | } 171 | 172 | void pyfuzzer_init(PyObject **test_one_input_pp, 173 | PyObject **args_pp, 174 | PyObject **test_one_input_print_pp) 175 | { 176 | PyObject *module_under_test_p; 177 | PyObject *mutator_module_p; 178 | PyObject *setup_p; 179 | PyObject *mutator_p; 180 | 181 | Py_Initialize(); 182 | 183 | module_under_test_p = import_module_under_test(); 184 | mutator_module_p = import_mutator_module(); 185 | setup_p = find_setup_function(mutator_module_p); 186 | mutator_p = call_setup_function(setup_p, module_under_test_p); 187 | 188 | if (test_one_input_pp != NULL) { 189 | *test_one_input_pp = find_test_one_input_method(mutator_p); 190 | } 191 | 192 | if (args_pp != NULL) { 193 | *args_pp = create_test_one_input_args(); 194 | } 195 | 196 | if (test_one_input_print_pp != NULL) { 197 | *test_one_input_print_pp = find_test_one_input_print_method(mutator_p); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/test_mutators_generic.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | from unittest.mock import patch 4 | import struct 5 | from io import StringIO 6 | import re 7 | 8 | from pyfuzzer.mutators.generic import setup 9 | 10 | from . import c_extension 11 | 12 | 13 | class MutatorsGenericTest(unittest.TestCase): 14 | 15 | maxDiff = None 16 | 17 | def test_test_one_input(self): 18 | datas = [ 19 | # add(1, 2) -> 3 20 | (b'\x00\x00\x01\x02' 21 | + b'\x00' + struct.pack('>q', 1) 22 | + b'\x00' + struct.pack('>q', 2), 3), 23 | # add(1, 2) -> 3 using signature. 24 | (b'\x00\x00\x00' 25 | + b'\x00' + struct.pack('>q', 1) 26 | + b'\x00' + struct.pack('>q', 2), 3), 27 | # func_0() 28 | (b'\x00\x01\x00', 'func 0'), 29 | # func_1() 30 | (b'\x00\x02\x00', 'func 1'), 31 | # sub(2, 3) -> -1 using signature and annotations. 32 | (b'\x00\x04\x00' + struct.pack('>q', 2) + struct.pack('>q', 3), -1) 33 | ] 34 | 35 | mutator = setup(c_extension) 36 | 37 | for data, res in datas: 38 | self.assertEqual(mutator.test_one_input(data), res) 39 | 40 | def test_test_one_input_print(self): 41 | datas = [ 42 | (b'\x00\x00\x01\x02' 43 | + b'\x00' + struct.pack('>q', 1) 44 | + b'\x00' + struct.pack('>q', 2)), 45 | b'\x00\x01\x00', 46 | b'\x00\x02\x00' 47 | ] 48 | 49 | stdout = StringIO() 50 | mutator = setup(c_extension) 51 | 52 | with patch('sys.stdout', stdout): 53 | for data in datas: 54 | mutator.test_one_input_print(data, colors=False) 55 | 56 | self.assertEqual(stdout.getvalue(), 57 | ' add(1, 2) = 3\n' 58 | " func_0() = 'func 0'\n" 59 | " func_1() = 'func 1'\n") 60 | 61 | def test_test_one_input_counter(self): 62 | mutator = setup(c_extension) 63 | 64 | # counter = Counter() 65 | # counter.get() = 0 66 | # counter.increment(1) 67 | # counter.decrement(2) 68 | # counter.get() = -1 69 | mutator.test_one_input( 70 | b'\x01\x00\x00\x04' 71 | b'\x01\x00' 72 | b'\x02\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x01' 73 | b'\x03\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x02' 74 | b'\x01\x00' 75 | ) 76 | 77 | def test_test_one_input_print_counter(self): 78 | stdout = StringIO() 79 | mutator = setup(c_extension) 80 | 81 | with patch('sys.stdout', stdout): 82 | mutator.test_one_input_print( 83 | b'\x01\x00\x00\x04' 84 | b'\x01\x00' 85 | b'\x02\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x01' 86 | b'\x03\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x02' 87 | b'\x01\x00', 88 | colors=False) 89 | 90 | output = re.sub(r'object at 0x[0-9a-f]+', 91 | 'object at
', 92 | stdout.getvalue()) 93 | 94 | self.assertEqual( 95 | output, 96 | " Counter() = >\n" 97 | " get(>) = 0\n" 98 | " increment(>, 1) = None\n" 99 | " decrement(>, 2) = None\n" 100 | " get(>) = -1\n") 101 | 102 | def test_test_one_input_noop(self): 103 | datas = [ 104 | # int 105 | (b'\x00\x03\x01\x01' + b'\x00' + struct.pack('>q', 5), 5), 106 | # bool 107 | (b'\x00\x03\x01\x01' + b'\x01\xff', True), 108 | # str 109 | (b'\x00\x03\x01\x01' + b'\x02\x03\x31\x32\x33', '123'), 110 | # bytes 111 | (b'\x00\x03\x01\x01' + b'\x03\x03\x31\x32\x33', b'123'), 112 | # None 113 | (b'\x00\x03\x01\x01' + b'\x04', None), 114 | # list 115 | (b'\x00\x03\x01\x01' + b'\x05\x00', []), 116 | # dict 117 | (b'\x00\x03\x01\x01' + b'\x06\x00', {}), 118 | # bytearray 119 | (b'\x00\x03\x01\x01' + b'\x07\x03', bytearray(b'\x00\x00\x00')) 120 | ] 121 | 122 | mutator = setup(c_extension) 123 | 124 | for data, res in datas: 125 | self.assertEqual(mutator.test_one_input(data), res) 126 | 127 | def test_test_one_input_print_noop(self): 128 | datas = [ 129 | # int 130 | b'\x00\x03\x01\x01' + b'\x00' + struct.pack('>q', 5), 131 | # bool 132 | b'\x00\x03\x01\x01' + b'\x01\xff', 133 | # str 134 | b'\x00\x03\x01\x01' + b'\x02\x03\x31\x32\x33', 135 | # bytes 136 | b'\x00\x03\x01\x01' + b'\x03\x03\x31\x32\x33', 137 | # None 138 | b'\x00\x03\x01\x01' + b'\x04', 139 | # list 140 | b'\x00\x03\x01\x01' + b'\x05\x00', 141 | # dict 142 | b'\x00\x03\x01\x01' + b'\x06\x00', 143 | # bytearray 144 | b'\x00\x03\x01\x01' + b'\x07\x03' 145 | ] 146 | 147 | stdout = StringIO() 148 | mutator = setup(c_extension) 149 | 150 | with patch('sys.stdout', stdout): 151 | for data in datas: 152 | mutator.test_one_input_print(data, colors=False) 153 | 154 | self.assertEqual( 155 | stdout.getvalue(), 156 | " noop(5) = 5\n" 157 | " noop(True) = True\n" 158 | " noop('123') = '123'\n" 159 | " noop(b'123') = b'123'\n" 160 | " noop(None) = None\n" 161 | " noop([]) = []\n" 162 | " noop({}) = {}\n" 163 | " noop(bytearray(b'\\x00\\x00\\x00')) = bytearray(b'\\x00\\x00\\x00')\n") 164 | 165 | 166 | if __name__ == '__main__': 167 | unittest.main() 168 | -------------------------------------------------------------------------------- /pyfuzzer/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import argparse 4 | import subprocess 5 | import sysconfig 6 | import shutil 7 | import glob 8 | 9 | from .version import __version__ 10 | 11 | 12 | SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) 13 | 14 | MODULE_SRC = '''\ 15 | #include 16 | 17 | extern PyMODINIT_FUNC PyInit_{module_name}(void); 18 | 19 | PyMODINIT_FUNC pyfuzzer_module_init(void) 20 | {{ 21 | return (PyInit_{module_name}()); 22 | }} 23 | ''' 24 | 25 | 26 | def mkdir_p(name): 27 | if not os.path.exists(name): 28 | os.makedirs(name) 29 | 30 | 31 | def includes(): 32 | include = sysconfig.get_path('include') 33 | 34 | return [f'-I{include}'] 35 | 36 | 37 | def ldflags(): 38 | ldflags = sysconfig.get_config_var('LDFLAGS') 39 | ldversion = sysconfig.get_config_var('LDVERSION') 40 | ldflags += f' -lpython{ldversion}' 41 | 42 | return ldflags.split() 43 | 44 | 45 | def run_command(command, env=None): 46 | print(' '.join(command)) 47 | 48 | subprocess.check_call(command, env=env) 49 | 50 | 51 | def generate(module_name, mutator): 52 | with open('module.c', 'w') as fout: 53 | fout.write(MODULE_SRC.format(module_name=module_name)) 54 | 55 | if mutator is not None: 56 | shutil.copyfile(mutator, 'mutator.py') 57 | 58 | 59 | def format_cflags(cflags): 60 | return [f'-{cflag}' for cflag in cflags if cflag] 61 | 62 | 63 | def build(csources, cflags): 64 | command = ['clang'] 65 | command += [ 66 | '-fprofile-instr-generate', 67 | '-fcoverage-mapping', 68 | '-g', 69 | '-fsanitize=fuzzer', 70 | '-DCYTHON_PEP489_MULTI_PHASE_INIT=0' 71 | ] 72 | 73 | if cflags: 74 | command += format_cflags(cflags) 75 | else: 76 | command += [ 77 | '-fsanitize=undefined', 78 | '-fsanitize=signed-integer-overflow', 79 | '-fsanitize=alignment', 80 | '-fsanitize=bool', 81 | '-fsanitize=builtin', 82 | '-fsanitize=bounds', 83 | '-fsanitize=enum', 84 | '-fno-sanitize-recover=all' 85 | ] 86 | 87 | command += includes() 88 | command += csources 89 | command += [ 90 | 'module.c', 91 | os.path.join(SCRIPT_DIR, 'pyfuzzer_common.c'), 92 | os.path.join(SCRIPT_DIR, 'pyfuzzer.c') 93 | ] 94 | command += ldflags() 95 | command += [ 96 | '-o', 'pyfuzzer' 97 | ] 98 | 99 | run_command(command) 100 | 101 | 102 | def build_print(csources, cflags): 103 | command = ['clang'] 104 | command += [ 105 | '-DCYTHON_PEP489_MULTI_PHASE_INIT=0' 106 | ] 107 | command += format_cflags(cflags) 108 | command += includes() 109 | command += csources 110 | command += [ 111 | 'module.c', 112 | os.path.join(SCRIPT_DIR, 'pyfuzzer_common.c'), 113 | os.path.join(SCRIPT_DIR, 'pyfuzzer_print.c') 114 | ] 115 | command += ldflags() 116 | command += [ 117 | '-o', 'pyfuzzer_print' 118 | ] 119 | 120 | run_command(command) 121 | 122 | 123 | def run(libfuzzer_arguments): 124 | run_command(['rm', '-f', 'pyfuzzer.profraw']) 125 | mkdir_p('corpus') 126 | command = [ 127 | './pyfuzzer', 128 | 'corpus', 129 | '-print_final_stats=1' 130 | ] 131 | command += [f'-{a}' for a in libfuzzer_arguments] 132 | env = os.environ.copy() 133 | env['LLVM_PROFILE_FILE'] = 'pyfuzzer.profraw' 134 | run_command(command, env=env) 135 | 136 | 137 | def print_coverage(): 138 | run_command([ 139 | 'llvm-profdata', 140 | 'merge', 141 | '-sparse', 'pyfuzzer.profraw', 142 | '-o', 'pyfuzzer.profdata' 143 | ]) 144 | run_command([ 145 | 'llvm-cov', 146 | 'show', 147 | 'pyfuzzer', 148 | '-instr-profile=pyfuzzer.profdata', 149 | '-ignore-filename-regex=/usr/|pyfuzzer.c|module.c' 150 | ]) 151 | 152 | 153 | def do_run(args): 154 | if args.module_name is None: 155 | filename = os.path.basename(args.csources[0]) 156 | module_name = os.path.splitext(filename)[0] 157 | else: 158 | module_name = args.module_name 159 | 160 | generate(module_name, args.mutator) 161 | build(args.csources, args.cflag) 162 | build_print(args.csources, args.cflag) 163 | run(args.libfuzzer_argument) 164 | 165 | 166 | def do_print_corpus(args): 167 | if args.units: 168 | filenames = args.units 169 | else: 170 | filenames = glob.glob('corpus/*') 171 | 172 | paths = '\n'.join(filenames) 173 | 174 | subprocess.run(['./pyfuzzer_print'], input=paths.encode('utf-8'), check=True) 175 | 176 | 177 | def do_print_crashes(args): 178 | if args.units: 179 | filenames = args.units 180 | else: 181 | filenames = glob.glob('crash-*') 182 | 183 | for filename in filenames: 184 | proc = subprocess.run(['./pyfuzzer_print'], input=filename.encode('utf-8')) 185 | print() 186 | 187 | try: 188 | proc.check_returncode() 189 | except Exception as e: 190 | print(e) 191 | 192 | 193 | def do_print_coverage(_args): 194 | print_coverage() 195 | 196 | 197 | def do_clean(_args): 198 | shutil.rmtree('corpus', ignore_errors=True) 199 | 200 | for filename in glob.glob('crash-*'): 201 | os.remove(filename) 202 | 203 | for filename in glob.glob('oom-*'): 204 | os.remove(filename) 205 | 206 | for filename in glob.glob('slow-unit-*'): 207 | os.remove(filename) 208 | 209 | 210 | def main(): 211 | parser = argparse.ArgumentParser( 212 | description='Use libFuzzer to fuzz test Python 3.6+ C extension modules.') 213 | 214 | parser.add_argument('-d', '--debug', action='store_true') 215 | parser.add_argument('--version', 216 | action='version', 217 | version=__version__, 218 | help='Print version information and exit.') 219 | 220 | # Workaround to make the subparser required in Python 3. 221 | subparsers = parser.add_subparsers(title='subcommands', 222 | dest='subcommand') 223 | subparsers.required = True 224 | 225 | # The run subparser. 226 | subparser = subparsers.add_parser( 227 | 'run', 228 | description='Build and run the fuzz tester.') 229 | subparser.add_argument('-m', '--mutator', help='Mutator module.') 230 | subparser.add_argument( 231 | '-l', '--libfuzzer-argument', 232 | action='append', 233 | default=[], 234 | help="Add a libFuzzer command line argument without its leading '-'.") 235 | subparser.add_argument( 236 | '-c', '--cflag', 237 | action='append', 238 | default=[], 239 | help=("Add a C extension compilation flag without its leading '-'. If " 240 | "given, all default sanitizers are removed.")) 241 | subparser.add_argument( 242 | '-M', '--module-name', 243 | help=('C extension module name, or first C source filename without ' 244 | 'extension if not given.')) 245 | subparser.add_argument('csources', nargs='+', help='C extension source files.') 246 | subparser.set_defaults(func=do_run) 247 | 248 | # The print_coverage subparser. 249 | subparser = subparsers.add_parser('print_coverage', 250 | description='Print code coverage.') 251 | subparser.set_defaults(func=do_print_coverage) 252 | 253 | # The print_corpus subparser. 254 | subparser = subparsers.add_parser( 255 | 'print_corpus', 256 | description=('Print corpus units as Python functions with arguments and ' 257 | 'return value or exception.')) 258 | subparser.add_argument('units', 259 | nargs='*', 260 | help='Units to print, or whole corpus if none given.') 261 | subparser.set_defaults(func=do_print_corpus) 262 | 263 | # The print_crashes subparser. 264 | subparser = subparsers.add_parser('print_crashes', 265 | description='Print all crashes.') 266 | subparser.add_argument('units', 267 | nargs='*', 268 | help='Crashes to print, or all if none given.') 269 | subparser.set_defaults(func=do_print_crashes) 270 | 271 | # The clean subparser. 272 | subparser = subparsers.add_parser( 273 | 'clean', 274 | description='Remove the corpus and all crashes to start over.') 275 | subparser.set_defaults(func=do_clean) 276 | 277 | args = parser.parse_args() 278 | 279 | if args.debug: 280 | args.func(args) 281 | else: 282 | try: 283 | args.func(args) 284 | except BaseException as e: 285 | sys.exit('error: ' + str(e)) 286 | -------------------------------------------------------------------------------- /pyfuzzer/mutators/generic.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import struct 3 | import inspect 4 | from io import BytesIO 5 | 6 | from pygments import highlight 7 | from pygments.lexers import PythonLexer 8 | from pygments.formatter import Formatter 9 | from pygments.token import Name 10 | from pygments.token import String 11 | from pygments.token import Operator 12 | from pygments.token import Literal 13 | from pygments.token import Punctuation 14 | from pygments.token import Text 15 | 16 | 17 | def format_value(value, limit): 18 | if isinstance(value, str): 19 | value = f"'{value}'"[:limit] 20 | 21 | if value[-1] != "'": 22 | value += "...'" 23 | elif isinstance(value, bytes): 24 | value = str(value)[:limit] 25 | 26 | if value[-1] != "'": 27 | value += "...'" 28 | elif isinstance(value, bytearray): 29 | value = str(value)[:limit] 30 | 31 | if value[-2] != "'": 32 | value += "...'" 33 | 34 | if value[-1] != ")": 35 | value += ")" 36 | else: 37 | value = str(value)[:limit] 38 | 39 | return value 40 | 41 | 42 | def format_args(args, limit): 43 | """Returns a comma separated list of given arguemnts. Each argument is 44 | at most ``limit`` characters. 45 | 46 | """ 47 | 48 | return ', '.join([format_value(arg, limit) for arg in args]) 49 | 50 | 51 | RESET = '\033[0m' 52 | RED_BOLD = '\033[1;31m' 53 | GREEN = '\033[1;32m' 54 | YELLOW = '\033[0;33m' 55 | YELLOW_BOLD = '\033[1;33m' 56 | MAGENTA = '\033[0;35m' 57 | MAGENTA_BOLD = '\033[1;35m' 58 | CYAN = '\033[0;36m' 59 | 60 | 61 | class DefaultFormatter(Formatter): 62 | 63 | COLOR_MAP = { 64 | Name: CYAN, 65 | String: MAGENTA, 66 | Literal.Number.Integer: GREEN, 67 | Literal.String.Single: MAGENTA, 68 | Punctuation: YELLOW, 69 | Operator: YELLOW_BOLD, 70 | Literal.String.Affix: CYAN, 71 | Literal.String.Escape: MAGENTA, 72 | Name.Builtin: CYAN, 73 | Name.Builtin.Pseudo: YELLOW 74 | } 75 | 76 | def format(self, tokens, fout): 77 | for kind, value in tokens: 78 | if value == '\n': 79 | continue 80 | 81 | try: 82 | color = self.COLOR_MAP[kind] 83 | except KeyError: 84 | # print(kind) 85 | color = RESET 86 | 87 | fout.write(f'{color}{value}{RESET}') 88 | 89 | 90 | class TracebackFormatter(Formatter): 91 | 92 | COLOR_MAP = { 93 | Literal.Number.Integer: MAGENTA_BOLD, 94 | Literal.String.Double: CYAN, 95 | Text: GREEN, 96 | Name.Exception: RED_BOLD 97 | } 98 | 99 | def format(self, tokens, fout): 100 | tokens = list(tokens) 101 | 102 | if tokens[1][1] == 'File': 103 | self.format_location(tokens, fout) 104 | elif tokens[1][0] == Name.Exception: 105 | self.format_exception(tokens, fout) 106 | else: 107 | self.format_none(tokens, fout) 108 | 109 | def format_location(self, tokens, fout): 110 | for kind, value in tokens[:-2]: 111 | try: 112 | color = self.COLOR_MAP[kind] 113 | except KeyError: 114 | color = RESET 115 | 116 | fout.write(f'{color}{value}{RESET}') 117 | 118 | fout.write(f'{CYAN}{tokens[-2][1]}{RESET}') 119 | 120 | def format_exception(self, tokens, fout): 121 | fout.write(tokens[0][1]) 122 | fout.write(f'{RED_BOLD}{tokens[1][1]}{RESET}') 123 | fout.write(tokens[2][1]) 124 | 125 | for kind, value in tokens[3:-1]: 126 | fout.write(f'{CYAN}{value}{RESET}') 127 | 128 | def format_none(self, tokens, fout): 129 | for kind, value in tokens[:-1]: 130 | fout.write(value) 131 | 132 | 133 | def colorize(code, colors): 134 | if colors: 135 | code = highlight(code, PythonLexer(), DefaultFormatter()) 136 | 137 | return code 138 | 139 | 140 | def colorize_traceback(code, colors): 141 | if colors: 142 | code = highlight(code, PythonLexer(), TracebackFormatter()) 143 | 144 | return code 145 | 146 | 147 | def print_callable(obj, args, indent=' ', limit=1024, colors=True): 148 | """Print given callable name and its arguments, call it and then print 149 | the returned value or raised exception. 150 | 151 | """ 152 | 153 | fargs = format_args(args, limit) 154 | 155 | # Print and flush callable name and arguments before calling it 156 | # since it may crash. 157 | print(colorize(f'{indent}{obj.__name__}({fargs})', colors), end='', flush=True) 158 | 159 | try: 160 | res = obj(*args) 161 | print(colorize(f' = {format_value(res, limit)}', colors)) 162 | except Exception: 163 | res = None 164 | print() 165 | 166 | for line in traceback.format_exc().splitlines(): 167 | print(colorize_traceback(f'{indent}{line}', colors)) 168 | 169 | return res 170 | 171 | 172 | def generate_integer(data): 173 | return struct.unpack('>q', data.read(8))[0] 174 | 175 | 176 | def generate_bool(data): 177 | return bool(data.read(1)[0]) 178 | 179 | 180 | def generate_string(data): 181 | return str(data.read(data.read(1)[0]))[2:-1] 182 | 183 | 184 | def generate_bytes(data): 185 | return data.read(data.read(1)[0]) 186 | 187 | 188 | def generate_bytearray(data): 189 | return bytearray(data.read(1)[0]) 190 | 191 | 192 | def generate_none(_data): 193 | return None 194 | 195 | 196 | def generate_list(data): 197 | return generate_args(None, data) 198 | 199 | 200 | def generate_dict(data): 201 | return {value: value for value in generate_args(None, data)} 202 | 203 | 204 | def get_signature(callable): 205 | """Get the signature of given callable. 206 | 207 | """ 208 | 209 | try: 210 | return inspect.signature(callable) 211 | except Exception as e: 212 | return None 213 | 214 | 215 | DATA_KINDS = { 216 | 0: generate_integer, 217 | 1: generate_bool, 218 | 2: generate_string, 219 | 3: generate_bytes, 220 | 4: generate_none, 221 | 5: generate_list, 222 | 6: generate_dict, 223 | 7: generate_bytearray 224 | } 225 | 226 | 227 | def generate_args(signature, data, is_method=False): 228 | args = [] 229 | number_of_args = data.read(1)[0] 230 | 231 | try: 232 | if signature is None: 233 | for _ in range(number_of_args): 234 | args.append(DATA_KINDS[data.read(1)[0] % len(DATA_KINDS)](data)) 235 | else: 236 | if number_of_args == 0: 237 | for parameter in signature.parameters.values(): 238 | # Skip first parameter (self) for methods. 239 | if is_method: 240 | is_method = False 241 | continue 242 | 243 | if parameter.kind == inspect.Parameter.VAR_POSITIONAL: 244 | args += generate_args(None, data) 245 | else: 246 | if parameter.annotation == int: 247 | func = generate_integer 248 | else: 249 | func = DATA_KINDS[data.read(1)[0] % len(DATA_KINDS)] 250 | 251 | args.append(func(data)) 252 | else: 253 | number_of_args = data.read(1)[0] 254 | 255 | for _ in range(number_of_args): 256 | args.append(DATA_KINDS[data.read(1)[0] % len(DATA_KINDS)](data)) 257 | except (IndexError, TypeError, struct.error): 258 | pass 259 | 260 | return args 261 | 262 | 263 | def is_function(member): 264 | return (inspect.isbuiltin(member) 265 | or inspect.isfunction(member) 266 | or type(member).__name__ == 'cython_function_or_method') 267 | 268 | 269 | def lookup_functions(module): 270 | return [ 271 | (m[1], get_signature(m[1])) 272 | for m in inspect.getmembers(module, is_function) 273 | ] 274 | 275 | 276 | def lookup_class_methods(cls): 277 | return [ 278 | (m[1], get_signature(m[1])) 279 | for m in inspect.getmembers(cls) 280 | if m[0][0] != '_' 281 | ] 282 | 283 | 284 | def lookup_classes(module): 285 | return [ 286 | (cls, get_signature(cls), lookup_class_methods(cls)) 287 | for _, cls in inspect.getmembers(module, inspect.isclass) 288 | ] 289 | 290 | 291 | class Mutator: 292 | 293 | def __init__(self, module): 294 | self._module = module 295 | self._attribute_kind = { 296 | 0: self.test_one_function, 297 | 1: self.test_one_class 298 | } 299 | self._attribute_kind_print = { 300 | 0: self.test_one_function_print, 301 | 1: self.test_one_class_print 302 | } 303 | self._functions = lookup_functions(module) 304 | self._classes = lookup_classes(module) 305 | 306 | def test_one_input(self, data): 307 | data = BytesIO(data) 308 | kind = (data.read(1)[0] % len(self._attribute_kind)) 309 | 310 | return self._attribute_kind[kind](data) 311 | 312 | def test_one_input_print(self, data, colors=True): 313 | data = BytesIO(data) 314 | kind = (data.read(1)[0] % len(self._attribute_kind_print)) 315 | self._attribute_kind_print[kind](data, colors) 316 | 317 | def test_one_function(self, data): 318 | func, signature = self._functions[data.read(1)[0] % len(self._functions)] 319 | args = generate_args(signature, data) 320 | 321 | return func(*args) 322 | 323 | def test_one_class(self, data): 324 | cls, signature, methods = self._classes[data.read(1)[0] % len(self._classes)] 325 | args = generate_args(signature, data) 326 | obj = cls(*args) 327 | 328 | for _ in range(min(data.read(1)[0], 5)): 329 | method, signature = methods[data.read(1)[0] % len(methods)] 330 | method(obj, *generate_args(signature, data, True)) 331 | 332 | return obj 333 | 334 | def test_one_function_print(self, data, colors=True): 335 | func, signature = self._functions[data.read(1)[0] % len(self._functions)] 336 | args = generate_args(signature, data) 337 | print_callable(func, args, colors=colors) 338 | 339 | def test_one_class_print(self, data, colors=True): 340 | cls, signature, methods = self._classes[data.read(1)[0] % len(self._classes)] 341 | args = generate_args(signature, data) 342 | obj = print_callable(cls, args, colors=colors) 343 | 344 | if obj is None: 345 | return 346 | 347 | try: 348 | for _ in range(min(data.read(1)[0], 5)): 349 | method, signature = methods[data.read(1)[0] % len(methods)] 350 | print_callable(method, 351 | [obj, *generate_args(signature, data, True)], 352 | 8 * ' ', 353 | colors=colors) 354 | except IndexError: 355 | pass 356 | 357 | 358 | def setup(module): 359 | return Mutator(module) 360 | --------------------------------------------------------------------------------