├── .github ├── FUNDING.yml ├── requirements.txt └── workflows │ └── ci.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.rst ├── afl.pyx ├── doc ├── README ├── changelog └── trophy-case ├── private ├── build-and-test ├── check-rst ├── run-pylint └── update-version ├── py-afl-cmin ├── py-afl-fuzz ├── py-afl-showmap ├── py-afl-tmin ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── target.py ├── target_persistent.py ├── test_cmin.py ├── test_fuzz.py ├── test_hash.py ├── test_import.py ├── test_init.py ├── test_loop.py ├── test_showmap.py ├── test_tmin.py ├── test_version.py └── tools.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://paypal.me/ijklw 2 | -------------------------------------------------------------------------------- /.github/requirements.txt: -------------------------------------------------------------------------------- 1 | cython 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | permissions: {} 3 | on: 4 | - push 5 | - pull_request 6 | jobs: 7 | main: 8 | strategy: 9 | matrix: 10 | include: 11 | - python: '2.7' 12 | os: ubuntu-22.04 13 | cython: cython==0.28 14 | - python: '2.7' 15 | os: ubuntu-22.04 16 | - python: '3.8' 17 | os: ubuntu-22.04 18 | cython: cython==0.28 19 | - python: '3.9' 20 | os: ubuntu-22.04 21 | - python: '3.10' 22 | os: ubuntu-22.04 23 | - python: '3.11' 24 | os: ubuntu-22.04 25 | - python: '3.12' 26 | os: ubuntu-22.04 27 | - python: '3.13' 28 | os: ubuntu-24.04 29 | runs-on: ${{matrix.os}} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: set up PATH 33 | run: | 34 | PATH+=:~/.local/bin 35 | echo "$PATH" >> $GITHUB_PATH 36 | - name: set up Python ${{matrix.python}} 37 | if: matrix.python != '2.7' 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: ${{matrix.python}} 41 | - name: set up APT 42 | if: matrix.python == '2.7' 43 | run: | 44 | printf 'Apt::Install-Recommends "false";\n' | sudo tee -a /etc/apt/apt.conf 45 | sudo apt-get update 46 | - name: set up Python 2.7 (with APT + get-pip) 47 | if: matrix.python == '2.7' 48 | run: | 49 | sudo apt-get install -y python2-dev 50 | sudo ln -sf python2 /usr/bin/python 51 | wget https://bootstrap.pypa.io/pip/2.7/get-pip.py 52 | sudo python get-pip.py 53 | rm get-pip.py 54 | - name: upgrade TLS stack 55 | if: matrix.python == '2.7' 56 | run: | 57 | sudo apt-get install --only-upgrade -y ca-certificates libgnutls30 58 | - name: install setuptools (if distutils is missing) 59 | if: env.pythonLocation 60 | run: | 61 | if ! [ -d ${{env.pythonLocation}}/lib/python*/distutils/ ]; then 62 | python -m pip install setuptools 63 | fi 64 | - name: set up pip cache 65 | uses: actions/cache@v4 66 | with: 67 | path: ~/.cache/pip 68 | key: pip-${{matrix.os}}-python${{matrix.python}}-${{matrix.cython || 'cython'}} 69 | - name: build and install AFL 70 | run: | 71 | mkdir deps 72 | wget https://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz -O deps/afl.tar.gz 73 | tar -xvzf deps/afl.tar.gz -C deps/ 74 | make -C deps/afl-*/ install PREFIX=~/.local 75 | - name: install Cython 76 | run: 77 | python -m pip install --verbose ${{matrix.cython || 'cython'}} 78 | - name: setup.py build 79 | run: 80 | PYTHONWARNINGS=error::FutureWarning python setup.py build 81 | - name: setup.py install 82 | run: 83 | python setup.py install --user 84 | - name: run tests 85 | run: | 86 | python -m pip install pytest 87 | python -m pytest --verbose 88 | - name: run pycodestyle 89 | run: | 90 | pip install pycodestyle 91 | pycodestyle . 92 | - name: run pydiatra 93 | run: | 94 | python -m pip install pydiatra 95 | python -m pydiatra -v . 96 | - name: run pyflakes 97 | run: | 98 | python -m pip install pyflakes 99 | python -m pyflakes . 100 | - name: run pylint 101 | run: | 102 | pip install pylint 103 | private/run-pylint 104 | - name: check changelog syntax 105 | run: 106 | dpkg-parsechangelog -ldoc/changelog --all 2>&1 >/dev/null | { ! grep .; } 107 | - name: check reST syntax 108 | run: | 109 | pip install docutils pygments 110 | private/check-rst 111 | - name: run shellcheck 112 | if: matrix.python != '2.7' 113 | run: | 114 | shellcheck py-afl-* 115 | 116 | # vim:ts=2 sts=2 sw=2 et 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.c 2 | *.o 3 | *.py[co] 4 | *.so 5 | /MANIFEST 6 | /build 7 | /dist 8 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | extension-pkg-whitelist = afl 3 | load-plugins = pylint.extensions.check_elif 4 | 5 | [MESSAGES CONTROL] 6 | disable = 7 | bad-continuation, 8 | bad-option-value, 9 | consider-using-f-string, 10 | duplicate-code, 11 | fixme, 12 | invalid-name, 13 | locally-disabled, 14 | missing-docstring, 15 | no-else-raise, 16 | no-else-return, 17 | raise-missing-from, 18 | redefined-variable-type, 19 | subprocess-popen-preexec-fn, 20 | too-few-public-methods, 21 | too-many-locals, 22 | use-dict-literal, 23 | useless-object-inheritance, 24 | 25 | [TYPECHECK] 26 | # FIXME: Pylint doesn't grok setuptools' distutils. 27 | ignored-modules = 28 | distutils.command.sdist, 29 | distutils.core, 30 | distutils.version, 31 | 32 | [REPORTS] 33 | reports = no 34 | score = no 35 | msg-template = {path}:{line}: {C}: {symbol} [{obj}] {msg} 36 | 37 | [FORMAT] 38 | max-line-length = 120 39 | expected-line-ending-format = LF 40 | 41 | # vim:ft=dosini ts=4 sts=4 sw=4 et 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2013-2024 Jakub Wilk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude *.c 2 | exclude README.rst 3 | include *.pyx 4 | include LICENSE 5 | include MANIFEST.in 6 | include doc/* 7 | include private/* 8 | include py-afl-* 9 | include pyproject.toml 10 | recursive-include tests *.py 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | doc/README -------------------------------------------------------------------------------- /afl.pyx: -------------------------------------------------------------------------------- 1 | # Copyright © 2014-2018 Jakub Wilk 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the “Software”), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | #cython: autotestdict=False 22 | #cython: c_string_encoding=default 23 | #cython: cdivision=True 24 | #cython: language_level=2 25 | 26 | ''' 27 | American Fuzzy Lop fork server and instrumentation for pure-Python code 28 | ''' 29 | 30 | __version__ = '0.7.4' 31 | 32 | cdef object os, signal, struct, sys, warnings 33 | import os 34 | import signal 35 | import struct 36 | import sys 37 | import warnings 38 | 39 | cdef extern from *: 40 | # These constants must be kept in sync with afl-fuzz: 41 | ''' 42 | #define SHM_ENV_VAR "__AFL_SHM_ID" 43 | #define FORKSRV_FD 198 44 | #define MAP_SIZE_POW2 16 45 | #define MAP_SIZE (1 << MAP_SIZE_POW2) 46 | ''' 47 | extern const char *SHM_ENV_VAR 48 | extern int FORKSRV_FD 49 | extern int MAP_SIZE 50 | 51 | from cpython.exc cimport PyErr_SetFromErrno 52 | from libc cimport errno 53 | from libc.signal cimport SIG_DFL 54 | from libc.stddef cimport size_t 55 | from libc.stdint cimport uint32_t 56 | from libc.stdlib cimport getenv 57 | from libc.string cimport strlen 58 | from posix.signal cimport sigaction, sigaction_t, sigemptyset 59 | 60 | cdef extern from 'sys/shm.h': 61 | unsigned char *shmat(int shmid, void *shmaddr, int shmflg) 62 | 63 | cdef unsigned char *afl_area = NULL 64 | cdef unsigned int prev_location = 0 65 | 66 | cdef inline unsigned int lhash(const char *key, size_t offset): 67 | # 32-bit Fowler–Noll–Vo hash function 68 | cdef size_t len = strlen(key) 69 | cdef uint32_t h = 0x811C9DC5 70 | while len > 0: 71 | h ^= key[0] 72 | h *= 0x01000193 73 | len -= 1 74 | key += 1 75 | while offset > 0: 76 | h ^= offset 77 | h *= 0x01000193 78 | offset >>= 8 79 | return h 80 | 81 | def _hash(key, offset): 82 | # This function is not a part of public API. 83 | # It is provided only to facilitate testing. 84 | return lhash(key, offset) 85 | 86 | cdef object trace 87 | def trace(frame, event, arg): 88 | global prev_location, tstl_mode 89 | cdef unsigned int location, offset 90 | cdef object filename = frame.f_code.co_filename 91 | if tstl_mode and (filename[-7:] in ['sut.py', '/sut.py']): 92 | return None 93 | location = ( 94 | lhash(filename, frame.f_lineno) 95 | % MAP_SIZE 96 | ) 97 | offset = location ^ prev_location 98 | prev_location = location // 2 99 | afl_area[offset] += 1 100 | # TODO: make it configurable which modules are instrumented, and which are not 101 | return trace 102 | 103 | cdef int except_signal_id = 0 104 | cdef object except_signal_name = os.getenv('PYTHON_AFL_SIGNAL') or '0' 105 | if except_signal_name.isdigit(): 106 | except_signal_id = int(except_signal_name) 107 | else: 108 | if except_signal_name[:3] != 'SIG': 109 | except_signal_name = 'SIG' + except_signal_name 110 | except_signal_id = getattr(signal, except_signal_name) 111 | 112 | cdef object excepthook 113 | def excepthook(tp, value, traceback): 114 | os.kill(os.getpid(), except_signal_id) 115 | 116 | cdef bint init_done = False 117 | cdef bint tstl_mode = False 118 | 119 | cdef int _init(bint persistent_mode) except -1: 120 | global afl_area, init_done, tstl_mode 121 | tstl_mode = os.getenv('PYTHON_AFL_TSTL') is not None 122 | use_forkserver = True 123 | try: 124 | os.write(FORKSRV_FD + 1, b'\0\0\0\0') 125 | except OSError as exc: 126 | if exc.errno == errno.EBADF: 127 | use_forkserver = False 128 | else: 129 | raise 130 | if init_done: 131 | raise RuntimeError('AFL already initialized') 132 | init_done = True 133 | child_stopped = False 134 | child_pid = 0 135 | cdef sigaction_t old_sigchld 136 | cdef sigaction_t dfl_sigchld 137 | dfl_sigchld.sa_handler = SIG_DFL 138 | dfl_sigchld.sa_sigaction = NULL 139 | dfl_sigchld.sa_flags = 0 140 | sigemptyset(&dfl_sigchld.sa_mask) 141 | if use_forkserver: 142 | rc = sigaction(signal.SIGCHLD, &dfl_sigchld, &old_sigchld) 143 | if rc: 144 | PyErr_SetFromErrno(OSError) 145 | while use_forkserver: 146 | [child_killed] = struct.unpack('I', os.read(FORKSRV_FD, 4)) 147 | if child_stopped and child_killed: 148 | os.waitpid(child_pid, 0) 149 | child_stopped = False 150 | if child_stopped: 151 | os.kill(child_pid, signal.SIGCONT) 152 | child_stopped = False 153 | else: 154 | child_pid = os.fork() 155 | if not child_pid: 156 | # child: 157 | break 158 | # parent: 159 | os.write(FORKSRV_FD + 1, struct.pack('I', child_pid)) 160 | (child_pid, status) = os.waitpid(child_pid, os.WUNTRACED if persistent_mode else 0) 161 | child_stopped = os.WIFSTOPPED(status) 162 | os.write(FORKSRV_FD + 1, struct.pack('I', status)) 163 | if use_forkserver: 164 | rc = sigaction(signal.SIGCHLD, &old_sigchld, NULL) 165 | if rc: 166 | PyErr_SetFromErrno(OSError) 167 | os.close(FORKSRV_FD) 168 | os.close(FORKSRV_FD + 1) 169 | if except_signal_id != 0: 170 | sys.excepthook = excepthook 171 | cdef const char * afl_shm_id = getenv(SHM_ENV_VAR) 172 | if afl_shm_id == NULL: 173 | return 0 174 | afl_area = shmat(int(afl_shm_id), NULL, 0) 175 | if afl_area == -1: 176 | PyErr_SetFromErrno(OSError) 177 | sys.settrace(trace) 178 | return 0 179 | 180 | def init(): 181 | ''' 182 | init() 183 | 184 | Start the fork server and enable instrumentation. 185 | 186 | This function should be called as late as possible, 187 | but before the input is read. 188 | ''' 189 | _init(persistent_mode=False) 190 | 191 | def start(): 192 | ''' 193 | deprecated alias for afl.init() 194 | ''' 195 | warnings.warn('afl.start() is deprecated, use afl.init() instead', DeprecationWarning) 196 | _init(persistent_mode=False) 197 | 198 | cdef bint persistent_allowed = False 199 | cdef unsigned long persistent_counter = 0 200 | 201 | def loop(max=None): 202 | ''' 203 | while loop([max]): 204 | ... 205 | 206 | Start the fork server and enable instrumentation, 207 | then run the code inside the loop body in persistent mode. 208 | 209 | afl-fuzz >= 1.82b is required for this feature. 210 | ''' 211 | global persistent_allowed, persistent_counter, prev_location 212 | prev_location = 0 213 | if persistent_counter == 0: 214 | persistent_allowed = os.getenv('PYTHON_AFL_PERSISTENT') is not None 215 | _init(persistent_mode=persistent_allowed) 216 | persistent_counter = 1 217 | return True 218 | cont = persistent_allowed and ( 219 | max is None or 220 | persistent_counter < max 221 | ) 222 | if cont: 223 | os.kill(os.getpid(), signal.SIGSTOP) 224 | persistent_counter += 1 225 | return True 226 | else: 227 | sys.settrace(None) 228 | return False 229 | 230 | __all__ = [ 231 | 'init', 232 | 'loop', 233 | ] 234 | 235 | # vim:ts=4 sts=4 sw=4 et 236 | -------------------------------------------------------------------------------- /doc/README: -------------------------------------------------------------------------------- 1 | This is experimental module that enables 2 | `American Fuzzy Lop`_ fork server and instrumentation for pure-Python code. 3 | 4 | .. _American Fuzzy Lop: https://lcamtuf.coredump.cx/afl/ 5 | 6 | HOWTO 7 | ----- 8 | 9 | * Add this code (ideally, after all other modules are already imported) to 10 | the target program: 11 | 12 | .. code:: python 13 | 14 | import afl 15 | afl.init() 16 | 17 | * The instrumentation is currently implemented with a `trace function`_, 18 | which is called whenever a new local scope is entered. 19 | You might need to wrap the code of the main program in a function 20 | to get it instrumented correctly. 21 | 22 | .. _trace function: 23 | https://docs.python.org/2/library/sys.html#sys.settrace 24 | 25 | * Optionally, add this code at the end of the target program: 26 | 27 | .. code:: python 28 | 29 | os._exit(0) 30 | 31 | This should speed up fuzzing considerably, 32 | at the risk of not catching bugs that could happen during normal exit. 33 | 34 | * For persistent mode, wrap the tested code in this loop: 35 | 36 | .. code:: python 37 | 38 | while afl.loop(N): 39 | ... 40 | 41 | where ``N`` is the number of inputs to process before restarting. 42 | 43 | You shouldn't call ``afl.init()`` in this case. 44 | 45 | If you read input from ``sys.stdin``, 46 | you must rewind it in every loop iteration: 47 | 48 | .. code:: python 49 | 50 | sys.stdin.seek(0) 51 | 52 | afl-fuzz ≥ 1.82b is required for this feature. 53 | 54 | * Use *py-afl-fuzz* instead of *afl-fuzz*:: 55 | 56 | $ py-afl-fuzz [options] -- /path/to/fuzzed/python/script [...] 57 | 58 | * The instrumentation is a bit slow at the moment, 59 | so you might want to enable the dumb mode (``-n``), 60 | while still leveraging the fork server. 61 | 62 | afl-fuzz ≥ 1.95b is required for this feature. 63 | 64 | Environment variables 65 | --------------------- 66 | 67 | The following environment variables affect *python-afl* behavior: 68 | 69 | ``PYTHON_AFL_SIGNAL`` 70 | If this variable is set, *python-afl* installs an exception hook 71 | that kills the current process with the selected signal. 72 | That way *afl-fuzz* can treat unhandled exceptions as crashes. 73 | 74 | By default, *py-afl-fuzz*, *py-afl-showmap*, *python-afl-cmin*, 75 | and *py-afl-tmin* set this variable to ``SIGUSR1``. 76 | 77 | You can set ``PYTHON_AFL_SIGNAL`` to another signal; 78 | or set it to ``0`` to disable the exception hook. 79 | 80 | ``PYTHON_AFL_PERSISTENT`` 81 | Persistent mode is enabled only if this variable is set. 82 | 83 | *py-afl-fuzz* sets this variable automatically, 84 | so there should normally no need to set it manually. 85 | 86 | ``PYTHON_AFL_TSTL`` 87 | `TSTL`_ test harness code is ignored if this variable is set; 88 | relevant only to users of TSTL interface to python-afl. 89 | 90 | .. _TSTL: https://github.com/agroce/tstl 91 | 92 | Bugs 93 | ---- 94 | 95 | Multi-threaded code is not supported. 96 | 97 | Further reading 98 | --------------- 99 | 100 | * `Taking a look at python-afl `_ by Jussi Judin 101 | * `Introduction to Fuzzing in Python with AFL `_ by Alex Gaynor 102 | * `AFL's README `_ 103 | 104 | Prerequisites 105 | ------------- 106 | 107 | To build the module, you will need: 108 | 109 | * Python 2.6+ or 3.2+ 110 | * Cython ≥ 0.28 (only at build time) 111 | 112 | *py-afl-fuzz* requires AFL proper to be installed. 113 | 114 | .. vim:ft=rst ts=3 sts=3 sw=3 et 115 | -------------------------------------------------------------------------------- /doc/changelog: -------------------------------------------------------------------------------- 1 | python-afl (0.7.4) UNRELEASED; urgency=low 2 | 3 | * Improve the test suite: 4 | + Stop using nose. 5 | + Stop using deprecated distutils.version. 6 | + Fix an unsound comparison between bytes and string. 7 | + Add support for AFL++. 8 | Thanks to Markus Linnala for the initial patch. 9 | + Print debugging info when a called program fails. 10 | * Fail build early non-POSIX systems. 11 | https://github.com/jwilk/python-afl/issues/26 12 | * Don't print full executable path in py-afl-* error messages. 13 | * Use HTTPS for lcamtuf.coredump.cx. 14 | * Use HTTPS for jwilk.net. 15 | 16 | -- Jakub Wilk Mon, 13 Feb 2023 15:06:55 +0100 17 | 18 | python-afl (0.7.3) unstable; urgency=low 19 | 20 | * Improve the build system: 21 | + Declare build-dependencies (as per PEP-518). 22 | * Improve the test suite. 23 | 24 | -- Jakub Wilk Tue, 06 Oct 2020 20:12:39 +0200 25 | 26 | python-afl (0.7.2) unstable; urgency=low 27 | 28 | * Document that multi-threaded code is not supported. 29 | * Update URLs in the trophy-case. 30 | 31 | -- Jakub Wilk Fri, 15 Feb 2019 15:31:23 +0100 32 | 33 | python-afl (0.7.1) unstable; urgency=low 34 | 35 | * Reset the SIGCHLD signal handler in the forkserver. 36 | Thanks to Kuang-che Wu for the bug report. 37 | * Document that sys.stdin rewinding is necessary in persistent mode. 38 | * Improve the test suite. 39 | + Improve error messages for missing command-line tools. 40 | * Explicitly set Cython's Python language level to 2. 41 | This might fix build failures with future versions of Cython. 42 | 43 | -- Jakub Wilk Thu, 21 Jun 2018 23:17:56 +0200 44 | 45 | python-afl (0.7) unstable; urgency=low 46 | 47 | [ Jakub Wilk ] 48 | * Fix stability issues in persistent mode. 49 | * Capitalize “American Fuzzy Lop” in documentation. 50 | * Speed up integer division and modulo operators. 51 | * Improve the build system: 52 | + Add the bdist_wheel command. 53 | * Improve the test suite. 54 | + Print helpful error message when the required command-line tools are 55 | missing. 56 | + Fix stability of the persistent target. 57 | + Rewind stdin in the persistent target. 58 | Thanks to Alex Groce for the bug report. 59 | * Improve documentation: 60 | + Add another “Further reading” link to README. 61 | + Update PyPI URLs. 62 | 63 | [ Alex Groce ] 64 | * Add the PYTHON_AFL_TSTL environment variable. 65 | 66 | -- Jakub Wilk Mon, 30 Apr 2018 10:42:18 +0200 67 | 68 | python-afl (0.6.1) unstable; urgency=low 69 | 70 | * Improve the test suite. 71 | + Make the py-afl-cmin test pass when run in a subdirectory of /tmp. 72 | * Improve the build system: 73 | + Use distutils644 to normalize source tarball permissions etc. 74 | 75 | -- Jakub Wilk Fri, 28 Jul 2017 16:43:06 +0200 76 | 77 | python-afl (0.6) unstable; urgency=low 78 | 79 | * Add py-afl-cmin. 80 | Thanks to @jabdoa2 for the bug report. 81 | https://github.com/jwilk/python-afl/issues/6 82 | * Add py-afl-showmap and py-afl-tmin. 83 | Bare afl-showmap and afl-tmin were broken since 0.5. 84 | * Put license into a separate file. 85 | * Improve the test suite. 86 | * Update URLs in the trophy-case. 87 | 88 | -- Jakub Wilk Wed, 05 Apr 2017 13:28:37 +0200 89 | 90 | python-afl (0.5.5) unstable; urgency=low 91 | 92 | * Improve the test suite: 93 | + Kill stray processes that afl-fuzz might have left behind. 94 | Thanks to Daniel Stender for the bug report. 95 | https://bugs.debian.org/833675 96 | https://github.com/jwilk/python-afl/issues/4 97 | 98 | -- Jakub Wilk Tue, 16 Aug 2016 22:08:57 +0200 99 | 100 | python-afl (0.5.4) unstable; urgency=low 101 | 102 | * Improve README: 103 | + Fix formatting. 104 | + Add “Further reading” links. 105 | + Document runtime and build-time dependencies. 106 | * Improve error handling. 107 | Thanks to @PhillipSz for the bug report. 108 | https://github.com/jwilk/python-afl/issues/1 109 | * Improve the setup script: 110 | + Make the package installable with pip, even when Cython were missing. 111 | Thanks to @mrmagooey for the bug report. 112 | https://github.com/jwilk/python-afl/issues/3 113 | + Add “Programming Language” classifiers. 114 | * Improve the test suite. 115 | 116 | -- Jakub Wilk Sat, 30 Jul 2016 16:43:52 +0200 117 | 118 | python-afl (0.5.3) unstable; urgency=low 119 | 120 | * Fix compatibility with Cython 0.24. 121 | 122 | -- Jakub Wilk Thu, 07 Apr 2016 12:56:08 +0200 123 | 124 | python-afl (0.5.2) unstable; urgency=low 125 | 126 | [ Jakub Wilk ] 127 | * Document that afl-fuzz ≥ 1.95b is required for the -n option. 128 | * Document that you might need to wrap code in a function to get it 129 | instrumented correctly. 130 | Thanks to Peter Dohm for the bug report. 131 | * Improve the test suite. 132 | 133 | [ Alex Gaynor ] 134 | * Fix the afl.loop() docstring. 135 | 136 | -- Jakub Wilk Sat, 13 Feb 2016 23:41:05 +0100 137 | 138 | python-afl (0.5.1) unstable; urgency=low 139 | 140 | * Fix TypeError when built with Cython 0.23.2. 141 | 142 | -- Jakub Wilk Fri, 18 Sep 2015 11:12:12 +0200 143 | 144 | python-afl (0.5) unstable; urgency=low 145 | 146 | * Fix deprecated call to afl.start() in README. 147 | * Make afl.start() emit DeprecationWarning. 148 | * Enable persistent mode only if PYTHON_AFL_PERSISTENT is set. 149 | Let py-afl-fuzz set this variable. 150 | * Don't install the exception hook, unless enabled with PYTHON_AFL_SIGNAL. 151 | Let py-afl-fuzz set this variable to SIGUSR1. 152 | 153 | -- Jakub Wilk Wed, 02 Sep 2015 11:12:42 +0200 154 | 155 | python-afl (0.4) unstable; urgency=low 156 | 157 | * Rename afl.start() as afl.init(), for consistency with AFL >= 1.89b. 158 | The old name is still available, but it's deprecated. 159 | * Add new interface for persistent mode, afl.loop(). 160 | Remove the old interface, afl.persistent(). 161 | * Improve the test suite. 162 | 163 | -- Jakub Wilk Tue, 01 Sep 2015 16:28:06 +0200 164 | 165 | python-afl (0.3) unstable; urgency=low 166 | 167 | * Implement persistent mode. 168 | * Add docstrings for the exported functions. 169 | * Add __version__. 170 | * Don't rely on the Python hash() function for computing code location 171 | identifiers. 172 | + Don't set PYTHONHASHSEED in py-afl-fuzz. 173 | + Remove the py-afl-showmap command. 174 | afl-showmap proper can be now used for Python code. 175 | + Remove the AflError class. It was only used for checking PYTHONHASHSEED. 176 | * Improve the setup script: 177 | + Check Cython version. 178 | * Implement a small test suite. 179 | 180 | -- Jakub Wilk Mon, 31 Aug 2015 16:56:12 +0200 181 | 182 | python-afl (0.2.1) unstable; urgency=low 183 | 184 | * Make the setup script install the py-afl-fuzz and py-afl-showmap binaries. 185 | 186 | -- Jakub Wilk Tue, 14 Jul 2015 19:34:35 +0200 187 | 188 | python-afl (0.2) unstable; urgency=low 189 | 190 | * Automatically disable instrumentation when the -n option is provided. 191 | Setting the PYTHON_AFL_DUMB environment variable is no longer needed. 192 | Thanks to Michal Zalewski for the hint how to implement this feature. 193 | * Update the module description to stress that it's dedicated for 194 | pure-Python code. 195 | 196 | -- Jakub Wilk Mon, 27 Apr 2015 19:31:08 +0200 197 | 198 | python-afl (0.1) unstable; urgency=low 199 | 200 | * Initial release. 201 | 202 | -- Jakub Wilk Fri, 17 Apr 2015 00:15:16 +0200 203 | -------------------------------------------------------------------------------- /doc/trophy-case: -------------------------------------------------------------------------------- 1 | The bug-o-rama trophy case 2 | ========================== 3 | 4 | The following bugs were found with help of *python-afl*: 5 | 6 | i18nspector__ 7 | ------------- 8 | Multiple bugs in the plural expression parser: 9 | 10 | | https://github.com/jwilk/i18nspector/commit/c340dc28a1fe 11 | | https://github.com/jwilk/i18nspector/commit/74ac2b9e9882 12 | | https://github.com/jwilk/i18nspector/commit/c9e4fd0efc13 13 | | https://github.com/jwilk/i18nspector/commit/1febfc2bd612 14 | | https://github.com/jwilk/i18nspector/commit/1d671f43497e 15 | | https://github.com/jwilk/i18nspector/commit/0767e9924ab1 16 | | https://github.com/jwilk/i18nspector/commit/1f6993b34ca5 17 | | https://github.com/jwilk/i18nspector/commit/6a76c4884d0b 18 | 19 | .. __: https://jwilk.net/software/i18nspector 20 | 21 | PyPDF2__ 22 | -------- 23 | | https://github.com/mstamy2/PyPDF2/issues/184 24 | 25 | .. __: https://mstamy2.github.io/PyPDF2/ 26 | 27 | enzyme__ 28 | -------- 29 | | https://github.com/Diaoul/enzyme/issues/9 30 | | https://github.com/Diaoul/enzyme/issues/10 31 | | https://github.com/Diaoul/enzyme/issues/11 32 | | https://github.com/Diaoul/enzyme/issues/12 33 | | https://github.com/Diaoul/enzyme/issues/13 34 | | https://github.com/Diaoul/enzyme/issues/14 35 | | https://github.com/Diaoul/enzyme/issues/15 36 | | https://github.com/Diaoul/enzyme/issues/16 37 | | https://github.com/Diaoul/enzyme/issues/17 38 | | https://github.com/Diaoul/enzyme/issues/18 39 | | https://github.com/Diaoul/enzyme/issues/19 40 | | https://github.com/Diaoul/enzyme/issues/20 41 | | https://github.com/Diaoul/enzyme/issues/21 42 | | https://github.com/Diaoul/enzyme/issues/22 43 | 44 | .. __: https://github.com/Diaoul/enzyme 45 | 46 | PyASN1__ 47 | -------- 48 | 49 | | https://github.com/pyca/cryptography/issues/1838 50 | 51 | .. __: http://snmplabs.com/pyasn1/ 52 | 53 | gunicorn__ 54 | ---------- 55 | 56 | | https://github.com/benoitc/gunicorn/issues/1023 57 | 58 | .. __: https://gunicorn.org/ 59 | 60 | dateutil__ 61 | ---------- 62 | 63 | | https://github.com/dateutil/dateutil/issues/82 64 | 65 | .. __: https://pypi.org/project/python-dateutil/ 66 | 67 | regex__ 68 | ------- 69 | 70 | | https://github.com/mrabarnett/mrab-regex/issues/197 71 | | https://github.com/mrabarnett/mrab-regex/issues/198 72 | | https://github.com/mrabarnett/mrab-regex/issues/199 73 | | https://github.com/mrabarnett/mrab-regex/issues/200 74 | 75 | .. __: https://pypi.org/project/regex/ 76 | 77 | .. vim:ft=rst ts=3 sts=3 sw=3 et 78 | -------------------------------------------------------------------------------- /private/build-and-test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright © 2015-2020 Jakub Wilk 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 13 | # all 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 | 23 | set -e -u 24 | 25 | usage() 26 | { 27 | printf 'Usage: %s [--no-build] [pythonX.Y...]\n' "$0" 28 | } 29 | 30 | if ! args=$(getopt -n "$0" -o 'hj:' --long 'help,jobs:,no-build' -- "$@") 31 | then 32 | usage >&2 33 | exit 1 34 | fi 35 | eval set -- "$args" 36 | opt_jobs=$(nproc) || opt_jobs=1 37 | opt_build=y 38 | while true 39 | do 40 | case "$1" in 41 | -h|--help) usage; exit 0;; 42 | -j|--jobs) opt_jobs=$2; shift 2;; 43 | --no-build) opt_build=; shift;; 44 | --) shift; break;; 45 | *) printf '%s: internal error (%s)\n' "$0" "$1" >&2; exit 1;; 46 | esac 47 | done 48 | 49 | [ $# = 0 ] && set -- python 50 | [ -z $opt_build ] || 51 | printf '%s\n' "$@" \ 52 | | xargs -P"$opt_jobs" -t -I'{python}' env '{python}' setup.py build --build-lib 'build/{python}' 53 | cd ./tests 54 | export PATH="$PWD/..:$PATH" 55 | printf '%s\n' "$@" \ 56 | | xargs -t -I'{python}' env PYTHONPATH="$PWD/../build/{python}" '{python}' -c 'import nose; nose.main()' --verbose 57 | 58 | # vim:ts=4 sts=4 sw=4 et 59 | -------------------------------------------------------------------------------- /private/check-rst: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright © 2016-2022 Jakub Wilk 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 13 | # all 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 | 23 | set -e -u 24 | set -o pipefail 25 | here=${0%/*} 26 | here=${here#./} 27 | root="$here/../" 28 | root=${root#private/../} 29 | rst2xml=$(command -v rst2xml) \ 30 | || rst2xml=$(command -v rst2xml.py) \ 31 | || { printf 'rst2xml not found\n' >&2; exit 1; } 32 | rst2xml=${rst2xml##*/} 33 | options='--input-encoding=UTF-8 --strict' 34 | if [ $# -eq 0 ] 35 | then 36 | print_desc='python setup.py --long-description' 37 | echo "(cd ${root:-.} && $print_desc) | $rst2xml $options -" >&2 38 | (cd "${root:-.}" && $print_desc) | "$rst2xml" $options - > /dev/null 39 | fi 40 | if [ $# -eq 0 ] 41 | then 42 | grep -rwl 'ft=rst' "${root}doc" 43 | else 44 | printf '%s\n' "$@" 45 | fi | 46 | xargs -t -I{} "$rst2xml" $options {} > /dev/null 47 | 48 | # vim:ts=4 sts=4 sw=4 et 49 | -------------------------------------------------------------------------------- /private/run-pylint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright © 2015-2022 Jakub Wilk 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 13 | # all 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 | 23 | set -e -u 24 | PYTHON=${PYTHON:-python} 25 | "$PYTHON" -m pylint --version >/dev/null || exit 1 26 | if [ $# -eq 0 ] 27 | then 28 | set -- setup.py tests/*.py 29 | fi 30 | if [ -n "${VIRTUAL_ENV:-}" ] 31 | then 32 | # https://github.com/PyCQA/pylint/issues/73 33 | set -- --ignored-modules=distutils "$@" 34 | fi 35 | log=$(mktemp -t pylint.XXXXXX) 36 | "$PYTHON" -m pylint "$@" > "$log" || [ $? != 1 ] 37 | ! grep -P '^\S+:' "$log" \ 38 | | grep -v -P ": redefined-builtin \\[.*\\] Redefining built-in '(file|dir|input)'$" \ 39 | | grep -v -P ": superfluous-parens \\[.*\\] Unnecessary parens after u?'print' keyword$" \ 40 | | LC_ALL=C sort -k2 \ 41 | | grep '.' || exit 1 42 | rm "$log" 43 | 44 | # vim:ts=4 sts=4 sw=4 et 45 | -------------------------------------------------------------------------------- /private/update-version: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export version=${1:?"no version number provided"} 3 | set -e -u 4 | set -x 5 | dch -m -v "$version" -u low -c doc/changelog 6 | perl -pi -e 's/^__version__ = '"'"'\K[\w.]+/$ENV{version}/' afl.pyx 7 | -------------------------------------------------------------------------------- /py-afl-cmin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export AFL_SKIP_BIN_CHECK=1 3 | export PYTHON_AFL_SIGNAL="${PYTHON_AFL_SIGNAL:-SIGUSR1}" 4 | prog="${0##*/}" 5 | if ! command -v afl-cmin > /dev/null 6 | then 7 | cat >&2 < installed? 10 | EOF 11 | exit 127 12 | fi 13 | exec afl-cmin "$@" 14 | 15 | # vim:ts=4 sts=4 sw=4 et 16 | -------------------------------------------------------------------------------- /py-afl-fuzz: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export AFL_SKIP_CHECKS=1 # AFL << 1.20b 3 | export AFL_SKIP_BIN_CHECK=1 # AFL >= 1.20b 4 | export AFL_DUMB_FORKSRV=1 5 | prog="${0##*/}" 6 | if [ -n "$PYTHON_AFL_DUMB" ] 7 | then 8 | # shellcheck disable=SC2016 9 | printf '%s: $PYTHON_AFL_DUMB is deprecated; use -n instead\n' "$prog" >&2 10 | set -- -n "$@" 11 | fi 12 | export PYTHON_AFL_SIGNAL="${PYTHON_AFL_SIGNAL:-SIGUSR1}" 13 | export PYTHON_AFL_PERSISTENT=1 14 | if ! command -v afl-fuzz > /dev/null 15 | then 16 | cat >&2 < installed? 19 | EOF 20 | exit 127 21 | fi 22 | exec afl-fuzz "$@" 23 | 24 | # vim:ts=4 sts=4 sw=4 et 25 | -------------------------------------------------------------------------------- /py-afl-showmap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export PYTHON_AFL_SIGNAL="${PYTHON_AFL_SIGNAL:-SIGUSR1}" 3 | prog="${0##*/}" 4 | if ! command -v afl-showmap > /dev/null 5 | then 6 | cat >&2 < installed? 9 | EOF 10 | exit 127 11 | fi 12 | exec afl-showmap "$@" 13 | 14 | # vim:ts=4 sts=4 sw=4 et 15 | -------------------------------------------------------------------------------- /py-afl-tmin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export PYTHON_AFL_SIGNAL="${PYTHON_AFL_SIGNAL:-SIGUSR1}" 3 | prog="${0##*/}" 4 | if ! command -v afl-tmin > /dev/null 5 | then 6 | cat >&2 < installed? 9 | EOF 10 | exit 127 11 | fi 12 | exec afl-tmin "$@" 13 | 14 | # vim:ts=4 sts=4 sw=4 et 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | 'setuptools', 'wheel', # needed only to make pip happy 4 | 'Cython>=0.28', 5 | ] 6 | 7 | # vim:ts=4 sts=4 sw=4 et 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | filename = *.py,*.pyx 3 | ignore = E12,E21,E22,E265,E3,E4,E722,W504 4 | max-line-length = 120 5 | show-source = true 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2014-2024 Jakub Wilk 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 13 | # all 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 | 23 | ''' 24 | *python-afl* is an experimental module that enables 25 | `American Fuzzy Lop`_ fork server and instrumentation for pure-Python code. 26 | 27 | .. _American Fuzzy Lop: https://lcamtuf.coredump.cx/afl/ 28 | ''' 29 | 30 | import glob 31 | import io 32 | import os 33 | import sys 34 | 35 | # pylint: disable=deprecated-module 36 | import distutils.core 37 | import distutils.version 38 | from distutils.command.sdist import sdist as distutils_sdist 39 | # pylint: enable=deprecated-module 40 | 41 | try: 42 | from wheel.bdist_wheel import bdist_wheel 43 | except ImportError: 44 | bdist_wheel = None 45 | 46 | try: 47 | import distutils644 48 | except ImportError: 49 | pass 50 | else: 51 | distutils644.install() 52 | 53 | b = b'' # Python >= 2.6 is required 54 | 55 | def get_version(): 56 | with io.open('doc/changelog', encoding='UTF-8') as file: 57 | line = file.readline() 58 | return line.split()[1].strip('()') 59 | 60 | classifiers = ''' 61 | Development Status :: 3 - Alpha 62 | Intended Audience :: Developers 63 | License :: OSI Approved :: MIT License 64 | Operating System :: POSIX 65 | Programming Language :: Cython 66 | Programming Language :: Python :: 2 67 | Programming Language :: Python :: 3 68 | Topic :: Software Development :: Quality Assurance 69 | Topic :: Software Development :: Testing 70 | '''.strip().splitlines() 71 | 72 | meta = dict( 73 | name='python-afl', 74 | version=get_version(), 75 | license='MIT', 76 | description='American Fuzzy Lop fork server and instrumentation for pure-Python code', 77 | long_description=__doc__.strip(), 78 | classifiers=classifiers, 79 | url='https://jwilk.net/software/python-afl', 80 | author='Jakub Wilk', 81 | author_email='jwilk@jwilk.net', 82 | ) 83 | 84 | if os.name != 'posix': 85 | raise RuntimeError('non-POSIX systems are not supported') 86 | 87 | min_cython_version = '0.28' 88 | try: 89 | import Cython 90 | except ImportError: 91 | # This shouldn't happen with pip >= 10, thanks to PEP-518 support. 92 | # For older versions, we use this hack to trick it into installing Cython: 93 | if 'setuptools' in sys.modules and sys.argv[1] == 'egg_info': 94 | distutils.core.setup( # pylint: disable=no-member 95 | install_requires=['Cython>={v}'.format(v=min_cython_version)], 96 | # Conceptually, “setup_requires” would make more sense than 97 | # “install_requires”, but the former is not supported by pip. 98 | **meta 99 | ) 100 | sys.exit(0) 101 | raise RuntimeError('Cython >= {v} is required'.format(v=min_cython_version)) 102 | 103 | try: 104 | cython_version = Cython.__version__ 105 | except AttributeError: 106 | # Cython prior to 0.14 didn't have __version__. 107 | # Oh well. We don't support such old versions anyway. 108 | cython_version = '0' 109 | cython_version = distutils.version.LooseVersion(cython_version) # pylint: disable=no-member 110 | if cython_version < min_cython_version: 111 | raise RuntimeError('Cython >= {v} is required'.format(v=min_cython_version)) 112 | 113 | import Cython.Build # pylint: disable=wrong-import-position 114 | 115 | class cmd_sdist(distutils_sdist): 116 | 117 | def maybe_move_file(self, base_dir, src, dst): 118 | src = os.path.join(base_dir, src) 119 | dst = os.path.join(base_dir, dst) 120 | if os.path.exists(src): 121 | self.move_file(src, dst) 122 | 123 | def make_release_tree(self, base_dir, files): 124 | distutils_sdist.make_release_tree(self, base_dir, files) 125 | self.maybe_move_file(base_dir, 'LICENSE', 'doc/LICENSE') 126 | 127 | def d(**kwargs): 128 | return dict( 129 | (k, v) for k, v in kwargs.items() 130 | if v is not None 131 | ) 132 | 133 | distutils.core.setup( # pylint: disable=no-member 134 | ext_modules=Cython.Build.cythonize('afl.pyx'), 135 | scripts=glob.glob('py-afl-*'), 136 | cmdclass=d( 137 | bdist_wheel=bdist_wheel, 138 | sdist=cmd_sdist, 139 | ), 140 | **meta 141 | ) 142 | 143 | # vim:ts=4 sts=4 sw=4 et 144 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwilk/python-afl/6a3fc2d15a02f60e4ea24273f8ed93404c9ffa43/tests/__init__.py -------------------------------------------------------------------------------- /tests/target.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2018 Jakub Wilk 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 13 | # all 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 | 23 | import signal 24 | import sys 25 | 26 | import afl 27 | 28 | def main(): 29 | s = sys.stdin.read() 30 | if not s: 31 | print('Hum?') 32 | sys.exit(1) 33 | s.encode('ASCII') 34 | if s[0] == '0': 35 | print('Looks like a zero to me!') 36 | else: 37 | print('A non-zero value? How quaint!') 38 | 39 | if __name__ == '__main__': 40 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) # this should have no effect on the forkserver 41 | afl.init() 42 | main() 43 | 44 | # vim:ts=4 sts=4 sw=4 et 45 | -------------------------------------------------------------------------------- /tests/target_persistent.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2018 Jakub Wilk 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 13 | # all 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 | 23 | import signal 24 | import sys 25 | 26 | import afl 27 | 28 | def main(): 29 | sys.stdin.seek(0) # work-around for C stdio caching EOF status 30 | s = sys.stdin.read() 31 | if not s: 32 | print('Hum?') 33 | sys.exit(1) 34 | s.encode('ASCII') 35 | if s[0] == '0': 36 | print('Looks like a zero to me!') 37 | else: 38 | print('A non-zero value? How quaint!') 39 | 40 | if __name__ == '__main__': 41 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) # this should have no effect on the forkserver 42 | ''.encode('ASCII') # make sure the codec module is loaded before the loop 43 | while afl.loop(): 44 | main() 45 | 46 | # vim:ts=4 sts=4 sw=4 et 47 | -------------------------------------------------------------------------------- /tests/test_cmin.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2022 Jakub Wilk 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 13 | # all 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 | 23 | import os 24 | import sys 25 | 26 | from .tools import ( 27 | assert_equal, 28 | require_commands, 29 | run, 30 | tempdir, 31 | ) 32 | 33 | here = os.path.dirname(__file__) 34 | target = here + '/target.py' 35 | 36 | def run_afl_cmin(input, xoutput, crashes_only=False): 37 | require_commands('py-afl-cmin', 'afl-cmin') 38 | input = sorted(input) 39 | xoutput = sorted(xoutput) 40 | with tempdir() as workdir: 41 | indir = '//{dir}/in'.format(dir=workdir) 42 | outdir = '//{dir}/out'.format(dir=workdir) 43 | for dir in [indir, outdir]: 44 | os.mkdir(dir) 45 | for n, blob in enumerate(input): 46 | path = '{dir}/{n}'.format(dir=indir, n=n) 47 | with open(path, 'wb') as file: 48 | file.write(blob) 49 | cmdline = ['py-afl-cmin', '-i', indir, '-o', outdir, '--', sys.executable, target] 50 | if crashes_only: 51 | cmdline[1:1] = ['-C'] 52 | run(cmdline) 53 | output = [] 54 | for n in os.listdir(outdir): 55 | path = '{dir}/{n}'.format(dir=outdir, n=n) 56 | with open(path, 'rb') as file: 57 | blob = file.read() 58 | output += [blob] 59 | output.sort() 60 | assert_equal(xoutput, output) 61 | 62 | def test(): 63 | run_afl_cmin([ 64 | b'0' * 6, b'0', 65 | b'X' * 7, b'1', 66 | b'\xCF\x87', 67 | ], [ 68 | b'0', 69 | b'1', 70 | ]) 71 | 72 | def test_crashes_only(): 73 | run_afl_cmin([ 74 | b'0' * 6, b'0', 75 | b'X' * 7, b'1', 76 | b'\xCF\x87', 77 | ], [ 78 | b'\xCF\x87', 79 | ], crashes_only=True) 80 | 81 | # vim:ts=4 sts=4 sw=4 et 82 | -------------------------------------------------------------------------------- /tests/test_fuzz.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2024 Jakub Wilk 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 13 | # all 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 | 23 | from __future__ import print_function 24 | 25 | import base64 26 | import contextlib 27 | import glob 28 | import os 29 | import re 30 | import signal 31 | import subprocess as ipc 32 | import sys 33 | import time 34 | import warnings 35 | 36 | try: 37 | # Python >= 3.3 38 | from shlex import quote as shell_quote 39 | except ImportError: 40 | # Python << 3.3 41 | from pipes import quote as shell_quote # pylint: disable=deprecated-module 42 | 43 | try: 44 | # Python 3 45 | from itertools import izip_longest as zip_longest 46 | except ImportError: 47 | # Python 2 48 | from itertools import zip_longest 49 | 50 | from .tools import ( 51 | SkipTest, 52 | assert_true, 53 | clean_environ, 54 | require_commands, 55 | tempdir, 56 | ) 57 | 58 | here = os.path.dirname(__file__) 59 | 60 | token = base64.b64encode(os.urandom(8)) 61 | if not isinstance(token, str): 62 | token = token.decode('ASCII') 63 | 64 | def vcmp(v1, v2): 65 | ''' 66 | cmp()-style version comparison 67 | ''' 68 | v1 = v1.split('.') 69 | v2 = v2.split('.') 70 | for c1, c2 in zip_longest(v1, v2, fillvalue=0): 71 | c1 = int(c1) 72 | c2 = int(c2) 73 | if c1 > c2: 74 | return 1 75 | elif c1 < c2: 76 | return -1 77 | return 0 78 | 79 | def get_afl_version(): 80 | require_commands('afl-fuzz') 81 | child = ipc.Popen(['afl-fuzz'], stdout=ipc.PIPE) # pylint: disable=consider-using-with 82 | version = child.stdout.readline() 83 | child.stdout.close() 84 | child.wait() 85 | if str is not bytes: 86 | version = version.decode('ASCII') 87 | version = re.sub(r'\x1B\[[^m]+m', '', version) 88 | match = re.match(r'^afl-fuzz[+\s]+([0-9.]+)[a-z]?\b', version) 89 | if match is None: 90 | raise RuntimeError('could not parse AFL version') 91 | return match.group(1) 92 | 93 | def sleep(n): 94 | time.sleep(n) 95 | return n 96 | 97 | def check_core_pattern(): 98 | with open('/proc/sys/kernel/core_pattern', 'rb') as file: 99 | pattern = file.read() 100 | if str is not bytes: 101 | pattern = pattern.decode('ASCII', 'replace') 102 | pattern = pattern.rstrip('\n') 103 | if pattern.startswith('|'): 104 | raise SkipTest('/proc/sys/kernel/core_pattern = ' + pattern) 105 | 106 | def __test_fuzz(workdir, target, dumb=False): 107 | require_commands('py-afl-fuzz', 'afl-fuzz') 108 | input_dir = workdir + '/in' 109 | output_dir = workdir + '/out' 110 | os.mkdir(input_dir) 111 | os.mkdir(output_dir) 112 | with open(input_dir + '/in', 'wb') as file: 113 | file.write(b'0') 114 | have_crash = False 115 | have_paths = False 116 | n_paths = 0 117 | with open('/dev/null', 'wb') as devnull: 118 | with open(workdir + '/stdout', 'wb') as stdout: 119 | cmdline = ['py-afl-fuzz', '-i', input_dir, '-o', output_dir, '--', sys.executable, target, token] 120 | if dumb: 121 | cmdline[1:1] = ['-n'] 122 | print('$ ' + str.join(' ', map(shell_quote, cmdline))) 123 | afl = ipc.Popen( # pylint: disable=consider-using-with 124 | cmdline, 125 | stdout=stdout, 126 | stdin=devnull, 127 | preexec_fn=clean_environ, 128 | ) 129 | try: 130 | timeout = 10 131 | while timeout > 0: 132 | if afl.poll() is not None: 133 | break 134 | for ident in '', 'default': 135 | inst_out_dir = output_dir + '/' + ident 136 | crash_dir = inst_out_dir + '/crashes' 137 | queue_dir = inst_out_dir + '/queue' 138 | if os.path.isdir(queue_dir): 139 | break 140 | have_crash = len(glob.glob(crash_dir + '/id:*')) >= 1 141 | n_paths = len(glob.glob(queue_dir + '/id:*')) 142 | have_paths = (n_paths == 1) if dumb else (n_paths >= 2) 143 | if have_crash and have_paths: 144 | break 145 | timeout -= sleep(0.1) 146 | if afl.returncode is None: 147 | afl.terminate() 148 | afl.wait() 149 | except: 150 | afl.kill() 151 | raise 152 | with open(workdir + '/stdout', 'rb') as file: 153 | stdout = file.read() 154 | if str is not bytes: 155 | stdout = stdout.decode('ASCII', 'replace') 156 | print(stdout) 157 | if not have_crash and '/proc/sys/kernel/core_pattern' in stdout: 158 | check_core_pattern() 159 | assert_true(have_crash, "target program didn't crash") 160 | assert_true(have_paths, 'target program produced {n} distinct paths'.format(n=n_paths)) 161 | 162 | @contextlib.contextmanager 163 | def stray_process_cleanup(): 164 | # afl-fuzz doesn't always kill the target process: 165 | # https://groups.google.com/d/topic/afl-users/E37s4YDti7o 166 | require_commands('ps') 167 | try: 168 | yield 169 | finally: 170 | ps = ipc.Popen(['ps', 'ax'], stdout=ipc.PIPE) # pylint: disable=consider-using-with 171 | strays = [] 172 | for line in ps.stdout: 173 | if not isinstance(line, str): 174 | line = line.decode('ASCII', 'replace') 175 | if token in line: 176 | strays += [line] 177 | if strays: 178 | warnings.warn('stray process{es} left behind:\n{ps}'.format( 179 | es=('' if len(strays) == 1 else 'es'), 180 | ps=str.join('', strays) 181 | ), category=RuntimeWarning) 182 | for line in strays: 183 | pid = int(line.split()[0]) 184 | os.kill(pid, signal.SIGKILL) 185 | ps.wait() 186 | 187 | def _test_fuzz(target, dumb=False): 188 | with stray_process_cleanup(): 189 | with tempdir() as workdir: 190 | __test_fuzz( 191 | workdir=workdir, 192 | target=os.path.join(here, target), 193 | dumb=dumb, 194 | ) 195 | 196 | def test_fuzz_nonpersistent(): 197 | _test_fuzz('target.py') 198 | 199 | def test_fuzz_persistent(): 200 | _test_fuzz('target_persistent.py') 201 | 202 | def _maybe_skip_fuzz_dumb(): 203 | if vcmp(get_afl_version(), '1.95') < 0: 204 | raise SkipTest('afl-fuzz >= 1.95b is required') 205 | 206 | def test_fuzz_dumb_nonpersistent(): 207 | _maybe_skip_fuzz_dumb() 208 | _test_fuzz('target.py', dumb=True) 209 | 210 | def test_fuzz_dumb_persistent(): 211 | _maybe_skip_fuzz_dumb() 212 | _test_fuzz('target_persistent.py', dumb=True) 213 | 214 | # vim:ts=4 sts=4 sw=4 et 215 | -------------------------------------------------------------------------------- /tests/test_hash.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2017 Jakub Wilk 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 13 | # all 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 | 23 | import afl 24 | 25 | from .tools import ( 26 | assert_equal, 27 | ) 28 | 29 | def test_hash(): 30 | h = afl._hash # pylint: disable=protected-access 31 | assert_equal(h('', 0), 2166136261) 32 | assert_equal(h('', 42), 789356349) 33 | assert_equal(h('moo', 23), 3934561083) 34 | assert_equal(h('moo', 37), 3162790609) 35 | assert_equal(h('wół', 23), 2298935884) 36 | assert_equal(h('wół', 37), 3137816834) 37 | 38 | # vim:ts=4 sts=4 sw=4 et 39 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2018 Jakub Wilk 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 13 | # all 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 | 23 | import afl 24 | 25 | from .tools import ( 26 | assert_equal, 27 | ) 28 | 29 | exports = [ 30 | 'init', 31 | 'loop', 32 | ] 33 | 34 | deprecated = [ 35 | 'start', 36 | ] 37 | 38 | # pylint: disable=exec-used 39 | 40 | def wildcard_import(mod): 41 | ns = {} 42 | exec('from {mod} import *'.format(mod=mod), {}, ns) 43 | return ns 44 | 45 | def test_wildcard_import(): 46 | ns = wildcard_import('afl') 47 | assert_equal( 48 | sorted(ns.keys()), 49 | sorted(exports) 50 | ) 51 | 52 | def test_dir(): 53 | assert_equal( 54 | sorted(o for o in dir(afl) if not o.startswith('_')), 55 | sorted(exports + deprecated) 56 | ) 57 | 58 | # vim:ts=4 sts=4 sw=4 et 59 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2017 Jakub Wilk 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 13 | # all 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 | 23 | import re 24 | 25 | import afl 26 | 27 | from .tools import ( 28 | assert_raises_regex, 29 | assert_warns_regex, 30 | fork_isolation, 31 | ) 32 | 33 | @fork_isolation 34 | def test_deprecated_start(): 35 | msg = 'afl.start() is deprecated, use afl.init() instead' 36 | msg_re = '^{0}$'.format(re.escape(msg)) 37 | with assert_warns_regex(DeprecationWarning, msg_re): 38 | afl.start() 39 | 40 | @fork_isolation 41 | def test_double_init(): 42 | afl.init() 43 | with assert_raises_regex(RuntimeError, '^AFL already initialized$'): 44 | afl.init() 45 | 46 | # vim:ts=4 sts=4 sw=4 et 47 | -------------------------------------------------------------------------------- /tests/test_loop.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2017 Jakub Wilk 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 13 | # all 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 | 23 | import os 24 | import signal 25 | 26 | import afl 27 | 28 | from .tools import ( 29 | assert_equal, 30 | assert_raises_regex, 31 | fork_isolation, 32 | ) 33 | 34 | def test_persistent(): 35 | _test_persistent(None) 36 | _test_persistent(1, 1) 37 | _test_persistent(1, max=1) 38 | _test_persistent(42, 42) 39 | _test_persistent(42, max=42) 40 | 41 | @fork_isolation 42 | def _test_persistent(n, *args, **kwargs): 43 | os.environ['PYTHON_AFL_PERSISTENT'] = '1' 44 | n_max = 1000 45 | k = [0] 46 | def kill(pid, sig): 47 | assert_equal(pid, os.getpid()) 48 | assert_equal(sig, signal.SIGSTOP) 49 | k[0] += 1 50 | os.kill = kill 51 | x = 0 52 | while afl.loop(*args, **kwargs): 53 | x += 1 54 | if x == n_max: 55 | break 56 | if n is None: 57 | n = n_max 58 | assert_equal(x, n) 59 | assert_equal(k[0], n - 1) 60 | 61 | def test_docile(): 62 | _test_docile() 63 | _test_docile(1) 64 | _test_docile(max=1) 65 | _test_docile(42) 66 | _test_docile(max=42) 67 | 68 | @fork_isolation 69 | def _test_docile(*args, **kwargs): 70 | os.environ.pop('PYTHON_AFL_PERSISTENT', None) 71 | x = 0 72 | while afl.loop(*args, **kwargs): 73 | x += 1 74 | assert_equal(x, 1) 75 | 76 | @fork_isolation 77 | def test_double_init(): 78 | afl.init() 79 | with assert_raises_regex(RuntimeError, '^AFL already initialized$'): 80 | while afl.loop(): 81 | pass 82 | 83 | # vim:ts=4 sts=4 sw=4 et 84 | -------------------------------------------------------------------------------- /tests/test_showmap.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2021 Jakub Wilk 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 13 | # all 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 | 23 | import os 24 | import sys 25 | 26 | from .tools import ( 27 | assert_in, 28 | assert_not_equal, 29 | require_commands, 30 | run, 31 | tempdir, 32 | ) 33 | 34 | here = os.path.dirname(__file__) 35 | target = here + '/target.py' 36 | 37 | def run_afl_showmap(stdin, xstdout=None, xstatus=0): 38 | require_commands('py-afl-showmap', 'afl-showmap') 39 | with tempdir() as workdir: 40 | outpath = workdir + '/out' 41 | (stdout, stderr) = run( 42 | ['py-afl-showmap', '-o', outpath, sys.executable, target], 43 | stdin=stdin, 44 | xstatus=xstatus, 45 | ) 46 | del stderr # make pylint happy 47 | if xstdout is not None: 48 | # FIXME! This works in AFL, but not in AFL++: 49 | # assert_equal(stdout, xstdout) 50 | assert_in(xstdout, stdout) 51 | with open(outpath, 'rb') as file: 52 | return file.read() 53 | 54 | def test_diff(): 55 | out1 = run_afl_showmap(b'0', xstdout=b'Looks like a zero to me!\n') 56 | out2 = run_afl_showmap(b'1', xstdout=b'A non-zero value? How quaint!\n') 57 | assert_not_equal(out1, out2) 58 | 59 | def test_exception(): 60 | out = run_afl_showmap(b'\xFF', 61 | xstatus=2, 62 | ) 63 | assert_not_equal(out, b'') 64 | 65 | # vim:ts=4 sts=4 sw=4 et 66 | -------------------------------------------------------------------------------- /tests/test_tmin.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2018 Jakub Wilk 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 13 | # all 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 | 23 | import os 24 | import sys 25 | 26 | from .tools import ( 27 | assert_equal, 28 | require_commands, 29 | run, 30 | tempdir, 31 | ) 32 | 33 | here = os.path.dirname(__file__) 34 | target = here + '/target.py' 35 | 36 | def run_afl_tmin(input, xoutput, xstatus=0): 37 | require_commands('py-afl-tmin', 'afl-tmin') 38 | with tempdir() as workdir: 39 | inpath = workdir + '/in' 40 | with open(inpath, 'wb') as file: 41 | file.write(input) 42 | outpath = workdir + '/out' 43 | run( 44 | ['py-afl-tmin', '-i', inpath, '-o', outpath, '--', sys.executable, target], 45 | xstatus=xstatus, 46 | ) 47 | with open(outpath, 'rb') as file: 48 | output = file.read() 49 | assert_equal(output, xoutput) 50 | 51 | def test(): 52 | run_afl_tmin(b'0' * 6, b'0') 53 | run_afl_tmin(b'X' * 7, b'X') 54 | 55 | def test_exc(): 56 | run_afl_tmin(b'\xCF\x87', b'\x87') 57 | 58 | # vim:ts=4 sts=4 sw=4 et 59 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2015-2019 Jakub Wilk 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 13 | # all 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 | 23 | import io 24 | import os 25 | 26 | import afl 27 | 28 | from .tools import ( 29 | assert_equal 30 | ) 31 | 32 | here = os.path.dirname(__file__) 33 | docdir = here + '/../doc' 34 | 35 | def test_changelog(): 36 | path = docdir + '/changelog' 37 | with io.open(path, 'rt', encoding='UTF-8') as file: 38 | line = file.readline() 39 | changelog_version = line.split()[1].strip('()') 40 | assert_equal(changelog_version, afl.__version__) 41 | 42 | # vim:ts=4 sts=4 sw=4 et 43 | -------------------------------------------------------------------------------- /tests/tools.py: -------------------------------------------------------------------------------- 1 | # encoding=UTF-8 2 | 3 | # Copyright © 2013-2022 Jakub Wilk 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 13 | # all 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 | 23 | from __future__ import print_function 24 | 25 | import contextlib 26 | import functools 27 | import os 28 | import re 29 | import shutil 30 | import subprocess as ipc 31 | import sys 32 | import tempfile 33 | import traceback 34 | import unittest 35 | import warnings 36 | 37 | try: 38 | # Python >= 3.3 39 | from shlex import quote as sh_quote 40 | except ImportError: 41 | # Python << 3.3 42 | from pipes import quote as sh_quote # pylint: disable=deprecated-module 43 | 44 | SkipTest = unittest.SkipTest 45 | 46 | def assert_fail(msg): 47 | assert_true(False, msg=msg) # pylint: disable=redundant-unittest-assert 48 | 49 | tc = unittest.TestCase('__hash__') 50 | 51 | assert_equal = tc.assertEqual 52 | 53 | assert_in = tc.assertIn 54 | 55 | assert_not_equal = tc.assertNotEqual 56 | 57 | assert_true = tc.assertTrue 58 | 59 | assert_raises = tc.assertRaises 60 | 61 | # pylint: disable=no-member 62 | if sys.version_info >= (3,): 63 | assert_raises_regex = tc.assertRaisesRegex 64 | else: 65 | assert_raises_regex = tc.assertRaisesRegexp 66 | # pylint: enable=no-member 67 | 68 | # pylint: disable=no-member 69 | if sys.version_info >= (3,): 70 | assert_regex = tc.assertRegex 71 | else: 72 | assert_regex = tc.assertRegexpMatches 73 | # pylint: enable=no-member 74 | 75 | if sys.version_info >= (3,): 76 | assert_warns_regex = tc.assertWarnsRegex # pylint: disable=no-member 77 | else: 78 | @contextlib.contextmanager 79 | def assert_warns_regex(exc_type, regex): 80 | with warnings.catch_warnings(record=True) as wlog: 81 | warnings.simplefilter('always', exc_type) 82 | yield 83 | firstw = None 84 | for warning in wlog: 85 | w = warning.message 86 | if not isinstance(w, exc_type): 87 | continue 88 | if firstw is None: 89 | firstw = w 90 | if re.search(regex, str(w)): 91 | return 92 | if firstw is None: 93 | assert_fail(msg='{exc} not triggered'.format(exc=exc_type.__name__)) 94 | else: 95 | assert_fail(msg='{exc!r} does not match {re!r}'.format(exc=str(firstw), re=regex)) 96 | 97 | class IsolatedException(Exception): 98 | pass 99 | 100 | def _n_relevant_tb_levels(tb): 101 | n = 0 102 | while tb and '__unittest' not in tb.tb_frame.f_globals: 103 | n += 1 104 | tb = tb.tb_next 105 | return n 106 | 107 | def clean_environ(): 108 | for key in list(os.environ): 109 | if key.startswith('PYTHON_AFL_'): 110 | del os.environ[key] 111 | os.environ['AFL_SKIP_CPUFREQ'] = '1' 112 | os.environ['AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES'] = '1' 113 | os.environ['AFL_NO_AFFINITY'] = '1' 114 | os.environ['AFL_ALLOW_TMP'] = '1' # AFL >= 2.48b 115 | os.environ['PWD'] = '//' + os.getcwd() # poor man's AFL_ALLOW_TMP for AFL << 2.48b 116 | 117 | def require_commands(*cmds): 118 | PATH = os.environ.get('PATH', os.defpath) 119 | PATH = PATH.split(os.pathsep) 120 | for cmd in cmds: 121 | for dir in PATH: 122 | path = os.path.join(dir, cmd) 123 | if os.access(path, os.X_OK): 124 | break 125 | else: 126 | if cmd == 'ps': 127 | cmd = 'ps(1)' 128 | reason = 'procps installed' 129 | elif cmd.startswith('afl-'): 130 | reason = 'AFL installed' 131 | else: 132 | reason = 'PATH set correctly' 133 | raise RuntimeError('{cmd} not found; is {reason}?'.format(cmd=cmd, reason=reason)) 134 | 135 | def run(cmd, stdin='', xstatus=0): 136 | cmd = list(cmd) 137 | child = ipc.Popen( # pylint: disable=consider-using-with 138 | cmd, 139 | stdin=ipc.PIPE, 140 | stdout=ipc.PIPE, 141 | stderr=ipc.PIPE, 142 | preexec_fn=clean_environ, 143 | ) 144 | (stdout, stderr) = child.communicate(stdin) 145 | if child.returncode != xstatus: 146 | print('command:', '\n ', *map(sh_quote, cmd)) 147 | def xprint(**kwargs): 148 | [(name, out)] = kwargs.items() # pylint: disable=unbalanced-tuple-unpacking,unbalanced-dict-unpacking 149 | if not out: 150 | return 151 | print() 152 | print(name, ':', sep='') 153 | if str is not bytes: 154 | out = out.decode('ASCII', 'replace') 155 | for line in out.splitlines(): 156 | print(' ', line) 157 | xprint(stdout=stdout) 158 | xprint(stderr=stderr) 159 | raise ipc.CalledProcessError(child.returncode, cmd[0]) 160 | return (stdout, stderr) 161 | 162 | def fork_isolation(f): 163 | 164 | EXIT_EXCEPTION = 101 165 | EXIT_SKIP_TEST = 102 166 | 167 | exit = os._exit # pylint: disable=redefined-builtin,protected-access 168 | # sys.exit() can't be used here, because the test harness 169 | # catches all exceptions, including SystemExit 170 | 171 | # pylint:disable=consider-using-sys-exit 172 | 173 | @functools.wraps(f) 174 | def wrapper(*args, **kwargs): 175 | readfd, writefd = os.pipe() 176 | pid = os.fork() 177 | if pid == 0: 178 | # child: 179 | os.close(readfd) 180 | try: 181 | f(*args, **kwargs) 182 | except SkipTest as exc: 183 | s = str(exc) 184 | if not isinstance(s, bytes): 185 | s = s.encode('UTF-8') 186 | with os.fdopen(writefd, 'wb') as fp: 187 | fp.write(s) 188 | exit(EXIT_SKIP_TEST) 189 | except Exception: # pylint: disable=broad-except 190 | exctp, exc, tb = sys.exc_info() 191 | s = traceback.format_exception(exctp, exc, tb, _n_relevant_tb_levels(tb)) 192 | s = str.join('', s) 193 | if not isinstance(s, bytes): 194 | s = s.encode('UTF-8') 195 | del tb 196 | with os.fdopen(writefd, 'wb') as fp: 197 | fp.write(s) 198 | exit(EXIT_EXCEPTION) 199 | exit(0) 200 | else: 201 | # parent: 202 | os.close(writefd) 203 | with os.fdopen(readfd, 'rb') as fp: 204 | msg = fp.read() 205 | if not isinstance(msg, str): 206 | msg = msg.decode('UTF-8') 207 | msg = msg.rstrip('\n') 208 | pid, status = os.waitpid(pid, 0) 209 | if status == (EXIT_EXCEPTION << 8): 210 | raise IsolatedException('\n\n' + msg) 211 | elif status == (EXIT_SKIP_TEST << 8): 212 | raise SkipTest(msg) 213 | elif status == 0 and msg == '': 214 | pass 215 | else: 216 | raise RuntimeError('unexpected isolated process status {0}'.format(status)) 217 | 218 | # pylint:enable=consider-using-sys-exit 219 | 220 | return wrapper 221 | 222 | @contextlib.contextmanager 223 | def tempdir(): 224 | d = tempfile.mkdtemp(prefix='python-afl.') 225 | try: 226 | yield d 227 | finally: 228 | shutil.rmtree(d) 229 | 230 | __all__ = [ 231 | 'SkipTest', 232 | 'assert_equal', 233 | 'assert_in', 234 | 'assert_not_equal', 235 | 'assert_raises', 236 | 'assert_raises_regex', 237 | 'assert_regex', 238 | 'assert_true', 239 | 'assert_warns_regex', 240 | 'fork_isolation', 241 | 'require_commands', 242 | 'run', 243 | 'tempdir', 244 | ] 245 | 246 | # vim:ts=4 sts=4 sw=4 et 247 | --------------------------------------------------------------------------------