From 37ff10e345b974400d7b3991d48961108fa2056e Mon Sep 17 00:00:00 2001 From: erfrimod Date: Tue, 7 Jul 2026 13:53:31 -0700 Subject: [PATCH 01/15] fixing update_vtl0_vf when revoking, revoking vtl0 vf when failing reconfiguyre --- openhcl/underhill_core/src/emuplat/netvsp.rs | 87 ++++++++++++++------ 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 8fc7577891..30ca3528ba 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -814,31 +814,63 @@ impl HclNetworkVFManagerWorker { present = bus_control.is_some(), "VTL0 VF device change" ); - if matches!(&self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent) { - self.vtl0_bus_control = Vtl0Bus::HiddenPresent(bus_control.unwrap()) - } else if matches!(&self.vtl0_bus_control, Vtl0Bus::HiddenPresent(_)) { - self.vtl0_bus_control = Vtl0Bus::HiddenNotPresent; - } else if matches!(vtl2_device_state, Vtl2DeviceState::Present) { - let bus_control = bus_control - .map(Vtl0Bus::Present) - .unwrap_or(Vtl0Bus::NotPresent); - *self.guest_state.vtl0_vfid.lock().await = vtl0_vfid_from_bus_control(&bus_control); - let old_bus_control = std::mem::replace(&mut self.vtl0_bus_control, bus_control); - match self.vtl0_bus_control { - Vtl0Bus::Present(_) => self.notify_vtl0_vf_arrival(), - Vtl0Bus::NotPresent => { + // The reaction to a VTL0 VF add/remove depends on both the current + // VTL0 bus visibility and backing VTL2 device state. + match (*vtl2_device_state, &self.vtl0_bus_control) { + // The VF is hidden from the guest (e.g. prepared for hibernate). + // Store the new backing state; it will be surfaced to the guest + // if/when it is unhidden. + (_, Vtl0Bus::HiddenNotPresent) => { + self.vtl0_bus_control = Vtl0Bus::HiddenPresent(bus_control.unwrap()); + } + (_, Vtl0Bus::HiddenPresent(_)) => { + self.vtl0_bus_control = Vtl0Bus::HiddenNotPresent; + } + // The VTL2 device is present; apply the VTL0 change immediately. + (Vtl2DeviceState::Present, _) => { + let bus_control = bus_control + .map(Vtl0Bus::Present) + .unwrap_or(Vtl0Bus::NotPresent); + *self.guest_state.vtl0_vfid.lock().await = + vtl0_vfid_from_bus_control(&bus_control); + let old_bus_control = + std::mem::replace(&mut self.vtl0_bus_control, bus_control); + match self.vtl0_bus_control { + Vtl0Bus::Present(_) => self.notify_vtl0_vf_arrival(), + Vtl0Bus::NotPresent => { + self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) + .await + } + _ => unreachable!(), + } + } + // The VTL2 device is reconfiguring. If the reconfigure did not + // revoke the VTL0 VF, then VTL0 bus must be changing to NotPresent, + // so revoke the VTL0 VF. Then store the new VTL0 bus state. + (Vtl2DeviceState::Reconfiguring, _) => { + if self.guest_state.is_offered_to_guest().await { + tracing::info!( + vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), + "Revoking VTL0 VF" + ); + *self.guest_state.vtl0_vfid.lock().await = None; + let old_bus_control = + std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::NotPresent); self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) - .await + .await; } - _ => unreachable!(), + self.vtl0_bus_control = bus_control + .map(Vtl0Bus::Present) + .unwrap_or(Vtl0Bus::NotPresent); } - } else { // When the VTL2 device is restored, the VTL0 update will be applied. - assert_eq!(*self.guest_state.offered_to_guest.lock().await, false); - assert!(self.guest_state.vtl0_vfid.lock().await.is_none()); - self.vtl0_bus_control = bus_control - .map(Vtl0Bus::Present) - .unwrap_or(Vtl0Bus::NotPresent); + (Vtl2DeviceState::Missing | Vtl2DeviceState::DeviceEnumerated, _) => { + assert!(!self.guest_state.is_offered_to_guest().await); + assert!(self.guest_state.vtl0_vfid.lock().await.is_none()); + self.vtl0_bus_control = bus_control + .map(Vtl0Bus::Present) + .unwrap_or(Vtl0Bus::NotPresent); + } } }) .await @@ -1066,8 +1098,9 @@ impl HclNetworkVFManagerWorker { /// /// Assumes the worker is not in shutdown. /// On success, returns `Ok(None)` and sets device state to `Present` when the device has - /// started up. If retries have run out, sets device state to `Missing` and returns an - /// error; otherwise, `Ok(Some(backoff))` with updated retry timing. + /// started up. If retries have run out, revokes any still-offered VTL0 VF, + /// sets device state to `Missing`, and returns an error; otherwise, + /// `Ok(Some(backoff))` with updated retry timing. async fn reconfigure_vf_restart( &mut self, vtl2_device_state: &mut Vtl2DeviceState, @@ -1093,7 +1126,13 @@ impl HclNetworkVFManagerWorker { attempts = backoff.attempts, "VTL2 device restart not ready after VF reconfiguration" ); - // Stop further attempts. + // Stop further attempts and treat the VTL2 device as missing. + if self.guest_state.is_offered_to_guest().await { + // If reconfigure VF did not revoke the VTL0 VF, revoke it. + *self.guest_state.vtl0_vfid.lock().await = None; + self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent) + .await; + } *vtl2_device_state = Vtl2DeviceState::Missing; anyhow::bail!("vtl2 device not ready") } From ec353a7f95f5880fe57095b51544e098eafce722 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Tue, 7 Jul 2026 14:47:45 -0700 Subject: [PATCH 02/15] fix comment and replace vtl0 bus ctrl --- openhcl/underhill_core/src/emuplat/netvsp.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 30ca3528ba..7b0346ff14 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -794,8 +794,10 @@ impl HclNetworkVFManagerWorker { /// /// Assumes the worker is not in shutdown. /// When `vtl2_device_state` is `Present`, the guest-visible VF id and - /// arrival/removal notifications are updated immediately. Otherwise the bus - /// change is recorded on `self.vtl0_bus_control` and the guest-facing state + /// arrival/removal notifications are updated immediately. When it is + /// `Reconfiguring` and a VF is still offered to the guest, that VF is + /// revoked before the new bus state is recorded. Otherwise the bus change + /// is recorded on `self.vtl0_bus_control` and the guest-facing state /// remains cleared until the VTL2 device is started again. async fn update_vtl0_vf( &mut self, @@ -1130,7 +1132,9 @@ impl HclNetworkVFManagerWorker { if self.guest_state.is_offered_to_guest().await { // If reconfigure VF did not revoke the VTL0 VF, revoke it. *self.guest_state.vtl0_vfid.lock().await = None; - self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent) + let old_bus_control = + std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::NotPresent); + self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) .await; } *vtl2_device_state = Vtl2DeviceState::Missing; From ba142d44d84f834859e9edebbccd9994a2a32894 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Wed, 8 Jul 2026 11:03:34 -0700 Subject: [PATCH 03/15] hide_vtl0_vf now revokes vtl0 vf if present --- openhcl/underhill_core/src/emuplat/netvsp.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 7b0346ff14..f1fe249901 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -713,7 +713,21 @@ impl HclNetworkVFManagerWorker { let old_bus_control = std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent); if matches!(old_bus_control, Vtl0Bus::Present(_)) { - if matches!(vtl2_device_state, Vtl2DeviceState::Present) { + let vtl0_vf_offered = self.guest_state.is_offered_to_guest().await; + if vtl0_vf_offered { + // Reconfigure may have left the VTL0 VF offered to the guest. + // Regardless of device state, if the VF is offered, revoke it + // to preserve the invariant that a hidden VF is not offered. + tracing::info!( + vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), + vtl0_vf_offered, + vtl2_device_state = ?vtl2_device_state, + "VTL0 VF will be revoked as part of hide operation" + ); + } + + if matches!(vtl2_device_state, Vtl2DeviceState::Present) || vtl0_vf_offered + { *self.guest_state.vtl0_vfid.lock().await = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) From 086458506fe21cb758240cee1747953662050209 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Wed, 8 Jul 2026 11:32:24 -0700 Subject: [PATCH 04/15] copilot feedback logging fix --- openhcl/underhill_core/src/emuplat/netvsp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index f1fe249901..27a77a6427 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -719,7 +719,7 @@ impl HclNetworkVFManagerWorker { // Regardless of device state, if the VF is offered, revoke it // to preserve the invariant that a hidden VF is not offered. tracing::info!( - vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), + vtl0_vfid = vtl0_vfid_from_bus_control(&old_bus_control), vtl0_vf_offered, vtl2_device_state = ?vtl2_device_state, "VTL0 VF will be revoked as part of hide operation" From 39c08ce8629f34aba6f2a06a946b6192994d944d Mon Sep 17 00:00:00 2001 From: erfrimod Date: Wed, 15 Jul 2026 12:50:26 -0700 Subject: [PATCH 05/15] try revoke vtl0 vf will skip if bus not present, update vtl0 vf no longer asserts not offered --- openhcl/underhill_core/src/emuplat/netvsp.rs | 104 +++++++++---------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 27a77a6427..946d993447 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -506,6 +506,7 @@ impl HclNetworkVFManagerWorker { /// /// On return, the worker will no longer treat the VF as offered, regardless /// of whether notification or revoke operations encountered errors. + /// If no vPCI bus is `Present`, then VTL0 VF revoke is skipped. async fn try_notify_guest_and_revoke_vtl0_vf(&mut self, bus_control: &Vtl0Bus) { if !self.guest_state.is_offered_to_guest().await { return; @@ -571,22 +572,29 @@ impl HclNetworkVFManagerWorker { *direction_to_vtl0 = Some(false); } } - if let Err(err) = { - let vpci_bus_control = if let Vtl0Bus::Present(current_bus) = bus_control { - current_bus - } else { - let Vtl0Bus::Present(current_bus) = &self.vtl0_bus_control else { - unreachable!(); - }; - current_bus - }; + // Pick a `Present` vPCI bus control to revoke against: try the caller-supplied + // bus first, then fall back to the worker's current bus. + let vpci_bus_control = match bus_control { + Vtl0Bus::Present(current_bus) => Some(current_bus), + _ => match &self.vtl0_bus_control { + Vtl0Bus::Present(current_bus) => Some(current_bus), + _ => None, + }, + }; - self.revoke_vtl0_vf(vpci_bus_control).await - } { - tracing::error!( + if let Some(vpci_bus_control) = vpci_bus_control { + if let Err(err) = self.revoke_vtl0_vf(vpci_bus_control).await { + tracing::error!( + vtl2_vfid, + vtl0_bus = %self.vtl0_bus_control, + err = err.as_ref() as &dyn std::error::Error, + "Failed to revoke VTL0 VF" + ); + } + } else { + tracing::info!( vtl2_vfid, - err = err.as_ref() as &dyn std::error::Error, - "Failed to revoke VTL0 VF" + "VTL0 VF offered, but vPCI bus is either not present or hidden; skipping revoke" ); } } @@ -808,11 +816,11 @@ impl HclNetworkVFManagerWorker { /// /// Assumes the worker is not in shutdown. /// When `vtl2_device_state` is `Present`, the guest-visible VF id and - /// arrival/removal notifications are updated immediately. When it is - /// `Reconfiguring` and a VF is still offered to the guest, that VF is - /// revoked before the new bus state is recorded. Otherwise the bus change - /// is recorded on `self.vtl0_bus_control` and the guest-facing state + /// arrival/removal notifications are updated immediately. Otherwise the bus + /// change is recorded on `self.vtl0_bus_control` and the guest-facing state /// remains cleared until the VTL2 device is started again. + /// If the VTL0 VF is still offered to the guest when a removal notification + /// is received, the VF is revoked so the guest will switch to synthetic. async fn update_vtl0_vf( &mut self, rpc: Rpc, ()>, @@ -824,10 +832,13 @@ impl HclNetworkVFManagerWorker { Vtl0Bus::Present(_) | Vtl0Bus::HiddenPresent(_) ); assert!(is_present != bus_control.is_some()); + let is_offered_to_guest = self.guest_state.is_offered_to_guest().await; tracing::info!( vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control), - vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), + vtl0_bus = %self.vtl0_bus_control, present = bus_control.is_some(), + vtl2_device_state = ?vtl2_device_state, + is_offered_to_guest, "VTL0 VF device change" ); // The reaction to a VTL0 VF add/remove depends on both the current @@ -860,32 +871,28 @@ impl HclNetworkVFManagerWorker { _ => unreachable!(), } } - // The VTL2 device is reconfiguring. If the reconfigure did not - // revoke the VTL0 VF, then VTL0 bus must be changing to NotPresent, - // so revoke the VTL0 VF. Then store the new VTL0 bus state. - (Vtl2DeviceState::Reconfiguring, _) => { - if self.guest_state.is_offered_to_guest().await { - tracing::info!( - vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), - "Revoking VTL0 VF" - ); + // When the VTL2 device is restored, the VTL0 update will be applied. + ( + Vtl2DeviceState::Reconfiguring + | Vtl2DeviceState::Missing + | Vtl2DeviceState::DeviceEnumerated, + _, + ) => { + let is_removal = bus_control.is_none(); + let old_bus_control = std::mem::replace( + &mut self.vtl0_bus_control, + bus_control + .map(Vtl0Bus::Present) + .unwrap_or(Vtl0Bus::NotPresent), + ); + if is_removal && is_offered_to_guest { + // A VTL0 VF may be offered even though VTL2 is not present. + // Since the vPCI bus is now gone, revoke the VF now so the + // guest will switch to synthetic. *self.guest_state.vtl0_vfid.lock().await = None; - let old_bus_control = - std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::NotPresent); self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) .await; } - self.vtl0_bus_control = bus_control - .map(Vtl0Bus::Present) - .unwrap_or(Vtl0Bus::NotPresent); - } - // When the VTL2 device is restored, the VTL0 update will be applied. - (Vtl2DeviceState::Missing | Vtl2DeviceState::DeviceEnumerated, _) => { - assert!(!self.guest_state.is_offered_to_guest().await); - assert!(self.guest_state.vtl0_vfid.lock().await.is_none()); - self.vtl0_bus_control = bus_control - .map(Vtl0Bus::Present) - .unwrap_or(Vtl0Bus::NotPresent); } } }) @@ -1114,9 +1121,8 @@ impl HclNetworkVFManagerWorker { /// /// Assumes the worker is not in shutdown. /// On success, returns `Ok(None)` and sets device state to `Present` when the device has - /// started up. If retries have run out, revokes any still-offered VTL0 VF, - /// sets device state to `Missing`, and returns an error; otherwise, - /// `Ok(Some(backoff))` with updated retry timing. + /// started up. If retries have run out, sets device state to `Missing` and returns an + /// error; otherwise, `Ok(Some(backoff))` with updated retry timing. async fn reconfigure_vf_restart( &mut self, vtl2_device_state: &mut Vtl2DeviceState, @@ -1142,15 +1148,7 @@ impl HclNetworkVFManagerWorker { attempts = backoff.attempts, "VTL2 device restart not ready after VF reconfiguration" ); - // Stop further attempts and treat the VTL2 device as missing. - if self.guest_state.is_offered_to_guest().await { - // If reconfigure VF did not revoke the VTL0 VF, revoke it. - *self.guest_state.vtl0_vfid.lock().await = None; - let old_bus_control = - std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::NotPresent); - self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) - .await; - } + // Stop further attempts. *vtl2_device_state = Vtl2DeviceState::Missing; anyhow::bail!("vtl2 device not ready") } From ecaa69a004fd955f2d6bbd24085d612be5efc79a Mon Sep 17 00:00:00 2001 From: erfrimod Date: Wed, 15 Jul 2026 14:02:50 -0700 Subject: [PATCH 06/15] adding vtl0 vfid to error trace --- openhcl/underhill_core/src/emuplat/netvsp.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 946d993447..9b1de98bb6 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -587,6 +587,7 @@ impl HclNetworkVFManagerWorker { tracing::error!( vtl2_vfid, vtl0_bus = %self.vtl0_bus_control, + revoke_vtl0_vfid = vfid_from_guid(&vpci_bus_control.instance_id()), err = err.as_ref() as &dyn std::error::Error, "Failed to revoke VTL0 VF" ); From 15e31eebb4f88aa2cd896f365f91591010cd53bb Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 17 Jul 2026 15:16:47 -0700 Subject: [PATCH 07/15] PR feedback --- openhcl/underhill_core/src/emuplat/netvsp.rs | 165 ++++++++++++------- 1 file changed, 104 insertions(+), 61 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 9b1de98bb6..dbadd1aa94 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -273,6 +273,7 @@ enum Vtl0Bus { Present(HclVpciBusControl), HiddenNotPresent, HiddenPresent(HclVpciBusControl), + PendingRevoke(HclVpciBusControl), } impl std::fmt::Display for Vtl0Bus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -289,6 +290,13 @@ impl std::fmt::Display for Vtl0Bus { bus_control.instance_id().data1 ) } + Vtl0Bus::PendingRevoke(bus_control) => { + write!( + f, + "PendingRevoke(vtl0_vfid={})", + bus_control.instance_id().data1 + ) + } } } } @@ -506,12 +514,26 @@ impl HclNetworkVFManagerWorker { /// /// On return, the worker will no longer treat the VF as offered, regardless /// of whether notification or revoke operations encountered errors. + /// + /// If the VTL2 device is not `Present`, then VTL0 VF revoke is skipped. + /// /// If no vPCI bus is `Present`, then VTL0 VF revoke is skipped. - async fn try_notify_guest_and_revoke_vtl0_vf(&mut self, bus_control: &Vtl0Bus) { + async fn try_notify_guest_and_revoke_vtl0_vf( + &mut self, + bus_control: &Vtl0Bus, + vtl2_device_state: &Vtl2DeviceState, + ) { if !self.guest_state.is_offered_to_guest().await { return; } + // VTL0 VF revoke issues a MAC filter change command to the SOC. + // When VTL2 is not present, skip the revoke and leave the guest + // with the VF offered. + if !matches!(vtl2_device_state, Vtl2DeviceState::Present) { + return; + } + let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); // Make removal request a no-op by setting offered to false. The actual removal will be done at the end of this @@ -663,8 +685,19 @@ impl HclNetworkVFManagerWorker { /// Assumes the worker is not in shutdown. /// On return, `guest_state.offered_to_guest` is true only if the offer /// RPC succeeds; otherwise the worker state is left unchanged. - async fn add_vtl0_vf(&mut self) { + async fn add_vtl0_vf(&mut self, vtl2_device_state: &Vtl2DeviceState) { let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); + if matches!( + vtl2_device_state, + Vtl2DeviceState::Missing | Vtl2DeviceState::Reconfiguring + ) { + tracing::info!( + vtl2_vfid, + vtl2_device_state = ?vtl2_device_state, + "VTL2 device not present; skipping VTL0 VF offer" + ); + return; + } if !self.guest_state.is_offered_to_guest().await && self.guest_state.vtl0_vfid().await.is_some() { @@ -722,29 +755,22 @@ impl HclNetworkVFManagerWorker { let old_bus_control = std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent); if matches!(old_bus_control, Vtl0Bus::Present(_)) { - let vtl0_vf_offered = self.guest_state.is_offered_to_guest().await; - if vtl0_vf_offered { - // Reconfigure may have left the VTL0 VF offered to the guest. - // Regardless of device state, if the VF is offered, revoke it - // to preserve the invariant that a hidden VF is not offered. - tracing::info!( - vtl0_vfid = vtl0_vfid_from_bus_control(&old_bus_control), - vtl0_vf_offered, - vtl2_device_state = ?vtl2_device_state, - "VTL0 VF will be revoked as part of hide operation" - ); - } - - if matches!(vtl2_device_state, Vtl2DeviceState::Present) || vtl0_vf_offered - { + if matches!(vtl2_device_state, Vtl2DeviceState::Present) { *self.guest_state.vtl0_vfid.lock().await = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); - self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) - .await; + self.try_notify_guest_and_revoke_vtl0_vf( + &old_bus_control, + vtl2_device_state, + ) + .await; } let Vtl0Bus::Present(bus_control) = old_bus_control else { unreachable!(); }; + // When VTL2 is not present the VTL0 VF revoke is deferred. + // VTL0 VF stays offered while bus is `HiddenPresent`. + // In `startup_vtl2_device` once the VTL2 has returned, + // the VTL0 VF will be revoked. self.vtl0_bus_control = Vtl0Bus::HiddenPresent(bus_control); } } @@ -788,29 +814,9 @@ impl HclNetworkVFManagerWorker { /// Removes the VTL0 VF from the guest. /// /// Generally called when not in shutdown, but it's not assumed here. - /// The guest-facing offer bit is cleared before the revoke RPC is issued so - /// duplicate removals become no-ops. On return, - /// `guest_state.offered_to_guest` is always false even if the RPC fails or - /// times out. - async fn remove_vtl0_vf(&mut self) { - let vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); - let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); - if self.guest_state.is_offered_to_guest().await { - *self.guest_state.offered_to_guest.lock().await = false; - if let Vtl0Bus::Present(vtl0_bus_control) = &self.vtl0_bus_control { - match self.revoke_vtl0_vf(vtl0_bus_control).await { - Ok(_) => (), - Err(err) => { - tracing::error!( - vtl2_vfid, - vtl0_vfid, - err = err.as_ref() as &dyn std::error::Error, - "Failed to remove VTL0 VF" - ); - } - } - } - } + async fn remove_vtl0_vf(&mut self, vtl2_device_state: &Vtl2DeviceState) { + self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent, vtl2_device_state) + .await; } /// Updates which VTL0 VF, if any, is associated with this worker. @@ -820,8 +826,6 @@ impl HclNetworkVFManagerWorker { /// arrival/removal notifications are updated immediately. Otherwise the bus /// change is recorded on `self.vtl0_bus_control` and the guest-facing state /// remains cleared until the VTL2 device is started again. - /// If the VTL0 VF is still offered to the guest when a removal notification - /// is received, the VF is revoked so the guest will switch to synthetic. async fn update_vtl0_vf( &mut self, rpc: Rpc, ()>, @@ -866,8 +870,11 @@ impl HclNetworkVFManagerWorker { match self.vtl0_bus_control { Vtl0Bus::Present(_) => self.notify_vtl0_vf_arrival(), Vtl0Bus::NotPresent => { - self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) - .await + self.try_notify_guest_and_revoke_vtl0_vf( + &old_bus_control, + vtl2_device_state, + ) + .await } _ => unreachable!(), } @@ -879,20 +886,20 @@ impl HclNetworkVFManagerWorker { | Vtl2DeviceState::DeviceEnumerated, _, ) => { - let is_removal = bus_control.is_none(); let old_bus_control = std::mem::replace( &mut self.vtl0_bus_control, bus_control .map(Vtl0Bus::Present) .unwrap_or(Vtl0Bus::NotPresent), ); - if is_removal && is_offered_to_guest { - // A VTL0 VF may be offered even though VTL2 is not present. - // Since the vPCI bus is now gone, revoke the VF now so the - // guest will switch to synthetic. - *self.guest_state.vtl0_vfid.lock().await = None; - self.try_notify_guest_and_revoke_vtl0_vf(&old_bus_control) - .await; + // When VTL2 is not present the VTL0 VF revoke is deferred. + // VTL0 VF stays offered while bus is `PendingRevoke`. + // In `startup_vtl2_device` once the VTL2 has returned, + // the VTL0 VF will be revoked. + if is_offered_to_guest { + if let Vtl0Bus::Present(bus_control) = old_bus_control { + self.vtl0_bus_control = Vtl0Bus::PendingRevoke(bus_control); + } } } } @@ -1004,6 +1011,36 @@ impl HclNetworkVFManagerWorker { *self.guest_state.vtl0_vfid.lock().await = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); self.notify_vtl0_vf_arrival(); + } else if matches!(&self.vtl0_bus_control, Vtl0Bus::HiddenPresent(_)) + && self.guest_state.is_offered_to_guest().await + { + // Reconcile a `hide_vtl0_vf` that arrived while VTL2 was absent: + // the bus is `HiddenPresent` but VF is still offered. + let Vtl0Bus::HiddenPresent(bus_control) = + std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent) + else { + unreachable!(); + }; + // Now that VTL2 is present, the VTL0 VF revoke can switch datapaths. + let revoke_bus = Vtl0Bus::Present(bus_control); + self.try_notify_guest_and_revoke_vtl0_vf(&revoke_bus, &Vtl2DeviceState::Present) + .await; + let Vtl0Bus::Present(bus_control) = revoke_bus else { + unreachable!(); + }; + self.vtl0_bus_control = Vtl0Bus::HiddenPresent(bus_control); + } else if matches!(&self.vtl0_bus_control, Vtl0Bus::PendingRevoke(_)) { + // Reconcile an `update_vtl0_vf` that removed the VF while VTL2 + // was absent. The bus is `PendingRevoke` and VF is still offered. + let Vtl0Bus::PendingRevoke(bus_control) = + std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::NotPresent) + else { + unreachable!(); + }; + *self.guest_state.vtl0_vfid.lock().await = None; + let revoke_bus = Vtl0Bus::Present(bus_control); + self.try_notify_guest_and_revoke_vtl0_vf(&revoke_bus, &Vtl2DeviceState::Present) + .await; } } @@ -1100,8 +1137,11 @@ impl HclNetworkVFManagerWorker { vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), "VTL0 VF being removed as a result of VF Reconfiguration." ); - self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent) - .await; + self.try_notify_guest_and_revoke_vtl0_vf( + &Vtl0Bus::NotPresent, + &Vtl2DeviceState::Present, + ) + .await; } // Don't 'keep alive'. VTL2 is reconfigured when in a bad state. @@ -1224,8 +1264,11 @@ impl HclNetworkVFManagerWorker { vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), "VTL0 VF being removed as a result of VTL2 VF revoke." ); - self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent) - .await; + self.try_notify_guest_and_revoke_vtl0_vf( + &Vtl0Bus::NotPresent, + &Vtl2DeviceState::Present, + ) + .await; } if matches!(vtl2_device_state, Vtl2DeviceState::Present) { @@ -1402,7 +1445,7 @@ impl HclNetworkVFManagerWorker { continue; } - self.add_vtl0_vf() + self.add_vtl0_vf(&vtl2_device_state) .instrument(tracing::info_span!("add vtl0 vf", vtl2_vfid)) .await; } @@ -1412,7 +1455,7 @@ impl HclNetworkVFManagerWorker { } let vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); - self.remove_vtl0_vf() + self.remove_vtl0_vf(&vtl2_device_state) .instrument(tracing::info_span!("remove vtl0 vf", vtl2_vfid, vtl0_vfid)) .await; } @@ -1456,7 +1499,7 @@ impl HclNetworkVFManagerWorker { "beginning VTL2 device shutdown" ); if remove_vtl0_vf { - self.remove_vtl0_vf() + self.remove_vtl0_vf(&vtl2_device_state) .instrument(tracing::info_span!( "remove vtl0 vf for shutdown", vtl2_vfid, From 2f6b6040ac08fb4cf350fc2d9d78f18022a3b2c8 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 17 Jul 2026 17:40:26 -0700 Subject: [PATCH 08/15] last bits --- openhcl/underhill_core/src/emuplat/netvsp.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index dbadd1aa94..8d94ea731f 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -687,10 +687,7 @@ impl HclNetworkVFManagerWorker { /// RPC succeeds; otherwise the worker state is left unchanged. async fn add_vtl0_vf(&mut self, vtl2_device_state: &Vtl2DeviceState) { let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); - if matches!( - vtl2_device_state, - Vtl2DeviceState::Missing | Vtl2DeviceState::Reconfiguring - ) { + if !matches!(vtl2_device_state, Vtl2DeviceState::Present) { tracing::info!( vtl2_vfid, vtl2_device_state = ?vtl2_device_state, @@ -815,6 +812,9 @@ impl HclNetworkVFManagerWorker { /// /// Generally called when not in shutdown, but it's not assumed here. async fn remove_vtl0_vf(&mut self, vtl2_device_state: &Vtl2DeviceState) { + // VTL0 VF revoke will no-op when VTL2 is absent. Once the VTL2 device + // returns `startup_vtl2_device` will notify the netvsp cooridnator of + // the arrival, which will send a fresh `RemoveVtl0VF`. self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent, vtl2_device_state) .await; } @@ -896,6 +896,12 @@ impl HclNetworkVFManagerWorker { // VTL0 VF stays offered while bus is `PendingRevoke`. // In `startup_vtl2_device` once the VTL2 has returned, // the VTL0 VF will be revoked. + // + // TODO: Known limitation - two `UpdateVtl0VF` messages in + // the same VTL2-absent window (a remove that parks + // `PendingRevoke`, then an add before VTL2 returns) + // overwrites the parked handle. The first VF is never + // gets revoked, so the Guest will not be offered the new VF. if is_offered_to_guest { if let Vtl0Bus::Present(bus_control) = old_bus_control { self.vtl0_bus_control = Vtl0Bus::PendingRevoke(bus_control); From 6a0d48bb1e843f1d1d4c38bbbeafc9b717a4706a Mon Sep 17 00:00:00 2001 From: erfrimod Date: Mon, 20 Jul 2026 12:30:57 -0700 Subject: [PATCH 09/15] try revoke vtl0 vf handles missing vtl2 --- openhcl/underhill_core/src/emuplat/netvsp.rs | 171 ++++++------------- 1 file changed, 55 insertions(+), 116 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 8d94ea731f..90dda007ac 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -273,7 +273,6 @@ enum Vtl0Bus { Present(HclVpciBusControl), HiddenNotPresent, HiddenPresent(HclVpciBusControl), - PendingRevoke(HclVpciBusControl), } impl std::fmt::Display for Vtl0Bus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -290,13 +289,6 @@ impl std::fmt::Display for Vtl0Bus { bus_control.instance_id().data1 ) } - Vtl0Bus::PendingRevoke(bus_control) => { - write!( - f, - "PendingRevoke(vtl0_vfid={})", - bus_control.instance_id().data1 - ) - } } } } @@ -515,7 +507,7 @@ impl HclNetworkVFManagerWorker { /// On return, the worker will no longer treat the VF as offered, regardless /// of whether notification or revoke operations encountered errors. /// - /// If the VTL2 device is not `Present`, then VTL0 VF revoke is skipped. + /// If the VTL2 device is not `Present`, skip HWC call to move MAC filter. /// /// If no vPCI bus is `Present`, then VTL0 VF revoke is skipped. async fn try_notify_guest_and_revoke_vtl0_vf( @@ -527,54 +519,50 @@ impl HclNetworkVFManagerWorker { return; } - // VTL0 VF revoke issues a MAC filter change command to the SOC. - // When VTL2 is not present, skip the revoke and leave the guest - // with the VF offered. - if !matches!(vtl2_device_state, Vtl2DeviceState::Present) { - return; - } - + let vtl2_present = matches!(vtl2_device_state, Vtl2DeviceState::Present); let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); // Make removal request a no-op by setting offered to false. The actual removal will be done at the end of this // method. *self.guest_state.offered_to_guest.lock().await = false; // Give the network stack a chance to prepare for the removal. - if let Err(err) = self + let guest_notification_result = self .send_vf_state_change_notifications() .instrument(tracing::info_span!( "sending VTL0 VF removal notice", vtl2_vfid, vtl0_bus = %bus_control)) - .await - { + .await; + if let Err(err) = &guest_notification_result { tracing::error!( vtl2_vfid, err = err.as_ref() as &dyn std::error::Error, "Notify VTL0 VF removal" ); - // Force data path to VTL2 on error. - if let Err(err) = - futures::future::join_all(self.endpoint_controls.iter_mut().map(async |control| { - let endpoint = control - .disconnect() - .await - .context("failed to disconnect endpoint")?; - if let Some(endpoint) = endpoint { - if let Err(err) = endpoint.set_data_path_to_guest_vf(false).await { - tracing::error!( - vtl2_vfid, - err = err.as_ref() as &dyn std::error::Error, - "Failed to force data path to synthetic" - ); + if vtl2_present { + // Force data path to VTL2 on error. + if let Err(err) = futures::future::join_all(self.endpoint_controls.iter_mut().map( + async |control| { + let endpoint = control + .disconnect() + .await + .context("failed to disconnect endpoint")?; + if let Some(endpoint) = endpoint { + if let Err(err) = endpoint.set_data_path_to_guest_vf(false).await { + tracing::error!( + vtl2_vfid, + err = err.as_ref() as &dyn std::error::Error, + "Failed to force data path to synthetic" + ); + } + control + .connect(endpoint) + .context("failed to reconnect endpoint")?; } - control - .connect(endpoint) - .context("failed to reconnect endpoint")?; - } - Ok::<(), anyhow::Error>(()) - })) + Ok::<(), anyhow::Error>(()) + }, + )) .instrument(tracing::info_span!( "forcing datapath to synthetic", vtl2_vfid @@ -582,14 +570,18 @@ impl HclNetworkVFManagerWorker { .await .into_iter() .collect::, _>>() - { - tracing::error!( - vtl2_vfid, - err = err.as_ref() as &dyn std::error::Error, - "Failed forcing endpoint to switch data path" - ); + { + tracing::error!( + vtl2_vfid, + err = err.as_ref() as &dyn std::error::Error, + "Failed forcing endpoint to switch data path" + ); + } } - // Explicitly update save state mac filter settings in case of errors. + } + // Explicitly update save state mac filter settings in case of errors or + // if VTL2 is no longer preset. + if !vtl2_present || guest_notification_result.is_err() { for direction_to_vtl0 in &mut *self.save_state.direction_to_vtl0.lock() { *direction_to_vtl0 = Some(false); } @@ -752,22 +744,16 @@ impl HclNetworkVFManagerWorker { let old_bus_control = std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent); if matches!(old_bus_control, Vtl0Bus::Present(_)) { - if matches!(vtl2_device_state, Vtl2DeviceState::Present) { - *self.guest_state.vtl0_vfid.lock().await = - vtl0_vfid_from_bus_control(&self.vtl0_bus_control); - self.try_notify_guest_and_revoke_vtl0_vf( - &old_bus_control, - vtl2_device_state, - ) - .await; - } + *self.guest_state.vtl0_vfid.lock().await = + vtl0_vfid_from_bus_control(&self.vtl0_bus_control); + self.try_notify_guest_and_revoke_vtl0_vf( + &old_bus_control, + vtl2_device_state, + ) + .await; let Vtl0Bus::Present(bus_control) = old_bus_control else { unreachable!(); }; - // When VTL2 is not present the VTL0 VF revoke is deferred. - // VTL0 VF stays offered while bus is `HiddenPresent`. - // In `startup_vtl2_device` once the VTL2 has returned, - // the VTL0 VF will be revoked. self.vtl0_bus_control = Vtl0Bus::HiddenPresent(bus_control); } } @@ -812,9 +798,6 @@ impl HclNetworkVFManagerWorker { /// /// Generally called when not in shutdown, but it's not assumed here. async fn remove_vtl0_vf(&mut self, vtl2_device_state: &Vtl2DeviceState) { - // VTL0 VF revoke will no-op when VTL2 is absent. Once the VTL2 device - // returns `startup_vtl2_device` will notify the netvsp cooridnator of - // the arrival, which will send a fresh `RemoveVtl0VF`. self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent, vtl2_device_state) .await; } @@ -892,20 +875,12 @@ impl HclNetworkVFManagerWorker { .map(Vtl0Bus::Present) .unwrap_or(Vtl0Bus::NotPresent), ); - // When VTL2 is not present the VTL0 VF revoke is deferred. - // VTL0 VF stays offered while bus is `PendingRevoke`. - // In `startup_vtl2_device` once the VTL2 has returned, - // the VTL0 VF will be revoked. - // - // TODO: Known limitation - two `UpdateVtl0VF` messages in - // the same VTL2-absent window (a remove that parks - // `PendingRevoke`, then an add before VTL2 returns) - // overwrites the parked handle. The first VF is never - // gets revoked, so the Guest will not be offered the new VF. - if is_offered_to_guest { - if let Vtl0Bus::Present(bus_control) = old_bus_control { - self.vtl0_bus_control = Vtl0Bus::PendingRevoke(bus_control); - } + if matches!(old_bus_control, Vtl0Bus::Present(_)) { + self.try_notify_guest_and_revoke_vtl0_vf( + &old_bus_control, + vtl2_device_state, + ) + .await; } } } @@ -1017,36 +992,6 @@ impl HclNetworkVFManagerWorker { *self.guest_state.vtl0_vfid.lock().await = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); self.notify_vtl0_vf_arrival(); - } else if matches!(&self.vtl0_bus_control, Vtl0Bus::HiddenPresent(_)) - && self.guest_state.is_offered_to_guest().await - { - // Reconcile a `hide_vtl0_vf` that arrived while VTL2 was absent: - // the bus is `HiddenPresent` but VF is still offered. - let Vtl0Bus::HiddenPresent(bus_control) = - std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent) - else { - unreachable!(); - }; - // Now that VTL2 is present, the VTL0 VF revoke can switch datapaths. - let revoke_bus = Vtl0Bus::Present(bus_control); - self.try_notify_guest_and_revoke_vtl0_vf(&revoke_bus, &Vtl2DeviceState::Present) - .await; - let Vtl0Bus::Present(bus_control) = revoke_bus else { - unreachable!(); - }; - self.vtl0_bus_control = Vtl0Bus::HiddenPresent(bus_control); - } else if matches!(&self.vtl0_bus_control, Vtl0Bus::PendingRevoke(_)) { - // Reconcile an `update_vtl0_vf` that removed the VF while VTL2 - // was absent. The bus is `PendingRevoke` and VF is still offered. - let Vtl0Bus::PendingRevoke(bus_control) = - std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::NotPresent) - else { - unreachable!(); - }; - *self.guest_state.vtl0_vfid.lock().await = None; - let revoke_bus = Vtl0Bus::Present(bus_control); - self.try_notify_guest_and_revoke_vtl0_vf(&revoke_bus, &Vtl2DeviceState::Present) - .await; } } @@ -1143,11 +1088,8 @@ impl HclNetworkVFManagerWorker { vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), "VTL0 VF being removed as a result of VF Reconfiguration." ); - self.try_notify_guest_and_revoke_vtl0_vf( - &Vtl0Bus::NotPresent, - &Vtl2DeviceState::Present, - ) - .await; + self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent, vtl2_device_state) + .await; } // Don't 'keep alive'. VTL2 is reconfigured when in a bad state. @@ -1270,11 +1212,8 @@ impl HclNetworkVFManagerWorker { vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control), "VTL0 VF being removed as a result of VTL2 VF revoke." ); - self.try_notify_guest_and_revoke_vtl0_vf( - &Vtl0Bus::NotPresent, - &Vtl2DeviceState::Present, - ) - .await; + self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent, vtl2_device_state) + .await; } if matches!(vtl2_device_state, Vtl2DeviceState::Present) { From e594a6ff966831d7e03bfeae04fb47b738f44bd2 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Mon, 20 Jul 2026 13:16:30 -0700 Subject: [PATCH 10/15] spelling --- openhcl/underhill_core/src/emuplat/netvsp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 90dda007ac..7c596f722d 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -580,7 +580,7 @@ impl HclNetworkVFManagerWorker { } } // Explicitly update save state mac filter settings in case of errors or - // if VTL2 is no longer preset. + // if VTL2 is no longer present. if !vtl2_present || guest_notification_result.is_err() { for direction_to_vtl0 in &mut *self.save_state.direction_to_vtl0.lock() { *direction_to_vtl0 = Some(false); From 8c8c4335106402788008313098cd5214a5d832c7 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Mon, 20 Jul 2026 13:50:45 -0700 Subject: [PATCH 11/15] vtl0 vfid cleared on any update when vtl2 device is not present --- openhcl/underhill_core/src/emuplat/netvsp.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 7c596f722d..6a5fc58bfa 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -869,6 +869,7 @@ impl HclNetworkVFManagerWorker { | Vtl2DeviceState::DeviceEnumerated, _, ) => { + *self.guest_state.vtl0_vfid.lock().await = None; let old_bus_control = std::mem::replace( &mut self.vtl0_bus_control, bus_control From afc51b5b39ff237eb3a23e911cf15947bef7252a Mon Sep 17 00:00:00 2001 From: erfrimod Date: Mon, 20 Jul 2026 17:26:49 -0700 Subject: [PATCH 12/15] make vtl0 vf functions check for VTL2 device state --- openhcl/underhill_core/src/emuplat/netvsp.rs | 66 +++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 6a5fc58bfa..4f204b098d 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -744,13 +744,15 @@ impl HclNetworkVFManagerWorker { let old_bus_control = std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent); if matches!(old_bus_control, Vtl0Bus::Present(_)) { - *self.guest_state.vtl0_vfid.lock().await = - vtl0_vfid_from_bus_control(&self.vtl0_bus_control); - self.try_notify_guest_and_revoke_vtl0_vf( - &old_bus_control, - vtl2_device_state, - ) - .await; + if matches!(vtl2_device_state, Vtl2DeviceState::Present) { + *self.guest_state.vtl0_vfid.lock().await = + vtl0_vfid_from_bus_control(&self.vtl0_bus_control); + self.try_notify_guest_and_revoke_vtl0_vf( + &old_bus_control, + vtl2_device_state, + ) + .await; + } let Vtl0Bus::Present(bus_control) = old_bus_control else { unreachable!(); }; @@ -797,9 +799,38 @@ impl HclNetworkVFManagerWorker { /// Removes the VTL0 VF from the guest. /// /// Generally called when not in shutdown, but it's not assumed here. + /// The guest-facing offer bit is cleared before the revoke RPC is issued so + /// duplicate removals become no-ops. On return, + /// `guest_state.offered_to_guest` is always false even if the RPC fails or + /// times out. async fn remove_vtl0_vf(&mut self, vtl2_device_state: &Vtl2DeviceState) { - self.try_notify_guest_and_revoke_vtl0_vf(&Vtl0Bus::NotPresent, vtl2_device_state) - .await; + if !matches!(vtl2_device_state, Vtl2DeviceState::Present) { + let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); + tracing::info!( + vtl2_vfid, + vtl2_device_state = ?vtl2_device_state, + "VTL2 device not present; skipping VTL0 VF revoke" + ); + return; + } + let vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); + let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); + if self.guest_state.is_offered_to_guest().await { + *self.guest_state.offered_to_guest.lock().await = false; + if let Vtl0Bus::Present(vtl0_bus_control) = &self.vtl0_bus_control { + match self.revoke_vtl0_vf(vtl0_bus_control).await { + Ok(_) => (), + Err(err) => { + tracing::error!( + vtl2_vfid, + vtl0_vfid, + err = err.as_ref() as &dyn std::error::Error, + "Failed to remove VTL0 VF" + ); + } + } + } + } } /// Updates which VTL0 VF, if any, is associated with this worker. @@ -869,20 +900,9 @@ impl HclNetworkVFManagerWorker { | Vtl2DeviceState::DeviceEnumerated, _, ) => { - *self.guest_state.vtl0_vfid.lock().await = None; - let old_bus_control = std::mem::replace( - &mut self.vtl0_bus_control, - bus_control - .map(Vtl0Bus::Present) - .unwrap_or(Vtl0Bus::NotPresent), - ); - if matches!(old_bus_control, Vtl0Bus::Present(_)) { - self.try_notify_guest_and_revoke_vtl0_vf( - &old_bus_control, - vtl2_device_state, - ) - .await; - } + self.vtl0_bus_control = bus_control + .map(Vtl0Bus::Present) + .unwrap_or(Vtl0Bus::NotPresent); } } }) From 94b7e5cf871b3538e06121112f10511d5cb27ab2 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Mon, 20 Jul 2026 17:43:02 -0700 Subject: [PATCH 13/15] removing skip in remove_vtl0_vf --- openhcl/underhill_core/src/emuplat/netvsp.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 4f204b098d..f50f7a1f60 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -803,16 +803,7 @@ impl HclNetworkVFManagerWorker { /// duplicate removals become no-ops. On return, /// `guest_state.offered_to_guest` is always false even if the RPC fails or /// times out. - async fn remove_vtl0_vf(&mut self, vtl2_device_state: &Vtl2DeviceState) { - if !matches!(vtl2_device_state, Vtl2DeviceState::Present) { - let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); - tracing::info!( - vtl2_vfid, - vtl2_device_state = ?vtl2_device_state, - "VTL2 device not present; skipping VTL0 VF revoke" - ); - return; - } + async fn remove_vtl0_vf(&mut self) { let vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); let vtl2_vfid = vtl2_vfid_from_bus_control(&self.vtl2_bus_control); if self.guest_state.is_offered_to_guest().await { @@ -1421,7 +1412,7 @@ impl HclNetworkVFManagerWorker { } let vtl0_vfid = vtl0_vfid_from_bus_control(&self.vtl0_bus_control); - self.remove_vtl0_vf(&vtl2_device_state) + self.remove_vtl0_vf() .instrument(tracing::info_span!("remove vtl0 vf", vtl2_vfid, vtl0_vfid)) .await; } @@ -1465,7 +1456,7 @@ impl HclNetworkVFManagerWorker { "beginning VTL2 device shutdown" ); if remove_vtl0_vf { - self.remove_vtl0_vf(&vtl2_device_state) + self.remove_vtl0_vf() .instrument(tracing::info_span!( "remove vtl0 vf for shutdown", vtl2_vfid, From 9f563307d8c8e070d367fa233014562f2744483b Mon Sep 17 00:00:00 2001 From: erfrimod Date: Tue, 21 Jul 2026 11:35:10 -0700 Subject: [PATCH 14/15] try notify revoke vtl0 now checks HiddenPresent. hide_vtl0_vf calls revoke reguardless of Present. --- openhcl/underhill_core/src/emuplat/netvsp.rs | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index f50f7a1f60..693311b1b5 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -589,9 +589,13 @@ impl HclNetworkVFManagerWorker { // Pick a `Present` vPCI bus control to revoke against: try the caller-supplied // bus first, then fall back to the worker's current bus. let vpci_bus_control = match bus_control { - Vtl0Bus::Present(current_bus) => Some(current_bus), + Vtl0Bus::Present(current_bus) | Vtl0Bus::HiddenPresent(current_bus) => { + Some(current_bus) + } _ => match &self.vtl0_bus_control { - Vtl0Bus::Present(current_bus) => Some(current_bus), + Vtl0Bus::Present(current_bus) | Vtl0Bus::HiddenPresent(current_bus) => { + Some(current_bus) + } _ => None, }, }; @@ -744,15 +748,13 @@ impl HclNetworkVFManagerWorker { let old_bus_control = std::mem::replace(&mut self.vtl0_bus_control, Vtl0Bus::HiddenNotPresent); if matches!(old_bus_control, Vtl0Bus::Present(_)) { - if matches!(vtl2_device_state, Vtl2DeviceState::Present) { - *self.guest_state.vtl0_vfid.lock().await = - vtl0_vfid_from_bus_control(&self.vtl0_bus_control); - self.try_notify_guest_and_revoke_vtl0_vf( - &old_bus_control, - vtl2_device_state, - ) - .await; - } + *self.guest_state.vtl0_vfid.lock().await = + vtl0_vfid_from_bus_control(&self.vtl0_bus_control); + self.try_notify_guest_and_revoke_vtl0_vf( + &old_bus_control, + vtl2_device_state, + ) + .await; let Vtl0Bus::Present(bus_control) = old_bus_control else { unreachable!(); }; From 18efadcc3fc7e3c88aeda0ac7cfd5f44cfe7a776 Mon Sep 17 00:00:00 2001 From: erfrimod <31358361+erfrimod@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:47:45 -0700 Subject: [PATCH 15/15] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- openhcl/underhill_core/src/emuplat/netvsp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openhcl/underhill_core/src/emuplat/netvsp.rs b/openhcl/underhill_core/src/emuplat/netvsp.rs index 693311b1b5..4f64d60175 100644 --- a/openhcl/underhill_core/src/emuplat/netvsp.rs +++ b/openhcl/underhill_core/src/emuplat/netvsp.rs @@ -507,9 +507,9 @@ impl HclNetworkVFManagerWorker { /// On return, the worker will no longer treat the VF as offered, regardless /// of whether notification or revoke operations encountered errors. /// - /// If the VTL2 device is not `Present`, skip HWC call to move MAC filter. + /// If the VTL2 device is not `Present`, skip HWC calls to move the MAC filter. /// - /// If no vPCI bus is `Present`, then VTL0 VF revoke is skipped. + /// If no vPCI bus control is available (neither `Present` nor `HiddenPresent`), then VTL0 VF revoke is skipped. async fn try_notify_guest_and_revoke_vtl0_vf( &mut self, bus_control: &Vtl0Bus,