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
43    // Allocation size is multiple of alignment.
44    // See <https://doc.rust-lang.org/reference/type-layout.html>
45    let alloc_size = increase_to_alignment(tag_size);
46    let layout = Layout::from_size_align(alloc_size, ALIGNMENT).unwrap();
47    // Use a zeroed allocation so that the trailing padding in
48    // `[tag_size, alloc_size)` is initialized. The header and body writes
49    // below only cover `[0, tag_size)`; without zeroing, reading the padding
50    // through the safe `MaybeDynSized::as_bytes`/`payload` accessors would be
51    // undefined behavior. The Multiboot2 spec also mandates zero padding.
52    // SAFETY: `layout` matches the requested allocation size and alignment.
53    let heap_ptr = unsafe { alloc::alloc::alloc_zeroed(layout) };
54    assert!(!heap_ptr.is_null());
55
56    // write header
57    {
58        let len = size_of::<T::Header>();
59        let ptr = &raw const header;
60        // SAFETY: `header` is a fully initialized stack value and `heap_ptr`
61        // points into the freshly allocated destination buffer.
62        unsafe {
63            ptr::copy_nonoverlapping(ptr.cast::<u8>(), heap_ptr, len);
64        }
65    }
66
67    // write body
68    {
69        let mut write_offset = size_of::<T::Header>();
70        for &bytes in additional_bytes_slices {
71            let len = bytes.len();
72            let src = bytes.as_ptr();
73            let dst = heap_ptr.wrapping_add(write_offset);
74            // SAFETY: `src` is a valid slice and `dst` stays inside the
75            // allocated object without overlapping `src`.
76            unsafe {
77                ptr::copy_nonoverlapping(src, dst, len);
78            }
79            write_offset += len;
80        }
81    }
82
83    // This is a fat pointer for DSTs and a thin pointer for sized `T`s.
84    // SAFETY: The allocation was sized for `T` and all bytes up to the
85    // reported dynamic length were initialized above.
86    let ptr: *mut T = ptr_meta::from_raw_parts_mut(heap_ptr.cast(), T::dst_len(&header));
87    // SAFETY: `ptr` points to the initialized allocation described above.
88    let reference = unsafe { Box::from_raw(ptr) };
89
90    // If this panic triggers, there is a fundamental flaw in my logic. This is
91    // not the fault of an API user.
92    assert_eq!(
93        size_of_val(reference.deref()),
94        alloc_size,
95        "Allocation should match Rusts expectation"
96    );
97
98    reference
99}
100
101/// Clones a [`MaybeDynSized`] by calling [`new_boxed`].
102#[must_use]
103pub fn clone_dyn<T: MaybeDynSized<Metadata = usize> + ?Sized>(tag: &T) -> Box<T> {
104    new_boxed(tag.header().clone(), &[tag.payload()])
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::Tag;
111    use crate::test_utils::{DummyDstTag, DummyTestHeader};
112    use core::slice;
113
114    #[test]
115    fn test_new_boxed() {
116        let header = DummyTestHeader::new(DummyDstTag::ID, 0);
117        let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3]]);
118        assert_eq!(tag.header().typ(), 42);
119        assert_eq!(tag.payload(), &[0, 1, 2, 3]);
120
121        // Test that bytes are added consecutively without gaps.
122        let header = DummyTestHeader::new(0xdead_beef, 0);
123        let tag = new_boxed::<DummyDstTag>(header, &[&[0], &[1], &[2, 3]]);
124        assert_eq!(tag.header().typ(), 0xdead_beef);
125        assert_eq!(tag.payload(), &[0, 1, 2, 3]);
126    }
127
128    #[test]
129    fn test_new_boxed_zeroes_padding() {
130        // A payload of 1 byte yields a 9-byte tag in a 16-byte allocation.
131        // `as_bytes()` must exclude the 7 trailing padding bytes, while the
132        // allocation itself must be zeroed there (guaranteed by
133        // `alloc_zeroed`), as the Multiboot2 spec mandates zeroed padding
134        // between tags.
135        let header = DummyTestHeader::new(DummyDstTag::ID, 0);
136        let tag = new_boxed::<DummyDstTag>(header, &[&[0xff]]);
137        assert_eq!(tag.as_bytes().len(), 9);
138        let ptr = (&raw const *tag).cast::<u8>();
139        // SAFETY: The allocation spans `size_of_val` bytes and is fully
140        // initialized by `new_boxed` (zeroed allocation).
141        let all_bytes = unsafe { slice::from_raw_parts(ptr, size_of_val(&*tag)) };
142        assert_eq!(all_bytes.len(), 16);
143        assert_eq!(&all_bytes[9..16], &[0, 0, 0, 0, 0, 0, 0]);
144    }
145
146    #[test]
147    fn test_clone_tag() {
148        // A 5-byte payload, so that the reported tag size (13) is no
149        // multiple of the alignment.
150        let header = DummyTestHeader::new(DummyDstTag::ID, 0);
151        let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3, 4]]);
152        assert_eq!(tag.header().typ(), 42);
153        assert_eq!(tag.payload(), &[0, 1, 2, 3, 4]);
154
155        let cloned = clone_dyn(tag.as_ref());
156        // The clone must round-trip exactly; especially, the reported size
157        // must not grow to the padded allocation size.
158        assert_eq!(cloned.header(), tag.header());
159        assert_eq!(cloned.payload(), tag.payload());
160    }
161}