nros_rmw/fallible.rs
1//! Fallible heap allocation for entity-creation paths (issue 1551, defect 2).
2//!
3//! # Why this exists
4//!
5//! `Box::new` cannot fail: when the global allocator returns NULL it calls
6//! `alloc::alloc::handle_alloc_error`, and in a `std` build that runs
7//! libstd's `default_alloc_error_hook`, which writes
8//! `"memory allocation of N bytes failed"` to **fd 2** before aborting.
9//!
10//! On a Zephyr `native_sim` image fd 2 is the POSIX fdtable's `stdinout`
11//! entry, whose write re-enters `zvfs_write` forever (issue 0589). So the
12//! message never appears: the thread overflows its stack inside a recursive
13//! `k_mutex` and the process dies with SIGSEGV (exit 139). Measured on the
14//! derived-tiers-cpp image, ~104,735 frames deep, from a `Box::new` in the
15//! RMW adapter whose C caller had a perfectly good error return.
16//!
17//! None of the three levers that would make the OOM handler itself safe are
18//! available on the pinned stable toolchain (measured on rustc 1.98.1):
19//! `std::alloc::set_alloc_error_hook` (feature `alloc_error_hook`),
20//! `#[alloc_error_handler]` (feature `alloc_error_handler`) and `-Zoom=panic`
21//! are all nightly-only, and a `GlobalAlloc` cannot tell a fallible caller
22//! from an infallible one, so the allocator cannot halt on the caller's
23//! behalf without also breaking every `try_reserve`. The remaining lever is
24//! at the CALL SITE: an allocation whose failure the caller can report must
25//! not go through the infallible API at all.
26//!
27//! # The rule
28//!
29//! On a path that creates an entity (session, publisher, subscription,
30//! service, client, event registration) AND already has an error return to
31//! a C/C++ caller, allocate through [`try_box`] and map `Err` to the
32//! caller's out-of-memory code (`NROS_RMW_RET_BAD_ALLOC` /
33//! [`crate::TransportError::BadAlloc`]). `Box::try_new` is the std spelling
34//! and is unstable; this is its stable equivalent, and the one spelling in
35//! the tree.
36
37use alloc::boxed::Box;
38use core::alloc::Layout;
39
40/// Move `value` into a new heap allocation, or hand it back if the global
41/// allocator is exhausted.
42///
43/// The stable equivalent of the unstable `Box::try_new`. On `Err(value)` the
44/// caller still owns `value` and decides how to dispose of it — for a backend
45/// handle, dropping it undeclares the entity the backend just created, which
46/// is what keeps a failed create from leaking a half-registered entity.
47///
48/// A zero-sized `T` never allocates and always succeeds, exactly as
49/// `Box::new` does.
50pub fn try_box<T>(value: T) -> Result<Box<T>, T> {
51 let layout = Layout::new::<T>();
52 if layout.size() == 0 {
53 // `Box::new` of a ZST performs no allocation, so it cannot reach the
54 // OOM handler.
55 return Ok(Box::new(value));
56 }
57 // SAFETY: `layout` has non-zero size (checked above).
58 let ptr = unsafe { alloc::alloc::alloc(layout) }.cast::<T>();
59 if ptr.is_null() {
60 return Err(value);
61 }
62 // SAFETY: `ptr` is a fresh, non-null allocation from the global
63 // allocator with `Layout::new::<T>()`, which is exactly the memory layout
64 // `Box<T>` documents it owns and frees; `write` initialises it without
65 // reading the uninitialised contents.
66 unsafe {
67 ptr.write(value);
68 Ok(Box::from_raw(ptr))
69 }
70}
71
72/// An UNINITIALISED heap allocation for one `T`, or `None` if the global
73/// allocator is exhausted.
74///
75/// The fallible form of `Box::<T>::new_uninit()`, for a value too large to
76/// build on the stack and move in (issue 0756 -- the parameter store is
77/// 285,696 bytes at the default sizing, and [`try_box`] takes its value BY
78/// VALUE, which materialises all of it in the caller's frame first). The
79/// caller initialises through the pointer and then `assume_init`s, exactly as
80/// with `new_uninit`.
81///
82/// Issue 1706 -- that store is the one allocation every parameter
83/// declaration reaches, on every RTOS, and `new_uninit` sent its failure to
84/// the OOM handler: a halt (Zephyr's `HEAP EXHAUSTED` then issue 0589's
85/// recursion) or a `MALLOC FAILED` hang (FreeRTOS heap_4), never a refusal the
86/// declaration could return.
87pub fn try_box_uninit<T>() -> Option<Box<core::mem::MaybeUninit<T>>> {
88 let layout = Layout::new::<T>();
89 if layout.size() == 0 {
90 return Some(Box::new_uninit());
91 }
92 // SAFETY: `layout` has non-zero size (checked above).
93 let ptr = unsafe { alloc::alloc::alloc(layout) }.cast::<core::mem::MaybeUninit<T>>();
94 if ptr.is_null() {
95 return None;
96 }
97 // SAFETY: a fresh, non-null allocation from the global allocator with
98 // `Layout::new::<T>()`, which is `MaybeUninit<T>`'s layout too; a
99 // `MaybeUninit` needs no initialisation to be owned by a `Box`.
100 Some(unsafe { Box::from_raw(ptr) })
101}
102
103/// A zeroed `len`-byte heap buffer, or `None` if the allocator is exhausted.
104///
105/// The fallible form of `vec![0u8; len].into_boxed_slice()`, which aborts
106/// through the same OOM hook as `Box::new`. `try_reserve_exact` is stable;
107/// the capacity it records is exactly `len`, so `into_boxed_slice` does not
108/// reallocate.
109pub fn try_zeroed_bytes(len: usize) -> Option<Box<[u8]>> {
110 let mut v = alloc::vec::Vec::new();
111 v.try_reserve_exact(len).ok()?;
112 v.resize(len, 0u8);
113 Some(v.into_boxed_slice())
114}
115
116#[cfg(test)]
117mod tests {
118 use super::{try_box, try_zeroed_bytes};
119
120 #[test]
121 fn zeroed_bytes_are_zero_and_exact() {
122 let b = try_zeroed_bytes(37).expect("host heap is not exhausted");
123 assert_eq!(b.len(), 37);
124 assert!(b.iter().all(|&x| x == 0));
125 }
126
127 #[test]
128 fn an_impossible_request_is_none_not_an_abort() {
129 // `isize::MAX` bytes is a capacity overflow / allocation failure that
130 // `vec![0; n]` would turn into a process abort.
131 assert!(try_zeroed_bytes(isize::MAX as usize).is_none());
132 }
133
134 #[test]
135 fn boxes_a_sized_value() {
136 let b = try_box([7u64; 4]).expect("host heap is not exhausted");
137 assert_eq!(*b, [7u64; 4]);
138 }
139
140 #[test]
141 fn an_impossible_uninit_box_is_none_not_an_abort() {
142 // Issue 1706 -- the parameter store's allocation, at a size no heap
143 // has: `Box::new_uninit` would reach the OOM handler.
144 #[cfg(target_pointer_width = "64")]
145 assert!(super::try_box_uninit::<[u8; 1usize << 60]>().is_none());
146 let mut b = super::try_box_uninit::<[u64; 4]>().expect("host heap is not exhausted");
147 b.write([3u64; 4]);
148 // SAFETY: written just above.
149 assert_eq!(*unsafe { b.assume_init() }, [3u64; 4]);
150 }
151
152 #[test]
153 fn boxes_a_zero_sized_value() {
154 struct Zst;
155 assert!(try_box(Zst).is_ok());
156 }
157
158 #[test]
159 fn a_box_from_try_box_drops_its_value_once() {
160 use core::sync::atomic::{AtomicUsize, Ordering};
161 static DROPS: AtomicUsize = AtomicUsize::new(0);
162 struct Counted;
163 impl Drop for Counted {
164 fn drop(&mut self) {
165 DROPS.fetch_add(1, Ordering::SeqCst);
166 }
167 }
168 drop(try_box(Counted).ok().expect("host heap is not exhausted"));
169 assert_eq!(DROPS.load(Ordering::SeqCst), 1);
170 }
171}