Skip to content

Commit 3c07d60

Browse files
[viona] adapt to stlouis#986 (775, merged)
1 parent e691d68 commit 3c07d60

5 files changed

Lines changed: 164 additions & 27 deletions

File tree

bin/propolis-server/src/lib/initializer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,8 @@ impl MachineInitializer<'_> {
10321032
// - Geneve: 8–16 (due to options)
10331033
// - (and then round up to nearest 8)
10341034
header_pad: 80,
1035+
// Remains "off" until Nexus supplies an allowed-MAC policy.
1036+
allow_guest_mac_change: false,
10351037
});
10361038

10371039
let viona = virtio::PciVirtioViona::new(

bin/propolis-standalone/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ struct MemAsyncConfig {
154154
pub struct VionaDeviceParams {
155155
tx_copy_data: Option<bool>,
156156
tx_header_pad: Option<u16>,
157+
allow_guest_mac_change: Option<bool>,
157158
}
158159
impl VionaDeviceParams {
159160
pub fn from_opts(
@@ -164,12 +165,16 @@ impl VionaDeviceParams {
164165
let parsed: Self = opt_deser(opts)?;
165166
let out = if parsed.tx_copy_data.is_some()
166167
|| parsed.tx_header_pad.is_some()
168+
|| parsed.allow_guest_mac_change.is_some()
167169
{
168170
let default = DeviceParams::default();
169171

170172
Some(DeviceParams {
171173
copy_data: parsed.tx_copy_data.unwrap_or(default.copy_data),
172174
header_pad: parsed.tx_header_pad.unwrap_or(default.header_pad),
175+
allow_guest_mac_change: parsed
176+
.allow_guest_mac_change
177+
.unwrap_or(default.allow_guest_mac_change),
173178
})
174179
} else {
175180
None

crates/viona-api/src/ffi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,19 @@ pub const VMA_ERR_MCAST_RESTORE: u32 = 3;
225225
/// an address remains installed, as a failed restoration leaves none. A
226226
/// nonzero return means no result was returned at all.
227227
///
228+
/// Viona performs the swap as directed. The consumer must validate the
229+
/// requested address against host policy before exposing the swap to a
230+
/// guest, as neither the ability to issue the ioctl nor link protections
231+
/// such as [`mac-nospoof`] stand in for that validation.
232+
///
228233
/// For [`VNA_IOC_GET_MAC_ADDR`], [`vma_addr`] is copied out with the
229234
/// active unicast address of the client, and [`vma_present`] is nonzero
230235
/// when one is installed.
231236
///
232237
/// [`vma_addr`]: vioc_mac_addr::vma_addr
233238
/// [`vma_present`]: vioc_mac_addr::vma_present
234239
/// [`vma_err`]: vioc_mac_addr::vma_err
240+
/// [`mac-nospoof`]: https://github.com/illumos/illumos-gate/blob/d8b08b811c1375eb45accfc2aa105f39f967d364/usr/src/lib/libdladm/common/linkprop.c#L469-L474
235241
#[repr(C)]
236242
#[derive(Default)]
237243
pub struct vioc_mac_addr {

lib/propolis/src/hw/virtio/viona.rs

Lines changed: 150 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
495504
impl DeviceParams {
496505
#[cfg(target_os = "illumos")]
@@ -520,8 +529,9 @@ impl DeviceParams {
520529
impl 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);

tools/check_headers

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,7 @@ set -e
88
# if your changes to Propolis track changes in the OS as well.
99
#
1010
# As a default this ref should probably not change.
11-
#
12-
# This is temporarily pinned to the viona MAC filter table change (stlouis#986,
13-
# https://code.oxide.computer/c/illumos-gate/+/775), which adds the
14-
# VNA_IOC_SET_MAC_FILTERS ioctl and struct vioc_mac_filters checked by the
15-
# viona-api header-check.
16-
#
17-
# Restore to "stlouis" once that change integrates. Note: this ref names a
18-
# specific patchset and must be bumped if a new patchset is uploaded.
19-
HEADER_CHECK_REF="refs/changes/75/775/7"
11+
HEADER_CHECK_REF="stlouis"
2012

2113
# Directories with `ctest2`-based `header-check` crates. This list should track
2214
# the similar exclusions in `Cargo.toml`, and are described more there.

0 commit comments

Comments
 (0)