TLDR

I 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 wants. I reported it to [email protected] on May 9, 2026 and they told me someone else reported it a few weeks earlier.

Email reponse

This post is the story of that bug, why my fix was wrong, why theirs was better, how bad this thing actually is, and what you can learn from all of it. I also built a safe CTF challenge so you can practice the same primitive without setting anything on fire.

CVE-2026-53360. Fixed in mainline commit db3f2195d293. Affected every kernel with SNP PSC support since ~v6.10. Patched in v7.0.12, v6.18.35, v6.12.93.


I found a VM escape bug and got an email which hurt my feelings

I have been reading KVM code on and off for a while. Not because I enjoy pain (becasue I got no day job), 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 mess with the host and that’s the whole game. (Kinda wow, you spin up a VM in cloud and pwned whole cloud. Is this what they call the God syndrome?? )

In early May I was staring at arch/x86/kvm/svm/sev.c, specifically the Page State Change handler for SEV-SNP guests. If you do not know what any of those words mean, do not worry, I will try to explain all of it in a minute. TLDR is : SNP guests talk to the host through a shared page called the GHCB and PSC is one of the things they can ask for. The host parses a buffer the guest provides and the buffer has a header and an array of entries.

I noticed something that made me sit up. The guest gets to choose how big the buffer is, The host allocates exactly that many bytes But then the host loops over the entries using a count from the header and the only bounds check on that count is against a protocol constant (253)and not 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 is sitting on the kernel heap.

I wrote a proposed fix and I verified it compiled cleanly, sent it to [email protected] on May 9. I felt pretty good about myself (oh! no god syndrome again).

Then I got this back:

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!

If you have read my post about disclosure timelines, you know this feeling. I talked about being reporter number eleven on a different bug. This time I was reporter number two Progress, I guess.

The first reporter was Stan Shaw, who reported this on April 8, about a month before mine. He got the CVE (CVE-2026-53360), he got the Reported-by credit in the commit and he published a detailed writeup and a PoC. His analysis was thorough and his PoC produced 73 KASAN reports from a single insmod. Credit where it is due.

But the part that 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 me more than finding the bug did. This is Fine


okay but what even is SEV-SNP (the 5 minute version)

If you already know what SEV-SNP is, skip ahead. If you do not, here is the minimum you need to follow the rest of this post.

AMD makes server chips called EPYC. Starting with the Milan generation (2021), these chips support something called 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. The CPU hardware enforces this, hypervisor is treated as untrusted.

This is built for cloud computing, uou are running your workload on someone else’s server, do not trust the cloud provider, do not trust their hypervisor. SEV-SNP says: We will encrypt your memory so even if the hypervisor is compromised, your data stays private. Nice one

That is the marketing pitch And reality, hardware does enforce it.

But here is the thing nobody puts in the marketing materials: the host also has to defend itself against the guest. The whole 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 because the SEV-SNP marketing is all about protecting the guest.


the GHCB: how a VM whispers through the wall

SEV-SNP guests cannot just talk to the hypervisor normally. Their memory is encrypted, So there is a shared page called the GHCB (Guest-Hypervisor Communication Block). Think of it as a 4 KB mailbox that both sides can read and write.

When the guest wants something from the host, it fills in some fields on the GHCB and triggers a VMGEXIT. The host reads the GHCB, does whatever the guest asked, writes the response and lets the guest resume.

The GHCB has a few important parts:

  • SW_EXITCODE: what the guest wants (like a function number)
  • SW_EXITINFO1, SW_EXITINFO2: parameters
  • SW_SCRATCH: points to a scratch area for requests that need more data
  • Shared Buffer: a 2032-byte region inside the GHCB itself

For GHCB version 2 and later, the spec says the scratch area should live inside the Shared Buffer. This matters a lot Keep it in mind.


PSC: the request that went wrong

PSC stands for Page State Change. It is how an SNP guest tells the host “hey, I want this page to be private” or “hey, 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 are coming from the guest.

How many entries can fit? Well, (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. 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. I am walking through the v7.0.5 code because that is what I was reading when I 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 nd 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 just uses its existing mapping. No allocation 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, the guest chose it. GFP_KERNEL_ACCOUNT puts it in the cgroup-accounted slab caches, which is why KASAN says kmalloc-cg-32 later.

Set len = 24 and you get a 24-byte allocation in a 32-byte slab slot. That is room for the 8-byte header and exactly two entries. entries[0] and entries[1].

entries[2] starts at byte 24. That is 8 bytes of slab slack. entries[3] is another object entirely.

Step 3: snp_begin_psc() processes the entries.

Here 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 check allows 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.

drake approved

Step 4: the write.

For each OOB entry, the host reads 8 bytes of neighboring memory and decodes it as a psc_entry. If the decoded entry passes some validation checks, 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 that belongs to another kernel object. The value (1 or 512) depends on bit 56 of the victim memory, not on anything the attacker chooses. The upper 52 bits are preserved.

Sounds small? indeed 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

Here is what I 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;
}

I was proud of this. It checks end_entry against the actual buffer size, not the protocol constant, It compiles ¯_(ツ)_/¯ It produces the right instruction sequence and It stops the OOB.

Here is what they 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 they are better than my fix in every way, Here is why. (In my defense I am not a dev, me inside ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻ )

Problem 1: I fixed the symptom, they fixed the disease.

My patch says “the loop should not go past the buffer.” Their patch says “the guest should not be choosing the buffer size at all.” For GHCB v2 and later, the spec requires the scratch area to be inside the GHCB Shared Buffer. 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 do not matter because the buffer is always the known, fixed size Shared Buffer.

I was adding a guardrail to a road that should not exist.

Problem 2: I missed the TOCTOU.

The v7.0.5 code reads hdr->cur_entry and hdr->end_entry straight from the guest-accessible buffer. No READ_ONCE(). The PSC handler is re-entrant (it exits to QEMU userspace and comes back), which means the guest can change those values between the check and the use. My patch adds a bounds check, but the value it checks 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);

And the loop uses the cached copies, guest cannot touch them.

I did not even think about this <(^_^)> , The code had 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, I read that comment and still missed it.

Problem 3: I missed the offset variant.

Even if the scratch 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 the page). My check works for external allocations but does not cover this case. The upstream series bounds end_entry against max_nr_entries derived from the actual remaining length.

The lesson. I fixed one bug and they closed three bugs and an entire invalid state, with fewer lines. The kernel team said “the issue is bigger than what you found” and they meant it literally.

Galaxy Brain

If you take one thing from this section: do not patch the handler, patch the input. Fix the state that makes the bug possible, not the bug itself. When you eliminate invalid state at the boundary, every downstream consumer is safe automatically. When you add checks inside each consumer, you have to get every single one right, forever.


how bad is this actually

I am going to go deep. If you want to understand VM escape primitives, this is the section. Everything above was the story.

primitive 1: the failure oracle

This one is free and reliable.

When snp_begin_psc() hits an OOB entry that fails validation (bad cur_page value, misaligned GFN), it returns an error with the index it stopped at. The guest sees this 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 1-bit-per-8-bytes oracle of the neighboring heap.

What can you learn from 1 bit per 8 bytes?

  • Zero vs non-zero memory.
  • Object boundaries (transition from data to freelist metadata).
  • Which slab slots are allocated vs free.
  • 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 OOB 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:

  1. Read 8 bytes at &entries[idx].
  2. Clear bits [0:11].
  3. Set bits [0:11] to 0x001 or 0x200.
  4. Write 8 bytes back.

Bits [12:63] are preserved. The value written depends on bit 56 of the victim memory, not on anything the attacker controls.

“Oh come on, you can only write 1 or 512 into the bottom 12 bits? That is useless.”

Is it though?

If bits [0:11] of a neighboring object happen to be a length field, you just changed a length from (say) 0x020 to 0x200. That is 32 to 512. A 16x buffer expansion. If that length controls how much data gets copied in or out of the object, you now have a much 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 just redirected it within the same 4 KB page. Low 12 bits = page offset. The pointer now points at a different offset in the same page. If the page contains freed objects that have been reclaimed with attacker-influenced data, that redirect might land somewhere useful.

If bits [0:11] are a refcount, you just changed 1 to 512. The object will not free when the last reference drops. 511 more reference decrements needed. That is a use-after-free setup: the code thinks the object is freed, but the refcount disagrees.

If bits [0:11] are a state flag, bit 0 or bit 9 being flipped can change object behavior. Access control bits, lock states, “is-initialized” flags.

I am not proving any of these work for a specific target object. But none of them are ridiculous. They are all shapes that have shown up in real kernel exploits before.

primitive 3: the GFN leak to QEMU

When an OOB entry has operation = 1 or 2 (the valid PSC operations) and passes validation, the host forwards bits [12:51] of the OOB memory to QEMU as a GPA:

vcpu->run->hypercall.args[0] = gfn_to_gpa(gfn);

That is 40 bits of adjacent heap content leaked to the host userspace process. The guest does not see this 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 (timing, error handling, device state), there is an indirect leak channel.

primitive 4: repeatability

This is what makes the whole thing dangerous instead of just interesting.

Each VMGEXIT re-allocates the scratch buffer. New slot on the freelist, New neighbors. 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, this 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 in reach.

That is target selection at the cache level. You still need luck or grooming for slot-level adjacency, but you get to pick the neighborhood.

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

I am going to be honest.

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 VM escape and this bug gives 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. You do not even pick which of the two values you get. Finding a 32-byte cgroup-accounted kernel object where that specific corruption leads to something 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.

My assessment:

This bug sits in a specific class that kernel exploitation researchers call “interesting but constrained.” The cross-boundary aspect (guest to host) makes it more interesting than a typical local kernel bug. The constraint (12-bit write, two possible values) makes it harder to exploit than a typical heap overflow. In a responsible-for-the-cloud scenario, “interesting but constrained” is still a five-alarm fire, because the attacker has unlimited time and retries inside their own VM.

Do I think a motivated attacker with AMD EPYC hardware and a few months of kernel exploitation research could turn this into an escape? Yeah, I do. The pieces are there. But I have not done it and I am not going to pretend I have.


what else would you chain it with

Real world VM escapes are almost never one bug. There are multiple issue schained togather to chieve it. Here is how I would think about this if I were planning the chain (and to be clear, I am writing this for defenders to understand the threat model, not as a recipe).

If the constrained write is not enough alone, you need a second bug that gives you one of:

  1. A better information leak. KASLR bypass, heap pointer leak, something that tells you exactly where things are instead of the 1-bit oracle. Candidates: KVM instruction emulation bugs that leak register state, QEMU device model bugs that expose host addresses, side-channel attacks (like the APIC MMIO leak class or speculative execution variants).

  2. A more powerful write primitive. If you can find a second KVM or QEMU bug that gives you 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. QEMU device emulation (virtio-net, USB passthrough, display backends) has been a rich source of memory corruption bugs historically.

  3. A host-side privilege escalation. Maybe the PSC primitive only gets you limited kernel corruption. Maybe you can crash a specific object into a state that gives you a lesser capability, like writing a file or calling a restricted ioctl. Then you chain that with a separate local privilege escalation bug on the host. The Dirty Pipe / Dirty Frag class of bugs would be perfect partners if any were unpatched on the host.

Historical context. VM escapes that have been publicly demonstrated tend to go through the device emulation layer (QEMU) rather than through the hypervisor kernel code directly. VENOM (CVE-2015-3456) was a floppy controller bug in QEMU. Cloudburst (2009) was a display driver bug. 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 you full host control without needing to escape QEMU’s sandbox first.

The PSC bug is interesting specifically because it is in the kernel path, not in QEMU. If you could amplify the primitive, you skip the entire QEMU sandbox.


try it yourself: the safe CTF

I built two practice challenges so you can experience the primitives without needing AMD EPYC hardware or risking anyone’s infrastructure.

psc-vault (beginner)

A C program that models the core bug in a fake heap arena. Your goal: use the OOB PSC write-back to open a toy “host vault” without hitting the tripwire.

kvm-sev-snp-psc-research/ctf-challenge/psc-vault/

Build it (make), run ./psc-vault for vulnerable mode, ./psc-vault --fixed for patched mode.

The solve: send an 8-byte header with cur_entry = end_entry = 17. That indexes past the 2-entry scratch allocation into the vault object. The write-back sets the gate’s low bits to 0x200, which opens the vault. In fixed mode, the same input gets 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). Vulnerable mode only has the first check.

psc-escape-school (intermediate)

A Python toy hypervisor with a randomized fake heap. Your goal: use the failure oracle to scan for a target object, then land the constrained write to trigger a fictional “escape.”

kvm-sev-snp-psc-research/ctf-challenge/psc-vault/

Run python solve.py to see the staged exploit workflow:

  1. Probe: scan OOB slots one at a time using the failure oracle.
  2. Find: locate the target object (where the oracle response changes from “invalid” to “completed”).
  3. Write: the completion write-back modifies the target.
  4. Escape: the toy “host door” is now open.
  5. Fixed mode: same request gets rejected.

The randomized target index changes every run, so you cannot hardcode it. 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, here is what this bug teaches you.

1. Bound against the real buffer, not protocol constants.

The 253 was correct for the protocol. It was wrong for memory safety. Every time 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.

2. Reject invalid state at the boundary, not inside the handler.

The upstream fix does not add a check in the PSC loop. It rejects the invalid scratch allocation before the PSC loop exists. Fix the input, not every place that reads the input. If you find yourself adding bounds checks inside a loop that processes attacker data, ask: why does this loop even have access to a buffer that could be the wrong size? Can I prevent that earlier?

3. Treat guest data as attacker input.

Every size, offset, count, and index the guest writes into the GHCB is untrusted. Parse it like you would parse a network packet from the internet. This is easy to forget in SEV-SNP because the marketing is about protecting the guest from the host. The host still has to protect itself from the guest. Both directions.

4. Use READ_ONCE() on shared memory.

If the data can change between the time you validate it and the time you use it, you have a TOCTOU. The PSC handler had a comment about this and then ignored it. READ_ONCE() and caching into local variables costs nothing.

5. Test with KASAN.

One insmod produced 73 KASAN reports. Sixty-two slab-out-of-bounds, seven slab-use-after-free, four use-after-free. All against kmalloc-cg-32. If this code had been fuzz-tested with KASAN enabled, the bug would have been found before it shipped. KASAN is not optional for security-critical code paths. Build a KASAN kernel. Run your tests. Read the output.

6. The protocol is not the implementation.

The protocol says the maximum PSC entry count is 253. The protocol says the scratch area must be inside the GHCB for v2+. Both of those things were true. The implementation did not enforce either of them. Specifications do not write bounds checks. Engineers do. And engineers miss things, especially when the spec is “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 [email protected] 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 ties back to my post about disclosure timelines. I found the same bug as someone else, independently, about a month later. 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 instead of 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. In the old world, that is fast. In the 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

I am not going to pretend this experience did not sting. You find a bug in the hardest attack surface in computing, you write it up, you send it in, and you get told someone else was there first. It happens and honestly, the learning was worth more than the CVE credit would have been.

Finding the bug took me a few hours. Understanding why my fix was wrong took longer. Understanding why their fix was better took even longer than that. The 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 I have a lot of respect for the people who closed that gap in this case.

If you work on hypervisors, sandboxes or anything that parses attacker-controlled data at a trust boundary, I hope the primitives analysis was useful. The defensive lessons are real. The CTF challenges are there if you want to feel it in your hands.

And if you made it this far, you are awesome. Thanks for sticking with me. (Liek subscribe and share my blog ʕ·͡ᴥ·ʔ )


The code and challenges from this post are at: kvm-sev-snp-psc-research/ in my workspace. The CTF challenges do not require SEV-SNP hardware.

Hit me up on X (@anand_himanshu) if any of this resonated. And if you think my primitives analysis is wrong, especially hit me up. I would rather be corrected than confident.