Griller: Reactive Bottom-Up Vulnerability Testing
- Griller is a reactive bottom-up vulnerability testing framework that directly fuzzes internal functions and validates crash feasibility via call-graph backtracking.
- It integrates tools like LLVM, AFL++, KLEE, and Z3 to generate type- and context-aware harnesses while extracting and stitching symbolic constraints.
- This modular approach enhances detection of deep function bugs and reduces false positives by combining aggressive exploration with rigorous feasibility checks.
Searching arXiv for the specified paper and closely related work on bottom-up fuzzing/testing.
Griller is an automated vulnerability-testing framework that implements the Reactive Bottom-Up Testing paradigm for C/C++ user-space programs. Rather than beginning at main() and attempting to drive execution downward through complex program logic, it starts from internal functions, fuzzes them in isolation to find crashing states, and then validates whether those states are feasible along real call paths by symbolically backtracking constraints through the call graph. The prototype is built on LLVM, AFL++, KLEE, and Z3, and is designed to improve detection of vulnerabilities in deep functions while reducing the false positives traditionally associated with function-level fuzzing (Muralee et al., 3 Sep 2025).
1. Paradigm and problem setting
Reactive Bottom-Up Testing is defined by a two-phase logic. First, internal functions are tested aggressively in isolation, without initially constraining the search to states that are feasible in executions from main(). Second, each discovered crash is treated reactively: the system asks whether the crashing state can actually arise in the real program by tracing backward through callers and composing constraints along call edges (Muralee et al., 3 Sep 2025).
This paradigm is explicitly contrasted with two established approaches. In Top-Down Testing, fuzzing or dynamic analysis starts from main or another top-level interface and attempts to reach deep functions through normal execution. The stated limitations are that deep functions are difficult to reach because validations and control-flow prune most states, and that substantial effort is spent exploring states and functions that never approach the vulnerable code. In traditional bottom-up or function-level fuzzing, an internal function is called directly with invented arguments, which makes it easy to trigger rare states, but many such states are infeasible in any real execution from main(), producing a high false-positive rate (Muralee et al., 3 Sep 2025).
Griller operationalizes a middle position. It preserves the reachability advantages of bottom-up exploration by performing unconstrained function-level fuzzing first, but it restores program-context soundness by selectively validating only bug-triggering states. This suggests a decomposition of the vulnerability-finding problem into exploration of local failure states and post hoc feasibility checking across the call graph, rather than proactive construction of fully realistic contexts for every tested function.
2. System architecture
Griller is organized as a staged framework whose components communicate through a Program Database, allowing independent and incremental execution. The five major components are Target Identifier, Function Harnesser, Function Fuzzer, Constraint Catcher, and Stitcher (Muralee et al., 3 Sep 2025).
The high-level workflow begins with LLVM IR analysis. The Target Identifier scores functions by vulnerability and fuzzability, selects target functions, and collects caller-to-callee edges up to main. The Function Harnesser then determines the input space of each target, including parameters and accessed globals, infers rich types and relationships, and generates a driver program that reads bytes from stdin, constructs typed inputs, and invokes either the target function or the caller function for call-edge testing. The Function Fuzzer runs AFL++ on these drivers, using sanitizers to detect memory errors and recording inputs that reach specific callsites in call-edge mode. The Constraint Catcher replays crashing inputs under KLEE using pre-constrained symbolic execution, extracting path constraints and root-cause constraints. Finally, the Stitcher composes callee crash constraints with caller edge constraints and propagates them backward until either main is reached or further backtracking fails (Muralee et al., 3 Sep 2025).
The staged structure is central to the design. It permits incremental analysis, avoids recomputation through the Program Database, and separates the concerns of target prioritization, input generation, crash discovery, symbolic explanation, and feasibility validation. A plausible implication is that this modularity makes the system more adaptable to heterogeneous program-analysis workloads than a monolithic fuzzing pipeline.
3. Function prioritization and harness synthesis
Vulnerability and fuzzability scoring
Griller’s Target Identifier computes two metrics per function. The first is a vulnerability metric inspired by LEOPARD. It measures structural features correlated with past vulnerabilities, including number of parameters, number of pointer arithmetic operations, number of variables involved in pointer arithmetic and their maximum arithmetic depth, nesting levels of control structures, number of if without else, number of variables in branch predicates, and control/data dependence complexity. Each metric is normalized across the program and summed into a vulnerability score (Muralee et al., 3 Sep 2025).
The second is a fuzzability metric based on the types of parameters and globals. A recursive Type Fuzzability Score is defined over base types, structs, arrays, and pointers; self-referential structs are marked as very hard with score ∞, arrays use known size or default 128, and pointers multiply the pointed-type score by 2. The final function score combines vulnerability and fuzzability:
Functions are then ranked and binned into HIGH, MEDIUM, and LOW priority classes (Muralee et al., 3 Sep 2025).
Call-graph recovery and indirect calls
Griller builds a directed call graph from LLVM IR and enumerates caller-to-callee paths up to main, visiting each call edge at most once per path. For indirect calls and function pointers, it approximates targets by requiring that a function’s address be taken somewhere and that the signature match the callsite, then rewrites indirect calls to direct calls to simplify subsequent analysis (Muralee et al., 3 Sep 2025).
Type- and context-aware harnesses
Harness generation is required to satisfy completeness, type and context awareness, and minimality. Completeness means including all inputs that influence the function, notably parameters and all globals used by the function or its callees. Type and context awareness is achieved through analyses that distinguish arrays from single objects, correlate sizes and buffers, and refine void *-style types through cast destinations. Minimality is achieved by avoiding inputs for locals that are always internally defined and by materializing only struct fields that are actually read (Muralee et al., 3 Sep 2025).
The generated harnesses recursively synthesize inputs from stdin. Base types read the appropriate number of bytes; structs allocate storage and recursively generate relevant fields; arrays either use known lengths or read a length from stdin and fill elements in a loop; pointers read a special null-option byte and with probability approximately 5% (byte < 13) become NULL, otherwise memory is allocated and the pointed-to type is generated recursively. Griller also emits a grammar describing the input-byte layout, which is later used for seed generation and for reconstructing symbolic variables during replay (Muralee et al., 3 Sep 2025).
Opaque external-library types are handled through custom C harness functions supplied by the operator. This indicates that the framework is not restricted to fully self-describing LLVM IR types, though automation is reduced when external abstractions dominate the interface.
4. Fuzzing, symbolic replay, and constraint extraction
Function-level fuzzing
The Function Fuzzer uses AFL++ with standard edge coverage instrumentation. A notable modification is that crashes with new coverage are retained in the queue rather than discarded, because function-level fuzzing may crash early and frequently. The generated driver repeatedly constructs inputs and invokes the target in persistent mode. Seed inputs can be grammar-based or optionally produced by a short KLEE exploration in symbolic mode; the concrete models found by KLEE then serve as AFL++ seeds (Muralee et al., 3 Sep 2025).
Sanitization is based on AddressSanitizer but strengthened to detect root causes earlier, such as pointer arithmetic escaping an object rather than only the eventual invalid access. Griller also adds runtime hooks for read, readv, fread, recv, getenv, fopen, and related calls, redirecting them to stdin so that the fuzzer controls all input channels and concrete and symbolic executions remain aligned (Muralee et al., 3 Sep 2025).
Crashing constraints
For a target function f, the paper defines the set of crashes observed in the driver program as
For each crash, Griller distinguishes a root-cause constraint , such as p == NULL or an object-size violation, and a path constraint , which is the conjunction of branch conditions along the replayed execution. The crashing constraint is then
This decomposition is significant because it separates the local semantic condition that causes the bug from the dynamic path conditions needed to reach it (Muralee et al., 3 Sep 2025).
Pre-constrained symbolic execution
Constraint extraction proceeds by reconstructing harness variables from the raw AFL++ input bytes using the same input grammar and emitting a KTEST file with correctly named and sized symbolic variables. KLEE then executes the driver in a pre-constrained concolic mode so that symbolic variables are constrained to equal the crashing concrete input, thereby forcing execution along the same path while recording symbolic conditions (Muralee et al., 3 Sep 2025).
Root-cause constraints are obtained by inserting checks such as grill_check and grill_check_buf at memory dereferences and operations like memcpy and strcpy. Buffer-size reasoning is implemented by tracking symbolic sizes of allocations in a runtime map, while pointer nullness is encoded through the harness-generated null-option bytes, with p_nullopt < 13 representing p == NULL. Constraint reduction then removes harness-induced artifacts such as irrelevant array-size constraints and null-pointer conditions that do not affect reproducibility (Muralee et al., 3 Sep 2025).
The emphasis on root-cause assertions rather than only terminal undefined behavior is methodologically important. It yields crash characterizations that more directly express vulnerability preconditions, which in turn makes later satisfiability-based backtracking more precise.
5. Constraint stitching and contextual validation
The Stitcher is the component that distinguishes Griller from ordinary function-level fuzzing. For a target function f, a call edge with cf = f, and a callsite j, Griller combines a callee crash constraint with the caller’s path and argument information. If the callee crash constraint is and the caller contributes path constraint and symbolic argument values , then the stitched constraint is
where 0 denotes substitution of the callee’s formal parameters with the corresponding caller-side symbolic expressions. Z3 is then used to test satisfiability. If 1 is satisfiable, the crash is reachable and triggerable from that callsite; if it is unsatisfiable, that callsite cannot realize the crash (Muralee et al., 3 Sep 2025).
The paper’s running example uses handle_req → process_req → add_elem. Griller fuzzes add_elem directly and finds four crash scenarios. These are stitched first to callsites in process_req and then to those in handle_req, and only one scenario remains feasible from main after the full backtracking process (Muralee et al., 3 Sep 2025).
Multi-level backtracking proceeds iteratively until either main is reached or no further propagation is possible. Griller distinguishes two outcomes. A crash that is fully backtracked to main is reported as a true bug. A crash that is only partially backtracked is flagged as potentially false positive. The framework also handles stitching for complex types: integers and chars via bitvector equality with width extension or truncation as needed, arrays via size and elementwise constraints, and pointers via both pointed data and nullness variables, while removing constraints that refer only to harness internals (Muralee et al., 3 Sep 2025).
This mechanism addresses a central controversy in bottom-up testing: whether local crashes discovered under synthetic harnesses correspond to real vulnerabilities. Griller’s answer is not to avoid synthetic states, but to accept them during discovery and filter them afterward through call-graph-level satisfiability reasoning.
6. Implementation, evaluation, and empirical findings
Implementation stack
Griller is implemented on LLVM 10.0.0 and extends 3c for type inference, while drawing on ideas from CCured and Locksmith. AFL++ is used as the fuzzing engine, KLEE provides LLVM-level symbolic execution, and Z3 is accessed through PySMT for satisfiability checking and UNSAT core extraction. The implementation comprises approximately 29,743 lines of C/C++ and approximately 28,432 lines of Python (Muralee et al., 3 Sep 2025).
The system targets C/C++ user-space applications and libraries compiled with Clang/LLVM. The details note an effective single-thread assumption, partial modeling of system and environment calls through runtime hooks, and approximations for recursive or highly dynamic data structures (Muralee et al., 3 Sep 2025).
Controlled benchmark
The primary evaluation uses a refined AFGen dataset containing 48 known vulnerabilities across 5 projects: ngiflib, ffjpeg, tcpreplay, jasper, and lupng. These comprise 11 configurations, 9 programs, 39 unique target functions, and 96 unique call edges. The vulnerabilities were selected to be in actively maintained projects, verified and fixed, and reproducible with crashing inputs (Muralee et al., 3 Sep 2025).
Fuzzing budgets ranged from 30 minutes to 3 hours per target function or call edge on a single CPU core. Constraint extraction had a 30-minute limit per crash, and stitching had a 1-minute timeout per constraint. Griller was given the vulnerable functions via manual mapping from CVE descriptions (Muralee et al., 3 Sep 2025).
Main results
| Result category | Griller | Comparison |
|---|---|---|
| Known vulnerabilities reproduced | 28/48 | AFGen 33/48; AFL++ 16/48; Beacon 15/48 |
| Target-function vulnerabilities triggered | 37/48 | 77% |
| New vulnerabilities found in real applications | 6 | 1 in mblaze, 4 in pacman, 1 in pspg |
On the benchmark, Griller reproduced 28 of 48 known vulnerabilities, outperforming AFL++ and Beacon by a wide margin but trailing AFGen on this file-parser-heavy dataset. The paper notes that Griller found 4 vulnerabilities that AFGen missed, partly due to better modeling of null pointers and sizes (Muralee et al., 3 Sep 2025).
At the target-function level, Griller triggered 37 of 48 vulnerabilities, often within approximately 2 seconds on average, and found 6 crashes that no other fuzzer in the AFGen study triggered. Of those 37, 9 were not fully backtracked to main, yielding the final 28/48 result (Muralee et al., 3 Sep 2025).
For 15 vulnerabilities that were completely backtracked to main, all 15 were true positives, giving 100% precision for fully backtracked bugs. The 6 reported false positives occurred only in partial backtracking cases, where the crashes were reproducible up to some intermediate caller but not from main (Muralee et al., 3 Sep 2025).
Component-level effectiveness
The evaluation reports that the Target Identifier matched 165 of 166 call edges from CVE reports, and that indirect-call promotion raised call-edge matching coverage from approximately 77.1% to approximately 99.4%. Harness generation correctly handled all sampled functions and identified 6,862 pointers as arrays with 412 size relationships. Constraint generation produced 284 constraints from 338 unique target-function crashes, approximately 84%, and 193 constraints from 204 call-edge crashes, approximately 94.6%. Constraint reduction shrank constraint size by approximately 43% on average. Function-level fuzzing successfully reached approximately 89% of call edges, and constraint generation covered approximately 94% of those reached edges (Muralee et al., 3 Sep 2025).
Real-world vulnerability discovery
In a separate experiment on mblaze, pacman, and pspg, with budgets up to 24 hours per target, Griller found 6 previously unknown vulnerabilities, of which 4 were already patched and the others remained under review at the time described. The examples include a Pacman heap overflow arising from 64-bit unsigned overflow in filename_size and an mblaze out-of-bounds read caused by a missing check on indent leading to index -1 (Muralee et al., 3 Sep 2025).
7. Relation to prior work, limitations, and implications
Griller is presented as the first implementation of Reactive Bottom-Up Testing. Its novelty lies not merely in function-level fuzzing or auto-generated harnesses, but in the explicit sequencing of unconstrained bottom-up exploration followed by backward feasibility checking through stitched constraints. Existing function-level fuzzers and auto-harness generators such as Fudge, FuzzGen, and AFGen are described as focusing on realistic application-context harnesses, often without systematically validating whether discovered crashing states are reachable from main across the full call graph (Muralee et al., 3 Sep 2025).
The framework’s limitations are also clearly stated. It does not reliably generate full concrete inputs to program entry points that satisfy all validations and trigger stitched constraints. Fuzzing top-level functions remains difficult for callbacks and parsers that require complex file or protocol formats. Recursive and cyclic data structures are approximated and often treated as very hard. Environmental dependencies such as devices, filesystem state, or permissions can block coverage. KLEE’s limitations, including lack of floating-point reasoning and pointer-model mismatches, can cause constraint-generation failures or path discrepancies. Backtracking can also be expensive because symbolic execution and SMT solving are performed per crash and per call edge (Muralee et al., 3 Sep 2025).
The suggested future directions include improved concretization of full-program inputs satisfying stitched constraints, better top-level fuzzing strategies using grammar-aware or format-aware methods, richer modeling of cyclic data structures, stronger static-dynamic integration, and extension to more languages or binary-only settings (Muralee et al., 3 Sep 2025).
For security engineering practice, Griller’s significance lies in its ability to make deep internal routines directly testable while still distinguishing actionable vulnerabilities from harness artifacts. The framework produces crash reports, symbolic constraint traces, function-level proof-of-concept inputs, and a classification into fully versus partially backtracked crashes. This suggests a workflow in which deep helper functions can be incorporated into CI as regression targets, while feasibility information from backtracking can prioritize remediation toward bugs that are demonstrably reachable in real executions (Muralee et al., 3 Sep 2025).