
Executive Summary
CVE-2026-53360 is a heap out-of-bounds read and write in KVM’s SEV-SNP Page State Change handler, reachable by any malicious guest VM against its host kernel. The root cause is a single mismatched bounds check: the guest chooses the size of a scratch buffer, the host allocates exactly that many bytes, and then the host loops over entries using a count validated against a protocol constant (253) rather than against the buffer it just allocated. Ask for a 24-byte buffer, declare 252 entries, and the host walks roughly 2 KB past the end of a 32-byte slab object. The bug shipped with SNP PSC support around v6.10 and sat in production kernels for about two years.
What makes this write-up unusual is that its author found the bug independently and lost the race by roughly a month — and then turned the loss into the most useful part of the article. He publishes his own proposed patch alongside the one the kernel team actually shipped, and explains in detail why his was worse: it fixed the symptom rather than the disease, missed a TOCTOU on guest-writable header fields, and missed an offset variant that walks off the end of the GHCB Shared Buffer even when the scratch area is where it belongs. The second half is a candid primitives analysis — a failure oracle giving one bit per eight bytes of adjacent heap, a 12-bit constrained write of either 0x001 or 0x200, a 40-bit GFN leak to QEMU, unlimited repeatability and attacker-chosen slab class — followed by an honest assessment of what that does and does not add up to. Two safe CTF challenges accompany the post.
TL;DR
The author independently found a heap out-of-bounds read/write in KVM’s SEV-SNP Page State Change handler. A malicious guest VM can corrupt host kernel heap memory and leak its layout, across the VM boundary, as many times as it likes. He reported it to security@kernel.org on May 9, 2026, and was told someone else had reported it a few weeks earlier.

The identifiers, for reference: CVE-2026-53360, fixed in mainline commit db3f2195d293. It affected every kernel with SNP PSC support since roughly v6.10, and is patched in v7.0.12, v6.18.35 and v6.12.93.
Finding a VM Escape Bug — and an Email That Hurt
The author describes reading KVM code on and off for a while, not out of masochism but because the hypervisor boundary is one of the most interesting attack surfaces in modern computing. A bug there means a guest can punch through the wall and interfere with the host — which is the whole game. He puts the appeal bluntly: you spin up a VM in someone’s cloud and, in principle, own the cloud.
In early May he was staring at arch/x86/kvm/svm/sev.c, specifically the Page State Change handler for SEV-SNP guests. The short version of what that code does: SNP guests talk to the host through a shared page called the GHCB, PSC is one of the things they can request, and the host parses a buffer the guest provides consisting of a header followed by an array of entries.
Then he noticed the mismatch. The guest gets to choose how big the buffer is. The host allocates exactly that many bytes. But the host then loops over the entries using a count taken from the header, and the only bounds check on that count is against a protocol constant — 253 — rather than against the size of the buffer it just allocated.
So if the guest says “here is an 8-byte buffer” and then says “please process 252 entries,” the host happily walks 2 KB past the end of an 8-byte allocation into whatever else happens to be sitting on the kernel heap.
He wrote a proposed fix, verified it compiled cleanly, and sent it to security@kernel.org on May 9. He felt good about it. Then the reply arrived:
Hi Himanshu, this was reported already a couple weeks ago. It’s not an easy fix because the issue is bigger than what you found. Sorry about that, I appreciate trying to work on the fix!
Kernel security team reply, quoted in the original article
He connects this to an earlier post of his on disclosure timelines, where he had been reporter number eleven on a different bug. This time he was reporter number two — progress, of a sort.
The first reporter was Stan Shaw, who filed on April 8, about a month earlier. Shaw got the CVE, the Reported-by credit in the commit, and published a detailed writeup with a PoC that produced 73 KASAN reports from a single insmod. The author gives credit where it is due.
What stung was not losing the CVE. It was the second sentence: “the issue is bigger than what you found.” That turned out to be completely true, and understanding why taught him more than finding the bug did.

What SEV-SNP Actually Is (the Five-Minute Version)
AMD’s EPYC server chips, starting with the Milan generation in 2021, support SEV-SNP: Secure Encrypted Virtualization with Secure Nested Paging. The idea is simple — the guest VM’s memory is encrypted with a key that even the hypervisor cannot read, and the CPU hardware enforces this, treating the hypervisor as untrusted.
The target use case is cloud computing. You are running your workload on someone else’s server; you do not trust the provider and you do not trust their hypervisor. SEV-SNP answers: we will encrypt your memory so that even a compromised hypervisor cannot read your data. That is the marketing pitch, and the hardware does enforce it.
But here is the part that does not appear in the marketing materials: the host also has to defend itself against the guest. The entire point of confidential computing is running someone else’s code that you do not trust. The guest is untrusted from the host’s perspective too. Both directions matter.
This bug is in the second direction — guest attacks host — the direction everyone forgets about, precisely because the SEV-SNP messaging is entirely about protecting the guest.
The GHCB: How a VM Whispers Through the Wall
SEV-SNP guests cannot talk to the hypervisor normally, because their memory is encrypted. So there is a shared page called the GHCB — the Guest-Hypervisor Communication Block. Think of it as a 4 KB mailbox both sides can read and write.
When the guest wants something from the host, it fills in fields on the GHCB and triggers a VMGEXIT. The host reads the GHCB, does whatever was asked, writes the response, and lets the guest resume. The relevant parts are:
SW_EXITCODE: what the guest wants, effectively a function number.SW_EXITINFO1,SW_EXITINFO2: parameters.SW_SCRATCH: points to a scratch area for requests needing more data.- Shared Buffer: a 2032-byte region inside the GHCB itself.
For GHCB version 2 and later, the specification says the scratch area should live inside the Shared Buffer. That detail matters enormously and is worth holding onto.
PSC: The Request That Went Wrong
PSC stands for Page State Change. It is how an SNP guest tells the host “I want this page to be private” or “I want this page to be shared.” The guest fills in a PSC descriptor: an 8-byte header followed by an array of 8-byte entries.
struct psc_hdr {
u16 cur_entry;
u16 end_entry;
u32 reserved;
} __packed; /* 8 bytes */
struct psc_entry {
u64 cur_page : 12;
u64 gfn : 40;
u64 operation : 4;
u64 pagesize : 1;
u64 reserved : 7;
} __packed; /* 8 bytes, packed into one u64 */
The host processes entries from hdr->cur_entry to hdr->end_entry. Both values come from the guest.
How many entries fit? (2032 - 8) / 8 = 253. That is where the protocol maximum VMGEXIT_PSC_MAX_COUNT comes from — it is the capacity of the GHCB Shared Buffer after the header. Which makes perfect sense, if the buffer is actually the Shared Buffer.
The Bug: When 253 Is the Right Number for the Wrong Buffer
Here is the vulnerable path step by step, walking through the v7.0.5 code the author was reading when he found it.
Step 1: the guest sets up the request. The guest puts SW_EXITCODE = SVM_VMGEXIT_PSC (0x80000010), points SW_SCRATCH at a guest page containing a crafted PSC descriptor, and puts the descriptor length in SW_EXITINFO2. That length is completely guest-controlled.
Step 2: setup_vmgexit_scratch() allocates the buffer. If the scratch area is inside the GHCB, the host uses its existing mapping and no allocation is needed. But if the guest points the scratch area outside the GHCB — which SNP should never do, but nothing stopped it — the host allocates a kernel buffer:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT);
len comes from SW_EXITINFO2, chosen by the guest. GFP_KERNEL_ACCOUNT places it in the cgroup-accounted slab caches, which is why KASAN reports kmalloc-cg-32 later.
Set len = 24 and you get a 24-byte allocation in a 32-byte slab slot — room for the 8-byte header and exactly two entries, entries[0] and entries[1]. entries[2] starts at byte 24, which is 8 bytes of slab slack. entries[3] is another object entirely.
Step 3: snp_begin_psc() processes the entries. This is where it goes wrong:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { // checks 253, NOT the buffer
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; // OOB when idx >= 2
...
}
The check asks: “Is this index valid for the biggest possible PSC buffer?” The right question is: “Is this index valid for the buffer I actually allocated?”
253 is the capacity of the 2032-byte Shared Buffer. But the host allocated a 24-byte buffer. Two entries fit, and the check permits 252. Set end_entry = 10 and you read 8 entries past the end. Set it to 252 and you walk about 2 KB into adjacent slab objects.

Step 4: the write. For each out-of-bounds entry the host reads 8 bytes of neighbouring memory and decodes it as a psc_entry. If the decoded entry passes validation, the completion code writes back:
entries[idx].cur_page = entry.pagesize ? 512 : 1;
That is a 12-bit write into the low bits of a u64 belonging to another kernel object. The value — 1 or 512 — depends on bit 56 of the victim memory rather than on anything the attacker chooses, and the upper 52 bits are preserved. It sounds small, and it is small. But small writes have a long and storied history of being enough.
My Patch vs. Their Patch, or: Why the Kernel Team Was Right
This section is the heart of the article. Here is what the author proposed:
/* Verify entries fit within the scratch allocation */
if (offsetof(struct psc_buffer, entries) +
((u64)(idx_end + 1)) * sizeof(struct psc_entry) >
svm->sev_es.ghcb_sa_len) {
snp_complete_psc(svm, VMGEXIT_PSC_ERROR_INVALID_HDR);
return 1;
}
He was proud of it, and on its own terms it is defensible: it checks end_entry against the actual buffer size rather than the protocol constant, it compiles, it produces the right instruction sequence, and it stops the OOB. Here is what the kernel team shipped instead:
/* GHCB v2 requires the scratch area to be within the GHCB. */
if (to_kvm_sev_info(svm->vcpu.kvm)->ghcb_version >= 2)
goto e_scratch;
Four lines — and better in every way. The author walks through exactly why, and it is worth following closely because the reasoning generalizes well beyond this bug.
Problem 1: he fixed the symptom, they fixed the disease. His patch says “the loop should not go past the buffer.” Theirs says “the guest should not be choosing the buffer size at all.” For GHCB v2 and later the specification requires the scratch area to be inside the GHCB Shared Buffer, so the host should never allocate a separate buffer for SNP guests. By rejecting external scratch at the input layer, the entire class of “guest picks a tiny allocation” attacks disappears — the loop bounds stop mattering because the buffer is always the known, fixed-size Shared Buffer. As he puts it, he was adding a guardrail to a road that should not exist.
Problem 2: he missed the TOCTOU. The v7.0.5 code reads hdr->cur_entry and hdr->end_entry straight from the guest-accessible buffer, with no READ_ONCE(). The PSC handler is re-entrant — it exits to QEMU userspace and comes back — so the guest can change those values between the check and the use. A bounds check does nothing if the value it checked can change before the loop reads it. The upstream series caches the indices into private per-vCPU state on first read:
sev_es->psc.cur_idx = READ_ONCE(guest_psc->hdr.cur_entry);
sev_es->psc.end_idx = READ_ONCE(guest_psc->hdr.end_entry);
The loop then uses the cached copies, which the guest cannot touch. The author is candid about missing this: the code carried a comment saying “the buffer can be modified by a misbehaved guest after validation,” and then went ahead and re-read the values in the loop anyway. He read that comment and still missed it.
Problem 3: he missed the offset variant. Even when the scratch area is inside the GHCB, the guest can place the descriptor at an offset into the Shared Buffer. If it sits near the end and end_entry is close to 253, the loop walks off the end of the Shared Buffer — and off the page. His check works for external allocations but does not cover this case. The upstream series bounds end_entry against a max_nr_entries derived from the actual remaining length.
The tally: he fixed one bug; they closed three bugs and an entire invalid state, in fewer lines. When the kernel team said “the issue is bigger than what you found,” they meant it literally.

The lesson he draws is the one sentence worth taking from the whole article: do not patch the handler, patch the input. Fix the state that makes the bug possible, not the bug itself. Eliminate invalid state at the boundary and every downstream consumer is safe automatically. Add checks inside each consumer and you have to get every single one right, forever.
How Bad Is This Actually
Everything above was the story. This is the section for anyone who wants to understand VM escape primitives, and the author goes deep.
Primitive 1: the failure oracle
This one is free and reliable. When snp_begin_psc() hits an out-of-bounds entry that fails validation — a bad cur_page value, a misaligned GFN — it returns an error carrying the index it stopped at, and the guest sees that in the PSC response.
By setting end_entry to increasing values one at a time and checking the response, the guest learns, per slot, whether the adjacent 8 bytes decoded as a valid or invalid PSC entry. That is a one-bit-per-eight-bytes oracle over neighbouring heap memory. What can you learn from it?
- Zero versus non-zero memory.
- Object boundaries — the transition from data to freelist metadata.
- Which slab slots are allocated and which are free.
- The rough structure of adjacent objects.
It is not a full read. But it is enough to find your target.
Primitive 2: the constrained write
When an out-of-bounds entry passes validation, the completion code writes back:
entries[idx].cur_page = entry.pagesize ? 512 : 1;
This is a compiler-generated read-modify-write on the full u64: read 8 bytes at &entries[idx], clear bits [0:11], set bits [0:11] to 0x001 or 0x200, write 8 bytes back. Bits [12:63] are preserved, and the value written depends on bit 56 of the victim memory rather than on anything the attacker controls.
The obvious objection — that writing only 1 or 512 into the bottom 12 bits is useless — is one the author takes head on, and his answer is the most instructive part of the analysis:
- If bits [0:11] are a length field, you have just changed a length from, say,
0x020to0x200— 32 to 512, a 16× buffer expansion. If that length controls how much data is copied in or out of the object, you now have a far more powerful OOB read/write through a completely different code path. That is primitive amplification. - If bits [0:11] are part of a kernel pointer, you have redirected it within the same 4 KB page, since the low 12 bits are the page offset. If that page contains freed objects reclaimed with attacker-influenced data, the redirect might land somewhere useful.
- If bits [0:11] are a refcount, you have just changed 1 to 512. The object will not free when the last reference drops — 511 more decrements are needed. That is a use-after-free setup where the code thinks the object is freed but the refcount disagrees.
- If bits [0:11] are a state flag, flipping bit 0 or bit 9 can change object behavior: access-control bits, lock states, is-initialized flags.
He is careful here: he is not proving any of these work against a specific target object. But none of them are ridiculous, and all of them are shapes that have appeared in real kernel exploits before.
Primitive 3: the GFN leak to QEMU
When an out-of-bounds entry has operation = 1 or 2 — the valid PSC operations — and passes validation, the host forwards bits [12:51] of the out-of-bounds memory to QEMU as a GPA:
vcpu->run->hypercall.args[0] = gfn_to_gpa(gfn);
That is 40 bits of adjacent heap content leaked into the host userspace process. The guest does not see it directly — QEMU does — but if the guest has a second bug in QEMU, or if QEMU’s behavior in response to a nonsensical GPA is observable to the guest through timing, error handling or device state, there is an indirect leak channel.
Primitive 4: repeatability
This is what makes the whole thing dangerous rather than merely interesting. Each VMGEXIT re-allocates the scratch buffer — a new slot on the freelist, new neighbours — and the guest can fire unlimited VMGEXITs. Over hundreds or thousands of requests the guest sweeps across different heap positions, building a picture of the slab layout and landing the constrained write at different targets. This is not a one-shot bug. It is a scanner with a built-in spray.
Primitive 5: slab selection
The guest controls SW_EXITINFO2, which is the allocation size. Set it to 24 and you land in kmalloc-cg-32; set it to 60 and you land in kmalloc-cg-64. The guest picks which slab class to attack, which determines what objects are within reach. That is target selection at the cache level — you still need luck or grooming for slot-level adjacency, but you get to choose the neighbourhood.
Adding it up
| primitive | reliability | information | control |
|---|---|---|---|
| failure oracle | high (works on every OOB slot) | 1 bit per 8 bytes | guest sees result directly |
| constrained write | ~0.024% per slot per request | N/A | writes 0x001 or 0x200 to bits [0:11] |
| GFN leak | requires operation=1 or 2 in OOB data | 40 bits to QEMU | guest sees indirectly |
| repeatability | unlimited | cumulative | scans heap over time |
| slab selection | deterministic | N/A | guest picks cache class |
Individually each primitive looks weak. Together, with unlimited retries and slab selection, they form the kind of toolkit that real exploitation research starts from.
So Can You Actually Escape a VM With This?
The author is refreshingly honest here, and the honesty is worth preserving rather than flattening into a verdict.
The primitives are real: guest-triggered host kernel heap OOB read/write, repeatable, with slab selection and an information oracle. That is the hard part of a VM escape, and this bug hands it to you. But the raw write is constrained — you can only write 0x001 or 0x200 into the bottom 12 bits of an 8-byte slot, and you do not even choose which of the two you get. Finding a 32-byte cgroup-accounted kernel object where that specific corruption leads somewhere useful is the real research problem.
The most realistic path is primitive amplification:
failure oracle → heap layout → groom a target with a length field
→ corrupt the length from ~32 to 0x200 → now you have a 512-byte OOB
→ use the bigger OOB for arbitrary read/write → game over
That chain is plausible — it is the same shape as many real kernel exploits — but the gap between “plausible” and “reliable” is where months of target-object research live.
His assessment places the bug in the class kernel exploitation researchers call “interesting but constrained.” The cross-boundary aspect makes it more interesting than a typical local kernel bug; the 12-bit, two-value write makes it harder to exploit than a typical heap overflow. But he adds the caveat that matters operationally: in a responsible-for-the-cloud scenario, “interesting but constrained” is still a five-alarm fire, because the attacker has unlimited time and unlimited retries inside their own VM.
Could a motivated attacker with AMD EPYC hardware and a few months of kernel exploitation research turn this into an escape? He thinks yes — the pieces are there. But he has not done it, and says plainly that he is not going to pretend otherwise.
What Else Would You Chain It With?
Real-world VM escapes are almost never a single bug; they are multiple issues chained together. The author frames this section explicitly for defenders understanding a threat model rather than as a recipe. If the constrained write is not enough alone, you need a second bug providing one of:
- A better information leak. A KASLR bypass or heap pointer leak — something that tells you exactly where things are instead of the one-bit oracle. Candidates include KVM instruction emulation bugs that leak register state, QEMU device model bugs exposing host addresses, and side channels such as the APIC MMIO leak class or speculative execution variants.
- A more powerful write primitive. With a second KVM or QEMU bug offering a wider write, you use the PSC oracle to find the target and the second bug to hit it. KVM has had OOB writes in other handlers before, and QEMU device emulation — virtio-net, USB passthrough, display backends — has historically been a rich source of memory corruption.
- A host-side privilege escalation. If the PSC primitive only yields limited kernel corruption, you might crash a specific object into a state granting a lesser capability — writing a file, calling a restricted ioctl — then chain that with a separate local privilege escalation on the host. The Dirty Pipe / Dirty Frag class would be ideal partners if any were unpatched.
The historical context he adds is useful. Publicly demonstrated VM escapes have tended to go through the device emulation layer rather than hypervisor kernel code: VENOM (CVE-2015-3456) was a floppy controller bug in QEMU, Cloudburst (2009) was a display driver bug, and the Pwn2Own 2024–2025 VM escapes targeted QEMU device models and VMware display handling. Going through KVM kernel code directly is harder, because the kernel has stronger mitigations — KASLR, SMAP, SMEP, CFI — but it is also more powerful, because a kernel primitive gives full host control without needing to escape QEMU’s sandbox first. The PSC bug is interesting precisely because it sits in the kernel path: amplify the primitive and you skip the QEMU sandbox entirely.
Try It Yourself: The Safe CTF
The author built two practice challenges so readers can experience the primitives without needing AMD EPYC hardware or risking anyone’s infrastructure. Both live at unknownhad/kvm-sev-snp-psc-research.
psc-vault (beginner)
A C program modelling the core bug in a fake heap arena. The goal is to use the OOB PSC write-back to open a toy “host vault” without hitting the tripwire. Build with make, then run ./psc-vault for vulnerable mode or ./psc-vault --fixed for patched mode.
The solve: send an 8-byte header with cur_entry = end_entry = 17. That indexes past the two-entry scratch allocation into the vault object, and the write-back sets the gate’s low bits to 0x200, opening the vault. In fixed mode the same input is rejected because the entry count is checked against the actual allocation size.
That is the whole bug in one interaction: the “protocol max” check passes (17 < 253), the “actual buffer” check fails (17 >= 2), and vulnerable mode only has the first check.
psc-escape-school (intermediate)
A Python toy hypervisor with a randomized fake heap. The goal is to use the failure oracle to scan for a target object, then land the constrained write to trigger a fictional escape. Run python solve.py to see the staged workflow:
- Probe: scan OOB slots one at a time using the failure oracle.
- Find: locate the target object, where the oracle response changes from “invalid” to “completed”.
- Write: the completion write-back modifies the target.
- Escape: the toy “host door” is now open.
- Fixed mode: the same request is rejected.
The randomized target index changes every run, so it cannot be hardcoded — you have to use the oracle. Neither challenge touches real KVM, real hardware or real guests; they model the same primitive at a conceptual level.
What Defenders Should Steal From This Story
If you design hypervisors, sandboxes or any other kind of trust boundary, the author distills six lessons:
- Bound against the real buffer, not protocol constants. The 253 was correct for the protocol and wrong for memory safety. Whenever you have a “maximum count” from a specification and a “buffer size” from an allocation, those are two different numbers — check the one that matters for memory safety. This sounds obvious. It was not obvious to the person who wrote the code, and it was not obvious to the reviewers who approved it.
- Reject invalid state at the boundary, not inside the handler. The upstream fix adds no check to the PSC loop; it rejects the invalid scratch allocation before the loop exists. If you find yourself adding bounds checks inside a loop that processes attacker data, ask why that loop has access to a buffer that could be the wrong size in the first place.
- Treat guest data as attacker input. Every size, offset, count and index the guest writes into the GHCB is untrusted. Parse it the way you would parse a network packet from the internet. This is easy to forget with SEV-SNP because the marketing is about protecting the guest from the host — the host still has to protect itself from the guest.
- Use
READ_ONCE()on shared memory. If data can change between validation and use, you have a TOCTOU. The PSC handler had a comment about exactly this and then ignored it.READ_ONCE()and caching into local variables costs nothing. - Test with KASAN. One
insmodproduced 73 KASAN reports — 62 slab-out-of-bounds, 7 slab-use-after-free, 4 use-after-free, all againstkmalloc-cg-32. Had this code been fuzz-tested with KASAN enabled, the bug would have been caught before it shipped. KASAN is not optional for security-critical code paths. - The protocol is not the implementation. The spec says the maximum PSC entry count is 253 and that the scratch area must be inside the GHCB for v2+. Both were true; the implementation enforced neither. Specifications do not write bounds checks — engineers do, and engineers miss things, especially when the spec seems obvious and the code looks like it should work.
The Timeline
| date | what happened |
|---|---|
| ~May 2024 | SNP PSC handler introduced in KVM (~v6.10). Bug exists from day one. |
| April 8, 2026 | Stan Shaw reports to security@kernel.org with analysis, PoC, and proposed fix. |
| Same day | Greg Kroah-Hartman forwards to KVM maintainers. Paolo Bonzini confirms. |
| April 8-13, 2026 | Mike Roth (AMD) and Sean Christopherson (Google) work out the proper fix. |
| May 9, 2026 | I independently report the same bug with my own analysis and patch. |
| May 9, 2026 | Kernel team responds: already reported, fix is in progress, my patch is incomplete. |
| Late May 2026 | Fix lands in mainline: db3f2195d293 (authored by Mike Roth, committed by Paolo Bonzini). |
| July 4, 2026 | CVE-2026-53360 published on NVD. |
| July 4, 2026 | Stan Shaw publishes writeup and PoC. |
The Part Where I Was Wrong About Timing Too
This section ties back to the author’s earlier post on disclosure timelines. He found the same bug as someone else, independently, about a month later. His question is the uncomfortable one: if two unrelated people found the same kernel VM escape bug within weeks of each other, how many others also found it and decided to use it rather than report it?
The 90-day disclosure window is not protecting anyone here either. The bug existed for two years. It was independently found by at least two people in April–May 2026. The fix took about six weeks from first report to mainline — which by historical standards is fast. But in a world where LLMs help people find bugs and turn patches into exploits, six weeks is a long time to leave a VM escape primitive sitting in production kernels.
Final Thoughts From the Author
He does not pretend the experience did not sting. You find a bug in one of the hardest attack surfaces in computing, write it up, send it in — and get told someone else was there first. It happens, and honestly, he concludes, the learning was worth more than the CVE credit would have been.
Finding the bug took a few hours. Understanding why his fix was wrong took longer. Understanding why theirs was better took longer still. That gap — between “I can find a bug” and “I can architect the right fix” — is the gap between a vulnerability researcher and a kernel engineer, and he closes with genuine respect for the people who closed it in this case.
The code and challenges are at unknownhad/kvm-sev-snp-psc-research, and the CTF challenges do not require SEV-SNP hardware. He invites corrections on the primitives analysis in particular, via @anand_himanshu — noting he would rather be corrected than confident.
Key Takeaways
- The bug is a single confused bounds check.
end_entrywas validated againstVMGEXIT_PSC_MAX_COUNT(253, the capacity of the GHCB Shared Buffer) instead of against the guest-sized buffer the host had just allocated. A protocol constant and an allocation size are different numbers, and only one of them is about memory safety. - Confidential computing is bidirectional and the second direction gets forgotten. SEV-SNP marketing is entirely about protecting the guest from an untrusted host. This bug lives in the opposite direction — an untrusted guest attacking the host — which is exactly where attention is thinnest.
- Patch the input, not the handler. The upstream fix rejects external scratch allocations for GHCB v2+ in four lines, eliminating the invalid state entirely rather than guarding one loop against it. That is why it also closed a TOCTOU and an offset variant the author’s bounds check missed.
- A 12-bit write with two possible values is not automatically useless. Landed on a length field it becomes a 16× buffer expansion; on a refcount it becomes a use-after-free setup; on a pointer’s low bits it becomes an intra-page redirect. Primitive amplification is the realistic exploitation path.
- Repeatability and slab selection are what make it dangerous. Each
VMGEXITre-allocates with fresh neighbours, andSW_EXITINFO2lets the guest choose thekmalloc-cgclass. Unlimited retries plus neighbourhood selection turns a weak primitive into a heap scanner. - KASAN would have caught this before it shipped. A single
insmodof the public PoC produces 73 reports. Two years of production exposure for a bug that a KASAN-enabled fuzz run surfaces immediately is the real process failure. - Independent rediscovery is a disclosure-policy signal. Two unrelated researchers found a two-year-old KVM VM escape within a month of each other, which raises a fair question about how many others found it and chose not to report.
Defensive Recommendations
- Patch to v7.0.12, v6.18.35 or v6.12.93 or later on any host running SEV-SNP guests. The fix is mainline commit
db3f2195d293; anything with SNP PSC support since roughly v6.10 is affected. - Inventory which hosts actually expose SNP. The attack surface only exists where SEV-SNP guests can be started on AMD EPYC hardware. Knowing which fleet members qualify turns a fleet-wide emergency into a scoped one.
- Audit your own trust boundaries for the same shape. Grep for places where a length or count arrives from untrusted input, an allocation is sized from it, and a loop is then bounded by a specification constant. That pattern is the bug, and it is not unique to KVM.
- Require
READ_ONCE()on any shared-memory field read more than once. Re-entrant handlers that exit to userspace and return are TOCTOU-prone by construction. Cache validated indices into private state rather than re-reading guest-writable memory. - Run KASAN in CI for hypervisor and driver code paths. This bug is trivially detectable with KASAN plus a fuzzer that varies guest-controlled lengths. Build a KASAN kernel, run the tests, and read the output — the cost is far below two years of exposure.
- Review patches for whether they remove invalid state or merely guard it. A reviewer’s most useful question is “why can this buffer be the wrong size at all?” rather than “is this bounds check correct?” The first eliminates a class; the second defends one call site.
- Do not treat constrained-write bugs as low severity by default. Severity assessment should account for repeatability and attacker-chosen slab class. An attacker inside their own VM has unlimited time and retries, which converts low per-attempt probability into eventual success.
- Assume chaining. A kernel-path primitive that skips the QEMU sandbox is worth pairing with your QEMU and host-side privilege escalation patch posture, not assessed in isolation.
Conclusion
The technical content here is a clean, well-explained heap overflow at one of the most consequential trust boundaries in computing, and the primitives analysis is unusually careful about separating what is demonstrated from what is merely plausible. But the more durable contribution is the patch comparison. Publishing your own rejected fix next to the one that shipped, and then explaining in detail why yours was worse, is rare and genuinely instructive: the difference between the two is not skill at finding bugs but a habit of asking why the invalid state exists at all. Anyone reviewing code at a trust boundary can steal that habit immediately, which is worth considerably more than a CVE credit.
Original text: “I found a KVM guest-to-host heap corruption bug and someone else got there first” by Himanshu Anand at Security & Other Notes.


