multiboot2_common/boxed.rs
1//! Module for [`new_boxed`].
2
3use crate::{ALIGNMENT, Header, MaybeDynSized, increase_to_alignment};
4use alloc::boxed::Box;
5use core::alloc::Layout;
6use core::ops::Deref;
7use core::ptr;
8
9/// Creates a new tag implementing [`MaybeDynSized`] on the heap.
10///
11/// This works for sized and unsized tags. However, it only makes sense to use
12/// this for tags that are DSTs (unsized). For regular sized structs, you can
13/// just create a typical constructor and box the result.
14///
15/// The provided `header`' total size (see [`Header`]) will be set dynamically
16/// by this function using [`Header::set_size`]. However, it must contain all
17/// other relevant metadata or update it in the `set_size` callback.
18///
19/// # Requirements
20///
21/// `T` must uphold the requirements of [`MaybeDynSized`], in particular a
22/// correct [`MaybeDynSized::BASE_SIZE`] and [`MaybeDynSized::dst_len`]
23/// implementation. These requirements ensure that the allocation made here
24/// matches the layout of `T`.
25///
26/// # Parameters
27/// - `additional_bytes_slices`: Array of byte slices that should be included
28/// without additional padding in-between. You don't need to add the bytes
29/// for [`Header`], but only additional payload.
30#[must_use]
31pub fn new_boxed<T: MaybeDynSized<Metadata = usize> + ?Sized>(
32 mut header: T::Header,
33 additional_bytes_slices: &[&[u8]],
34) -> Box<T> {
35 let additional_size = additional_bytes_slices
36 .iter()
37 .map(|b| b.len())
38 .sum::<usize>();
39
40 let tag_size = size_of::<T::Header>() + additional_size;
41 header.set_size(tag_size);
42 // Protect against incorrect set_size() implementations:
43 assert_eq!(
44 header.total_size(),
45 tag_size,
46 "the reported size should round-trip through the header"
47 );
48
49 // Allocation size is multiple of alignment.
50 // See <https://doc.rust-lang.org/reference/type-layout.html>
51 let alloc_size = increase_to_alignment(tag_size);
52 let layout = Layout::from_size_align(alloc_size, ALIGNMENT).unwrap();
53 // Use a zeroed allocation so that the trailing padding in
54 // `[tag_size, alloc_size)` is initialized. The header and body writes
55 // below only cover `[0, tag_size)`; without zeroing, reading the padding
56 // through the safe `MaybeDynSized::as_bytes`/`payload` accessors would be
57 // undefined behavior. The Multiboot2 spec also mandates zero padding.
58 // SAFETY: `layout` matches the requested allocation size and alignment.
59 let heap_ptr = unsafe { alloc::alloc::alloc_zeroed(layout) };
60 assert!(!heap_ptr.is_null());
61
62 // write header
63 {
64 let len = size_of::<T::Header>();
65 let ptr = &raw const header;
66 // SAFETY: `header` is a fully initialized stack value and `heap_ptr`
67 // points into the freshly allocated destination buffer.
68 unsafe {
69 ptr::copy_nonoverlapping(ptr.cast::<u8>(), heap_ptr, len);
70 }
71 }
72
73 // write body
74 {
75 let mut write_offset = size_of::<T::Header>();
76 for &bytes in additional_bytes_slices {
77 let len = bytes.len();
78 let src = bytes.as_ptr();
79 let dst = heap_ptr.wrapping_add(write_offset);
80 // SAFETY: `src` is a valid slice and `dst` stays inside the
81 // allocated object without overlapping `src`.
82 unsafe {
83 ptr::copy_nonoverlapping(src, dst, len);
84 }
85 write_offset += len;
86 }
87 }
88
89 // This is a fat pointer for DSTs and a thin pointer for sized `T`s.
90 // SAFETY: The allocation was sized for `T` and all bytes up to the
91 // reported dynamic length were initialized above.
92 let ptr: *mut T = ptr_meta::from_raw_parts_mut(heap_ptr.cast(), T::dst_len(&header));
93 // SAFETY: `ptr` points to the initialized allocation described above.
94 let reference = unsafe { Box::from_raw(ptr) };
95
96 // If this panic triggers, there is a fundamental flaw in my logic. This is
97 // not the fault of an API user.
98 assert_eq!(
99 size_of_val(reference.deref()),
100 alloc_size,
101 "Allocation should match Rusts expectation"
102 );
103
104 reference
105}
106
107/// Clones a [`MaybeDynSized`] by calling [`new_boxed`].
108#[must_use]
109pub fn clone_dyn<T: MaybeDynSized<Metadata = usize> + ?Sized>(tag: &T) -> Box<T> {
110 new_boxed(tag.header().clone(), &[tag.payload()])
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::Tag;
117 use crate::test_utils::{DummyDstTag, DummyTestHeader};
118 use core::slice;
119
120 #[test]
121 fn test_new_boxed() {
122 let header = DummyTestHeader::new(DummyDstTag::ID, 0);
123 let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3]]);
124 assert_eq!(tag.header().typ(), 42);
125 assert_eq!(tag.payload(), &[0, 1, 2, 3]);
126
127 // Test that bytes are added consecutively without gaps.
128 let header = DummyTestHeader::new(0xdead_beef, 0);
129 let tag = new_boxed::<DummyDstTag>(header, &[&[0], &[1], &[2, 3]]);
130 assert_eq!(tag.header().typ(), 0xdead_beef);
131 assert_eq!(tag.payload(), &[0, 1, 2, 3]);
132 }
133
134 #[test]
135 fn test_new_boxed_zeroes_padding() {
136 // A payload of 1 byte yields a 9-byte tag in a 16-byte allocation.
137 // `as_bytes()` must exclude the 7 trailing padding bytes, while the
138 // allocation itself must be zeroed there (guaranteed by
139 // `alloc_zeroed`), as the Multiboot2 spec mandates zeroed padding
140 // between tags.
141 let header = DummyTestHeader::new(DummyDstTag::ID, 0);
142 let tag = new_boxed::<DummyDstTag>(header, &[&[0xff]]);
143 assert_eq!(tag.as_bytes().len(), 9);
144 let ptr = (&raw const *tag).cast::<u8>();
145 // SAFETY: The allocation spans `size_of_val` bytes and is fully
146 // initialized by `new_boxed` (zeroed allocation).
147 let all_bytes = unsafe { slice::from_raw_parts(ptr, size_of_val(&*tag)) };
148 assert_eq!(all_bytes.len(), 16);
149 assert_eq!(&all_bytes[9..16], &[0, 0, 0, 0, 0, 0, 0]);
150 }
151
152 /// Header whose size field is artificially small, mimicking a lossy
153 /// `set_size` implementation without needing a huge allocation.
154 #[derive(Clone, Debug, PartialEq, Eq)]
155 #[repr(C)]
156 struct TinySizeHeader {
157 size: u8,
158 _pad: [u8; 7],
159 }
160
161 // SAFETY: The header is a padding-free repr(C) struct of raw integers,
162 // and any bit pattern is valid for it.
163 unsafe impl crate::Header for TinySizeHeader {
164 fn total_size(&self) -> usize {
165 self.size as usize
166 }
167
168 fn set_size(&mut self, total_size: usize) {
169 self.size = total_size as u8;
170 }
171 }
172
173 #[test]
174 #[should_panic(expected = "round-trip")]
175 fn test_new_boxed_rejects_lossy_set_size() {
176 // A total size the header can't store must cause a panic before the
177 // allocation happens. Continuing with a truncated size would create a
178 // `Box` whose layout disagrees with the allocation, which is
179 // undefined behavior when the `Box` is deallocated.
180 let header = TinySizeHeader {
181 size: 0,
182 _pad: [0; 7],
183 };
184 let _ = new_boxed::<crate::DynSizedStructure<TinySizeHeader>>(header, &[&[0_u8; 256]]);
185 }
186
187 #[test]
188 fn test_clone_tag() {
189 // A 5-byte payload, so that the reported tag size (13) is no
190 // multiple of the alignment.
191 let header = DummyTestHeader::new(DummyDstTag::ID, 0);
192 let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3, 4]]);
193 assert_eq!(tag.header().typ(), 42);
194 assert_eq!(tag.payload(), &[0, 1, 2, 3, 4]);
195
196 let cloned = clone_dyn(tag.as_ref());
197 // The clone must round-trip exactly; especially, the reported size
198 // must not grow to the padded allocation size.
199 assert_eq!(cloned.header(), tag.header());
200 assert_eq!(cloned.payload(), tag.payload());
201 }
202}