This directory contains fuzz testing infrastructure using ClusterFuzzLite and Atheris.
Fuzz testing helps discover edge cases, crashes, and potential security vulnerabilities by feeding random inputs to functions. This setup uses:
- ClusterFuzzLite: Google's continuous fuzzing solution for GitHub projects
- Atheris: Coverage-guided Python fuzzing engine
- AddressSanitizer: Memory safety checks
fuzz/
├── Dockerfile # Docker image for ClusterFuzzLite fuzzing
├── build.sh # Build script for compiling fuzzers
├── fuzz_tokenize.py # Fuzzer for word_tokenize()
├── fuzz_util_normalize.py # Fuzzer for normalize()
└── README.md # This file
Tests pythainlp.tokenize.word_tokenize() with random Unicode input to ensure:
- No crashes on malformed input
- Proper handling of edge cases
- Memory safety
Tests pythainlp.util.normalize() with random Unicode input to ensure:
- No crashes on malformed input
- Proper string normalization
- Type safety
To test fuzzers locally:
# Install atheris
pip install atheris
# Run a specific fuzzer for 60 seconds
python fuzz/fuzz_tokenize.py -max_total_time=60
# Run with specific corpus directory
python fuzz/fuzz_tokenize.py corpus_dir/ -max_total_time=60Fuzzing runs automatically via GitHub Actions:
- On pull requests to
devbranch (focuses on code changes) - On push to
devbranch - Daily at 06:00 UTC (full fuzzing run)
Configuration: .github/workflows/clusterfuzzlite.yml
To add a new fuzzing target:
- Create a new file
fuzz/fuzz_<module_name>.py:
# SPDX-FileCopyrightText: 2026 PyThaiNLP Project
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileType: SOURCE
"""Fuzzing harness for pythainlp.<module>.<function>()"""
import sys
import atheris
import pythainlp.<module>
def TestOneInput(data: bytes) -> None:
"""Fuzz target for <function>."""
fdp = atheris.FuzzedDataProvider(data)
try:
# Generate test input
text = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())
# Call target function
result = pythainlp.<module>.<function>(text)
# Validate output
assert isinstance(result, <expected_type>)
except (ValueError, TypeError, UnicodeDecodeError):
# Expected exceptions
pass
def main() -> None:
"""Entry point for the fuzzer."""
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
if __name__ == "__main__":
main()-
The fuzzer will be automatically discovered and built by
build.sh -
No changes needed to GitHub Actions workflow
Future fuzzing targets to consider:
- spell/ - Spelling correction functions
- soundex/ - Phonetic encoding functions
- transliterate/ - Romanization functions
- corpus/ - Data loading and corpus functions
- tag/ - Part-of-speech tagging
- parse/ - Parsing functions
- classify/ - Classification functions
- generate/ - Text generation functions
- summarize/ - Summarization functions
If a fuzzer finds a crash:
- Check the GitHub Actions artifacts for crash reports
- Reproduce locally:
python fuzz/fuzz_<name>.py <crash_file> - Fix the underlying issue in the target function
- Re-run fuzzer to verify fix
- Adjust fuzzing time in
.github/workflows/clusterfuzzlite.yml - Default is 300 seconds (5 minutes) per fuzzer
- For longer sessions, increase the value
- Update the exception handling in the fuzzer
- Add expected exceptions to the
exceptblock - Document the reasoning in comments