From 47d1db6f77f58dd006da1b371618368bb30a7e00 Mon Sep 17 00:00:00 2001 From: Lyric Lake Date: Wed, 6 May 2026 16:07:31 -0700 Subject: [PATCH 1/3] CP-11462 ZOA infrastructure for snapshot reference and hold management Adds Rust bindings and safe wrappers for two new libzfs_core symbols introduced in delphix/zfs CP-11462: - lzc_list_snapshot_refs: enumerates snapshots in a pool that carry the DS_FIELD_SNAPSHOT_REFERENCE ZAP entry, returning each snapshot's guid and sender-assigned ref_id. Used by ZOA's child reconciler to recover missing recv-refs and write missing delete-refs. - lzc_list_pool_holds: enumerates user holds across every snapshot in a pool, optionally filtered by tag prefix. Used by ZOA's parent reconciler with prefix "snapref:" to find and release orphaned holds. The safe wrappers (Zfs::list_snapshot_refs, Zfs::list_pool_holds) mirror the existing HoldList iterator pattern. PoolHoldList flattens the kernel's two-level snap -> { tag -> time } map into a (snap, tag, created_at) iterator. Bumps zfs-core and zfs-core-sys to 0.5.1-delphix0. --- Cargo.lock | 6 +- zfs-core-sys/Cargo.toml | 2 +- zfs-core-sys/src/bindings.rs | 13 +++ zfs-core/Cargo.toml | 4 +- zfs-core/src/lib.rs | 209 +++++++++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9e5528..3b44b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "bitflags" @@ -358,7 +358,7 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "zfs-core" -version = "0.5.0" +version = "0.5.1-delphix0" dependencies = [ "cstr-argument", "foreign-types", @@ -373,7 +373,7 @@ dependencies = [ [[package]] name = "zfs-core-sys" -version = "0.5.0" +version = "0.5.1-delphix0" dependencies = [ "build-env", "libc", diff --git a/zfs-core-sys/Cargo.toml b/zfs-core-sys/Cargo.toml index 61064fb..635b17d 100644 --- a/zfs-core-sys/Cargo.toml +++ b/zfs-core-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zfs-core-sys" -version = "0.5.0" +version = "0.5.1-delphix0" authors = ["Cody P Schafer "] include = ["**/*.rs", "Cargo.toml"] documentation = "https://docs.rs/zfs-core-sys" diff --git a/zfs-core-sys/src/bindings.rs b/zfs-core-sys/src/bindings.rs index b427dc6..4bcd5c5 100644 --- a/zfs-core-sys/src/bindings.rs +++ b/zfs-core-sys/src/bindings.rs @@ -1978,6 +1978,19 @@ extern "C" { arg2: *mut *mut nvlist_t, ) -> ::std::os::raw::c_int; } +extern "C" { + pub fn lzc_list_snapshot_refs( + arg1: *const ::std::os::raw::c_char, + arg2: *mut *mut nvlist_t, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn lzc_list_pool_holds( + arg1: *const ::std::os::raw::c_char, + arg2: *const ::std::os::raw::c_char, + arg3: *mut *mut nvlist_t, + ) -> ::std::os::raw::c_int; +} pub mod lzc_send_flags { pub type Type = ::std::os::raw::c_uint; pub const LZC_SEND_FLAG_EMBED_DATA: Type = 1; diff --git a/zfs-core/Cargo.toml b/zfs-core/Cargo.toml index a9d9923..66775ad 100644 --- a/zfs-core/Cargo.toml +++ b/zfs-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zfs-core" -version = "0.5.0" +version = "0.5.1-delphix0" authors = ["Cody P Schafer "] include = ["**/*.rs", "Cargo.toml"] documentation = "https://docs.rs/zfs-core" @@ -14,7 +14,7 @@ v2_00 = [] [dependencies] nvpair = { path = "../nvpair", version = "0.5.2-delphix0" } -zfs-core-sys = { path = "../zfs-core-sys", version = "0.5.0" } +zfs-core-sys = { path = "../zfs-core-sys", version = "0.5.1-delphix0" } cstr-argument = "0.1" foreign-types = "0.5.0" rand = "0.8" diff --git a/zfs-core/src/lib.rs b/zfs-core/src/lib.rs index e56222c..be65f2d 100644 --- a/zfs-core/src/lib.rs +++ b/zfs-core/src/lib.rs @@ -549,6 +549,62 @@ impl Zfs { } } + /// Enumerate snapshots in the pool that owns `fsname` which carry the + /// `DS_FIELD_SNAPSHOT_REFERENCE` ZAP entry written by the layered-receive + /// path. + /// + /// `fsname` may be a pool name or any descendant filesystem name; the + /// kernel walks the entire owning pool. The returned [`SnapshotRefList`] + /// iterates `(snap_name, guid, ref_id)` triples. + /// + /// Corresponds to `lzc_list_snapshot_refs()`. + #[doc(alias = "lzc_list_snapshot_refs")] + pub fn list_snapshot_refs( + &self, + fsname: S, + ) -> io::Result { + let fsname = fsname.into_cstr(); + let mut refs = ptr::null_mut(); + let v = unsafe { sys::lzc_list_snapshot_refs(fsname.as_ref().as_ptr(), &mut refs) }; + if v != 0 { + Err(io::Error::from_raw_os_error(v)) + } else { + Ok(SnapshotRefList::new(unsafe { NvList::from_ptr(refs) })) + } + } + + /// Enumerate every user hold on every snapshot in `fsname`'s pool whose + /// tag starts with `prefix`. If `prefix` is `None`, all holds are + /// returned. + /// + /// The returned [`PoolHoldList`] iterates `(snap_name, tag, created_at)` + /// triples, flattening the kernel's two-level `snap -> { tag -> time }` + /// map. + /// + /// Corresponds to `lzc_list_pool_holds()`. + #[doc(alias = "lzc_list_pool_holds")] + pub fn list_pool_holds( + &self, + fsname: S, + prefix: Option

, + ) -> io::Result { + let fsname = fsname.into_cstr(); + let prefix = prefix.map(|p| p.into_cstr()); + let prefix_ptr = prefix + .as_ref() + .map(|p| p.as_ref().as_ptr()) + .unwrap_or(ptr::null()); + let mut holds = ptr::null_mut(); + let v = unsafe { + sys::lzc_list_pool_holds(fsname.as_ref().as_ptr(), prefix_ptr, &mut holds) + }; + if v != 0 { + Err(io::Error::from_raw_os_error(v)) + } else { + Ok(PoolHoldList::new(unsafe { NvList::from_ptr(holds) })) + } + } + /// Send the described stream /// /// Internally, is a wrapper around [`send_resume_redacted()`] @@ -1548,3 +1604,156 @@ impl<'a> Iterator for HoldListIter<'a> { } } } + +/// A list of snapshots in a pool carrying the `DS_FIELD_SNAPSHOT_REFERENCE` +/// ZAP entry, returned by [`Zfs::list_snapshot_refs`]. +/// +/// The underlying nvlist maps each snapshot name to an inner nvlist with +/// `guid` (u64) and `ref_id` (u64) fields. +#[derive(Debug)] +pub struct SnapshotRefList { + nv: NvList, +} + +impl SnapshotRefList { + fn new(nv: NvList) -> Self { + Self { nv } + } +} + +impl From for NvList { + fn from(l: SnapshotRefList) -> Self { + l.nv + } +} + +impl AsRef for SnapshotRefList { + fn as_ref(&self) -> &NvListRef { + &self.nv + } +} + +/// A single entry in [`SnapshotRefList`]. +#[derive(Debug, Clone, Copy)] +pub struct SnapshotRef<'a> { + /// Snapshot name (`pool/dataset@snap`). + pub name: &'a ffi::CStr, + /// `dsl_dataset_phys(ds)->ds_guid` of the snapshot. + pub guid: u64, + /// Sender-assigned reference id stored in `DS_FIELD_SNAPSHOT_REFERENCE`. + pub ref_id: u64, +} + +/// Iterator of snapshot refs in the [`SnapshotRefList`]. +#[derive(Debug)] +pub struct SnapshotRefListIter<'a> { + iter: NvListIter<'a>, +} + +impl<'a> IntoIterator for &'a SnapshotRefList { + type Item = SnapshotRef<'a>; + type IntoIter = SnapshotRefListIter<'a>; + fn into_iter(self) -> Self::IntoIter { + SnapshotRefListIter { + iter: (&self.nv).into_iter(), + } + } +} + +impl<'a> Iterator for SnapshotRefListIter<'a> { + type Item = SnapshotRef<'a>; + + fn next(&mut self) -> Option { + let nvp = self.iter.next()?; + let inner = match nvp.data() { + nvpair::NvData::NvListRef(l) => l, + v => panic!("unexpected datatype in snapshot ref list {:?}", v), + }; + let guid = inner + .lookup_uint64("guid") + .expect("snapshot ref entry missing 'guid'"); + let ref_id = inner + .lookup_uint64("ref_id") + .expect("snapshot ref entry missing 'ref_id'"); + Some(SnapshotRef { + name: nvp.name(), + guid, + ref_id, + }) + } +} + +/// A flattened view of all user holds on every snapshot in a pool, returned +/// by [`Zfs::list_pool_holds`]. +/// +/// The underlying nvlist is a two-level map `snap -> { tag -> time }`; +/// iteration flattens it into one `(snap, tag, created_at)` per hold. +#[derive(Debug)] +pub struct PoolHoldList { + nv: NvList, +} + +impl PoolHoldList { + fn new(nv: NvList) -> Self { + Self { nv } + } +} + +impl From for NvList { + fn from(l: PoolHoldList) -> Self { + l.nv + } +} + +impl AsRef for PoolHoldList { + fn as_ref(&self) -> &NvListRef { + &self.nv + } +} + +/// Iterator over the flattened `(snap, tag, created_at)` entries of a +/// [`PoolHoldList`]. +#[derive(Debug)] +pub struct PoolHoldListIter<'a> { + outer: NvListIter<'a>, + current: Option<(&'a ffi::CStr, NvListIter<'a>)>, +} + +impl<'a> IntoIterator for &'a PoolHoldList { + type Item = (&'a ffi::CStr, &'a ffi::CStr, std::time::SystemTime); + type IntoIter = PoolHoldListIter<'a>; + fn into_iter(self) -> Self::IntoIter { + PoolHoldListIter { + outer: (&self.nv).into_iter(), + current: None, + } + } +} + +impl<'a> Iterator for PoolHoldListIter<'a> { + type Item = (&'a ffi::CStr, &'a ffi::CStr, std::time::SystemTime); + + fn next(&mut self) -> Option { + loop { + if let Some((snap, ref mut inner)) = self.current { + if let Some(nvp) = inner.next() { + let t = match nvp.data() { + nvpair::NvData::Uint64(time_sec) => { + std::time::UNIX_EPOCH + + std::time::Duration::from_secs(time_sec) + } + v => panic!("unexpected datatype in pool hold list {:?}", v), + }; + return Some((snap, nvp.name(), t)); + } + self.current = None; + } + let outer = self.outer.next()?; + let inner_ref = match outer.data() { + nvpair::NvData::NvListRef(l) => l, + v => panic!("unexpected datatype in pool hold list {:?}", v), + }; + self.current = Some((outer.name(), inner_ref.into_iter())); + } + } +} From 1b82488ad7509b68c320fc0b3d22ed500396a292 Mon Sep 17 00:00:00 2001 From: Lyric Lake Date: Wed, 6 May 2026 18:13:26 -0700 Subject: [PATCH 2/3] Remove lzc_list_pool_holds binding and PoolHoldList types delphix/rust-libzfs does not have lzc_list_pool_holds in its ZFS kernel interface, so remove the sys binding and the Rust wrapper. Co-Authored-By: Claude Sonnet 4.6 --- zfs-core-sys/src/bindings.rs | 7 --- zfs-core/src/lib.rs | 107 ----------------------------------- 2 files changed, 114 deletions(-) diff --git a/zfs-core-sys/src/bindings.rs b/zfs-core-sys/src/bindings.rs index 4bcd5c5..d2bbe88 100644 --- a/zfs-core-sys/src/bindings.rs +++ b/zfs-core-sys/src/bindings.rs @@ -1984,13 +1984,6 @@ extern "C" { arg2: *mut *mut nvlist_t, ) -> ::std::os::raw::c_int; } -extern "C" { - pub fn lzc_list_pool_holds( - arg1: *const ::std::os::raw::c_char, - arg2: *const ::std::os::raw::c_char, - arg3: *mut *mut nvlist_t, - ) -> ::std::os::raw::c_int; -} pub mod lzc_send_flags { pub type Type = ::std::os::raw::c_uint; pub const LZC_SEND_FLAG_EMBED_DATA: Type = 1; diff --git a/zfs-core/src/lib.rs b/zfs-core/src/lib.rs index be65f2d..102b8e1 100644 --- a/zfs-core/src/lib.rs +++ b/zfs-core/src/lib.rs @@ -573,38 +573,6 @@ impl Zfs { } } - /// Enumerate every user hold on every snapshot in `fsname`'s pool whose - /// tag starts with `prefix`. If `prefix` is `None`, all holds are - /// returned. - /// - /// The returned [`PoolHoldList`] iterates `(snap_name, tag, created_at)` - /// triples, flattening the kernel's two-level `snap -> { tag -> time }` - /// map. - /// - /// Corresponds to `lzc_list_pool_holds()`. - #[doc(alias = "lzc_list_pool_holds")] - pub fn list_pool_holds( - &self, - fsname: S, - prefix: Option

, - ) -> io::Result { - let fsname = fsname.into_cstr(); - let prefix = prefix.map(|p| p.into_cstr()); - let prefix_ptr = prefix - .as_ref() - .map(|p| p.as_ref().as_ptr()) - .unwrap_or(ptr::null()); - let mut holds = ptr::null_mut(); - let v = unsafe { - sys::lzc_list_pool_holds(fsname.as_ref().as_ptr(), prefix_ptr, &mut holds) - }; - if v != 0 { - Err(io::Error::from_raw_os_error(v)) - } else { - Ok(PoolHoldList::new(unsafe { NvList::from_ptr(holds) })) - } - } - /// Send the described stream /// /// Internally, is a wrapper around [`send_resume_redacted()`] @@ -1682,78 +1650,3 @@ impl<'a> Iterator for SnapshotRefListIter<'a> { }) } } - -/// A flattened view of all user holds on every snapshot in a pool, returned -/// by [`Zfs::list_pool_holds`]. -/// -/// The underlying nvlist is a two-level map `snap -> { tag -> time }`; -/// iteration flattens it into one `(snap, tag, created_at)` per hold. -#[derive(Debug)] -pub struct PoolHoldList { - nv: NvList, -} - -impl PoolHoldList { - fn new(nv: NvList) -> Self { - Self { nv } - } -} - -impl From for NvList { - fn from(l: PoolHoldList) -> Self { - l.nv - } -} - -impl AsRef for PoolHoldList { - fn as_ref(&self) -> &NvListRef { - &self.nv - } -} - -/// Iterator over the flattened `(snap, tag, created_at)` entries of a -/// [`PoolHoldList`]. -#[derive(Debug)] -pub struct PoolHoldListIter<'a> { - outer: NvListIter<'a>, - current: Option<(&'a ffi::CStr, NvListIter<'a>)>, -} - -impl<'a> IntoIterator for &'a PoolHoldList { - type Item = (&'a ffi::CStr, &'a ffi::CStr, std::time::SystemTime); - type IntoIter = PoolHoldListIter<'a>; - fn into_iter(self) -> Self::IntoIter { - PoolHoldListIter { - outer: (&self.nv).into_iter(), - current: None, - } - } -} - -impl<'a> Iterator for PoolHoldListIter<'a> { - type Item = (&'a ffi::CStr, &'a ffi::CStr, std::time::SystemTime); - - fn next(&mut self) -> Option { - loop { - if let Some((snap, ref mut inner)) = self.current { - if let Some(nvp) = inner.next() { - let t = match nvp.data() { - nvpair::NvData::Uint64(time_sec) => { - std::time::UNIX_EPOCH - + std::time::Duration::from_secs(time_sec) - } - v => panic!("unexpected datatype in pool hold list {:?}", v), - }; - return Some((snap, nvp.name(), t)); - } - self.current = None; - } - let outer = self.outer.next()?; - let inner_ref = match outer.data() { - nvpair::NvData::NvListRef(l) => l, - v => panic!("unexpected datatype in pool hold list {:?}", v), - }; - self.current = Some((outer.name(), inner_ref.into_iter())); - } - } -} From 5e7caa4bf43c363c53c78b141393635aaea3e58e Mon Sep 17 00:00:00 2001 From: Lyric Lake Date: Thu, 21 May 2026 15:46:02 -0700 Subject: [PATCH 3/3] CP-11462 rename lzc_list_snapshot_refs -> lzc_get_snapshot_refs Co-Authored-By: Claude Sonnet 4.6 --- zfs-core-sys/src/bindings.rs | 2 +- zfs-core/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zfs-core-sys/src/bindings.rs b/zfs-core-sys/src/bindings.rs index d2bbe88..070d635 100644 --- a/zfs-core-sys/src/bindings.rs +++ b/zfs-core-sys/src/bindings.rs @@ -1979,7 +1979,7 @@ extern "C" { ) -> ::std::os::raw::c_int; } extern "C" { - pub fn lzc_list_snapshot_refs( + pub fn lzc_get_snapshot_refs( arg1: *const ::std::os::raw::c_char, arg2: *mut *mut nvlist_t, ) -> ::std::os::raw::c_int; diff --git a/zfs-core/src/lib.rs b/zfs-core/src/lib.rs index 102b8e1..f154600 100644 --- a/zfs-core/src/lib.rs +++ b/zfs-core/src/lib.rs @@ -557,15 +557,15 @@ impl Zfs { /// kernel walks the entire owning pool. The returned [`SnapshotRefList`] /// iterates `(snap_name, guid, ref_id)` triples. /// - /// Corresponds to `lzc_list_snapshot_refs()`. - #[doc(alias = "lzc_list_snapshot_refs")] + /// Corresponds to `lzc_get_snapshot_refs()`. + #[doc(alias = "lzc_get_snapshot_refs")] pub fn list_snapshot_refs( &self, fsname: S, ) -> io::Result { let fsname = fsname.into_cstr(); let mut refs = ptr::null_mut(); - let v = unsafe { sys::lzc_list_snapshot_refs(fsname.as_ref().as_ptr(), &mut refs) }; + let v = unsafe { sys::lzc_get_snapshot_refs(fsname.as_ref().as_ptr(), &mut refs) }; if v != 0 { Err(io::Error::from_raw_os_error(v)) } else {