loader: add support for measured product policies#3572
loader: add support for measured product policies#3572mayank-microsoft wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an optional measured ContainerPolicy payload to the fixed VTL2 measured config region, wires it through IGVM generation, and introduces an end-to-end CWCOW (Confidential Windows Container on Windows) recipe + manifests with accompanying runtime parsing and contributor documentation.
Changes:
- Extend the measured VTL2 config region (now 2 pages) and append an inline mesh-encoded
ContainerPolicybody with explicit size framing. - Enable manifest-driven
ContainerPolicyconfiguration inigvmfilegen, and encode it into the measured config region during image build. - Add runtime decoding support in Underhill plus a new Flowey recipe and Guide documentation for onboarding new container products.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| vm/loader/src/paravisor.rs | Builds a fixed-size measured config region image (struct + optional policy bytes) and passes it to import_pages; adds unit tests for encoding/region layout. |
| vm/loader/loader_defs/src/paravisor.rs | Extends ParavisorMeasuredVtl2Config, defines ContainerPolicy wire types + encode/decode helpers, and exports sizing/offset constants. |
| vm/loader/loader_defs/Cargo.toml | Adds feature-gated serde/base64 support for manifest-shaped policy types; adds serde_json for tests. |
| vm/loader/igvmfilegen/src/main.rs | Threads optional ContainerPolicy from manifest config into the OpenHCL loader calls. |
| vm/loader/igvmfilegen_config/src/lib.rs | Extends manifest schema with image.openhcl.container_policy and adds JSON round-trip tests. |
| vm/loader/igvmfilegen_config/Cargo.toml | Enables loader_defs manifest feature so ContainerPolicy can deserialize from JSON. |
| vm/loader/manifests/openhcl-x64-cvm-cwcow-release.json | New release manifest enabling CWCOW container policy for SNP/TDX guest configs. |
| vm/loader/manifests/openhcl-x64-cvm-cwcow-dev.json | New dev manifest enabling CWCOW container policy (and debug) for SNP/TDX/VBS configs. |
| openhcl/underhill_core/src/loader/vtl2_config/mod.rs | Reads container_policy_size, bounds-checks, reads bytes from the measured region, and decodes into MeasuredVtl2Info. |
| openhcl/underhill_core/src/loader/vtl2_config/container_policy.rs | New runtime helper module to decode the measured policy bytes with tests. |
| Guide/src/SUMMARY.md | Adds the new ContainerPolicy contributor page to the Guide navigation. |
| Guide/src/dev_guide/contrib/container_policy.md | New onboarding/format documentation for adding new container products/policies. |
| flowey/flowey_lib_hvlite/src/build_openhcl_igvm_from_recipe.rs | Adds the X64CvmCwcow IGVM recipe wiring to the new manifests. |
| flowey/flowey_lib_hvlite/src/artifact_openhcl_igvm_from_recipe.rs | Adds filename ↔ recipe mappings for the new CWCOW recipe. |
| flowey/flowey_lib_hvlite/src/_jobs/local_build_igvm.rs | Adds local build output naming for the new recipe. |
| flowey/flowey_hvlite/src/pipelines/build_igvm.rs | Adds CLI exposure/plumbing for X64CvmCwcow. |
| Cargo.lock | Records dependency graph changes (notably base64/serde feature usage via loader_defs). |
| Some( | ||
| container_policy::read_container_policy(&buf) | ||
| .context("container policy decode failed")?, | ||
| ) |
There was a problem hiding this comment.
These changes will be taken up in follow up PRs.
|
@chris-oo @sunilmut I have taken an approach to append the new policy after vtl2 config structure ends. Right now, we don't need more than the existing page, tomorrow we can increase the config region pages constant value and the solution should scale. We hard exit if the policy a user is trying to add goes beyond 1 page today. |
| X64CvmDevkern, | ||
| /// X64 OpenHCL with CVM support and the CWCOW (Confidential Windows | ||
| /// Container on Windows) container policy enabled. | ||
| X64CvmCwcow, |
There was a problem hiding this comment.
Can we just call it X64Cwcow ?
| pub padding: [u8; 7], | ||
| /// Byte length of the inline [`ContainerPolicy`] body, or `0` if | ||
| /// absent. | ||
| pub container_policy_size: u32, |
There was a problem hiding this comment.
My proposal is to keep the variable part generic and not hard code any specific product related stuff here. I have been working with AI to create some implementation for the concepts I had in mind for this. Take a look
The entry point to look at is the definition of ParavisorMeasuredVtl2Config
We can discuss this together
Add an optional, measured ProductPolicy payload to the paravisor's
VTL2 measured config region, and a first product variant CWCOW
(Confidential Windows Container on Windows). This gives CWCOW a
tamper-evident, attestable place to carry product invariants
(VMGS read-only, secure-boot requirements, custom UEFI JSON, ...).
Wire format
-----------
* `ParavisorMeasuredVtl2Config` (in loader_defs::paravisor, repr(C))
gains `product_policy_size: u32`. The encoded policy is appended
in-place immediately after the struct; size 0 means absent.
* Region is statically `PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES`
pages (currently 1). IGVM build panics if the encoded policy
overflows, forcing a deliberate page-count bump (which is itself
a measurement change).
* `ProductPolicy` is a mesh-protobuf oneof; tags are part of the
measured wire format and must never be reused.
* CWCOW's `custom_uefi_json` field is `#[mesh(6)]` and mandatory at
build time: encode panics if empty, to prevent attested-but-
meaningless images.
Crate structure
---------------
New crate `openhcl/openhcl_product_policy/`:
* `wire.rs` - `ProductPolicy` enum, decode error, encode/decode
fns, `ProductPolicy::name() -> &'static str`
* `cwcow.rs` - `CwcowPolicy` struct, base64 serde adapter,
`CwcowPolicyView` (via `product_view!` macro),
hand-written `validate_secure_boot_enabled`
* `lib.rs` - `product_view!` declarative macro (emits view
scaffolding + module-level `pub fn policy()`),
`OnceLock<Option<ProductPolicy>>` global, idempotent
`init()` / `get()` with CVM_ALLOWED-gated info log
of the variant tag (or "none")
Runtime install
---------------
`underhill_core::loader::vtl2_config::read_vtl2_params` decodes the
optional ProductPolicy from the measured page and installs it via
`openhcl_product_policy::init(...)`. The decoded value is intentionally
not stored on `MeasuredVtl2Info` - consumers reach the policy via
the global.
Consumer API (used by follow-up commits as enforcement points land):
openhcl_product_policy::cwcow::policy()
.validate_secure_boot_enabled(secure_boot_on)?;
Empty view (no policy installed, or installed policy is for a
different product) makes every `validate_*` a no-op, so consumers
call unconditionally without "is CWCOW active?" branching.
loader_defs shrinks
-------------------
`loader_defs` keeps only the region layout struct
(`ParavisorMeasuredVtl2Config`) and the two layout constants
(`PRODUCT_POLICY_INLINE_OFFSET`, `PRODUCT_POLICY_MAX_SIZE_BYTES`).
The `product_policy/` directory, the five re-exports, and the
`manifest` feature have all moved into `openhcl_product_policy`.
Manifest support
----------------
Manifest JSON deserializes directly into the wire enum via serde
(`rename_all = "snake_case"`, `deny_unknown_fields`). `custom_uefi_json`
is base64-encoded in JSON via a symmetric `serialize`/`deserialize`
adapter; `json_round_trip_is_byte_identical` enforces symmetry.
Bundled `X64CvmCwcow` recipe + `openhcl-x64-cvm-cwcow-{dev,release}.json`
manifests demonstrate the full pipeline.
Documentation
-------------
`Guide/src/dev_guide/contrib/product_policy.md` documents the wire
schema, region layout, runtime decode behaviour, and the "Adding a
new product" walkthrough.
Lab validation (SNP)
--------------------
Built and booted `openhcl-x64-cvm-cwcow.bin` on a real SNP VM. With
a temporary inspect-projection patch applied, `uhdiag-dev inspect`
surfaced all six decoded `CwcowPolicy` fields (including the 151-byte
`custom_uefi_json`), confirming the full manifest -> mesh ->
measured-region -> runtime-decode round trip.
Verified locally:
* cargo build of loader_defs, openhcl_product_policy (manifest +
inspect + std), loader, igvmfilegen, igvmfilegen_config,
underhill_core
* cargo test on each (18/18 in openhcl_product_policy, 3/3 in
loader_defs paravisor layout, 14/14 in loader, 6/6 in
igvmfilegen_config)
* cargo doc -p openhcl_product_policy --features std,manifest,inspect
* cargo xflowey build-igvm x64-cvm-cwcow produces a valid IGVM
end-to-end from the renamed manifest
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
c6884f7 to
18e5ef7
Compare
The three rust fenced blocks in this chapter are illustrative fragments (orphan field declarations, abstract `FooPolicy` / `CwcowPolicy`, references to `mesh_protobuf` / `serde` / `base64` which rustdoc doesn't link). Mark them as `rust,ignore` so `mdbook test Guide` highlights but does not compile them. Verified: `mdbook test Guide` completes without errors. Signed-off-by: mayank-microsoft <mayank@microsoft.com>
| let policy = policy_bytes.unwrap_or(&[]); | ||
| config.product_policy_size = policy.len() as u32; | ||
|
|
||
| let buf_bytes = (PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES as usize) * (HV_PAGE_SIZE as usize); | ||
| let mut buf = vec![0u8; buf_bytes]; | ||
|
|
||
| let struct_bytes = config.as_bytes(); | ||
| buf[..struct_bytes.len()].copy_from_slice(struct_bytes); | ||
| if !policy.is_empty() { | ||
| let off = PRODUCT_POLICY_INLINE_OFFSET; | ||
| buf[off..off + policy.len()].copy_from_slice(policy); | ||
| } |
| if let Err(_) = openhcl_product_policy::init(product_policy) { | ||
| anyhow::bail!("conflicting product policy already installed"); | ||
| } | ||
|
|
| [features] | ||
| # Pull in serde::Serialize/Deserialize derives on the wire types | ||
| # (manifest authoring). Mirrors the same-named feature that previously | ||
| # lived on `loader_defs`. | ||
| manifest = ["dep:serde", "dep:base64"] | ||
| # Derive `inspect::Inspect` on the wire types. | ||
| inspect = ["dep:inspect"] | ||
| # Enables the OnceLock-backed global (init/get) and the | ||
| # anyhow-returning validate_* methods on the product views. Only | ||
| # OpenHCL runtime crates enable this. | ||
| std = ["dep:anyhow"] | ||
|
|
||
| [dependencies] | ||
| mesh_protobuf.workspace = true | ||
|
|
||
| # CVM_ALLOWED-gated tracing in `init()`. | ||
| cvm_tracing.workspace = true | ||
| tracing.workspace = true | ||
|
|
||
| anyhow = { workspace = true, optional = true } | ||
| base64 = { workspace = true, optional = true, features = ["alloc"] } | ||
| inspect = { workspace = true, optional = true } | ||
| serde = { workspace = true, optional = true, features = ["alloc", "derive"] } |
| `encode_product_policy_bytes` will `panic!` at IGVM-build time with | ||
| a message that names `PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES`. The | ||
| fix is to bump that constant (e.g. from 1 to 2) in | ||
| `openhcl/openhcl_product_policy/src/wire.rs`. Bumping it is a measurement | ||
| change — every IGVM, with or without a configured policy, will have a |
| [`ParavisorMeasuredVtl2Config`]: https://openvmm.dev/rustdoc/openhcl_product_policy/struct.ParavisorMeasuredVtl2Config.html | ||
| [`ProductPolicy`]: https://openvmm.dev/rustdoc/openhcl_product_policy/enum.ProductPolicy.html |
| pub fn init(policy: Option<ProductPolicy>) -> Result<(), ()> { | ||
| match POLICY.get() { | ||
| Some(existing) if *existing == policy => Ok(()), | ||
| Some(_) => Err(()), |
| a message that names `PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES`. The | ||
| fix is to bump that constant (e.g. from 1 to 2) in | ||
| `openhcl/openhcl_product_policy/src/wire.rs`. Bumping it is a measurement | ||
| change — every IGVM, with or without a configured policy, will have a |
| [`ParavisorMeasuredVtl2Config`]: https://openvmm.dev/rustdoc/openhcl_product_policy/struct.ParavisorMeasuredVtl2Config.html | ||
| [`ProductPolicy`]: https://openvmm.dev/rustdoc/openhcl_product_policy/enum.ProductPolicy.html |
| use cvm_tracing::CVM_ALLOWED; | ||
| use std::sync::OnceLock; | ||
|
|
||
| static POLICY: OnceLock<Option<ProductPolicy>> = OnceLock::new(); |
There was a problem hiding this comment.
Can we not have this as global but instead store it in the vm object that gets created?
| /// Paravisor measured config for vtl2. | ||
| /// | ||
| /// Followed in place by the optional [`ProductPolicy`] body at | ||
| /// [`PRODUCT_POLICY_INLINE_OFFSET`]; `product_policy_size == 0` (the | ||
| /// pre-feature zero-filled tail) means absent. |
| /// Byte length of the inline [`ProductPolicy`] body, or `0` if | ||
| /// absent. | ||
| pub product_policy_size: u32, |
| let policy = policy_bytes.unwrap_or(&[]); | ||
| config.product_policy_size = policy.len() as u32; | ||
|
|
||
| let buf_bytes = (PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES as usize) * (HV_PAGE_SIZE as usize); | ||
| let mut buf = vec![0u8; buf_bytes]; | ||
|
|
||
| let struct_bytes = config.as_bytes(); | ||
| buf[..struct_bytes.len()].copy_from_slice(struct_bytes); | ||
| if !policy.is_empty() { | ||
| let off = PRODUCT_POLICY_INLINE_OFFSET; | ||
| buf[off..off + policy.len()].copy_from_slice(policy); | ||
| } | ||
| buf |
| ## Adding a new product | ||
|
|
||
| The default flow is two edits in `openhcl/openhcl_product_policy/src/wire.rs`: | ||
|
|
| `encode_product_policy_bytes` will `panic!` at IGVM-build time with | ||
| a message that names `PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES`. The | ||
| fix is to bump that constant (e.g. from 1 to 2) in | ||
| `openhcl/openhcl_product_policy/src/wire.rs`. Bumping it is a measurement | ||
| change — every IGVM, with or without a configured policy, will have a |
| [`ParavisorMeasuredVtl2Config`]: https://openvmm.dev/rustdoc/openhcl_product_policy/struct.ParavisorMeasuredVtl2Config.html | ||
| [`ProductPolicy`]: https://openvmm.dev/rustdoc/openhcl_product_policy/enum.ProductPolicy.html |
| define_product_policy! { | ||
| package = "openhcl.product_policy"; | ||
|
|
||
| /// Sivm. | ||
| 1 => Sivm(sivm::SivmPolicy); | ||
| } |
| /// Byte offset of the inline [`ProductPolicy`] body within the | ||
| /// measured VTL2 config region. | ||
| pub const PRODUCT_POLICY_INLINE_OFFSET: usize = size_of::<ParavisorMeasuredVtl2Config>(); | ||
|
|
||
| /// Maximum byte size of an inline [`ProductPolicy`] body. | ||
| pub const PRODUCT_POLICY_MAX_SIZE_BYTES: usize = |
Adds an optional measured ContainerPolicy payload to the paravisor's VTL2 config region, plus its first product CWCOW (Confidential Windows Container on Windows). The policy ismesh-encoded into the measured config region at IGVM-build time and strongly-typed-decoded at boot — giving CWCOW a tamper-evident, attestable place to carry product invariants(read-only VMGS, secure-boot requirements, custom UEFI JSON, etc.) without inventing a new framing layer.
What changes
Lab validation (SNP)
Built x64-cvm-cwcow IGVM and booted it on a real SNP VM. uhdiag-dev inspect /vm/measured_vtl2_info surfaced the full decoded CwcowPolicy (vmgs_read_only: true,custom_uefi_json: 151 B, all require_* flags as configured) — confirming the manifest → mesh → measured-region → runtime-decode round trip works end-to-end. Theinspect-surfacing patch itself was not committed; only IGVM-build and runtime decode are shipped.
Adding a new product
See Guide/src/dev_guide/contrib/container_policy.md — two edits in loader_defs/src/paravisor.rs (define body struct, add #[mesh(N)] enum variant). No new dispatch trait, noproduct_id field; the mesh tag is the product identifier and the compiler enforces the strongly-typed body.