Vulnerability Survey

V8 Bugs in the Agentic Era

Vulnerability Survey

V8 Bugs in the Agentic Era

September 29, 2026

By the end of September 2026, Chrome had fixed 131 V8 CVEs this year. That is more than 2024 and 2025 together (60 and 48). Chrome as a whole jumped even more: 2,908 CVEs so far in 2026, compared with 190 in all of 2025.

In Part 1 of our AI series, we described how language models went from helping with code review to working as research agents that read code, come up with ideas, and test them. This post looks at the bugs themselves. If agents now help find V8 bugs, the public record should show it: in how many bugs get fixed, in what kinds of bugs they are, and in where they are in the engine.

We looked at every V8 CVE Chrome has published since 2009, the commit that fixed each one, and the bugs known to have been exploited in the wild.

TLDR: far more bugs are being found, in more places and of more kinds. The bugs that attackers actually use still look the way they always have.

Data and method

We started from two public lists: v8-cves and chrome-cves. Both come from the Chrome Releases blog. For each CVE they record the description, Chromium’s severity rating, whether it was exploited in the wild, the issue number, the fix commits, and the Chrome release with the fix. The V8 list has 544 CVEs and the Chrome list has 6,709.

We added two things:

  • Bug class. Chrome describes each CVE in a few words, such as “Type confusion in V8” or “Use after free in V8”. We sorted these into type confusion, out-of-bounds access, use-after-free, race condition, integer overflow, and “inappropriate implementation”, which is Chromium’s name for logic bugs.
  • Area. For each CVE we downloaded the V8 fix commit and noted which source folders it changed, ignoring tests. src/compiler/ counts as TurboFan, src/compiler/turboshaft/ as Turboshaft, src/maglev/ as Maglev, and so on. This worked for 491 of the 493 fix commits.

A few caveats:

  • Chrome describes each bug in a few words, so some bugs could fit more than one class.
  • We look at where a bug was fixed, which is not always where it started.
  • A bug is counted in the year its fix shipped, not the year it was found.
  • 2026 isn’t over yet, so its numbers will only go up.

Figure 1 shows every year since 2009.

V8 CVEs per year

Figure 1. V8 CVEs per year, with bugs exploited in the wild in orange. 2026 runs through September 22.

For about ten years, V8 had 20 to 40 CVEs a year. 2024 had 60. 2026 already has more than twice that.

The 2026 jump

The increase did not happen all at once. Figure 2 shows each 2026 V8 CVE on the day its fix shipped, in the style of v8bugs.fyi.

V8 CVEs per release day in 2026

Figure 2. V8 CVEs per Chrome release day in 2026.

January and February look like any recent year, with 4 and 3 CVEs. From March the number goes up every month, reaches 33 in June, and stays around 20 a month after that. Figure 3 shows the same months for all of Chrome.

Chrome and V8 CVEs per month in 2026

Figure 3. CVEs per month in 2026, all of Chrome on the left and V8 on the right.

Chrome follows the same pattern on a much bigger scale, with 1,018 CVEs in June alone. This lines up with the AI research agents we covered in Part 1 becoming publicly available.

Google has said more about this. In July, the Chrome security team wrote that Chrome 149 and 150 fixed 1,072 security bugs, more than the 23 releases before them combined, and described using AI agents built on Gemini both to find bugs and to fix them. The same month, Google DeepMind reported that its security-tuned model, Gemini 3.5 Flash Cyber, found 55 confirmed issues in V8 in a fixed test.

None of this tells us who found each individual bug, so we cannot prove that agents caused the jump. It does show that the jump is real, that it covers all of Chrome, and that the V8 bugs being fixed have changed. The rest of this post looks at what kinds of bugs they are and where in the engine they are.

What kinds of bugs

Figure 4 splits each year by bug class.

Bug classes by year

Figure 4. V8 CVEs by bug class and year. Numbers inside the bars are counts.

From 2021 to 2023, most V8 CVEs were type confusions: 16 of 19 in 2023. 2026 is different. Type confusion is still the biggest class with 32, but use-after-free went from 3 in 2025 to 30. There are also 21 logic bugs (“inappropriate implementation”), 10 integer overflows, and 9 race conditions.

Severity changed too (Figure 5). Every V8 CVE in 2023 was rated High, and about 85% were in 2024 and 2025. In 2026, 81 of 131 are High, 30 are Medium, and 20 are Low.

Severity by year

Figure 5. Chromium severity ratings for V8 CVEs by year.

More bugs, more kinds of bugs, and more lower-severity ones. This is what you would expect if people were searching more widely, not only more deeply. The next sections go through each class, using real 2026 fixes as examples.

Type confusion

V8 keeps a description of each object’s layout, called its map, and tracks what kind of values each array holds. The optimizing compilers generate code for the layouts they have seen, and add checks to make sure those layouts still hold. A type confusion happens when a check is missing or out of date, and the code treats an object as something it is not.

The first example was exploited in the wild. It is in how the compiler inlines Array.prototype.sort [9]. When the arrays passed in had different element kinds, the inlined sort used a combined kind that no single array had to match. It copied the elements, called the user’s compare function, and then wrote the copy back. If the compare function changed the array’s element kind in the meantime, the write-back used the wrong layout. The fix stops inlining sort in this case.

The second example is smaller. The Array.prototype.flat builtin converted a length to a small integer without checking that it was one [10]:

Copy to Clipboard
 // src/builtins/array-flat.tq
-  // The source is a FastJSArray, thus its length must be a Smi.
-  dcheck(Is<Smi>(sourceLength));
-  const smiSourceLength = UnsafeCast<Smi>(sourceLength);
+  const smiSourceLength = Cast<Smi>(sourceLength)
+      otherwise goto Bailout(targetIndex, smiSourceIndex);

The old code assumed the length was a small integer (Smi) and only checked it in debug builds. The fix checks in every build and takes the slow path when the check fails.

Both bugs come down to code trusting a type it never checked.

Out-of-bounds access

Bounds and range checks cost time, so the compilers try to prove they are not needed and remove them. Out-of-bounds bugs often come from a proof that is slightly wrong. One 2026 fix keeps a safe-integer check that TurboFan could drop when turning a number conversion into a 32-bit operation [11]. Another is in instruction selection, the step that turns the compiler’s graph into machine instructions [12]:

Copy to Clipboard
 // src/compiler/backend/x64/instruction-selector-x64.cc
   if (imm == 0) {
-    if (this->Get(left).outputs_rep()[0] ==
-        RegisterRepresentation::Word32()) {
+    if (ZeroExtendsWord32ToWord64(left)) {
       // {EmitIdentity} reuses the virtual register of the first input
-      // for the output. This is exactly what we want here.
+      // for the output. This is only safe if it is known to zero-extend
+      // (as the int32 subtraction advertises itself as zero-extending.)
       EmitIdentity(node);

For x - 0, the compiler reused x as the result whenever x was a 32-bit value. But the result promises that its upper 32 bits are zero, and not every 32-bit value keeps that promise. The fix only reuses x when it does.

Neither bug is a missing if in C++. Both are small mistakes in how the compiler reasons about values.

Use-after-free

A use-after-free is when code keeps using memory after the object in it has been freed. In V8 this usually means code still holds a raw pointer to an object the garbage collector has moved or freed, or uses state left behind after deoptimization.

A 2026 fix shows the classic version [13]. When V8 works with two strings at once, for example to search for one inside the other, it first takes a direct pointer to each string’s characters. For an external string, one whose characters live outside V8’s heap, that pointer does not keep the string alive. Preparing the second string could allocate memory, the allocation could start a garbage collection, and the collection could free the first string’s characters while V8 still pointed at them:

Copy to Clipboard
 // src/objects/string.tq
 macro TwoStringsToSlices<Result: type, Functor: type>(
     s1: String, s2: String, f: Functor): Result {
+  // Flatten both strings before taking either slice. A slice into an external
+  // string is a raw pointer that doesn't keep the string alive, so flattening
+  // s2 while holding s1's slice could collect s1 and free its resource.
+  const s1 = Flatten(s1);
+  const s2 = Flatten(s2);
   try {
     StringToSlice(s1) otherwise FirstOneByte, FirstTwoByte;

The comment is V8’s own. The fix does all the work that can allocate memory before it takes any raw pointers.

Race conditions

V8 compiles code, collects garbage, and more and more often resizes buffers on background threads. A race condition happens when the main thread and a background thread disagree about the same object. One 2026 fix handles a typed array that a background thread grows while Object.values is reading it [14]:

Copy to Clipboard
 // src/objects/elements.cc
       size_t length = AccessorClass::GetCapacityImpl(*object, *elements);
+      // The TypedArray might have been grown by a background thread. Handle it
+      // gracefully.
+      if (length > max_nof_items) {
+        length = max_nof_items;
+      }
       for (size_t index = 0; index < length; ++index) {

The result array was sized before this loop. If another thread grew the typed array in between, the loop could run past the end of the result. The fix caps the loop at the size the result was made for.

Another makes Maglev create a new number object for each deoptimization instead of sharing one [15].

Integer overflow

Integer overflows are usually size calculations: a length multiplied, a separator repeated, or a count that gets too big. Array.prototype.join now checks early for an overflow in the separator length [16]:

Copy to Clipboard
 // src/builtins/array-join.tq
-    // Detect integer overflow
-    // TODO(turbofan): Replace with overflow-checked multiplication.
-    if (sepsLen / separatorLength != nofSeparatorsInt) deferred {
-        ThrowInvalidStringLength(context);
-      }
 ...
+  if (separatorLength > 1 && len > 1) {
+    ...
+      const res = TryInt32Mul(lenInt - 1, sepLenInt) otherwise IfOverflow;
+      if (res > Convert<int32>(kStringMaxLength)) goto IfOverflow;
+    } label IfOverflow {
+      ThrowInvalidStringLength(context);
+    }
+  }

The old code multiplied first and checked for overflow afterwards. The fix does an overflow-checked multiply at the start, before any work is done.

Maglev also now stops compiling when an input node gets too big [17].

Logic bugs

“Inappropriate implementation” covers bugs where V8 does something the language does not allow, without a memory bug in the fix itself. One 2026 example is in the parser: V8 skipped a temporal dead zone check for hoisted functions inside sloppy-mode eval [18]. The fix moves six lines:

Copy to Clipboard
 // src/ast/scopes.cc
     if (scope->outer_scope_ == outer_scope_end) break;
+    if (scope->is_hoisted_in_context()) {
+      *access_position = scope->outer_scope()->start_position();
+    } else if (scope->is_eval_scope()) {
+      *access_position = scope->AsDeclarationScope()->eval_position();
+    }
+
     if (V8_UNLIKELY(scope->is_dynamic_scope())) {
 ...
-    if (scope->is_hoisted_in_context()) {
-      *access_position = scope->outer_scope()->start_position();
-    } else if (scope->is_eval_scope()) {
-      *access_position = scope->AsDeclarationScope()->eval_position();
-    }

V8 decides whether a variable needs a temporal dead zone check by comparing where it is used with where it is declared. The old code updated that position too late for code inside eval, so the check could be skipped.

Another logic bug fix changed how for-of loops return values in the interpreter and all three compilers [19]. To find bugs like these you need to know the JavaScript specification, not only the engine.

Where the bugs are

Figure 6 compares where fixes went in 2019–2025 with where they went in 2026.

Where the fixes land

Figure 6. Share of V8 CVEs whose fix touched each area. A fix can touch more than one area, so the shares add up to more than 100%.

Three things changed.

TurboFan is no longer on top. From 2019 to 2025, 27% of V8 fixes touched TurboFan, far more than any other area. In 2026 that dropped to 15%. The number of TurboFan CVEs still went up, from about 8 a year to 18, but other areas went up faster.

The newer compilers caught up. Maglev, V8’s mid-tier optimizing compiler, went from 7% of fixes to 18% and is now the most common area. Turboshaft, the newer compiler backend used by TurboFan and WebAssembly, went from 5% to 11%. Both are newer code that has been fuzzed for fewer years.

The debugger showed up. The debugger and inspector went from 4% to 16%. This code has always been part of V8, but security research rarely looked at it, partly because web pages usually cannot reach it.

In 2026, 19 CVEs had fixes in debugger or inspector code. 11 of them are use-after-frees, and only 5 are rated High, which fits code that usually needs a debugger attached.

A closer look at Maglev

Maglev is V8’s mid-tier optimizing compiler. It shipped in Chrome 117 in 2023 and sits between Sparkplug, which compiles quickly but simply, and TurboFan, which compiles slowly but makes the fastest code. Its job is to make good-enough code quickly.

In 2026, 22 V8 CVEs had fixes in Maglev. 19 of them are rated High and 12 are type confusions, and one of them, the sort bug above, was exploited in the wild. Unlike the debugger bugs, these are the kind attackers look for.

Several are the same mistake in one place. To make code faster, Maglev skips checks it can prove are not needed. One of these checks makes sure a value is a small integer, which V8 calls a Smi. At least five 2026 fixes tighten when Maglev may skip it, and two of them change the same function [20][21]:

Copy to Clipboard
 // src/maglev/maglev-graph-builder.cc, in BuildCheckSmi()
-  // For constants, we may be able to skip the runtime check.
-  if (std::optional<int32_t> constant_value = TryGetInt32Constant(object)) {
-    if (Smi::IsValid(constant_value.value())) return object;
+  // For non-tagged constants, we may be able to skip the runtime check ...
+  if (object->value_representation() != ValueRepresentation::kTagged) {
+    if (std::optional<int32_t> constant_value = TryGetInt32Constant(object)) {
+      if (Smi::IsValid(constant_value.value())) return object;
+    }
   }

Maglev skipped the check when it knew a value was equal to a small number. But a JavaScript value can equal a small number and still be stored as a full number object instead of a Smi. The fix only skips the check for raw machine numbers, where the value is all that matters.

What still gets exploited

Bugs used in real attacks look different. Since 2016, Chrome has published 75 CVEs exploited in the wild, and 41 of them were in V8. Figure 7 compares the bug classes of all V8 CVEs in that period with the exploited ones.

Found vs exploited

Figure 7. Bug class share of all V8 CVEs since 2016 (grey) and of those exploited in the wild (orange).

Type confusions are 31% of all V8 CVEs but 46% of the exploited ones. Use-after-frees go the other way: 14% of all V8 CVEs, but only 5% of the exploited ones.

Figure 8 shows the same gap by area.

Where exploited bugs are fixed

Figure 8. Share of V8 CVEs since 2016 whose fix touched each area, for all V8 CVEs (grey) and for those exploited in the wild (orange).

TurboFan appears in 23% of all V8 fixes but in 50% of the exploited ones. WebAssembly goes the other way, 16% against 6%, and none of the exploited bugs were fixed in the debugger.

In 2026, 4 of the 131 V8 CVEs were exploited in the wild: one type confusion (the sort bug above [9]), two out-of-bounds bugs, and one logic bug. That is too few to show a trend on its own.

A note on the V8 sandbox

Not every V8 security fix gets a CVE. The V8 sandbox is meant to keep memory corruption contained inside the engine, and it gets its own hardening fixes, which usually have no CVE. v8bugs.fyi tracks these separately and counts 128 so far in 2026. The fix titles we looked at cover the same kinds of problems as above: races on shared tables, values read twice from memory an attacker can change, and state passed between trusted and untrusted objects.

What this means for researchers

More V8 bugs are being found, in more parts of the engine, and Google says AI now helps both find and fix them. That is good for users.

But the bugs attackers use have barely changed. Type confusions are still the most common kind, often in the optimizing compilers. And since 2023, the new findings have looked less like them each year: type confusions fell from 84% of V8 CVEs in 2023 to 24% in 2026, while they make up 46% of the V8 bugs exploited in the wild.

That gap is where we think research time is best spent. Finding these bugs takes deep knowledge of one part of the engine and a way to aim a search at it, which is what we built SeekZero for.

References

  1. Tashita, “AI Browser Vulnerability Research Part 1: The Rise of AI-Driven Browser Vulnerability Research.” https://tashita.net/ai-browser-vulnerability-research-part-1/
  2. v8-cves. https://github.com/str8outtaheap/v8-cves
  3. chrome-cves. https://github.com/str8outtaheap/chrome-cves
  4. Chrome Releases. https://chromereleases.googleblog.com/
  5. V8 blog, “Maglev – V8’s Fastest Optimizing JIT,” December 5, 2023. https://v8.dev/blog/maglev
  6. v8bugs.fyi. https://v8bugs.fyi/
  7. Chrome Security Team, “Stronger with every update: How we’re making Chrome and the web safer in the AI Era,” July 30, 2026. https://blog.google/security/chrome-stronger-with-every-update/
  8. Google DeepMind, “Introducing Gemini 3.5 Flash Cyber,” July 21, 2026. https://deepmind.google/blog/introducing-gemini-3-5-flash-cyber/
  9. CVE-2026-85046 fix: “[compiler] Don’t inline Array.prototype.sort on mixed elements kinds.” https://chromium.googlesource.com/v8/v8/+/e0562d87ad9c17042b581582c99237d798572e67
  10. CVE-2026-87564 fix: “[builtins] Fix Array.prototype.flat.” https://chromium.googlesource.com/v8/v8/+/50a6c0c69c1384720d9c3599dadfa73e55b865d7
  11. CVE-2026-15903 fix: “[turbofan] Keep safe-integer check in ToNumber Word32 lowering.” https://chromium.googlesource.com/v8/v8/+/a7835b8c294c97d6d6cbfc22582de40cfcbf0c19
  12. CVE-2026-95304 fix: “[compiler] ISel: Fix VisitInt32Sub for 32-bit lhs.” https://chromium.googlesource.com/v8/v8/+/057dcc408de1583cf68c4034bbba89d0f78a88a4
  13. CVE-2026-19556 fix: “[string] Fix UAF in TwoStringsToSlices with external strings.” https://chromium.googlesource.com/v8/v8/+/9062318d79629d4084053747fec654a1a49dc690
  14. CVE-2026-5893 fix: “[RAB/GSAB] Object.values: Handle a TA grown by a background thread gracefully.” https://chromium.googlesource.com/v8/v8/+/02cd73dfc58dbe73054382c85f9f126af9ce597e
  15. CVE-2026-85045 fix: “[maglev] Materialize a fresh HeapNumber per deoptimization.” https://chromium.googlesource.com/v8/v8/+/7d5eb1df2a721c8fe3aa03b999464c4b86242080
  16. CVE-2026-14430 fix: “Check early for separator overflow in Array.prototype.join().” https://chromium.googlesource.com/v8/v8/+/8c3c9f464a45a8809581ff45382bb46d01eb9171
  17. CVE-2025-10892 fix: “[maglev] Abort compilation if input node is too big.” https://chromium.googlesource.com/v8/v8/+/618d1a029005753ca036166b9b0684c7bc333077
  18. CVE-2026-10935 fix: “[ast] Fix hole check elision for hoisted functions in sloppy eval.” https://chromium.googlesource.com/v8/v8/+/ad07ea9d88fd5c2a529d35fb4ebec046f5404b99
  19. CVE-2026-14383 fix: “[for-of] Use a single return value.” https://chromium.googlesource.com/v8/v8/+/02a725e2a0762f39266fdac2ba2869c71af37ed2
  20. CVE-2026-7337 fix: “[maglev] Restrict BuildCheckSmi constant elision to non-tagged inputs.” https://chromium.googlesource.com/v8/v8/+/cc9a97f192319ed218c191fa20cbecd1bf51cb71
  21. CVE-2026-5865 fix: “[maglev] Avoid eliding Smi checks too aggressively.” https://chromium.googlesource.com/v8/v8/+/fac60cc3fdf84591594354fb3629a2fcb376ff23

Keep reading

Related research

Learn more about SeekZero, our browser vulnerability research framework, or browse all posts on the Tashita research blog.

Vulnerability Survey

V8 Bugs in the Agentic Era

Vulnerability Survey

V8 Bugs in the Agentic Era

September 29, 2026

By the end of September 2026, Chrome had fixed 131 V8 CVEs this year. That is more than 2024 and 2025 together (60 and 48). Chrome as a whole jumped even more: 2,908 CVEs so far in 2026, compared with 190 in all of 2025.

In Part 1 of our AI series, we described how language models went from helping with code review to working as research agents that read code, come up with ideas, and test them. This post looks at the bugs themselves. If agents now help find V8 bugs, the public record should show it: in how many bugs get fixed, in what kinds of bugs they are, and in where they are in the engine.

We looked at every V8 CVE Chrome has published since 2009, the commit that fixed each one, and the bugs known to have been exploited in the wild.

TLDR: far more bugs are being found, in more places and of more kinds. The bugs that attackers actually use still look the way they always have.

Data and method

We started from two public lists: v8-cves and chrome-cves. Both come from the Chrome Releases blog. For each CVE they record the description, Chromium’s severity rating, whether it was exploited in the wild, the issue number, the fix commits, and the Chrome release with the fix. The V8 list has 544 CVEs and the Chrome list has 6,709.

We added two things:

  • Bug class. Chrome describes each CVE in a few words, such as “Type confusion in V8” or “Use after free in V8”. We sorted these into type confusion, out-of-bounds access, use-after-free, race condition, integer overflow, and “inappropriate implementation”, which is Chromium’s name for logic bugs.
  • Area. For each CVE we downloaded the V8 fix commit and noted which source folders it changed, ignoring tests. src/compiler/ counts as TurboFan, src/compiler/turboshaft/ as Turboshaft, src/maglev/ as Maglev, and so on. This worked for 491 of the 493 fix commits.

A few caveats:

  • Chrome describes each bug in a few words, so some bugs could fit more than one class.
  • We look at where a bug was fixed, which is not always where it started.
  • A bug is counted in the year its fix shipped, not the year it was found.
  • 2026 isn’t over yet, so its numbers will only go up.

Figure 1 shows every year since 2009.

V8 CVEs per year

Figure 1. V8 CVEs per year, with bugs exploited in the wild in orange. 2026 runs through September 22.

For about ten years, V8 had 20 to 40 CVEs a year. 2024 had 60. 2026 already has more than twice that.

The 2026 jump

The increase did not happen all at once. Figure 2 shows each 2026 V8 CVE on the day its fix shipped, in the style of v8bugs.fyi.

V8 CVEs per release day in 2026

Figure 2. V8 CVEs per Chrome release day in 2026.

January and February look like any recent year, with 4 and 3 CVEs. From March the number goes up every month, reaches 33 in June, and stays around 20 a month after that. Figure 3 shows the same months for all of Chrome.

Chrome and V8 CVEs per month in 2026

Figure 3. CVEs per month in 2026, all of Chrome on the left and V8 on the right.

Chrome follows the same pattern on a much bigger scale, with 1,018 CVEs in June alone. This lines up with the AI research agents we covered in Part 1 becoming publicly available.

Google has said more about this. In July, the Chrome security team wrote that Chrome 149 and 150 fixed 1,072 security bugs, more than the 23 releases before them combined, and described using AI agents built on Gemini both to find bugs and to fix them. The same month, Google DeepMind reported that its security-tuned model, Gemini 3.5 Flash Cyber, found 55 confirmed issues in V8 in a fixed test.

None of this tells us who found each individual bug, so we cannot prove that agents caused the jump. It does show that the jump is real, that it covers all of Chrome, and that the V8 bugs being fixed have changed. The rest of this post looks at what kinds of bugs they are and where in the engine they are.

What kinds of bugs

Figure 4 splits each year by bug class.

Bug classes by year

Figure 4. V8 CVEs by bug class and year. Numbers inside the bars are counts.

From 2021 to 2023, most V8 CVEs were type confusions: 16 of 19 in 2023. 2026 is different. Type confusion is still the biggest class with 32, but use-after-free went from 3 in 2025 to 30. There are also 21 logic bugs (“inappropriate implementation”), 10 integer overflows, and 9 race conditions.

Severity changed too (Figure 5). Every V8 CVE in 2023 was rated High, and about 85% were in 2024 and 2025. In 2026, 81 of 131 are High, 30 are Medium, and 20 are Low.

Severity by year

Figure 5. Chromium severity ratings for V8 CVEs by year.

More bugs, more kinds of bugs, and more lower-severity ones. This is what you would expect if people were searching more widely, not only more deeply. The next sections go through each class, using real 2026 fixes as examples.

Type confusion

V8 keeps a description of each object’s layout, called its map, and tracks what kind of values each array holds. The optimizing compilers generate code for the layouts they have seen, and add checks to make sure those layouts still hold. A type confusion happens when a check is missing or out of date, and the code treats an object as something it is not.

The first example was exploited in the wild. It is in how the compiler inlines Array.prototype.sort [9]. When the arrays passed in had different element kinds, the inlined sort used a combined kind that no single array had to match. It copied the elements, called the user’s compare function, and then wrote the copy back. If the compare function changed the array’s element kind in the meantime, the write-back used the wrong layout. The fix stops inlining sort in this case.

The second example is smaller. The Array.prototype.flat builtin converted a length to a small integer without checking that it was one [10]:

Copy to Clipboard
 // src/builtins/array-flat.tq
-  // The source is a FastJSArray, thus its length must be a Smi.
-  dcheck(Is<Smi>(sourceLength));
-  const smiSourceLength = UnsafeCast<Smi>(sourceLength);
+  const smiSourceLength = Cast<Smi>(sourceLength)
+      otherwise goto Bailout(targetIndex, smiSourceIndex);

The old code assumed the length was a small integer (Smi) and only checked it in debug builds. The fix checks in every build and takes the slow path when the check fails.

Both bugs come down to code trusting a type it never checked.

Out-of-bounds access

Bounds and range checks cost time, so the compilers try to prove they are not needed and remove them. Out-of-bounds bugs often come from a proof that is slightly wrong. One 2026 fix keeps a safe-integer check that TurboFan could drop when turning a number conversion into a 32-bit operation [11]. Another is in instruction selection, the step that turns the compiler’s graph into machine instructions [12]:

Copy to Clipboard
 // src/compiler/backend/x64/instruction-selector-x64.cc
   if (imm == 0) {
-    if (this->Get(left).outputs_rep()[0] ==
-        RegisterRepresentation::Word32()) {
+    if (ZeroExtendsWord32ToWord64(left)) {
       // {EmitIdentity} reuses the virtual register of the first input
-      // for the output. This is exactly what we want here.
+      // for the output. This is only safe if it is known to zero-extend
+      // (as the int32 subtraction advertises itself as zero-extending.)
       EmitIdentity(node);

For x - 0, the compiler reused x as the result whenever x was a 32-bit value. But the result promises that its upper 32 bits are zero, and not every 32-bit value keeps that promise. The fix only reuses x when it does.

Neither bug is a missing if in C++. Both are small mistakes in how the compiler reasons about values.

Use-after-free

A use-after-free is when code keeps using memory after the object in it has been freed. In V8 this usually means code still holds a raw pointer to an object the garbage collector has moved or freed, or uses state left behind after deoptimization.

A 2026 fix shows the classic version [13]. When V8 works with two strings at once, for example to search for one inside the other, it first takes a direct pointer to each string’s characters. For an external string, one whose characters live outside V8’s heap, that pointer does not keep the string alive. Preparing the second string could allocate memory, the allocation could start a garbage collection, and the collection could free the first string’s characters while V8 still pointed at them:

Copy to Clipboard
 // src/objects/string.tq
 macro TwoStringsToSlices<Result: type, Functor: type>(
     s1: String, s2: String, f: Functor): Result {
+  // Flatten both strings before taking either slice. A slice into an external
+  // string is a raw pointer that doesn't keep the string alive, so flattening
+  // s2 while holding s1's slice could collect s1 and free its resource.
+  const s1 = Flatten(s1);
+  const s2 = Flatten(s2);
   try {
     StringToSlice(s1) otherwise FirstOneByte, FirstTwoByte;

The comment is V8’s own. The fix does all the work that can allocate memory before it takes any raw pointers.

Race conditions

V8 compiles code, collects garbage, and more and more often resizes buffers on background threads. A race condition happens when the main thread and a background thread disagree about the same object. One 2026 fix handles a typed array that a background thread grows while Object.values is reading it [14]:

Copy to Clipboard
 // src/objects/elements.cc
       size_t length = AccessorClass::GetCapacityImpl(*object, *elements);
+      // The TypedArray might have been grown by a background thread. Handle it
+      // gracefully.
+      if (length > max_nof_items) {
+        length = max_nof_items;
+      }
       for (size_t index = 0; index < length; ++index) {

The result array was sized before this loop. If another thread grew the typed array in between, the loop could run past the end of the result. The fix caps the loop at the size the result was made for.

Another makes Maglev create a new number object for each deoptimization instead of sharing one [15].

Integer overflow

Integer overflows are usually size calculations: a length multiplied, a separator repeated, or a count that gets too big. Array.prototype.join now checks early for an overflow in the separator length [16]:

Copy to Clipboard
 // src/builtins/array-join.tq
-    // Detect integer overflow
-    // TODO(turbofan): Replace with overflow-checked multiplication.
-    if (sepsLen / separatorLength != nofSeparatorsInt) deferred {
-        ThrowInvalidStringLength(context);
-      }
 ...
+  if (separatorLength > 1 && len > 1) {
+    ...
+      const res = TryInt32Mul(lenInt - 1, sepLenInt) otherwise IfOverflow;
+      if (res > Convert<int32>(kStringMaxLength)) goto IfOverflow;
+    } label IfOverflow {
+      ThrowInvalidStringLength(context);
+    }
+  }

The old code multiplied first and checked for overflow afterwards. The fix does an overflow-checked multiply at the start, before any work is done.

Maglev also now stops compiling when an input node gets too big [17].

Logic bugs

“Inappropriate implementation” covers bugs where V8 does something the language does not allow, without a memory bug in the fix itself. One 2026 example is in the parser: V8 skipped a temporal dead zone check for hoisted functions inside sloppy-mode eval [18]. The fix moves six lines:

Copy to Clipboard
 // src/ast/scopes.cc
     if (scope->outer_scope_ == outer_scope_end) break;
+    if (scope->is_hoisted_in_context()) {
+      *access_position = scope->outer_scope()->start_position();
+    } else if (scope->is_eval_scope()) {
+      *access_position = scope->AsDeclarationScope()->eval_position();
+    }
+
     if (V8_UNLIKELY(scope->is_dynamic_scope())) {
 ...
-    if (scope->is_hoisted_in_context()) {
-      *access_position = scope->outer_scope()->start_position();
-    } else if (scope->is_eval_scope()) {
-      *access_position = scope->AsDeclarationScope()->eval_position();
-    }

V8 decides whether a variable needs a temporal dead zone check by comparing where it is used with where it is declared. The old code updated that position too late for code inside eval, so the check could be skipped.

Another logic bug fix changed how for-of loops return values in the interpreter and all three compilers [19]. To find bugs like these you need to know the JavaScript specification, not only the engine.

Where the bugs are

Figure 6 compares where fixes went in 2019–2025 with where they went in 2026.

Where the fixes land

Figure 6. Share of V8 CVEs whose fix touched each area. A fix can touch more than one area, so the shares add up to more than 100%.

Three things changed.

TurboFan is no longer on top. From 2019 to 2025, 27% of V8 fixes touched TurboFan, far more than any other area. In 2026 that dropped to 15%. The number of TurboFan CVEs still went up, from about 8 a year to 18, but other areas went up faster.

The newer compilers caught up. Maglev, V8’s mid-tier optimizing compiler, went from 7% of fixes to 18% and is now the most common area. Turboshaft, the newer compiler backend used by TurboFan and WebAssembly, went from 5% to 11%. Both are newer code that has been fuzzed for fewer years.

The debugger showed up. The debugger and inspector went from 4% to 16%. This code has always been part of V8, but security research rarely looked at it, partly because web pages usually cannot reach it.

In 2026, 19 CVEs had fixes in debugger or inspector code. 11 of them are use-after-frees, and only 5 are rated High, which fits code that usually needs a debugger attached.

A closer look at Maglev

Maglev is V8’s mid-tier optimizing compiler. It shipped in Chrome 117 in 2023 and sits between Sparkplug, which compiles quickly but simply, and TurboFan, which compiles slowly but makes the fastest code. Its job is to make good-enough code quickly.

In 2026, 22 V8 CVEs had fixes in Maglev. 19 of them are rated High and 12 are type confusions, and one of them, the sort bug above, was exploited in the wild. Unlike the debugger bugs, these are the kind attackers look for.

Several are the same mistake in one place. To make code faster, Maglev skips checks it can prove are not needed. One of these checks makes sure a value is a small integer, which V8 calls a Smi. At least five 2026 fixes tighten when Maglev may skip it, and two of them change the same function [20][21]:

Copy to Clipboard
 // src/maglev/maglev-graph-builder.cc, in BuildCheckSmi()
-  // For constants, we may be able to skip the runtime check.
-  if (std::optional<int32_t> constant_value = TryGetInt32Constant(object)) {
-    if (Smi::IsValid(constant_value.value())) return object;
+  // For non-tagged constants, we may be able to skip the runtime check ...
+  if (object->value_representation() != ValueRepresentation::kTagged) {
+    if (std::optional<int32_t> constant_value = TryGetInt32Constant(object)) {
+      if (Smi::IsValid(constant_value.value())) return object;
+    }
   }

Maglev skipped the check when it knew a value was equal to a small number. But a JavaScript value can equal a small number and still be stored as a full number object instead of a Smi. The fix only skips the check for raw machine numbers, where the value is all that matters.

What still gets exploited

Bugs used in real attacks look different. Since 2016, Chrome has published 75 CVEs exploited in the wild, and 41 of them were in V8. Figure 7 compares the bug classes of all V8 CVEs in that period with the exploited ones.

Found vs exploited

Figure 7. Bug class share of all V8 CVEs since 2016 (grey) and of those exploited in the wild (orange).

Type confusions are 31% of all V8 CVEs but 46% of the exploited ones. Use-after-frees go the other way: 14% of all V8 CVEs, but only 5% of the exploited ones.

Figure 8 shows the same gap by area.

Where exploited bugs are fixed

Figure 8. Share of V8 CVEs since 2016 whose fix touched each area, for all V8 CVEs (grey) and for those exploited in the wild (orange).

TurboFan appears in 23% of all V8 fixes but in 50% of the exploited ones. WebAssembly goes the other way, 16% against 6%, and none of the exploited bugs were fixed in the debugger.

In 2026, 4 of the 131 V8 CVEs were exploited in the wild: one type confusion (the sort bug above [9]), two out-of-bounds bugs, and one logic bug. That is too few to show a trend on its own.

A note on the V8 sandbox

Not every V8 security fix gets a CVE. The V8 sandbox is meant to keep memory corruption contained inside the engine, and it gets its own hardening fixes, which usually have no CVE. v8bugs.fyi tracks these separately and counts 128 so far in 2026. The fix titles we looked at cover the same kinds of problems as above: races on shared tables, values read twice from memory an attacker can change, and state passed between trusted and untrusted objects.

What this means for researchers

More V8 bugs are being found, in more parts of the engine, and Google says AI now helps both find and fix them. That is good for users.

But the bugs attackers use have barely changed. Type confusions are still the most common kind, often in the optimizing compilers. And since 2023, the new findings have looked less like them each year: type confusions fell from 84% of V8 CVEs in 2023 to 24% in 2026, while they make up 46% of the V8 bugs exploited in the wild.

That gap is where we think research time is best spent. Finding these bugs takes deep knowledge of one part of the engine and a way to aim a search at it, which is what we built SeekZero for.

References

  1. Tashita, “AI Browser Vulnerability Research Part 1: The Rise of AI-Driven Browser Vulnerability Research.” https://tashita.net/ai-browser-vulnerability-research-part-1/
  2. v8-cves. https://github.com/str8outtaheap/v8-cves
  3. chrome-cves. https://github.com/str8outtaheap/chrome-cves
  4. Chrome Releases. https://chromereleases.googleblog.com/
  5. V8 blog, “Maglev – V8’s Fastest Optimizing JIT,” December 5, 2023. https://v8.dev/blog/maglev
  6. v8bugs.fyi. https://v8bugs.fyi/
  7. Chrome Security Team, “Stronger with every update: How we’re making Chrome and the web safer in the AI Era,” July 30, 2026. https://blog.google/security/chrome-stronger-with-every-update/
  8. Google DeepMind, “Introducing Gemini 3.5 Flash Cyber,” July 21, 2026. https://deepmind.google/blog/introducing-gemini-3-5-flash-cyber/
  9. CVE-2026-85046 fix: “[compiler] Don’t inline Array.prototype.sort on mixed elements kinds.” https://chromium.googlesource.com/v8/v8/+/e0562d87ad9c17042b581582c99237d798572e67
  10. CVE-2026-87564 fix: “[builtins] Fix Array.prototype.flat.” https://chromium.googlesource.com/v8/v8/+/50a6c0c69c1384720d9c3599dadfa73e55b865d7
  11. CVE-2026-15903 fix: “[turbofan] Keep safe-integer check in ToNumber Word32 lowering.” https://chromium.googlesource.com/v8/v8/+/a7835b8c294c97d6d6cbfc22582de40cfcbf0c19
  12. CVE-2026-95304 fix: “[compiler] ISel: Fix VisitInt32Sub for 32-bit lhs.” https://chromium.googlesource.com/v8/v8/+/057dcc408de1583cf68c4034bbba89d0f78a88a4
  13. CVE-2026-19556 fix: “[string] Fix UAF in TwoStringsToSlices with external strings.” https://chromium.googlesource.com/v8/v8/+/9062318d79629d4084053747fec654a1a49dc690
  14. CVE-2026-5893 fix: “[RAB/GSAB] Object.values: Handle a TA grown by a background thread gracefully.” https://chromium.googlesource.com/v8/v8/+/02cd73dfc58dbe73054382c85f9f126af9ce597e
  15. CVE-2026-85045 fix: “[maglev] Materialize a fresh HeapNumber per deoptimization.” https://chromium.googlesource.com/v8/v8/+/7d5eb1df2a721c8fe3aa03b999464c4b86242080
  16. CVE-2026-14430 fix: “Check early for separator overflow in Array.prototype.join().” https://chromium.googlesource.com/v8/v8/+/8c3c9f464a45a8809581ff45382bb46d01eb9171
  17. CVE-2025-10892 fix: “[maglev] Abort compilation if input node is too big.” https://chromium.googlesource.com/v8/v8/+/618d1a029005753ca036166b9b0684c7bc333077
  18. CVE-2026-10935 fix: “[ast] Fix hole check elision for hoisted functions in sloppy eval.” https://chromium.googlesource.com/v8/v8/+/ad07ea9d88fd5c2a529d35fb4ebec046f5404b99
  19. CVE-2026-14383 fix: “[for-of] Use a single return value.” https://chromium.googlesource.com/v8/v8/+/02a725e2a0762f39266fdac2ba2869c71af37ed2
  20. CVE-2026-7337 fix: “[maglev] Restrict BuildCheckSmi constant elision to non-tagged inputs.” https://chromium.googlesource.com/v8/v8/+/cc9a97f192319ed218c191fa20cbecd1bf51cb71
  21. CVE-2026-5865 fix: “[maglev] Avoid eliding Smi checks too aggressively.” https://chromium.googlesource.com/v8/v8/+/fac60cc3fdf84591594354fb3629a2fcb376ff23

Keep reading

Related research

Learn more about SeekZero, our browser vulnerability research framework, or browse all posts on the Tashita research blog.