Skip to content

Conversation

@garybeihl
Copy link
Contributor

@garybeihl garybeihl commented Jan 15, 2026

Description

Fixed resize() to prevent capacity shrinking and properly copy all nodes during resizing.

The root issue was that resize() allowed shrinking to a buffer smaller than the current capacity. This caused out-of-bounds access when the resize loop tried to copy node pointers. The storage layout can have gaps like [VALID | VALID | INVALID | VALID | INVALID] where deleted nodes remain in the buffer at their original indices.

Changes:

  1. Changed assert from buffer.len() >= self.len() to buffer.len() >= self.capacity() to prevent resizing to a buffer smaller than current capacity
  2. Changed loop from 0..self.len() to 0..self.capacity() to ensure all nodes including gaps are copied
  • Impacts functionality?
  • Impacts security?
  • Breaking change?
  • Includes tests?
  • Includes documentation?

How This Was Tested

  • Added test_resize_prevents_capacity_shrink(): Verifies assert prevents shrinking capacity
  • Added test_resize_copies_all_nodes_including_gaps(): Verifies all nodes copied including gaps
  • All 49 unit tests passing (cargo test --package patina_internal_collections)
  • Cargo clippy and fmt passing

Integration Instructions

  • N/A

@codecov
Copy link

codecov bot commented Jan 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@garybeihl garybeihl force-pushed the fix-resize-bounds-checks branch from df4da14 to 3d33aab Compare January 15, 2026 17:20
@makubacki
Copy link
Collaborator

@garybeihl, for future reference to this PR. This is adding additional cases for the change previously made in a single location in #1243, right?

@makubacki
Copy link
Collaborator

@garybeihl, for future reference to this PR. This is adding additional cases for the change previously made in a single location in #1243, right?

Nvm, I see your comment in the previous PR now, that is the case. Thanks for updating these too.

@Javagedes
Copy link
Contributor

Resize requires that the new buffer is always equal to or larger than the old buffer. I think the bug is that you are even able to resize to a lower capacity.

We have an assert in the resize code that should be preventing resizing to a lower capacity

@Javagedes
Copy link
Contributor

Javagedes commented Jan 15, 2026

We cannot inherently resize to a lower capacity because there is no guarantee of ordering of nodes. Loosing any active node due to a resize to a lower capacity will actively destroy the tree.

I guess that is, unless we resize capacity to a lower capacity that is still greater than the current size. But I thought my currently implementation just directly moves everything to the new buffer as is. We don't actually build a brand new tree.

@garybeihl
Copy link
Contributor Author

We have an assert in the resize code that should be preventing resizing to a lower capacity

Shouldn't that assert be assert!(buffer.len() >= self.capacity()) instead of assert!(buffer.len() >= self.len());?

@Javagedes
Copy link
Contributor

Javagedes commented Jan 15, 2026

We have an assert in the resize code that should be preventing resizing to a lower capacity

Shouldn't that assert be assert!(buffer.len() >= self.capacity()) instead of assert!(buffer.len() >= self.len());?

Yes, I agree that we should change it to that. I think that is the true bug. The buffer that makes up all of the nodes does not guarantee having all of the valid nodes at the start of the buffer. Looking at the Storage code, you can see that we "delete" a node by simply clearing its pointers and adding it to the linked list that tracks available nodes for use (

pub fn delete(&mut self, node: *mut Node<D>) {
).

That means the buffer looks something like this:

[VALID | VALID | INVALID | VALID | INVALID | VALID | VALID].

So on top of us needing to change the assert above, as you suggested, we also need to loop through all nodes (self.capacity()) during self.resize(), not just self.size().

@garybeihl garybeihl force-pushed the fix-resize-bounds-checks branch 2 times, most recently from 731575b to 64bdafb Compare January 16, 2026 15:06
@garybeihl
Copy link
Contributor Author

Sounds good. I've made those changes and re-worded the PR title to more accurately reflect the changes made. Thanks for your guidance on this.

Copy link
Contributor

@Javagedes Javagedes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing all of my mistakes. This is great work :)

@Javagedes
Copy link
Contributor

@garybeihl can you rename resize to expand? I think that will make it clear that we cannot shrink it. Maybe a bit of documentation about that in the function docs too

@garybeihl garybeihl force-pushed the fix-resize-bounds-checks branch 2 times, most recently from fb0637a to ad17b4a Compare January 20, 2026 17:22
@garybeihl
Copy link
Contributor Author

@garybeihl can you rename resize to expand? I think that will make it clear that we cannot shrink it. Maybe a bit of documentation about that in the function docs too

Done - please have a look - thanks!

@Javagedes
Copy link
Contributor

@garybeihl can you rename resize to expand? I think that will make it clear that we cannot shrink it. Maybe a bit of documentation about that in the function docs too

Done - please have a look - thanks!

@garybeihl Looks good to me. There are a few documentation examples that look like they need updated. But once that is done I will merge :)

## Description

Changed resize() to properly enforce capacity constraints and copy all
nodes including gaps:

1. Changed assert from self.len() to self.capacity() to prevent
   resizing to a buffer smaller than current capacity
2. Changed loop from 0..self.len() to 0..self.capacity() to ensure
   all nodes are copied, not just the count in use

The storage layout can have gaps like [VALID | VALID | INVALID | VALID]
where deleted nodes remain in the buffer. The loop must iterate over
capacity to copy all nodes including invalid ones.

## How This Was Tested

Added two tests:
- test_resize_prevents_capacity_shrink(): Verifies assert prevents shrinking
- test_resize_copies_all_nodes_including_gaps(): Verifies all nodes copied

All 49 tests passing. Cargo clippy and fmt passing.
Per review feedback, renamed the resize() method to expand() to make it
clear that this function cannot shrink the storage capacity - it only
allows expansion. Updated all function names, tests, and added documentation
to clarify this behavior.

The expand() function now clearly indicates that it can only increase
capacity, preventing confusion about whether the buffer can be shrunk.
@garybeihl garybeihl force-pushed the fix-resize-bounds-checks branch from 4c35d80 to 05e11b2 Compare January 20, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

impact:testing Affects testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants