python-afl-0.5.1/0000755000000000000000000000000012576753413013611 5ustar00rootroot00000000000000python-afl-0.5.1/MANIFEST.in0000644000000000000000000000017412575575634015357 0ustar00rootroot00000000000000exclude *.c include *.pyx include MANIFEST.in include doc/* include private/* include py-afl-* recursive-include tests *.py python-afl-0.5.1/doc/0000755000000000000000000000000012576753413014356 5ustar00rootroot00000000000000python-afl-0.5.1/doc/trophy-case.rst0000644000000000000000000000356012571343202017334 0ustar00rootroot00000000000000The bug-o-rama trophy case ========================== The following bugs were found with help of *python-afl*: i18nspector__ ------------- Multiple bugs in the plural expression parser: | https://bitbucket.org/jwilk/i18nspector/commits/9bc2165dcd95 | https://bitbucket.org/jwilk/i18nspector/commits/14ca54c29cf8 | https://bitbucket.org/jwilk/i18nspector/commits/b724d7d744ad | https://bitbucket.org/jwilk/i18nspector/commits/53e0049117f1 | https://bitbucket.org/jwilk/i18nspector/commits/2b68fb57404d | https://bitbucket.org/jwilk/i18nspector/commits/8e5f3709da8d | https://bitbucket.org/jwilk/i18nspector/commits/18f9b9e29c7a | https://bitbucket.org/jwilk/i18nspector/commits/0da379394722 .. __: http://jwilk.net/software/i18nspector PyPDF2__ -------- | https://github.com/mstamy2/PyPDF2/issues/184 .. __: https://mstamy2.github.io/PyPDF2/ enzyme__ -------- | https://github.com/Diaoul/enzyme/issues/9 | https://github.com/Diaoul/enzyme/issues/10 | https://github.com/Diaoul/enzyme/issues/11 | https://github.com/Diaoul/enzyme/issues/12 | https://github.com/Diaoul/enzyme/issues/13 | https://github.com/Diaoul/enzyme/issues/14 | https://github.com/Diaoul/enzyme/issues/15 | https://github.com/Diaoul/enzyme/issues/16 | https://github.com/Diaoul/enzyme/issues/17 | https://github.com/Diaoul/enzyme/issues/18 | https://github.com/Diaoul/enzyme/issues/19 | https://github.com/Diaoul/enzyme/issues/20 | https://github.com/Diaoul/enzyme/issues/21 | https://github.com/Diaoul/enzyme/issues/22 .. __: https://github.com/Diaoul/enzyme PyASN.1__ --------- | https://github.com/pyca/cryptography/issues/1838 .. __: http://pyasn1.sourceforge.net/ gunicorn__ ---------- | https://github.com/benoitc/gunicorn/issues/1023 .. __: http://gunicorn.org/ dateutil__ ---------- | https://github.com/dateutil/dateutil/issues/82 .. __: https://pypi.python.org/pypi/python-dateutil/ python-afl-0.5.1/doc/changelog0000644000000000000000000000453112576752555016241 0ustar00rootroot00000000000000python-afl (0.5.1) unstable; urgency=low * Fix TypeError when built with Cython 0.23.2. -- Jakub Wilk Fri, 18 Sep 2015 11:12:12 +0200 python-afl (0.5) unstable; urgency=low * Fix deprecated call to afl.start() in README. * Make afl.start() emit DeprecationWarning. * Enable persistent mode only if PYTHON_AFL_PERSISTENT set. Let py-afl-fuzz set this variable. * Don't install the exception hook, unless enabled with PYTHON_AFL_SIGNAL. Let py-afl-fuzz set this variable to SIGUSR1. -- Jakub Wilk Wed, 02 Sep 2015 11:12:42 +0200 python-afl (0.4) unstable; urgency=low * Rename afl.start() as afl.init(), for consistency with AFL >= 1.89b. The old name is still available, but it's deprecated. * Add new interface for persistent mode, afl.loop(). Remove the old interface, afl.persistent(). * Improve the test suite. -- Jakub Wilk Tue, 01 Sep 2015 16:28:06 +0200 python-afl (0.3) unstable; urgency=low * Implement persistent mode. * Add docstrings for the exported functions. * Add __version__. * Don't rely on the Python hash() function for computing code location identifiers. + Don't set PYTHONHASHSEED in py-afl-fuzz. + Remove the py-afl-showmap command. afl-showmap proper can be now used for Python code. + Remove the AflError class. It was only used for checking PYTHONHASHSEED. * Improve the setup script: + Run Cython only in those setup.py commands that actually build extensions. + Check Cython version. * Implement a small test suite. -- Jakub Wilk Mon, 31 Aug 2015 16:56:12 +0200 python-afl (0.2.1) unstable; urgency=low * Make the setup script install the py-afl-fuzz and py-afl-showmap binaries. -- Jakub Wilk Tue, 14 Jul 2015 19:34:35 +0200 python-afl (0.2) unstable; urgency=low * Automatically disable instrumentation when the -n option is provided. Setting the PYTHON_AFL_DUMB environment variable is no longer needed. Thanks to Michal Zalewski for the hint how to implement this feature. * Update the module description to stress that it's dedicated for pure-Python code. -- Jakub Wilk Mon, 27 Apr 2015 19:31:08 +0200 python-afl (0.1) unstable; urgency=low * Initial release. -- Jakub Wilk Fri, 17 Apr 2015 00:15:16 +0200 python-afl-0.5.1/doc/README.rst0000644000000000000000000000346112571420276016042 0ustar00rootroot00000000000000This is experimental module that enables `American fuzzy lop`_ fork server and instrumentation for pure-Python code. .. _American fuzzy lop: http://lcamtuf.coredump.cx/afl/ HOWTO ----- * Add this code (ideally, after all other modules are already imported) to the target program: .. code:: python import afl afl.init() * Optionally, add this code at the end of the target program: .. code:: python os._exit(0) This should speed up fuzzing considerably, at the risk of not catching bugs that could happen during normal exit. * For persistent mode, wrap the tested code in this loop: .. code:: python while afl.loop(N): ... where ``N`` is the number of inputs to process before restarting. You shouldn't call ``afl.init()`` in this case. afl-fuzz ≥ 1.82b is required for this feature. * Use *py-afl-fuzz* instead of *afl-fuzz*:: $ py-afl-fuzz [options] -- /path/to/fuzzed/python/script [...] * The instrumentation is a bit slow at the moment, so you might want to enable the dumb mode (``-n``), while still leveraging the fork server. Environment variables --------------------- The following environment variables affect *python-afl* behavior: ``PYTHON_AFL_SIGNAL`` If this variable is set, *python-afl* installs an exception hook that kills the current process with the selected signal. That way *afl-fuzz* can treat unhandled exceptions as crashes. By default, *py-afl-fuzz* set this variable to ``SIGUSR1``. You can set ``PYTHON_AFL_SIGNAL`` to another signal; or set it to ``0`` to disable the exception hook. ``PYTHON_AFL_PERSISTENT`` Persistent mode is enabled only if this variable is set. *py-afl-fuzz* sets this variable automatically, so there should normally no need to set it manually. .. vim:ts=3 sts=3 sw=3 et python-afl-0.5.1/afl.pyx0000644000000000000000000001423012575575634015123 0ustar00rootroot00000000000000# Copyright © 2014-2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. #cython: autotestdict=False #cython: c_string_encoding=default ''' American fuzzy lop fork server and instrumentation for pure-Python code ''' __version__ = '0.5.1' cdef object os, signal, struct, sys, warnings import os import signal import struct import sys import warnings # These constants must be kept in sync with afl-fuzz: DEF SHM_ENV_VAR = '__AFL_SHM_ID' DEF FORKSRV_FD = 198 DEF MAP_SIZE_POW2 = 16 DEF MAP_SIZE = 1 << MAP_SIZE_POW2 from cpython.exc cimport PyErr_SetFromErrno from libc cimport errno from libc.stddef cimport size_t from libc.stdint cimport uint32_t from libc.stdlib cimport getenv from libc.string cimport strlen cdef extern from 'sys/shm.h': unsigned char *shmat(int shmid, void *shmaddr, int shmflg) cdef unsigned char *afl_area = NULL cdef unsigned int prev_location = 0 cdef inline unsigned int lhash(const char *key, size_t offset): # 32-bit Fowler–Noll–Vo hash function cdef size_t len = strlen(key) cdef uint32_t h = 0x811C9DC5 while len > 0: h ^= key[0]; h *= 0x01000193 len -= 1 key += 1 while offset > 0: h ^= offset; h *= 0x01000193 offset >>= 8 return h def _hash(key, offset): # This function is not a part of public API. # It is provided only to facilitate testing. return lhash(key, offset) cdef object trace def trace(frame, event, arg): global prev_location cdef unsigned int location, offset location = ( lhash(frame.f_code.co_filename, frame.f_lineno) % MAP_SIZE ) offset = location ^ prev_location prev_location = location // 2 afl_area[offset] += 1 # TODO: make it configurable which modules are instrumented, and which are not return trace cdef int except_signal_id = 0 cdef object except_signal_name = os.getenv('PYTHON_AFL_SIGNAL') or '0' if except_signal_name.isdigit(): except_signal_id = int(except_signal_name) else: if except_signal_name[:3] != 'SIG': except_signal_name = 'SIG' + except_signal_name except_signal_id = getattr(signal, except_signal_name) cdef object excepthook def excepthook(tp, value, traceback): os.kill(os.getpid(), except_signal_id) cdef bint init_done = False cdef int _init(bint persistent_mode) except -1: global afl_area, init_done use_forkserver = True try: os.write(FORKSRV_FD + 1, b'\0\0\0\0') except OSError as exc: if exc.errno == errno.EBADF: use_forkserver = False else: raise if init_done: raise RuntimeError('AFL already initialized') init_done = True child_stopped = False child_pid = 0 while use_forkserver: [child_killed] = struct.unpack('I', os.read(FORKSRV_FD, 4)) if child_stopped and child_killed: if child_killed: os.waitpid(child_pid, 0) child_stopped = False if child_stopped: os.kill(child_pid, signal.SIGCONT) child_stopped = False else: child_pid = os.fork() if not child_pid: # child: break # parent: os.write(FORKSRV_FD + 1, struct.pack('I', child_pid)) (child_pid, status) = os.waitpid(child_pid, os.WUNTRACED if persistent_mode else 0) child_stopped = os.WIFSTOPPED(status) os.write(FORKSRV_FD + 1, struct.pack('I', status)) if use_forkserver: os.close(FORKSRV_FD) os.close(FORKSRV_FD + 1) if except_signal_id != 0: sys.excepthook = excepthook cdef const char * afl_shm_id = getenv(SHM_ENV_VAR) if afl_shm_id == NULL: return 0 afl_area = shmat(int(afl_shm_id), NULL, 0) if afl_area == -1: PyErr_SetFromErrno(OSError) sys.settrace(trace) return 0 def init(): ''' init() Start the fork server and enable instrumentation. This function should be called as late as possible, but before the input is read, and before any threads are started. ''' _init(persistent_mode=False) def start(): ''' deprecated alias for afl.init() ''' warnings.warn('afl.start() is deprecated, use afl.init() instead', DeprecationWarning) _init(persistent_mode=False) cdef bint persistent_allowed = False cdef unsigned long persistent_counter = 0 def loop(max=None): ''' while persistent([max]): ... Start the fork server and enable instrumentation, then run the code inside the loop body in persistent mode. afl-fuzz >= 1.82b is required for this feature. ''' global persistent_allowed, persistent_counter if persistent_counter == 0: persistent_allowed = os.getenv('PYTHON_AFL_PERSISTENT') is not None _init(persistent_mode=persistent_allowed) persistent_counter = 1 return True cont = persistent_allowed and ( max is None or persistent_counter < max ) if cont: os.kill(os.getpid(), signal.SIGSTOP) persistent_counter += 1 return True else: return False __all__ = [ 'init', 'loop', ] # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/0000755000000000000000000000000012576753413014753 5ustar00rootroot00000000000000python-afl-0.5.1/tests/target_persistent.py0000644000000000000000000000056512571324651021072 0ustar00rootroot00000000000000#!/usr/bin/python import sys import afl def main(): s = sys.stdin.read() if len(s) < 1: print('Hum?') sys.exit(1) s.encode('ASCII') if s[0] == '0': print('Looks like a zero to me!') else: print('A non-zero value? How quaint!') if __name__ == '__main__': while afl.loop(): main() # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/tools.py0000644000000000000000000001350212571542655016465 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2013-2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import contextlib import functools import os import re import sys import traceback import warnings import nose.tools from nose import SkipTest from nose.tools import ( assert_equal, assert_not_equal, assert_true, ) def noseimport(vmaj, vmin, name=None): def wrapper(f): if f.__module__ == 'unittest.case': return f if sys.version_info >= (vmaj, vmin): return getattr(nose.tools, name or f.__name__) return f return wrapper @noseimport(2, 7) class assert_raises(object): def __init__(self, exc_type): self._exc_type = exc_type self.exception = None def __enter__(self): return self def __exit__(self, exc_type, exc_value, tb): if exc_type is None: assert_true(False, '{0} not raised'.format(self._exc_type.__name__)) if not issubclass(exc_type, self._exc_type): return False if isinstance(exc_value, exc_type): pass # This branch is not always taken in Python 2.6: # https://bugs.python.org/issue7853 elif isinstance(exc_value, tuple): exc_value = exc_type(*exc_value) else: exc_value = exc_type(exc_value) self.exception = exc_value return True @noseimport(2, 7, 'assert_raises_regexp') @noseimport(3, 2) @contextlib.contextmanager def assert_raises_regex(exc_type, regex): with assert_raises(exc_type) as ecm: yield assert_regex(str(ecm.exception), regex) @noseimport(2, 7, 'assert_regexp_matches') @noseimport(3, 2) def assert_regex(text, regex): if isinstance(regex, basestring): regex = re.compile(regex) if not regex.search(text): message = "Regex didn't match: {0!r} not found in {1!r}".format(regex.pattern, text) assert_true(False, msg=message) @noseimport(3, 2) @contextlib.contextmanager def assert_warns_regex(exc_type, regex): with warnings.catch_warnings(record=True) as wlog: warnings.simplefilter('always', exc_type) yield firstw = None for warning in wlog: w = warning.message if not isinstance(w, exc_type): continue if firstw is None: firstw = w if re.search(regex, str(w)): return if firstw is None: assert_true(False, msg='{exc} not triggered'.format(exc=exc_type.__name__)) else: assert_true(False, msg='{exc!r} does not match {re!r}'.format(exc=str(firstw), re=regex)) class IsolatedError(Exception): pass def _n_relevant_tb_levels(tb): n = 0 while tb and '__unittest' not in tb.tb_frame.f_globals: n += 1 tb = tb.tb_next return n def fork_isolation(f): EXIT_EXCEPTION = 101 EXIT_SKIP_TEST = 102 exit = os._exit # sys.exit() can't be used here, because nose catches all exceptions, # including SystemExit @functools.wraps(f) def wrapper(*args, **kwargs): readfd, writefd = os.pipe() pid = os.fork() if pid == 0: # child: os.close(readfd) try: f(*args, **kwargs) except SkipTest as exc: s = str(exc) if not isinstance(s, bytes): s = s.encode('UTF-8') with os.fdopen(writefd, 'wb') as fp: fp.write(s) exit(EXIT_SKIP_TEST) except Exception: exctp, exc, tb = sys.exc_info() s = traceback.format_exception(exctp, exc, tb, _n_relevant_tb_levels(tb)) s = ''.join(s) if not isinstance(s, bytes): s = s.encode('UTF-8') del tb with os.fdopen(writefd, 'wb') as fp: fp.write(s) exit(EXIT_EXCEPTION) exit(0) else: # parent: os.close(writefd) with os.fdopen(readfd, 'rb') as fp: msg = fp.read() msg = msg if not isinstance(msg, str): msg = msg.decode('UTF-8') msg = msg.rstrip('\n') pid, status = os.waitpid(pid, 0) if status == (EXIT_EXCEPTION << 8): raise IsolatedError('\n\n' + msg) elif status == (EXIT_SKIP_TEST << 8): raise SkipTest(msg) elif status == 0 and msg == '': pass else: raise RuntimeError('unexpected isolated process status {}'.format(status)) return wrapper __all__ = [ 'SkipTest', 'assert_equal', 'assert_not_equal', 'assert_raises', 'assert_raises_regex', 'assert_regex', 'assert_true', 'assert_warns_regex', 'fork_isolation', ] # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/test_showmap.py0000644000000000000000000000535112571523753020043 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import shutil import subprocess as ipc import sys import tempfile from .tools import ( assert_equal, assert_not_equal, ) here = os.path.dirname(__file__) target = here + '/target.py' def run(cmd, stdin='', env=None, xstatus=0): child = ipc.Popen( list(cmd), stdin=ipc.PIPE, stdout=ipc.PIPE, stderr=ipc.PIPE, env=(dict(os.environ, **env) if env else None), ) (stdout, stderr) = child.communicate(stdin) if child.returncode != xstatus: if str != bytes: stderr = stderr.decode('ASCII', 'replace') print(stderr) raise ipc.CalledProcessError(child.returncode, cmd[0]) return (stdout, stderr) def run_afl_showmap(stdin, env=None, xstdout=None, xstatus=0): tmpdir = tempfile.mkdtemp(prefix='python-afl.') outpath = tmpdir + '/out' try: (stdout, stderr) = run( ['afl-showmap', '-o', outpath, sys.executable, target], stdin=stdin, env=env, xstatus=xstatus, ) del stderr # make pylint happy if xstdout is not None: assert_equal(stdout, xstdout) with open(outpath, 'r') as file: return file.read() finally: shutil.rmtree(tmpdir) def test_diff(): out1 = run_afl_showmap(b'0', xstdout=b'Looks like a zero to me!\n') out2 = run_afl_showmap(b'1', xstdout=b'A non-zero value? How quaint!\n') assert_not_equal(out1, out2) def test_exception(): out = run_afl_showmap(b'\xff', env=dict(PYTHON_AFL_SIGNAL='SIGUSR1'), xstatus=2, ) assert_not_equal(out, b'') # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/test_version.py0000644000000000000000000000314312571327432020043 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os from .tools import ( assert_equal ) import afl here = os.path.dirname(__file__) docdir = here + '/../doc' def uopen(path): if str != bytes: return open(path, 'rt', encoding='UTF-8') else: return open(path, 'rt') def test_changelog(): path = docdir + '/changelog' with uopen(path) as file: line = file.readline() changelog_version = line.split()[1].strip('()') assert_equal(changelog_version, afl.__version__) # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/target.py0000644000000000000000000000055212571324651016606 0ustar00rootroot00000000000000#!/usr/bin/python import sys import afl def main(): s = sys.stdin.read() if len(s) < 1: print('Hum?') sys.exit(1) s.encode('ASCII') if s[0] == '0': print('Looks like a zero to me!') else: print('A non-zero value? How quaint!') if __name__ == '__main__': afl.init() main() # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/test_fuzz.py0000644000000000000000000000732412575514226017364 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import glob import os import shutil import subprocess as ipc import sys import tempfile import time from .tools import ( SkipTest, assert_true, ) here = os.path.dirname(__file__) def sleep(n): time.sleep(n) return n def check_core_pattern(): with open('/proc/sys/kernel/core_pattern', 'rb') as file: pattern = file.read() if str != bytes: pattern = pattern.decode('ASCII', 'replace') pattern = pattern.rstrip('\n') if pattern.startswith('|'): raise SkipTest('/proc/sys/kernel/core_pattern = ' + pattern) def _test_fuzz(workdir, target): input_dir = workdir + '/in' output_dir = workdir + '/out' os.mkdir(input_dir) os.mkdir(output_dir) with open(input_dir + '/in', 'w') as file: file.write('0') crash_dir = output_dir + '/crashes' queue_dir = output_dir + '/queue' have_crash = False have_paths = False def setup_env(): os.environ['AFL_SKIP_CPUFREQ'] = '1' os.environ['AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES'] = '1' with open('/dev/null', 'wb') as devnull: with open(workdir + '/stdout', 'wb') as stdout: afl = ipc.Popen( ['py-afl-fuzz', '-i', input_dir, '-o', output_dir, '--', sys.executable, target], stdout=stdout, stdin=devnull, preexec_fn=setup_env, ) try: timeout = 10 while timeout > 0: if afl.poll() is not None: break have_crash = len(glob.glob(crash_dir + '/id:*')) >= 1 have_paths = len(glob.glob(queue_dir + '/id:*')) >= 2 if have_crash and have_paths: break timeout -= sleep(0.1) if afl.returncode is None: afl.terminate() afl.wait() except: afl.kill() raise with open(workdir + '/stdout', 'rb') as file: stdout = file.read() if str != bytes: stdout = stdout.decode('ASCII', 'replace') print(stdout) if not have_crash and '/proc/sys/kernel/core_pattern' in stdout: check_core_pattern() assert_true(have_crash, "target program didn't crash") assert_true(have_paths, "target program didn't produce two distinct paths") def test_fuzz(): def t(target): tmpdir = tempfile.mkdtemp(prefix='python-afl.') try: _test_fuzz( workdir=tmpdir, target=os.path.join(here, target) ) finally: shutil.rmtree(tmpdir) yield t, 'target.py' yield t, 'target_persistent.py' # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/test_loop.py0000644000000000000000000000455112571524344017334 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import signal from .tools import ( assert_equal, assert_raises_regex, fork_isolation, ) import afl def test_persistent(): _test_persistent(None) _test_persistent(1, 1) _test_persistent(1, max=1) _test_persistent(42, 42) _test_persistent(42, max=42) @fork_isolation def _test_persistent(n, *args, **kwargs): os.environ['PYTHON_AFL_PERSISTENT'] = '1' n_max = 1000 k = [0] def kill(pid, sig): assert_equal(pid, os.getpid()) assert_equal(sig, signal.SIGSTOP) k[0] += 1 os.kill = kill x = 0 while afl.loop(*args, **kwargs): x += 1 if x == n_max: break if n is None: n = n_max assert_equal(x, n) assert_equal(k[0], n - 1) def test_docile(): _test_docile() _test_docile(1) _test_docile(max=1) _test_docile(42) _test_docile(max=42) @fork_isolation def _test_docile(*args, **kwargs): os.environ.pop('PYTHON_AFL_PERSISTENT', None) x = 0 while afl.loop(*args, **kwargs): x += 1 assert_equal(x, 1) @fork_isolation def test_double_init(): afl.init() with assert_raises_regex(RuntimeError, '^AFL already initialized$'): while afl.loop(): pass # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/test_import.py0000644000000000000000000000320312571327376017674 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .tools import ( assert_equal, ) import afl exports = [ 'init', 'loop', ] deprecated = [ 'start', ] def wildcard_import(mod): ns = {} exec('from {mod} import *'.format(mod=mod), {}, ns) return ns def test_wildcard_import(): ns = wildcard_import('afl') assert_equal( sorted(ns.keys()), sorted(exports) ) def test_dir(): assert_equal( sorted(o for o in dir(afl) if not o.startswith('_')), sorted(exports + deprecated) ) # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/__init__.py0000644000000000000000000000000012571036353017042 0ustar00rootroot00000000000000python-afl-0.5.1/tests/test_hash.py0000644000000000000000000000273612571327366017316 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import afl from .tools import ( assert_equal, ) def test_hash(): h = afl._hash assert_equal(h('', 0), 2166136261) assert_equal(h('', 42), 789356349) assert_equal(h('moo', 23), 3934561083) assert_equal(h('moo', 37), 3162790609) assert_equal(h('wół', 23), 2298935884) assert_equal(h('wół', 37), 3137816834) # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/tests/test_init.py0000644000000000000000000000316312571517667017336 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re from .tools import ( assert_raises_regex, assert_warns_regex, fork_isolation, ) import afl @fork_isolation def test_deprecated_start(): msg = 'afl.start() is deprecated, use afl.init() instead' msg_re = '^{0}$'.format(re.escape(msg)) with assert_warns_regex(DeprecationWarning, msg_re): afl.start() @fork_isolation def test_double_init(): afl.init() with assert_raises_regex(RuntimeError, '^AFL already initialized$'): afl.init() # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/setup.py0000644000000000000000000000541612575575634015337 0ustar00rootroot00000000000000# encoding=UTF-8 # Copyright © 2014-2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. ''' *python-afl* is an experimental module that enables `American fuzzy lop`_ fork server and instrumentation for pure-Python code. .. _American fuzzy lop: http://lcamtuf.coredump.cx/afl/ ''' import distutils.core import distutils.version import Cython.Build try: cython_version = Cython.__version__ except AttributeError: cython_version = '0' cython_version = distutils.version.LooseVersion(cython_version) if cython_version < '0.19': raise RuntimeError('Cython >= 0.19 is required') def get_version(): try: f = open('doc/changelog', encoding='UTF-8') except TypeError: f = open('doc/changelog') with f: return f.readline().split()[1].strip('()') class lazylist(list): def __init__(self, obj): self._obj = obj def __len__(self): return len(self._obj) def __getitem__(self, n): return self._obj[n] def __iter__(self): return iter(self._obj) classifiers = ''' Development Status :: 2 - Pre-Alpha Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: POSIX Topic :: Software Development :: Quality Assurance Topic :: Software Development :: Testing '''.strip().splitlines() distutils.core.setup( name='python-afl', version=get_version(), license='MIT', description='American fuzzy lop fork server and instrumentation for pure-Python code', long_description=__doc__.strip(), classifiers=classifiers, url='http://jwilk.net/software/python-afl', author='Jakub Wilk', author_email='jwilk@jwilk.net', ext_modules=lazylist(Cython.Build.cythonize('afl.pyx')), scripts=['py-afl-fuzz'], ) # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/py-afl-fuzz0000755000000000000000000000061312575575634015731 0ustar00rootroot00000000000000#!/bin/sh export AFL_SKIP_CHECKS=1 # AFL << 1.20b export AFL_SKIP_BIN_CHECK=1 # AFL >= 1.20b export AFL_DUMB_FORKSRV=1 if [ -n "$PYTHON_AFL_DUMB" ] then printf '%s: $PYTHON_AFL_DUMB is deprecated; use -n instead\n' "$(basename "$0")" >&2 set -- -n "$@" fi export PYTHON_AFL_SIGNAL=${PYTHON_AFL_SIGNAL:-SIGUSR1} export PYTHON_AFL_PERSISTENT=1 exec afl-fuzz "$@" # vim:ts=4 sts=4 sw=4 et python-afl-0.5.1/PKG-INFO0000644000000000000000000000141612576753413014710 0ustar00rootroot00000000000000Metadata-Version: 1.1 Name: python-afl Version: 0.5.1 Summary: American fuzzy lop fork server and instrumentation for pure-Python code Home-page: http://jwilk.net/software/python-afl Author: Jakub Wilk Author-email: jwilk@jwilk.net License: MIT Description: *python-afl* is an experimental module that enables `American fuzzy lop`_ fork server and instrumentation for pure-Python code. .. _American fuzzy lop: http://lcamtuf.coredump.cx/afl/ Platform: UNKNOWN Classifier: Development Status :: 2 - Pre-Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing python-afl-0.5.1/private/0000755000000000000000000000000012576753413015263 5ustar00rootroot00000000000000python-afl-0.5.1/private/update-version0000755000000000000000000000026612571257011020146 0ustar00rootroot00000000000000#!/bin/sh version=${1:?"no version number provided"} set -e set -x dch -m -v "$version" -u low -c doc/changelog sed -i -r -e "s/^(__version__) = '[0-9.]+'$/\1 = '$version'/" afl.pyx python-afl-0.5.1/private/build-and-test0000755000000000000000000000365512571257234020031 0ustar00rootroot00000000000000#!/bin/sh # Copyright © 2015 Jakub Wilk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. usage() { printf 'Usage: %s [--no-build] [pythonX.Y...]\n' "$0" } args=$(getopt -n "$0" -o 'hj:' --long 'help,jobs:,no-build' -- "$@") if [ $? -ne 0 ] then usage >&2 exit 1 fi eval set -- "$args" opt_jobs=$(nproc) || opt_jobs=1 opt_build=y while true do case "$1" in -h|--help) usage; exit 0;; -j|--jobs) opt_jobs=$2; shift 2;; --no-build) opt_build=; shift;; --) shift; break;; *) printf '%s: internal error (%s)\n' "$0" "$1" >&2; exit 1;; esac done set -e [ $# = 0 ] && set -- python [ -z $opt_build ] || \ printf '%s\n' "$@" \ | xargs -P"$opt_jobs" -t -I{python} env {python} setup.py build --build-lib build/{python} cd tests printf '%s\n' "$@" \ | xargs -t -I{python} env PYTHONPATH="$PWD/../build/{python}" {python} $(which nosetests) --verbose # vim:ts=4 sts=4 sw=4 et