LoveFuzz is a fuzz testing suite designed to identify and analyze bugs in the da65 disassembler, a key component of the cc65 toolchain for 6502-based systems. The primary goal is to ensure the robustness and correctness of da65 by performing automated "round-trip" tests.
The fuzzer, lovefuzz.py, employs a round-trip testing methodology which is a highly effective way to validate the integrity of a disassembler/assembler pair. The process is as follows:
- Generate Source: A pseudo-random, but syntactically valid, assembly source file (
.s) is created. - Assemble (Pass 1): The
cl65utility assembles the source code into an initial binary program. - Disassemble: The
da65disassembler is invoked to convert the binary back into assembly source code. Optionally, an.infofile can be generated to guide this process. - Patch (Conditional): A post-processing step is available to apply workarounds for known bugs to the disassembled output. This allows the fuzzer to uncover new issues that might otherwise be masked. This step can be disabled with the
--no-patchflag. Note: The patching logic is currently a placeholder and does not yet apply any fixes. - Re-assemble (Pass 2): The
cl65utility attempts to re-assemble the patched assembly source back into a second binary. - Verify: The initial binary and the re-assembled binary are compared byte-for-byte. A mismatch or a failure during any step indicates a bug, and the failing test case is saved for analysis.
The lovefuzz.py script employs two primary methods to ensure the robustness of the toolchain and the fuzzer itself:
-
Static Advanced Syntax Test: When run with the
--test-advanced-syntaxflag, the script executes a deterministic, hand-crafted test (test_static_advanced_directives). This test uses a static assembly source file containing complex nested directives like.proc,.scope, and.if/.elseto verify that the toolchain can correctly handle a round-trip with these advanced features. -
Randomized Round-Trip Fuzzing: This is the core of the fuzzer, implemented within the main execution loop. It follows the round-trip methodology described previously, but uses pseudo-randomly generated assembly source as its starting point. This process is designed to uncover unexpected bugs by testing a wide range of inputs.
This fuzzing campaign has successfully identified two distinct bugs in the da65 disassembler, with the analysis documented below.
- Summary:
da65fails to correctly emit a.segment "CODE"directive after processing a data range (e.g.,TYPE = BYTETABLE) specified in an.infofile. - Root Cause & Impact: This issue stems from
da65's design, which requires explicit segment boundaries in the.infofile. The disassembler does not automatically revert to theCODEsegment after a data range unless a newSEGMENTorRANGEdirective for code immediately follows. This behavior contradicts the officialda65documentation (section 4.5), which states that the disassembler should automatically revert. The practical impact is that subsequent instructions are incorrectly placed within a data segment (e.g.,.segment "RODATA"), causing theca65assembler to fail during re-assembly. - Status: Identified. A conditional patching step (
apply_patches) exists as a placeholder for a future workaround. The intent is to mitigate this bug to allow the fuzzer to uncover other issues that might be masked. As of now, this step performs no actions. The--no-patchflag can be used to disable this placeholder step.
- Summary:
da65incorrectly guesses the addressing mode for certain ambiguous 6502 opcodes. - Impact: The 6502 instruction set has opcodes that are used for multiple addressing modes (e.g., zeropage vs. absolute).
da65must use heuristics to guess the correct mode. The fuzzer has generated numerous binaries whereda65makes the wrong guess. For example, it might disassemble a zeropage instruction as an absolute one. Whenca65attempts to re-assemble this output, it encounters a mismatch between the opcode and the operand size, resulting in aRange error (Address size 2 does not match fragment size 1). - Status: Exposed. The fuzzer now consistently generates multiple test cases that trigger this specific error, providing a valuable corpus for debugging
da65's static analysis logic.
- Prerequisites: Ensure the
cc65toolchain is installed and thatda65,cl65, andca65are in your system'sPATH. - Execution: Run the fuzzer from the command line:
# Run 100 tests with default settings python3 lovefuzz.py 100 # Run 500 tests, generating 200 instructions each, and only log failed tests python3 lovefuzz.py 500 -n 200 --log-failed-only # Run a test with advanced syntax and data range generation python3 lovefuzz.py 10 --test-advanced-syntax --test-da65-ranges # Run tests but disable the post-disassembly patching step python3 lovefuzz.py 20 --no-patch # Run indefinitely until 5 failed tests are found, using brief terminal output python3 lovefuzz.py 5 --collect-failed --brief # Run indefinitely until 10 passed tests are collected and saved to the 'passed/' directory python3 lovefuzz.py 10 --collect-passed # Clear all previous logs and artifacts before starting a new run python3 lovefuzz.py --clear-log-all
- Output:
* A detailed log of test runs is written to
fuzz_test_log.txt. The--log-failed-onlyor--log-passed-onlyflags can be used to filter the log. * When a test fails, all of its artifacts (the original source, generated binaries, logs, etc.) are saved to a uniquely named subdirectory inside thefailed/directory for persistent analysis. * If using--collect-passed, artifacts from successful runs are saved to thepassed/directory. * The--clear-log-all,--clear-log-failed, and--clear-log-passedflags can be used to clean up these artifacts.