├── fusil ├── __init__.py ├── linux │ ├── __init__.py │ └── syslog.py ├── mas │ ├── __init__.py │ ├── agent_id.py │ ├── message.py │ ├── application_agent.py │ ├── agent_list.py │ ├── univers.py │ ├── mailbox.py │ └── mta.py ├── network │ ├── __init__.py │ ├── tools.py │ ├── tcp_server.py │ ├── tcp_client.py │ ├── unix_client.py │ ├── http_request.py │ ├── http_server.py │ ├── server.py │ └── server_client.py ├── process │ ├── __init__.py │ ├── cmdline.py │ ├── time_watch.py │ ├── stdout.py │ ├── mangle.py │ ├── cpu_probe.py │ ├── watch.py │ ├── prepare.py │ └── attach.py ├── python │ ├── jit │ │ └── __init__.py │ ├── h5py │ │ └── __init__.py │ ├── samples │ │ ├── __init__.py │ │ ├── mangle_loop.py │ │ ├── tricky_typing.py │ │ ├── mangle_obj.py │ │ └── tricky_objects.py │ ├── unicode.py │ ├── mangle_object.py │ ├── template_strings.py │ ├── tricky_weird.py │ └── utils.py ├── error.py ├── version.py ├── dummy_mangle.py ├── score.py ├── terminal_echo.py ├── xhost.py ├── unsafe.py ├── mangle_op.py ├── session_agent.py ├── project_agent.py ├── file_tools.py ├── time_watch.py ├── auto_mangle.py ├── fixpng.py ├── mockup.py ├── tools.py ├── system_calm.py ├── zzuf.py ├── project_directory.py ├── bytes_generator.py ├── mangle_agent.py ├── x11.py ├── bits.py ├── write_code.py ├── incr_mangle_op.py ├── directory.py ├── mangle.py └── session.py ├── tests ├── __init__.py ├── python │ ├── __init__.py │ ├── samples │ │ ├── __init__.py │ │ ├── test_tricky_typing.py │ │ ├── test_tricky_objects.py │ │ └── test_weird_classes.py │ ├── test_unicode.py │ └── test_values.py ├── cmd_help │ ├── ping.help │ ├── identify.help │ ├── python.help │ └── gcc.help ├── file_watch_ignore.rst ├── file_watch_read.rst └── cmd_help_parser.rst ├── setup.cfg ├── pyflakes.sh ├── lsall.sh ├── README.windows.txt ├── doc ├── Makefile ├── mas.rst ├── time.rst ├── safety.rst ├── network.rst ├── linux_process_limits.rst ├── events.rst ├── score.rst ├── architecture.rst ├── index.rst ├── configuration.rst ├── agent.rst ├── mangle.rst └── c_tools.rst ├── AUTHORS ├── MANIFEST.in ├── TODO ├── graph.sh ├── examples ├── hello-world ├── xterm └── good-bye-world ├── INSTALL ├── test_doc.py ├── IDEAS ├── fuzzers ├── fusil-python-threaded ├── notworking │ ├── fusil-libexif │ └── fusil-linux-proc ├── fusil-ogg123 ├── fusil-imagemagick ├── fusil-poppler ├── fusil-clamav └── fusil-gimp ├── setup.py ├── tools ├── fuzz_loop.sh └── state_tool.py ├── jit_config.py ├── README.rst └── .gitignore /fusil/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/mas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/network/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/process/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/python/jit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/python/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/python/h5py/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/python/samples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/python/samples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fusil/error.py: -------------------------------------------------------------------------------- 1 | class FusilError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_date = 0 4 | tag_svn_revision = 0 5 | 6 | -------------------------------------------------------------------------------- /pyflakes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pyflakes $(find fusil -name "*.py") examples/* fuzzers/fusil-* fuzzers/notworking/fusil-* 3 | -------------------------------------------------------------------------------- /fusil/version.py: -------------------------------------------------------------------------------- 1 | PACKAGE = "fusil" 2 | VERSION = "1.5" 3 | WEBSITE = "https://github.com/devdanzin/fusil" 4 | LICENSE = "GNU GPL v2" 5 | -------------------------------------------------------------------------------- /lsall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find fusil -name "*.py" 3 | ls -1 examples/* 4 | ls -1 fuzzers/fusil-* 5 | ls -1 fuzzers/notworking/fusil-* 6 | -------------------------------------------------------------------------------- /README.windows.txt: -------------------------------------------------------------------------------- 1 | Status of Windows support 2 | ========================= 3 | 4 | The current iteration of fusil is not compatible with Windows. 5 | -------------------------------------------------------------------------------- /fusil/dummy_mangle.py: -------------------------------------------------------------------------------- 1 | from fusil.mangle import MangleAgent 2 | 3 | 4 | class DummyMangle(MangleAgent): 5 | def mangleData(self, data, file_index): 6 | return data 7 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | DOCS=$(wildcard *.rst) 2 | HTML=$(patsubst %.rst,%.html,$(DOCS)) 3 | RST2HTML=rst2html 4 | 5 | all: $(HTML) 6 | @echo $(HTML) 7 | 8 | %.html: %.rst 9 | $(RST2HTML) $< $@ 10 | 11 | clean: 12 | rm -f $(HTML) 13 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Main developers 2 | =============== 3 | 4 | Victor Stinner aka haypo 5 | 6 | Contributor 7 | =========== 8 | 9 | Geoffroy Couprie aka geal 10 | 11 | Others 12 | ====== 13 | 14 | Daniel Diniz aka @devdanzin 15 | -------------------------------------------------------------------------------- /tests/cmd_help/ping.help: -------------------------------------------------------------------------------- 1 | ping: invalid option -- - 2 | Usage: ping [-LRUbdfnqrvVaA] [-c count] [-i interval] [-w deadline] 3 | [-p pattern] [-s packetsize] [-t ttl] [-I interface or address] 4 | [-M mtu discovery hint] [-S sndbuf] 5 | [ -T timestamp option ] [ -Q tos ] [hop1 ...] destination 6 | -------------------------------------------------------------------------------- /fusil/mas/agent_id.py: -------------------------------------------------------------------------------- 1 | class AgentID(object): 2 | instance = None 3 | counter = 0 4 | 5 | def __new__(cls): 6 | if cls.instance is None: 7 | obj = object.__new__(cls) 8 | cls.instance = obj 9 | return cls.instance 10 | 11 | def generate(self): 12 | self.counter += 1 13 | return self.counter 14 | -------------------------------------------------------------------------------- /fusil/process/cmdline.py: -------------------------------------------------------------------------------- 1 | from fusil.project_agent import ProjectAgent 2 | 3 | 4 | class CommandLine(ProjectAgent): 5 | def __init__(self, process, arguments): 6 | ProjectAgent.__init__(self, process.project(), "%s:cmdline" % process.name) 7 | self.arguments = arguments 8 | 9 | def create(self): 10 | return list(self.arguments) 11 | -------------------------------------------------------------------------------- /fusil/score.py: -------------------------------------------------------------------------------- 1 | from fusil.tools import minmax 2 | 3 | 4 | def scoreLogFunc(object, score): 5 | if score in (None, 0): 6 | return object.info 7 | elif 0.50 <= abs(score): 8 | return object.error 9 | else: 10 | return object.warning 11 | 12 | 13 | def normalizeScore(score): 14 | score = minmax(-1.0, score, 1.0) 15 | return round(score, 2) 16 | -------------------------------------------------------------------------------- /fusil/terminal_echo.py: -------------------------------------------------------------------------------- 1 | from ptrace.terminal import enableEchoMode 2 | 3 | from fusil.project_agent import ProjectAgent 4 | 5 | 6 | class TerminalEcho(ProjectAgent): 7 | def __init__(self, project): 8 | ProjectAgent.__init__(self, project, "terminal") 9 | 10 | def deinit(self): 11 | if enableEchoMode(): 12 | self.info("Terminal: restore echo mode to stdin") 13 | -------------------------------------------------------------------------------- /fusil/network/tools.py: -------------------------------------------------------------------------------- 1 | from socket import AF_INET 2 | 3 | 4 | def formatAddress(family, address, short=False): 5 | if family == AF_INET: 6 | host, port = address 7 | if not host: 8 | host = "(localhost)" 9 | if not short: 10 | return "(host %s, port %s)" % (host, port) 11 | else: 12 | return "%s:%s" % (host, port) 13 | else: 14 | return repr(address) 15 | -------------------------------------------------------------------------------- /fusil/xhost.py: -------------------------------------------------------------------------------- 1 | from fusil.process.tools import locateProgram 2 | 3 | 4 | def xhostCommand(xhost_program, user, allow=True): 5 | if not xhostCommand.program: 6 | xhostCommand.program = locateProgram(xhost_program, raise_error=True) 7 | if allow: 8 | prefix = "+" 9 | else: 10 | prefix = "-" 11 | return [xhostCommand.program, "%slocal:%s" % (prefix, user)] 12 | 13 | 14 | xhostCommand.program = None 15 | -------------------------------------------------------------------------------- /fusil/unsafe.py: -------------------------------------------------------------------------------- 1 | from os import getuid 2 | 3 | 4 | def permissionHelp(options): 5 | """ 6 | On "Operation not permitted error", propose some help to fix this problem. 7 | Example: "retry as root". 8 | """ 9 | help = [] 10 | if getuid() != 0: 11 | help.append("retry as root") 12 | if not options.unsafe: 13 | help.append("use --unsafe option") 14 | if not help: 15 | return None 16 | return " or ".join(help) 17 | -------------------------------------------------------------------------------- /doc/mas.rst: -------------------------------------------------------------------------------- 1 | Multi agent system (MAS) 2 | ======================== 3 | 4 | Univers agent is responsible to execute all agents. Univers is stopped using 5 | univers_stop() event. 6 | 7 | A session can be stopped using session_stop() event. 8 | 9 | Main MAS events: 10 | 11 | * project_start(): event received at first step but only for the first 12 | session of a project 13 | * session_start(): event received at first step on a session 14 | * session_done(score): event received at the last step of a session 15 | 16 | -------------------------------------------------------------------------------- /fusil/mas/message.py: -------------------------------------------------------------------------------- 1 | class Message: 2 | def __init__(self, event, arguments): 3 | self.event = event 4 | self.arguments = arguments 5 | 6 | def __repr__(self): 7 | return "" % (self.event, len(self.arguments)) 8 | 9 | def __call__(self, agent): 10 | try: 11 | function = "on_%s" % self.event 12 | function = getattr(agent, function) 13 | except AttributeError: 14 | return 15 | function(*self.arguments) 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include ChangeLog 3 | include COPYING 4 | include doc/*.rst 5 | include doc/Makefile 6 | include examples/good-bye-world* 7 | include examples/hello-world* 8 | include examples/xterm* 9 | include fuzzers/notworking/fusil-* 10 | include graph.sh 11 | include IDEAS 12 | include INSTALL 13 | include lsall.sh 14 | include pyflakes.sh 15 | include MANIFEST.in 16 | include README 17 | include README.windows.txt 18 | include test_doc.py 19 | include tests/*.rst 20 | include tests/cmd_help/*.help 21 | include TODO 22 | -------------------------------------------------------------------------------- /fusil/mas/application_agent.py: -------------------------------------------------------------------------------- 1 | from weakref import ref as weakref_ref 2 | 3 | from fusil.mas.agent import Agent 4 | 5 | 6 | class ApplicationAgent(Agent): 7 | def __init__(self, name, application, mta): 8 | Agent.__init__(self, name, mta) 9 | self.application = weakref_ref(application) 10 | if application is not self: 11 | self.register() 12 | 13 | def register(self): 14 | self.application().registerAgent(self) 15 | 16 | def unregister(self, destroy=True): 17 | self.application().unregisterAgent(self, destroy) 18 | -------------------------------------------------------------------------------- /fusil/network/tcp_server.py: -------------------------------------------------------------------------------- 1 | from socket import AF_INET 2 | 3 | from fusil.network.server import NetworkServer 4 | from fusil.network.tools import formatAddress 5 | 6 | 7 | class TcpServer(NetworkServer): 8 | def __init__(self, project, port, host=""): 9 | name = "tcp_server:" + formatAddress(AF_INET, (host, port), short=True) 10 | NetworkServer.__init__(self, project, name) 11 | self.host = host 12 | self.port = port 13 | 14 | def init(self): 15 | if self.socket: 16 | return 17 | self.bind(address=(self.host, self.port)) 18 | -------------------------------------------------------------------------------- /fusil/process/time_watch.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | from fusil.time_watch import TimeWatch 4 | 5 | 6 | class ProcessTimeWatch(TimeWatch): 7 | def init(self): 8 | TimeWatch.init(self) 9 | self.time0 = None 10 | 11 | def on_process_create(self, agent): 12 | self.time0 = time() 13 | 14 | def on_session_done(self, score): 15 | pass 16 | 17 | def on_process_exit(self, agent, status): 18 | duration = time() - self.time0 19 | self.warning("Process done: duration=%.1f ms" % (duration * 1000)) 20 | self.setScore(duration) 21 | -------------------------------------------------------------------------------- /fusil/network/tcp_client.py: -------------------------------------------------------------------------------- 1 | from socket import AF_INET, SOCK_STREAM 2 | 3 | from fusil.network.client import NetworkClient 4 | 5 | 6 | class TcpClient(NetworkClient): 7 | def __init__(self, project, host, port, connect_timeout=5.0): 8 | NetworkClient.__init__(self, project, "network:%s:%s" % (host, port)) 9 | self.host = host 10 | self.port = port 11 | self.connect_timeout = connect_timeout 12 | 13 | def on_session_start(self): 14 | self.connect( 15 | (self.host, self.port), AF_INET, SOCK_STREAM, timeout=self.connect_timeout 16 | ) 17 | -------------------------------------------------------------------------------- /fusil/network/unix_client.py: -------------------------------------------------------------------------------- 1 | from socket import AF_UNIX, SOCK_STREAM 2 | 3 | from fusil.network.client import NetworkClient 4 | 5 | 6 | class UnixSocketClient(NetworkClient): 7 | def __init__(self, project, socket_filename, connect_timeout=5.0): 8 | NetworkClient.__init__(self, project, "unix_socket:%s" % socket_filename) 9 | self.socket_filename = socket_filename 10 | self.connect_timeout = connect_timeout 11 | 12 | def on_session_start(self): 13 | self.connect( 14 | self.socket_filename, AF_UNIX, SOCK_STREAM, timeout=self.connect_timeout 15 | ) 16 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Fusil TODO list 2 | =============== 3 | 4 | * setup.py: run 2to3 on docstrings and rst files ("2to3 -w -d . doc/*.rst 5 | tests/*.rst"). See also python3.0.rst 6 | * replay.py is unable to open a file as stdin, required by fusil-gimp 7 | * Factorize code responsible to rename the session on process exit 8 | (share code between Debugger and CreateProcess) 9 | * Protect the terminal using setsid(), setpgrp(), or setpgid() 10 | * Use initgroups() in CreateProcess? 11 | * Remove the class WatchProcess: move code to CreateProcess to avoid duplicate 12 | events (agent score) and duplicate code 13 | 14 | -------------------------------------------------------------------------------- /fusil/python/unicode.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions and constants to prepare Unicode strings. 3 | """ 4 | 5 | ESCAPE_CHARACTERS = "'" + '"' + "\\" 6 | 7 | 8 | def formatCharacter(char): 9 | if char in ESCAPE_CHARACTERS: 10 | # >\"< 11 | return "\\" + char 12 | code = ord(char) 13 | if 32 <= code <= 126: 14 | # >a< 15 | return char 16 | elif code <= 255: 17 | # >\xEF< 18 | return "\\x%02X" % code 19 | elif code <= 65535: 20 | # >\u0101< 21 | return "\\u%04X" % code 22 | else: 23 | # >\U00010FA3< 24 | return "\\U%08X" % code 25 | 26 | 27 | def escapeUnicode(text): 28 | return "".join(formatCharacter(char) for char in text) 29 | -------------------------------------------------------------------------------- /fusil/python/samples/mangle_loop.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from sys import stderr 3 | 4 | if False: 5 | obj = None 6 | mangle_obj = None 7 | 8 | # The weird indentation is necessary because we write this in two levels 9 | if hasattr(obj.__class__, '__dict__'): 10 | for key, attr in obj.__class__.__dict__.items(): 11 | args = REPLACEMENT_PLACEHOLDER # noqa 12 | try: 13 | args = len(inspect.getfullargspec(attr).args) 14 | except Exception: 15 | pass 16 | if key.startswith('__'): 17 | continue 18 | try: 19 | mangle_obj(obj, key, (1,) * args) 20 | except Exception as err: 21 | print(f"{err.__class__}: {err}", file=stderr) 22 | -------------------------------------------------------------------------------- /fusil/process/stdout.py: -------------------------------------------------------------------------------- 1 | from weakref import ref as weakref_ref 2 | 3 | from fusil.file_watch import FileWatch 4 | 5 | 6 | class WatchStdout(FileWatch): 7 | def __init__(self, process): 8 | FileWatch.__init__(self, process.project(), None, "watch:stdout") 9 | self.process = weakref_ref(process) 10 | 11 | def on_process_stdout(self, agent, filename): 12 | if agent != self.process(): 13 | return 14 | input_file = open(filename, "rb") 15 | self.setFileObject(input_file) 16 | 17 | def on_process_exit(self, agent, status): 18 | if agent != self.process(): 19 | return 20 | self.live() 21 | self.close() 22 | 23 | def deinit(self): 24 | FileWatch.deinit(self) 25 | self.close() 26 | -------------------------------------------------------------------------------- /fusil/python/mangle_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object Mangling for Python Fuzzing 3 | 4 | This module provides functionality to "mangle" Python objects by temporarily replacing 5 | their attributes with mock objects while preserving specific methods for testing. 6 | It helps discover bugs by testing how functions behave when their object dependencies 7 | are corrupted or invalid, then safely restores the original state afterward. 8 | """ 9 | 10 | import pathlib 11 | 12 | parent_dir = pathlib.Path(__file__).parent 13 | mangle_obj_file = parent_dir / "samples/mangle_obj.py" 14 | mangle_obj = mangle_obj_file.read_text() 15 | 16 | mangle_loop_file = parent_dir / "samples/mangle_loop.py" 17 | mangle_loop = mangle_loop_file.read_text() 18 | mangle_loop = mangle_loop.replace("REPLACEMENT_PLACEHOLDER", "%s") 19 | -------------------------------------------------------------------------------- /fusil/python/template_strings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from ast import literal_eval 3 | from string.templatelib import Interpolation, Template 4 | 5 | from fusil.python.tricky_weird import weird_instance_names, weird_names 6 | from fusil.python.values import INTERESTING, SURROGATES 7 | 8 | sys.set_int_max_str_digits(4305) 9 | 10 | TEMPLATES = [] 11 | for value in SURROGATES: 12 | TEMPLATES.append(f"""Template({value}, Interpolation({value}, "value"))""") 13 | 14 | for value in INTERESTING: 15 | TEMPLATES.append(f"""Template("\\x00", Interpolation({value}, "value"))""") 16 | 17 | for name in weird_instance_names: 18 | TEMPLATES.append(f"""Template("\\x00", Interpolation(weird_instances['{name}'], "name"))""") 19 | 20 | for name in weird_names: 21 | TEMPLATES.append(f"""Template("\\x00", Interpolation(weird_classes['{name}'], "name"))""") 22 | -------------------------------------------------------------------------------- /fusil/mangle_op.py: -------------------------------------------------------------------------------- 1 | def generateSpecialValues(): 2 | values = ( 3 | # Special values in big endian 4 | # SPECIAL_VALUES will contain value in big endian and little endian 5 | b"\x00", 6 | b"\x00\x00", 7 | b"\x01", 8 | b"\x00\x01", 9 | b"\x7f", 10 | b"\x7f\xff", 11 | b"\x7f\xff\xff\xff", 12 | b"\x80", 13 | b"\x80\x00", 14 | b"\x80\x00\x00\x00", 15 | b"\xfe", 16 | b"\xfe\xff", 17 | b"\xfe\xff\xff\xff", 18 | b"\xff", 19 | b"\xff\xff", 20 | b"\xff\xff\xff\xff", 21 | ) 22 | result = [] 23 | for item in values: 24 | result.append(item) 25 | itemb = item[::-1] 26 | if item != itemb: 27 | result.append(itemb) 28 | return result 29 | 30 | 31 | SPECIAL_VALUES = generateSpecialValues() 32 | 33 | MAX_INCR = 8 34 | -------------------------------------------------------------------------------- /fusil/session_agent.py: -------------------------------------------------------------------------------- 1 | from weakref import ref as weakref_ref 2 | 3 | from fusil.project_agent import ProjectAgent 4 | 5 | 6 | class SessionAgent(ProjectAgent): 7 | def __init__(self, session, name, project=None): 8 | if project: 9 | mta = project.mta() 10 | else: 11 | mta = session.mta() 12 | project = session.project() 13 | self._session = weakref_ref(session) 14 | ProjectAgent.__init__(self, project, name, mta=mta) 15 | self.activate() 16 | 17 | def session(self): 18 | return self._session() 19 | 20 | def register(self): 21 | ProjectAgent.register(self) 22 | self.session().registerAgent(self) 23 | 24 | def unregister(self, destroy=True): 25 | ProjectAgent.unregister(self, destroy) 26 | session = self.session() 27 | if session: 28 | session.unregisterAgent(self, destroy) 29 | -------------------------------------------------------------------------------- /graph.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DATA=$1 3 | OUTPUT=/tmp/graph.png 4 | 5 | if [ "x$DATA" = "x" ]; then 6 | echo "usage: $0 aggressivity.dat" 7 | exit 1 8 | fi 9 | 10 | if [ ! -f "$DATA" ]; then 11 | echo "File $DATA doesn't exit" 12 | exit 1 13 | fi 14 | 15 | # Exit on error 16 | set -e 17 | 18 | cat <>> from fusil.mockup import Project, Logger 2 | >>> from fusil.file_watch import FileWatch 3 | >>> from os import unlink 4 | >>> from StringIO import StringIO 5 | >>> logger = Logger(show=True) 6 | >>> project = Project(logger) 7 | 8 | >>> buffer = StringIO() 9 | >>> def writeText(text): 10 | ... buffer.write(text) 11 | ... 12 | >>> watch = FileWatch(project, buffer, 'test') 13 | >>> watch.show_matching = True 14 | >>> watch.show_not_matching = True 15 | >>> watch.ignoreRegex("[Hh]ello") 16 | >>> watch.ignoreRegex("hello") 17 | >>> watch.addRegex('XDSDFOS', 1.0) 18 | >>> watch.activate() 19 | >>> watch.init() 20 | >>> writeText("HELLO\nhello\nHello\n") 21 | >>> watch.live() 22 | Not matching line: 'HELLO' 23 | >>> writeText("this is an error\n") 24 | >>> watch.live() 25 | Match pattern 'error' (score 30.0%) in 'this is an error' 26 | >>> writeText("test pattern XDSDFOS\n") 27 | >>> watch.live() 28 | Match pattern 'XDSDFOS' (score 100.0%) in 'test pattern XDSDFOS' 29 | 30 | -------------------------------------------------------------------------------- /doc/network.rst: -------------------------------------------------------------------------------- 1 | Network server 2 | ============== 3 | 4 | - from fusil.network.server import NetworkServer 5 | - from fusil.network.tcp_server import TcpServer 6 | - from fusil.network.http_server import HttpServer 7 | 8 | On new client connection, a ServerClient object is created. 9 | 10 | Network client 11 | ============== 12 | 13 | - from fusil.network.client import NetworkClient 14 | - from fusil.network.tcp_client import TcpClient 15 | - from fusil.network.unix_client import UnixClient 16 | 17 | TpcClient methods: 18 | 19 | * __init__(project, host, port, timeout=10.0): constructor 20 | * recvBytes(max_size=None, timeout=0.250, buffer_size=1024): Read max_size 21 | bytes by chunks of buffer_size bytes, stop after timeout seconds 22 | * sendBytes(bytes): send bytes on socket. Return False on error, True 23 | on success 24 | 25 | TpcClient attributes: 26 | 27 | * host: host name/IP address 28 | * port: port number 29 | * timeout: socket timeout (in second) 30 | * tx_bytes: number of bytes sent to host 31 | * socket: socket object (set to None on error) 32 | 33 | -------------------------------------------------------------------------------- /tests/file_watch_read.rst: -------------------------------------------------------------------------------- 1 | >>> filename = 'test.txt' 2 | >>> from fusil.mockup import Project 3 | >>> from fusil.file_watch import FileWatch 4 | >>> from os import unlink 5 | >>> output = open(filename, 'wb') 6 | >>> def writeText(text): 7 | ... output.write(text) 8 | ... output.flush() 9 | ... 10 | >>> input = open(filename, 'rb') 11 | >>> project = Project() 12 | >>> watch = FileWatch(project, input, 'test') 13 | >>> watch.read_size = 1 14 | >>> watch.init() 15 | >>> list(watch.readlines()) 16 | [] 17 | >>> writeText('da') 18 | >>> list(watch.readlines()) 19 | [] 20 | >>> writeText('t') 21 | >>> list(watch.readlines()) 22 | [] 23 | >>> writeText('a\n') 24 | >>> list(watch.readlines()) 25 | ['data'] 26 | >>> writeText('linea\nlineb\n') 27 | >>> list(watch.readlines()) 28 | ['linea', 'lineb'] 29 | >>> writeText('line1\nline2\nline') 30 | >>> list(watch.readlines()) 31 | ['line1', 'line2'] 32 | >>> writeText('3\n') 33 | >>> list(watch.readlines()) 34 | ['line3'] 35 | >>> unlink(filename) 36 | 37 | -------------------------------------------------------------------------------- /examples/hello-world: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # The most simple fuzzer for Fusil: just starts the command: 3 | # echo "Hello World" 4 | # 5 | # Since the process is not watched, Fusil will kills the process after the 6 | # timeout (10 seconds by default). 7 | 8 | # Reuse objets from Fusil library 9 | from fusil.application import Application 10 | from fusil.process.create import ProjectProcess 11 | 12 | # Any fuzzer have to create a class based on Application 13 | class Fuzzer(Application): 14 | # Fuzzer name: short alphanumeric string 15 | NAME = "hello" 16 | 17 | # setupProject() is the main fuzzer function: 18 | # it creates the fuzzer agents 19 | def setupProject(self): 20 | # Create an agent: don't store the object, it's already done 21 | # in the agent constructor 22 | ProjectProcess(self.project, ['echo', 'Hello World!']) 23 | 24 | if __name__ == "__main__": 25 | # Create the fuzzer application and call its method main() 26 | # Fusil will parse the command line, create all agents, and start the 27 | # fuzzing project 28 | Fuzzer().main() 29 | 30 | -------------------------------------------------------------------------------- /fusil/mas/agent_list.py: -------------------------------------------------------------------------------- 1 | from ptrace.error import PTRACE_ERRORS, writeError 2 | 3 | 4 | class AgentList: 5 | def __init__(self): 6 | self.agents = [] 7 | 8 | def append(self, agent): 9 | if agent in self: 10 | raise KeyError("Agent %r already registred") 11 | self.agents.append(agent) 12 | 13 | def _destroy(self, agent): 14 | try: 15 | agent.deactivate() 16 | except PTRACE_ERRORS as error: 17 | writeError(None, error, "Agent deinit error") 18 | agent.unregister(False) 19 | 20 | def remove(self, agent, destroy=True): 21 | if agent not in self: 22 | return 23 | self.agents.remove(agent) 24 | if destroy: 25 | self._destroy(agent) 26 | 27 | def clear(self): 28 | while self.agents: 29 | agent = self.agents.pop() 30 | self._destroy(agent) 31 | 32 | def __del__(self): 33 | self.clear() 34 | 35 | def __contains__(self, agent): 36 | return agent in self.agents 37 | 38 | def __iter__(self): 39 | return iter(self.agents) 40 | -------------------------------------------------------------------------------- /fusil/python/samples/tricky_typing.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import builtins 3 | import collections.abc 4 | import itertools 5 | import types 6 | import typing 7 | from functools import reduce 8 | from operator import or_ 9 | 10 | abc_types = [cls for cls in abc.__dict__.values() if isinstance(cls, type)] 11 | builtins_types = [cls for cls in builtins.__dict__.values() if isinstance(cls, type)] 12 | collections_abc_types = [cls for cls in collections.abc.__dict__.values() if isinstance(cls, type)] 13 | collections_types = [cls for cls in collections.__dict__.values() if isinstance(cls, type)] 14 | itertools_types = [cls for cls in itertools.__dict__.values() if isinstance(cls, type)] 15 | types_types = [cls for cls in types.__dict__.values() if isinstance(cls, type)] 16 | typing_types = [cls for cls in typing.__dict__.values() if isinstance(cls, type)] 17 | 18 | all_types = (abc_types + builtins_types + collections_abc_types + collections_types + itertools_types 19 | + types_types + typing_types) 20 | all_types = [t for t in all_types if not (isinstance(t, type) and issubclass(t, BaseException))] 21 | big_union = reduce(or_, all_types, int) 22 | -------------------------------------------------------------------------------- /fusil/mas/univers.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from fusil.mas.application_agent import ApplicationAgent 4 | 5 | 6 | class Univers(ApplicationAgent): 7 | def __init__(self, application, mta, step_sleep): 8 | ApplicationAgent.__init__(self, "univers", application, mta) 9 | self.on_stop = None 10 | self.is_done = False 11 | self.step_sleep = step_sleep 12 | 13 | def executeAgent(self, agent): 14 | if not agent.is_active: 15 | return 16 | agent.readMailbox() 17 | agent.live() 18 | 19 | def execute(self, project): 20 | age = 0 21 | self.is_done = False 22 | while True: 23 | age += 1 24 | # Execute one univers step 25 | for agent in project.agents: 26 | self.executeAgent(agent) 27 | 28 | # Application is done? stop 29 | if self.is_done: 30 | return 31 | 32 | # Be nice with CPU: sleep some milliseconds 33 | sleep(self.step_sleep) 34 | 35 | def on_univers_stop(self): 36 | if self.on_stop: 37 | self.on_stop() 38 | self.is_done = True 39 | -------------------------------------------------------------------------------- /doc/linux_process_limits.rst: -------------------------------------------------------------------------------- 1 | +++++++++ 2 | setrlimit 3 | +++++++++ 4 | 5 | Linux limits 6 | ============ 7 | 8 | * RLIMIT_AS: maximum size of the process’s virtual memory in bytes. 9 | * RLIMIT_CORE: Maximum size of core file. 10 | * RLIMIT_CPU: CPU time limit in seconds. 11 | * RLIMIT_FSIZE: Maximum size of files that the process may create. 12 | * RLIMIT_LOCKS: Combined number of flock() locks and fcntl() leases 13 | * RLIMIT_MEMLOCK: Maximum number of bytes of memory that may be locked into RAM. 14 | * RLIMIT_MSGQUEUE: Limit on the number of bytes that can be allocated for POSIX message queues 15 | * RLIMIT_NICE: Ceiling to which the process’s nice value can be raised 16 | * RLIMIT_NOFILE: Value one greater than the maximum file descriptor number that can be opened by this process. 17 | * RLIMIT_NPROC: The maximum number of processes that can be created 18 | * RLIMIT_RTPRIO: Ceiling on the real-time priority 19 | * RLIMIT_SIGPENDING: Limit on the number of signals that may be queued 20 | * RLIMIT_STACK: Maximum size of the process stack in bytes 21 | 22 | Not implemented in Linux 23 | ======================== 24 | 25 | * RLIMIT_DATA 26 | * RLIMIT_RSS 27 | 28 | -------------------------------------------------------------------------------- /fusil/mas/mailbox.py: -------------------------------------------------------------------------------- 1 | from weakref import ref as weakref_ref 2 | 3 | 4 | class Mailbox: 5 | def __init__(self, agent, mta): 6 | self.messages = [] 7 | self.agent = weakref_ref(agent) 8 | self.mta = weakref_ref(mta) 9 | self.events = agent.getEvents() 10 | for event in self.events: 11 | mta.registerMailingList(self, event) 12 | 13 | def unregister(self): 14 | mta = self.mta() 15 | if not mta: 16 | return 17 | for event in self.events: 18 | mta.unregisterMailingList(self, event) 19 | 20 | def clear(self): 21 | self.messages = [] 22 | 23 | def deliver(self, message): 24 | agent = self.agent() 25 | if not agent: 26 | self.unregister() 27 | return 28 | if not agent.is_active: 29 | return 30 | self.messages.append(message) 31 | 32 | def popMessages(self): 33 | messages = self.messages 34 | self.messages = [] 35 | return messages 36 | 37 | def __repr__(self): 38 | agent = self.agent() 39 | if agent: 40 | return "" % agent 41 | else: 42 | return "" 43 | -------------------------------------------------------------------------------- /fusil/process/mangle.py: -------------------------------------------------------------------------------- 1 | from fusil.file_tools import relativePath 2 | from fusil.process.create import CreateProcess 3 | 4 | 5 | class MangleProcess(CreateProcess): 6 | def __init__( 7 | self, project, arguments, mangle_pattern, use_relative_mangle=True, **kw 8 | ): 9 | CreateProcess.__init__(self, project, arguments, **kw) 10 | self.orig_cmdline = self.cmdline.arguments 11 | self.mangle_pattern = mangle_pattern 12 | self.use_relative_mangle = use_relative_mangle 13 | 14 | def mangleCmdline(self, filenames): 15 | file_index = 0 16 | args = list(self.orig_cmdline) 17 | directory = self.getWorkingDirectory() 18 | for index, arg in enumerate(args): 19 | if self.mangle_pattern not in arg: 20 | continue 21 | filename = filenames[file_index] 22 | if self.use_relative_mangle: 23 | filename = relativePath(filename, directory) 24 | args[index] = arg.replace(self.mangle_pattern, filename) 25 | file_index += 1 26 | self.cmdline.arguments = args 27 | 28 | def on_mangle_filenames(self, filenames): 29 | self.mangleCmdline(filenames) 30 | self.createProcess() 31 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Fusil dependencies 2 | ================== 3 | 4 | * Python 2.6+ 5 | http://python.org/ 6 | * python-ptrace 0.7+ 7 | http://python-ptrace.hachoir.org/ 8 | * GCC needed by compileC() function from fusil.c_tools: 9 | http://gcc.gnu.org/ 10 | 11 | Optional dependencies: 12 | 13 | * rst2html program, part of docutils Python project 14 | Debian package: python-docutils 15 | http://docutils.sourceforge.net/ 16 | * Xlib Python module, required by fusil.xlib module 17 | and used by fusil-firefox: 18 | http://python-xlib.sourceforge.net/ 19 | 20 | 21 | Projects dependencies 22 | ===================== 23 | 24 | Each project may require external program or special environment: 25 | 26 | * Linux operating system: *linux_ioctl* and *linux_syscall* projects are specific 27 | to Linux 28 | * Mplayer program: needed by *mplayer* project 29 | * MySQL server and MySQL command line client: needed by *mysql* project 30 | * etc. 31 | 32 | 33 | Installation 34 | ============ 35 | 36 | Fusil uses the user "fusil" and the group "fusil" to run child processes to 37 | avoid remove an arbitrary file or kill an arbitrary process. 38 | 39 | Type as root: 40 | 41 | ./setup.py install 42 | 43 | Or using sudo program: 44 | 45 | sudo python setup.py install 46 | 47 | -------------------------------------------------------------------------------- /examples/xterm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Demontration of xterm bug: off-by-one error in memory allocation used 4 | to parse PATH environement variable. 5 | 6 | Bug fixed in xterm version 236: 7 | 8 | http://bugs.freedesktop.org/show_bug.cgi?id=16790 9 | """ 10 | 11 | from fusil.application import Application 12 | from fusil.process.env import EnvVarLength 13 | from fusil.process.create import ProjectProcess 14 | from fusil.process.watch import WatchProcess 15 | from fusil.process.stdout import WatchStdout 16 | 17 | class Fuzzer(Application): 18 | NAME = "xterm" 19 | 20 | def setupProject(self): 21 | # Run "xterm ls" command with a timeout of one second 22 | process = ProjectProcess(self.project, ['xterm', 'ls'], timeout=1.0) 23 | 24 | # Project is using X11 (eg. setup environment variables) 25 | process.setupX11() 26 | 27 | # Ask Fusil to generate the environment variable "PATH" with a size in 0..1000 28 | process.env.add(EnvVarLength('PATH', max_length=1000)) 29 | 30 | # Watch the created process 31 | # Since timeouts are meaningless, just ignore them (use nul score) 32 | WatchProcess(process, timeout_score=0) 33 | WatchStdout(process) 34 | 35 | if __name__ == "__main__": 36 | Fuzzer().main() 37 | 38 | -------------------------------------------------------------------------------- /fusil/linux/syslog.py: -------------------------------------------------------------------------------- 1 | from os.path import basename, exists 2 | 3 | from fusil.file_watch import FileWatch 4 | from fusil.project_agent import ProjectAgent 5 | 6 | FILENAMES = ( 7 | # Linux: system logs 8 | "syslog", 9 | "messages", 10 | # Linux: kernel logs 11 | "dmesg", 12 | "kern.log", 13 | # Linux: authentication (PAM) logs 14 | "auth.log", 15 | # Linux: user logs 16 | "user.log", 17 | # FreeBSD: kernel logs 18 | "dmesg.today", 19 | # FreeBSD: user logs 20 | "userlog", 21 | ) 22 | 23 | 24 | class Syslog(ProjectAgent): 25 | def __init__(self, project): 26 | ProjectAgent.__init__(self, project, "syslog") 27 | self.logs = [] 28 | for filename in FILENAMES: 29 | agent = self.create(project, "/var/log/" + filename) 30 | if not agent: 31 | continue 32 | self.logs.append(agent) 33 | 34 | def create(self, project, filename): 35 | if exists(filename): 36 | return FileWatch( 37 | project, open(filename), "syslog:%s" % basename(filename), start="end" 38 | ) 39 | else: 40 | self.warning("Skip (non existent) log file: %s" % filename) 41 | return None 42 | 43 | def __iter__(self): 44 | return iter(self.logs) 45 | -------------------------------------------------------------------------------- /tests/python/test_unicode.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | 5 | # --- Test Setup: Path Configuration --- 6 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 7 | PROJECT_ROOT = os.path.join(SCRIPT_DIR, '..', '..') 8 | sys.path.insert(0, PROJECT_ROOT) 9 | 10 | try: 11 | from fusil.python.unicode import escapeUnicode 12 | UNICODE_AVAILABLE = True 13 | except ImportError as e: 14 | print(f"Could not import unicode module, skipping tests: {e}", file=sys.stderr) 15 | escapeUnicode = None 16 | UNICODE_AVAILABLE = False 17 | 18 | 19 | @unittest.skipIf(not UNICODE_AVAILABLE, "Could not import unicode module, skipping tests.") 20 | class TestUnicode(unittest.TestCase): 21 | """ 22 | Test suite for the unicode.py module. 23 | 24 | Verifies the escapeUnicode function. 25 | """ 26 | 27 | def test_escape_unicode_function(self): 28 | """ 29 | Tests the escapeUnicode helper function with various inputs. 30 | """ 31 | self.assertEqual(escapeUnicode("hello"), "hello") 32 | self.assertEqual(escapeUnicode('hello"world'), 'hello\\"world') 33 | self.assertEqual(escapeUnicode("\x07"), "\\x07") 34 | self.assertEqual(escapeUnicode("¢"), "\\xA2") 35 | self.assertEqual(escapeUnicode("😀"), "\\U0001F600") 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /fusil/project_agent.py: -------------------------------------------------------------------------------- 1 | from weakref import ref as weakref_ref 2 | 3 | from fusil.mas.agent import Agent 4 | from fusil.score import scoreLogFunc 5 | 6 | 7 | class ProjectAgent(Agent): 8 | def __init__(self, project, name, mta=None, application=None): 9 | if not mta: 10 | mta = project.mta() 11 | Agent.__init__(self, name, mta) 12 | self.project = weakref_ref(project) 13 | if not application: 14 | application = project.application() 15 | self.application = weakref_ref(application) 16 | if project is not self: 17 | self.score_weight = 1.0 18 | self.register() 19 | 20 | def session(self): 21 | project = self.project() 22 | if not project: 23 | return None 24 | return project.session 25 | 26 | def register(self): 27 | self.project().registerAgent(self) 28 | 29 | def unregister(self, destroy=True): 30 | project = self.project() 31 | if not project: 32 | return 33 | project.unregisterAgent(self, destroy) 34 | 35 | def scoreLogFunc(self): 36 | score = self.getScore() 37 | return scoreLogFunc(self, score) 38 | 39 | def getScore(self): 40 | # Score: floating number, -1.0 <= score <= 1.0 41 | # 1: bug found 42 | # 0: nothing special 43 | # -1: inputs rejected 44 | return None 45 | -------------------------------------------------------------------------------- /fusil/python/tricky_weird.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tricky and Weird Objects 3 | 4 | This module defines problematic Python objects, classes, and edge cases designed to 5 | trigger bugs. It contains boundary values like maximum integers, weird classes, 6 | circular references, and other pathological objects that can expose crashes and other 7 | undesirable behavior in Python code and C extensions. 8 | """ 9 | 10 | import pathlib 11 | from fusil.python.samples import weird_classes, tricky_typing, tricky_objects 12 | 13 | try: 14 | from fusil.python.samples import tricky_numpy 15 | except ImportError: 16 | print("Could not import tricky_numpy.") 17 | tricky_numpy = None 18 | 19 | weird_instance_names = list(weird_classes.weird_instances.keys()) 20 | weird_names = list(weird_classes.weird_classes.keys()) 21 | 22 | tricky_objects_dict = tricky_objects.__dict__ 23 | 24 | tricky_objects_names = [ 25 | key for key in tricky_objects_dict.keys() 26 | if isinstance(key, str) and not key.startswith('_') 27 | ] 28 | 29 | tricky_numpy_names = [ 30 | name for name in dir(tricky_numpy) 31 | if name.startswith('numpy_') 32 | ] if tricky_numpy else [] 33 | 34 | weird_classes = pathlib.Path(weird_classes.__file__).read_text() 35 | tricky_typing = pathlib.Path(tricky_typing.__file__).read_text() 36 | tricky_objects = pathlib.Path(tricky_objects.__file__).read_text() 37 | if tricky_numpy: 38 | tricky_numpy = pathlib.Path(tricky_numpy.__file__).read_text() 39 | 40 | type_names = ("list", "tuple", "dict") 41 | -------------------------------------------------------------------------------- /fusil/file_tools.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from errno import EEXIST 3 | from os import fstat, getcwd, getpid, mkdir 4 | from os.path import basename 5 | 6 | from ptrace.linux_proc import readProcessLink 7 | 8 | 9 | def safeMkdir(path): 10 | try: 11 | mkdir(path) 12 | except OSError as err: 13 | if err.errno == EEXIST: 14 | return 15 | else: 16 | raise 17 | 18 | 19 | def filenameExtension(filename): 20 | ext = basename(filename) 21 | if "." in ext: 22 | return "." + ext.rsplit(".", 1)[-1] 23 | else: 24 | return None 25 | 26 | 27 | def dumpFileInfo(logger, file_obj): 28 | try: 29 | fileno = file_obj.fileno() 30 | except AttributeError: 31 | logger.info("File object class: %s" % file_obj.__class__.__name__) 32 | return 33 | filename = readProcessLink(getpid(), "fd/%s" % fileno) 34 | logger.info("File name: %r" % filename) 35 | logger.info("File descriptor: %s" % fileno) 36 | 37 | stat = fstat(fileno) 38 | logger.info("File user/group: %s/%s" % (stat.st_uid, stat.st_gid)) 39 | logger.info("File size: %s bytes" % stat.st_size) 40 | logger.info("File mode: %04o" % stat.st_mode) 41 | mtime = datetime.fromtimestamp(stat.st_mtime) 42 | logger.info("File modification: %s" % mtime) 43 | 44 | 45 | def relativePath(path, cwd=None): 46 | if not cwd: 47 | cwd = getcwd() 48 | if path.startswith(cwd): 49 | path = path[len(cwd) + 1 :] 50 | return path 51 | -------------------------------------------------------------------------------- /doc/events.rst: -------------------------------------------------------------------------------- 1 | ++++++++++++ 2 | Fusil events 3 | ++++++++++++ 4 | 5 | An event can only be sent once in a session step (eg. you can not send session_stop 6 | event twice). 7 | 8 | Application 9 | =========== 10 | 11 | - application_done(): Fusil is done (exit) 12 | - application_interrupt(): Ask Fusil application to stop 13 | - application_error(message): Fatal Fusil error 14 | 15 | Project 16 | ======= 17 | 18 | - project_start(): Creation of the project 19 | - project_stop(): Ask to stop active project 20 | - project_session_destroy(): Destroy session and create a new session 21 | if we are not done 22 | 23 | Session 24 | ======= 25 | 26 | - session_start(): Creation of a new session 27 | - session_stop(): Ask session to stop 28 | - session_done(score): End of the active session, score is the 29 | final session score 30 | - session_success(): The session is a success, sent at the end of 31 | the session 32 | - session_rename('name'): Rename the session: all names are joined using '-' 33 | separator to rename the session directory 34 | 35 | Aggressivity 36 | ============ 37 | 38 | - aggressivity_value(value): New aggressivity value with -1.0 <= value <= 1.0 39 | 40 | Process 41 | ======= 42 | 43 | - process_create(agent): New process created 44 | - process_stdout(agent, filename): Filename of the process stdout 45 | - process_exit(agent, status): Process finished (exited or killed by a signal) 46 | - process_pid(agent, pid): Attached process identifier 47 | 48 | MangleFile 49 | ========== 50 | 51 | - mangle_filenames(filenames): Generated filenames 52 | 53 | -------------------------------------------------------------------------------- /test_doc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from doctest import testfile, ELLIPSIS, testmod 3 | from sys import exit, path as sys_path 4 | from os.path import dirname 5 | 6 | def testDoc(filename, name=None): 7 | print("--- %s: Run tests" % filename) 8 | failure, nb_test = testfile( 9 | filename, optionflags=ELLIPSIS, name=name) 10 | if failure: 11 | exit(1) 12 | print("--- %s: End of tests" % filename) 13 | 14 | def importModule(name): 15 | mod = __import__(name) 16 | components = name.split('.') 17 | for comp in components[1:]: 18 | mod = getattr(mod, comp) 19 | return mod 20 | 21 | def testModule(name): 22 | print("--- Test module %s" % name) 23 | module = importModule(name) 24 | failure, nb_test = testmod(module) 25 | if failure: 26 | exit(1) 27 | print("--- End of test") 28 | 29 | def main(): 30 | fusil_dir = dirname(__file__) 31 | sys_path.append(fusil_dir) 32 | 33 | 34 | # Test documentation in doc/*.rst files 35 | testDoc('doc/c_tools.rst') 36 | testDoc('doc/file_watch.rst') 37 | testDoc('doc/mangle.rst') 38 | testDoc('doc/process.rst') 39 | 40 | # Unit tests as reST 41 | testDoc('tests/file_watch_read.rst') 42 | testDoc('tests/file_watch_ignore.rst') 43 | testDoc('tests/cmd_help_parser.rst') 44 | 45 | # Test documentation of some functions/classes 46 | testModule("fusil.bits") 47 | testModule("fusil.tools") 48 | testModule("fusil.process.replay_python") 49 | testModule("fusil.process.tools") 50 | 51 | if __name__ == "__main__": 52 | main() 53 | 54 | -------------------------------------------------------------------------------- /examples/good-bye-world: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Improved version of the Fusil "Hello World!": run the echo command with 4 | # random arguments and watch the created process (status and standard output). 5 | 6 | from fusil.application import Application 7 | from fusil.process.create import CreateProcess 8 | from fusil.bytes_generator import BytesGenerator, ASCII0 9 | from fusil.process.watch import WatchProcess 10 | from fusil.process.stdout import WatchStdout 11 | from random import randint, choice 12 | 13 | class EchoProcess(CreateProcess): 14 | OPTIONS = ("-e", "-E", "-n") 15 | 16 | def __init__(self, project): 17 | CreateProcess.__init__(self, project, ["echo"]) 18 | self.datagen = BytesGenerator(1, 10, ASCII0) 19 | 20 | def createCmdline(self): 21 | arguments = ['echo'] 22 | for index in range(randint(3, 6)): 23 | if randint(1, 5) == 1: 24 | option = choice(self.OPTIONS) 25 | arguments.append(option) 26 | else: 27 | data = self.datagen.createValue() 28 | arguments.append(data) 29 | self.error("Command line=%s" % repr(arguments)) 30 | return arguments 31 | 32 | def on_session_start(self): 33 | self.cmdline.arguments = self.createCmdline() 34 | self.createProcess() 35 | 36 | class Fuzzer(Application): 37 | NAME = "goodbye" 38 | 39 | def setupProject(self): 40 | process = EchoProcess(self.project) 41 | WatchProcess(process) 42 | WatchStdout(process) 43 | 44 | if __name__ == "__main__": 45 | Fuzzer().main() 46 | 47 | -------------------------------------------------------------------------------- /fusil/time_watch.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | from fusil.project_agent import ProjectAgent 4 | 5 | 6 | class TimeWatch(ProjectAgent): 7 | def __init__( 8 | self, 9 | project, 10 | too_fast=None, 11 | too_slow=None, 12 | too_fast_score=-1.0, 13 | too_slow_score=1.0, 14 | ): 15 | if too_fast is not None and too_slow is not None: 16 | if too_fast > too_slow: 17 | raise ValueError("too_fast > too_slow") 18 | else: 19 | if too_fast is None and too_slow is None: 20 | raise ValueError("TimeWatch requires too_fast or too_slow parameters") 21 | 22 | ProjectAgent.__init__(self, project, "time watch") 23 | self.too_fast = too_fast 24 | self.too_slow = too_slow 25 | self.too_fast_score = too_fast_score 26 | self.too_slow_score = too_slow_score 27 | self.time0 = None 28 | self.duration = None 29 | 30 | def init(self): 31 | self.score = None 32 | self.duration = None 33 | self.time0 = time() 34 | 35 | def getScore(self): 36 | return self.score 37 | 38 | def setScore(self, duration): 39 | if self.too_fast is not None and duration < self.too_fast: 40 | self.score = self.too_fast_score 41 | if self.too_slow is not None and duration > self.too_slow: 42 | self.score = self.too_slow_score 43 | 44 | def on_session_done(self, score): 45 | duration = time() - self.time0 46 | self.warning("Session done: duration=%.1f ms" % (duration * 1000)) 47 | self.setScore(duration) 48 | -------------------------------------------------------------------------------- /fusil/network/http_request.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class HttpRequest: 5 | def __init__(self, data): 6 | self.method = None 7 | self.uri = None 8 | self.http_version = "1.0" 9 | self.host = None 10 | self.headers = [] 11 | self.parse(data) 12 | 13 | def parse(self, data): 14 | state = "init" 15 | for line in data.splitlines(): 16 | if state == "init": 17 | self.parseRequest(line) 18 | state = "host" 19 | continue 20 | if state == "host": 21 | match = re.match("host: (.*)$", line, re.IGNORECASE) 22 | if match: 23 | self.host = match.group(1) 24 | state = "keys" 25 | continue 26 | if not line: 27 | continue 28 | line = line.split(":", 1) 29 | if len(line) == 1: 30 | raise SyntaxError("Unable to parse client header: %r" % line[0]) 31 | key, value = line 32 | self.headers.append((key, value)) 33 | 34 | def parseRequest(self, line): 35 | # Extract method 36 | match = re.match("^(GET|POST) (.*)$", line) 37 | if not match: 38 | raise SyntaxError("Unable to parse request method: %r" % line) 39 | line = match.group(2) 40 | self.method = match.group(1) 41 | 42 | # Extract HTTP version if present 43 | match = re.match("^(.*) HTTP/(1.[01])$", line) 44 | if match: 45 | line = match.group(1) 46 | self.http_version = match.group(2) 47 | 48 | # Rest is the URI 49 | self.uri = line 50 | -------------------------------------------------------------------------------- /IDEAS: -------------------------------------------------------------------------------- 1 | Reuse existing libraries and projects 2 | ===================================== 3 | 4 | * http://www.nongnu.org/failmalloc/ 5 | 6 | 7 | Ideas to crash programs 8 | ======================= 9 | 10 | * Don't create stdin, stdout or stderr to check if first open file gets file descriptor #0 11 | * Continue to analyze gettext :-) 12 | * write C library to inject errors in libc calls (eg. malloc() failure) 13 | 14 | * Generate [http://michael-prokop.at/blog/2007/06/12/error-handling-enospc/ ENOSPC] errors? 15 | * file: open(), close(), read(), write() 16 | * directory: opendir(), chdir() 17 | * memory: malloc(), realloc(), calloc() 18 | * network: socket(), setsockopt() 19 | * time: time(), gettimeofday() 20 | http://software.inl.fr/trac/trac.cgi/wiki/Macfly 21 | 22 | * network socket proxy fuzzer 23 | 24 | Signals 25 | ------- 26 | 27 | Send signals like SIGINT, SIGTERM, SIGSTOP, SIGUSR1, SIGUSR2. 28 | 29 | Old bugs: 30 | * broken pipe (SIGPIPE) 31 | https://bugzilla.mindrot.org/show_bug.cgi?id=85 32 | * libc deadlock 33 | http://sourceware.org/bugzilla/show_bug.cgi?id=838 34 | * openssh pre-authentification denial of service 35 | https://bugzilla.mindrot.org/show_bug.cgi?id=1129 36 | 37 | 38 | Score 39 | ===== 40 | 41 | * Code coverage: 42 | 43 | * gcov: http://gcc.gnu.org/onlinedocs/gcc/Gcov.html 44 | * Valgrind: http://www.valgrind.org/ Valgrind 45 | * DynamoRio: http://www.cag.lcs.mit.edu/dynamorio/ 46 | 47 | * Check invalid use of memory using Valgrind (or any memory checker tool) 48 | * increment score if one of these function is called: 49 | 50 | - fgets(), memcpy(), strcpy() 51 | - input comes from user (?) 52 | - bytes read by the program 53 | - memory usage 54 | 55 | -------------------------------------------------------------------------------- /fusil/auto_mangle.py: -------------------------------------------------------------------------------- 1 | from fusil.mangle import MangleFile 2 | from fusil.tools import minmax 3 | 4 | 5 | class AutoMangle(MangleFile): 6 | def __init__(self, project, *args, **kw): 7 | MangleFile.__init__(self, project, *args, **kw) 8 | self.hard_max_op = 10000 9 | self.hard_min_op = 0 10 | self.aggressivity = None 11 | self.fixed_size_factor = 1.0 12 | 13 | def on_session_start(self): 14 | pass 15 | 16 | def on_aggressivity_value(self, value): 17 | self.aggressivity = value 18 | self.mangle() 19 | 20 | def setupConf(self, data): 21 | operations = ["bit"] 22 | size_factor = 0.30 23 | 24 | if 0.25 <= self.aggressivity: 25 | operations.append("increment") 26 | if 0.30 <= self.aggressivity: 27 | operations.extend(("replace", "special_value")) 28 | if 0.50 <= self.aggressivity: 29 | operations.extend(("insert_bytes", "delete_bytes")) 30 | size_factor = 0.20 31 | self.config.operations = operations 32 | 33 | # Display config 34 | count = len(data) * size_factor * self.fixed_size_factor 35 | count = minmax(self.hard_min_op, count, self.hard_max_op) 36 | count = int(count * self.aggressivity) 37 | self.config.max_op = max(count, self.hard_min_op) 38 | self.config.min_op = max(int(self.config.max_op * 0.80), self.hard_min_op) 39 | self.warning( 40 | "operation#:%s..%s operations=%s" 41 | % (self.config.min_op, self.config.max_op, self.config.operations) 42 | ) 43 | 44 | def mangleData(self, data, file_index): 45 | self.setupConf(data) 46 | return MangleFile.mangleData(self, data, file_index) 47 | -------------------------------------------------------------------------------- /doc/score.rst: -------------------------------------------------------------------------------- 1 | Scoring system 2 | ============== 3 | 4 | Problematic 5 | ----------- 6 | 7 | Guess a fuzzing session success or failure is a complex task. We can use 8 | different parameters like process exit code, stdout, session duration, etc. 9 | But for each project, the meaning of the values may change. For some projects, 10 | session timeout is a success whereas you may ignore timeout for other 11 | projects. 12 | 13 | Fusil probes 14 | ------------ 15 | 16 | That's why Fusil use a scoring system. You can use multiple "probes" and each 17 | probe compute its own score. Session score is the sum of all scores. A probe 18 | score is a value between -1.0 and 1.0 where: 19 | 20 | * 1.0 is a success (eg. program crash) 21 | * 0.0 means "nothing special" 22 | * -1.0 means that the application just rejects your input, you may 23 | try next session with less noise 24 | 25 | Each probe score is normalized in -1.0..1.0 interval. Session score is not 26 | normalized, 130% value is allowed. 27 | 28 | You can also set a probe "weight" ('score_weight' attribute, default value: 29 | 1.0) to change its importance in session score (see example above). 30 | 31 | Example 32 | ------- 33 | 34 | Let's take a project with 4 probes: 35 | * WatchProcess(A) 36 | * WatchProcess(B) 37 | * TimeWatch: weight=0.5 (less important) 38 | * FileWatch: weight=2 (more important) 39 | 40 | At the end of the session, the scores are: 41 | * WatchProcess(A): score=0.25 42 | * WatchProcess(B): score=None (no score) 43 | * TimeWatch: score=-0.10 44 | * FileWatch: score=0.15 45 | 46 | Session score is:: 47 | 48 | 0.25 + -0.10 * 0.5 + 0.15 * 2 = 0.50 49 | 50 | Since minimum score for a success is 'project.success_score' (default: 50%), 51 | we can say that the session is a success! 52 | 53 | -------------------------------------------------------------------------------- /fusil/fixpng.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions to recompute (fix) CRC32 checksums of an PNG picture. 3 | """ 4 | 5 | from array import array 6 | 7 | try: 8 | from io import StringIO 9 | except ImportError: 10 | # Python 2 11 | from StringIO import StringIO 12 | 13 | from logging import info 14 | from zlib import crc32 15 | 16 | from fusil.bits import BIG_ENDIAN, bytes2uint, uint2bytes 17 | 18 | 19 | def pngCRC32(data): 20 | """ 21 | Compute the CRC32 of specified data (str) as an unsigned integer. 22 | """ 23 | return crc32(data) & 0xFFFFFFFF 24 | 25 | 26 | def fixPNG(data): 27 | """ 28 | Fix a mangled PNG picture: 29 | - Rewrite PNG header (first 8 bytes) 30 | - Recompute CRC32 of each PNG chunk 31 | 32 | Stop if a chunk length is invalid. 33 | """ 34 | # array -> str 35 | data = data.tostring() 36 | 37 | origdata = data 38 | datalen = len(data) 39 | data = StringIO(data) 40 | 41 | data.seek(0) 42 | data.write("\x89PNG\r\n\x1a\n") 43 | 44 | index = 8 45 | while index < (datalen - 4): 46 | data.seek(index) 47 | size = bytes2uint(data.read(4), BIG_ENDIAN) 48 | chunksize = size + 12 49 | if datalen < (index + chunksize): 50 | info("fixPNG: Skip invalid chunk at %s" % index) 51 | break 52 | 53 | data.seek(index + 4) 54 | crcdata = data.read(chunksize - 8) 55 | newcrc = uint2bytes(pngCRC32(crcdata), BIG_ENDIAN, 4) 56 | 57 | data.seek(index + chunksize - 4) 58 | data.write(newcrc) 59 | 60 | index += chunksize 61 | 62 | data.seek(0, 0) 63 | data = data.read() 64 | assert len(data) == len(origdata) 65 | 66 | # str -> array 67 | data = array("B", data) 68 | return data 69 | -------------------------------------------------------------------------------- /doc/architecture.rst: -------------------------------------------------------------------------------- 1 | ++++++++++++++++++ 2 | Fusil architecture 3 | ++++++++++++++++++ 4 | 5 | Architecture 6 | ============ 7 | 8 | Fusil is a multi-agent system (MAS): it uses simple objets called "agents" 9 | exchanging messages though asynchronus "message (mail) transfer agent" (MTA). 10 | This architecture allows the whole project to be very modular and very 11 | customisable. 12 | 13 | Each agent have a live() method called at each session "step", but also event 14 | handler. An event has a name and may contains arguments. The name is used in 15 | agent method name: eg. "on_session_start()" method is called when the session 16 | starts. 17 | 18 | Some agents do change the environment and some other watchs for errors and 19 | strange behaviour of programs. 20 | 21 | 22 | Action agents 23 | ============= 24 | 25 | * CreateProcess: create a process 26 | * StdoutFile: created by CreateProcess to store 27 | process output 28 | * MangleFile: generate an invalid file using valid file 29 | * AutoMangle: MangleFile with autoconfiguration based on aggressivity factor 30 | 31 | Network: 32 | 33 | * NetworkClient / NetworkServer: network client / server 34 | * TcpClient: TCP network client 35 | * UnixSocketClient: UNIX socket client 36 | * HttpServer: HTTP server 37 | 38 | Probes 39 | ====== 40 | 41 | * FileWatch: watch a text file, search specific text patterns 42 | like "segmentation fault" 43 | * CpuProbe: watch CPU used by the process 44 | created by CreateProcess 45 | * ProcessTimeWatch: watch process 46 | execution duration 47 | * WatchStdout: watch process output (stdout) 48 | * WatchProcess: watch process created by CreateProcess 49 | * AttachProcess: watch running process 50 | * Syslog: watch /var/log/messages and /var/log/syslog files 51 | 52 | -------------------------------------------------------------------------------- /fuzzers/fusil-python-threaded: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Fusil Python Fuzzer 4 | 5 | A Python fuzzer based on the fusil fuzzing framework for testing Python modules 6 | by generating random function and method calls with diverse argument types. 7 | This fuzzer targets Python modules to discover crashes and other issues through 8 | exploration of API surfaces. 9 | 10 | Key Features: 11 | - Discovery and filtering of Python modules (stdlib, site-packages, C-only) 12 | - Generation of complex arguments, including edge cases and malformed data 13 | - Thread-based and async execution, mostly targeting free-threaded builds 14 | - Support for numpy arrays and template strings (PEP 750) when available 15 | - Blacklisting system to filter out dangerous or irrelevant functions and modules 16 | - Resource monitoring and timeout handling 17 | - Configurable fuzzing parameters (function calls, methods, classes, objects) 18 | 19 | The fuzzer generates Python source code that imports target modules and executes 20 | randomized function calls, capturing crashes and unexpected behaviors for analysis. 21 | It's particularly effective at finding issues in C extension modules where memory 22 | safety bugs are more common. Stressing and exercising the core interpreter can 23 | also find crashes. 24 | 25 | Usage: 26 | python fusil-python-threaded [options] 27 | 28 | Example: 29 | python fusil-python-threaded --only-c --timeout 300 --modules json,sqlite3 30 | """ 31 | 32 | from __future__ import annotations 33 | 34 | import warnings 35 | 36 | # Hide Python deprecation warnings coming from ptrace 37 | with warnings.catch_warnings(action="ignore"): 38 | from fusil.python import Fuzzer 39 | from fusil.python.utils import remove_logging_pycache 40 | 41 | if __name__ == "__main__": 42 | remove_logging_pycache() 43 | Fuzzer().main() 44 | -------------------------------------------------------------------------------- /fusil/mockup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes mockup used for unit tests. 3 | """ 4 | 5 | from weakref import ref 6 | 7 | 8 | class Logger: 9 | def __init__(self, show=False): 10 | self.show = show 11 | 12 | def debug(self, message, sender=None): 13 | self.display(message) 14 | 15 | def info(self, message, sender=None): 16 | self.display(message) 17 | 18 | def warning(self, message, sender=None): 19 | self.display(message) 20 | 21 | def error(self, message, sender=None): 22 | self.display(message) 23 | 24 | def display(self, message): 25 | if not self.show: 26 | return 27 | print(message) 28 | 29 | 30 | class MTA: 31 | def __init__(self, logger=None): 32 | if not logger: 33 | logger = Logger() 34 | self.logger = logger 35 | 36 | def registerMailingList(self, mailbox, event): 37 | pass 38 | 39 | def deliver(self, message): 40 | pass 41 | 42 | 43 | class Options: 44 | def __init__(self): 45 | self.debug = False 46 | 47 | 48 | class Application: 49 | def __init__(self): 50 | self.options = Options() 51 | 52 | def initX11(self): 53 | pass 54 | 55 | 56 | class Config: 57 | def __getattr__(self, name): 58 | return None 59 | 60 | 61 | class Debugger: 62 | def tracePID(self, agent, pid): 63 | pass 64 | 65 | 66 | class Project: 67 | def __init__(self, logger=None): 68 | self._mta = MTA(logger) 69 | self.mta = ref(self._mta) 70 | self._application = Application() 71 | self.application = ref(self._application) 72 | self.debugger = Debugger() 73 | self.config = Config() 74 | 75 | def registerAgent(self, agent): 76 | pass 77 | 78 | def unregisterAgent(self, agent): 79 | pass 80 | -------------------------------------------------------------------------------- /fusil/tools.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def minmax(min_value, value, max_value): 5 | """ 6 | Restrict value to [min_value; max_value] 7 | 8 | >>> minmax(-2, -3, 10) 9 | -2 10 | >>> minmax(-2, 27, 10) 11 | 10 12 | >>> minmax(-2, 0, 10) 13 | 0 14 | """ 15 | return min(max(min_value, value), max_value) 16 | 17 | 18 | def listDiff(old, new): 19 | """ 20 | Difference of two lists item by item. 21 | 22 | >>> listDiff([4, 0, 3], [10, 0, 50]) 23 | [6, 0, 47] 24 | """ 25 | return [item[1] - item[0] for item in zip(old, new)] 26 | 27 | 28 | def timedeltaSeconds(delta): 29 | """ 30 | Convert a datetime.timedelta() objet to a number of second 31 | (floatting point number). 32 | 33 | >>> from datetime import timedelta 34 | >>> timedeltaSeconds(timedelta(seconds=2, microseconds=40000)) 35 | 2.04 36 | >>> timedeltaSeconds(timedelta(minutes=1, milliseconds=250)) 37 | 60.25 38 | """ 39 | return delta.microseconds / 1000000.0 + delta.seconds + delta.days * 3600 * 24 40 | 41 | 42 | def makeUnicode(text): 43 | if isinstance(text, str): 44 | return text 45 | try: 46 | return str(text, "utf8") 47 | except UnicodeError: 48 | pass 49 | return str(text, "ISO-8859-1") 50 | 51 | 52 | def makeFilename(text): 53 | """ 54 | >>> makeFilename('Fatal error!') 55 | 'fatal_error' 56 | """ 57 | if isinstance(text, str): 58 | text = text.lower() 59 | text = re.sub("[^a-z_-]", "_", text) 60 | text = re.sub("_{2,}", "_", text) 61 | text = re.sub("_$", "", text) 62 | else: 63 | # byte string 64 | text = text.lower() 65 | text = re.sub(b"[^a-z_-]", b"_", text) 66 | text = re.sub(b"_{2,}", b"_", text) 67 | text = re.sub(b"_$", b"", text) 68 | return text 69 | -------------------------------------------------------------------------------- /fusil/python/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions for the fusil Python fuzzer.""" 2 | 3 | from __future__ import annotations 4 | 5 | import datetime 6 | import importlib 7 | import logging 8 | import pathlib 9 | import resource 10 | import sys 11 | import time 12 | 13 | from fusil.python.blacklists import MODULE_BLACKLIST 14 | 15 | 16 | def import_all() -> None: 17 | """Import all standard library C modules before running the fuzzer.""" 18 | # Currently we have to import all C modules before running the fuzzer. 19 | # TODO: figure out why and fix it properly. 20 | for name in sys.stdlib_module_names: 21 | if name not in MODULE_BLACKLIST and "test" not in name: 22 | try: 23 | sys.modules[name] = __import__(name) 24 | except ImportError as e: 25 | print("Failed to import module %s\n" % name, e) 26 | 27 | 28 | def remove_logging_pycache() -> None: 29 | """Remove stale logging __pycache__ that causes logging errors.""" 30 | 31 | pycache = pathlib.Path(logging.__file__).parent / "__pycache__" 32 | for entry in pycache.iterdir(): 33 | try: 34 | entry.unlink() 35 | except Exception as e: 36 | print(f"Error deleting file {entry.name}: {e}") 37 | try: 38 | pycache.rmdir() 39 | except Exception as e: 40 | print(f"Error removing directory {pycache.name}: {e}") 41 | importlib.reload(logging) 42 | 43 | 44 | def print_running_time(time_start: float) -> str: 45 | """Calculate and return a string with total and user running times.""" 46 | raw_utime = resource.getrusage(resource.RUSAGE_SELF).ru_utime 47 | user_time = str(datetime.timedelta(0, round(raw_utime, 2))) 48 | total_time = str(datetime.timedelta(0, round(time.time() - time_start, 2))) 49 | return f"\nRunning time: {total_time[:-4]}\nUser time: {user_time[:-4]}" 50 | -------------------------------------------------------------------------------- /fusil/process/cpu_probe.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | from ptrace.linux_proc import ProcError 4 | 5 | from fusil.linux.cpu_load import ProcessCpuLoad 6 | from fusil.project_agent import ProjectAgent 7 | 8 | 9 | class CpuProbe(ProjectAgent): 10 | def __init__(self, project, name, max_load=0.75, max_duration=10.0, max_score=1.0): 11 | ProjectAgent.__init__(self, project, name) 12 | self.max_load = max_load 13 | self.max_duration = max_duration 14 | self.max_score = max_score 15 | 16 | def init(self): 17 | self.score = None 18 | self.timeout = None 19 | self.load = None 20 | 21 | def setPid(self, pid): 22 | self.load = ProcessCpuLoad(pid) 23 | 24 | def live(self): 25 | # Read CPU load 26 | if not self.load: 27 | return 28 | try: 29 | load = self.load.get() 30 | if not load: 31 | return 32 | except ProcError: 33 | self.load = None 34 | return 35 | 36 | # Check maximum load 37 | if load < self.max_load: 38 | self.timeout = None 39 | return 40 | if self.timeout is None: 41 | self.warning("CPU load: %.1f%%" % (load * 100)) 42 | self.timeout = time() 43 | return 44 | 45 | # Check maximum duration 46 | duration = time() - self.timeout 47 | if duration < self.max_duration: 48 | return 49 | 50 | # Success 51 | self.score = self.max_score 52 | self.error( 53 | "CPU load (%.1f%%) bigger than maximum (%.1f%%) during %.1f sec: score=%.1f%%" 54 | % (load * 100, self.max_load * 100, duration, self.score * 100) 55 | ) 56 | self.send("session_rename", "cpu_load") 57 | self.load = None 58 | 59 | def getScore(self): 60 | return self.score 61 | -------------------------------------------------------------------------------- /tests/cmd_help/identify.help: -------------------------------------------------------------------------------- 1 | Version: ImageMagick 6.2.4 10/02/07 Q16 http://www.imagemagick.org 2 | Copyright: Copyright (C) 1999-2005 ImageMagick Studio LLC 3 | 4 | Usage: identify [options ...] file [ [options ...] file ... ] 5 | 6 | Where options include: 7 | -authenticate value decrypt image with this password 8 | -channel type apply option to select image channels 9 | -crop geometry cut out a rectangular region of the image 10 | -debug events display copious debugging information 11 | -define format:option 12 | define one or more image format options 13 | -density geometry horizontal and vertical density of the image 14 | -depth value image depth 15 | -extract geometry extract area from image 16 | -format "string" output formatted image characteristics 17 | -fuzz distance colors within this distance are considered equal 18 | -help print program options 19 | -interlace type type of image interlacing scheme 20 | -limit type value pixel cache resource limit 21 | -list type Color, Configure, Delegate, Format, Magic, Module, 22 | Resource, or Type 23 | -log format format of debugging information 24 | -matte store matte channel if the image has one 25 | -monitor monitor progress 26 | -ping efficiently determine image attributes 27 | -quiet suppress all error or warning messages 28 | -sampling-factor geometry 29 | horizontal and vertical sampling factor 30 | -set attribute value set an image attribute 31 | -size geometry width and height of image 32 | -strip strip image of all profiles and comments 33 | -units type the units of image resolution 34 | -verbose print detailed information about the image 35 | -version print version information 36 | -virtual-pixel method 37 | virtual pixel access method 38 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | +++++++++++++++++++ 2 | Documentation index 3 | +++++++++++++++++++ 4 | 5 | User documentation 6 | ================== 7 | 8 | Start with `Fusil usage guide`_: quick guide to learn how to execute a fuzzer. 9 | 10 | * configuration_: Fusil configuration file 11 | * safety_: Protection used in Fusil to avoid denial of service of your computer 12 | 13 | .. _`Fusil usage guide`: usage.html 14 | .. _configuration: configuration.html 15 | .. _safety: safety.html 16 | 17 | Fuzzer developer documentation 18 | ============================== 19 | 20 | Start with the `HOWTO: Write a fuzzer using Fusil`_ document: quick introduction to write 21 | your own fuzzer. 22 | 23 | Technical documents: 24 | 25 | * architecture_: List of the most common action and probe agents 26 | * c_tools_: Tools for C source code manipulation 27 | * file_watch_: Probe reading a text to search text patterns (eg. stdout) 28 | * mangle_: Inject errors in a valid file 29 | * process_: Create your process (create the command line, set environment 30 | variables, setup X11) and watch its activity 31 | * score_: Probe agent score and probe weight 32 | * time_: Session timeout and process execution time 33 | * network_: Network client and server agents 34 | 35 | .. _`HOWTO: Write a fuzzer using Fusil`: howto_write_fuzzer.html 36 | .. _architecture: architecture.html 37 | .. _c_tools: c_tools.html 38 | .. _file_watch: file_watch.html 39 | .. _mangle: mangle.html 40 | .. _process: process.html 41 | .. _score: score.html 42 | .. _time: time.html 43 | .. _network: network.html 44 | 45 | Multi agent system 46 | ================== 47 | 48 | * agent_: Agent API, Fusil is a multi-agent system 49 | * events_: List of the Fusil agent events 50 | * mas_: Description of the multi agent system 51 | 52 | .. _agent: agent.html 53 | .. _events: events.html 54 | .. _mas: mas.html 55 | 56 | Misc documents 57 | ============== 58 | 59 | * linux_process_limits_: Process limits supported by Linux kernel 60 | 61 | .. _linux_process_limits: linux_process_limits.html 62 | 63 | -------------------------------------------------------------------------------- /tests/cmd_help/python.help: -------------------------------------------------------------------------------- 1 | usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ... 2 | Options and arguments (and corresponding environment variables): 3 | -c cmd : program passed in as string (terminates option list) 4 | -d : debug output from parser (also PYTHONDEBUG=x) 5 | -E : ignore environment variables (such as PYTHONPATH) 6 | -h : print this help message and exit (also --help) 7 | -i : inspect interactively after running script, (also PYTHONINSPECT=x) 8 | and force prompts, even if stdin does not appear to be a terminal 9 | -m mod : run library module as a script (terminates option list) 10 | -O : optimize generated bytecode (a tad; also PYTHONOPTIMIZE=x) 11 | -OO : remove doc-strings in addition to the -O optimizations 12 | -Q arg : division options: -Qold (default), -Qwarn, -Qwarnall, -Qnew 13 | -S : don't imply 'import site' on initialization 14 | -t : issue warnings about inconsistent tab usage (-tt: issue errors) 15 | -u : unbuffered binary stdout and stderr (also PYTHONUNBUFFERED=x) 16 | see man page for details on internal buffering relating to '-u' 17 | -v : verbose (trace import statements) (also PYTHONVERBOSE=x) 18 | -V : print the Python version number and exit (also --version) 19 | -W arg : warning control (arg is action:message:category:module:lineno) 20 | -x : skip first line of source, allowing use of non-Unix forms of #!cmd 21 | file : program read from script file 22 | - : program read from stdin (default; interactive mode if a tty) 23 | arg ...: arguments passed to program in sys.argv[1:] 24 | Other environment variables: 25 | PYTHONSTARTUP: file executed on interactive startup (no default) 26 | PYTHONPATH : ':'-separated list of directories prefixed to the 27 | default module search path. The result is sys.path. 28 | PYTHONHOME : alternate directory (or :). 29 | The default module search path uses /pythonX.X. 30 | PYTHONCASEOK : ignore case in 'import' statements (Windows). 31 | -------------------------------------------------------------------------------- /fusil/system_calm.py: -------------------------------------------------------------------------------- 1 | from time import sleep, time 2 | 3 | from fusil.error import FusilError 4 | from fusil.linux.cpu_load import SystemCpuLoad 5 | 6 | 7 | class SystemCalm: 8 | def __init__(self, max_load, sleep_second): 9 | self.load = SystemCpuLoad() 10 | self.max_load = max_load 11 | self.sleep = sleep_second 12 | self.first_message = 3.0 13 | self.repeat_message = 5.0 14 | self.max_wait = 60 * 5 # seconds (5 minutes) 15 | 16 | def wait(self, agent): 17 | first_message = False 18 | start = time() 19 | next_message = time() + self.first_message 20 | while True: 21 | load = self.load.get(estimate=False) 22 | if load <= self.max_load: 23 | break 24 | duration = time() - start 25 | if next_message < time(): 26 | first_message = True 27 | next_message = time() + self.repeat_message 28 | agent.error( 29 | "Wait until system load is under %.1f%% since %.1f seconds (current: %.1f%%)..." 30 | % (self.max_load * 100, duration, load * 100) 31 | ) 32 | elif not first_message: 33 | first_message = True 34 | agent.info( 35 | "Wait until system load is under %.1f%% (current: %.1f%%)..." 36 | % (self.max_load * 100, load * 100) 37 | ) 38 | if self.max_wait <= duration: 39 | raise FusilError( 40 | "Unable to calm down system load after " 41 | "%.1f seconds (current load: %.1f%% > max: %.1f%%)" 42 | % (duration, load * 100, self.max_load * 100) 43 | ) 44 | sleep(self.sleep) 45 | if first_message: 46 | duration = time() - start 47 | agent.info( 48 | "System is now calm after %.1f seconds (current load: %.1f%%)" 49 | % (duration, load * 100) 50 | ) 51 | -------------------------------------------------------------------------------- /fuzzers/notworking/fusil-libexif: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | libexif fuzzer: use "exif picture.jpeg" command. 4 | 5 | Supported file formats: JPEG 6 | """ 7 | 8 | INCR_MANGLE = False 9 | 10 | from fusil.application import Application 11 | from fusil.process.mangle import MangleProcess 12 | from fusil.process.watch import WatchProcess 13 | from fusil.process.stdout import WatchStdout 14 | if INCR_MANGLE: 15 | from fusil.incr_mangle import IncrMangle 16 | else: 17 | from fusil.auto_mangle import AutoMangle 18 | 19 | class Fuzzer(Application): 20 | NAME = "libexif" 21 | USAGE = "%prog [options] image.jpg" 22 | NB_ARGUMENTS = 1 23 | 24 | def setupProject(self): 25 | project = self.project 26 | orig_filename = self.arguments[0] 27 | if INCR_MANGLE: 28 | mangle = IncrMangle(project, orig_filename) 29 | mangle.operation_per_version = 25 30 | mangle.max_version = 50 31 | # FIXME: Only fuzz JPEG EXIF header 32 | #mangle.min_offset = 2 33 | #mangle.max_offset = 555 34 | else: 35 | AutoMangle(project, orig_filename) 36 | 37 | process = MangleProcess(project, ['exif', ""], "") 38 | WatchProcess(process, 39 | # exitcode_score=-0.50, 40 | exitcode_score=0, 41 | ) 42 | 43 | stdout = WatchStdout(process) 44 | stdout.min_nb_line = (3, -0.5) 45 | stdout.words['error'] = 0.10 46 | # "Color Space |Internal error (unknown value 4097)." is not a fatal error 47 | stdout.ignoreRegex(r'unknown (value|data)') 48 | stdout.ignoreRegex(r'Unknown Exif version') 49 | stdout.addRegex(r'^Corrupt data', -1.0) 50 | stdout.addRegex(r'does not contain EXIF data!$', -1.0) 51 | stdout.addRegex(r'The data supplied does not seem to contain EXIF data.$', -1.0) 52 | stdout.addRegex(r'does not contain EXIF data!$', -1.0) 53 | stdout.addRegex(r'^Unknown encoding\.$', -1.0) 54 | 55 | if __name__ == "__main__": 56 | Fuzzer().main() 57 | 58 | -------------------------------------------------------------------------------- /fusil/mas/mta.py: -------------------------------------------------------------------------------- 1 | from weakref import ref as weakref_ref 2 | 3 | from fusil.mas.application_agent import ApplicationAgent 4 | 5 | 6 | class MTA(ApplicationAgent): 7 | """ 8 | Mail (message) transfer agent: 9 | 10 | - send(): store messages in a mailbox of message category 11 | - live(): deliver messages in agent mailboxes 12 | """ 13 | 14 | def __init__(self, application): 15 | ApplicationAgent.__init__(self, "mta", application, None) 16 | self.setupMTA(self, application.logger) 17 | self.mailing_list = {} 18 | self.queue = [] 19 | 20 | def hasMessage(self): 21 | return bool(self.queue) 22 | 23 | def clear(self): 24 | self.queue = [] 25 | 26 | def registerMailingList(self, mailbox, event): 27 | mailbox_ref = weakref_ref(mailbox) 28 | if event not in self.mailing_list: 29 | self.mailing_list[event] = [mailbox_ref] 30 | elif mailbox_ref not in self.mailing_list[event]: 31 | self.mailing_list[event].append(mailbox_ref) 32 | 33 | def unregisterMailingList(self, mailbox, event): 34 | if event not in self.mailing_list: 35 | return 36 | if mailbox not in self.mailing_list[event]: 37 | return 38 | self.mailing_list[event].remove(mailbox) 39 | 40 | def deliver(self, message): 41 | self.queue.append(message) 42 | 43 | def live(self): 44 | # Delive messages to agents including myself 45 | for message in self.queue: 46 | if message.event not in self.mailing_list: 47 | continue 48 | mailing_list = self.mailing_list[message.event] 49 | broken_refs = [] 50 | for mailbox_ref in mailing_list: 51 | mailbox = mailbox_ref() 52 | if mailbox is None: 53 | broken_refs.append(mailbox_ref) 54 | continue 55 | mailbox.deliver(message) 56 | # Remove broken references 57 | for mailbox_ref in broken_refs: 58 | mailing_list.remove(mailbox_ref) 59 | self.clear() 60 | -------------------------------------------------------------------------------- /fusil/network/http_server.py: -------------------------------------------------------------------------------- 1 | from fusil.network.http_request import HttpRequest 2 | from fusil.network.server_client import ServerClientDisconnect 3 | from fusil.network.tcp_server import TcpServer 4 | 5 | 6 | class HttpServer(TcpServer): 7 | def __init__(self, *args): 8 | TcpServer.__init__(self, *args) 9 | self.http_version = "1.0" 10 | 11 | def clientRead(self, client): 12 | # Read data 13 | try: 14 | data = client.recvBytes() 15 | except ServerClientDisconnect: 16 | self.clientDisconnection(client) 17 | return 18 | if not data: 19 | return 20 | 21 | # Process data 22 | request = HttpRequest(data) 23 | self.serveRequest(client, request) 24 | 25 | def serveRequest(self, client, request): 26 | url = request.uri[1:] 27 | if not url: 28 | url = "index.html" 29 | if url == "index.html": 30 | self.serveData( 31 | client, 200, "OK", "

Hello World!

" 32 | ) 33 | else: 34 | self.error404(client, url) 35 | 36 | def error404(self, client, url): 37 | self.warning("Error 404: %r" % url) 38 | self.serveData(client, 404, "Not Found") 39 | 40 | def serveData(self, client, code, code_text, data=None, content_type="text/html"): 41 | if data: 42 | data_len = len(data) 43 | else: 44 | data_len = 0 45 | http_headers = [ 46 | ("Server", "Fusil"), 47 | ("Pragma", "no-cache"), 48 | ("Content-Type", content_type), 49 | ("Content-Length", str(data_len)), 50 | ] 51 | try: 52 | header = "HTTP/%s %s %s\r\n" % (self.http_version, code, code_text) 53 | for key, value in http_headers: 54 | header += "%s: %s\r\n" % (key, value) 55 | header += "\r\n" 56 | if data: 57 | data = header + data 58 | else: 59 | data = header 60 | client.sendBytes(data) 61 | client.close() 62 | except ServerClientDisconnect: 63 | self.clientDisconnection(client) 64 | -------------------------------------------------------------------------------- /fusil/zzuf.py: -------------------------------------------------------------------------------- 1 | from os.path import exists 2 | 3 | from fusil.process.create import ProjectProcess 4 | 5 | LIBRARY_PATHS = ( 6 | # Linux 7 | "/usr/lib/zzuf/libzzuf.so", 8 | # BSD 9 | "/usr/local/lib/zzuf/libzzuf.so", 10 | ) 11 | DEFAULT_RATIO = 0.004 12 | 13 | 14 | class ZzufProcess(ProjectProcess): 15 | def __init__(self, project, arguments, library_path=None, **options): 16 | ProjectProcess.__init__(self, project, arguments, **options) 17 | 18 | # Options 19 | self.use_debug_file = True 20 | self.setRatio(DEFAULT_RATIO, DEFAULT_RATIO) 21 | 22 | # Locate libzzuf library 23 | if not library_path: 24 | for path in LIBRARY_PATHS: 25 | if not exists(path): 26 | continue 27 | library_path = path 28 | break 29 | if not library_path: 30 | raise ValueError( 31 | "Unable to find zzuf library (try %s)" % ", ".join(LIBRARY_PATHS) 32 | ) 33 | 34 | # Load zzuf using LD_PRELOAD 35 | self.env.set("LD_PRELOAD", library_path) 36 | 37 | def init(self): 38 | ProjectProcess.init(self) 39 | self.zzuf_file = None 40 | 41 | def closeStreams(self): 42 | ProjectProcess.closeStreams(self) 43 | if self.zzuf_file: 44 | self.zzuf_file.close() 45 | self.zzuf_file = None 46 | 47 | def createProcess(self): 48 | if self.use_debug_file: 49 | filename = self.session().createFilename("zzuf.dbg") 50 | self.zzuf_file = open(filename, "w") 51 | self.env.set("ZZUF_DEBUG", str(self.zzuf_file.fileno())) 52 | ProjectProcess.createProcess(self) 53 | 54 | def on_aggressivity_value(self, value): 55 | ratio = value / 10.0 56 | self.min_ratio = ratio 57 | self.max_ratio = ratio 58 | self.error("Set zzuf ratio to: %.3f" % ratio) 59 | self.setRatio(self.min_ratio, self.max_ratio) 60 | 61 | def setRatio(self, min_ratio, max_ratio): 62 | self.min_ratio = min_ratio 63 | self.env.set("ZZUF_MINRATIO", str(self.min_ratio)) 64 | self.max_ratio = max_ratio 65 | self.env.set("ZZUF_MAXRATIO", str(self.max_ratio)) 66 | -------------------------------------------------------------------------------- /doc/configuration.rst: -------------------------------------------------------------------------------- 1 | +++++++++++++++++++ 2 | Fusil configuration 3 | +++++++++++++++++++ 4 | 5 | You can configure Fusil using a fusil.conf file in your configuration directory 6 | ($XDG_CONFIG_HOME environment variable or ~/.config/). Template file: :: 7 | 8 | ############################################################### 9 | # General Fusil options 10 | ############################################################### 11 | [fusil] 12 | 13 | # Maximum number of session (0=unlimited) 14 | session = 0 15 | 16 | # Maximum number of success before exit (0=unlimited) 17 | success = 1 18 | 19 | # Minimum score for a successful session 20 | success_score = 0.50 21 | 22 | # Maximum score for a session error 23 | error_score = -0.50 24 | 25 | # Maximum memory in bytes (0=unlimited) 26 | max_memory = 0 27 | 28 | # (Normal) Maximum system load 29 | normal_calm_load = 0.50 30 | 31 | # (Normal) Seconds to sleep until system load is low 32 | normal_calm_sleep = 0.5 33 | 34 | # (Slow) Maximum system load 35 | slow_calm_load = 0.30 36 | 37 | # (Slow) Seconds to sleep until system load is low 38 | slow_calm_sleep = 3.0 39 | 40 | # xhost program path (change X11 permissions) 41 | xhost_program = xhost 42 | 43 | ############################################################### 44 | # Debugger used to trace child processes 45 | ############################################################### 46 | [debugger] 47 | 48 | # Use the debugger? 49 | use_debugger = True 50 | 51 | # Enable trace forks option 52 | trace_forks = True 53 | 54 | 55 | ############################################################### 56 | # Child processes options 57 | ############################################################### 58 | [process] 59 | 60 | # Dump core on crash 61 | core_dump = True 62 | 63 | # Maximum user process (RLIMIT_NPROC) 64 | max_user_process = 10 65 | 66 | # Default maximum memory in bytes (O=unlimited) 67 | max_memory = 104857600 68 | 69 | # Change the user (setuid) 70 | user = fusil 71 | 72 | # Change the group (setgid) 73 | group = fusil 74 | 75 | # Use a probe to watch CPU activity 76 | use_cpu_probe = True 77 | 78 | -------------------------------------------------------------------------------- /fusil/project_directory.py: -------------------------------------------------------------------------------- 1 | from os import getcwd 2 | from os.path import basename 3 | 4 | from fusil.directory import Directory 5 | from fusil.project_agent import ProjectAgent 6 | 7 | 8 | class ProjectDirectory(ProjectAgent, Directory): 9 | def __init__(self, project): 10 | # Create $PWD/run-0001 directory name 11 | Directory.__init__(self, getcwd()) 12 | name = project.application().NAME 13 | self.directory = self.uniqueFilename(name, save=False) 14 | 15 | # Initialize the agent and create the directory 16 | ProjectAgent.__init__(self, project, "directory:%s" % basename(self.directory)) 17 | self.warning("Create the directory: %s" % self.directory) 18 | self.mkdir(not self.application().options.only_generate) 19 | 20 | def keepDirectory(self, verbose=True): 21 | if not self.directory: 22 | return False 23 | 24 | # No session executed? Remove the directory 25 | project = self.project() 26 | application = self.application() 27 | 28 | # Fusil error? Keep the directory 29 | if application and application.exitcode: 30 | if verbose: 31 | self.warning("Fusil error: keep the directory %s" % self.directory) 32 | return True 33 | 34 | # Not session executed: remove the directory 35 | if ( 36 | project 37 | and not project.session_executed 38 | and (not application or not application.options.keep_sessions) 39 | ): 40 | return False 41 | 42 | # Keep generated files? 43 | if not self.isEmpty(True): 44 | # Project generated some extra files: keep the directory 45 | if verbose: 46 | self.error("Keep the non-empty directory %s" % self.directory) 47 | return True 48 | 49 | # Default: remove the directory 50 | return False 51 | 52 | def rmtree(self): 53 | if not self.directory: 54 | return 55 | self.info("Remove the directory: %s" % self.directory) 56 | Directory.rmtree(self) 57 | self.directory = None 58 | 59 | def destroy(self): 60 | keep = self.keepDirectory(verbose=False) 61 | if not keep: 62 | self.rmtree() 63 | -------------------------------------------------------------------------------- /fusil/python/samples/mangle_obj.py: -------------------------------------------------------------------------------- 1 | 2 | from sys import stderr 3 | from unittest.mock import MagicMock 4 | 5 | def mangle_obj(instance, method, *args): 6 | print(f'Mangling {instance} with MagicMock()s, leaving {method} intact.', file=stderr) 7 | 8 | # If the instance doesn't have a __dict__ (e.g., uses __slots__), 9 | # we can't mangle its instance attributes. Call the method directly. 10 | if not hasattr(instance, '__dict__'): 11 | try: 12 | func = getattr(instance, method) 13 | print(f'Calling {instance}.{method} on object without __dict__...', file=stderr) 14 | func(*args) 15 | except Exception as err: 16 | print(f"[{instance}] {method} => {err.__class__.__name__}: {err}", file=stderr) 17 | return 18 | 19 | real_instance_dict = instance.__dict__.copy() 20 | real_class_dict = instance.__class__.__dict__.copy() 21 | 22 | func = getattr(instance, method) 23 | 24 | try: 25 | for key, value in instance.__dict__.items(): 26 | if key.startswith('__') or key == func.__name__: 27 | continue 28 | try: 29 | setattr(instance, key, MagicMock()) 30 | except Exception: 31 | pass 32 | 33 | for key, value in instance.__class__.__dict__.items(): 34 | if key.startswith('__') or key == func.__name__: 35 | continue 36 | try: 37 | setattr(instance.__class__, key, MagicMock()) 38 | except Exception: 39 | pass 40 | print(f'Calling {instance}.{method}...', file=stderr) 41 | func(*args) 42 | 43 | except Exception as err: 44 | try: 45 | errmsg = repr(err) 46 | except ValueError as e: 47 | errmsg = repr(e) 48 | errmsg = errmsg.encode('ASCII', 'replace') 49 | print (f'[{instance}] {func.__name__} => {err.__class__.__name__}: {errmsg}', file=stderr) 50 | 51 | finally: 52 | instance.__dict__.update(real_instance_dict) 53 | for key, value in real_class_dict.items(): 54 | if key.startswith('__') or key == func.__name__: 55 | continue 56 | try: 57 | setattr(instance.__class__, key, value) 58 | except Exception: 59 | pass -------------------------------------------------------------------------------- /fusil/bytes_generator.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Bytes generators: 3 | - BytesGenerator 4 | - LengthGenerator 5 | 6 | Byte sets: 7 | - ASCII8: 0..255 8 | - ASCII0: 1..255 9 | - ASCII7: 0..127 10 | - PRINTABLE_ASCII: 32..126 11 | - UPPER_LETTERS: 'A'..'Z' 12 | - LOWER_LETTERS: 'a'..'z' 13 | - LETTERS: UPPER_LETTERS | LOWER_LETTERS 14 | - DECIMAL_DIGITS: "0".."9" 15 | - HEXADECIMAL_DIGITS: DECIMAL_DIGITS | 'a'..'f' | 'A'..'F' 16 | - PUNCTUATION: --> .,-;?!:(){}[]<>'"/\<-- 17 | """ 18 | 19 | from random import choice, randint 20 | 21 | 22 | def createBytesSet(start, stop): 23 | return set(range(start, stop + 1)) 24 | 25 | 26 | # ASCII codes 0..255 27 | ASCII8 = createBytesSet(0, 255) 28 | 29 | # ASCII codes 1..255 30 | ASCII0 = createBytesSet(1, 255) 31 | 32 | # ASCII codes 0..127 33 | ASCII7 = createBytesSet(0, 127) 34 | 35 | # ASCII codes 32..126 36 | PRINTABLE_ASCII = createBytesSet(32, 126) 37 | 38 | # Letters and digits 39 | UPPER_LETTERS = set(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ") 40 | LOWER_LETTERS = set(b"abcdefghijklmnopqrstuvwxyz") 41 | LETTERS = UPPER_LETTERS | LOWER_LETTERS 42 | DECIMAL_DIGITS = set(b"0123456789") 43 | HEXADECIMAL_DIGITS = DECIMAL_DIGITS | set(b"abcdefABCDEF") 44 | PUNCTUATION = set(b" .,-;?!:(){}[]<>'\"/\\") 45 | 46 | 47 | class Generator: 48 | def __init__(self, min_length, max_length): 49 | self.min_length = min_length 50 | self.max_length = max_length 51 | 52 | def createLength(self): 53 | return randint(self.min_length, self.max_length) 54 | 55 | def _createValue(self, length): 56 | raise NotImplementedError() 57 | 58 | def createValue(self, length=None): 59 | if length is None: 60 | length = self.createLength() 61 | return self._createValue(length) 62 | 63 | 64 | class BytesGenerator(Generator): 65 | def __init__(self, min_length, max_length, bytes_set=ASCII8): 66 | Generator.__init__(self, min_length, max_length) 67 | self.bytes_set = bytes_set 68 | 69 | def _createValue(self, length): 70 | bytes_list = list(self.bytes_set) 71 | if len(bytes_list) != 1: 72 | return bytes(choice(bytes_list) for index in range(length)) 73 | else: 74 | value = bytes_list[0] 75 | value = bytes((value,)) 76 | return value * length 77 | 78 | 79 | class LengthGenerator(BytesGenerator): 80 | def __init__(self, min_length, max_length): 81 | BytesGenerator.__init__(self, min_length, max_length, set(b"A")) 82 | -------------------------------------------------------------------------------- /doc/agent.rst: -------------------------------------------------------------------------------- 1 | Agent 2 | ===== 3 | 4 | An agent is an object able to send/receive messages to/from other agents. 5 | Agents have a limited vision of the whole application: agents do not see them 6 | each other. For pratical reasons, they have access to Application, Project and 7 | Session objets. 8 | 9 | Active or inactive 10 | ------------------ 11 | 12 | By default, an agent is inactive. It does not receive any event and is not 13 | allowed to send messages. Calling activate() method does active then agent 14 | and then call init() method. To disable an agent, use deactive() which 15 | calls deinit() method. 16 | 17 | Sum up of attributes and methods: 18 | 19 | - is_active: boolean 20 | - activate(): enable the agent, call init() 21 | - deactivate(): disable the agent, call deinit() 22 | - init(): create objects (eg. open file) 23 | - deinit(): destroy objects (eg. close file) 24 | 25 | live() 26 | ------ 27 | 28 | When an agent is active, it's live() method is called at each session step. 29 | The method have to be fastest as possible, so don't use any blocking 30 | function (eg. select() before read()). 31 | 32 | Events 33 | ------ 34 | 35 | Method related to message handling: 36 | - readMailbox(): read messages and call related agent event handler 37 | - send(event, \*arguments): send an event to other agents 38 | 39 | You don't have to call readMailbox(), this job is done by 40 | Session.executeObject(). 41 | 42 | To register to a message, just add a method to your class with the prototype:: 43 | 44 | def on_EVENT(self, *arguments): ... 45 | 46 | Example of method called on session start:: 47 | 48 | def on_session_start(self): 49 | ... 50 | 51 | Other attributes 52 | ----------------- 53 | 54 | - name (str): Agent name (should be unique in the whole application) 55 | - agent_id (int): Unique identifier in the whole application (integer counter 56 | starting at 1) 57 | - logger: Logger object used by methods debug(), info(), ... 58 | - mailbox: Mailbox used to store message until readMailbox() is called 59 | 60 | Logging 61 | ------- 62 | 63 | To write string to logger, use methods: 64 | 65 | - debug(message): DEBUG level 66 | - info(message): INFO level 67 | - warning(message): WARNING level 68 | - error(message): ERROR level 69 | 70 | Score 71 | ----- 72 | 73 | ProjectAgent and SessionAgent have a method getScore() which return the agent 74 | score. Default value is None (agent has no score). An agent has also 75 | 'score_weight' attribute (default value: 1.0) which is used to compute final 76 | agent score: 77 | 78 | minmax(-1.0, agent.getScore() * agent.score_weight, 1.0) 79 | 80 | -------------------------------------------------------------------------------- /fusil/process/watch.py: -------------------------------------------------------------------------------- 1 | from weakref import ref as weakref_ref 2 | 3 | from fusil.project_agent import ProjectAgent 4 | 5 | from fusil.process.cpu_probe import CpuProbe 6 | 7 | DEFAULT_EXITCODE_SCORE = 0.50 8 | DEFAULT_TIMEOUT_SCORE = 1.0 9 | DEFAULT_SIGNAL_SCORE = 1.0 10 | 11 | 12 | class WatchProcess(ProjectAgent): 13 | def __init__( 14 | self, 15 | process, 16 | exitcode_score=DEFAULT_EXITCODE_SCORE, 17 | signal_score=DEFAULT_SIGNAL_SCORE, 18 | default_score=0.0, 19 | timeout_score=DEFAULT_TIMEOUT_SCORE, 20 | ): 21 | self.process = weakref_ref(process) 22 | project = process.project() 23 | ProjectAgent.__init__(self, project, "watch:%s" % process.name) 24 | self.cpu = CpuProbe(project, "%s:cpu" % self.name) 25 | 26 | # Score if process exited normally 27 | self.default_score = default_score 28 | 29 | # Score if process exit code is not nul 30 | self.exitcode_score = exitcode_score 31 | 32 | # Score if process has been killed by signal 33 | self.signal_score = signal_score 34 | 35 | # Score if process timeout has been reached 36 | self.timeout_score = timeout_score 37 | 38 | def init(self): 39 | self.score = None 40 | self.pid = None 41 | 42 | def on_process_create(self, agent): 43 | if agent != self.process(): 44 | return 45 | self.pid = agent.process.pid 46 | self.prepareProcess() 47 | 48 | def on_session_start(self): 49 | if self.pid is not None: 50 | self.prepareProcess() 51 | 52 | def prepareProcess(self): 53 | if self.cpu: 54 | self.cpu.setPid(self.pid) 55 | 56 | def live(self): 57 | if not self.pid: 58 | return 59 | 60 | # Check if process is done or not 61 | status = self.process().poll() 62 | if status is not None: 63 | self.processDone(status) 64 | return 65 | 66 | def processDone(self, status): 67 | self.score = self.computeScore(status) 68 | self.send("session_stop") 69 | self.pid = None 70 | 71 | def getScore(self): 72 | return self.score 73 | 74 | def computeScore(self, status): 75 | # Timeout reached 76 | if self.process().timeout_reached: 77 | return self.timeout_score 78 | 79 | # No status: no way to compute score 80 | if status is None: 81 | return None 82 | 83 | # Process exit code is not nul? 84 | if 0 < status: 85 | return self.exitcode_score 86 | 87 | # Process killed by a signal 88 | if status < 0: 89 | return self.signal_score 90 | 91 | # Process exited normally: default score 92 | return self.default_score 93 | 94 | def deinit(self): 95 | self.pid = None 96 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Todo list to prepare a release: 4 | # - hg in # check that there is no incoming changes 5 | # - run: ./pyflakes.sh 6 | # - run: ./test_doc.py 7 | # - run: sudo bash -c "PYTHONPATH=$PWD ./fuzzers/fusil-gettext" 8 | # - edit fusil/version.py: check/set version 9 | # - edit ChangeLog: set release date 10 | # - hg ci 11 | # - hg tag fusil-x.y 12 | # - hg push 13 | # - ./setup.py sdist register upload 14 | # - upload the tarball to Python Package Index 15 | # - update the website home page (url, md5 and news) 16 | # 17 | # After the release: 18 | # - edit fusil/version.py: set version to n+1 19 | # - edit ChangeLog: add a new empty section for version n+1 20 | # - hg ci 21 | # - hg push 22 | 23 | from importlib.machinery import SourceFileLoader 24 | from os import path 25 | from sys import argv 26 | from glob import glob 27 | 28 | CLASSIFIERS = [ 29 | 'Intended Audience :: Developers', 30 | 'Development Status :: 5 - Production/Stable', 31 | 'Environment :: Console', 32 | 'License :: OSI Approved :: GNU General Public License (GPL)', 33 | 'Operating System :: OS Independent', 34 | 'Natural Language :: English', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 3', 37 | ] 38 | 39 | MODULES = ( 40 | "fusil", 41 | "fusil.linux", 42 | "fusil.mas", 43 | "fusil.network", 44 | "fusil.process", 45 | "fusil.python", 46 | "fusil.python.jit", 47 | "fusil.python.samples", 48 | ) 49 | 50 | SCRIPTS = glob("fuzzers/fusil-*") 51 | 52 | def main(): 53 | if "--setuptools" in argv: 54 | argv.remove("--setuptools") 55 | from setuptools import setup 56 | use_setuptools = True 57 | else: 58 | from distutils.core import setup 59 | use_setuptools = False 60 | 61 | fusil = SourceFileLoader("version", path.join("fusil", "version.py")).load_module() 62 | PACKAGES = {} 63 | for name in MODULES: 64 | PACKAGES[name] = name.replace(".", "/") 65 | 66 | with open('README.rst') as fp: 67 | long_description = fp.read() 68 | with open('ChangeLog') as fp: 69 | long_description += fp.read() 70 | 71 | install_options = { 72 | "name": fusil.PACKAGE, 73 | "version": fusil.VERSION, 74 | "url": fusil.WEBSITE, 75 | "download_url": fusil.WEBSITE, 76 | "author": "Victor Stinner", 77 | "description": "Fuzzing framework", 78 | "long_description": long_description, 79 | "classifiers": CLASSIFIERS, 80 | "license": fusil.LICENSE, 81 | "packages": list(PACKAGES.keys()), 82 | "package_dir": PACKAGES, 83 | "scripts": SCRIPTS, 84 | } 85 | 86 | if use_setuptools: 87 | install_options["install_requires"] = ["python-ptrace>=0.7"] 88 | setup(**install_options) 89 | 90 | if __name__ == "__main__": 91 | main() 92 | 93 | -------------------------------------------------------------------------------- /fuzzers/fusil-ogg123: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | ogg123 fuzzer 4 | """ 5 | 6 | MAX_FILESIZE = 32*1024 7 | PROGRAM = 'ogg123' 8 | MANGLE = "fixed" 9 | 10 | from fusil.application import Application 11 | from optparse import OptionGroup 12 | from fusil.process.mangle import MangleProcess 13 | from fusil.process.watch import WatchProcess 14 | from fusil.process.stdout import WatchStdout 15 | if MANGLE == "incr": 16 | from fusil.incr_mangle import IncrMangle as OggMangle 17 | elif MANGLE == "auto": 18 | from fusil.auto_mangle import AutoMangle as OggMangle 19 | else: 20 | from fusil.mangle import MangleFile as OggMangle 21 | 22 | class Fuzzer(Application): 23 | NAME = "ogg123" 24 | USAGE = "%prog [options] audio.ogg" 25 | NB_ARGUMENTS = 1 26 | 27 | def createFuzzerOptions(self, parser): 28 | options = OptionGroup(parser, "ogg123 fuzzer") 29 | options.add_option("--max-filesize", help="Maximum file size in bytes (default: %s)" % MAX_FILESIZE, 30 | type="int", default=MAX_FILESIZE) 31 | options.add_option("--program", help="Ogg program: ogg123 or ogginfo (default: %s)" % PROGRAM, 32 | choices=("ogg123", "ogginfo"), default=PROGRAM) 33 | return options 34 | 35 | def setupProject(self): 36 | project = self.project 37 | 38 | orig_filename = self.arguments[0] 39 | mangle = OggMangle(project, orig_filename) 40 | mangle.max_size = self.options.max_filesize 41 | if MANGLE == "auto": 42 | mangle.hard_min_op = 1 43 | mangle.hard_max_op = 100 44 | elif MANGLE == "incr": 45 | from fusil.incr_mangle_op import InverseBit, Increment 46 | mangle.operations = (InverseBit, Increment) 47 | else: 48 | mangle.config.min_op = 1 49 | mangle.config.max_op = 10 50 | 51 | if self.options.program == "ogginfo": 52 | COMMAND = ['ogginfo', ''] 53 | else: 54 | COMMAND = ['ogg123', '-d', 'null', ''] 55 | 56 | process = MangleProcess(project, COMMAND, "", timeout=60.0) 57 | process.env.copy('HOME') 58 | 59 | if COMMAND[0] == 'ogg123': 60 | WatchProcess(process, exitcode_score=-0.25) 61 | else: 62 | WatchProcess(process, exitcode_score=0) 63 | 64 | stdout = WatchStdout(process) 65 | stdout.max_nb_line = None 66 | stdout.show_matching = True 67 | stdout.addRegex(r"The file may be corrupted", -0.50) 68 | stdout.addRegex(r"Corrupted ogg", -0.50) 69 | stdout.addRegex(r"Could not decode vorbis header packet", -0.50) 70 | # stdout.ignoreRegex('^Warning: Could not decode vorbis header packet') 71 | stdout.ignoreRegex('^Warning: sequence number gap') 72 | stdout.ignoreRegex('^New logical stream.*: type invalid$') 73 | 74 | if __name__ == "__main__": 75 | Fuzzer().main() 76 | 77 | -------------------------------------------------------------------------------- /doc/mangle.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Mangle valid file 3 | ***************** 4 | 5 | MangleFile 6 | ========== 7 | 8 | To fuzz file parser, you can use MangleFile agent. It takes one or multiple 9 | valid files on input and then injects errors to create invalid files. It can 10 | generate multiple files for each session. 11 | 12 | Operations 13 | ---------- 14 | 15 | * replace: replace a byte by a random byte 16 | * bit: invert one bit value 17 | * special_value: replace one or more bytes to write a special value, 18 | eg. four bytes: "0xFF 0xFF 0xFF 0xFF" 19 | * insert_bytes: insert one or more random bytes 20 | * delete_bytes: delete one or more bytes 21 | 22 | 23 | MangleConfig 24 | ------------ 25 | 26 | You can configure some options to help fuzzing using 'config' attribute of 27 | MangleFile. The value is an instance of MangleConfig class. Options: 28 | 29 | * min_op: Minimum number of mangle operations (default: 1) 30 | * max_op: Maximum number of mangle operations (default: 10) 31 | * operations: List of operation name (default: ["replace", "bit", "special_value"]) 32 | * max_insert_bytes: Maximum number of insered bytes (default: 8) 33 | * max_delete_bytes: Maximum number of deleted bytes (default: 8) 34 | * change_size: Allow operations which change data size (default: False) 35 | 36 | 37 | Truncate 38 | -------- 39 | 40 | You can limit maximum file size using 'max_size' attribute of MangleFile. 41 | The value is the maximum number of bytes read from input file. 42 | 43 | AutoMangle 44 | ========== 45 | 46 | AutoMangle is an helper to MangleFile: it tries to find the best parameters 47 | to fuzz the target using session aggressivity. Option attributes: 48 | 49 | * hard_min_op (default: 0): Minimum number of operations 50 | * hard_max_op (default: 10000): Maximum number of operations 51 | * fixed_size_factor (default: 1.0): ratio used to compute the number 52 | of operations depending on the file 53 | 54 | IncrMangle 55 | ========== 56 | 57 | IncrMangle is the incremental mangle agent. Whereas AutoMangle regenerates 58 | all errors for each session, IncrMangle keeps errors between the sessions 59 | and add some new errors. Option attributes: 60 | 61 | * operation_per_version: Maximum number of operations applied 62 | to new session 63 | * max_version: Maximum version number for a file, if a file 64 | is older max_version, the operations are truncated to a random number 65 | of versions 66 | * min_offset and max_offset (default None): Minimum and maximum file offset, 67 | both are optional (use None value) 68 | 69 | Default values: 70 | 71 | >>> from fusil.mockup import Project 72 | >>> project = Project() 73 | >>> from fusil.incr_mangle import IncrMangle 74 | >>> mangle = IncrMangle(project, 'filename') 75 | >>> mangle.operation_per_version 76 | 1 77 | >>> mangle.max_version 78 | 25 79 | >>> mangle.min_offset, mangle.max_offset 80 | (None, None) 81 | 82 | -------------------------------------------------------------------------------- /fusil/mangle_agent.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from array import array 4 | from os import fstat 5 | from random import choice 6 | from stat import ST_SIZE 7 | 8 | from fusil.project_agent import ProjectAgent 9 | 10 | 11 | class MangleAgent(ProjectAgent): 12 | def __init__(self, project, sources, nb_file=1): 13 | ProjectAgent.__init__(self, project, "mangle") 14 | if isinstance(sources, str): 15 | self.source_filenames = (sources,) 16 | else: 17 | # Remove duplicates 18 | self.source_filenames = tuple(set(sources)) 19 | if 1 < len(self.source_filenames): 20 | self.error("Sources filenames: %s" % len(self.source_filenames)) 21 | self.max_size = None # 10*1024*1024 22 | self.nb_file = nb_file 23 | 24 | def readData(self, filename, file_index): 25 | # Open file and read file size 26 | self.info("Load input file: %s" % filename) 27 | data = open(filename, "rb") 28 | orig_filesize = fstat(data.fileno())[ST_SIZE] 29 | if not orig_filesize: 30 | raise ValueError("Input file (%s) is empty!" % filename) 31 | 32 | # Read bytes 33 | if self.max_size: 34 | data = data.read(self.max_size) 35 | else: 36 | data = data.read() 37 | 38 | # Display message if input is truncated 39 | if len(data) < orig_filesize: 40 | percent = len(data) * 100.0 / orig_filesize 41 | self.warning( 42 | "Truncate file to %s bytes (%.2f%% of %s bytes)" 43 | % (len(data), percent, orig_filesize) 44 | ) 45 | 46 | # Convert to Python array object 47 | return array("B", data) 48 | 49 | def writeData(self, filename, data): 50 | self.info("Generate file: %s" % filename) 51 | with open(filename, "wb") as output: 52 | data.tofile(output) 53 | return filename 54 | 55 | def createFilename(self, filename, file_index): 56 | if 1 < self.nb_file: 57 | count = 1 58 | else: 59 | count = None 60 | return self.session().createFilename(filename, count=count) 61 | 62 | def mangle(self): 63 | filenames = [] 64 | for file_index in range(self.nb_file): 65 | filename = choice(self.source_filenames) 66 | data = self.readData(filename, file_index) 67 | data = self.mangleData(data, file_index) 68 | filename = self.createFilename(filename, file_index) 69 | self.writeData(filename, data) 70 | filenames.append(filename) 71 | self.send("mangle_filenames", filenames) 72 | 73 | # --- Abstract methods --- 74 | 75 | def mangleData(self, data, file_index): 76 | # data: array of unsigned bytes, array('B', ...) 77 | raise NotImplementedError() 78 | 79 | def on_session_start(self): 80 | self.mangle() 81 | -------------------------------------------------------------------------------- /fuzzers/notworking/fusil-linux-proc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Write random data in /proc/PID/* files 4 | """ 5 | 6 | from fusil.application import Application 7 | from fusil.process.create import ProjectProcess 8 | from fusil.process.watch import WatchProcess 9 | from fusil.process.stdout import WatchStdout 10 | from fusil.project_agent import ProjectAgent 11 | from fusil.bytes_generator import BytesGenerator 12 | from fusil.linux.syslog import Syslog 13 | from os.path import join as path_join 14 | from random import choice 15 | from errno import ENOENT, EACCES, EINVAL, EPERM 16 | 17 | class Fuzzer(Application): 18 | NAME = "proc" 19 | 20 | def setupProject(self): 21 | project = self.project 22 | # project.session_timeout = 1.0 23 | 24 | process = ProjectProcess(project, 25 | ['/bin/bash'], timeout=5.0) 26 | AttackProc(project) 27 | WatchProcess(process, timeout_score=0) 28 | WatchStdout(process) 29 | syslog = Syslog(project) 30 | for watch in syslog: 31 | watch.ignoreRegex('info="invalid command"') 32 | watch.show_not_matching = True 33 | 34 | class AttackProc(ProjectAgent): 35 | def __init__(self, project): 36 | ProjectAgent.__init__(self, project, "proc") 37 | self.generator = BytesGenerator(1, 256) 38 | 39 | def init(self): 40 | self.proc_keys = [ 41 | 'attr/current', 42 | 'attr/exec', 43 | 'attr/fscreate', 44 | 'attr/keycreate', 45 | 'attr/sockcreate', 46 | 'clear_refs', 47 | 'seccomp', 48 | 49 | # Strange keys 50 | #'mem', 51 | #'oom_adj', 52 | ] 53 | self.proc_path = None 54 | 55 | def on_process_create(self, agent): 56 | self.proc_path = "/proc/%s/" % agent.process.pid 57 | 58 | def live(self): 59 | if not self.proc_path: 60 | return 61 | self.info("Proc path: %s" % self.proc_path) 62 | 63 | key = choice(self.proc_keys) 64 | filename = path_join(self.proc_path, key) 65 | data = self.generator.createValue() 66 | self.info("Write data in %s: (len=%s) %r" 67 | % (filename, len(data), data)) 68 | try: 69 | output = open(filename, 'wb') 70 | output.write(data) 71 | output.close() 72 | except IOError as err: 73 | if err.errno in (EINVAL, EPERM): 74 | pass 75 | elif err.errno in (ENOENT, EACCES): 76 | self.error("Unable to write %s: %s" % (filename, err)) 77 | self.removeKey(key) 78 | else: 79 | raise 80 | 81 | def removeKey(self, key): 82 | self.proc_keys.remove(key) 83 | if not self.proc_keys: 84 | self.error("All /proc entries are invalid!") 85 | self.send('project_done') 86 | self.proc_path = None 87 | 88 | if __name__ == "__main__": 89 | Fuzzer().main() 90 | 91 | -------------------------------------------------------------------------------- /fuzzers/fusil-imagemagick: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | ImageMagick picture toolkit 4 | 5 | Use "identify -verbose image" or "convert image temp.bmp" command line. 6 | 7 | Supported file formats: BMP, GIF, JPG, ICO, ... 8 | """ 9 | 10 | INCR_MANGLE = False 11 | 12 | from fusil.application import Application 13 | from optparse import OptionGroup 14 | from fusil.process.mangle import MangleProcess 15 | from fusil.process.watch import WatchProcess 16 | from fusil.process.stdout import WatchStdout 17 | if INCR_MANGLE: 18 | from fusil.incr_mangle import IncrMangle as BaseMangle 19 | else: 20 | from fusil.auto_mangle import AutoMangle as BaseMangle 21 | from fusil.fixpng import fixPNG 22 | 23 | class Fuzzer(Application): 24 | NAME = "imagemagick" 25 | USAGE = "%prog [--convert] [options] filename" 26 | NB_ARGUMENTS = 1 27 | 28 | def createFuzzerOptions(self, parser): 29 | options = OptionGroup(parser, "ImageMagick fuzzer") 30 | options.add_option("--convert", help="Use convert program instead of identify", 31 | action="store_true") 32 | options.add_option("--no-stdout", dest="use_stdout", help="Don't use stdout/stderr", 33 | action="store_false", default=True) 34 | return options 35 | 36 | def setupProject(self): 37 | project = self.project 38 | 39 | orig_filename = self.arguments[0] 40 | mangle = ImageMangle(project, orig_filename) 41 | if INCR_MANGLE: 42 | mangle.operation_per_version = 1 43 | mangle.max_version = 50 44 | else: 45 | mangle.fixed_size_factor = 0.5 46 | 47 | options = {'timeout': 2.0} 48 | if self.options.convert: 49 | output = project.createFilename('output.bmp') 50 | cmdline = ['convert', '', output] 51 | else: 52 | cmdline = ['identify', '-verbose', ''] 53 | if not self.options.use_stdout: 54 | options['stdout'] = 'null' 55 | process = MangleProcess(project, cmdline, '', **options) 56 | options = {'exitcode_score': -0.25} 57 | if orig_filename.endswith(".jpg"): 58 | # Don't care about libjpeg stdout flooding 59 | options['timeout_score'] = -0.25 60 | WatchProcess(process, **options) 61 | 62 | if self.options.use_stdout: 63 | stdout = WatchStdout(process) 64 | stdout.max_nb_line = (3000, 0.20) 65 | stdout.addRegex('Memory allocation failed', 1.0) 66 | stdout.addRegex('no decode delegate for this image format', -1.0) 67 | stdout.addRegex('Corrupt', 0.05) 68 | stdout.addRegex('Unsupported', 0.05) 69 | stdout.addRegex('Not a JPEG file', -0.50) 70 | stdout.addRegex('JPEG datastream contains no image', -0.50) 71 | stdout.show_not_matching = False 72 | 73 | class ImageMangle(BaseMangle): 74 | def writeData(self, filename, data): 75 | if filename.endswith(".png"): 76 | self.info("Fix CRC32 of PNG chunks") 77 | data = fixPNG(data) 78 | BaseMangle.writeData(self, filename, data) 79 | 80 | if __name__ == "__main__": 81 | Fuzzer().main() 82 | 83 | -------------------------------------------------------------------------------- /tools/fuzz_loop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ============================================================================== 4 | # Fusil JIT Fuzzer - Master Feedback Loop Script 5 | # ============================================================================== 6 | # This script automates the coverage-guided fuzzing process. It continuously: 7 | # 1. Generates a new test case (prioritizing mutation of the corpus). 8 | # 2. Runs the test case with JIT logging enabled. 9 | # 3. Parses the log for new coverage with jit_coverage_parser.py. 10 | # 4. The parser automatically saves interesting test cases to the corpus. 11 | # 5. The loop repeats, creating an evolutionary fuzzing cycle. 12 | # ============================================================================== 13 | 14 | # --- Configuration --- 15 | # Path to the fusil executable 16 | FUSIL_PATH="/mnt/c/Users/ddini/PycharmProjects/fusil/fuzzers/fusil-python-threaded" 17 | 18 | # Path to the coverage parser tool 19 | PARSER_PATH="/mnt/c/Users/ddini/PycharmProjects/fusil/tools/jit_coverage_parser.py" 20 | 21 | # Temporary files for the current run 22 | TMP_SOURCE_FILE="/home/fusil/runs/source_01.py" 23 | TMP_LOG_FILE="/home/fusil/runs/stdout_01.txt" 24 | 25 | # Environment variables to enable JIT's verbose logging 26 | export PYTHON_LLTRACE=4 27 | export PYTHON_OPT_DEBUG=4 28 | 29 | # --- Main Fuzzing Loop --- 30 | echo "[+] Starting JIT fuzzer feedback loop. Press Ctrl+C to stop." 31 | 32 | # Ensure corpus directory exists 33 | mkdir -p corpus/jit_interesting_tests 34 | 35 | session_count=0 36 | while true; do 37 | session_count=$((session_count + 1)) 38 | echo "----------------------------------------------------------------------" 39 | echo "[+] Fuzzing Session #$session_count: Generating new test case..." 40 | 41 | # Step 1: Generate a new test case using feedback-driven mode 42 | # The --jit-target-uop=ALL is a fallback for the first few runs before the corpus is populated. 43 | python3 "$FUSIL_PATH" \ 44 | --jit-fuzz \ 45 | --jit-feedback-driven-mode \ 46 | --jit-target-uop=ALL \ 47 | --classes-number=0 \ 48 | --functions-number=1 \ 49 | --methods-number=0 \ 50 | --objects-number=0 \ 51 | --sessions=1 \ 52 | --python=/home/danzin/venvs/jit_cpython_venv/bin/python \ 53 | -v \ 54 | --no-threads \ 55 | --no-async \ 56 | --jit-loop-iterations 300 \ 57 | --no-numpy \ 58 | --modules=encodings.ascii \ 59 | --source-output-path /home/fusil/runs/source_01.py \ 60 | --stdout-path /home/fusil/runs/stdout_01.txt 61 | 62 | if [ $? -ne 0 ]; then 63 | echo "[!] ERROR: Fusil failed to generate a test case. Exiting." 64 | exit 1 65 | fi 66 | 67 | echo "[+] Running test case and capturing JIT log..." 68 | # Step 2: Execute the test case, redirecting all output to the log file 69 | python3 "$TMP_SOURCE_FILE" > "$TMP_LOG_FILE" 2>&1 70 | 71 | echo "[+] Analyzing log for new coverage..." 72 | # Step 3 & 4: Parse the log. The parser handles corpus saving. 73 | python3 "$PARSER_PATH" "$TMP_LOG_FILE" "$TMP_SOURCE_FILE" 74 | 75 | echo "[+] Session #$session_count complete." 76 | sleep 1 # Small delay to prevent overwhelming the system 77 | done 78 | -------------------------------------------------------------------------------- /fusil/x11.py: -------------------------------------------------------------------------------- 1 | from re import IGNORECASE 2 | from re import compile as compileRegex 3 | 4 | from Xlib.display import Display 5 | from Xlib.protocol.event import KeyPress as KeyPressEvent 6 | from Xlib.protocol.event import KeyRelease as KeyReleaseEvent 7 | from Xlib.protocol.request import InternAtom 8 | from Xlib.X import NONE, AnyPropertyType, CurrentTime, KeyPress, KeyRelease 9 | 10 | 11 | def listWindows(root): 12 | children = root.query_tree().children 13 | for window in children: 14 | yield window 15 | for window in children: 16 | for window in listWindows(window): 17 | yield window 18 | 19 | 20 | def findWindowById(root, window_id): 21 | for window in listWindows(root): 22 | if window.id == window_id: 23 | return window 24 | raise KeyError("Unable to find Window 0x%08x" % window_id) 25 | 26 | 27 | def findWindowByNameRegex(root, name_regex, ignore_case=True): 28 | if ignore_case: 29 | flags = IGNORECASE 30 | else: 31 | flags = 0 32 | match = compileRegex(name_regex, flags).search 33 | for window in listWindows(root): 34 | name = window.get_wm_name() 35 | if name and match(name): 36 | return window 37 | raise KeyError("Unable to find window with name regex: %r" % name_regex) 38 | 39 | 40 | def formatWindow(window): 41 | name = window.get_wm_name() 42 | info = [] 43 | if name: 44 | info.append(name) 45 | 46 | geometry = window.get_geometry() 47 | info.append( 48 | "%sx%sx%s at (%s,%s)" 49 | % (geometry.width, geometry.height, geometry.depth, geometry.x, geometry.y) 50 | ) 51 | 52 | atom = InternAtom(display=window.display, name="_NET_WM_PID", only_if_exists=1) 53 | pid = window.get_property(atom.atom, AnyPropertyType, 0, 10) 54 | if pid: 55 | pid = int(pid.value.tolist()[0]) 56 | info.append("PID=%r" % pid) 57 | 58 | info.append("ID=0x%08x" % window.id) 59 | return "; ".join(info) 60 | 61 | 62 | def displayWindows(root, level=0): 63 | tree = root.query_tree() 64 | children = tree.children 65 | if not children: 66 | return 67 | indent = " " * level 68 | print(indent + "|== %s ===" % formatWindow(root)) 69 | parent = tree.parent 70 | if parent: 71 | print(indent + "|-- parent: %s" % formatWindow(parent)) 72 | for window in children: 73 | print(indent + "|-> %s" % formatWindow(window)) 74 | displayWindows(window, level + 1) 75 | 76 | 77 | def sendKey(window, keycode, modifiers=0, released=True): 78 | if released: 79 | type = KeyRelease 80 | event_class = KeyReleaseEvent 81 | else: 82 | type = KeyPress 83 | event_class = KeyPressEvent 84 | event = event_class( 85 | type=type, 86 | detail=keycode, 87 | time=CurrentTime, 88 | root=NONE, 89 | window=window, 90 | child=NONE, 91 | root_x=0, 92 | root_y=0, 93 | event_x=0, 94 | event_y=0, 95 | state=modifiers, 96 | same_screen=1, 97 | ) 98 | window.send_event(event) 99 | window.display.flush() 100 | 101 | 102 | def getDisplay(): 103 | return Display() 104 | -------------------------------------------------------------------------------- /fusil/network/server.py: -------------------------------------------------------------------------------- 1 | from select import select 2 | from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket 3 | from socket import error as socket_error 4 | 5 | from ptrace.error import writeError 6 | 7 | from fusil.network.server_client import ServerClient 8 | from fusil.network.tools import formatAddress 9 | from fusil.project_agent import ProjectAgent 10 | 11 | 12 | class NetworkServer(ProjectAgent): 13 | CLIENT_CLASS = ServerClient 14 | 15 | def __init__(self, project, name): 16 | ProjectAgent.__init__(self, project, name) 17 | self.log_data_exchange = False 18 | self.backlog = 5 19 | self.client_class = self.CLIENT_CLASS 20 | self.socket = None 21 | self.family = None 22 | self.clients = [] 23 | 24 | def bind(self, address, family=AF_INET, type=SOCK_STREAM, reuse_address=True): 25 | try: 26 | self.socket = socket(family, type) 27 | if reuse_address: 28 | self.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 29 | self.family = family 30 | self.socket.bind(address) 31 | self.socket.listen(self.backlog) 32 | self.error("Server waiting on %s" % formatAddress(family, address)) 33 | except socket_error as err: 34 | writeError( 35 | self, err, "Unable to bind on %s" % formatAddress(family, address) 36 | ) 37 | self.socket = None 38 | self.send("application_error", "Network server bind error") 39 | 40 | def close(self): 41 | if self.clients: 42 | for client in self.clients: 43 | client.close(emit_exception=False) 44 | self.clients = [] 45 | if self.socket: 46 | self.info("Close socket") 47 | self.socket.close() 48 | self.socket = None 49 | 50 | def destroy(self): 51 | self.close() 52 | 53 | def acceptClient(self): 54 | client_socket, client_address = self.socket.accept() 55 | client = self.client_class( 56 | self.session(), self, client_socket, client_address, self.family 57 | ) 58 | self.warning("New client: %s" % client) 59 | self.clients.append(client) 60 | 61 | def clientDisconnection(self, client): 62 | if client not in self.clients: 63 | return 64 | self.info("Client closed: %r" % client) 65 | self.clients.remove(client) 66 | 67 | def clientRead(self, client): 68 | # FIXME: use received bytes 69 | client.recvBytes() 70 | 71 | def live(self): 72 | if not self.socket: 73 | return 74 | server_fileno = self.socket.fileno() 75 | read_fds = [server_fileno] 76 | client_fds = dict((client.socket.fileno(), client) for client in self.clients) 77 | read_fds += list(client_fds.keys()) 78 | read_available = select(read_fds, [], [], 0)[0] 79 | if read_available is None: 80 | return 81 | for fd in read_available: 82 | if fd in client_fds: 83 | client = client_fds[fd] 84 | self.info("Read data from %s" % client) 85 | self.clientRead(client) 86 | else: 87 | self.info("Accept client") 88 | self.acceptClient() 89 | -------------------------------------------------------------------------------- /tests/python/samples/test_tricky_typing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | import types 5 | import typing 6 | import collections.abc 7 | 8 | # --- Test Setup: Path Configuration --- 9 | # This ensures the test runner can find the 'fusil' package. 10 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 11 | PROJECT_ROOT = os.path.join(SCRIPT_DIR, '..', '..', '..') 12 | sys.path.insert(0, PROJECT_ROOT) 13 | 14 | try: 15 | # --- Import the object to be tested --- 16 | from fusil.python.samples.tricky_typing import big_union 17 | TYPING_AVAILABLE = True 18 | except (ImportError, TypeError) as e: 19 | # This can happen if the Python version is too old for some typing features 20 | print(f"Could not import tricky_typing module, skipping tests: {e}", file=sys.stderr) 21 | big_union = None 22 | TYPING_AVAILABLE = False 23 | 24 | 25 | @unittest.skipIf(not TYPING_AVAILABLE, "Could not import tricky_typing module, skipping tests.") 26 | class TestTrickyTyping(unittest.TestCase): 27 | """ 28 | Test suite for the tricky_typing sample module. 29 | 30 | These tests verify that the final 'big_union' object is constructed 31 | correctly, contains the expected types, and has filtered out unwanted types. 32 | """ 33 | 34 | def test_big_union_is_valid_type(self): 35 | """ 36 | Verifies that 'big_union' is a valid Union type object. 37 | """ 38 | # In Python 3.10+, unions created with | are types.UnionType. 39 | # In older versions, they are typing.Union. We check for both. 40 | self.assertTrue( 41 | isinstance(big_union, (types.UnionType, typing.Union)), 42 | f"'big_union' is not a valid Union type, but {type(big_union)}" 43 | ) 44 | 45 | def test_big_union_contains_expected_types(self): 46 | """ 47 | Verifies that 'big_union' contains a sample of key expected types. 48 | """ 49 | # typing.get_args() lets us inspect the contents of the Union 50 | union_contents = typing.get_args(big_union) 51 | 52 | # Check for types from various modules to ensure they were all processed 53 | self.assertIn(int, union_contents, "int (the base type) should be in the union") 54 | self.assertIn(str, union_contents, "str from builtins should be in the union") 55 | self.assertIn(collections.abc.Iterable, union_contents, "Iterable from collections.abc should be in the union") 56 | self.assertIn(typing.Generic, union_contents, "Generic from typing should be in the union") 57 | self.assertIn(types.ModuleType, union_contents, "ModuleType from types should be in the union") 58 | 59 | def test_big_union_excludes_exceptions(self): 60 | """ 61 | Verifies that the filtering logic correctly excluded exception types. 62 | """ 63 | union_contents = typing.get_args(big_union) 64 | 65 | # Check that common exception types are NOT in the union 66 | self.assertNotIn(Exception, union_contents, "Exception class should have been filtered out") 67 | self.assertNotIn(ValueError, union_contents, "ValueError class should have been filtered out") 68 | self.assertNotIn(BaseException, union_contents, "BaseException class should have been filtered out") 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /fusil/network/server_client.py: -------------------------------------------------------------------------------- 1 | from socket import SHUT_RDWR 2 | from socket import error as socket_error 3 | from socket import timeout as socket_timeout 4 | from weakref import ref as weakref_ref 5 | 6 | from ptrace.error import formatError 7 | 8 | from fusil.network.tools import formatAddress 9 | from fusil.session_agent import SessionAgent 10 | 11 | 12 | class ServerClientDisconnect(Exception): 13 | pass 14 | 15 | 16 | class ServerClient(SessionAgent): 17 | def __init__(self, session, server, socket, address, family): 18 | self.server = weakref_ref(server) 19 | self.socket = socket 20 | self.address = address 21 | self.family = family 22 | name = "net_client:" + formatAddress(self.family, self.address, short=True) 23 | SessionAgent.__init__(self, session, name) 24 | self.tx_bytes = 0 25 | self.rx_bytes = 0 26 | 27 | def recvBytes(self, buffer_size=1024): 28 | log_data_exchange = self.server().log_data_exchange 29 | datas = [] 30 | while True: 31 | try: 32 | self.socket.settimeout(0.010) 33 | data = self.socket.recv(buffer_size) 34 | except socket_timeout: 35 | break 36 | except socket_error as err: 37 | errcode = err[0] 38 | if errcode == 11: # Resource temporarily unavailable 39 | break 40 | else: 41 | self.close() 42 | break 43 | if not data: 44 | break 45 | data_len = len(data) 46 | self.rx_bytes += data_len 47 | if log_data_exchange: 48 | self.warning("Read bytes: (%s) %r" % (data_len, data)) 49 | datas.append(data) 50 | 51 | if not datas: 52 | self.close() 53 | return None 54 | return "".join(datas) 55 | 56 | def sendBytes(self, data, buffer_size=None): 57 | log_data_exchange = self.server().log_data_exchange 58 | index = 0 59 | while index < len(data): 60 | if buffer_size: 61 | chunk = data[index : index + buffer_size] 62 | else: 63 | chunk = data[index:] 64 | if log_data_exchange: 65 | self.warning("Send bytes: (%s) %r" % (len(chunk), chunk)) 66 | try: 67 | count = self.socket.send(chunk) 68 | index += count 69 | self.tx_bytes += count 70 | except socket_error as err: 71 | self.warning("Send error: %s" % formatError(err)) 72 | self.close() 73 | break 74 | 75 | def close(self, emit_exception=True): 76 | if not self.socket: 77 | return 78 | self.info("Close socket") 79 | self.socket.shutdown(SHUT_RDWR) 80 | self.socket.close() 81 | self.socket = None 82 | if emit_exception: 83 | raise ServerClientDisconnect() 84 | 85 | def destroy(self): 86 | if self.socket: 87 | self.close(False) 88 | 89 | def __str__(self): 90 | return repr(self) 91 | 92 | def __repr__(self): 93 | return "<%s %s>" % ( 94 | self.__class__.__name__, 95 | formatAddress(self.family, self.address), 96 | ) 97 | -------------------------------------------------------------------------------- /fusil/bits.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convert bytes string to integer, and integer to bytes string. 3 | """ 4 | 5 | from itertools import chain, repeat 6 | from struct import calcsize, unpack 7 | from struct import error as struct_error 8 | 9 | BIG_ENDIAN = "ABCD" 10 | LITTLE_ENDIAN = "DCBA" 11 | 12 | 13 | def uint2bytes(value, endian, size=None): 14 | r""" 15 | Convert an unsigned integer to a bytes string in the specified endian. 16 | If size is given, add nul bytes to fill to size bytes. 17 | 18 | >>> uint2bytes(0x1219, BIG_ENDIAN) 19 | '\x12\x19' 20 | >>> uint2bytes(0x1219, BIG_ENDIAN, 4) # 32 bits 21 | '\x00\x00\x12\x19' 22 | >>> uint2bytes(0x1219, LITTLE_ENDIAN, 4) # 32 bits 23 | '\x19\x12\x00\x00' 24 | """ 25 | assert (not size and 0 < value) or (0 <= value) 26 | assert endian in (LITTLE_ENDIAN, BIG_ENDIAN) 27 | text = [] 28 | while value != 0 or text == "": 29 | byte = value % 256 30 | text.append(chr(byte)) 31 | value >>= 8 32 | if size: 33 | need = max(size - len(text), 0) 34 | else: 35 | need = 0 36 | if need: 37 | if endian is BIG_ENDIAN: 38 | text = chain(repeat("\0", need), reversed(text)) 39 | else: 40 | text = chain(text, repeat("\0", need)) 41 | else: 42 | if endian is BIG_ENDIAN: 43 | text = reversed(text) 44 | return "".join(text) 45 | 46 | 47 | def _createStructFormat(): 48 | """ 49 | Create a dictionnary (endian, size_byte) => struct format used 50 | by bytes2uint() to convert raw data to positive integer. 51 | """ 52 | format = { 53 | BIG_ENDIAN: {}, 54 | LITTLE_ENDIAN: {}, 55 | } 56 | for struct_format in "BHILQ": 57 | try: 58 | size = calcsize(struct_format) 59 | format[BIG_ENDIAN][size] = ">%s" % struct_format 60 | format[LITTLE_ENDIAN][size] = "<%s" % struct_format 61 | except struct_error: 62 | pass 63 | return format 64 | 65 | 66 | _struct_format = _createStructFormat() 67 | 68 | 69 | def bytes2uint(data, endian): 70 | r""" 71 | Convert a bytes string into an unsigned integer. 72 | 73 | >>> chr(bytes2uint('*', BIG_ENDIAN)) 74 | '*' 75 | >>> bytes2uint("\x00\x01\x02\x03", BIG_ENDIAN) == 0x10203 76 | True 77 | >>> bytes2uint("\x2a\x10", LITTLE_ENDIAN) == 0x102a 78 | True 79 | >>> bytes2uint("\xff\x14\x2a\x10", BIG_ENDIAN) == 0xff142a10 80 | True 81 | >>> bytes2uint("\x00\x01\x02\x03", LITTLE_ENDIAN) == 0x3020100 82 | True 83 | >>> bytes2uint("\xff\x14\x2a\x10\xab\x00\xd9\x0e", BIG_ENDIAN) == 0xff142a10ab00d90e 84 | True 85 | >>> bytes2uint("\xff\xff\xff\xff\xff\xff\xff\xff", BIG_ENDIAN) == (2**64-1) 86 | True 87 | """ 88 | assert 1 <= len(data) <= 32 # arbitrary limit: 256 bits 89 | try: 90 | return unpack(_struct_format[endian][len(data)], data)[0] 91 | except KeyError: 92 | pass 93 | 94 | assert endian in (BIG_ENDIAN, LITTLE_ENDIAN) 95 | shift = 0 96 | value = 0 97 | if endian is BIG_ENDIAN: 98 | data = reversed(data) 99 | for character in data: 100 | byte = ord(character) 101 | value += byte << shift 102 | shift += 8 103 | return value 104 | -------------------------------------------------------------------------------- /fuzzers/fusil-poppler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | libpoppler fuzzer using "pdftotext" command line program. 4 | """ 5 | 6 | MANGLE = "auto" 7 | USE_TIME = False 8 | USE_STDOUT = True 9 | 10 | from fusil.application import Application 11 | from fusil.process.time_watch import ProcessTimeWatch 12 | from fusil.process.mangle import MangleProcess 13 | from fusil.process.watch import WatchProcess 14 | from fusil.process.stdout import WatchStdout 15 | if MANGLE == "auto": 16 | from fusil.auto_mangle import AutoMangle as MangleFile 17 | elif MANGLE == "incr": 18 | from fusil.incr_mangle import IncrMangle as MangleFile 19 | else: 20 | from fusil.mangle import MangleFile 21 | import re 22 | 23 | class Fuzzer(Application): 24 | NAME = "poppler" 25 | USAGE = "%prog [options] document.pdf" 26 | NB_ARGUMENTS = 1 27 | 28 | def setupProject(self): 29 | project = self.project 30 | 31 | if USE_TIME: 32 | ProcessTimeWatch(project, 33 | too_slow=3.0, too_slow_score=0.10, 34 | too_fast=0.100, too_fast_score=-0.80, 35 | ) 36 | 37 | orig_filename = self.arguments[0] 38 | mangle = MangleFile(project, orig_filename) 39 | if MANGLE == "auto": 40 | mangle.hard_max_op = 1000 41 | elif MANGLE == "incr": 42 | mangle.operation_per_version = 100 43 | mangle.max_version = 50 44 | else: 45 | mangle.config.max_op = 1000 46 | 47 | options = {'timeout': 5.0} 48 | if not USE_STDOUT: 49 | options['stdout'] = 'null' 50 | process = MangleProcess(project, ['pdftotext', '', 'output.txt'], '', **options) 51 | WatchProcess(process, exitcode_score=-0.10) 52 | 53 | if USE_STDOUT: 54 | stdout = WatchStdout(process) 55 | def cleanupLine(line): 56 | match = re.match(r"Error(?: \([0-9]+\))?: (.*)", line) 57 | if match: 58 | line = match.group(1) 59 | return line 60 | stdout.cleanup_func = cleanupLine 61 | del stdout.words['error'] 62 | del stdout.words['unknown'] 63 | 64 | # stdout.show_not_matching = True 65 | # stdout.ignoreRegex(r"Unknown operator 'allocate'$") 66 | # stdout.ignoreRegex(r" operator is wrong type \(error\)$") 67 | # stdout.ignoreRegex(r'^No current point in lineto$') 68 | # stdout.ignoreRegex(r'^No current point in lineto') 69 | # stdout.ignoreRegex(r'^Unknown operator ') 70 | # stdout.ignoreRegex(r"^Couldn't open 'nameToUnicode' file ") 71 | # stdout.ignoreRegex(r"^Illegal character ") 72 | # stdout.ignoreRegex(r"^No font in show$") 73 | # stdout.ignoreRegex(r"^Element of show/space array must be number or string$") 74 | # stdout.ignoreRegex(r"^No current point in curveto$") 75 | # stdout.ignoreRegex(r"^Badly formatted number$") 76 | # stdout.ignoreRegex(r"^Dictionary key must be a name object$") 77 | # stdout.ignoreRegex(r"^End of file inside array$") 78 | # stdout.ignoreRegex(r"^Too few \([0-9]+\) args to .* operator$") 79 | # stdout.ignoreRegex(r"Too many args in content stream") 80 | 81 | stdout.max_nb_line = (100, 0.20) 82 | 83 | if __name__ == "__main__": 84 | Fuzzer().main() 85 | 86 | -------------------------------------------------------------------------------- /tests/python/test_values.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | import ast 5 | 6 | # --- Test Setup: Path Configuration --- 7 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 8 | PROJECT_ROOT = os.path.join(SCRIPT_DIR, '..', '..') 9 | sys.path.insert(0, PROJECT_ROOT) 10 | 11 | try: 12 | from fusil.python.values import INTERESTING, BUFFER_OBJECTS, SURROGATES 13 | VALUES_AVAILABLE = True 14 | except ImportError as e: 15 | print(f"Could not import values module, skipping tests: {e}", file=sys.stderr) 16 | INTERESTING, BUFFER_OBJECTS, SURROGATES = None, None, None 17 | VALUES_AVAILABLE = False 18 | 19 | 20 | @unittest.skipIf(not VALUES_AVAILABLE, "Could not import values module, skipping tests.") 21 | class TestValues(unittest.TestCase): 22 | """ 23 | Test suite for the values.py module. 24 | 25 | Verifies that the constant lists of interesting values and buffer 26 | objects are correctly defined and syntactically valid. 27 | """ 28 | 29 | def test_interesting_values_list(self): 30 | """ 31 | Verifies the INTERESTING list of boundary values and their syntax. 32 | """ 33 | self.assertIsInstance(INTERESTING, tuple) 34 | self.assertGreater(len(INTERESTING), 0) 35 | 36 | # Check for the presence of a few key boundary values 37 | self.assertIn("0", INTERESTING) 38 | self.assertIn('float("-inf")', INTERESTING) 39 | self.assertIn("-2 ** 31", INTERESTING) 40 | self.assertIn("sys.maxsize", INTERESTING) 41 | 42 | # NEW: Verify that every single item in the list is valid Python syntax 43 | for i, expr in enumerate(INTERESTING): 44 | with self.subTest(i=i, expr=expr): 45 | try: 46 | ast.parse(expr) 47 | except SyntaxError as e: 48 | self.fail(f"INTERESTING contains an invalid expression: '{expr}'. Error: {e}") 49 | 50 | def test_buffer_objects_list(self): 51 | """ 52 | Verifies the BUFFER_OBJECTS list of buffer-like expressions. 53 | """ 54 | self.assertIsInstance(BUFFER_OBJECTS, tuple) 55 | self.assertGreater(len(BUFFER_OBJECTS), 0) 56 | 57 | # Check for a representative example 58 | self.assertIn('bytearray(b"test")', BUFFER_OBJECTS) 59 | 60 | # Ensure every expression in the list is syntactically valid 61 | for i, expr in enumerate(BUFFER_OBJECTS): 62 | with self.subTest(i=i, expr=expr): 63 | try: 64 | ast.parse(expr) 65 | except SyntaxError as e: 66 | self.fail(f"BUFFER_OBJECTS contains an invalid expression: '{expr}'. Error: {e}") 67 | 68 | def test_surrogates_constant(self): 69 | """ 70 | Verifies the SURROGATES constant for Unicode surrogates. 71 | """ 72 | self.assertIsInstance(SURROGATES, tuple) 73 | self.assertGreater(len(SURROGATES), 0) 74 | # Verify that all surrogates are strings and are not valid ASCII 75 | for i, expr in enumerate(SURROGATES): 76 | with self.subTest(i=i, expr=expr): 77 | self.assertIsInstance(expr, str) 78 | evaluated = eval(expr) 79 | self.assertFalse(evaluated.isascii() and evaluated != "\x00", f"Surrogate pair string '{expr}' should not be ASCII.") 80 | 81 | 82 | if __name__ == '__main__': 83 | unittest.main() 84 | -------------------------------------------------------------------------------- /fusil/write_code.py: -------------------------------------------------------------------------------- 1 | import re 2 | import textwrap 3 | from os import chmod 4 | from textwrap import dedent, indent 5 | 6 | 7 | class CodeTemplate: 8 | def __init__(self, template_text: str): 9 | # Dedent the template to handle templates defined in indented code 10 | self.template = textwrap.dedent(template_text) 11 | 12 | def render(self_, **kwargs) -> str: 13 | """ 14 | Renders the template by substituting placeholders, handling both 15 | multi-line indented blocks and single-line inline values. 16 | """ 17 | output = self_.template 18 | 19 | # For each key-value pair, perform both block and inline substitutions. 20 | for key, value in kwargs.items(): 21 | placeholder = f"{{{key}}}" 22 | 23 | # 1. BLOCK SUBSTITUTION: 24 | # First, find and replace all occurrences of the placeholder that are 25 | # at the beginning of a line (i.e., need indentation). 26 | block_pattern = re.compile(f"^(?P\\s*){re.escape(placeholder)}", re.MULTILINE) 27 | 28 | def replacer(match): 29 | """A replacer function for re.sub that indents the value.""" 30 | indent_str = match.group('indent') 31 | # Dedent the value to normalize it, then re-indent it to match the placeholder. 32 | return textwrap.indent(textwrap.dedent(str(value)).strip(), indent_str) 33 | 34 | # Perform the substitution for all block-style placeholders. 35 | output = block_pattern.sub(replacer, output) 36 | 37 | # 2. INLINE SUBSTITUTION: 38 | # After handling the blocks, any remaining placeholders must be inline. 39 | # Perform a simple, global string replacement for them. 40 | output = output.replace(placeholder, str(value)) 41 | 42 | return output 43 | 44 | 45 | class WriteCode: 46 | def __init__(self): 47 | self.indent = " " * 4 48 | self.base_level = 0 49 | 50 | def useStream(self, stream): 51 | self.output = stream 52 | 53 | def createFile(self, filename, mode=None): 54 | self.output = open(filename, "w") 55 | if mode: 56 | chmod(filename, mode) 57 | 58 | def close(self): 59 | if not self.output: 60 | return 61 | self.output.close() 62 | self.output = None 63 | 64 | def emptyLine(self): 65 | self.output.write("\n") 66 | 67 | def addLevel(self, delta): 68 | level = self.base_level 69 | self.base_level += delta 70 | if self.base_level < 0: 71 | raise ValueError("Negative indentation level in addLevel()") 72 | return level 73 | 74 | def restoreLevel(self, level): 75 | if level < 0: 76 | raise ValueError("Negative indentation level in restoreLevel()") 77 | self.base_level = level 78 | 79 | def indentLine(self, level, text): 80 | if not isinstance(text, str): 81 | text = str(text, "ASCII") 82 | return self.indent * (self.base_level + level) + text 83 | 84 | def write(self, level, text): 85 | line = self.indentLine(level, text) 86 | self.output.write(line + "\n") 87 | 88 | def write_block(self, level: int, code_block: str): 89 | """ 90 | Writes a multi-line block of code at a specific indentation level. 91 | Correctly handles indentation for the entire block. 92 | """ 93 | for line in dedent(code_block).strip().splitlines(): 94 | self.write(level, line) 95 | -------------------------------------------------------------------------------- /fusil/incr_mangle_op.py: -------------------------------------------------------------------------------- 1 | from array import array 2 | from random import choice, randint 3 | 4 | from fusil.mangle_op import MAX_INCR, SPECIAL_VALUES 5 | from fusil.tools import minmax 6 | 7 | 8 | def createBitOffset(agent, datalen): 9 | min_offset = 0 10 | if agent.min_offset is not None: 11 | min_offset = max(agent.min_offset * 8, min_offset) 12 | max_offset = datalen * 8 - 1 13 | if agent.max_offset is not None: 14 | max_offset = min(agent.max_offset * 8 + 7, max_offset) 15 | return randint(min_offset, max_offset) 16 | 17 | 18 | def createByteOffset(agent, datalen): 19 | min_offset = 0 20 | if agent.min_offset is not None: 21 | min_offset = max(agent.min_offset, min_offset) 22 | max_offset = datalen - 1 23 | if agent.max_offset is not None: 24 | max_offset = min(agent.max_offset, max_offset) 25 | return randint(min_offset, max_offset) 26 | 27 | 28 | class Operation: 29 | def __init__(self, offset, size): 30 | self.offset = offset 31 | self.size = size 32 | 33 | def __call__(self, data): 34 | raise NotImplementedError() 35 | 36 | def __str__(self): 37 | raise NotImplementedError() 38 | 39 | 40 | class InverseBit(Operation): 41 | def __init__(self, agent, datalen): 42 | offset = createBitOffset(agent, datalen) 43 | Operation.__init__(self, offset, 1) 44 | 45 | def __call__(self, data): 46 | mask = 1 << (self.offset & 7) 47 | offset = self.offset >> 3 48 | if data[offset] & mask: 49 | data[offset] &= ~mask & 0xFF 50 | else: 51 | data[offset] |= mask 52 | 53 | def __str__(self): 54 | return "InverseBit(offset=%s.%s)" % (self.offset // 8, self.offset % 8) 55 | 56 | 57 | class ReplaceByte(Operation): 58 | def __init__(self, agent, datalen): 59 | offset = createByteOffset(agent, datalen - 1) 60 | byte = randint(0, 255) 61 | Operation.__init__(self, offset * 8, 8) 62 | self.byte = byte 63 | 64 | def __call__(self, data): 65 | data[self.offset // 8] = self.byte 66 | 67 | def __str__(self): 68 | return "ReplaceByte(byte=0x%02x, offset=%s)" % (self.byte, self.offset // 8) 69 | 70 | 71 | class SpecialValue(Operation): 72 | def __init__(self, agent, datalen): 73 | self.bytes = array("B", choice(SPECIAL_VALUES)) 74 | offset = createByteOffset(agent, datalen - len(self.bytes)) 75 | Operation.__init__(self, offset * 8, len(self.bytes) * 8) 76 | 77 | def __call__(self, data): 78 | offset = self.offset // 8 79 | data[offset : offset + len(self.bytes)] = self.bytes 80 | 81 | def __str__(self): 82 | bytes = " ".join("0x%02x" % byte for byte in self.bytes) 83 | return "SpecialValue(bytes=%r, offset=%s)" % (bytes, self.offset // 8) 84 | 85 | 86 | class Increment(Operation): 87 | def __init__(self, agent, datalen): 88 | self.incr = randint(1, MAX_INCR) 89 | if randint(0, 1) == 1: 90 | self.incr = -self.incr 91 | offset = createByteOffset(agent, datalen - 1) 92 | Operation.__init__(self, offset * 8, 8) 93 | 94 | def __call__(self, data): 95 | offset = self.offset // 8 96 | data[offset] = minmax(0, data[offset] + self.incr, 255) 97 | 98 | def __str__(self): 99 | return "Increment(incr=%+u, offset=%s)" % (self.incr, self.offset // 8) 100 | 101 | 102 | OPERATIONS = (InverseBit, ReplaceByte, SpecialValue, Increment) 103 | -------------------------------------------------------------------------------- /tests/cmd_help/gcc.help: -------------------------------------------------------------------------------- 1 | Usage: gcc [options] file... 2 | Options: 3 | -pass-exit-codes Exit with highest error code from a phase 4 | --help Display this information 5 | --target-help Display target specific command line options 6 | (Use '-v --help' to display command line options of sub-processes) 7 | -dumpspecs Display all of the built in spec strings 8 | -dumpversion Display the version of the compiler 9 | -dumpmachine Display the compiler's target processor 10 | -print-search-dirs Display the directories in the compiler's search path 11 | -print-libgcc-file-name Display the name of the compiler's companion library 12 | -print-file-name= Display the full path to library 13 | -print-prog-name= Display the full path to compiler component 14 | -print-multi-directory Display the root directory for versions of libgcc 15 | -print-multi-lib Display the mapping between command line options and 16 | multiple library search directories 17 | -print-multi-os-directory Display the relative path to OS libraries 18 | -Wa, Pass comma-separated on to the assembler 19 | -Wp, Pass comma-separated on to the preprocessor 20 | -Wl, Pass comma-separated on to the linker 21 | -Xassembler Pass on to the assembler 22 | -Xpreprocessor Pass on to the preprocessor 23 | -Xlinker Pass on to the linker 24 | -combine Pass multiple source files to compiler at once 25 | -save-temps Do not delete intermediate files 26 | -pipe Use pipes rather than intermediate files 27 | -time Time the execution of each subprocess 28 | -specs= Override built-in specs with the contents of 29 | -std= Assume that the input sources are for 30 | --sysroot= Use as the root directory for headers 31 | for headers and libraries 32 | -B Add to the compiler's search paths 33 | -b Run gcc for target , if installed 34 | -V Run gcc version number , if installed 35 | -v Display the programs invoked by the compiler 36 | -### Like -v but options quoted and commands not executed 37 | -E Preprocess only; do not compile, assemble or link 38 | -S Compile only; do not assemble or link 39 | -c Compile and assemble, but do not link 40 | -o Place the output into 41 | -x Specify the language of the following input files 42 | Permissible languages include: c c++ assembler none 43 | 'none' means revert to the default behavior of 44 | guessing the language based on the file's extension 45 | 46 | Options starting with -g, -f, -m, -O, -W, or --param are automatically 47 | passed on to the various sub-processes invoked by gcc. In order to pass 48 | other options on to these processes the -W options must be used. 49 | 50 | For bug reporting instructions, please see: 51 | . 52 | For Debian GNU/Linux specific bug reporting instructions, please see: 53 | . 54 | -------------------------------------------------------------------------------- /fuzzers/fusil-clamav: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | ClamAV anti-virus. 4 | 5 | Supported file formats: 6 | - ZIP, CAB archive 7 | - JPEG 8 | - Windows PE program (.exe) 9 | - HTML 10 | """ 11 | 12 | NB_FILES = 10 13 | MAX_MUTATIONS = 100 14 | MAX_MEMORY = 100*1024*1024 15 | 16 | from fusil.application import Application 17 | from optparse import OptionGroup 18 | from fusil.process.create import CreateProcess 19 | from fusil.process.watch import WatchProcess 20 | from fusil.process.attach import AttachProcess 21 | from fusil.process.stdout import WatchStdout 22 | from fusil.auto_mangle import AutoMangle 23 | from fusil.file_watch import FileWatch 24 | 25 | class Fuzzer(Application): 26 | NAME = "clamav" 27 | USAGE = "%prog [options] filename" 28 | NB_ARGUMENTS = 1 29 | 30 | def createFuzzerOptions(self, parser): 31 | options = OptionGroup(parser, "ClamAV") 32 | options.add_option("--use-clamd", help="Use the ClamAV daemon (clamd)", 33 | action="store_true") 34 | options.add_option("--change-filesize", help="Allow mutation to change file size", 35 | action="store_true", default=False) 36 | options.add_option("--nb-files", help="Number of generated files (default: %s)" % NB_FILES, 37 | type="int", default=NB_FILES) 38 | options.add_option("--max-mutations", help="Maximum number of mutations (default: %s)" % MAX_MUTATIONS, 39 | type="int", default=MAX_MUTATIONS) 40 | options.add_option("--max-memory", help="Maximum clamd server memory in bytes (default: %s)" % MAX_MEMORY, 41 | type="int", default=MAX_MEMORY) 42 | return options 43 | 44 | def setupProject(self): 45 | project = self.project 46 | 47 | if self.options.use_clamd: 48 | PROGRAM = 'clamdscan' 49 | else: 50 | PROGRAM = 'clamscan' 51 | 52 | orig_filename = self.arguments[0] 53 | 54 | mangle = AutoMangle(project, orig_filename, self.options.nb_files) 55 | mangle.config.max_op = self.options.max_mutations 56 | mangle.config.change_size = self.options.change_filesize 57 | 58 | # Watch clamd server 59 | if self.options.use_clamd: 60 | clamd = AttachProcess(project, 'clamd') 61 | clamd.max_memory = self.options.max_memory 62 | 63 | process = ClamavProcess(project, [PROGRAM], timeout=100.0) 64 | process.max_memory = self.options.max_memory 65 | WatchProcess(process, exitcode_score=0.10) 66 | stdout = WatchStdout(process) 67 | stdout.max_nb_line = (50+self.options.nb_files, 1.0) 68 | stdout.addRegex(r"Can't connect to clamd", 1.0) 69 | 70 | logs = [stdout] 71 | if self.options.use_clamd: 72 | log = FileWatch.fromFilename(project, 73 | '/var/log/clamav/clamav.log', start="end") 74 | log.max_nb_line = None 75 | logs.append(log) 76 | 77 | for log in logs: 78 | log.ignoreRegex(r"\*\*\* DON'T PANIC!") 79 | log.ignoreRegex('SCAN SUMMARY') 80 | log.ignoreRegex(': OK$') 81 | log.ignoreRegex('^Infected files: 0$') 82 | log.ignoreRegex('^Time: ') 83 | log.addRegex(' FOUND$', 0.05) 84 | del log.words['error'] 85 | log.show_matching = True 86 | log.show_not_matching = True 87 | 88 | class ClamavProcess(CreateProcess): 89 | def on_mangle_filenames(self, new_files): 90 | self.cmdline.arguments = self.cmdline.arguments[:1] + new_files 91 | self.createProcess() 92 | 93 | if __name__ == "__main__": 94 | Fuzzer().main() 95 | 96 | -------------------------------------------------------------------------------- /jit_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | import argparse 4 | from pathlib import Path 5 | 6 | # Dictionary mapping file paths (relative to CPYTHON_SRC_PATH) to the changes. 7 | JIT_TWEAKS = { 8 | "Include/internal/pycore_backoff.h": [ 9 | # (Parameter Name, New Value) 10 | ("JUMP_BACKWARD_INITIAL_VALUE", 63), 11 | ("JUMP_BACKWARD_INITIAL_BACKOFF", 6), 12 | ("SIDE_EXIT_INITIAL_VALUE", 63), 13 | ("SIDE_EXIT_INITIAL_BACKOFF", 6), 14 | # Add other parameters from this file... 15 | ], 16 | "Include/internal/pycore_optimizer.h": [ 17 | ("MAX_CHAIN_DEPTH", 8), 18 | ("UOP_MAX_TRACE_LENGTH", 1600), 19 | ("TRACE_STACK_SIZE", 10), 20 | ("MAX_ABSTRACT_INTERP_SIZE", 8192), 21 | ("JIT_CLEANUP_THRESHOLD", 150000), 22 | # Add other parameters... 23 | ] 24 | # Add other files and parameters as needed 25 | } 26 | 27 | 28 | def apply_jit_tweaks(cpython_path: Path, dry_run: bool = False): 29 | """ 30 | Finds and replaces CPython JIT parameters using regular expressions. 31 | """ 32 | print(f"[*] Starting JIT parameter tweaks for CPython at: {cpython_path.resolve()}") 33 | 34 | if not cpython_path.is_dir(): 35 | print(f"[!] Error: CPython source directory not found at '{cpython_path}'") 36 | return 37 | 38 | for rel_path, tweaks in JIT_TWEAKS.items(): 39 | file_path = cpython_path / rel_path 40 | if not file_path.exists(): 41 | print(f"[-] Warning: File not found, skipping: {file_path}") 42 | continue 43 | 44 | print(f"[*] Processing file: {file_path}") 45 | try: 46 | content = file_path.read_text() 47 | original_content = content 48 | 49 | for param_name, new_value in tweaks: 50 | # This regex looks for a line starting with #define, followed by the 51 | # parameter name, and then one or more digits. It's not tied to line numbers. 52 | # It captures the part before the number to preserve whitespace. 53 | pattern = re.compile(rf"^(#define\s+{param_name}\s+)\d+", re.MULTILINE) 54 | 55 | # The replacement string uses the captured group `\g<1>` 56 | replacement = rf"\g<1>{new_value}" 57 | 58 | content, num_subs = pattern.subn(replacement, content) 59 | 60 | if num_subs > 0: 61 | print(f" - Changed '{param_name}' to '{new_value}'") 62 | else: 63 | print(f" - Warning: Could not find and replace '{param_name}'") 64 | 65 | if content != original_content and not dry_run: 66 | print(f"[*] Writing changes to: {file_path}") 67 | file_path.write_text(content) 68 | elif dry_run: 69 | print(f"[*] Dry run: Changes for {file_path} were not written.") 70 | 71 | 72 | except Exception as e: 73 | print(f"[!] Error processing {file_path}: {e}") 74 | 75 | 76 | if __name__ == "__main__": 77 | parser = argparse.ArgumentParser( 78 | description="Apply aggressive JIT settings to the CPython source code." 79 | ) 80 | parser.add_argument( 81 | "cpython_dir", 82 | type=str, 83 | help="Path to the root of the CPython source repository.", 84 | ) 85 | parser.add_argument( 86 | "--dry-run", 87 | action="store_true", 88 | help="Print changes without modifying files.", 89 | ) 90 | args = parser.parse_args() 91 | 92 | CPYTHON_SRC_PATH = Path(args.cpython_dir) 93 | apply_jit_tweaks(CPYTHON_SRC_PATH, args.dry_run) 94 | print("[*] Done.") 95 | -------------------------------------------------------------------------------- /tests/cmd_help_parser.rst: -------------------------------------------------------------------------------- 1 | Setup tests 2 | =========== 3 | 4 | >>> from os.path import join as path_join 5 | >>> def openTest(name): 6 | ... filename = path_join('tests', 'cmd_help', name) 7 | ... return open(filename) 8 | ... 9 | >>> from fusil.cmd_help_parser import CommandHelpParser 10 | >>> from StringIO import StringIO 11 | >>> def testcase(program): 12 | ... stdout = openTest(program + '.help') 13 | ... parser = CommandHelpParser(program) 14 | ... parser.parseFile(stdout) 15 | ... for option in parser.options: 16 | ... print option 17 | ... 18 | 19 | Test identify 20 | ============= 21 | 22 | >>> testcase('identify') 23 | -authenticate ARG1 24 | -channel ARG1 25 | -crop ARG1 26 | -debug ARG1 27 | -define ARG1 28 | -density ARG1 29 | -depth ARG1 30 | -extract ARG1 31 | -format "ARG1" 32 | -fuzz ARG1 33 | -help 34 | -interlace ARG1 35 | -limit ARG1 ARG2 36 | -list ARG1 37 | -log ARG1 38 | -matte 39 | -monitor 40 | -ping 41 | -quiet 42 | -sampling-factor ARG1 43 | -set ARG1 ARG2 44 | -size ARG1 45 | -strip 46 | -units ARG1 47 | -verbose 48 | -version 49 | -virtual-pixel ARG1 50 | 51 | Test gcc 52 | ======== 53 | 54 | >>> testcase('gcc') 55 | -pass-exit-codes 56 | --help 57 | --target-help 58 | -dumpspecs 59 | -dumpversion 60 | -dumpmachine 61 | -print-search-dirs 62 | -print-libgcc-file-name 63 | -print-file-name=ARG1 64 | -print-prog-name=ARG1 65 | -print-multi-directory 66 | -print-multi-lib 67 | -print-multi-os-directory ARG1 ARG2 68 | -Wa,ARG1 69 | -Wp,ARG1 70 | -Wl,ARG1 71 | -Xassembler ARG1 72 | -Xpreprocessor ARG1 73 | -Xlinker ARG1 74 | -combine 75 | -save-temps 76 | -pipe 77 | -time 78 | -specs=ARG1 79 | -std=ARG1 80 | --sysroot=ARG1 81 | -B ARG1 82 | -b ARG1 83 | -V ARG1 84 | -v 85 | -E 86 | -S 87 | -c 88 | -o ARG1 89 | -x ARG1 90 | 91 | 92 | Test ls 93 | ======= 94 | 95 | >>> testcase('ls') 96 | -a 97 | --all 98 | -A 99 | --almost-all 100 | --author 101 | -b 102 | --escape 103 | --block-size=ARG1 104 | -B 105 | --ignore-backups 106 | -c 107 | -C 108 | --color=ARG1 109 | -d 110 | --directory 111 | -D 112 | --dired 113 | -f 114 | -F 115 | --classify 116 | --file-type 117 | --format=ARG1 118 | --full-time 119 | -g 120 | -G 121 | --no-group 122 | -h 123 | --human-readable 124 | --si 125 | -H 126 | --dereference-command-line 127 | --dereference-command-line-symlink-to-dir 128 | --hide=ARG1 129 | --indicator-style=ARG1 130 | -i 131 | --inode 132 | -I ARG1 133 | --ignore=ARG1 134 | -k 135 | -l 136 | -L 137 | --dereference 138 | -m 139 | -n 140 | --numeric-uid-gid 141 | -N 142 | --literal 143 | -o 144 | -p ARG1 145 | -q 146 | --hide-control-chars 147 | --show-control-chars 148 | -Q 149 | --quote-name 150 | --quoting-style=ARG1 151 | -r 152 | --reverse 153 | -R 154 | --recursive 155 | -s 156 | --size 157 | -S 158 | --sort=ARG1 159 | --time=ARG1 160 | --time-style=ARG1 161 | -t 162 | -T ARG1 163 | --tabsize=ARG1 164 | -u 165 | -U 166 | -v 167 | -w ARG1 168 | --width=ARG1 169 | -x 170 | -X 171 | -1 172 | --lcontext 173 | -Z 174 | --context 175 | --scontext 176 | --help 177 | --version 178 | 179 | Test ping 180 | ========= 181 | 182 | >>> testcase('ping') 183 | -L 184 | -R 185 | -U 186 | -b 187 | -d 188 | -f 189 | -n 190 | -q 191 | -r 192 | -v 193 | -V 194 | -a 195 | -A 196 | -c ARG1 197 | -i ARG1 198 | -w ARG1 199 | -p ARG1 200 | -s ARG1 201 | -t ARG1 202 | -I ARG1 ARG2 ARG3 203 | -M ARG1 ARG2 ARG3 204 | -S ARG1 205 | -T ARG1 ARG2 206 | -Q ARG1 207 | 208 | Test python 209 | ========= 210 | 211 | >>> testcase('python') 212 | -c ARG1 213 | -m ARG1 214 | -d 215 | -E 216 | -h 217 | -i 218 | -O 219 | -Q ARG1 220 | -S 221 | -t 222 | -u 223 | -v 224 | -V 225 | -W ARG1 226 | -x 227 | 228 | -------------------------------------------------------------------------------- /doc/c_tools.rst: -------------------------------------------------------------------------------- 1 | Tools for C code manipulation 2 | ============================= 3 | 4 | The fusil.c_tools module contains many tools to manipulation C code. 5 | 6 | String manipulation 7 | ------------------- 8 | 9 | >>> from fusil.c_tools import quoteString, encodeUTF32 10 | >>> quoteString('Hello World\n\0') 11 | '"Hello World\\n\\0"' 12 | >>> encodeUTF32("Hello") 13 | 'H\x00\x00\x00e\x00\x00\x00l\x00\x00\x00l\x00\x00\x00o\x00\x00\x00' 14 | 15 | encodeUTF32() use host endian. 16 | 17 | Generate C script 18 | ----------------- 19 | 20 | Hello World! 21 | ++++++++++++ 22 | 23 | >>> from fusil.c_tools import CodeC 24 | >>> from sys import stdout 25 | >>> hello = CodeC() 26 | >>> hello.includes.append('') 27 | >>> main = hello.addMain() 28 | >>> main.callFunction('printf', [quoteString("Hello World\n")]) 29 | >>> hello.useStream(stdout) 30 | >>> hello.writeCode() 31 | #include 32 | 33 | int main() { 34 | printf( 35 | "Hello World\n" 36 | ); 37 | 38 | return 0; 39 | } 40 | 41 | 42 | FunctionC 43 | +++++++++ 44 | 45 | addMain() is an helper to create main() function, but you can write your own 46 | functions with addFunction() method: 47 | 48 | >>> from fusil.c_tools import FunctionC 49 | >>> testcode = CodeC() 50 | >>> test = testcode.addFunction( FunctionC('test', type='int') ) 51 | >>> test.variables.append('int x') 52 | >>> test.add('x = 1+1') 53 | >>> test.add('return x') 54 | >>> testcode.useStream(stdout) 55 | >>> testcode.writeCode() 56 | int test() { 57 | int x; 58 | 59 | x = 1+1; 60 | return x; 61 | } 62 | 63 | 64 | You can get the function with: 65 | 66 | >>> hello['main'] 67 | 68 | >>> testcode['test'] 69 | 70 | 71 | Write to a file 72 | =============== 73 | 74 | To write a code to a file, use writeIntoFile() method: 75 | 76 | >>> from pprint import pprint 77 | >>> hello.writeIntoFile('hello.c') 78 | >>> pprint(open('hello.c').readlines()) 79 | ['#include \n', 80 | '\n', 81 | 'int main() {\n', 82 | ' printf(\n', 83 | ' "Hello World\\n"\n', 84 | ' );\n', 85 | '\n', 86 | ' return 0;\n', 87 | '}\n', 88 | '\n'] 89 | 90 | Compile the code 91 | ================== 92 | 93 | To compile the code, use compile() method: 94 | 95 | >>> from os import system, WEXITSTATUS, unlink 96 | >>> from fusil.mockup import Logger 97 | >>> logger = Logger() 98 | >>> hello = CodeC() 99 | >>> main = hello.addMain(footer='return 2*3*7;') 100 | >>> hello.compile(logger, 'hello.c', 'hello') 101 | >>> WEXITSTATUS(system('./hello')) 102 | 42 103 | >>> unlink('hello.c') 104 | >>> unlink('hello') 105 | 106 | Misc attributes 107 | =============== 108 | 109 | You can customize write() output: 110 | 111 | * 'indent' is the indententation string (default: 4 spaces) 112 | * 'eol' is the end of line string (default: "\n") 113 | 114 | Set gnu_source to True to get:: 115 | 116 | #define _GNU_SOURCE 117 | 118 | FuzzyFunctionC 119 | ============== 120 | 121 | Ok, let's play with fuzzing! FuzzFunctionC has methods to generate values. 122 | 123 | >>> from fusil.c_tools import FuzzyFunctionC 124 | >>> fuzzy = CodeC() 125 | >>> main = fuzzy.addFunction(FuzzyFunctionC('main', type='int')) 126 | 127 | Methods to generate data: 128 | 129 | * createInt32() 130 | * createInt() 131 | * createString() 132 | * createRandomBytes() 133 | 134 | Example: 135 | 136 | >>> main.add('return %s' % main.createInt()) 137 | 138 | -------------------------------------------------------------------------------- /fusil/python/samples/tricky_objects.py: -------------------------------------------------------------------------------- 1 | import types 2 | import inspect 3 | import itertools 4 | tricky_cell = types.CellType(None) 5 | tricky_simplenamespace = types.SimpleNamespace(dummy=None, cell=tricky_cell) 6 | tricky_simplenamespace.dummy = tricky_simplenamespace 7 | tricky_capsule = types.CapsuleType 8 | tricky_module = types.ModuleType("tricky_module", "docs") 9 | tricky_module2 = types.ModuleType("tricky_module2\\x00", "docs\\x00") 10 | try: 11 | tricky_genericalias = types.GenericAlias(list, (int,)) 12 | except AttributeError: 13 | tricky_genericalias = None 14 | 15 | tricky_dict = {} 16 | if tricky_capsule: tricky_dict[tricky_capsule] = tricky_cell 17 | if tricky_module: tricky_dict[tricky_module] = tricky_genericalias 18 | tricky_dict["tricky_dict"] = tricky_dict 19 | tricky_mappingproxy = types.MappingProxyType(tricky_dict) 20 | 21 | 22 | def tricky_function(*args, **kwargs): 23 | if len(args) > 150: raise RecursionError("Fuzzer controlled depth") 24 | a = 1 25 | def b(x=a): 26 | v = x 27 | return v 28 | return tricky_function(*(args + (1,)), **kwargs) 29 | 30 | 31 | tricky_lambda = lambda *args, **kwargs: tricky_lambda(*args, **kwargs) 32 | tricky_classmethod = classmethod(tricky_lambda) 33 | tricky_staticmethod = staticmethod(tricky_lambda) 34 | tricky_property = property(tricky_lambda) 35 | tricky_code = tricky_lambda.__code__ 36 | tricky_closure = tricky_function.__code__.co_freevars 37 | tricky_classmethod_descriptor = types.ClassMethodDescriptorType # This is the type itself 38 | 39 | 40 | class TrickyDescriptor: 41 | def __get__(self, obj, objtype=None): 42 | return self 43 | def __set__(self, obj, value): 44 | try: 45 | obj.__dict__["_value_descriptor"] = value 46 | except AttributeError: 47 | pass 48 | def __delete__(self, obj): 49 | try: 50 | del obj.__dict__["_value_descriptor"] 51 | except (AttributeError, KeyError): 52 | pass 53 | 54 | 55 | class TrickyMeta(type): 56 | @property 57 | def __signature__(self): 58 | raise AttributeError("Signature denied by TrickyMeta") 59 | def __mro_entries__(self, bases): 60 | return (object,) 61 | #return super().__mro_entries__(bases) 62 | 63 | 64 | class TrickyClass(metaclass=TrickyMeta): 65 | tricky_descriptor = TrickyDescriptor() 66 | 67 | def __new__(cls, *args, **kwargs): 68 | return super().__new__(cls) 69 | 70 | def __init__(self, *args, **kwargs): 71 | self._value_init = None 72 | 73 | def __getattr__(self, name): 74 | if name == "crash_on_getattr": raise ValueError("getattr manipulated") 75 | return self 76 | 77 | 78 | tricky_instance = TrickyClass() 79 | try: 80 | tricky_frame = inspect.currentframe() 81 | if tricky_frame: # currentframe() can be None 82 | # tricky_frame.f_builtins.update(tricky_dict) 83 | tricky_frame.f_globals.update(tricky_dict) 84 | tricky_frame.f_locals.update(tricky_dict) 85 | except RuntimeError: 86 | tricky_frame = None 87 | 88 | 89 | try: 90 | 1 / 0 91 | except ZeroDivisionError as e: 92 | tricky_traceback = e.__traceback__ 93 | else: 94 | tricky_traceback = None 95 | 96 | 97 | # tricky_generator = (x for x in itertools.count()) 98 | tricky_list_with_cycle = [[]] * 6 + [] 99 | tricky_list_with_cycle[0].append(tricky_list_with_cycle) 100 | tricky_list_with_cycle[-1].append(tricky_list_with_cycle) 101 | tricky_list_with_cycle.append(tricky_list_with_cycle) 102 | if tricky_list_with_cycle[0] and tricky_list_with_cycle[0][0] is tricky_list_with_cycle: 103 | tricky_list_with_cycle[0][0].append(tricky_list_with_cycle) 104 | -------------------------------------------------------------------------------- /fusil/directory.py: -------------------------------------------------------------------------------- 1 | import grp 2 | import pwd 3 | import resource 4 | from os import chmod, chown, mkdir, scandir, umask 5 | from os.path import basename 6 | from os.path import exists as path_exists 7 | from os.path import join as path_join 8 | from shutil import rmtree 9 | from sys import getfilesystemencoding 10 | 11 | 12 | class Directory: 13 | def __init__(self, directory): 14 | self.directory = directory 15 | # Filenames generated by uniqueFilename() method 16 | self.files = set() 17 | 18 | def ignore(self, filename): 19 | try: 20 | self.files.remove(filename) 21 | except KeyError: 22 | pass 23 | 24 | def mkdir(self, change_owner=True): 25 | old_umask = umask(0) 26 | mkdir(self.directory, 0o777) 27 | if change_owner: 28 | try: 29 | uid = pwd.getpwnam("fusil").pw_uid 30 | gid = grp.getgrnam("fusil").gr_gid 31 | chown(self.directory, uid, gid) 32 | except Exception as e: 33 | print(e) 34 | umask(old_umask) 35 | 36 | def isEmpty(self, ignore_generated=False): 37 | try: 38 | entries = scandir(self.directory) 39 | for entry in entries: 40 | if entry.name in (".", ".."): 41 | continue 42 | if entry.name in self.files and ignore_generated: 43 | continue 44 | entries.close() 45 | return False 46 | return True 47 | except OSError as e: 48 | print(e) 49 | print(resource.getrusage(resource.RUSAGE_SELF)) 50 | return False 51 | 52 | def rmtree(self): 53 | filename = self.directory 54 | if isinstance(filename, str): 55 | # Convert to byte strings because rmtree() doesn't support mixing 56 | # byte and unicode strings 57 | charset = getfilesystemencoding() 58 | filename = filename.encode(charset) 59 | rmtree(filename, onerror=self.rmtree_error) 60 | 61 | def rmtree_error(self, operation, argument, stack): 62 | # Try to change file permission (allow write) and retry 63 | try: 64 | chmod(argument, 0o777) 65 | except OSError: 66 | pass 67 | operation(argument) 68 | 69 | def uniqueFilename(self, name, count=None, count_format="%d", save=True): 70 | # Test with no count suffix 71 | name = basename(name) 72 | if not name: 73 | raise ValueError("Empty filename") 74 | if count is None and not self._exists(name): 75 | if save: 76 | self.files.add(name) 77 | return path_join(self.directory, name) 78 | 79 | # Create filename pattern: "archive.tar.gz" => "archive-%04u.tar.gz" 80 | name_pattern = name.split(".", 1) 81 | if count is None: 82 | count = 2 83 | count_format = "-" + count_format 84 | if 1 < len(name_pattern): 85 | name_pattern = name_pattern[0] + count_format + "." + name_pattern[1] 86 | else: 87 | name_pattern = name_pattern[0] + count_format 88 | 89 | # Try names and increment count at each step 90 | while True: 91 | name = name_pattern % count 92 | if not self._exists(name): 93 | if save: 94 | self.files.add(name) 95 | return path_join(self.directory, name) 96 | count += 1 97 | 98 | def _exists(self, name): 99 | if name in self.files: 100 | return True 101 | filename = path_join(self.directory, name) 102 | return path_exists(filename) 103 | -------------------------------------------------------------------------------- /fuzzers/fusil-gimp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Gimp fuzzer. 4 | """ 5 | 6 | from __future__ import print_function, with_statement 7 | from fusil.application import Application 8 | from optparse import OptionGroup 9 | from fusil.process.create import CreateProcess 10 | from fusil.process.watch import WatchProcess 11 | from fusil.process.stdout import WatchStdout 12 | from fusil.auto_mangle import AutoMangle 13 | from fusil.dummy_mangle import DummyMangle 14 | from os.path import basename 15 | 16 | PROGRAM = 'gimp' 17 | NB_FILES = 25 18 | MAX_FILESIZE = 1024*1024 19 | 20 | class Fuzzer(Application): 21 | NAME = "gimp" 22 | USAGE = "%prog [options] image1 [image2 ...]" 23 | NB_ARGUMENTS = (1, None) 24 | 25 | def createFuzzerOptions(self, parser): 26 | options = OptionGroup(parser, "Gimp") 27 | options.add_option("--nb-files", help="Number of generated files (default: %s)" % NB_FILES, 28 | type="int", default=NB_FILES) 29 | options.add_option("--program", help="Gimp program path (default: %s)" % PROGRAM, 30 | type="str", default=PROGRAM) 31 | options.add_option("--filesize", help="Maximum file size in bytes (default: %s)" % MAX_FILESIZE, 32 | type="int", default=MAX_FILESIZE) 33 | options.add_option("--test", help="Test mode (no fuzzing, just make sure that the fuzzer works)", 34 | action="store_true") 35 | return options 36 | 37 | def setupProject(self): 38 | if self.options.test: 39 | DummyMangle(self.project, self.arguments) 40 | else: 41 | mangle = AutoMangle(self.project, self.arguments, self.options.nb_files) 42 | mangle.max_size = self.options.filesize 43 | 44 | # Create the process 45 | arguments = [self.options.program, 46 | '--no-interface', 47 | # '--verbose', 48 | '--batch-interpreter', 'plug-in-script-fu-eval', 49 | '--batch', '-'] 50 | process = GimpProcess(self.project, arguments) 51 | WatchProcess(process) 52 | stdout = WatchStdout(process) 53 | stdout.ignoreRegex('fatal parse error') 54 | # > Error: Procedure execution of gimp-file-load failed: This XCF file is corrupt! 55 | # I could not even salvage any partial image data from it. 56 | stdout.ignoreRegex('file is corrupt') 57 | stdout.max_nb_line = None 58 | del stdout.words['warning'] 59 | del stdout.words['error'] 60 | 61 | class GimpProcess(CreateProcess): 62 | def init(self): 63 | CreateProcess.init(self) 64 | self.script_filename = None 65 | 66 | def on_mangle_filenames(self, filenames): 67 | self.script_filename = self.session().createFilename("script") 68 | filenames_str = ' '.join('"%s"' % basename(filename) for filename in filenames) 69 | with open(self.script_filename, "w") as fp: 70 | print('(gimp-message "Start Gimp fuzzer")', file=fp) 71 | print('(define (fuzzfiles n f)', file=fp) 72 | print(' (let* (', file=fp) 73 | print(' (fname (car f))', file=fp) 74 | print(' (img 0)', file=fp) 75 | print(' )', file=fp) 76 | print(' (gimp-message fname)', file=fp) 77 | print(' (set! img (car (gimp-file-load RUN-NONINTERACTIVE fname fname)))', file=fp) 78 | print(' (gimp-image-delete img)', file=fp) 79 | print(' (if (= n 1) 1 (fuzzfiles (- n 1) (cdr f)))', file=fp) 80 | print(' )', file=fp) 81 | print(')', file=fp) 82 | print("(fuzzfiles %s '(%s))" % (len(filenames), filenames_str), file=fp) 83 | print('(gimp-quit 0)', file=fp) 84 | self.createProcess() 85 | 86 | def createStdin(self): 87 | return open(self.script_filename, 'rb') 88 | 89 | if __name__ == "__main__": 90 | Fuzzer().main() 91 | 92 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Fusil is back, sorta 2 | ==================== 3 | 4 | This is a republishing of Victor Stinner's fusil project. It's probable 5 | that much of the code doesn't work, as only the Python fuzzing code is 6 | being tested and worked on. While some development is planned for 7 | fuzzing Python, many other aspects of the library and other fuzzers 8 | are currently out of scope for this repository. 9 | 10 | 11 | However, code contributions to any parts of fusil will be accepted. Just 12 | don't expect that new features will be worked on absent corresponding 13 | code. 14 | 15 | Many links in the docs don't work, but some can be retrieved using the 16 | WayBack Machine. We'll probably update them sometime. 17 | 18 | ------------------------------------------------------------ 19 | 20 | Fusil is a Python library used to write fuzzing programs. It helps to start 21 | process with a prepared environment (limit memory, environment variables, 22 | redirect stdout, etc.), start network client or server, and create mangled 23 | files. Fusil has many probes to detect program crash: watch process exit code, 24 | watch process stdout and syslog for text patterns (eg. "segmentation fault"), 25 | watch session duration, watch cpu usage (process and system load), etc. 26 | 27 | Fusil is based on a multi-agent system architecture. It computes a session 28 | score used to guess fuzzing parameters like number of injected errors to input 29 | files. 30 | 31 | Available fuzzing projects: ClamAV, Firefox (contains an HTTP server), 32 | gettext, gstreamer, identify, libc_env, libc_printf, libexif, linux_syscall, 33 | mplayer, php, poppler, vim, xterm. 34 | 35 | Website: http://bitbucket.org/haypo/fusil/wiki/Home 36 | 37 | 38 | Usage 39 | ===== 40 | 41 | Fusil is a library and a set of fuzzers called "fusil-...". To run a fuzzer, 42 | call it by its name. Example: :: 43 | 44 | $ fusil-gettext 45 | Fusil version 0.9.1 -- GNU GPL v2 46 | http://bitbucket.org/haypo/fusil/wiki/Home 47 | (...) 48 | [0][session 13] Start session 49 | [0][session 13] ------------------------------------------------------------ 50 | [0][session 13] PID: 16989 51 | [0][session 13] Signal: SIGSEGV 52 | [0][session 13] Invalid read from 0x0c1086e0 53 | [0][session 13] - instruction: CMP EDX, [EAX] 54 | [0][session 13] - mapping: 0x0c1086e0 is not mapped in memory 55 | [0][session 13] - register eax=0x0c1086e0 56 | [0][session 13] - register edx=0x00000019 57 | [0][session 13] ------------------------------------------------------------ 58 | [0][session 13] End of session: score=100.0%, duration=3.806 second 59 | (...) 60 | Success 1/1! 61 | Project done: 13 sessions in 5.4 seconds (414.5 ms per session), total 5.9 seconds, aggresssivity: 19.0% 62 | Total: 1 success 63 | Keep non-empty directory: /home/haypo/prog/SVN/fusil/trunk/run-3 64 | 65 | 66 | Features 67 | ======== 68 | 69 | Why using Fusil instead your own hand made C script? 70 | 71 | * Fusil limits child process environment: limit memory, use timeout, make 72 | sure that process is killed on session end 73 | * Fusil waits until system load is load before starting a fuzzing session 74 | * Fusil creates a session directory used as the process current working 75 | directory and Fusil only creates files in this directory (and not in /tmp) 76 | * Fusil stores all actions in fusil.log but also session.log for all 77 | actions related of a session 78 | * Fusil has multiple available probes to compute session score: guess if 79 | a sessions is a succes or not 80 | * Fusil redirects process output to a file and searchs bug text patterns 81 | in the stdout/stderr (Fusil contains many text patterns to detect crashes 82 | and problems) 83 | 84 | 85 | Installation 86 | ============ 87 | 88 | Read INSTALL documentation file. 89 | 90 | 91 | Documentation 92 | ============= 93 | 94 | Read doc/index.rst: documentation index. 95 | 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | 3 | ### Python template 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | .idea/.gitignore 168 | .idea/fusil.iml 169 | .idea/misc.xml 170 | .idea/modules.xml 171 | .idea/vcs.xml 172 | .idea/inspectionProfiles/profiles_settings.xml 173 | -------------------------------------------------------------------------------- /fusil/process/prepare.py: -------------------------------------------------------------------------------- 1 | import grp 2 | import pwd 3 | from errno import EACCES 4 | from os import X_OK, access, chdir 5 | from shutil import chown 6 | 7 | from fusil.process.tools import allowCoreDump, beNice, limitMemory, limitUserProcess 8 | from fusil.unsafe import permissionHelp 9 | 10 | from os import getuid, setgid, setuid 11 | from pwd import getpwuid 12 | 13 | 14 | class ChildError(Exception): 15 | # Exception raised after the fork(), in prepareProcess() 16 | pass 17 | 18 | 19 | def prepareProcess(process): 20 | from sys import stderr 21 | 22 | print(f"USER {getuid()}", file=stderr) 23 | project = process.project() 24 | config = project.config 25 | options = process.application().options 26 | 27 | # Trace the new process 28 | process.debugger.traceme() 29 | # Set current working directory 30 | directory = process.getWorkingDirectory() 31 | try: 32 | uid = pwd.getpwnam("fusil").pw_uid 33 | gid = grp.getgrnam("fusil").gr_gid 34 | chown(directory, uid, gid) 35 | except Exception as e: 36 | print(e) 37 | # Change the user and group 38 | try: 39 | changeUserGroup(config, options) 40 | except Exception as e: 41 | print(e) 42 | 43 | try: 44 | chdir(directory) 45 | except OSError as err: 46 | print(f"CHDIR ERROR: {err}", file=stderr) 47 | print(f"Make sure the whole path is accessible to user 'fusil' (chmod +xr path_part).", file=stderr) 48 | if err.errno != EACCES: 49 | raise 50 | user = getuid() 51 | user = getpwuid(user).pw_name 52 | message = "The user %s is not allowed enter directory to %s" % (user, directory) 53 | help = permissionHelp(options) 54 | if help: 55 | message += " (%s)" % help 56 | print(message, file=stderr) 57 | raise ChildError(message) 58 | 59 | # Make sure that the program is executable by the current user 60 | program = process.current_arguments[0] 61 | # if not access(program, X_OK): 62 | if 0: 63 | user = getuid() 64 | user = getpwuid(user).pw_name 65 | message = "The user %s is not allowed to execute the file %s" % (user, program) 66 | help = permissionHelp(options) 67 | if help: 68 | message += " (%s)" % help 69 | print(message, file=stderr) 70 | raise ChildError(message) 71 | 72 | # Limit process resources 73 | if 0: 74 | limitResources(process, config, options) 75 | 76 | 77 | def limitResources(process, config, options): 78 | # Change process priority to be nice 79 | if not options.fast: 80 | beNice() 81 | 82 | # Set process priority to nice and limit memory 83 | if 0 < process.max_memory: 84 | limitMemory(process.max_memory, hard=True) 85 | elif 0 < config.fusil_max_memory: 86 | # Reset Fusil process memory limit 87 | limitMemory(-1) 88 | if process.core_dump: 89 | allowCoreDump(hard=True) 90 | if config.process_user and (0 < process.max_user_process): 91 | limitUserProcess(process.max_user_process, hard=True) 92 | 93 | 94 | def changeUserGroup(config, options): 95 | # Change group? 96 | gid = config.process_gid 97 | errors = [] 98 | if gid is not None: 99 | try: 100 | setgid(gid) 101 | except OSError: 102 | errors.append("group to %s" % gid) 103 | except Exception as e: 104 | print(e) 105 | raise 106 | 107 | # Change user? 108 | uid = config.process_uid 109 | if uid is not None: 110 | try: 111 | setuid(uid) 112 | except OSError: 113 | errors.append("user to %s" % uid) 114 | except Exception as e: 115 | print(e) 116 | raise 117 | if not errors: 118 | return 119 | 120 | # On error: propose some help 121 | help = permissionHelp(options) 122 | 123 | # Raise an error message 124 | errors = " and ".join(reversed(errors)) 125 | message = "Unable to set " + errors 126 | if help: 127 | message += " (%s)" % help 128 | raise ChildError(message) 129 | -------------------------------------------------------------------------------- /fusil/mangle.py: -------------------------------------------------------------------------------- 1 | from array import array 2 | from random import choice, randint 3 | 4 | from fusil.mangle_agent import MangleAgent 5 | from fusil.mangle_op import MAX_INCR, SPECIAL_VALUES 6 | from fusil.tools import minmax 7 | 8 | 9 | class MangleConfig: 10 | def __init__(self, min_op=1, max_op=100, operations=None): 11 | """ 12 | Number of operations: min_op..max_op 13 | Operations: list of function names (eg. ["replace", "bit"]) 14 | """ 15 | self.min_op = min_op 16 | self.max_op = max_op 17 | self.max_insert_bytes = 4 18 | self.max_delete_bytes = 4 19 | self.max_incr = MAX_INCR 20 | self.first_offset = 0 21 | self.change_size = False 22 | if operations: 23 | self.operations = operations 24 | else: 25 | self.operations = None 26 | 27 | 28 | class Mangle: 29 | def __init__(self, config, data): 30 | self.config = config 31 | self.data = data 32 | 33 | def generateByte(self): 34 | return randint(0, 255) 35 | 36 | def offset(self, last=1): 37 | first = self.config.first_offset 38 | last = len(self.data) - last 39 | if last < first: 40 | raise ValueError( 41 | "Invalid first_offset value (first=%s > last=%s)" % (first, last) 42 | ) 43 | return randint(first, last) 44 | 45 | def mangle_replace(self): 46 | self.data[self.offset()] = self.generateByte() 47 | 48 | def mangle_bit(self): 49 | offset = self.offset() 50 | bit = randint(0, 7) 51 | if randint(0, 1) == 1: 52 | value = self.data[offset] | (1 << bit) 53 | else: 54 | value = self.data[offset] & (~(1 << bit) & 0xFF) 55 | self.data[offset] = value 56 | 57 | def mangle_special_value(self): 58 | text = choice(SPECIAL_VALUES) 59 | offset = self.offset(len(text)) 60 | self.data[offset : offset + len(text)] = array("B", text) 61 | 62 | def mangle_increment(self): 63 | incr = randint(1, self.config.max_incr) 64 | if randint(0, 1) == 1: 65 | incr = -incr 66 | offset = self.offset() 67 | self.data[offset] = minmax(0, self.data[offset] + incr, 255) 68 | 69 | def mangle_insert_bytes(self): 70 | offset = self.offset() 71 | count = randint(1, self.config.max_insert_bytes) 72 | for index in range(count): 73 | self.data.insert(offset, self.generateByte()) 74 | 75 | def mangle_delete_bytes(self): 76 | offset = self.offset(2) 77 | count = randint(1, self.config.max_delete_bytes) 78 | count = min(count, len(self.data) - offset) 79 | del self.data[offset : offset + count] 80 | 81 | def run(self): 82 | """ 83 | Mangle data and return number of applied operations 84 | """ 85 | 86 | operation_names = self.config.operations 87 | if not operation_names: 88 | operation_names = ["replace", "bit", "special_value"] 89 | if self.config.change_size: 90 | operation_names.extend(("insert_bytes", "delete_bytes")) 91 | 92 | operations = [] 93 | for name in operation_names: 94 | operation = getattr(self, "mangle_" + name) 95 | operations.append(operation) 96 | 97 | if self.config.max_op <= 0: 98 | return 0 99 | count = randint(self.config.min_op, self.config.max_op) 100 | for index in range(count): 101 | operation = choice(operations) 102 | operation() 103 | return count 104 | 105 | 106 | class MangleFile(MangleAgent): 107 | """ 108 | Inject errors in a valid file ("mutate" or "mangle" a file) to 109 | create new files. Use the config attribute (a MangleConfig 110 | instance) to configure the mutation parameters. 111 | """ 112 | 113 | def __init__(self, project, source, nb_file=1): 114 | MangleAgent.__init__(self, project, source, nb_file) 115 | self.config = MangleConfig() 116 | 117 | def mangleData(self, data, file_index): 118 | # Mangle bytes 119 | count = Mangle(self.config, data).run() 120 | self.info("Mangle operation: %s" % count) 121 | return data 122 | -------------------------------------------------------------------------------- /fusil/process/attach.py: -------------------------------------------------------------------------------- 1 | from ptrace.os_tools import HAS_PROC 2 | 3 | from fusil.project_agent import ProjectAgent 4 | 5 | if HAS_PROC: 6 | from ptrace.linux_proc import ProcError, readProcessStatm, searchProcessByName 7 | 8 | from ptrace.process_tools import dumpProcessInfo 9 | 10 | if HAS_PROC: 11 | from os import stat 12 | 13 | from fusil.process.cpu_probe import CpuProbe 14 | else: 15 | from os import kill 16 | 17 | from errno import ENOENT, ESRCH 18 | 19 | 20 | class AttachProcessPID(ProjectAgent): 21 | def __init__(self, project, pid, name=None): 22 | if not name: 23 | name = "pid:%s" % pid 24 | ProjectAgent.__init__(self, project, name) 25 | self.death_score = 1.0 26 | self.show_exit = True # needed by the debugger 27 | self.max_memory = 100 * 1024 * 1024 28 | self.memory_score = 1.0 29 | self.debugger = project.debugger 30 | self.dbg_process = None 31 | if HAS_PROC and project.config.process_use_cpu_probe: 32 | self.cpu = CpuProbe(project, "%s:cpu" % self.name) 33 | else: 34 | self.warning("CpuProbe is not available on your OS") 35 | self.cpu = None 36 | if pid: 37 | self.setPid(pid) 38 | else: 39 | self.pid = None 40 | 41 | def init(self): 42 | self.score = 0.0 43 | 44 | def on_project_stop(self): 45 | if self.dbg_process: 46 | self.dbg_process.detach() 47 | self.dbg_process = None 48 | 49 | def setPid(self, pid): 50 | self.pid = pid 51 | dumpProcessInfo(self.info, self.pid) 52 | if self.cpu: 53 | self.cpu.setPid(pid) 54 | if not self.dbg_process: 55 | self.dbg_process = self.debugger.tracePID(self, pid) 56 | 57 | def live(self): 58 | if self.pid is None: 59 | return 60 | if not self.checkAlive(): 61 | return 62 | if self.max_memory: 63 | if not self.checkMemory(): 64 | return 65 | 66 | def checkAlive(self): 67 | 68 | if self.dbg_process: 69 | status = self.debugger.pollPID(self.pid) 70 | if status is None: 71 | return True 72 | elif HAS_PROC: 73 | try: 74 | stat("/proc/%s" % self.pid) 75 | return True 76 | except OSError as err: 77 | if err.errno != ENOENT: 78 | raise 79 | else: 80 | try: 81 | kill(self.pid, 0) 82 | return True 83 | except OSError as err: 84 | if err.errno != ESRCH: 85 | raise 86 | self.error("Process %s disappeared" % self.pid) 87 | self.stop(self.death_score) 88 | return False 89 | 90 | def checkMemory(self): 91 | if not HAS_PROC: 92 | return True 93 | try: 94 | memory = readProcessStatm(self.pid)[0] 95 | except ProcError as error: 96 | self.error(error) 97 | self.stop() 98 | return False 99 | if memory < self.max_memory: 100 | return True 101 | self.error("Memory limit reached: %s > %s" % (memory, self.max_memory)) 102 | self.stop(self.memory_score) 103 | return False 104 | 105 | def stop(self, score=None): 106 | if score: 107 | self.score = score 108 | self.pid = None 109 | 110 | def getScore(self): 111 | return self.score 112 | 113 | 114 | class AttachProcess(AttachProcessPID): 115 | def __init__(self, project, process_name): 116 | AttachProcessPID.__init__( 117 | self, project, None, "attach_process:%s" % process_name 118 | ) 119 | self.process_name = process_name 120 | if not HAS_PROC: 121 | # Missing searchProcessByName() function 122 | raise NotImplementedError("AttachProcess is not supported on your OS") 123 | 124 | def init(self): 125 | AttachProcessPID.init(self) 126 | self.pid = None 127 | 128 | def on_session_start(self): 129 | pid = searchProcessByName(self.process_name) 130 | self.send("process_pid", self, pid) 131 | self.setPid(pid) 132 | -------------------------------------------------------------------------------- /tools/state_tool.py: -------------------------------------------------------------------------------- 1 | # state_tool.py 2 | import argparse 3 | import copy 4 | import json 5 | import pickle 6 | import sys 7 | from collections import Counter 8 | from pathlib import Path 9 | 10 | 11 | def def_jsonify(data): 12 | """ 13 | Recursively converts a data structure to be JSON serializable. 14 | Specifically, it converts `set` objects to `list` and `Counter` objects 15 | to `dict`. 16 | """ 17 | if isinstance(data, dict): 18 | return {k: def_jsonify(v) for k, v in data.items()} 19 | elif isinstance(data, (list, tuple)): 20 | return [def_jsonify(i) for i in data] 21 | elif isinstance(data, (set, Counter)): 22 | # Convert sets and Counter keys to a sorted list for consistent output 23 | return sorted(list(data)) 24 | else: 25 | return data 26 | 27 | 28 | def main(): 29 | """Main entry point for the state management tool.""" 30 | parser = argparse.ArgumentParser( 31 | description="A tool to inspect and convert the fuzzer's pickle state file.", 32 | formatter_class=argparse.RawTextHelpFormatter, 33 | epilog=""" 34 | Examples: 35 | - Show state file contents as JSON: 36 | python state_tool.py coverage_state.pkl 37 | 38 | - Convert a JSON state file to Pickle format: 39 | python state_tool.py coverage_state.json coverage_state.pkl 40 | 41 | - Convert a Pickle state file to JSON format: 42 | python state_tool.py coverage_state.pkl coverage_state.json 43 | """ 44 | ) 45 | parser.add_argument("input_file", type=Path, help="The input state file (.pkl or .json)") 46 | parser.add_argument("output_file", type=Path, nargs='?', default=None, 47 | help="The output file (optional). If omitted, prints to console.") 48 | args = parser.parse_args() 49 | 50 | # --- Validate Input --- 51 | if not args.input_file.exists(): 52 | print(f"Error: Input file not found at '{args.input_file}'", file=sys.stderr) 53 | sys.exit(1) 54 | 55 | input_ext = args.input_file.suffix 56 | if input_ext not in ['.pkl', '.json']: 57 | print(f"Error: Input file must be a .pkl or .json file.", file=sys.stderr) 58 | sys.exit(1) 59 | 60 | # --- Load Data --- 61 | print(f"[*] Loading {args.input_file}...") 62 | state_data = None 63 | try: 64 | if input_ext == '.pkl': 65 | with open(args.input_file, 'rb') as f: 66 | state_data = pickle.load(f) 67 | else: # .json 68 | with open(args.input_file, 'r', encoding='utf-8') as f: 69 | state_data = json.load(f) 70 | except Exception as e: 71 | print(f"Error: Failed to load data from '{args.input_file}': {e}", file=sys.stderr) 72 | sys.exit(1) 73 | 74 | print("[+] Data loaded successfully.") 75 | 76 | # --- Process and Output Data --- 77 | if args.output_file is None: 78 | # "Show" mode: Print to console as JSON 79 | print("[*] No output file specified. Pretty-printing state as JSON to console.") 80 | print("-" * 80) 81 | # We must convert non-serializable types like sets and Counters first 82 | json_compatible_data = def_jsonify(copy.deepcopy(state_data)) 83 | print(json.dumps(json_compatible_data, indent=2)) 84 | else: 85 | # "Convert" mode: Save to output file 86 | output_ext = args.output_file.suffix 87 | print(f"[*] Converting to {args.output_file}...") 88 | try: 89 | if output_ext == '.pkl': 90 | with open(args.output_file, 'wb') as f: 91 | pickle.dump(state_data, f) 92 | elif output_ext == '.json': 93 | json_compatible_data = def_jsonify(copy.deepcopy(state_data)) 94 | with open(args.output_file, 'w', encoding='utf-8') as f: 95 | json.dump(json_compatible_data, f, indent=2) 96 | else: 97 | print(f"Error: Output file must be .pkl or .json", file=sys.stderr) 98 | sys.exit(1) 99 | print(f"[+] Successfully saved to {args.output_file}") 100 | except Exception as e: 101 | print(f"Error: Failed to save data to '{args.output_file}': {e}", file=sys.stderr) 102 | sys.exit(1) 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /fusil/session.py: -------------------------------------------------------------------------------- 1 | import re 2 | from logging import INFO, Formatter 3 | 4 | from fusil.mas.agent_list import AgentList 5 | from fusil.project_agent import ProjectAgent 6 | from fusil.score import normalizeScore 7 | from fusil.session_agent import SessionAgent 8 | from fusil.session_directory import SessionDirectory 9 | 10 | # Match "[0][session 0010] " 11 | PREFIX_REGEX = re.compile(r"\[[0-9]\]\[+session [0-9]+\] ") 12 | 13 | 14 | class SessionFormatter(Formatter): 15 | """ 16 | Log formatter for session.log: only write the message and 17 | remove fusil prefix: 18 | 19 | "[0][session 0010] text" => "text" 20 | """ 21 | 22 | def format(self, record): 23 | text = Formatter.format(self, record) 24 | return PREFIX_REGEX.sub("", text) 25 | 26 | 27 | class Session(SessionAgent): 28 | """ 29 | A session of the fuzzer: 30 | - create a directory as working directory 31 | - compute the score of the session 32 | """ 33 | 34 | def __init__(self, project): 35 | self.agents = AgentList() 36 | self.score = None 37 | self.log_handler = None 38 | name = "session %s" % project.session_index 39 | SessionAgent.__init__(self, self, name, project=project) 40 | 41 | def isSuccess(self): 42 | if self.score is None: 43 | return False 44 | return self.project().success_score <= self.score 45 | 46 | def computeScore(self, verbose=False): 47 | """ 48 | Compute the score of the session: 49 | - call getScore() method of all agents 50 | - normalize the score in [-1.0; 1.0] 51 | - apply score factor (weight) 52 | - compute the sum of all scores 53 | """ 54 | session_score = 0 55 | for agent in self.project().agents: 56 | if not issubclass(agent.__class__, ProjectAgent): 57 | # Skip application agent which has no score 58 | continue 59 | if not agent.is_active: 60 | continue 61 | score = agent.getScore() 62 | if score is None: 63 | continue 64 | score = normalizeScore(score) 65 | score *= agent.score_weight 66 | score = normalizeScore(score) 67 | if verbose and score: 68 | self.info("- %s score: %.1f%%" % (agent, score * 100)) 69 | session_score += score 70 | return session_score 71 | 72 | def registerAgent(self, agent): 73 | self.agents.append(agent) 74 | 75 | def unregisterAgent(self, agent, destroy=True): 76 | if agent not in self.agents: 77 | return 78 | self.agents.remove(agent, destroy) 79 | 80 | def init(self): 81 | self.directory = SessionDirectory(self) 82 | 83 | log_filename = self.createFilename("session.log") 84 | self.log_handler = self.logger.addFileHandler( 85 | log_filename, level=INFO, formatter_class=SessionFormatter 86 | ) 87 | 88 | self.stopped = False 89 | 90 | def deinit(self): 91 | if self.log_handler: 92 | self.logger.removeFileHandler(self.log_handler) 93 | self.agents.clear() 94 | 95 | def live(self): 96 | """ 97 | Compute the score of the session and stop the session if the score is 98 | smaller than -50% or bigger than 50%. 99 | """ 100 | if self.stopped: 101 | return 102 | score = self.computeScore() 103 | if score is None: 104 | return 105 | project = self.project() 106 | if not (project.success_score <= score or score <= project.error_score): 107 | return 108 | self.send("session_stop") 109 | 110 | def on_session_stop(self): 111 | if self.stopped: 112 | return 113 | self.stopped = True 114 | score = self.computeScore(True) 115 | if self.project().success_score <= score: 116 | self.send("session_success") 117 | self.send("session_done", score) 118 | 119 | def createFilename(self, filename, count=None): 120 | """ 121 | Create a filename in the session working directory: add directory 122 | prefix and make sure that the generated filename is unique. 123 | """ 124 | return self.directory.uniqueFilename(filename, count=count) 125 | -------------------------------------------------------------------------------- /tests/python/samples/test_tricky_objects.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | import types 5 | 6 | # --- Test Setup: Path Configuration --- 7 | # This ensures the test runner can find the 'fusil' package. 8 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | PROJECT_ROOT = os.path.join(SCRIPT_DIR, '..', '..', '..') 10 | sys.path.insert(0, PROJECT_ROOT) 11 | 12 | try: 13 | # --- Import all the objects to be tested --- 14 | from fusil.python.samples.tricky_objects import ( 15 | TrickyDescriptor, 16 | TrickyMeta, 17 | TrickyClass, 18 | tricky_instance, 19 | tricky_cell, 20 | tricky_simplenamespace, 21 | tricky_capsule, 22 | tricky_module, 23 | tricky_module2, 24 | tricky_genericalias, 25 | tricky_dict, 26 | tricky_mappingproxy, 27 | tricky_function, 28 | tricky_lambda, 29 | tricky_classmethod, 30 | tricky_staticmethod, 31 | tricky_property, 32 | tricky_code, 33 | tricky_closure, 34 | tricky_classmethod_descriptor, 35 | tricky_frame, 36 | tricky_traceback, 37 | tricky_list_with_cycle, 38 | ) 39 | SAMPLES_AVAILABLE = True 40 | except (ImportError, TypeError, SyntaxError) as e: 41 | print(f"Could not import tricky_objects module, skipping tests: {e}", file=sys.stderr) 42 | SAMPLES_AVAILABLE = False 43 | 44 | 45 | @unittest.skipIf(not SAMPLES_AVAILABLE, "Could not import tricky_objects module, skipping tests.") 46 | class TestTrickyObjects(unittest.TestCase): 47 | """ 48 | Test suite for the tricky_objects sample module. 49 | 50 | These tests verify that the objects are created with their expected types 51 | and that their specific "tricky" characteristics (e.g., circular references, 52 | custom metaclass behavior) are working as intended. 53 | """ 54 | 55 | def test_object_types(self): 56 | """ 57 | Verifies the fundamental type of each tricky object. 58 | """ 59 | self.assertIsInstance(tricky_function, types.FunctionType) 60 | self.assertIsInstance(tricky_lambda, types.LambdaType) 61 | self.assertIsInstance(tricky_code, types.CodeType) 62 | self.assertIsInstance(tricky_cell, types.CellType) 63 | self.assertIsInstance(tricky_module, types.ModuleType) 64 | self.assertIsInstance(tricky_mappingproxy, types.MappingProxyType) 65 | self.assertIsInstance(tricky_instance, TrickyClass) 66 | self.assertIsInstance(tricky_list_with_cycle, list) 67 | 68 | # Some objects might not exist on all Python versions, so test conditionally. 69 | if tricky_genericalias: 70 | self.assertIsInstance(tricky_genericalias, types.GenericAlias) 71 | if tricky_frame: 72 | self.assertIsInstance(tricky_frame, types.FrameType) 73 | if tricky_traceback: 74 | self.assertIsInstance(tricky_traceback, types.TracebackType) 75 | 76 | def test_circular_references(self): 77 | """ 78 | Verifies that objects with intentional circular references are structured correctly. 79 | """ 80 | # Test the self-referential dictionary 81 | self.assertIs(tricky_dict["tricky_dict"], tricky_dict) 82 | 83 | # Test the self-referential list 84 | self.assertIs(tricky_list_with_cycle[0][0], tricky_list_with_cycle) 85 | 86 | # Test the self-referential namespace 87 | self.assertIs(tricky_simplenamespace.dummy, tricky_simplenamespace) 88 | 89 | def test_tricky_class_and_meta_behavior(self): 90 | """ 91 | Verifies the special behavior of TrickyClass, TrickyMeta, and TrickyDescriptor. 92 | """ 93 | # Test TrickyMeta's overridden __signature__ property 94 | with self.assertRaises(AttributeError, msg="Accessing __signature__ should raise an error"): 95 | _ = TrickyClass.__signature__ 96 | 97 | # Test TrickyClass's overridden __getattr__ 98 | # Accessing any undefined attribute should return the instance itself. 99 | self.assertIs(tricky_instance.some_non_existent_attribute, tricky_instance) 100 | 101 | # Test that the descriptor works 102 | self.assertIsInstance(tricky_instance.tricky_descriptor, TrickyDescriptor) 103 | 104 | 105 | if __name__ == '__main__': 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /tests/python/samples/test_weird_classes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | from decimal import Decimal 5 | 6 | # --- Test Setup: Path Configuration --- 7 | # This ensures the test runner can find the 'fusil' package. 8 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | PROJECT_ROOT = os.path.join(SCRIPT_DIR, '..', '..', '..') 10 | sys.path.insert(0, PROJECT_ROOT) 11 | 12 | try: 13 | # --- Import the objects to be tested --- 14 | from fusil.python.samples.weird_classes import weird_classes, weird_instances 15 | SAMPLES_AVAILABLE = True 16 | except (ImportError, TypeError) as e: 17 | print(f"Could not import weird_classes module, skipping tests: {e}", file=sys.stderr) 18 | weird_classes = None 19 | weird_instances = None 20 | SAMPLES_AVAILABLE = False 21 | 22 | 23 | @unittest.skipIf(not SAMPLES_AVAILABLE, "Could not import weird_classes module, skipping tests.") 24 | class TestWeirdClasses(unittest.TestCase): 25 | """ 26 | Test suite for the weird_classes sample module. 27 | 28 | These tests verify that the dictionaries of weird classes and instances 29 | are generated correctly, have the expected types, and follow the 30 | intended inheritance structure. 31 | """ 32 | 33 | def test_dictionaries_are_populated(self): 34 | """ 35 | Verifies that the main dictionaries are created and not empty. 36 | """ 37 | self.assertIsInstance(weird_classes, dict) 38 | self.assertIsInstance(weird_instances, dict) 39 | self.assertGreater(len(weird_classes), 0, "weird_classes dictionary should not be empty.") 40 | self.assertGreater(len(weird_instances), 0, "weird_instances dictionary should not be empty.") 41 | 42 | def test_weird_classes_inheritance(self): 43 | """ 44 | Verifies that generated classes inherit from their correct Python base types. 45 | """ 46 | # Test a few representative examples from the different base types. 47 | self.assertTrue(issubclass(weird_classes['weird_int'], int)) 48 | self.assertTrue(issubclass(weird_classes['weird_list'], list)) 49 | self.assertTrue(issubclass(weird_classes['weird_dict'], dict)) 50 | self.assertTrue(issubclass(weird_classes['weird_bytes'], bytes)) 51 | self.assertTrue(issubclass(weird_classes['weird_Decimal'], Decimal)) 52 | 53 | def test_weird_classes_metaclass_behavior(self): 54 | """ 55 | Verifies that the WeirdBase metaclass correctly modifies class behavior. 56 | """ 57 | # The WeirdBase metaclass overrides __eq__ to always return False. 58 | weird_int_class = weird_classes['weird_int'] 59 | self.assertNotEqual(weird_int_class, weird_int_class, 60 | "A weird class should not be equal to itself due to the metaclass.") 61 | 62 | def test_weird_instances_types(self): 63 | """ 64 | Verifies that generated instances are of the correct weird class type. 65 | """ 66 | # Test a few key instances 67 | empty_list = weird_instances['weird_list_empty'] 68 | self.assertIsInstance(empty_list, weird_classes['weird_list']) 69 | 70 | maxsize_int = weird_instances['weird_int_sys_maxsize'] 71 | self.assertIsInstance(maxsize_int, weird_classes['weird_int']) 72 | 73 | tricky_str_dict = weird_instances['weird_dict_tricky_strs'] 74 | self.assertIsInstance(tricky_str_dict, weird_classes['weird_dict']) 75 | 76 | def test_weird_instances_content(self): 77 | """ 78 | Verifies the contents of a few representative generated instances. 79 | """ 80 | # Test an empty instance 81 | empty_int = weird_instances['weird_int_empty'] 82 | self.assertEqual(empty_int, 0) # int() defaults to 0 83 | 84 | # Test a populated instance 85 | single_item_str = weird_instances['weird_str_single'] 86 | self.assertEqual(single_item_str, "a") 87 | 88 | # Test a numeric instance 89 | maxsize_int = weird_instances['weird_int_sys_maxsize'] 90 | self.assertEqual(maxsize_int, sys.maxsize) 91 | 92 | # Test an instance created from a range 93 | range_tuple = weird_instances['weird_tuple_range'] 94 | self.assertEqual(len(range_tuple), 20) 95 | self.assertEqual(range_tuple[5], 5) 96 | 97 | 98 | if __name__ == '__main__': 99 | unittest.main() 100 | --------------------------------------------------------------------------------