Skip to main content

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}