Skip to content

patina_internal_collections: Fix UB memory read in Node fields#1152

Open
garybeihl wants to merge 4 commits intoOpenDevicePartnership:mainfrom
garybeihl:fix-node-uninit
Open

patina_internal_collections: Fix UB memory read in Node fields#1152
garybeihl wants to merge 4 commits intoOpenDevicePartnership:mainfrom
garybeihl:fix-node-uninit

Conversation

@garybeihl
Copy link
Contributor

@garybeihl garybeihl commented Dec 3, 2025

Description

Fixes an undefined behavior issue where Cell::set() reads uninitialized memory during linked list creation in Storage::resize().

Root Cause

  • Cell::set() internally uses mem::replace(), which reads the old value before writing the new one.
  • When Storage::resize() allocates new nodes and calls build_linked_list(), the Cell fields contain uninitialized memory.
  • Reading uninitialized memory is undefined behavior, even if immediately overwritten. Unwanted compiler "optimizations" could follow.

Impact

Fix

  • Initialize all Cell fields using ptr::write() before build_linked_list()
  • Use addr_of_mut!() to read field pointers without creating references to uninitialized data

Introduced by

Related to #560

  • Impacts functionality?
  • Impacts security?
  • Breaking change?
  • Includes tests?
  • Includes documentation?

How This Was Tested

  • Tested with: cargo +nightly-2025-09-19 miri test -p patina_dxe_core.
  • 7 tests now pass (previously 0/469 due to this UB issue).

Integration Instructions

N/A

@github-actions github-actions bot added the impact:security Has a security impact label Dec 3, 2025
@garybeihl garybeihl added the type:bug Something isn't working label Dec 3, 2025
@codecov
Copy link

codecov bot commented Dec 3, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@garybeihl garybeihl force-pushed the fix-node-uninit branch 2 times, most recently from 492ef76 to 7a57e63 Compare December 3, 2025 16:57
@garybeihl garybeihl force-pushed the fix-node-uninit branch 4 times, most recently from f0178c1 to b1385f8 Compare December 9, 2025 00:26
@garybeihl garybeihl force-pushed the fix-node-uninit branch 2 times, most recently from ae5dcec to 7fcea9b Compare December 16, 2025 00:41
@Javagedes
Copy link
Contributor

Hi @garybeihl , I noticed you've been keeping this PR up to date, but not working on it (which is completely fine).

I just wanted to make sure you were not waiting on additional information from any of us that have commented on this PR (i.e. make sure we are not blocking you in any way)!

@garybeihl
Copy link
Contributor Author

No - not blocked - I uploaded a version of the changes that uses MaybeUninit and D: Default - just waiting for further comments or change requests. If you could have a look when you get a chance, that would be great - thanks!

@garybeihl garybeihl force-pushed the fix-node-uninit branch 3 times, most recently from a3c3321 to 490f0cf Compare December 23, 2025 21:23
@garybeihl garybeihl force-pushed the fix-node-uninit branch 4 times, most recently from f6e8da6 to 84636f4 Compare January 15, 2026 15:04
…ment

Replace Default trait requirement with MaybeUninit wrapper for Node data field.
Only initialize data when nodes move from available to in-use list.
Fixes UB where Cell::set() was reading uninitialized memory.
Add safety documentation to all unsafe blocks in production code.
Add allow(clippy::undocumented_unsafe_blocks) to test modules
to avoid redundant safety comments for test assertions.
@garybeihl
Copy link
Contributor Author

Is there anything more for me to do here? If not, can I get another approval?

Copy link
Collaborator

@makubacki makubacki left a comment

Choose a reason for hiding this comment

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

@Javagedes & @joschock should have a final review as well.

@patina-automation
Copy link
Contributor

patina-automation bot commented Mar 5, 2026

✅ QEMU Validation Passed

All QEMU validation jobs completed successfully.

Note: Windows Q35 is only built (QEMU boot is disabled due to QEMU vfat issue).

Workflow run: https://github.com/OpenDevicePartnership/patina/actions/runs/22733280504

Boot Time to EFI Shell

Platform Elapsed
Linux Q35 34.9s
Linux SBSA 41.9s

This comment was automatically generated by the Patina QEMU PR Validation Post workflow.

@garybeihl
Copy link
Contributor Author

Ping - please review so I can close this and move on to other Miri-found issues. Thanks!

Copy link
Collaborator

@makubacki makubacki left a comment

Choose a reason for hiding this comment

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

Approving latest

Comment on lines 211 to 216
let buffer = unsafe {
slice::from_raw_parts_mut::<'a, Node<D>>(
slice as *mut [u8] as *mut Node<D>,
slice.len() / mem::size_of::<Node<D>>(),
)
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this need to go through MaybeUnit<Node<D>> too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch — yes, expand() has the same problem. It creates a &mut [Node<D>] from raw bytes without going through MaybeUninit, which is UB for the same reason. I'll update it to use MaybeUninit<Node<D>> as well.

D: SliceKey,
{
pub data: D,
pub data: MaybeUninit<D>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this still need to be pub with functions like pub unsafe fn data() and pub unsafe fn data_mut() having been added? Or could it at least be pub(crate)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right — nothing outside the crate accesses the field directly; all external access goes through the data() and data_mut() accessors. I'll change it to pub(crate).

@@ -216,7 +229,13 @@
for i in 0..self.capacity() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is for i in 0..self.capacity() supposed to be for i in 0..self.len()? That's what the safety comment seems to indicate below.


#[cfg(test)]
#[coverage(off)]
#[allow(clippy::undocumented_unsafe_blocks)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

We're generally trying to provide safety comments more granularly (even for tests) to avoid needed to apply this at the module level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

impact:security Has a security impact type:bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants