diff --git a/Cargo.lock b/Cargo.lock index 036951799..cb7c65e4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2606,7 +2606,7 @@ dependencies = [ "dlpi 0.2.0 (git+https://github.com/oxidecomputer/dlpi-sys)", "libc", "num_enum 0.5.11", - "nvpair", + "nvpair 0.5.0", "nvpair-sys", "oxnet", "rand", @@ -3204,6 +3204,14 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "nvpair" +version = "0.0.0" +dependencies = [ + "libc", + "nvpair_sys", +] + [[package]] name = "nvpair" version = "0.5.0" @@ -3219,6 +3227,13 @@ name = "nvpair-sys" version = "0.4.0" source = "git+https://github.com/jmesmon/rust-libzfs?branch=master#ecd5a922247a6c5acef55d76c5b8d115572bc850" +[[package]] +name = "nvpair_sys" +version = "0.0.0" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -6816,6 +6831,7 @@ name = "viona_api" version = "0.0.0" dependencies = [ "libc", + "nvpair 0.0.0", "viona_api_sys", ] diff --git a/Cargo.toml b/Cargo.toml index 4d93671e8..396b1d102 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ default-members = [ exclude = [ "crates/bhyve-api/header-check", + "crates/nvpair/header-check", "crates/viona-api/header-check", "phd-tests/buildomat", ] @@ -46,6 +47,8 @@ bhyve_api_sys = { path = "crates/bhyve-api/sys" } cpuid_utils = { path = "crates/cpuid-utils" } cpuid_profile_config = { path = "crates/cpuid-profile-config" } dladm = { path = "crates/dladm" } +nvpair = { path = "crates/nvpair" } +nvpair_sys = { path = "crates/nvpair/sys" } propolis-server-config = { path = "crates/propolis-server-config" } propolis_api_types = { path = "crates/propolis-api-types" } propolis_types = { path = "crates/propolis-types" } diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 5e8069b7d..7779b9237 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -713,10 +713,32 @@ impl<'a> MachineInitializer<'a> { info!(self.log, "Creating vNIC {}", device_name); let bdf: pci::Bdf = nic.device_spec.pci_path.into(); + // Set viona device params if possible + let params = if virtio::viona::api_version() + .expect("can query viona version") + >= virtio::viona::ApiVersion::V3 + { + Some(virtio::viona::DeviceParams { + // Use guest memory "loaning", rather than copying and + // allocating entire transmitted packets + copy_data: false, + // Leave room for underlay encapsulation: + // - ethernet: 14 + // - IPv6: 40 + // - UDP: 8 + // - Geneve: 8 + // - (and then round up to nearest 8) + header_pad: 72, + }) + } else { + None + }; + let viona = virtio::PciVirtioViona::new( &nic.backend_spec.vnic_name, 0x100, &self.machine.hdl, + params, ) .with_context(|| { format!("failed to create viona device {device_name:?}") diff --git a/bin/propolis-standalone/src/config.rs b/bin/propolis-standalone/src/config.rs index 3631eddee..fcefeb1a8 100644 --- a/bin/propolis-standalone/src/config.rs +++ b/bin/propolis-standalone/src/config.rs @@ -122,6 +122,34 @@ struct MemAsyncConfig { workers: Option, } +#[derive(Deserialize)] +pub struct VionaDeviceParams { + tx_copy_data: Option, + tx_header_pad: Option, +} +impl VionaDeviceParams { + pub fn from_opts( + opts: &BTreeMap, + ) -> Result, anyhow::Error> + { + use propolis::hw::virtio::viona::DeviceParams; + let parsed: Self = opt_deser(opts)?; + let out = if parsed.tx_copy_data.is_some() + || parsed.tx_header_pad.is_some() + { + let default = DeviceParams::default(); + + Some(DeviceParams { + copy_data: parsed.tx_copy_data.unwrap_or(default.copy_data), + header_pad: parsed.tx_header_pad.unwrap_or(default.header_pad), + }) + } else { + None + }; + Ok(out) + } +} + // Try to turn unmatched flattened options into a config struct fn opt_deser<'de, T: Deserialize<'de>>( value: &BTreeMap, diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 1a0b23b8f..702fb409f 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -1192,8 +1192,24 @@ fn setup_instance( dev.options.get("vnic").unwrap().as_str().unwrap(); let bdf = bdf.unwrap(); + let viona_params = + config::VionaDeviceParams::from_opts(&dev.options) + .expect("viona params are valid"); + + if viona_params.is_some() + && hw::virtio::viona::api_version() + .expect("can query viona version") + < hw::virtio::viona::ApiVersion::V3 + { + // lazy cop-out for now + panic!("can't set viona params on too-old version"); + } + let viona = hw::virtio::PciVirtioViona::new( - vnic_name, 0x100, &hdl, + vnic_name, + 0x100, + &hdl, + viona_params, )?; guard.inventory.register_instance(&viona, &bdf.to_string()); chipset_pci_attach(bdf, viona); diff --git a/crates/nvpair/Cargo.toml b/crates/nvpair/Cargo.toml new file mode 100644 index 000000000..f04defc03 --- /dev/null +++ b/crates/nvpair/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nvpair" +version = "0.0.0" +license = "MPL-2.0" +edition = "2021" + +[lib] +doctest = false + +[dependencies] +nvpair_sys.workspace = true +libc.workspace = true diff --git a/crates/nvpair/header-check/Cargo.toml b/crates/nvpair/header-check/Cargo.toml new file mode 100644 index 000000000..b71141859 --- /dev/null +++ b/crates/nvpair/header-check/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nvpair-hdrchk" +version = "0.0.0" +license = "MPL-2.0" +edition = "2021" +build = "build.rs" +publish = false + +[dependencies] +nvpair_sys = { path = "../sys" } +libc = "0.2" + +[build-dependencies] +cc = "1" +ctest2 = "0.4.7" + +[[test]] +name = "main" +path = "test/main.rs" +harness = false diff --git a/crates/nvpair/header-check/build.rs b/crates/nvpair/header-check/build.rs new file mode 100644 index 000000000..fd64029c3 --- /dev/null +++ b/crates/nvpair/header-check/build.rs @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![deny(warnings)] + +fn main() { + let mut cfg = ctest2::TestGenerator::new(); + + cfg.header("libnvpair.h"); + + cfg.type_name(|ty, is_struct, is_union| match ty { + t if t.ends_with("_t") => t.to_string(), + t if is_struct => format!("struct {t}"), + t if is_union => format!("union {t}"), + t => t.to_string(), + }); + + cfg.skip_const(move |name| match name { + _ => false, + }); + + cfg.skip_struct(|name| match name { + _ => false, + }); + + cfg.skip_field_type(|ty, field| match (ty, field) { + _ => false, + }); + + cfg.generate("../sys/src/lib.rs", "main.rs"); +} diff --git a/crates/nvpair/header-check/test/main.rs b/crates/nvpair/header-check/test/main.rs new file mode 100644 index 000000000..26be28d05 --- /dev/null +++ b/crates/nvpair/header-check/test/main.rs @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use nvpair_sys::*; + +include!(concat!(env!("OUT_DIR"), "/main.rs")); diff --git a/crates/nvpair/src/lib.rs b/crates/nvpair/src/lib.rs new file mode 100644 index 000000000..f08dc6395 --- /dev/null +++ b/crates/nvpair/src/lib.rs @@ -0,0 +1,229 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![allow(clippy::new_without_default)] + +use nvpair_sys::*; + +use std::ffi::CStr; +use std::ptr::NonNull; + +pub struct NvList(NonNull); + +impl NvList { + pub fn new() -> Self { + unsafe { + let nvlp = fnvlist_alloc(); + Self(NonNull::new_unchecked(nvlp)) + } + } + pub fn pack(&mut self) -> Packed { + unsafe { + let mut size = 0; + let ptr = fnvlist_pack(self.0.as_mut(), &mut size); + Packed { data: NonNull::new_unchecked(ptr.cast()), size } + } + } + + pub fn unpack(buf: &mut [u8]) -> std::io::Result { + Self::unpack_ptr(buf.as_mut_ptr(), buf.len()) + } + + pub fn unpack_ptr(buf: *mut u8, len: usize) -> std::io::Result { + let mut nvp = std::ptr::null_mut(); + match unsafe { nvlist_unpack(buf.cast(), len, &mut nvp, 0) } { + 0 => Ok(Self( + NonNull::new(nvp) + .expect("nvlist_unpack emits non-NULL pointer on success"), + )), + err => Err(std::io::Error::from_raw_os_error(err)), + } + } + + #[inline(always)] + pub fn add<'a>( + &'a mut self, + name: impl Into>, + value: impl Into>, + ) { + self.add_name_value(name.into(), value.into()); + } + + pub fn add_name_value(&mut self, name: NvName, value: NvData) { + unsafe { + let name = name.as_ptr(); + let nvlp = self.0.as_mut(); + + match value { + NvData::Boolean => { + fnvlist_add_boolean(nvlp, name); + } + NvData::BooleanValue(val) => { + fnvlist_add_boolean_value(nvlp, name, val.into()); + } + NvData::Byte(val) => { + fnvlist_add_byte(nvlp, name, val); + } + NvData::Int8(val) => { + fnvlist_add_int8(nvlp, name, val); + } + NvData::UInt8(val) => { + fnvlist_add_uint8(nvlp, name, val); + } + NvData::Int16(val) => { + fnvlist_add_int16(nvlp, name, val); + } + NvData::UInt16(val) => { + fnvlist_add_uint16(nvlp, name, val); + } + NvData::Int32(val) => { + fnvlist_add_int32(nvlp, name, val); + } + NvData::UInt32(val) => { + fnvlist_add_uint32(nvlp, name, val); + } + NvData::Int64(val) => { + fnvlist_add_int64(nvlp, name, val); + } + NvData::UInt64(val) => { + fnvlist_add_uint64(nvlp, name, val); + } + NvData::NvList(val) => { + // SAFETY: while this takes a *mut nvlist_t, we are counting + // on libnvpair to not actually mutate the to-be-added list. + fnvlist_add_nvlist(nvlp, name, val.0.as_ptr()); + } + NvData::String(val) => { + fnvlist_add_string(nvlp, name, val.as_ptr()); + } + } + } + } +} +impl Drop for NvList { + fn drop(&mut self) { + unsafe { + fnvlist_free(self.0.as_mut()); + } + } +} + +pub struct Packed { + data: NonNull, + size: usize, +} +impl Packed { + pub fn as_ptr(&self) -> *const u8 { + self.data.as_ptr() + } + pub fn as_mut_ptr(&mut self) -> *mut u8 { + self.data.as_ptr() + } +} +impl AsRef<[u8]> for Packed { + fn as_ref(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.data.as_ptr(), self.size) } + } +} +impl Drop for Packed { + fn drop(&mut self) { + unsafe { fnvlist_pack_free(self.data.as_ptr().cast(), self.size) } + } +} + +macro_rules! nvdata_from { + (&$l:lifetime $t:ty, $i:ident) => { + impl<$l> From<& $l $t> for NvData<$l> { + fn from(value: & $l $t) -> Self { + Self::$i(value) + } + } + }; + ($t:ty, $i:ident) => { + impl From<$t> for NvData<'_> { + fn from(value: $t) -> Self { + Self::$i(value) + } + } + }; +} + +pub enum NvData<'a> { + Boolean, + BooleanValue(bool), + Byte(u8), + Int8(i8), + UInt8(u8), + Int16(i16), + UInt16(u16), + Int32(i32), + UInt32(u32), + Int64(i64), + UInt64(u64), + NvList(&'a NvList), + String(&'a CStr), +} + +nvdata_from!(bool, BooleanValue); +nvdata_from!(i8, Int8); +nvdata_from!(u8, UInt8); +nvdata_from!(i16, Int16); +nvdata_from!(u16, UInt16); +nvdata_from!(i32, Int32); +nvdata_from!(u32, UInt32); +nvdata_from!(i64, Int64); +nvdata_from!(u64, UInt64); +nvdata_from!(&'a CStr, String); + +pub enum NvName<'a> { + Owned(Vec), + Loaned(&'a [u8]), +} +impl<'a> NvName<'a> { + pub fn as_ptr(&self) -> *const i8 { + match self { + NvName::Owned(v) => v.as_ptr().cast(), + NvName::Loaned(s) => s.as_ptr().cast(), + } + } +} +impl AsRef<[u8]> for NvName<'_> { + fn as_ref(&self) -> &[u8] { + match self { + NvName::Owned(b) => b.as_slice(), + NvName::Loaned(s) => s, + } + } +} +impl Clone for NvName<'_> { + fn clone(&self) -> Self { + match self { + NvName::Owned(v) => NvName::Owned(v.clone()), + NvName::Loaned(s) => NvName::Owned(s.to_vec()), + } + } +} +impl<'a> From<&'a str> for NvName<'a> { + fn from(value: &'a str) -> Self { + let bytes = value.as_bytes(); + if let Some(nul_idx) = + bytes.iter().enumerate().find_map(|(idx, b)| match *b { + 0 => Some(idx), + _ => None, + }) + { + Self::Loaned(&bytes[..=nul_idx]) + } else { + let mut copy = Vec::with_capacity(bytes.len() + 1); + copy.extend(bytes); + copy.push(0); + Self::Owned(copy) + } + } +} +impl<'a> From<&'a CStr> for NvName<'a> { + fn from(value: &'a CStr) -> Self { + Self::Loaned(value.to_bytes()) + } +} diff --git a/crates/nvpair/sys/Cargo.toml b/crates/nvpair/sys/Cargo.toml new file mode 100644 index 000000000..b50edd82c --- /dev/null +++ b/crates/nvpair/sys/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nvpair_sys" +version = "0.0.0" +license = "MPL-2.0" +edition = "2021" + +[lib] +test = false +doctest = false + +[dependencies] +libc.workspace = true diff --git a/crates/nvpair/sys/src/lib.rs b/crates/nvpair/sys/src/lib.rs new file mode 100644 index 000000000..f6c1f1918 --- /dev/null +++ b/crates/nvpair/sys/src/lib.rs @@ -0,0 +1,206 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![allow(non_camel_case_types, non_snake_case)] + +use std::ffi::{c_char, c_int}; + +use libc::size_t; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(i32)] +pub enum data_type_t { + DATA_TYPE_DONTCARE = -1, + DATA_TYPE_UNKNOWN = 0, + DATA_TYPE_BOOLEAN, + DATA_TYPE_BYTE, + DATA_TYPE_INT16, + DATA_TYPE_UINT16, + DATA_TYPE_INT32, + DATA_TYPE_UINT32, + DATA_TYPE_INT64, + DATA_TYPE_UINT64, + DATA_TYPE_STRING, + DATA_TYPE_BYTE_ARRAY, + DATA_TYPE_INT16_ARRAY, + DATA_TYPE_UINT16_ARRAY, + DATA_TYPE_INT32_ARRAY, + DATA_TYPE_UINT32_ARRAY, + DATA_TYPE_INT64_ARRAY, + DATA_TYPE_UINT64_ARRAY, + DATA_TYPE_STRING_ARRAY, + DATA_TYPE_HRTIME, + DATA_TYPE_NVLIST, + DATA_TYPE_NVLIST_ARRAY, + DATA_TYPE_BOOLEAN_VALUE, + DATA_TYPE_INT8, + DATA_TYPE_UINT8, + DATA_TYPE_BOOLEAN_ARRAY, + DATA_TYPE_INT8_ARRAY, + DATA_TYPE_UINT8_ARRAY, +} + +#[derive(Copy, Clone, Debug)] +#[repr(C)] +pub struct nvpair_t { + pub nvp_size: i32, + pub nvp_name_sz: i16, + pub nvp_reserve: i16, + pub nvp_value_elem: i32, + pub nvp_type: i32, +} +impl nvpair_t { + /// Get the name string from an `nvpair_t` pointer + /// + /// # Safety + /// The `nvpair_t` pointer must be allocated from libnvpair to ensure + /// expected positioning of name data. + pub const unsafe fn NVP_NAME(nvp: *mut nvpair_t) -> *mut c_char { + nvp.add(1).cast() + } + /// Get the value address from an `nvpair_t` pointer + /// + /// # Safety + /// The `nvpair_t` pointer must be allocated from libnvpair to ensure + /// expected positioning of value data. + pub unsafe fn NVP_VALUE(nvp: *mut nvpair_t) -> *mut c_char { + let name_sz = (*nvp).nvp_name_sz; + + NV_ALIGN(nvp.add(1) as usize + name_sz as usize) as *mut c_char + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(C)] +pub struct nvlist_t { + pub nvl_version: i32, + pub nvl_nvflag: u32, + pub nvl_priv: u64, + pub nvl_flag: u32, + pub nvl_pad: i32, +} + +pub const NV_VERSION: i32 = 0; + +pub const NV_ENCODE_NATIVE: u32 = 0; +pub const NV_ENCODE_XDR: u32 = 1; + +pub const NV_UNIQUE_NAME: u32 = 0x1; +pub const NV_UNIQUE_NAME_TYPE: u32 = 0x2; + +pub const NV_FLAG_NOENTOK: u32 = 0x1; + +pub const fn NV_ALIGN(addr: usize) -> usize { + (addr + 7) & !7usize +} + +#[repr(C)] +pub enum boolean_t { + B_FALSE = 0, + B_TRUE = 1, +} +impl From for boolean_t { + fn from(value: bool) -> Self { + match value { + false => boolean_t::B_FALSE, + _ => boolean_t::B_TRUE, + } + } +} +impl From for bool { + fn from(value: boolean_t) -> Self { + match value { + boolean_t::B_FALSE => false, + _ => false, + } + } +} + +#[cfg_attr(target_os = "illumos", link(name = "nvpair"))] +extern "C" { + pub fn nvlist_remove( + nvl: *mut nvlist_t, + name: *const c_char, + dtype: data_type_t, + ) -> c_int; + pub fn nvlist_remove_all(nvl: *mut nvlist_t, name: *const c_char) -> c_int; + pub fn nvlist_remove_nvpair( + nvl: *mut nvlist_t, + nvp: *mut nvpair_t, + ) -> c_int; + pub fn nvlist_lookup_nvpair( + nvl: *mut nvlist_t, + name: *const c_char, + nvp: *mut *mut nvpair_t, + ) -> c_int; + + pub fn nvlist_next_nvpair( + nvl: *mut nvlist_t, + nvp: *mut nvpair_t, + ) -> *mut nvpair_t; + pub fn nvlist_prev_nvpair( + nvl: *mut nvlist_t, + nvp: *mut nvpair_t, + ) -> *mut nvpair_t; + + pub fn nvlist_exists(nvl: *mut nvlist_t, nvp: *const c_char) -> boolean_t; + pub fn nvlist_empty(nvl: *mut nvlist_t) -> boolean_t; + + pub fn nvlist_unpack( + buf: *mut c_char, + size: size_t, + nvlp: *mut *mut nvlist_t, + flags: c_int, + ) -> c_int; + + pub fn fnvlist_alloc() -> *mut nvlist_t; + pub fn fnvlist_free(nvl: *mut nvlist_t); + pub fn fnvlist_size(nvl: *mut nvlist_t) -> size_t; + pub fn fnvlist_pack(nvl: *mut nvlist_t, sizep: *mut size_t) -> *mut c_char; + pub fn fnvlist_pack_free(packed: *mut c_char, size: size_t); + pub fn fnvlist_unpack(buf: *mut c_char, size: size_t) -> *mut nvlist_t; + pub fn fnvlist_dup(nvl: *mut nvlist_t) -> *mut nvlist_t; + pub fn fnvlist_merge(dst_nvl: *mut nvlist_t, src_nvl: *mut nvlist_t); + pub fn fnvlist_num_pairs(nvl: *mut nvlist_t) -> size_t; + + pub fn fnvlist_add_boolean(nvl: *mut nvlist_t, name: *const c_char); + pub fn fnvlist_add_boolean_value( + nvl: *mut nvlist_t, + name: *const c_char, + val: boolean_t, + ); + pub fn fnvlist_add_byte(nvl: *mut nvlist_t, name: *const c_char, val: u8); + pub fn fnvlist_add_int8(nvl: *mut nvlist_t, name: *const c_char, val: i8); + pub fn fnvlist_add_uint8(nvl: *mut nvlist_t, name: *const c_char, val: u8); + pub fn fnvlist_add_int16(nvl: *mut nvlist_t, name: *const c_char, val: i16); + pub fn fnvlist_add_uint16( + nvl: *mut nvlist_t, + name: *const c_char, + val: u16, + ); + pub fn fnvlist_add_int32(nvl: *mut nvlist_t, name: *const c_char, val: i32); + pub fn fnvlist_add_uint32( + nvl: *mut nvlist_t, + name: *const c_char, + val: u32, + ); + pub fn fnvlist_add_int64(nvl: *mut nvlist_t, name: *const c_char, val: i64); + pub fn fnvlist_add_uint64( + nvl: *mut nvlist_t, + name: *const c_char, + val: u64, + ); + pub fn fnvlist_add_string( + nvl: *mut nvlist_t, + name: *const c_char, + val: *const c_char, + ); + pub fn fnvlist_add_nvlist( + nvl: *mut nvlist_t, + name: *const c_char, + val: *mut nvlist_t, + ); + pub fn fnvlist_add_nvpair(nvl: *mut nvlist_t, val: *mut nvpair_t); + // TODO: add_*_array functions +} diff --git a/crates/viona-api/Cargo.toml b/crates/viona-api/Cargo.toml index 5997a9f46..6c3aefc33 100644 --- a/crates/viona-api/Cargo.toml +++ b/crates/viona-api/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] libc.workspace = true viona_api_sys.workspace = true +nvpair.workspace = true [features] falcon = ["viona_api_sys/falcon"] diff --git a/crates/viona-api/src/lib.rs b/crates/viona-api/src/lib.rs index 69ad2c53d..6210807e7 100644 --- a/crates/viona-api/src/lib.rs +++ b/crates/viona-api/src/lib.rs @@ -7,6 +7,7 @@ use std::io::{Error, ErrorKind, Result}; use std::os::fd::*; use std::os::unix::fs::MetadataExt; +pub use nvpair::NvList; pub use viona_api_sys::*; pub const VIONA_DEV_PATH: &str = "/dev/viona"; @@ -31,6 +32,51 @@ impl VionaFd { Ok(Self(fp)) } + pub fn set_parameters( + &self, + params: &mut NvList, + ) -> std::result::Result<(), ParamError> { + let mut errbuf: Vec = Vec::with_capacity(VIONA_MAX_PARAM_NVLIST_SZ); + + let mut packed = params.pack(); + let vsp_param_sz = packed.as_ref().len(); + + let mut ioc = vioc_set_params { + vsp_param: packed.as_mut_ptr().cast(), + vsp_param_sz, + vsp_error: errbuf.as_mut_ptr().cast(), + vsp_error_sz: errbuf.capacity(), + }; + match unsafe { self.ioctl(VNA_IOC_SET_PARAMS, &mut ioc) } { + Ok(_) if ioc.vsp_error_sz == 0 => Ok(()), + Ok(_) => { + assert!(ioc.vsp_error_sz <= errbuf.capacity()); + unsafe { errbuf.set_len(ioc.vsp_error_sz) }; + + match NvList::unpack(&mut errbuf[..]) { + Ok(detail) => Err(ParamError::Detailed(detail)), + Err(e) => Err(ParamError::Io(e)), + } + } + Err(e) => Err(ParamError::Io(e)), + } + } + + pub fn get_parameters(&self) -> Result { + let mut buf: Vec = Vec::with_capacity(VIONA_MAX_PARAM_NVLIST_SZ); + + let mut ioc = vioc_get_params { + vgp_param: buf.as_mut_ptr().cast(), + vgp_param_sz: buf.capacity(), + }; + let _ = unsafe { self.ioctl(VNA_IOC_GET_PARAMS, &mut ioc) }?; + + assert!(ioc.vgp_param_sz <= buf.capacity()); + unsafe { buf.set_len(ioc.vgp_param_sz) }; + + NvList::unpack(&mut buf[..]) + } + /// Issue ioctl against open viona instance /// /// # Safety @@ -99,6 +145,11 @@ impl AsRawFd for VionaFd { } } +pub enum ParamError { + Io(std::io::Error), + Detailed(NvList), +} + #[cfg(target_os = "illumos")] unsafe fn ioctl(fd: RawFd, cmd: i32, data: *mut libc::c_void) -> Result { match libc::ioctl(fd, cmd, data) { @@ -121,6 +172,9 @@ unsafe fn ioctl( #[repr(u32)] #[derive(Copy, Clone)] pub enum ApiVersion { + /// Adds support for interface parameters + V3 = 3, + /// Adds support for non-vnic datalink devices V2 = 2, @@ -129,7 +183,7 @@ pub enum ApiVersion { } impl ApiVersion { pub const fn current() -> Self { - Self::V2 + Self::V3 } } impl PartialEq for u32 { @@ -143,6 +197,52 @@ impl PartialOrd for u32 { } } +use std::sync::atomic::{AtomicI64, Ordering}; + +/// Store a cached copy of the queried API version. Negative values indicate an +/// error occurred during query (and hold the corresponding negated `errno`). +/// A positive value indicates the cached version, and should be less than +/// `u32::MAX`. A value of 0 indicates that no query has been performed yet. +static VERSION_CACHE: AtomicI64 = AtomicI64::new(0); + +/// Query the API version from the viona device on the system. +/// +/// Caches said version (or any emitted error) for later calls. +pub fn api_version() -> Result { + cache_api_version(|| -> Result { + let ctl = VionaFd::open()?; + let vers = ctl.api_version()?; + Ok(vers) + }) +} + +fn cache_api_version(do_query: impl FnOnce() -> Result) -> Result { + if VERSION_CACHE.load(Ordering::Acquire) == 0 { + let newval = match do_query() { + Ok(x) => i64::from(x), + Err(e) => -i64::from(e.raw_os_error().unwrap_or(libc::ENOENT)), + }; + let _ = VERSION_CACHE.compare_exchange( + 0, + newval, + Ordering::Relaxed, + Ordering::Relaxed, + ); + } + + match VERSION_CACHE.load(Ordering::Acquire) { + 0 => { + panic!("expected VERSION_CACHE to be initialized") + } + x if x < 0 => Err(Error::from_raw_os_error(-x as i32)), + y => { + assert!(y < i64::from(u32::MAX)); + + Ok(y as u32) + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/viona-api/sys/src/lib.rs b/crates/viona-api/sys/src/lib.rs index fbb320c56..8fccbb698 100644 --- a/crates/viona-api/sys/src/lib.rs +++ b/crates/viona-api/sys/src/lib.rs @@ -8,6 +8,7 @@ pub mod ioctls { pub const VNA_IOC_CREATE: i32 = VNA_IOC | 0x01; pub const VNA_IOC_DELETE: i32 = VNA_IOC | 0x02; pub const VNA_IOC_VERSION: i32 = VNA_IOC | 0x03; + pub const VNA_IOC_DEFAULT_PARAMS: i32 = VNA_IOC | 0x04; pub const VNA_IOC_RING_INIT: i32 = VNA_IOC | 0x10; pub const VNA_IOC_RING_RESET: i32 = VNA_IOC | 0x11; @@ -23,6 +24,8 @@ pub mod ioctls { pub const VNA_IOC_GET_FEATURES: i32 = VNA_IOC | 0x22; pub const VNA_IOC_SET_NOTIFY_IOP: i32 = VNA_IOC | 0x23; pub const VNA_IOC_SET_PROMISC: i32 = VNA_IOC | 0x24; + pub const VNA_IOC_GET_PARAMS: i32 = VNA_IOC | 0x25; + pub const VNA_IOC_SET_PARAMS: i32 = VNA_IOC | 0x26; } pub const VIONA_VQ_MAX: u16 = 2; @@ -77,12 +80,30 @@ mod structs { #[cfg(feature = "falcon")] VIONA_PROMISC_ALL_VLAN, } + + use libc::size_t; + use std::ffi::c_void; + + pub struct vioc_get_params { + pub vgp_param: *mut c_void, + pub vgp_param_sz: size_t, + } + + pub struct vioc_set_params { + pub vsp_param: *mut c_void, + pub vsp_param_sz: size_t, + pub vsp_error: *mut c_void, + pub vsp_error_sz: size_t, + } } /// This is the viona interface version which viona_api expects to operate /// against. All constants and structs defined by the crate are done so in /// terms of that specific version. -pub const VIONA_CURRENT_INTERFACE_VERSION: u32 = 2; +pub const VIONA_CURRENT_INTERFACE_VERSION: u32 = 3; + +/// Maximum size of packed nvlists used in viona parameter ioctls +pub const VIONA_MAX_PARAM_NVLIST_SZ: usize = 4096; pub use ioctls::*; pub use structs::*; diff --git a/lib/propolis/src/hw/virtio/viona.rs b/lib/propolis/src/hw/virtio/viona.rs index 724e924c3..22e7b4fcc 100644 --- a/lib/propolis/src/hw/virtio/viona.rs +++ b/lib/propolis/src/hw/virtio/viona.rs @@ -27,6 +27,9 @@ use tokio::io::Interest; use tokio::sync::watch; use tokio::task::JoinHandle; +// Re-export API versioning interface for convenience of propolis consumers +pub use viona_api::{api_version, ApiVersion}; + const ETHERADDRL: usize = 6; /// Viona's in-kernel emulation of the device VirtQueues is performed in what it @@ -86,6 +89,34 @@ impl Inner { } } +/// Configuration parmaeters for the underlying viona device +/// +/// These parameters assume an [viona_api::ApiVersion::V3] device +#[derive(Copy, Clone)] +pub struct DeviceParams { + /// When transmitting packets, should viona (allocate and) copy the entire + /// contents of the packet, rather than "loaning" the guest memory beyond + /// the packet headers? + /// + /// There is a performance cost to copying the full packet, but it avoids + /// certain issues pertaining to looped-back viona packets being delivered + /// to native zones on the machine. + pub copy_data: bool, + + /// Byte count for padding added to the head of transmitted packets. This + /// padding can be used by subsequent operations in the transmission chain, + /// such as encapsulation, which would otherwise need to re-allocate for the + /// larger header. + pub header_pad: u16, +} +impl Default for DeviceParams { + fn default() -> Self { + // Viona (as of V3) allocs/copies entire packet by default, with no + // padding added to the header. + Self { copy_data: true, header_pad: 0 } + } +} + /// Represents a connection to the kernel's Viona (VirtIO Network Adapter) /// driver. pub struct PciVirtioViona { @@ -102,6 +133,7 @@ impl PciVirtioViona { vnic_name: &str, queue_size: u16, vm: &VmmHdl, + viona_params: Option, ) -> io::Result> { let dlhdl = dladm::Handle::new()?; let info = dlhdl.query_link(vnic_name)?; @@ -117,6 +149,22 @@ impl PciVirtioViona { eprintln!("failed to enable promisc mode on {vnic_name}: {e:?}"); } + if let Some(vp) = viona_params { + // Set parameters assuming an ApiVersion::V3 device + let mut params = viona_api::NvList::new(); + params.add(c"tx_copy_data", vp.copy_data); + params.add(c"tx_header_pad", vp.header_pad); + if let Err(e) = hdl.0.set_parameters(&mut params) { + return match e { + viona_api::ParamError::Io(io) => Err(io), + viona_api::ParamError::Detailed(_) => Err(Error::new( + ErrorKind::InvalidInput, + "unsupported viona parameters", + )), + }; + } + } + // TX and RX let queue_count = NonZeroU16::new(2).unwrap(); // interrupts for TX, RX, and device config @@ -864,8 +912,7 @@ use bits::*; /// Check that available viona API matches expectations of propolis crate pub(crate) fn check_api_version() -> Result<(), crate::api_version::Error> { - let fd = viona_api::VionaFd::open()?; - let vers = fd.api_version()?; + let vers = viona_api::api_version()?; // viona only requires the V2 bits for now let compare = viona_api::ApiVersion::V2;