LSPFuzz: Hunting Bugs in Language Servers
LSPFuzz is a grey-box hybrid fuzzer that generates test cases for Language Servers. It is implemented based on LibAFL.
What is this?
The language server crashed five times in the past three minutes.
The code completions are suddenly gone when I was typing.
Sound familiar? It should! Bugs in language servers can cause interruptions in your development workflow, even when you haven't done anything wrong. LSPFuzz is designed to automatically find such bugs before they are shipped to you.
Technical Details
LSPFuzz is equipped with a two-stage mutation pipeline that produces valid yet diverse inputs to trigger various analysis routines in LSP servers. To learn more about how it works, please check out the following research paper:
Hengcheng Zhu, Songqiang Chen, Valerio Terragni, Lili Wei, Yepang Liu, Jiarong Wu, and Shing-Chi Cheung. LSPFuzz: Hunting Bugs in Language Servers. In Proceedings of the 40th IEEE/ACM International Conference on Automated Software Engineering. Seoul, South Korea. November 2025.
🔗 DOI | 🎤 Conference | 📄 Preprint | 📦 Artifacts
If you use LSPFuzz for academic purposes, please cite the above paper. A snapshot of the code used to conduct the experiments in the paper can be found at the ase25-major-revision tag.
Usage
Preparation
-
Prepare a fuzz target compatible with AFL++. It is highly recommended to use LTO mode and persistent mode. The following is an annotated template for a fuzz target:
#include "your_header_file.h" #ifndef __AFL_FUZZ_TESTCASE_LEN // The following definitions allow compilation without the AFL++ compiler. ssize_t fuzz_len; #define __AFL_FUZZ_TESTCASE_LEN fuzz_len const uint8_t fuzz_buf[1024000]; #define __AFL_FUZZ_TESTCASE_BUF fuzz_buf #define __AFL_FUZZ_INIT() void sync(void); #define __AFL_LOOP(x) ((fuzz_len = read(0, fuzz_buf, sizeof(fuzz_buf))) > 0 ? 1 : 0) #define __AFL_INIT() sync() #endif __AFL_FUZZ_INIT(); int main(int argc, const char* argv[]) { #ifdef __AFL_HAVE_MANUAL_CONTROL __AFL_INIT(); #endif // [Initialization] // Perform one-time initialization for the target LSP server. // Or call `LLVMFuzzerInitialize(argc, argv)` here. const uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF; while (__AFL_LOOP(10000)) { ssize_t len = __AFL_FUZZ_TESTCASE_LEN; // [Input Processing] // Process an input here: // 1. Read `len` bytes from `buf` for LSP inputs, as if they were read from `stdin`. // 2. Process the LSP inputs. Note that the input contains the `Content-Length` header. // 3. Release resources and reset states. // Or call `LLVMFuzzerTestOneInput(buf, len)` here. } return 0; }
-
Obtain the coverage map size:
AFL_DUMP_MAP_SIZE=1 ./fuzz-target
-
Mine code fragments for code generation:
lsp-fuzz-cli mine-code-fragments \ --search-directory <code-dir> \ # Directory containing code files of the target language for the LSP servers --output <fragment-output> # File to store the mined code fragments
Caution
Although persistent mode can significantly improve fuzzing efficiency, users need to ensure that resources are properly released and states are reset in the fuzzing loop.
Start Fuzzing
lsp-fuzz-cli fuzz \ --state <state-dir> \ # Directory to store the fuzzing state (e.g., generated inputs, found crashes) --lsp-executable <fuzz-target> \ # Executable file of the LSP server fuzz target --language-fragments Language=<fragment-output>\ # Comma-separated list of files containing the mined code fragments, (e.g., `C=c.frag,CPlusPlus=cpp.frag`) --coverage-map-size <coverage-map-size> \ # Size of the coverage map to use for coverage-guided fuzzing --time-budget 24 # Time budget for fuzzing in hours
To learn more about the options, run lsp-fuzz-cli fuzz --help.
Reproduce Detected Crashes
-
Export the generated crash-triggering inputs:
lsp-fuzz-cli export \ --input <state-dir>/solutions \ # Directory containing the generated crash-triggering inputs --output <export-directory> # Directory to store the exported crash-triggering inputs
The contents of
<export-directory>will be organized as follows:<export-directory> ├── <input-id-0> │ ├── workspace │ │ ├── file1.txt │ │ └── file2.txt │ └── requests │ ├── message_0001 │ └── message_0002 ├── <input-id-1> │ ├── workspace │ │ ├── file1.txt │ │ └── file2.txt │ └── requests │ ├── message_0001 │ └── message_0002 └── ...Each directory
<input-id>represents a unique input generated by LSPFuzz. Within each<input-id>directory, there are two subdirectories:workspaceandrequests. Theworkspacedirectory contains the code files, and therequestsdirectory contains the LSP requests that were sent to the LSP server during fuzzing. -
Feed the exported input to the LSP server:
To reproduce the crash,
cdto a directory containing the exported inputs.cat requests/* | ./target-lsp-server
Note that
target-lsp-serveris the actual LSP server under test, not the fuzz target. Make sure it reads requests fromstdinand the CLI options are properly set. To reproduce bugs caught by sanitizers,target-lsp-servershould be compiled with sanitizers enabled.
Important
Do not move the exported test cases, because the LSP requests are encoded with absolute paths. Moving them will invalidate the requests (analogous to the concept of pinning in Rust – they are "pinned" to <export-directory>).
License
LSPFuzz is released under the MIT License. See the LICENSE file for details. The research paper and artifact are publicly available following the open-science policy.