From 05d0f66f4db2c6e07c82a45df745b320758e4524 Mon Sep 17 00:00:00 2001 From: Igor Malovitsa Date: Thu, 17 Sep 2026 03:31:13 +0000 Subject: [PATCH 1/3] Fix ZipperHead exclusive path at the head's own root With an empty path and the head's root inside a node, the walk popped a byte of the head's own path and never put it back. The next request then read past its key buffer (UB in release) or panicked. --- src/zipper_head.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/zipper_head.rs b/src/zipper_head.rs index cab1cfa9..4353d3f3 100644 --- a/src/zipper_head.rs +++ b/src/zipper_head.rs @@ -368,7 +368,11 @@ pub(crate) fn prepare_exclusive_write_path<'a, 'trie: 'a, 'path: 'a, V: Clone + let cell_node = end_node.make_mut().into_cell_node().unwrap(); let (exclusive_node, val) = cell_node.prepare_cf(last_path_byte); + //With an empty `path`, the popped byte was the zipper's own; put it back z.key.prefix_buf.truncate(original_path_len); + if z.key.prefix_buf.len() < original_path_len { + z.key.prefix_buf.push(last_path_byte); + } return (exclusive_node, val) }, @@ -409,6 +413,9 @@ pub(crate) fn prepare_exclusive_write_path<'a, 'trie: 'a, 'path: 'a, V: Clone + } else { //CASE 4 z.key.prefix_buf.truncate(original_path_len); + if z.key.prefix_buf.len() < original_path_len { + z.key.prefix_buf.push(last_path_byte); + } //If the node on top of the stack is not a cell node, we need to upgrade it if !z.focus_stack.top().unwrap().is_cell_node() { @@ -1515,4 +1522,42 @@ mod tests { paths.sort(); assert_eq!(paths, vec![b"ax".to_vec(), b"bx".to_vec(), b"c".to_vec(), b"dx".to_vec()]); } + + /// An exclusive zipper at the head's own root, requested more than once, from a head whose + /// root sits partway into a node + #[test] + fn exclusive_path_at_head_root_twice() { + let sample = || { + let mut m = PathMap::::new(); + for p in [&[1u8, 2, 1][..], &[1, 2, 1, 0], &[1, 2, 1, 3, 3], &[0], &[2, 2]] { m.set_val_at(p, 7); } + m + }; + + let zh = sample().into_zipper_head(&[1u8]); + for (path, v) in [(&[][..], 1), (&[9u8][..], 2), (&[][..], 3)] { + let mut wz = zh.write_zipper_at_exclusive_path(path).unwrap(); + wz.descend_to(&[5u8]); + wz.set_val(v); + } + let map = zh.into_map(); + assert_eq!(map.get_val_at(&[1u8, 5]), Some(&3)); + assert_eq!(map.get_val_at(&[1u8, 9, 5]), Some(&2)); + assert_eq!(map.get_val_at(&[1u8, 2, 1, 0]), Some(&7)); + assert_eq!(map.val_count(), 7); + + let mut map = sample(); + { + let mut wz = map.write_zipper(); + wz.descend_to(&[1u8]); + let zh = wz.zipper_head(); + for (path, v) in [(&[][..], 1), (&[][..], 2), (&[9u8][..], 3)] { + let mut child = zh.write_zipper_at_exclusive_path(path).unwrap(); + child.descend_to(&[5u8]); + child.set_val(v); + } + } + assert_eq!(map.get_val_at(&[1u8, 5]), Some(&2)); + assert_eq!(map.get_val_at(&[1u8, 9, 5]), Some(&3)); + assert_eq!(map.val_count(), 7); + } } From 3c236412dc067cf5c7dd434c2a1336c557d620d2 Mon Sep 17 00:00:00 2001 From: Igor Malovitsa Date: Thu, 17 Sep 2026 03:32:38 +0000 Subject: [PATCH 2/3] Fix zipper_head on a write zipper with a borrowed path as_static_path_zipper asserted the zipper held no borrowed origin path, but write_zipper_at_path always does. Once buffers are prepared the path lives in prefix_buf, so drop the borrowed slice instead. --- src/write_zipper.rs | 4 ++++ src/zipper_head.rs | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/write_zipper.rs b/src/write_zipper.rs index b38eed05..13e6845c 100644 --- a/src/write_zipper.rs +++ b/src/write_zipper.rs @@ -1343,6 +1343,10 @@ impl <'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> WriteZipperC /// Internal method to re-borrow a WriteZipperCore without the `'path` lifetime fn as_static_path_zipper(&mut self) -> &mut WriteZipperCore<'a, 'static, V, A> { self.prepare_buffers(); + //The path is in `prefix_buf` now, so drop the borrowed copy + if self.key.origin_path.len() > 0 { + self.key.origin_path = SliceOrLen::new_owned(self.key.origin_path.len()); + } debug_assert!(!self.key.origin_path.is_slice() || self.key.origin_path.len() == 0); unsafe{ &mut *(self as *mut WriteZipperCore).cast() } } diff --git a/src/zipper_head.rs b/src/zipper_head.rs index 4353d3f3..aabdad38 100644 --- a/src/zipper_head.rs +++ b/src/zipper_head.rs @@ -1560,4 +1560,27 @@ mod tests { assert_eq!(map.get_val_at(&[1u8, 9, 5]), Some(&3)); assert_eq!(map.val_count(), 7); } + + /// A `ZipperHead` from a write zipper made with a borrowed path, and the zipper used afterwards + #[test] + fn zipper_head_from_write_zipper_at_borrowed_path() { + let mut map = PathMap::::new(); + map.set_val_at(&[1u8, 2, 3], 7); + { + let path = [1u8, 2]; + let mut wz = map.write_zipper_at_path(&path); + { + let zh = wz.zipper_head(); + let mut child = zh.write_zipper_at_exclusive_path(&[4u8]).unwrap(); + child.set_val(1); + } + assert_eq!(wz.origin_path(), &[1u8, 2]); + wz.descend_to(&[5u8]); + wz.set_val(2); + assert_eq!(wz.origin_path(), &[1u8, 2, 5]); + } + assert_eq!(map.get_val_at(&[1u8, 2, 4]), Some(&1)); + assert_eq!(map.get_val_at(&[1u8, 2, 5]), Some(&2)); + assert_eq!(map.get_val_at(&[1u8, 2, 3]), Some(&7)); + } } From 67ced7322d933566f31c46968b7c49d5cc7f0812 Mon Sep 17 00:00:00 2001 From: Igor Malovitsa Date: Thu, 17 Sep 2026 03:58:57 +0000 Subject: [PATCH 3/3] Fix ZipperHead exclusive paths over an empty node make_cell_node called make_mut on the empty sentinel, which panics. Replace the sentinel with a new CellByteNode instead. --- src/trie_node.rs | 8 ++++++-- src/zipper_head.rs | 44 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/trie_node.rs b/src/trie_node.rs index 27366659..da01b49d 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2662,8 +2662,12 @@ pub(crate) fn node_along_path_mut<'a, 'k, V: Clone + Send + Sync, A: Allocator>( /// Ensures the node is a CellByteNode /// /// Returns `true` if the node was upgraded and `false` if it already was a CellByteNode -pub(crate) fn make_cell_node(node: &mut TrieNodeODRc) -> bool { - if !node.as_tagged().is_cell_node() { +pub(crate) fn make_cell_node(node: &mut TrieNodeODRc, alloc: A) -> bool { + if node.is_empty() { + //The empty sentinel can't be made mutable; there is nothing in it to keep + *node = TrieNodeODRc::new_in(crate::dense_byte_node::CellByteNode::new_in(alloc.clone()), alloc); + true + } else if !node.as_tagged().is_cell_node() { let replacement = node.make_mut().convert_to_cell_node(); *node = replacement; true diff --git a/src/zipper_head.rs b/src/zipper_head.rs index aabdad38..8cdaa6b8 100644 --- a/src/zipper_head.rs +++ b/src/zipper_head.rs @@ -343,7 +343,7 @@ pub(crate) fn prepare_exclusive_write_path<'a, 'trie: 'a, 'path: 'a, V: Clone + debug_assert_eq!(z.focus_stack.depth(), 1); z.focus_stack.to_root(); let stack_root = z.focus_stack.root_mut().unwrap(); - make_cell_node(stack_root); + make_cell_node(stack_root, z.alloc.clone()); let root_val = z.root_val.as_mut().unwrap(); return (stack_root, unsafe{ &mut **root_val }) } @@ -386,7 +386,7 @@ pub(crate) fn prepare_exclusive_write_path<'a, 'trie: 'a, 'path: 'a, V: Clone + |node, key| { let new_node = if key.len() > 0 { if let Some(mut remaining) = node.take_node_at_key(key, false) { - make_cell_node(&mut remaining); + make_cell_node(&mut remaining, alloc.clone()); remaining } else { TrieNodeODRc::new_in(CellByteNode::new_in(alloc.clone()), alloc) @@ -419,8 +419,9 @@ pub(crate) fn prepare_exclusive_write_path<'a, 'trie: 'a, 'path: 'a, V: Clone + //If the node on top of the stack is not a cell node, we need to upgrade it if !z.focus_stack.top().unwrap().is_cell_node() { + let alloc = z.alloc.clone(); swap_top_node(&mut z.focus_stack, &z.key, |mut existing_node| { - make_cell_node(&mut existing_node); + make_cell_node(&mut existing_node, alloc); existing_node }); } @@ -439,9 +440,9 @@ fn prepare_node_at_path_end<'a, V: Clone + Send + Sync, A: Allocator>(start_node let mut node_ref = node.make_mut(); let mut new_parent = match node_ref.take_node_at_key(remaining_key, false) { Some(downward_node) => downward_node, - None => TrieNodeODRc::new_in(CellByteNode::new_in(alloc.clone()), alloc) + None => TrieNodeODRc::new_in(CellByteNode::new_in(alloc.clone()), alloc.clone()) }; - make_cell_node(&mut new_parent); + make_cell_node(&mut new_parent, alloc.clone()); let result = node_ref.node_set_branch(remaining_key, new_parent); match result { Ok(_) => { }, @@ -452,7 +453,7 @@ fn prepare_node_at_path_end<'a, V: Clone + Send + Sync, A: Allocator>(start_node node = child_node; } else { //Otherwise just upgrade node - make_cell_node(node); + make_cell_node(node, alloc); } node } @@ -1583,4 +1584,35 @@ mod tests { assert_eq!(map.get_val_at(&[1u8, 2, 5]), Some(&2)); assert_eq!(map.get_val_at(&[1u8, 2, 3]), Some(&7)); } + + /// Exclusive paths from a head whose focus node is the empty sentinel + #[test] + fn exclusive_path_over_empty_node() { + let setups: [fn(&mut PathMap); 3] = [ + |m| { m.write_zipper_at_path(&[0u8, 0]).remove_branches(false); }, + |m| { let e = PathMap::::new(); m.write_zipper_at_path(&[0u8, 0]).graft(&e.read_zipper()); }, + |m| { m.write_zipper_at_path(&[0u8, 0]).take_map(false); }, + ]; + for (i, setup) in setups.iter().enumerate() { + for paths in [[&[][..], &[5u8][..]], [&[5u8, 6][..], &[][..]], [&[0u8, 0][..], &[1u8][..]]] { + let mut map = PathMap::::new(); + map.set_val_at(&[0u8, 0, 1, 2], 9); + map.set_val_at(&[7u8], 9); + setup(&mut map); + { + let mut wz = map.write_zipper_at_path(&[0u8, 0]); + let zh = wz.zipper_head(); + for (n, p) in paths.iter().enumerate() { + let mut w = zh.write_zipper_at_exclusive_path(p).unwrap(); + w.set_val(n as u64); + } + } + assert_eq!(map.get(&[7u8]), Some(&9), "setup {i} {paths:?}"); + for (n, p) in paths.iter().enumerate() { + let full: Vec = [&[0u8, 0][..], p].concat(); + assert_eq!(map.get(&full), Some(&(n as u64)), "setup {i} {paths:?}"); + } + } + } + } }