Journey to Root, Episode I: The Maglev King A team of security researchers used AI tools including Gemini, Claude, and Codex to discover and exploit five vulnerabilities in Google Chrome, earning $117,000 in bug bounties. The three-month effort targeted V8's Maglev compiler and GPU process, with AI accelerating heap spray tuning and memory recovery via Sterbenz's lemma. The series details the full exploit chain on Windows and a novel Linux technique. Journey to Root, Episode I: The Maglev King Hacking Chrome with AI Table of Contents Introduction Welcome to the first installment of our series on AI-assisted browser bug hunting and exploit development. We will walk you through a Google Chrome exploit chain, from the V8 JavaScript engine in the renderer to the GPU process. We demonstrate the full chain on Windows, and for the GPU compromise we also show a novel technique on Linux, where the mitigations are tougher to defeat. The exploit chain, comprising 5 vulnerabilities, took 3 months of part-time effort to go from initial discovery to complete exploitation. We found the first bug in March 2026, started working on this chain in April, and had a reliable exploit by the first week of June. We used a mixture of tools Gemini, Claude, and Codex to complete the majority of the work, and collected a total of US$117,000 in the Chrome Vulnerability Reward Program. AI carried a large share of this work, and it accelerated nearly every stage, from fine-tuning a heap spray to applying a math lemma to recover memory bytes. At one point we were stuck beating ASLR in the GPU process. We could read memory, but it came back to us as float coordinates rendered as pixels. We didn't know how to convert those pixels back to raw bytes, until Claude pointed us to Sterbenz's lemma, which makes the subtraction in our refinement loop exact and recovers bytes losslessly. It certainly helps when your collaborator has most of human knowledge on call. Even so, our human researchers stayed in the driver's seat, steering the tools and making the calls that mattered. Neither side could have done this alone. The AI needed experienced hands to aim it and check its work, and the researchers needed the AI both to cover ground that would otherwise have taken far longer and, at times, to connect a dot that lay outside their own expertise, like the lemma above. We grew up watching "Journey to the West": the Monkey King escorting the monk Xuanzang from Tang China to India to fetch the Buddhist scriptures, clearing 81 tribulations along the way. Breaking a modern browser is its own road West. The scripture waiting at the end is code execution deep inside Chrome, and each vulnerability in the chain is one more tribulation between us and it. AI lent us something like the Monkey King's own magic, the somersault cloud that crosses in a single leap ground that once took us days on foot, and the 72 transformations that slip past obstacles brute force alone could not. So consider this series our pilgrimage, and Episode I begins with the Maglev King: a bug in V8's Maglev compiler. Here is what the chain looks like in action: You'll get the most out of this series by following along in the testing environment below: Windows 11 x64 Build 26200.8457 V8 version 14.6.202.33, commit f09a91282a26caa91d016c962d785d852cfdec36 You can find the PoCs in poc/ poc/ . To follow along, you'll want a build of d8 the V8 developer shell at the pinned commit above; the covers building poc/README.md README.md d8 and running each PoC.We also assume some basic knowledge of browser exploitation. If that's new to you, LiveOverflow's browser exploitation series https://liveoverflow.com/topic/browser-exploitation/ is a good starting point. Samuel Groß's Phrack article Attacking JavaScript Engines https://phrack.org/papers/attacking javascript engines.html and his JITSploitation series https://projectzero.google/2020/09/jitsploitation-one.html on Project Zero are seminal work in this space. We also recently invited Sam to give a talk on the state of browser exploitation https://www.youtube.com/watch?v=maWnIKH3JQI . Although not necessary, we recommend feeding our articles to your favourite AI agent and asking it anything you might find unclear. As noted in our Coruna blog post https://blog.calif.io/p/learning-to-jailbreak-an-iphone-with , AI is the best teacher one can find these days. Why Do We Do This? We started doing this for fun, as a way to measure how far our capabilities can be accelerated with the help of AI, and we have no financial motivations behind this effort. The results came out spectacular in terms of how fast we achieved it: The first vulnerability in the GPU process was found at the beginning of March 2026. About 1 month later, the remaining vulnerabilities were found in the V8 engine. We finished the exploits for the renderer vulnerabilities within 2 weeks of their discovery and reported them to Google. At the beginning of May 2026, we completed the exploit chain demonstration for Windows. Reliability fell short of expectations, so we spent a couple more weeks tuning the exploit on and off, reaching ~100% afterward. All of this was accomplished by a team of 3 part-time members. The researcher who found all those bugs, Quang Luong, had virtually no background in browser research before this run. In the past, the same effort by top offensive security firms would have taken from half to a full year, from initial discovery to a reliable exploit, and they basically worked full-time. From a pure impact standpoint, this acceleration means vendors would have to adapt just as fast as threat actors; disclosure policies and security fix deadlines would never be the same again. This research is driven by the belief that open-source knowledge helps the community grow, similar to open-source software. In the past few years, such in-depth research was limited to an "elite circle", making it hard for beginners and "outsiders" to access. Since early V8 exploits 2017-2018 , novel research has declined due to financial and scarcity incentives to keep it private. This has created barriers for new researchers, so fewer novel vulnerability classes and exploitation techniques get disclosed, and security improvements arrive late. Much public research is outdated by the time it appears at conferences, and modern exploits evolve rapidly, leaving fundamentals behind. While determined hackers can still find their way, Chromium's security and fast patch-release cycle show that the open-source model works. It’s increasingly likely that vulnerabilities discovered and exploited by LLMs are also publicly known, which changes where cybersecurity threats come from. So, Where Were All The Humans? How did we use AI here? Our guiding principle is that the human is the captain and the AI is the steering wheel. AI shines at repetitive procedural work, while reasoning and verification still need human hands. We often had to fight the steering wheel like the Captain above. This was not "Claude exploit Chrome full chain, make no mistake"; we tried to be the major origin of ideas, direction, and verification. AI is indisputably better at holding a lot of things in working memory thanks to its large context window, giving it some big-brain moments such as the aforementioned application of Sterbenz's lemma , though we did observe it struggle at times. Here are the times we took the W in and when Claude, Codex, and Gemini did: Human W: The decisive factor in discovering these vulnerabilities, especially the GPU ones found with Gemini, was in how we choose the information to feed into AI . We did this without any agentic action. We essentially told AI to give us a candidate shortlist of exploitable bugs, then we looked at it, talking back and forth to confirm or reject the candidates. This methodology was discussed by our team member Quang Luong at the recent Real World AI Security conference https://seclab.stanford.edu/RealWorldAIsec/ at Stanford University.Human W: We tried leaving Claude to work on its own to port the exploit chain from the V8 interactive shell to the Chrome browser, where we could no longer use the %TerminateExecution built-in as a realistic trigger for the JSPI bug. To our surprise, it never discovered the faking of this internal exception despite trying many strategies. We gave it a single nudge: “Fake Terminate Exception object”, and it figured it out immediately.AI W: A big part of this exploit chain is spraying to reclaim the stale memory and shaping the heap to our advantage. If you have ever exploited these kinds of vulnerabilities in browsers, you know how fragile it can be at times. We didn't do it manually; Claude and Codex did it for us with very little guidance. We only specified how reliable we wanted the exploit to be, and when they finished, we tested it to confirm the claimed reliability. In addition, to avoid unknowingly triggering garbage collection and messing up the heap layout during early primitives construction, they set up part of the exploit primitives in a “critical zone” where nothing must change to ensure heap stability. This is the kind of work that, given sufficient time and effort, we can still do it, but we felt like spending our time on other tasks than tuning exploits byte-by-byte. There are a lot of takes in the security community that, in our opinion, fail to recognize the full picture in such efforts: they either believe that AI has reached the point where it can effortlessly perform this end-to-end with a 100% success rate all the time, or that it is nowhere near human capabilities. From our experience over the past few months working on this, we believe the reality and the future lie somewhere in between: the strongest, most capable researchers will be the ones who know how to drive powerful AI in the right way and in the right direction, and sometimes get crazy ideas from it. This may be a hard pill to swallow for some people who feel like things they have spent years studying and doing can suddenly be done somewhat effortlessly. As humans, we have had those feelings too. However, this is exactly why we started this project: to find out where humans stand in this ever-changing wave by looking at the full picture. As of right now, we’ve observed that most repetitive procedural work and simple, localized reasoning work are what LLMs and AI agents excel at. Those are the kinds of work that, given sufficient human time, attention, and a willingness to overcome boredom, can be accomplished without AI, as they have been for years. For example: Tracing the data flow of a variable within a function and nearby functions Renaming variables and functions, creating structures in reverse engineering work Repeatedly tuning heap spray parameters in exploits On the contrary, there are kinds of work where we still have an edge compared to AI: Choosing which information to give attention to, which is crucial, given the large but still limited context window of LLMs. Case-by-case insight into the exploit development process, which can only be built by exposure and experience. Long reasoning chain across many abstraction layers. Of course, it isn’t always a clear split. In the end, it all comes down to what we, as human beings, want to achieve. There isn’t an obvious difference between an all-human and an all-AI bug discovery or exploit, and the line is getting harder to tell day by day. Do you want to train to become the master of the craft yourself, or do you want to train to solely feel good about owning the products of the craft? This is where it makes a difference: a true master can tell good from bad like telling gold from slop , and a sole feel-good owner can’t. We hate to be the bearer of bad or just real news, but it is what it is, so pick your poison. And yes, this post and series is proudly brought to you by a human writer. We can let machines handle the technical work and fact-check us on grammar, typos, and technical details, but we believe there is value in communicating with each other as humans. So we are deeply grateful that you read it yourself. Anatomy Of An Exploit Chain To understand how attackers can compromise modern web browsers, we start with an example of the Chromium browser architecture, as illustrated below: Chromium browser architecture simplified As with most modern web browsers, Chromium employs a multi-process architecture with highly specialized processes and strict separation of privilege levels. The implementation varies slightly by operating system, but the idea is least privilege. Each process gets only what it needs for its job, so a compromised low-privilege process does not bring down the whole browser. In a typical web browser usage scenario, web content, such as HTML/CSS/JS, is treated as untrusted data by the browser. The first thing a user interacts with in a browser session is the renderer process, which houses all that untrusted data. It is therefore reasonable to assume that the renderer is the most likely to be compromised first when attacking a web browser, and that it must be the most contained, with the least privileges, of all processes in the web browser process tree. As shown in the model above, to compromise the entire web browser, one typically starts in the V8 JavaScript engine or the rendering engine, then either breaks the browser sandbox boundary by directly compromising the browser process or the underlying OS kernel, or takes a detour through more privileged but still restricted processes, such as the GPU process. Depending on the purpose of the exploit chain, the destination can be anywhere on the path from the renderer process to the OS kernel. Our exploit chain consists of 2 parts: compromising the renderer and then the GPU process. For the renderer compromise, we used 3 vulnerabilities: 1 in Maglev compiler optimizations for the initial caged primitives, 1 in JavaScript Promise Integration for V8 sandbox escape code execution, and 1 in of a legacy feature for an information leak that facilitated the V8 sandbox escape. ExternalOneByteString For the GPU component, we used 1 information leak and 1 for read/write/control primitive. In this post, we will walk you through the renderer exploitation of this chain. Renderer Exploit Chain Overview Here is a map of where we are headed: the chain breaks V8 to build its exploit primitives and ends in renderer code execution. Come back to this illustration if the details blur along the way. The exploit chain, from Maglev bug to renderer code execution Act I: A Forgotten Barrier That Derails Maglev In our initial foothold, we exploited a representation-confusion bug in Maglev that led to incorrect write-barrier elision. We need to understand a few key concepts before diving in. Core Concepts Maglev The V8 JavaScript engine uses a multi-tiered compilation pipeline to optimize JavaScript code into native machine code. Based on the “hotness” of the executed code, V8 decides how far in the pipeline the code will be optimized, using execution feedback. If the optimized code invalidates any previous assumptions derived from the feedback during runtime, it will be deoptimized back to the interpreted bytecode to ensure correctness. V8 JavaScript compilation and optimization pipeline Each tier in this pipeline has its own intermediate representation IR of the program being operated on. For example, during the interpreter phase, the JS source code is interpreted into Ignition bytecode, while optimization compilers like Maglev and Turboshaft use a lower-level IR that is closer to native machine instructions. Maglev is V8's mid-tier JIT optimization compiler, which operates on a Control-Flow Graph CFG representation of the program. Each node in this graph uses the Maglev IR and has known information attached, derived from execution feedback in earlier phases. Among this information, one important kind is type information. This is a major part of the optimization passes Maglev performs, since many optimizations rely on type assumptions to eliminate checks that are proven unnecessary. For example, say Maglev is trying to optimize the following JS function: function add a, b { return a + b; } If, during the execution of this function prior to optimization, V8 has only seen integer values being passed to the function, it will compile it down to the following machine instructions: mov rax, rdi mov rbx, rsi add rax, rbx As long as the parameters being passed to add remain integers, the machine will execute it correctly. However, if this is not the case, Maglev will need to perform additional runtime checks before treating the function's parameters as integers. We know that JavaScript is a dynamically-typed language, so it is technically possible to do things like “adding” strings together, or an integer to a string. In Maglev, values and representations are two distinct but related concepts. A single value can have multiple representations, or "views". This is similar to how, in C programs, the same value in memory can be cast to different types and operated on differently. In the world of Maglev, there are 2 main value representations: Untagged values, which are essentially machine-native types such as raw pointers, IEEE-754 floating-point numbers, and 32-bit integers. Tagged values, which are V8 values encoded as s or Small Integers HeapObject . Under Chrome's pointer compression, a Smi Smi is a 31-bit signed integer, -2^30 to 2^30 - 1 about ±1 billion . A HeapObject is the other case: a pointer low bit set to an object on the heap. Any number that doesn't fit a Smi is boxed as a. HeapNumber The distinction is that untagged values use all the bits to represent the values themselves, while tagged values use the least significant bit to mark the value as an object or an integer. Maglev doesn't read those bits to decide tagged versus untagged. A value's representation is a static property the compiler assigns to each node and tracks through the graph, so a wrong representation is dangerous: no bit in the word can catch it. Regarding the one-value-multiple-representation relations, Maglev has a term called "alternative", implemented by the AlternativeNodes class. Alternatives of a node record the representations the compiler has already produced for that value, and they double as a cache: the first time a node is needed in some representation, Maglev emits the conversion and stores the result as that node's alternative, so later uses reuse it instead of converting again. Because the cache lives on the value node, a conversion emitted for one operation becomes a fact that any later use of the same value can pick up. For example: js let x = obj.n; // x starts as a single tagged value let arr = new Int32Array 2 ; arr 0 = x; // converts x to int32; the int32 is cached on x arr 1 = x; // reuses the cached int32; x is not converted again The first store needs x as an int32, so Maglev converts it and caches the result on x . Now x has two views, tagged and int32, and the second store reuses the cached int32 instead of converting x again. Additional reading: Maglev - V8’s Fastest Optimizing JIT Garbage collection and write barriers The V8 JS engine uses a generational garbage collector GC , in which allocations are divided into spaces based on how many times they survive a garbage collection iteration: freshly allocated objects and objects surviving one GC reside in the Young Space , while objects surviving two or more GCs are evacuated and reside in the Old Space . The GC process is divided into 2 subprocesses: the Minor GC , which collects only in the Young Space, and the Major GC , which collects the whole heap. The Minor GC runs much more frequently than the Major GC. This is based on the Generational Hypothesis, which states that most objects die young, so collecting only the young generation reclaims most of the garbage while staying cheap. To collect, the GC must first identify which objects are still live. It does so by traversing the object graph, starting from a known set of live objects called the roots , and whatever it cannot reach is considered dead and freed. This traversal is expensive, so its scope, and therefore the set of roots, differs between Minor GC and Major GC. This is where the Minor GC hits a problem. It only wants to scan the Young Space, but a young object can be kept alive by a pointer that lives in the Old Space. If the Minor GC only looks inside the Young Space, it never sees that Old-to-Young pointer, wrongly concludes youngObj is dead, and frees it, leaving oldObj with a dangling reference. The obvious fix, scanning the Old Space to find such pointers, is exactly the whole-heap traversal a Major GC does, so it defeats the purpose of having a cheap Minor GC in the first place. To solve this, V8 maintains remembered sets that record Old-to-Young references, and the Minor GC treats them as an additional set of roots. That way it learns about these references without scanning the Old Space. The remembered sets have to be kept accurate, and this is where write barriers come in. In V8, they are pieces of code that run after a pointer store into a heap object and record the reference into the remembered sets if the store created an Old-to-Young pointer: store pointer into object.field → write barrier runs → remembered set updated While this seems minor compared to traversing the whole heap, write barriers are still an expensive cost in optimized code, so Maglev as well as other optimizing compilers tries its best to skip them whenever they are proven unnecessary. A store of a Smi , for instance, never needs a barrier, because a Smi is not a pointer. Security issues often arise when write barriers are incorrectly skipped, or in V8's terms, "elided". If a barrier that was actually needed is elided, the Old-to-Young pointer never makes it into the remembered set. The Minor GC then frees a young object that is still referenced, leading to a use-after-free. This is precisely the class of bug that opens our exploit chain: an optimizer convinced a store is barrier-free when it is not. Additional readings: Trash talk: the Orinoco garbage collector and Orinoco: young generation garbage collection JavaScript variable kinds In JavaScript, where a variable is stored depends on how it is declared. A let and a var can produce different bytecode in V8. In particular, at the script's top-level: A variable declared with the var keyword becomes a global property.A variable declared with the let keyword is stored in the script context. At the top level, a var is literally a property of the globalThis object. In V8's terms, storing to a let goes through the ContextCell https://github.com/v8/v8/blob/f09a91282a26caa91d016c962d785d852cfdec36/src/objects/contexts.h L899 paths, while storing to a top-level var goes through the global property path backed by a . https://github.com/v8/v8/blob/f09a91282a26caa91d016c962d785d852cfdec36/src/objects/property-cell.h L22 PropertyCell js d8 var cV = 1337 undefined d8 globalThis.cV 1337 d8 let cL = 1337 undefined d8 globalThis.cL undefined d8 cL 1337 Cell state vs value representation When reviewing V8's source code, it is common to encounter type enums such as kConstant , kTagged , kSmi , and kInt32 . The same names show up in two very different contexts, so it is easy to conflate them. The key is to keep straight which one describes the container the variable and which describes the contents a single value : Cell state describes the container . A PropertyCell backing a top-level var or a ContextCell backing a let is the storage slot for a variable, and its state records what the compiler has learned about that slot over time, across assignments. For example, a PropertyCell state of kConstantType means the compiler expects future writes to this cell to keep being the same kind of value. The relevant enum classes areand ContextCell::State . PropertyCellType Value representation describes the contents . As mentioned above, a single value can be viewed in several ways, and a value representation is simply how the compiler views one value at a given moment, for example as a tagged Smi or as a raw untagged int32. The relevant enum class is, which lists every representation a value can take. ValueRepresentation The two meet when the compiler stores a value into a variable, and it helps to read that store as a handshake between a source and a destination : The value representation is the source : it is where the compiler reads the value from, and it decides how the value gets extracted say, pulling an int32 out of a node .The cell state is the destination : it is the slot being written to, and it dictates what kind of value is legally allowed to land there. So each store asks: "I have a value in this representation; is that legal to place into a cell in this state?" Getting that question wrong is exactly where the bug in the next section lives. The Bug The bug is in MaglevGraphBuilder::BuildCheckSmi . At compile time, Maglev uses it to decide whether an optimized store needs a run-time CheckSmi , a guard that verifies a value really is a Smi Small Integer . If it can prove the value is a valid Smi , it drops the guard. To reach that conclusion it has a shortcut for constants, and that shortcut is where the bug lives: it checks only whether a value's number fits Smi range and treats that as proof the value is a Smi . A HeapNumber holding a Smi-range value like 11.0 slips through, so Maglev treats a heap pointer as an integer and elides the write barrier on a store, breaking the GC's bookkeeping and opening a use-after-free. Source: src/maglev/maglev-graph-builder.cc:4114 /// src/maglev/maglev-graph-builder.cc 4114 | ReduceResult MaglevGraphBuilder::BuildCheckSmi ValueNode object, 4115 | bool elidable { ... 4128 | // For constants, we may be able to skip the runtime check. 4129 | if std::optional