Summary
vc_get_apic_ids() in src/cpu/vc.rs validates the hypervisor-supplied entry
count (*count) against a constant (4096), but never against the size of
the buffer it is reading from (pages). Both values are chosen by the
hypervisor, which is untrusted in the SVSM threat model. A hypervisor that
returns a small pages together with a large count causes SVSM to read
up to 12,292 bytes past the end of the allocation.
I am aware this repository is no longer actively developed (the README directs
users to COCONUT SVSM). I checked COCONUT SVSM and it does not contain this
code path, so this is not a live advisory. Filing it for the record, and
because the artifacts may be useful to anyone still running or forking this
tree.
Affected code
src/cpu/vc.rs, vc_get_apic_ids() (commit a2d150f):
pages = (*ghcb).rax(); // hypervisor-controlled, never validated
let frame: PhysFrame = match mem_allocate_frames(pages) { ... }; // buffer = pages * PAGE_SIZE
...
let count: *const u32 = va.as_u64() as *const u32;
if *count == 0 || *count > 4096 { // <-- only bound: a CONSTANT
vc_terminate_svsm_resp_invalid();
}
apic_ids = Vec::with_capacity(*count as usize);
apic_ids.push(bsp_apic_id);
for i in 0..*count {
let id: *const u32 = (va.as_u64() + 4 + (i as u64 * 4)) as *const u32; // <-- OOB read
if *id != bsp_apic_id {
apic_ids.push(*id);
}
}
The loop reads bytes [4, 4 + count*4) from a buffer that is pages * 4096
bytes. Nothing ties count to pages.
Reproduction (by construction)
A malicious/buggy hypervisor responds to GHCB_NAE_GET_APIC_IDS with:
pages = 1 -> mem_allocate_frames(1) yields a 4096-byte buffer
count = 4096 (written into the first 4 bytes of that shared page) -> passes
the *count > 4096 check unchanged
The loop then reads through byte 4 + 4096*4 = 16388, i.e. 12,292 bytes past
the end of the 4096-byte allocation. The out-of-bounds values are stored as
APIC IDs via PERCPU.set_apic_id_for() and subsequently used for AP creation.
Note the allocator caps a single allocation at order < MAX_ORDER (6), i.e. 32
pages, so pages is effectively in [1, 32]; pages = 1 is comfortably inside
that range.
Machine-checked evidence (Verus)
A faithful transcription of the loop, given only the guard the code actually
performs, fails to verify:
error: precondition not met: index in bounds for this access
--> svsm_loop_exec.rs:24:23
|
24 | let id: u32 = buf[(1 + i) as usize];
verification results:: 2 verified, 1 errors
Verus also proves the counterexample exists, and that the worst-case overread is
exactly 12,292 bytes:
svsm_apic_bounds.rs -> 6 verified, 0 errors
svsm_findings.rs -> 5 verified, 0 errors
Attached artifacts (.rs files runnable with Verus 0.2026.07.18) contain the
full models and proofs.
Suggested fix
Bound count against the buffer that was actually returned, not just against
4096:
if *count == 0 || *count > 4096 {
vc_terminate_svsm_resp_invalid();
}
// The buffer must actually be able to hold 4 + count*4 bytes.
if (4 + (*count as u64) * 4) > pages * PAGE_SIZE {
vc_terminate_svsm_resp_invalid();
}
With that check added, the same transcription verifies:
svsm_loop_fixed.rs -> 3 verified, 0 errors
For the maximum permitted count of 4096, the hypervisor must supply at least
5 pages (16388 <= 5 * 4096); Verus confirms 4 pages is insufficient.
Secondary observation
pages is also entirely unvalidated before mem_allocate_frames(pages), which
computes count * PAGE_SIZE as an unchecked u64 multiplication. Cargo.toml
defines no [profile.release], so overflow-checks are off in release builds.
pages = 2^52 wraps pages * 4096 to 0, and get_order(0) then trips
assert!(size > 0). This is a panic/DoS rather than a memory-safety issue, and
an untrusted hypervisor has other ways to stop its guest, so severity is low.
Method / caveats
- Found by source review, then modelled and machine-checked in
Verus 0.2026.07.18.
- I did not build or boot SVSM; there is no runtime exploit here. The claim
is that the guard is insufficient by construction, which the proofs establish.
- Verified that the code above matches this repository's default branch as of
filing.
Summary
vc_get_apic_ids()insrc/cpu/vc.rsvalidates the hypervisor-supplied entrycount (
*count) against a constant (4096), but never against the size ofthe buffer it is reading from (
pages). Both values are chosen by thehypervisor, which is untrusted in the SVSM threat model. A hypervisor that
returns a small
pagestogether with a largecountcauses SVSM to readup to 12,292 bytes past the end of the allocation.
I am aware this repository is no longer actively developed (the README directs
users to COCONUT SVSM). I checked COCONUT SVSM and it does not contain this
code path, so this is not a live advisory. Filing it for the record, and
because the artifacts may be useful to anyone still running or forking this
tree.
Affected code
src/cpu/vc.rs,vc_get_apic_ids()(commita2d150f):The loop reads bytes
[4, 4 + count*4)from a buffer that ispages * 4096bytes. Nothing ties
counttopages.Reproduction (by construction)
A malicious/buggy hypervisor responds to
GHCB_NAE_GET_APIC_IDSwith:pages = 1->mem_allocate_frames(1)yields a 4096-byte buffercount = 4096(written into the first 4 bytes of that shared page) -> passesthe
*count > 4096check unchangedThe loop then reads through byte
4 + 4096*4 = 16388, i.e. 12,292 bytes pastthe end of the 4096-byte allocation. The out-of-bounds values are stored as
APIC IDs via
PERCPU.set_apic_id_for()and subsequently used for AP creation.Note the allocator caps a single allocation at
order < MAX_ORDER (6), i.e. 32pages, so
pagesis effectively in[1, 32];pages = 1is comfortably insidethat range.
Machine-checked evidence (Verus)
A faithful transcription of the loop, given only the guard the code actually
performs, fails to verify:
Verus also proves the counterexample exists, and that the worst-case overread is
exactly 12,292 bytes:
Attached artifacts (
.rsfiles runnable with Verus 0.2026.07.18) contain thefull models and proofs.
Suggested fix
Bound
countagainst the buffer that was actually returned, not just against4096:
With that check added, the same transcription verifies:
For the maximum permitted
countof 4096, the hypervisor must supply at least5 pages (
16388 <= 5 * 4096); Verus confirms 4 pages is insufficient.Secondary observation
pagesis also entirely unvalidated beforemem_allocate_frames(pages), whichcomputes
count * PAGE_SIZEas an uncheckedu64multiplication.Cargo.tomldefines no
[profile.release], so overflow-checks are off in release builds.pages = 2^52wrapspages * 4096to0, andget_order(0)then tripsassert!(size > 0). This is a panic/DoS rather than a memory-safety issue, andan untrusted hypervisor has other ways to stop its guest, so severity is low.
Method / caveats
Verus 0.2026.07.18.
is that the guard is insufficient by construction, which the proofs establish.
filing.