JIT-Spray was one of the flagship primitives of the original DSecRG era. We already put up a heritage revisit of that work, with the Internet Archive link to the material as it stood around 2010. This post is the follow-on: not the history, but the teardown. Fifteen years on, what did the mitigation stack actually close, and where does a spray-style primitive still have room to breathe.
Short recap of the primitive, no more than needed. A JIT compiler turns script into native code at runtime. Feed it the right constants and arithmetic and the bytes it emits double as attacker-chosen instructions when execution lands mid-stream, a few bytes off the intended boundary. Spray enough of these buffers and you defeat ASLR by probability; the JIT pages were writable and executable, so DEP did not help. Reliable, ugly, effective. That was the state of things.
What W^X closed
First thing that went was the writable-and-executable JIT page. Every serious engine now separates the two. The compiler writes machine code through a read-write mapping; execution happens through a separate read-execute mapping at a different address; the writable alias is never executable. V8, SpiderMonkey, JavaScriptCore all moved this way. Consequence for the primitive: you can still influence emitted bytes, but the page you influenced is not the page that runs, and the page that runs you cannot write. The classic spray-then-jump-into-the-buffer collapses here for the mainstream targets.
What constant blinding closed
The heart of JIT-Spray was attacker control over emitted constants. Put a 4-byte immediate in the script; the JIT emits it verbatim; that immediate is your shellcode fragment. Constant blinding kills this directly. At emit time the engine XORs immediates above a width threshold with a per-process random cookie, then emits an unblind at use. The bytes on the page are now cookie-dependent, not attacker-chosen. This is the “eXOR” line of defense in the old shorthand, and it is the single most targeted mitigation against the technique. It is also not free; blinding every constant costs code size and speed, so engines blind selectively, by width and context. Remember that; the selectivity is where part of the gap lives.
What CET and CFG closed
Say you still land somewhere useful. Control-flow integrity is the next wall. Microsoft’s Control Flow Guard checks indirect call targets against a bitmap of valid function entries; a mid-instruction landing in a sprayed buffer is not a valid target, so the call faults. Hardware went further. Intel’s Control-flow Enforcement Technology adds indirect branch tracking; an indirect branch must land on an ENDBR instruction or the CPU raises a fault. Sprayed bytes do not carry ENDBR at the landing offset, so the branch dies at the hardware level. CET also adds a shadow stack, which is aimed at ROP rather than spray, but it closes the usual follow-on. Between IBT and a shadow stack, the “land anywhere in a big executable spray and start running” model does not survive on hardened, CET-enabled targets.
Where the primitive still breathes
None of this means the technique is dead. It means it moved to the edges. The gap, concretely:
Uneven CET. IBT only helps where the silicon supports it and the OS and the binary opted in. Older CPUs in the field have none of it; plenty of processes still run without IBT marked. On those targets the hardware wall is not there, and you are back to software CFG alone, which is weaker.
Selective blinding. Blinding by width threshold means sub-threshold constants are not blinded. Small immediates, packed cleverly, still reach the page unmodified in some engines; the research over the last few years has been about composing many small unblinded emissions rather than one big sprayed buffer. Narrower channel, not a closed one.
Non-mainstream JITs. The three big browser engines are hard. The long tail is not: regex JITs, embedded scripting runtimes, game engines, some managed-language backends. Anywhere a JIT ships without W^X, without blinding, and without CET-aware codegen, the 2010 primitive works close to unchanged. That is where n-day reconstruction of this class keeps finding purchase.
W^X races and RWX bugs. The dual-mapping is only as good as the enforcement. A logic bug that leaves an RWX region, or a race in the permission flip during compilation, hands the writable-executable page back. These are rarer and count as their own vulnerability, but they turn the primitive back on when they exist.
The read for 2026
The honest summary: on a current browser, on a current CPU with IBT and shadow stack on, with constant blinding doing its job, JIT-Spray as it was practiced around 2010 does not work. Three independent mitigations have to fail together, and on a hardened target they do not. That is a real result and worth stating plainly; the defenders earned it.
The gap is that “hardened target” describes a subset of the deployed world, not all of it. The primitive did not get solved so much as pushed into the runtimes and the hardware generations that did not get the memo. For an attacker picking targets, that subset is still large. For a defender, the takeaway is narrower than it looks: enabling CET where the hardware allows it, and preferring runtimes that blind and separate their JIT pages, closes most of this, and the remaining exposure is a question of which of your processes still run without those three things on. Worth an inventory. The mitigations work; the coverage is the open question.