From 729c29286225997c793e4da1c677e2bf4bef880e Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Fri, 19 Dec 2025 15:04:53 +0000 Subject: [PATCH 1/4] Local storage [4/4]: Use raw zvols Request the new *raw* zvol type when creating disks backed by local storage. Using raw zvols requires waiting for them to be initialized, which the instance start saga is now responsible for polling. In the instance start saga, ensure each raw zvol in parallel because this can take some time. This PR also explicitly removes the inherited encryption for these zvols. This is accomplished by changing `EncryptionDetails` to be an enum where the call site now has to explicitly choose between inheriting the parent dataset's encryption, using `aes-256-gcm`, and explicitly setting encryption off. Also chopped out is setting `volblocksize`: this was a mistake to set, and it should instead be left to the default value. Still to do is to revisit the overhead reserved for zvols: the previous emperical value was not for the new raw zvol type. This is tracked by oxidecomputer/omicron#9591. Fixes #9519. --- Cargo.lock | 29 +- Cargo.toml | 1 + illumos-utils/Cargo.toml | 1 + illumos-utils/src/zfs.rs | 335 +++++++++++++++--- nexus/db-model/src/disk_type_local_storage.rs | 2 + nexus/src/app/sagas/instance_start.rs | 8 +- .../src/dataset_serialization_task.rs | 3 +- sled-agent/src/backing_fs.rs | 6 +- sled-agent/src/bootstrap/pre_server.rs | 3 +- sled-agent/src/sled_agent.rs | 44 ++- sled-storage/src/dataset.rs | 8 +- sled-storage/zfs-test-harness/src/lib.rs | 3 +- workspace-hack/Cargo.toml | 36 +- 13 files changed, 372 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e76aed1376c..f49e0a1e914 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -759,7 +759,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -782,7 +782,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -1745,7 +1745,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -2122,7 +2122,7 @@ dependencies = [ "futures-core", "mio", "parking_lot 0.12.3", - "rustix 1.0.7", + "rustix 1.1.2", "signal-hook", "signal-hook-mio", "winapi", @@ -5165,6 +5165,7 @@ dependencies = [ "oxlog", "oxnet", "regress", + "rustix 1.1.2", "schemars 0.8.22", "serde", "serde_json", @@ -6096,9 +6097,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -9063,6 +9064,8 @@ dependencies = [ "ipnet", "ipnetwork", "itertools 0.10.5", + "itertools 0.12.1", + "itertools 0.13.0", "lalrpop-util", "lazy_static", "libc", @@ -9105,7 +9108,7 @@ dependencies = [ "rsa", "rustc-hash 2.1.1", "rustix 0.38.37", - "rustix 1.0.7", + "rustix 1.1.2", "rustls 0.23.19", "rustls-webpki 0.102.8", "schemars 0.8.22", @@ -12030,15 +12033,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.1", ] [[package]] @@ -13538,7 +13541,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.111", @@ -14182,7 +14185,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.0.7", + "rustix 1.1.2", "windows-sys 0.59.0", ] diff --git a/Cargo.toml b/Cargo.toml index 72dc0a7c2ab..ff125f9dc33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -713,6 +713,7 @@ rustfmt-wrapper = "0.2" rustls = "0.22.2" rustls-pemfile = "2.2.0" rustyline = "14.0.0" +rustix = "1.1.2" samael = { version = "0.0.19", features = ["xmlsec"] } schemars = "0.8.22" scopeguard = "1.2.0" diff --git a/illumos-utils/Cargo.toml b/illumos-utils/Cargo.toml index 165aa2db031..2c1908d761a 100644 --- a/illumos-utils/Cargo.toml +++ b/illumos-utils/Cargo.toml @@ -45,6 +45,7 @@ uuid.workspace = true whoami.workspace = true zone.workspace = true tofino.workspace = true +rustix.workspace = true omicron-workspace-hack.workspace = true diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index de7519d8349..c78827d551f 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -233,6 +233,12 @@ pub enum EnsureDatasetVolumeErrorInner { #[error("expected {value_name} to be {expected}, but saw {actual}")] ValueMismatch { value_name: String, expected: u64, actual: u64 }, + + #[error(transparent)] + RustixErrno(#[from] rustix::io::Errno), + + #[error("created but not ready yet: {reason}")] + NotReady { reason: String }, } /// Error returned by [`Zfs::ensure_dataset_volume`]. @@ -245,6 +251,10 @@ pub struct EnsureDatasetVolumeError { } impl EnsureDatasetVolumeError { + pub fn is_not_ready(&self) -> bool { + matches!(&self.err, EnsureDatasetVolumeErrorInner::NotReady { .. }) + } + pub fn execution(name: String, err: crate::ExecutionError) -> Self { EnsureDatasetVolumeError { name, @@ -288,6 +298,86 @@ impl EnsureDatasetVolumeError { }, } } + + pub fn from_raw_zvol_open(name: String, e: rustix::io::Errno) -> Self { + EnsureDatasetVolumeError { + name, + err: EnsureDatasetVolumeErrorInner::RustixErrno(e), + } + } + + pub fn from_raw_zvol_read(name: String, e: rustix::io::Errno) -> Self { + if e == rustix::io::Errno::INPROGRESS { + EnsureDatasetVolumeError { + name, + err: EnsureDatasetVolumeErrorInner::NotReady { + reason: String::from("raw volume not done initializing"), + }, + } + } else { + EnsureDatasetVolumeError { + name, + err: EnsureDatasetVolumeErrorInner::RustixErrno(e), + } + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum DeleteDatasetVolumeErrorInner { + #[error(transparent)] + Execution(#[from] crate::ExecutionError), + + #[error(transparent)] + RustixErrno(#[from] rustix::io::Errno), + + #[error("created but not ready yet: {reason}")] + NotReady { reason: String }, +} + +/// Error returned by [`Zfs::delete_dataset_volume`]. +#[derive(thiserror::Error, Debug)] +#[error("Failed to delete volume '{name}': {err}")] +pub struct DeleteDatasetVolumeError { + name: String, + #[source] + err: DeleteDatasetVolumeErrorInner, +} + +impl DeleteDatasetVolumeError { + pub fn is_not_ready(&self) -> bool { + matches!(&self.err, DeleteDatasetVolumeErrorInner::NotReady { .. }) + } + + pub fn execution(name: String, err: crate::ExecutionError) -> Self { + DeleteDatasetVolumeError { + name, + err: DeleteDatasetVolumeErrorInner::Execution(err), + } + } + + pub fn from_raw_zvol_open(name: String, e: rustix::io::Errno) -> Self { + DeleteDatasetVolumeError { + name, + err: DeleteDatasetVolumeErrorInner::RustixErrno(e), + } + } + + pub fn from_raw_zvol_read(name: String, e: rustix::io::Errno) -> Self { + if e == rustix::io::Errno::INPROGRESS { + DeleteDatasetVolumeError { + name, + err: DeleteDatasetVolumeErrorInner::NotReady { + reason: String::from("raw volume not done initializing"), + }, + } + } else { + DeleteDatasetVolumeError { + name, + err: DeleteDatasetVolumeErrorInner::RustixErrno(e), + } + } + } } /// Wraps commands for interacting with ZFS. @@ -342,9 +432,20 @@ impl Keypath { } #[derive(Debug)] -pub struct EncryptionDetails { - pub keypath: Keypath, - pub epoch: u64, +pub enum EncryptionDetails { + /// Inherit the parent's encryption settings + Inherit, + + /// Explicitly set encryption to off + Off, + + /// Use a file based `keylocation` and configure `aes-256-gcm` + Aes256Gcm { + keypath: Keypath, + + /// Used to set the `oxide:epoch` option for the dataset. + epoch: u64, + }, } #[derive(Debug, Default)] @@ -623,7 +724,7 @@ pub struct DatasetEnsureArgs<'a> { /// root are implicitly encrypted. For existing filesystems, ensures that /// they are mounted (and that keys are loaded), but does not verify the /// input details. - pub encryption_details: Option, + pub encryption_details: EncryptionDetails, /// Optional properties that can be set for the dataset regarding /// space usage. @@ -899,6 +1000,29 @@ impl DatasetMountInfo { } } +/// Arguments to [Zfs::ensure_dataset_volume] +pub struct DatasetVolumeEnsureArgs<'a> { + /// The full path of the volume + pub name: &'a str, + + pub size: ByteCount, + + /// Create a raw zvol instead of a regular one + pub raw: bool, + + /// Optionally set the volblocksize + pub volblocksize: Option, +} + +/// Arguments to [Zfs::delete_dataset_volume] +pub struct DatasetVolumeDeleteArgs<'a> { + /// The full path of the volume + pub name: &'a str, + + /// Additional actions are required when deleting a raw zvol. + pub raw: bool, +} + impl Zfs { /// Lists all datasets within a pool or existing dataset. /// @@ -1122,19 +1246,27 @@ impl Zfs { if zoned { cmd.args(&["-o", "zoned=on"]); } - if let Some(details) = encryption_details { - let keyloc = format!("keylocation=file://{}", details.keypath); - let epoch = format!("oxide:epoch={}", details.epoch); - cmd.args(&[ - "-o", - "encryption=aes-256-gcm", - "-o", - "keyformat=raw", - "-o", - &keyloc, - "-o", - &epoch, - ]); + match encryption_details { + EncryptionDetails::Inherit => {} + + EncryptionDetails::Off => { + cmd.args(&["-o", "encryption=off"]); + } + + EncryptionDetails::Aes256Gcm { keypath, epoch } => { + let keyloc = format!("keylocation=file://{keypath}"); + let epoch = format!("oxide:epoch={epoch}"); + cmd.args(&[ + "-o", + "encryption=aes-256-gcm", + "-o", + "keyformat=raw", + "-o", + &keyloc, + "-o", + &epoch, + ]); + } } match can_mount { @@ -1411,27 +1543,36 @@ impl Zfs { } pub async fn ensure_dataset_volume( - name: String, - size: ByteCount, - block_size: u32, + params: DatasetVolumeEnsureArgs<'_>, ) -> Result<(), EnsureDatasetVolumeError> { let mut command = Command::new(PFEXEC); let cmd = command.args(&[ZFS, "create"]); - cmd.args(&[ - "-V", - &size.to_bytes().to_string(), - "-o", - &format!("volblocksize={}", block_size), - &name, - ]); + let mut args: Vec = + vec!["-V".to_string(), params.size.to_bytes().to_string()]; + + if params.raw { + args.push("-o".to_string()); + args.push("rawvol=on".to_string()); + } + + if let Some(volblocksize) = ¶ms.volblocksize { + args.push("-o".to_string()); + args.push(format!("volblocksize={}", volblocksize)); + } + + args.push(params.name.to_string()); + + cmd.args(&args); // The command to create a dataset is not idempotent and will fail with // "dataset already exists" if the volume is created already. Eat this // and return Ok instead. match execute_async(cmd).await { - Ok(_) => Ok(()), + Ok(_) => { + // no-op + } Err(crate::ExecutionError::CommandFailure(info)) if info.stderr.contains("dataset already exists") => @@ -1440,55 +1581,143 @@ impl Zfs { // being requested: these cannot be changed once the volume is // created. - let [actual_size, actual_block_size] = - Self::get_values(&name, &["volsize", "volblocksize"], None) - .await - .map_err(|err| { - EnsureDatasetVolumeError::get_value( - name.clone(), - err, - ) - })?; + let [actual_size, actual_volblocksize] = Self::get_values( + params.name, + &["volsize", "volblocksize"], + None, + ) + .await + .map_err(|err| { + EnsureDatasetVolumeError::get_value( + params.name.to_string(), + err, + ) + })?; let actual_size: u64 = actual_size.parse().map_err(|_| { EnsureDatasetVolumeError::value_parse( - name.clone(), + params.name.to_string(), String::from("volsize"), actual_size, ) })?; - let actual_block_size: u32 = - actual_block_size.parse().map_err(|_| { + let actual_volblocksize: u32 = + actual_volblocksize.parse().map_err(|_| { EnsureDatasetVolumeError::value_parse( - name.clone(), + params.name.to_string(), String::from("volblocksize"), - actual_block_size, + actual_volblocksize, ) })?; - if actual_size != size.to_bytes() { + if actual_size != params.size.to_bytes() { return Err(EnsureDatasetVolumeError::value_mismatch( - name.clone(), + params.name.to_string(), String::from("volsize"), - size.to_bytes(), + params.size.to_bytes(), actual_size, )); } - if actual_block_size != block_size { - return Err(EnsureDatasetVolumeError::value_mismatch( - name.clone(), - String::from("volblocksize"), - u64::from(block_size), - u64::from(actual_block_size), - )); + if let Some(volblocksize) = ¶ms.volblocksize { + if actual_volblocksize != *volblocksize { + return Err(EnsureDatasetVolumeError::value_mismatch( + params.name.to_string(), + String::from("volblocksize"), + u64::from(*volblocksize), + u64::from(actual_volblocksize), + )); + } } + } + Err(err) => { + return Err(EnsureDatasetVolumeError::execution( + params.name.to_string(), + err, + )); + } + } + + if params.raw { + // Open the raw zvol and read from it. If EINPROGRESS is seen from + // the read, then it's not done initializing yet. We can't use it + // until it's fully done, so return the `NotReady` variant to signal + // upstack that it should poll. + let path = format!("/dev/zvol/rdsk/{}", params.name); + + let fd = rustix::fs::open( + path, + rustix::fs::OFlags::RDONLY, + rustix::fs::Mode::empty(), + ) + .map_err(|e| { + EnsureDatasetVolumeError::from_raw_zvol_open( + params.name.to_string(), + e, + ) + })?; + + let mut buf = [0u8; 64]; + rustix::io::read(fd, &mut buf).map_err(|e| { + EnsureDatasetVolumeError::from_raw_zvol_read( + params.name.to_string(), + e, + ) + })?; + } + + Ok(()) + } + + pub async fn delete_dataset_volume( + params: DatasetVolumeDeleteArgs<'_>, + ) -> Result<(), DeleteDatasetVolumeError> { + if params.raw { + // Open the raw zvol and read from it. If EINPROGRESS is seen from + // the read, then it's not done initializing yet. We can't delete it + // until it's fully done, so return the `NotReady` variant to signal + // upstack that it should poll. + let path = format!("/dev/zvol/rdsk/{}", params.name); + + let fd = rustix::fs::open( + path, + rustix::fs::OFlags::RDONLY, + rustix::fs::Mode::empty(), + ) + .map_err(|e| { + DeleteDatasetVolumeError::from_raw_zvol_open( + params.name.to_string(), + e, + ) + })?; + + let mut buf = [0u8; 64]; + rustix::io::read(fd, &mut buf).map_err(|e| { + DeleteDatasetVolumeError::from_raw_zvol_read( + params.name.to_string(), + e, + ) + })?; + } + + let mut command = Command::new(PFEXEC); + let cmd = command.args(&[ZFS, "destroy", params.name]); + + match execute_async(cmd).await { + Ok(_) => Ok(()), + + Err(crate::ExecutionError::CommandFailure(info)) + if info.stderr.contains("dataset does not exist") => + { Ok(()) } - Err(err) => Err(EnsureDatasetVolumeError::execution(name, err)), + Err(err) => Err(DeleteDatasetVolumeError::execution( + params.name.to_string(), + err, + )), } } } diff --git a/nexus/db-model/src/disk_type_local_storage.rs b/nexus/db-model/src/disk_type_local_storage.rs index c5917084f05..1cedec33b84 100644 --- a/nexus/db-model/src/disk_type_local_storage.rs +++ b/nexus/db-model/src/disk_type_local_storage.rs @@ -46,6 +46,8 @@ impl DiskTypeLocalStorage { let overhead: u64 = external::ByteCount::from_mebibytes_u32(70).to_bytes() * gbs; + // XXX revisit, tracked by oxidecomputer/omicron#9591 + // Don't unwrap this - the size of this disk is a parameter set by an // API call, and we don't want to panic on out of range input. let required_dataset_overhead = diff --git a/nexus/src/app/sagas/instance_start.rs b/nexus/src/app/sagas/instance_start.rs index a7df97649c1..a5e22af0c06 100644 --- a/nexus/src/app/sagas/instance_start.rs +++ b/nexus/src/app/sagas/instance_start.rs @@ -173,8 +173,14 @@ impl NexusSaga for SagaInstanceStart { // Changing MAX_DISKS_PER_INSTANCE requires changing this saga static_assertions::const_assert!(MAX_DISKS_PER_INSTANCE == 12); + + // In parallel, ensure all local storage related to this instance. seq!(N in 0..12 { - builder.append(paste!([]())); + builder.append_parallel(vec![ + #( + paste!([]()), + )* + ]); }); builder.append(dpd_ensure_action()); diff --git a/sled-agent/config-reconciler/src/dataset_serialization_task.rs b/sled-agent/config-reconciler/src/dataset_serialization_task.rs index 9abad836c09..cf6b55fa559 100644 --- a/sled-agent/config-reconciler/src/dataset_serialization_task.rs +++ b/sled-agent/config-reconciler/src/dataset_serialization_task.rs @@ -23,6 +23,7 @@ use illumos_utils::zfs::CanMount; use illumos_utils::zfs::DatasetEnsureArgs; use illumos_utils::zfs::DatasetProperties; use illumos_utils::zfs::DestroyDatasetError; +use illumos_utils::zfs::EncryptionDetails; use illumos_utils::zfs::Mountpoint; use illumos_utils::zfs::WhichDatasets; use illumos_utils::zfs::Zfs; @@ -951,7 +952,7 @@ impl DatasetTask { // `crypt` dataset. Ensuring that dataset would require non-`None` // encryption details, but that's currently handled by `Disk::new()` // when we start managing external disks. - let encryption_details = None; + let encryption_details = EncryptionDetails::Inherit; let size_details = Some(illumos_utils::zfs::SizeDetails { quota: size_details.quota, diff --git a/sled-agent/src/backing_fs.rs b/sled-agent/src/backing_fs.rs index ac6e625512a..85482ec39a1 100644 --- a/sled-agent/src/backing_fs.rs +++ b/sled-agent/src/backing_fs.rs @@ -23,8 +23,8 @@ use camino::Utf8PathBuf; use illumos_utils::zfs::{ - CanMount, DatasetEnsureArgs, EnsureDatasetError, GetValueError, Mountpoint, - SizeDetails, Zfs, + CanMount, DatasetEnsureArgs, EncryptionDetails, EnsureDatasetError, + GetValueError, Mountpoint, SizeDetails, Zfs, }; use omicron_common::api::external::ByteCount; use omicron_common::disk::CompressionAlgorithm; @@ -149,7 +149,7 @@ pub(crate) async fn ensure_backing_fs( mountpoint: mountpoint.clone(), can_mount: CanMount::NoAuto, zoned: false, - encryption_details: None, + encryption_details: EncryptionDetails::Inherit, size_details, id: None, additional_options: None, diff --git a/sled-agent/src/bootstrap/pre_server.rs b/sled-agent/src/bootstrap/pre_server.rs index bd689156f88..3359e698371 100644 --- a/sled-agent/src/bootstrap/pre_server.rs +++ b/sled-agent/src/bootstrap/pre_server.rs @@ -29,6 +29,7 @@ use illumos_utils::addrobj::AddrObject; use illumos_utils::dladm; use illumos_utils::dladm::Dladm; use illumos_utils::zfs; +use illumos_utils::zfs::EncryptionDetails; use illumos_utils::zfs::Zfs; use illumos_utils::zone; use illumos_utils::zone::Api; @@ -281,7 +282,7 @@ async fn ensure_zfs_ramdisk_dataset() -> Result<(), StartError> { )), can_mount: zfs::CanMount::On, zoned: false, - encryption_details: None, + encryption_details: EncryptionDetails::Inherit, size_details: None, id: None, additional_options: None, diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 45de65083c2..89e743c6356 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -34,6 +34,9 @@ use illumos_utils::opte::PortManager; use illumos_utils::running_zone::RunningZone; use illumos_utils::zfs::CanMount; use illumos_utils::zfs::DatasetEnsureArgs; +use illumos_utils::zfs::DatasetVolumeDeleteArgs; +use illumos_utils::zfs::DatasetVolumeEnsureArgs; +use illumos_utils::zfs::EncryptionDetails; use illumos_utils::zfs::Mountpoint; use illumos_utils::zfs::SizeDetails; use illumos_utils::zfs::Zfs; @@ -1283,10 +1286,11 @@ impl SledAgent { ), can_mount: CanMount::Off, zoned: false, - // encryption details not required, will inherit from parent - // "oxp_UUID/crypt/local_storage", which inherits from - // "oxp_UUID/crypt" - encryption_details: None, + // Ordinarily, we would want to inherit encryption details from the + // parent "oxp_UUID/crypt/local_storage" dataset, which inherits + // from "oxp_UUID/crypt". We're making raw zvols though, so set + // encryption explicitly off. + encryption_details: EncryptionDetails::Off, size_details: Some(SizeDetails { quota: Some(dataset_size), reservation: Some(dataset_size), @@ -1298,13 +1302,20 @@ impl SledAgent { .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - Zfs::ensure_dataset_volume( - delegated_zvol.volume_name(), - volume_size, - delegated_zvol.volblocksize(), - ) + Zfs::ensure_dataset_volume(DatasetVolumeEnsureArgs { + name: &delegated_zvol.volume_name(), + size: volume_size, + raw: true, + volblocksize: None, + }) .await - .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + .map_err(|e| { + if e.is_not_ready() { + HttpError::for_unavail(None, e.to_string()) + } else { + HttpError::for_internal_error(e.to_string()) + } + })?; Ok(()) } @@ -1339,6 +1350,19 @@ impl SledAgent { let delegated_zvol = DelegatedZvol::LocalStorage { zpool_id, dataset_id }; + Zfs::delete_dataset_volume(DatasetVolumeDeleteArgs { + name: &delegated_zvol.volume_name(), + raw: true, + }) + .await + .map_err(|e| { + if e.is_not_ready() { + HttpError::for_unavail(None, e.to_string()) + } else { + HttpError::for_internal_error(e.to_string()) + } + })?; + Zfs::destroy_dataset(&delegated_zvol.parent_dataset_name()) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; diff --git a/sled-storage/src/dataset.rs b/sled-storage/src/dataset.rs index 1fcb7bb76a4..89cb1a0b628 100644 --- a/sled-storage/src/dataset.rs +++ b/sled-storage/src/dataset.rs @@ -247,7 +247,8 @@ pub(crate) async fn ensure_zpool_has_datasets( error, })?; - let encryption_details = EncryptionDetails { keypath, epoch }; + let encryption_details = + EncryptionDetails::Aes256Gcm { keypath, epoch }; info!( log, @@ -259,7 +260,7 @@ pub(crate) async fn ensure_zpool_has_datasets( mountpoint: Mountpoint(mountpoint), can_mount: zfs::CanMount::On, zoned, - encryption_details: Some(encryption_details), + encryption_details, size_details: None, id: None, additional_options: None, @@ -286,7 +287,6 @@ pub(crate) async fn ensure_zpool_has_datasets( zpool_name.dataset_mountpoint(&mount_config.root, dataset.name); let name = &format!("{}/{}", zpool_name, dataset.name); - let encryption_details = None; let size_details = Some(SizeDetails { quota: dataset.quota, reservation: None, @@ -297,7 +297,7 @@ pub(crate) async fn ensure_zpool_has_datasets( mountpoint: Mountpoint(mountpoint), can_mount: zfs::CanMount::On, zoned, - encryption_details, + encryption_details: EncryptionDetails::Inherit, size_details, id: None, additional_options: None, diff --git a/sled-storage/zfs-test-harness/src/lib.rs b/sled-storage/zfs-test-harness/src/lib.rs index 6ff3cceec04..e4f20a089ed 100644 --- a/sled-storage/zfs-test-harness/src/lib.rs +++ b/sled-storage/zfs-test-harness/src/lib.rs @@ -13,6 +13,7 @@ use illumos_utils::ExecutionError; use illumos_utils::PFEXEC; use illumos_utils::zfs::CanMount; use illumos_utils::zfs::DatasetEnsureArgs; +use illumos_utils::zfs::EncryptionDetails; use illumos_utils::zfs::Mountpoint; use illumos_utils::zfs::Zfs; use key_manager::KeyManager; @@ -308,7 +309,7 @@ impl Inner { let DatasetCreationDetails { zoned, mountpoint, full_name } = details; // The "crypt" dataset needs these details, but should already exist // by the time we're creating datasets inside. - let encryption_details = None; + let encryption_details = EncryptionDetails::Inherit; let size_details = Some(illumos_utils::zfs::SizeDetails { quota: config.quota, reservation: config.reservation, diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 70b6ad8ec8d..28245d05c3e 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -26,7 +26,7 @@ base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } base64ct = { version = "1.6.0", default-features = false, features = ["std"] } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["serde"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["serde", "std"] } bstr = { version = "1.10.0" } buf-list = { version = "1.0.3", default-features = false, features = ["tokio1"] } byteorder = { version = "1.5.0" } @@ -77,6 +77,7 @@ indexmap = { version = "2.12.1", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnet = { version = "2.11.0", features = ["serde"] } ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13.0" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2.174", features = ["extra_traits"] } @@ -166,7 +167,7 @@ base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } base64ct = { version = "1.6.0", default-features = false, features = ["std"] } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["serde"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["serde", "std"] } bstr = { version = "1.10.0" } buf-list = { version = "1.0.3", default-features = false, features = ["tokio1"] } byteorder = { version = "1.5.0" } @@ -218,6 +219,7 @@ indexmap = { version = "2.12.1", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnet = { version = "2.11.0", features = ["serde"] } ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] } +itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13.0" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2.174", features = ["extra_traits"] } @@ -304,7 +306,6 @@ zip-164d15cefe24d7eb = { package = "zip", version = "4.2.0", default-features = zip-3b31131e45eafb45 = { package = "zip", version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } [target.x86_64-unknown-linux-gnu.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } dof-468e82937335b1c9 = { package = "dof", version = "0.3.0", default-features = false, features = ["des"] } dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = false, features = ["des"] } @@ -315,11 +316,10 @@ linux-raw-sys = { version = "0.4.14", default-features = false, features = ["elf miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } [target.x86_64-unknown-linux-gnu.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } dof-468e82937335b1c9 = { package = "dof", version = "0.3.0", default-features = false, features = ["des"] } dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = false, features = ["des"] } @@ -330,11 +330,10 @@ linux-raw-sys = { version = "0.4.14", default-features = false, features = ["elf miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } [target.x86_64-apple-darwin.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } @@ -342,10 +341,9 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } [target.x86_64-apple-darwin.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } @@ -353,10 +351,9 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } [target.aarch64-apple-darwin.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } @@ -364,10 +361,9 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } [target.aarch64-apple-darwin.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } @@ -375,28 +371,27 @@ hyper-util = { version = "0.1.19", features = ["full"] } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } [target.x86_64-unknown-illumos.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } dof-468e82937335b1c9 = { package = "dof", version = "0.3.0", default-features = false, features = ["des"] } dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = false, features = ["des"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } hyper-util = { version = "0.1.19", features = ["full"] } -itertools = { version = "0.10.5" } +itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } +itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } nom = { version = "7.1.3" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } toml_edit-cdcf2f9584511fe6 = { package = "toml_edit", version = "0.19.15", features = ["serde"] } winnow-3b31131e45eafb45 = { package = "winnow", version = "0.6.26", features = ["simd"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } [target.x86_64-unknown-illumos.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["std"] } clang-sys = { version = "1.8.1", default-features = false, features = ["clang_11_0", "runtime"] } cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } dof-468e82937335b1c9 = { package = "dof", version = "0.3.0", default-features = false, features = ["des"] } @@ -404,12 +399,13 @@ dof-9fbad63c4bcf4a8f = { package = "dof", version = "0.4.0", default-features = getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["std"] } hyper-rustls = { version = "0.27.7", features = ["http2", "ring", "webpki-tokio"] } hyper-util = { version = "0.1.19", features = ["full"] } -itertools = { version = "0.10.5" } +itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } +itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } miniz_oxide = { version = "0.8.5", default-features = false, features = ["with-alloc"] } mio = { version = "1.0.2", features = ["net", "os-ext"] } nom = { version = "7.1.3" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.0.7", features = ["fs", "stdio", "termios"] } +rustix-dff4ba8e3ae991db = { package = "rustix", version = "1.1.2", features = ["fs", "stdio", "termios"] } toml_edit-cdcf2f9584511fe6 = { package = "toml_edit", version = "0.19.15", features = ["serde"] } winnow-3b31131e45eafb45 = { package = "winnow", version = "0.6.26", features = ["simd"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } From 8c4376b0cb68b1d8a2a065333a9c5e02df6bc2b9 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 6 Jan 2026 21:21:16 +0000 Subject: [PATCH 2/4] use DKIOCRAWVOLSTOP to stop initialization --- illumos-utils/src/zfs.rs | 59 +++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index c78827d551f..b419e7ebcde 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -16,6 +16,7 @@ use omicron_common::disk::CompressionAlgorithm; use omicron_common::disk::DiskIdentity; use omicron_common::disk::SharedDatasetConfig; use omicron_uuid_kinds::DatasetUuid; +use rustix::fd::AsRawFd; use std::collections::BTreeMap; use std::fmt; @@ -333,6 +334,9 @@ pub enum DeleteDatasetVolumeErrorInner { #[error("created but not ready yet: {reason}")] NotReady { reason: String }, + + #[error("cannot cancel raw volume initialization, ioctl returned {err}")] + CancellingInitialization { err: i32 }, } /// Error returned by [`Zfs::delete_dataset_volume`]. @@ -378,6 +382,15 @@ impl DeleteDatasetVolumeError { } } } + + pub fn cancelling_initialization(name: String, err: i32) -> Self { + DeleteDatasetVolumeError { + name, + err: DeleteDatasetVolumeErrorInner::CancellingInitialization { + err, + }, + } + } } /// Wraps commands for interacting with ZFS. @@ -1571,7 +1584,7 @@ impl Zfs { match execute_async(cmd).await { Ok(_) => { - // no-op + // Nothing to do } Err(crate::ExecutionError::CommandFailure(info)) @@ -1675,15 +1688,13 @@ impl Zfs { params: DatasetVolumeDeleteArgs<'_>, ) -> Result<(), DeleteDatasetVolumeError> { if params.raw { - // Open the raw zvol and read from it. If EINPROGRESS is seen from - // the read, then it's not done initializing yet. We can't delete it - // until it's fully done, so return the `NotReady` variant to signal - // upstack that it should poll. + // Open the raw zvol and check if it is still initializing. We can't + // delete it if it is, so stop the initialization before deleting. let path = format!("/dev/zvol/rdsk/{}", params.name); let fd = rustix::fs::open( path, - rustix::fs::OFlags::RDONLY, + rustix::fs::OFlags::WRONLY, rustix::fs::Mode::empty(), ) .map_err(|e| { @@ -1693,13 +1704,35 @@ impl Zfs { ) })?; - let mut buf = [0u8; 64]; - rustix::io::read(fd, &mut buf).map_err(|e| { - DeleteDatasetVolumeError::from_raw_zvol_read( - params.name.to_string(), - e, - ) - })?; + // XXX is there a way to pull this constant in from + // /usr/include/sys/dkio.h + + const DKIOC: i32 = 0x04 << 8; + const DKIOCRAWVOLSTOP: i32 = DKIOC | 31; + + match unsafe { libc::ioctl(fd.as_raw_fd(), DKIOCRAWVOLSTOP) } { + 0 => { + // DKIOCRAWVOLSTOP will stop the initialization and block + // waiting for the initialization thread to exit + // + // The initialization thread has stopped and exited ok, + // proceed with deletion. + } + + libc::ENOENT => { + // Raw volume was not initializing, proceed with deletion. + } + + e => { + // Unexpected error, return up the stack + return Err( + DeleteDatasetVolumeError::cancelling_initialization( + params.name.to_string(), + e, + ), + ); + } + } } let mut command = Command::new(PFEXEC); From 529ab049d9c81d5595c89e2053d3c7da3916fbbb Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 6 Jan 2026 22:04:00 +0000 Subject: [PATCH 3/4] correctly use libc::ioctl! --- illumos-utils/src/zfs.rs | 51 +++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index b419e7ebcde..a5efbbf7b70 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -1704,33 +1704,36 @@ impl Zfs { ) })?; + // The numeric type of these constants is not specified due to being + // different for illumos and linux. They are also `let dkioc` and + // not `const DKIOC` because `const` requires a type. + // // XXX is there a way to pull this constant in from // /usr/include/sys/dkio.h + let dkioc = 0x04 << 8; + let dkiocrawvolstop = dkioc | 31; + + // DKIOCRAWVOLSTOP will stop the initialization and block waiting + // for the initialization thread to exit. If this returns 0, the + // initialization thread has stopped and exited ok, proceed with + // deletion. + if unsafe { libc::ioctl(fd.as_raw_fd(), dkiocrawvolstop) } == -1 { + let err = std::io::Error::last_os_error(); + match err.raw_os_error().unwrap() { + libc::ENOENT => { + // The raw volume was not initializing, proceed with + // deletion. + } - const DKIOC: i32 = 0x04 << 8; - const DKIOCRAWVOLSTOP: i32 = DKIOC | 31; - - match unsafe { libc::ioctl(fd.as_raw_fd(), DKIOCRAWVOLSTOP) } { - 0 => { - // DKIOCRAWVOLSTOP will stop the initialization and block - // waiting for the initialization thread to exit - // - // The initialization thread has stopped and exited ok, - // proceed with deletion. - } - - libc::ENOENT => { - // Raw volume was not initializing, proceed with deletion. - } - - e => { - // Unexpected error, return up the stack - return Err( - DeleteDatasetVolumeError::cancelling_initialization( - params.name.to_string(), - e, - ), - ); + e => { + // Unexpected error, return up the stack + return Err( + DeleteDatasetVolumeError::cancelling_initialization( + params.name.to_string(), + e, + ), + ); + } } } } From ac3d056c8fe25af830ec92660212390dd45bf908 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Wed, 7 Jan 2026 17:54:24 +0000 Subject: [PATCH 4/4] back to const --- illumos-utils/src/zfs.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index a5efbbf7b70..dd17f4d9bfa 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -1704,20 +1704,16 @@ impl Zfs { ) })?; - // The numeric type of these constants is not specified due to being - // different for illumos and linux. They are also `let dkioc` and - // not `const DKIOC` because `const` requires a type. - // - // XXX is there a way to pull this constant in from - // /usr/include/sys/dkio.h - let dkioc = 0x04 << 8; - let dkiocrawvolstop = dkioc | 31; + #[cfg(target_os = "illumos")] + const DKIOCRAWVOLSTOP: libc::c_int = (0x04 << 8) | 0x1f; + #[cfg(not(target_os = "illumos"))] + const DKIOCRAWVOLSTOP: libc::c_ulong = (0x04 << 8) | 0x1f; // DKIOCRAWVOLSTOP will stop the initialization and block waiting // for the initialization thread to exit. If this returns 0, the // initialization thread has stopped and exited ok, proceed with // deletion. - if unsafe { libc::ioctl(fd.as_raw_fd(), dkiocrawvolstop) } == -1 { + if unsafe { libc::ioctl(fd.as_raw_fd(), DKIOCRAWVOLSTOP) } == -1 { let err = std::io::Error::last_os_error(); match err.raw_os_error().unwrap() { libc::ENOENT => {