Skip to content

vc_get_apic_ids(): hypervisor-controlled count is not bounded by the allocated buffer (OOB read up to 12292 bytes) #64

Description

@renz011tzar

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions