@@ -491,6 +491,15 @@ pub struct DeviceParams {
491491 /// This parameter requires [viona_api::ApiVersion::V3] or greater. This is
492492 /// before Propolis' minimum viona API version and can always be set.
493493 pub header_pad : u16 ,
494+
495+ /// Permit the guest to replace the device's unicast MAC address with a
496+ /// unicast address of its choosing (`VIRTIO_NET_CTRL_MAC_ADDR_SET`).
497+ ///
498+ /// This is a propolis-side policy knob rather than a kernel parameter, as
499+ /// viona performs the swap as directed and leaves validating the requested
500+ /// address against host policy to its consumer. When unset, the device does
501+ /// not advertise `VIRTIO_NET_F_CTRL_MAC_ADDR`.
502+ pub allow_guest_mac_change : bool ,
494503}
495504impl DeviceParams {
496505 #[ cfg( target_os = "illumos" ) ]
@@ -520,8 +529,9 @@ impl DeviceParams {
520529impl Default for DeviceParams {
521530 fn default ( ) -> Self {
522531 // Viona (as of V3) allocs/copies entire packet by default, with no
523- // padding added to the header.
524- Self { copy_data : true , header_pad : 0 }
532+ // padding added to the header. Guest MAC replacement requires an
533+ // explicit opt-in.
534+ Self { copy_data : true , header_pad : 0 , allow_guest_mac_change : false }
525535 }
526536}
527537
@@ -638,6 +648,10 @@ pub struct PciVirtioViona {
638648 /// [`viona_api::ApiVersion::V7`]: `VNA_IOC_SET_MAC_FILTERS` and
639649 /// `VNA_IOC_SET_MAC_ADDR`.
640650 kernel_mac_filters : bool ,
651+ /// Policy gate for guest-initiated MAC replacement.
652+ ///
653+ /// See [`DeviceParams::allow_guest_mac_change`].
654+ allow_guest_mac_change : bool ,
641655 /// Promiscuity pinned at construction, when the host supports ALL_VLAN.
642656 /// Falcon links may carry VLAN-tagged frames for the emulated fabric,
643657 /// which classified delivery would drop, so no guest or migration state
@@ -758,6 +772,8 @@ impl PciVirtioViona {
758772 mac_addr : info. mac_addr . into ( ) ,
759773 mtu : info. mtu ,
760774 kernel_mac_filters : api_version >= viona_api:: ApiVersion :: V7 ,
775+ allow_guest_mac_change : viona_params
776+ . is_some_and ( |vp| vp. allow_guest_mac_change ) ,
761777 #[ cfg( feature = "falcon" ) ]
762778 pinned_promisc,
763779 hdl,
@@ -1193,9 +1209,13 @@ impl PciVirtioViona {
11931209 /// [`PciVirtioViona::mac_addr`] the device was created with, from a
11941210 /// driver-installed override.
11951211 fn set_mac_override ( & self , mac : MacAddr ) -> Result < ( ) , ( ) > {
1196- if ( self . virtio_state . negotiated_features ( )
1197- & VIRTIO_NET_F_CTRL_MAC_ADDR )
1198- == 0
1212+ // The policy is checked directly rather than through feature
1213+ // negotiation alone: the bit cannot be negotiated without the
1214+ // grant, but the swap must not depend on that indirection.
1215+ if !self . allow_guest_mac_change
1216+ || ( self . virtio_state . negotiated_features ( )
1217+ & VIRTIO_NET_F_CTRL_MAC_ADDR )
1218+ == 0
11991219 || !mac. is_unicast ( )
12001220 {
12011221 return Err ( ( ) ) ;
@@ -1496,12 +1516,18 @@ impl VirtioDevice for PciVirtioViona {
14961516 if self . mtu . is_some ( ) {
14971517 feat |= VIRTIO_NET_F_MTU ;
14981518 }
1519+ feat |= self . dev_features ;
1520+
14991521 // The address swap behind `MacCmd::AddrSet` requires
1500- // `VNA_IOC_SET_MAC_ADDR` ([`viona_api::ApiVersion::V7`]).
1501- if self . kernel_mac_filters {
1522+ // `VNA_IOC_SET_MAC_ADDR` (viona API V7) and an explicit policy
1523+ // grant (`allow_guest_mac_change`). Apply the policy after merging
1524+ // kernel-advertised features so a future kernel capability cannot
1525+ // bypass the propolis-side opt-out.
1526+ if self . kernel_mac_filters && self . allow_guest_mac_change {
15021527 feat |= VIRTIO_NET_F_CTRL_MAC_ADDR ;
1528+ } else {
1529+ feat &= !VIRTIO_NET_F_CTRL_MAC_ADDR ;
15031530 }
1504- feat |= self . dev_features ;
15051531
15061532 feat
15071533 }
@@ -1801,6 +1827,20 @@ impl MigrateMulti for PciVirtioViona {
18011827 <dyn PciVirtio >:: import ( self , offer, ctx) ?;
18021828
18031829 let feat = self . virtio_state . negotiated_features ( ) ;
1830+
1831+ // A source that negotiated CTRL_MAC_ADDR granted its guest MAC
1832+ // replacement, a policy this host must extend as well because the
1833+ // guest keeps the negotiated bit and may issue further swaps here.
1834+ if ( feat & VIRTIO_NET_F_CTRL_MAC_ADDR ) != 0
1835+ && !self . allow_guest_mac_change
1836+ {
1837+ return Err ( MigrateStateError :: ImportFailed (
1838+ "source negotiated VIRTIO_NET_F_CTRL_MAC_ADDR but guest \
1839+ MAC replacement is not permitted on this host"
1840+ . to_string ( ) ,
1841+ ) ) ;
1842+ }
1843+
18041844 self . hdl . set_features ( feat) . map_err ( |e| {
18051845 MigrateStateError :: ImportFailed ( format ! (
18061846 "error while setting viona features ({feat:x}): {e:?}"
@@ -1895,6 +1935,19 @@ impl MigrateMulti for PciVirtioViona {
18951935 . to_string ( ) ,
18961936 ) ) ;
18971937 }
1938+ // An override can outlive the negotiated bit: a failed nominal
1939+ // restore during reset retains it after the features are
1940+ // cleared.
1941+ //
1942+ // The feature gate above therefore does not cover this
1943+ // payload, so the policy applies to it directly.
1944+ if !self . allow_guest_mac_change {
1945+ return Err ( MigrateStateError :: ImportFailed (
1946+ "source carried a driver-installed MAC address but \
1947+ guest MAC replacement is not permitted on this host"
1948+ . to_string ( ) ,
1949+ ) ) ;
1950+ }
18981951 let cover = self . pinned_promisc ( ) . unwrap_or ( PromiscLevel :: All ) ;
18991952 self . set_promisc ( cover, & mut state) . map_err ( |_| {
19001953 MigrateStateError :: ImportFailed (
@@ -2600,14 +2653,15 @@ mod test {
26002653 use crate :: hw:: pci:: Bdf ;
26012654 use crate :: hw:: virtio:: pci:: Status ;
26022655 use crate :: hw:: virtio:: viona:: {
2603- control, FilterState , MacAddr , PromiscLevel ,
2656+ control, DeviceParams , FilterState , MacAddr , PromiscLevel ,
26042657 VIRTIO_NET_F_CTRL_MAC_ADDR , VIRTIO_NET_F_CTRL_RX , VIRTIO_NET_F_CTRL_VQ ,
26052658 VIRTIO_NET_F_MAC , VIRTIO_NET_F_MQ , VIRTIO_NET_F_STATUS ,
26062659 } ;
26072660 use crate :: hw:: virtio:: { PciVirtioViona , VirtioDevice } ;
26082661 use crate :: lifecycle:: Lifecycle ;
26092662 use crate :: migrate:: {
2610- MigrateCtx , MigrateMulti , PayloadOffer , PayloadOffers , PayloadOutputs ,
2663+ MigrateCtx , MigrateMulti , MigrateStateError , PayloadOffer ,
2664+ PayloadOffers , PayloadOutputs ,
26112665 } ;
26122666 use crate :: Machine ;
26132667 use std:: env:: VarError ;
@@ -2620,6 +2674,7 @@ mod test {
26202674 vnic_name : String ,
26212675 machine : Machine ,
26222676 dev : Arc < PciVirtioViona > ,
2677+ params : Option < DeviceParams > ,
26232678 }
26242679
26252680 impl Drop for TestCtx {
@@ -2635,6 +2690,19 @@ mod test {
26352690 }
26362691
26372692 fn migrate ( self ) -> TestCtx {
2693+ let params = self . params ;
2694+ let ( ctx, res) = self . migrate_to ( params) ;
2695+ res. expect ( "can import PciVirtioViona" ) ;
2696+ ctx
2697+ }
2698+
2699+ /// Export this device, tear it down, and import the payload into a
2700+ /// fresh device built with `params`. The target is returned alongside
2701+ /// the import result so its state can be inspected after a rejection.
2702+ fn migrate_to (
2703+ self ,
2704+ params : Option < DeviceParams > ,
2705+ ) -> ( TestCtx , Result < ( ) , MigrateStateError > ) {
26382706 let mut dev_payloads = PayloadOutputs :: new ( ) ;
26392707 let acc_mem =
26402708 self . machine . acc_mem . access ( ) . expect ( "machine has memory" ) ;
@@ -2681,25 +2749,31 @@ mod test {
26812749 create_vnic ( & underlying_nic, & vnic_name) ;
26822750
26832751 let new_ctx =
2684- create_test_ctx ( test_name, & underlying_nic, & vnic_name) ;
2752+ create_test_ctx ( test_name, & underlying_nic, & vnic_name, params ) ;
26852753 let acc_mem = new_ctx
26862754 . machine
26872755 . acc_mem
26882756 . access ( )
26892757 . expect ( "new machine has memory" ) ;
26902758 let new_migrate = MigrateCtx { mem : & acc_mem } ;
2691- <PciVirtioViona >:: import ( & new_ctx. dev , & mut offers, & new_migrate)
2692- . expect ( "can import PciVirtioViona" ) ;
2759+ let res = <PciVirtioViona >:: import (
2760+ & new_ctx. dev ,
2761+ & mut offers,
2762+ & new_migrate,
2763+ ) ;
2764+ // Start the target either way: a rejected import leaves the
2765+ // kernel state untouched, so it comes up as a cold boot would.
26932766 Lifecycle :: start ( new_ctx. dev . as_ref ( ) )
26942767 . expect ( "can start viona device" ) ;
2695- new_ctx
2768+ ( new_ctx, res )
26962769 }
26972770 }
26982771
26992772 fn create_test_ctx (
27002773 test_name : & ' static str ,
27012774 underlying_nic : & str ,
27022775 vnic_name : & str ,
2776+ params : Option < DeviceParams > ,
27032777 ) -> TestCtx {
27042778 // Create the VM with `force: true`: if we're running tests concurrently
27052779 // this will trample an existing test (which should then fail!). We do
@@ -2735,7 +2809,7 @@ mod test {
27352809 enable_pcie : false ,
27362810 } ,
27372811 ) ;
2738- let viona_dev = PciVirtioViona :: new ( vnic_name, & machine. hdl , None )
2812+ let viona_dev = PciVirtioViona :: new ( vnic_name, & machine. hdl , params )
27392813 . expect ( "can create test vnic" ) ;
27402814
27412815 chipset_hb. pci_attach ( i440fx:: DEFAULT_HB_BDF , chipset_hb. clone ( ) , None ) ;
@@ -2752,6 +2826,7 @@ mod test {
27522826 test_name,
27532827 underlying_nic : underlying_nic. to_owned ( ) ,
27542828 vnic_name : vnic_name. to_owned ( ) ,
2829+ params,
27552830 }
27562831 }
27572832
@@ -4064,7 +4139,8 @@ mod test {
40644139 assert ! ( test_ctx. dev. inner. lock( ) . unwrap( ) . mac_override. is_none( ) ) ;
40654140
40664141 // The feature is only advertised with kernel support for the
4067- // address swap, so the remainder needs a V7 kernel.
4142+ // address swap and the policy grant (supplied by the test harness),
4143+ // so the remainder needs a V7 kernel.
40684144 if !test_ctx. dev . kernel_mac_filters {
40694145 return test_ctx;
40704146 }
@@ -4220,6 +4296,51 @@ mod test {
42204296 test_ctx
42214297 }
42224298
4299+ /// A host that does not grant guest MAC replacement neither advertises
4300+ /// `VIRTIO_NET_F_CTRL_MAC_ADDR` nor imports a source that negotiated
4301+ /// it. Instead, the guest would keep the bit and could issue further
4302+ /// swaps.
4303+ fn control_mac_addr_policy_denied ( test_ctx : TestCtx ) -> TestCtx {
4304+ if test_ctx. dev . pinned_promisc ( ) . is_some ( ) {
4305+ // Pinned link, see `control_rx_filtering`.
4306+ return test_ctx;
4307+ }
4308+
4309+ if !test_ctx. dev . kernel_mac_filters {
4310+ // See `control_mac_addr_replacement`.
4311+ return test_ctx;
4312+ }
4313+
4314+ // The source (with the grant) negotiates the bit and installs an
4315+ // override.
4316+ let mut driver = test_ctx. create_driver ( ) ;
4317+ driver. modern_device_init (
4318+ VIRTIO_NET_F_MAC
4319+ | VIRTIO_NET_F_STATUS
4320+ | VIRTIO_NET_F_CTRL_VQ
4321+ | VIRTIO_NET_F_CTRL_RX
4322+ | VIRTIO_NET_F_CTRL_MAC_ADDR ,
4323+ ) ;
4324+ let replacement = MacAddr ( [ 0x02 , 0x08 , 0x20 , 0xac , 0x70 , 0x88 ] ) ;
4325+ let ack = driver. ctrl_mac_addr_set ( replacement) ;
4326+ assert_eq ! ( ack, control:: Ack :: Ok as u8 ) ;
4327+
4328+ // A target without the grant must reject the import.
4329+ let no_grant = DeviceParams {
4330+ allow_guest_mac_change : false ,
4331+ ..Default :: default ( )
4332+ } ;
4333+ let ( test_ctx, res) = test_ctx. migrate_to ( Some ( no_grant) ) ;
4334+ let err = res. expect_err ( "import must fail without the policy grant" ) ;
4335+ assert ! ( matches!( err, MigrateStateError :: ImportFailed ( _) ) ) ;
4336+
4337+ // Nor does such a host advertise the feature in the first place,
4338+ // (kernel support notwithstanding).
4339+ assert_eq ! ( test_ctx. dev. features( ) & VIRTIO_NET_F_CTRL_MAC_ADDR , 0 ) ;
4340+
4341+ test_ctx
4342+ }
4343+
42234344 // Bears an uncanny resemblance to `phd-test`...
42244345 struct TestCase {
42254346 name : & ' static str ,
@@ -4280,6 +4401,7 @@ mod test {
42804401 testcase ! ( control_rx_migration) ,
42814402 testcase ! ( control_mac_addr_replacement) ,
42824403 testcase ! ( control_mac_addr_migration) ,
4404+ testcase ! ( control_mac_addr_policy_denied) ,
42834405 ] ;
42844406
42854407 let underlying_nic = match std:: env:: var ( "VIONA_TEST_NIC" ) {
@@ -4318,8 +4440,18 @@ mod test {
43184440 create_vnic ( & underlying_nic, TEST_VNIC ) ;
43194441
43204442 let res = std:: panic:: catch_unwind ( move || {
4321- let test_ctx =
4322- create_test_ctx ( test. name , & underlying_nic, TEST_VNIC ) ;
4443+ // Grant guest MAC replacement so the `control_mac_addr_*`
4444+ // tests can negotiate `VIRTIO_NET_F_CTRL_MAC_ADDR`
4445+ let params = DeviceParams {
4446+ allow_guest_mac_change : true ,
4447+ ..Default :: default ( )
4448+ } ;
4449+ let test_ctx = create_test_ctx (
4450+ test. name ,
4451+ & underlying_nic,
4452+ TEST_VNIC ,
4453+ Some ( params) ,
4454+ ) ;
43234455 Lifecycle :: start ( test_ctx. dev . as_ref ( ) )
43244456 . expect ( "can start viona device" ) ;
43254457 let test_ctx = ( test. test_fn ) ( test_ctx) ;
0 commit comments