Skip to content

fix(dstack-mr): a 200-byte SEV-SNP measurement document buys 11.3 seconds of CPU - #1252

Open
kvinwang wants to merge 1 commit into
nextfrom
fix/dstack-mr-sev-page-budget
Open

kvinwang wants to merge 1 commit into
nextfrom
fix/dstack-mr-sev-page-budget

Conversation

@kvinwang

Copy link
Copy Markdown
Collaborator

MAX_OVMF_METADATA_PAGES admitted 16 777 216 pages — 64 GiB. Every page costs one SHA-384 over a PAGE_INFO, and verify_sev_launch recomputes the whole measurement before comparing it with the hardware MEASUREMENT, so the entire budget is spent on input the requester authored. The document is bound only to vm_config.os_image_hash, which the requester also chooses, so no allowlist can reject it first.

Measured, not estimated

$ cargo test --release -p dstack-mr --lib measure_the_page_budget_cost -- --ignored --nocapture
others=5 probe_pages=1048576 elapsed=708.718871ms per_page=0.676us budget_cost=11.3s

11.3 seconds of one core for a measurement.snp.cbor of about 200 bytes — two OVMF sections, one SnpKernelHashes (required by validate_measurement_input) and one SnpSecMemory sized to the budget.

The path is reached from the verifier's POST /verify and from the KMS's key-release path. Both are gated by one valid SNP quote — which its holder can replay with an arbitrary vm_config, so the gate bounds who can do it, not how often.

The ceiling was 541 000× what a real firmware asks for

The shipped dstack-0.6.0 image's measurement.snp.cbor:

ovmf_sections: 7 sections, 31 pages = 0.12 MiB
    {'gpa': '0x800000', 'size': '0x9000', 'type': '0x1'}
    {'gpa': '0x80a000', 'size': '0x3000', 'type': '0x1'}
    {'gpa': '0x80d000', 'size': '0x1000', 'type': '0x2'}
    {'gpa': '0x80e000', 'size': '0x1000', 'type': '0x3'}
    {'gpa': '0x80f000', 'size': '0x1000', 'type': '0x4'}
    {'gpa': '0x811000', 'size': '0xf000', 'type': '0x1'}
    {'gpa': '0x810000', 'size': '0x1000', 'type': '0x10'}

Lowered to 65 536 pages: over 2000× headroom over a real image, about 44 ms at the ceiling, and deliberately the same number as tdvf::MAX_MEASURED_PAGES — the two measurement paths have the same shape and should have the same bound.

Tests

a_metadata_table_cannot_buy_unbounded_hashing builds the largest table the old ceiling accepted (one section filling the budget the others do not claim) and asserts it is refused, and refused before any hashing rather than after. Against the old constant:

panicked at dstack-mr/src/sev.rs:1254:9:
a table asking for 16777216 pages must be refused
test sev::tests::a_metadata_table_cannot_buy_unbounded_hashing ... FAILED

the_page_budget_admits_a_real_metadata_table transcribes the shipped 7-section table, asserts it is 31 pages, asserts the budget leaves it three orders of magnitude of headroom, and asserts validate_measurement_input accepts it — so a future tightening cannot quietly start rejecting real images.

cargo test -p dstack-mr: 31 passed. cargo clippy -p dstack-mr -- -D warnings --allow unused_variables: clean.

Independent of #1236, which bounds the TDX side (tdvf.rs, tdx.rs) and does not touch sev.rs. The spawn_blocking half of this — the recomputation running on a Rocket worker — is #1251.

… firmware needs

MAX_OVMF_METADATA_PAGES admitted 16777216 pages -- 64 GiB. Every page is
one SHA-384 over a PAGE_INFO, and verify_sev_launch recomputes the whole
measurement *before* comparing it with the hardware MEASUREMENT, so the
budget is spent entirely on input the requester authored. The document is
bound only to vm_config.os_image_hash, which the requester also chooses,
so no allowlist rejects it first.

Measured on an EPYC-class host: 0.68 us per page, so the ceiling is
**11.3 seconds of one core** for a measurement.snp.cbor of about 200
bytes. The path is reached from the verifier's POST /verify and from the
KMS's key-release path, both gated only by one valid SNP quote -- which a
holder can replay with an arbitrary vm_config.

The shipped dstack-0.6.0 image declares 7 sections totalling **31
pages**, so the ceiling was 541000x what a real firmware asks for. Lower
it to 65536 pages: over 2000x headroom, about 44 ms at the ceiling, and
deliberately the same number as tdvf::MAX_MEASURED_PAGES, since the two
measurement paths have the same shape and should have the same bound.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant