Fuzzing Complex JavaScript Structures

Building complex programs without losing the research goal

Fuzzing Complex JavaScript Structures

Building complex programs without losing the research goal

August 27, 2026

Finding the Unexpected

The basic idea behind a JavaScript engine fuzzer is very simple: generate programs that the engine does not expect to execute.

The difficult part is defining what unexpected actually means.

V8, JavaScriptCore, and SpiderMonkey continuously process millions of lines of code: frameworks, web applications, libraries, minifiers, obfuscators, and language features used in ways that were not originally anticipated by browser developers. In Chrome’s case, that variety is multiplied by more than 2 billion people using the browser every day and executing JavaScript.

When we develop a fuzzer for V8 or another engine, we have to keep in mind that we are competing against the vendor’s own fuzzers, infrastructures such as ClusterFuzz, public fuzzers like Fuzzilli, and private fuzzers developed by researchers and companies.

And, in a way, we are also competing against real-world browser usage itself.

Every Chrome installation is executing JavaScript code that our fuzzer would probably never generate. These browsers are not running a build of V8 compiled specifically for vulnerability research, but they can still detect certain bugs. A failing CHECK, for example, terminates the process and may produce a crash report that is captured by Crashpad, the internal crash reporting mechanism.

When doing research, we normally do not run the same binary that users run. We compile the JavaScript engine with DCHECKs and memory error detectors, or even add custom patches to the engine that detect specific failures. The same JavaScript program that looks completely normal in a production build can reveal inconsistencies in a research build.

Although vendors cannot bring all of this detection capability into the real world because the performance cost would be too high, they do try to take advantage of massive browser usage as a form of large-scale fuzzing to detect and fix bugs. For example, Chromium enables DCHECKs in certain small environments, and Google developed GWP-ASan to detect memory errors through sampling, reducing the resulting performance impact.

A modern fuzzer has to be capable of generating new execution sequences and states, and detecting failures that other systems fail to detect.

Generating Complex JavaScript

The biggest problem a JavaScript fuzzer has to solve is that it needs to generate programs. This forces it to maintain a representation of the program it is building and understand enough context for subsequent mutations to continue making sense. In each scope, it needs to know which variables are available and their types, which functions or classes it can use, and what new JavaScript code can be inserted, modified, or removed at that position.

A small mistake can make the entire sample useless. For example, creating a program that uses a variable outside its scope, or using await where it is not allowed, or calling an API with invalid data can cause the engine to reject the program before reaching the part of the code we actually wanted to execute.

Maintaining all of this consistency becomes more difficult as the program grows. JavaScript allows similar behaviors to be generated in many different ways and combined with one another: functions, generators, classes, inheritance, computed properties, eval, and a huge number of native APIs.

These constructs are made up of elements that depend on one another. A class can extend an expression whose result may change at runtime. A computed property can execute code while that class is being defined, and that code can create another class, capture variables from an outer scope, or execute a generator. Each new layer introduces constraints that the fuzzer has to know about and maintain. The deeper and more interdependent these relationships become, the easier it is for a mutation to break a precondition, change the evaluation order or, most importantly, cause the sample to stop reaching the behavior we wanted to explore.

As we have seen, modern fuzzers must be designed to generate unexpected programs while managing all of this complexity at the same time. This usually means imposing constraints on the fuzzer itself so that it continues generating valid and executable samples, which typically results in structures that are less deep, shorter, easier to modify, and more resilient to future mutations.

Even so, there is an important difference between generating large programs and building complex structures. If we want to explore the latter, a researcher cannot expect the fuzzer to reach that structure on its own and, on top of that, manage to preserve all of its dependencies while continuing to mutate it.

These structures that are difficult to build and maintain are what we want to explore. To do that, we need to add mechanisms to our fuzzers that allow us to introduce this complexity in a controlled way and have the fuzzer continue mutating around it.

From JavaScript to a Mutation Plan

In SeekZero, we have developed the Playground so that the researcher can develop ideas in a natural way. The Playground is an editor that allows you to write JavaScript code, execute it, and observe both the result and the generated coverage.

SeekZero Playground

From there, different JavaScript features can be combined until we build the kind of program we want to investigate.

For example:

As we can see, creativity is the limit. We can build structures as complex as we want and use the JavaScript directly as a Mutation Plan, to define which parts of the structure we want to keep, which ones we want to mutate, and how to do it. We do not need to translate it into a separate template system. The JavaScript program itself is the template, with its structure, dependencies, and data types.

The parts we want to keep can be marked as lock or preserve, while the rest remain available for mutation. This allows us to introduce complex structures into a campaign without forcing the fuzzer to rebuild them from scratch in every sample.

SeekZero Mutation Plan

Next, we are going to add complexity to this sample and turn it into a Mutation Plan directly from SeekZero.

Video: Building and fuzzing a complex JavaScript structure with SeekZero

References

Fuzzing Complex JavaScript Structures

Building complex programs without losing the research goal

Fuzzing Complex JavaScript Structures

Building complex programs without losing the research goal

August 27, 2026

Finding the Unexpected

The basic idea behind a JavaScript engine fuzzer is very simple: generate programs that the engine does not expect to execute.

The difficult part is defining what unexpected actually means.

V8, JavaScriptCore, and SpiderMonkey continuously process millions of lines of code: frameworks, web applications, libraries, minifiers, obfuscators, and language features used in ways that were not originally anticipated by browser developers. In Chrome’s case, that variety is multiplied by more than 2 billion people using the browser every day and executing JavaScript.

When we develop a fuzzer for V8 or another engine, we have to keep in mind that we are competing against the vendor’s own fuzzers, infrastructures such as ClusterFuzz, public fuzzers like Fuzzilli, and private fuzzers developed by researchers and companies.

And, in a way, we are also competing against real-world browser usage itself.

Every Chrome installation is executing JavaScript code that our fuzzer would probably never generate. These browsers are not running a build of V8 compiled specifically for vulnerability research, but they can still detect certain bugs. A failing CHECK, for example, terminates the process and may produce a crash report that is captured by Crashpad, the internal crash reporting mechanism.

When doing research, we normally do not run the same binary that users run. We compile the JavaScript engine with DCHECKs and memory error detectors, or even add custom patches to the engine that detect specific failures. The same JavaScript program that looks completely normal in a production build can reveal inconsistencies in a research build.

Although vendors cannot bring all of this detection capability into the real world because the performance cost would be too high, they do try to take advantage of massive browser usage as a form of large-scale fuzzing to detect and fix bugs. For example, Chromium enables DCHECKs in certain small environments, and Google developed GWP-ASan to detect memory errors through sampling, reducing the resulting performance impact.

A modern fuzzer has to be capable of generating new execution sequences and states, and detecting failures that other systems fail to detect.

Generating Complex JavaScript

The biggest problem a JavaScript fuzzer has to solve is that it needs to generate programs. This forces it to maintain a representation of the program it is building and understand enough context for subsequent mutations to continue making sense. In each scope, it needs to know which variables are available and their types, which functions or classes it can use, and what new JavaScript code can be inserted, modified, or removed at that position.

A small mistake can make the entire sample useless. For example, creating a program that uses a variable outside its scope, or using await where it is not allowed, or calling an API with invalid data can cause the engine to reject the program before reaching the part of the code we actually wanted to execute.

Maintaining all of this consistency becomes more difficult as the program grows. JavaScript allows similar behaviors to be generated in many different ways and combined with one another: functions, generators, classes, inheritance, computed properties, eval, and a huge number of native APIs.

These constructs are made up of elements that depend on one another. A class can extend an expression whose result may change at runtime. A computed property can execute code while that class is being defined, and that code can create another class, capture variables from an outer scope, or execute a generator. Each new layer introduces constraints that the fuzzer has to know about and maintain. The deeper and more interdependent these relationships become, the easier it is for a mutation to break a precondition, change the evaluation order or, most importantly, cause the sample to stop reaching the behavior we wanted to explore.

As we have seen, modern fuzzers must be designed to generate unexpected programs while managing all of this complexity at the same time. This usually means imposing constraints on the fuzzer itself so that it continues generating valid and executable samples, which typically results in structures that are less deep, shorter, easier to modify, and more resilient to future mutations.

Even so, there is an important difference between generating large programs and building complex structures. If we want to explore the latter, a researcher cannot expect the fuzzer to reach that structure on its own and, on top of that, manage to preserve all of its dependencies while continuing to mutate it.

These structures that are difficult to build and maintain are what we want to explore. To do that, we need to add mechanisms to our fuzzers that allow us to introduce this complexity in a controlled way and have the fuzzer continue mutating around it.

From JavaScript to a Mutation Plan

In SeekZero, we have developed the Playground so that the researcher can develop ideas in a natural way. The Playground is an editor that allows you to write JavaScript code, execute it, and observe both the result and the generated coverage.

SeekZero Playground

From there, different JavaScript features can be combined until we build the kind of program we want to investigate.

For example:

As we can see, creativity is the limit. We can build structures as complex as we want and use the JavaScript directly as a Mutation Plan, to define which parts of the structure we want to keep, which ones we want to mutate, and how to do it. We do not need to translate it into a separate template system. The JavaScript program itself is the template, with its structure, dependencies, and data types.

The parts we want to keep can be marked as lock or preserve, while the rest remain available for mutation. This allows us to introduce complex structures into a campaign without forcing the fuzzer to rebuild them from scratch in every sample.

SeekZero Mutation Plan

Next, we are going to add complexity to this sample and turn it into a Mutation Plan directly from SeekZero.

Video: Building and fuzzing a complex JavaScript structure with SeekZero

References