Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,27 @@ dependencies = [
"vm_resource",
]

[[package]]
name = "disk_storvsc"
version = "0.0.0"
dependencies = [
"anyhow",
"disk_backend",
"event-listener",
"guestmem",
"igvm_defs",
"inspect",
"scsi_buffers",
"scsi_defs",
"static_assertions",
"storvsc_driver",
"storvsp_protocol",
"tracelimit",
"tracing",
"vmbus_user_channel",
"zerocopy",
]

[[package]]
name = "disk_striped"
version = "0.0.0"
Expand Down Expand Up @@ -7345,10 +7366,14 @@ dependencies = [
name = "storvsc_driver"
version = "0.0.0"
dependencies = [
"anyhow",
"cvm_tracing",
"event-listener",
"futures",
"futures-concurrency",
"guestmem",
"inspect",
"mesh",
"mesh_channel",
"pal_async",
"scsi_buffers",
Expand All @@ -7360,6 +7385,7 @@ dependencies = [
"thiserror 2.0.16",
"tracing",
"tracing_helpers",
"user_driver",
"vmbus_async",
"vmbus_channel",
"vmbus_ring",
Expand Down Expand Up @@ -7415,6 +7441,7 @@ version = "0.0.0"
dependencies = [
"arbitrary",
"guid",
"inspect",
"open_enum",
"scsi_defs",
"zerocopy",
Expand Down Expand Up @@ -8312,6 +8339,7 @@ dependencies = [
"disk_blockdevice",
"disk_get_vmgs",
"disk_nvme",
"disk_storvsc",
"firmware_pcat",
"firmware_uefi",
"firmware_uefi_custom_vars",
Expand Down Expand Up @@ -8383,7 +8411,9 @@ dependencies = [
"sparse_mmap",
"state_unit",
"storage_string",
"storvsc_driver",
"storvsp",
"storvsp_protocol",
"storvsp_resources",
"string_page_buf",
"tee_call",
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ disk_file = { path = "vm/devices/storage/disk_file" }
disk_get_vmgs = { path = "vm/devices/storage/disk_get_vmgs" }
disk_layered = { path = "vm/devices/storage/disk_layered" }
disk_nvme = { path = "vm/devices/storage/disk_nvme" }
disk_storvsc = { path = "vm/devices/storage/disk_storvsc" }
disk_delay = { path = "vm/devices/storage/disk_delay" }
disk_prwrap = { path = "vm/devices/storage/disk_prwrap" }
disk_striped = { path = "vm/devices/storage/disk_striped" }
Expand Down
3 changes: 3 additions & 0 deletions openhcl/openhcl_boot/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ fn build_kernel_command_line(
"rdinit=/underhill-init",
// Default to user-mode NVMe driver.
"OPENHCL_NVME_VFIO=1",
// TODO(juantian): TEMP -- enable usermode storvsc to validate via CI pipeline.
// MUST be removed before merge. See PR description.
"OPENHCL_STORVSC_USERMODE=1",
Comment on lines 180 to +185
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard-codes OPENHCL_STORVSC_USERMODE=1 into the boot command line, which enables the feature by default and contradicts the PR’s stated opt-in behavior. Please remove this before merge (or gate it behind a test-only/dev-only configuration) so production images remain opt-in via environment only.

Copilot uses AI. Check for mistakes.
Comment on lines +183 to +185
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard-codes OPENHCL_STORVSC_USERMODE=1 into the default kernel command line, which contradicts the stated "opt-in only" behavior and would enable usermode storvsc by default for all boots that use this image. Please remove this before merge (or gate it behind a test-only build/feature) so the default remains disabled unless the environment variable is explicitly set at runtime.

Suggested change
// TODO(juantian): TEMP -- enable usermode storvsc to validate via CI pipeline.
// MUST be removed before merge. See PR description.
"OPENHCL_STORVSC_USERMODE=1",

Copilot uses AI. Check for mistakes.
// The next three items reduce the memory overhead of the storvsc driver.
Comment on lines 181 to 186
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard-codes OPENHCL_STORVSC_USERMODE=1 into the default kernel command line, which contradicts the PR’s “opt-in only / not enabled by default” requirement. Please remove this before merge (or gate it behind an explicit test-only feature/build flag) so production boots remain opt-in via environment.

Copilot uses AI. Check for mistakes.
// Since it is only used for DVD, performance is not critical.
"hv_storvsc.storvsc_vcpus_per_sub_channel=2048",
Expand Down
3 changes: 3 additions & 0 deletions openhcl/underhill_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ disk_backend_resources.workspace = true
disk_blockdevice.workspace = true
disk_get_vmgs.workspace = true
disk_nvme.workspace = true
disk_storvsc.workspace = true
firmware_uefi.workspace = true
firmware_uefi_custom_vars.workspace = true
gdma_defs.workspace = true
Expand All @@ -82,6 +83,8 @@ net_mana.workspace = true
netvsp.workspace = true
nvme_driver.workspace = true
nvme_resources.workspace = true
storvsc_driver.workspace = true
storvsp_protocol.workspace = true
openhcl_dma_manager.workspace = true
scsi_core.workspace = true
scsidisk.workspace = true
Expand Down
31 changes: 30 additions & 1 deletion openhcl/underhill_core/src/dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::reference_time::ReferenceTime;
use crate::servicing;
use crate::servicing::NvmeSavedState;
use crate::servicing::ServicingState;
use crate::storvsc_manager::StorvscManager;
use crate::vmbus_relay_unit::VmbusRelayHandle;
use crate::worker::FirmwareType;
use crate::worker::NetworkSettingsError;
Expand Down Expand Up @@ -153,6 +154,7 @@ pub(crate) struct LoadedVm {
pub uevent_listener: Arc<UeventListener>,
pub resolver: ResourceResolver,
pub nvme_manager: Option<NvmeManager>,
pub storvsc_manager: Option<StorvscManager>,
pub emuplat_servicing: EmuplatServicing,
pub device_interfaces: Option<DeviceInterfaces>,
pub vmbus_client: Option<vmbus_client::VmbusClient>,
Expand Down Expand Up @@ -350,6 +352,7 @@ impl LoadedVm {
resp.field("vmgs", self.vmgs.as_ref().map(|x| &x.0));
resp.field("network", &self.network_settings);
resp.field("nvme", &self.nvme_manager);
resp.field("storvsc", &self.storvsc_manager);
resp.field("resolver", &self.resolver);
resp.field(
"vtl0_memory_map",
Expand Down Expand Up @@ -723,6 +726,19 @@ impl LoadedVm {
};

// Reset all user-mode NVMe devices.
let shutdown_storvsc = async {
if let Some(storvsc_manager) = self.storvsc_manager.take() {
storvsc_manager
.shutdown()
.instrument(tracing::info_span!(
"shutdown_storvsc_usermode",
CVM_ALLOWED,
correlation_id = %correlation_id,
))
.await;
}
};

let shutdown_nvme = async {
if let Some(nvme_manager) = self.nvme_manager.take() {
nvme_manager
Expand Down Expand Up @@ -760,7 +776,9 @@ impl LoadedVm {
)
.context("failed to write persisted info")?;

let (r, (), ()) = (shutdown_pci, shutdown_mana, shutdown_nvme).join().await;
let (r, (), (), ()) = (shutdown_pci, shutdown_mana, shutdown_nvme, shutdown_storvsc)
.join()
.await;
r?;

Ok(state)
Expand Down Expand Up @@ -933,6 +951,16 @@ impl LoadedVm {
None
};

// Save StorVSC state if the usermode driver is active.
let storvsc_state = if let Some(s) = &self.storvsc_manager {
s.save()
.instrument(tracing::info_span!("storvsc_manager_save", CVM_ALLOWED))
.await
.map(|s| servicing::StorvscSavedState { storvsc_state: s })
} else {
None
};

let units = self.save_units().await.context("state unit save failed")?;
let mana_state = if let Some(network_settings) = &mut self.network_settings
&& mana_keepalive_mode.is_enabled()
Expand Down Expand Up @@ -971,6 +999,7 @@ impl LoadedVm {
dma_manager_state,
vmbus_client,
mana_state,
storvsc_state,
},
units,
};
Expand Down
56 changes: 56 additions & 0 deletions openhcl/underhill_core/src/dispatch/vtl2_settings_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

use super::LoadedVm;
use crate::nvme_manager::manager::NvmeDiskConfig;
use crate::storvsc_manager::StorvscDiskBounceConfig;
use crate::storvsc_manager::StorvscDiskConfig;
use crate::worker::NicConfig;
use anyhow::Context;
use cvm_tracing::CVM_ALLOWED;
Expand Down Expand Up @@ -244,6 +246,7 @@ pub struct DeviceInterfaces {
scsi_dvds: HashMap<StorageDevicePath, mesh::Sender<SimpleScsiDvdRequest>>,
scsi_request: HashMap<Guid, mesh::Sender<ScsiControllerRequest>>,
use_nvme_vfio: bool,
use_storvsc_usermode: bool,
}

impl Vtl2SettingsWorker {
Expand Down Expand Up @@ -397,6 +400,7 @@ impl Vtl2SettingsWorker {
&StorageContext {
uevent_listener,
use_nvme_vfio: self.interfaces.use_nvme_vfio,
use_storvsc_usermode: self.interfaces.use_storvsc_usermode,
},
&disk,
false,
Expand All @@ -415,6 +419,7 @@ impl Vtl2SettingsWorker {
&StorageContext {
uevent_listener,
use_nvme_vfio: self.interfaces.use_nvme_vfio,
use_storvsc_usermode: self.interfaces.use_storvsc_usermode,
},
&disk,
false,
Expand Down Expand Up @@ -978,6 +983,7 @@ async fn make_disk_type_from_physical_devices(
struct StorageContext<'a> {
uevent_listener: &'a UeventListener,
use_nvme_vfio: bool,
use_storvsc_usermode: bool,
}

#[instrument(skip_all, fields(CVM_ALLOWED))]
Expand Down Expand Up @@ -1021,6 +1027,27 @@ async fn make_disk_type_from_physical_device(
}));
}

// If storvsc usermode is enabled, route VScsi devices through StorvscDiskResolver
// instead of the kernel path. Early return -- no need to wait for kernel device.
if storage_context.use_storvsc_usermode
&& matches!(
single_device.device_type,
underhill_config::DeviceType::VScsi
)
{
let lun =
u8::try_from(sub_device_path).map_err(|_| Error::StorageCannotFindVtl2Device {
device_type: single_device.device_type,
instance_id: controller_instance_id,
sub_device_path,
source: anyhow::anyhow!("sub_device_path {} exceeds u8 LUN range", sub_device_path),
})?;
return Ok(Resource::new(StorvscDiskConfig {
instance_guid: controller_instance_id,
lun,
}));
}

// Wait for the device to arrive.
let devname = async {
let devname = ctx
Expand Down Expand Up @@ -1211,11 +1238,35 @@ async fn make_ide_disk_config(
Some(send),
))
} else {
// When storvsc usermode is active and this IDE disk is backed by a
// VScsi controller, the IDE direct (port I/O) path needs a bounce
// wrapper because IDE CommandBuffer uses fake GPNs. The IDE accel
// (storvsp VMBus) path gets the normal GPA-direct disk.
let ide_direct = if storage_context.use_storvsc_usermode {
match &disk.physical_devices {
PhysicalDevices::Single { device }
if device.device_type == underhill_config::DeviceType::VScsi =>
{
let lun = u8::try_from(device.sub_device_path).ok();
lun.map(|lun| {
Resource::new(StorvscDiskBounceConfig {
instance_guid: device.vmbus_instance_id,
lun,
})
})
Comment on lines +1250 to +1256
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This silently drops the bounce-disk configuration if sub_device_path doesn’t fit in u8 (.ok()), which makes failures harder to diagnose. Since the non-bounce storvsc path already treats out-of-range LUNs as an error, it would be more consistent to propagate an error here too (or reuse the already-validated LUN) so IDE direct-path routing can’t silently fall back to the unsafe/non-bounce behavior.

Suggested change
let lun = u8::try_from(device.sub_device_path).ok();
lun.map(|lun| {
Resource::new(StorvscDiskBounceConfig {
instance_guid: device.vmbus_instance_id,
lun,
})
})
let lun = u8::try_from(device.sub_device_path).context(
"VScsi IDE direct-path routing requires sub_device_path to fit in u8",
)?;
Some(Resource::new(StorvscDiskBounceConfig {
instance_guid: device.vmbus_instance_id,
lun,
}))

Copilot uses AI. Check for mistakes.
}
_ => None,
}
} else {
None
};

Ok((
IdeDeviceConfig {
path: ide_path_from_config(disk)?,
guest_media: GuestMedia::Disk {
disk_type: disk_type.unwrap(),
ide_direct_disk_type: ide_direct.map(Box::new),
read_only: false,
disk_parameters: Some(make_disk_config_inner(
disk.location,
Expand Down Expand Up @@ -1467,6 +1518,7 @@ pub async fn create_storage_controllers_from_vtl2_settings(
ctx: &mut CancelContext,
uevent_listener: &UeventListener,
use_nvme_vfio: bool,
use_storvsc_usermode: bool,
settings: &Vtl2SettingsDynamic,
sub_channels: u16,
is_restoring: bool,
Expand All @@ -1482,6 +1534,7 @@ pub async fn create_storage_controllers_from_vtl2_settings(
let storage_context = StorageContext {
uevent_listener,
use_nvme_vfio,
use_storvsc_usermode,
};
let ide_controller =
make_ide_controller_config(ctx, &storage_context, settings, is_restoring).await?;
Expand Down Expand Up @@ -1821,6 +1874,7 @@ impl InitialControllers {
uevent_listener: &UeventListener,
dps: &DevicePlatformSettings,
use_nvme_vfio: bool,
use_storvsc_usermode: bool,
is_restoring: bool,
default_io_queue_depth: u32,
config_timeout_in_seconds: u64,
Expand All @@ -1845,6 +1899,7 @@ impl InitialControllers {
&mut context,
uevent_listener,
use_nvme_vfio,
use_storvsc_usermode,
dynamic,
fixed.scsi_sub_channels,
is_restoring,
Expand Down Expand Up @@ -1895,6 +1950,7 @@ impl InitialControllers {
scsi_dvds,
scsi_request,
use_nvme_vfio,
use_storvsc_usermode,
},
};

Expand Down
2 changes: 2 additions & 0 deletions openhcl/underhill_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod nvme_manager;
mod options;
mod reference_time;
mod servicing;
mod storvsc_manager;
mod threadpool_vm_task_backend;
mod vmbus_relay_unit;
mod vmgs_logger;
Expand Down Expand Up @@ -318,6 +319,7 @@ async fn launch_workers(
emulated_serial_wait_for_rts: opt.serial_wait_for_rts,
force_load_vtl0_image: opt.force_load_vtl0_image,
nvme_vfio: opt.nvme_vfio,
storvsc_usermode: opt.storvsc_usermode,
halt_on_guest_halt: opt.halt_on_guest_halt,
no_sidecar_hotplug: opt.no_sidecar_hotplug,
gdbstub: opt.gdbstub,
Expand Down
5 changes: 5 additions & 0 deletions openhcl/underhill_core/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ pub struct Options {
/// Use the user-mode VFIO NVMe driver instead of the Linux driver.
pub nvme_vfio: bool,

/// (OPENHCL_STORVSC_USERMODE=1)
/// Use the user-mode StorVSC driver instead of the Linux kernel hv_storvsc.
pub storvsc_usermode: bool,
/// (OPENHCL_HIDE_ISOLATION=1)
/// Hide the isolation mode from the guest.
pub hide_isolation: bool,
Expand Down Expand Up @@ -395,6 +398,7 @@ impl Options {
let vtl0_starts_paused = parse_legacy_env_bool("OPENHCL_VTL0_STARTS_PAUSED");
let serial_wait_for_rts = parse_legacy_env_bool("OPENHCL_SERIAL_WAIT_FOR_RTS");
let nvme_vfio = parse_legacy_env_bool("OPENHCL_NVME_VFIO");
let storvsc_usermode = parse_env_bool("OPENHCL_STORVSC_USERMODE");
let hide_isolation = parse_env_bool("OPENHCL_HIDE_ISOLATION");
let halt_on_guest_halt = parse_legacy_env_bool("OPENHCL_HALT_ON_GUEST_HALT");
let no_sidecar_hotplug = parse_legacy_env_bool("OPENHCL_NO_SIDECAR_HOTPLUG");
Expand Down Expand Up @@ -515,6 +519,7 @@ impl Options {
serial_wait_for_rts,
force_load_vtl0_image,
nvme_vfio,
storvsc_usermode,
hide_isolation,
halt_on_guest_halt,
no_sidecar_hotplug,
Expand Down
Loading
Loading