Skip to content
6 changes: 6 additions & 0 deletions multiboot2-common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- `new_boxed` now panics if the total structure size does not round-trip
through `Header::set_size`/`Header::total_size`, for example due to a lossy
`set_size` implementation. Previously, such a mismatch led to a `Box` whose
layout disagrees with the allocation, which is undefined behavior on
deallocation.

## v0.6.0 (2026-09-02)

- **Breaking:** Fixed undefined behavior when serializing stack-constructed
Expand Down
41 changes: 41 additions & 0 deletions multiboot2-common/src/boxed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub fn new_boxed<T: MaybeDynSized<Metadata = usize> + ?Sized>(

let tag_size = size_of::<T::Header>() + additional_size;
header.set_size(tag_size);
// Protect against incorrect set_size() implementations:
assert_eq!(
header.total_size(),
tag_size,
"the reported size should round-trip through the header"
);

// Allocation size is multiple of alignment.
// See <https://doc.rust-lang.org/reference/type-layout.html>
Expand Down Expand Up @@ -143,6 +149,41 @@ mod tests {
assert_eq!(&all_bytes[9..16], &[0, 0, 0, 0, 0, 0, 0]);
}

/// Header whose size field is artificially small, mimicking a lossy
/// `set_size` implementation without needing a huge allocation.
#[derive(Clone, Debug, PartialEq, Eq)]
#[repr(C)]
struct TinySizeHeader {
size: u8,
_pad: [u8; 7],
}

// SAFETY: The header is a padding-free repr(C) struct of raw integers,
// and any bit pattern is valid for it.
unsafe impl crate::Header for TinySizeHeader {
fn total_size(&self) -> usize {
self.size as usize
}

fn set_size(&mut self, total_size: usize) {
self.size = total_size as u8;
}
}

#[test]
#[should_panic(expected = "round-trip")]
fn test_new_boxed_rejects_lossy_set_size() {
// A total size the header can't store must cause a panic before the
// allocation happens. Continuing with a truncated size would create a
// `Box` whose layout disagrees with the allocation, which is
// undefined behavior when the `Box` is deallocated.
let header = TinySizeHeader {
size: 0,
_pad: [0; 7],
};
let _ = new_boxed::<crate::DynSizedStructure<TinySizeHeader>>(header, &[&[0_u8; 256]]);
}

#[test]
fn test_clone_tag() {
// A 5-byte payload, so that the reported tag size (13) is no
Expand Down
9 changes: 6 additions & 3 deletions multiboot2-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ pub unsafe trait Header: Clone + Sized + PartialEq + Eq + Debug {
}

/// Updates the header with the given `total_size`.
///
/// Implementations should either store the size losslessly or panic on
/// overflow. Construction helpers such as `new_boxed` verify that the
/// size round-trips through the header and panic otherwise.
fn set_size(&mut self, total_size: usize);
}

Expand Down Expand Up @@ -457,9 +461,8 @@ impl<H: Header> DynSizedStructure<H> {
let t_dst_size = T::dst_len(self.header());
// Creates thin or fat pointer, depending on type.
let t_ptr = ptr_meta::from_raw_parts(base_ptr.cast(), t_dst_size);
// SAFETY: `self` is a valid reference and the cast keeps the same
// allocation; `T::dst_len` determines the matching tail length. The
// assertion above guarantees the retagged extent stays in bounds.
// SAFETY: The guarantees of DynSizedStructure ensures that the cast is
// valid and in bounds. The assertions above double check that.
let t_ref = unsafe { &*t_ptr };

assert_eq!(size_of_val(self), size_of_val(t_ref));
Expand Down
5 changes: 5 additions & 0 deletions multiboot2-common/src/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ use ptr_meta::Pointee;
/// within the reported range can cause out-of-bounds references. Trailing
/// padding beyond that range is fine.
///
/// Note that for sized implementors, the requirements above imply that
/// `size_of::<Self>()` exceeds [`MaybeDynSized::BASE_SIZE`] at most by
/// trailing padding up to the type's alignment. Same-size casts rely on this
/// to keep the created reference within the source allocation.
///
/// [`ID`]: Tag::ID
/// [`ALIGNMENT`]: crate::ALIGNMENT
/// [`DynSizedStructure`]: crate::DynSizedStructure
Expand Down
3 changes: 2 additions & 1 deletion multiboot2-common/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ unsafe impl Header for DummyTestHeader {
}

fn set_size(&mut self, total_size: usize) {
self.size = total_size as u32;
self.size = u32::try_from(total_size).unwrap();
}
}

Expand Down Expand Up @@ -111,6 +111,7 @@ unsafe impl MaybeDynSized for DummyDstTag {
const BASE_SIZE: usize = size_of::<DummyTestHeader>();

fn dst_len(header: &Self::Header) -> Self::Metadata {
assert!(header.size as usize >= Self::BASE_SIZE);
header.size as usize - Self::BASE_SIZE
}
}
Expand Down
2 changes: 1 addition & 1 deletion multiboot2-header/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ unsafe impl DynSizedHeader for Multiboot2BasicHeader {
}

fn set_size(&mut self, total_size: usize) {
self.length = total_size as u32;
self.length = u32::try_from(total_size).unwrap();
self.checksum = Self::calc_checksum(self.header_magic, self.arch(), total_size as u32);
}
}
Expand Down
2 changes: 1 addition & 1 deletion multiboot2-header/src/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,6 @@ unsafe impl Header for HeaderTagHeader {
}

fn set_size(&mut self, total_size: usize) {
self.size = total_size as u32;
self.size = u32::try_from(total_size).unwrap();
}
}
19 changes: 19 additions & 0 deletions multiboot2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

## Unreleased

- **Breaking:** The `VBEModeInfo` fields `resolution: (u16, u16)` and
`character_size: (u8, u8)` were split into the four fields `x_resolution`,
`y_resolution`, `x_char_size`, and `y_char_size`, following the VBE spec
names. Tuples have no layout guarantee in Rust, so their use in the
ABI-compatible struct was formally incorrect.
- The deprecated `BootInformation::elf_sections` no longer asserts a relation
between `entry_size`, `shndx`, and the tag size. The check could wrap and
guarded nothing; the section iterator is bounds-checked anyway. It now
behaves like `elf_sections_tag()` plus `sections()`.
- `ModuleTag::module_size` now always panics with a clear message if the tag
reports an end address below the start address. Previously, the subtraction
wrapped silently in release builds.
- The `MaybeDynSized::BASE_SIZE` constants of `EFISdt32Tag`,
`EFIImageHandle32Tag`, `ImageLoadPhysAddrTag`, `RsdpV1Tag`, and `RsdpV2Tag`
now report the spec-mandated structure size (12, 12, 12, 28, and 44 bytes)
instead of the padded Rust type size, matching the documented trait contract
and the other sized tags. The reported tag sizes and the built boot
information are unchanged.

## v0.27.0 (2026-09-02)

- Fixed undefined behavior when serializing stack-constructed sized tags with
Expand Down
12 changes: 3 additions & 9 deletions multiboot2/src/boot_information.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ use crate::{
};
use core::fmt;
use core::ptr::NonNull;
use multiboot2_common::{
DynSizedStructure, Header, MaybeDynSized, MemoryError, Tag, validate_tag_sequence,
};
use multiboot2_common::{DynSizedStructure, Header, MemoryError, Tag, validate_tag_sequence};
use thiserror::Error;

/// Errors that occur when a chunk of memory can't be parsed as
Expand Down Expand Up @@ -63,7 +61,7 @@ unsafe impl Header for BootInformationHeader {
}

fn set_size(&mut self, total_size: usize) {
self.total_size = total_size as u32;
self.total_size = u32::try_from(total_size).unwrap();
}
}

Expand Down Expand Up @@ -261,11 +259,7 @@ impl<'a> BootInformation<'a> {
#[must_use]
#[deprecated = "Use elf_sections_tag() instead and corresponding getters"]
pub fn elf_sections(&self) -> Option<ElfSectionIter<'_>> {
let tag = self.get_tag::<ElfSectionsTag>();
tag.map(|t| {
assert!((t.entry_size() * t.shndx()) <= t.header().size);
t.sections()
})
self.get_tag::<ElfSectionsTag>().map(|t| t.sections())
}

/// Returns the first [`ElfSectionsTag`], if present.
Expand Down
1 change: 0 additions & 1 deletion multiboot2/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,6 @@ mod tests {
VBEControlInfo::default(),
VBEModeInfo::default(),
))
// Currently causes UB.
.framebuffer(FramebufferTag::new(
0x1000,
1,
Expand Down
2 changes: 1 addition & 1 deletion multiboot2/src/command_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl CommandLineTag {
#[cfg(feature = "builder")]
#[must_use]
pub fn new(command_line: &str) -> Box<Self> {
let header = TagHeader::new(Self::ID, 0);
let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */);
let bytes = command_line.as_bytes();
if bytes.ends_with(&[0]) {
new_boxed(header, &[bytes])
Expand Down
19 changes: 13 additions & 6 deletions multiboot2/src/efi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ pub struct EFISdt32Tag {
}

impl EFISdt32Tag {
const BASE_SIZE: usize = size_of::<TagHeader>() + size_of::<u32>();

/// Create a new tag to pass the EFI32 System Table pointer.
#[must_use]
pub fn new(pointer: u32) -> Self {
Expand All @@ -42,7 +40,9 @@ impl EFISdt32Tag {
unsafe impl MaybeDynSized for EFISdt32Tag {
type Header = TagHeader;

const BASE_SIZE: usize = size_of::<Self>();
// Spec size (12), excluding the trailing padding that `size_of::<Self>()`
// (16) would add.
const BASE_SIZE: usize = size_of::<TagHeader>() + size_of::<u32>();
}

impl Tag for EFISdt32Tag {
Expand Down Expand Up @@ -100,8 +100,6 @@ pub struct EFIImageHandle32Tag {
}

impl EFIImageHandle32Tag {
const BASE_SIZE: usize = size_of::<TagHeader>() + size_of::<u32>();

/// Constructs a new tag.
#[must_use]
pub fn new(pointer: u32) -> Self {
Expand All @@ -123,7 +121,7 @@ impl EFIImageHandle32Tag {
unsafe impl MaybeDynSized for EFIImageHandle32Tag {
type Header = TagHeader;

const BASE_SIZE: usize = size_of::<Self>();
const BASE_SIZE: usize = size_of::<TagHeader>() + size_of::<u32>();
}

impl Tag for EFIImageHandle32Tag {
Expand Down Expand Up @@ -218,6 +216,15 @@ mod tests {

const ADDR: usize = 0xABCDEF;

/// The tags must report the spec-mandated size (12), not the padded Rust
/// type size (16).
#[test]
fn base_size_excludes_trailing_padding() {
use multiboot2_common::MaybeDynSized;
assert_eq!(<EFISdt32Tag as MaybeDynSized>::BASE_SIZE, 12);
assert_eq!(<EFIImageHandle32Tag as MaybeDynSized>::BASE_SIZE, 12);
}

#[test]
fn test_build_eftsdt32() {
let tag = EFISdt32Tag::new(ADDR.try_into().unwrap());
Expand Down
2 changes: 1 addition & 1 deletion multiboot2/src/elf_sections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl ElfSectionsTag {
#[cfg(feature = "builder")]
#[must_use]
pub fn new(number_of_sections: u32, entry_size: u32, shndx: u32, sections: &[u8]) -> Box<Self> {
let header = TagHeader::new(Self::ID, 0);
let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */);
let number_of_sections = number_of_sections.to_ne_bytes();
let entry_size = entry_size.to_ne_bytes();
let shndx = shndx.to_ne_bytes();
Expand Down
2 changes: 1 addition & 1 deletion multiboot2/src/framebuffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl FramebufferTag {
bpp: u8,
buffer_type: FramebufferType,
) -> Box<Self> {
let header = TagHeader::new(Self::ID, 0);
let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */);
let address = address.to_ne_bytes();
let pitch = pitch.to_ne_bytes();
let width = width.to_ne_bytes();
Expand Down
12 changes: 9 additions & 3 deletions multiboot2/src/image_load_addr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ pub struct ImageLoadPhysAddrTag {
}

impl ImageLoadPhysAddrTag {
const BASE_SIZE: usize = size_of::<TagHeader>() + size_of::<u32>();

/// Constructs a new tag.
#[must_use]
pub fn new(load_base_addr: u32) -> Self {
Expand All @@ -37,7 +35,7 @@ impl ImageLoadPhysAddrTag {
unsafe impl MaybeDynSized for ImageLoadPhysAddrTag {
type Header = TagHeader;

const BASE_SIZE: usize = size_of::<Self>();
const BASE_SIZE: usize = size_of::<TagHeader>() + size_of::<u32>();
}

impl Tag for ImageLoadPhysAddrTag {
Expand All @@ -52,6 +50,14 @@ mod tests {

const ADDR: u32 = 0xABCDEF;

/// The tag must report the spec-mandated size (12), not the padded Rust
/// type size (16).
#[test]
fn base_size_excludes_trailing_padding() {
use multiboot2_common::MaybeDynSized;
assert_eq!(<ImageLoadPhysAddrTag as MaybeDynSized>::BASE_SIZE, 12);
}

#[test]
fn test_build_load_addr() {
let tag = ImageLoadPhysAddrTag::new(ADDR);
Expand Down
6 changes: 4 additions & 2 deletions multiboot2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,8 +537,10 @@ mod tests {
assert_eq!({ vbe.mode_info().window_a_segment }, 40960);
assert_eq!({ vbe.mode_info().window_function_ptr }, 3221247162);
assert_eq!({ vbe.mode_info().pitch }, 5120);
assert_eq!({ vbe.mode_info().resolution }, (1280, 800));
assert_eq!(vbe.mode_info().character_size, (8, 16));
assert_eq!({ vbe.mode_info().x_resolution }, 1280);
assert_eq!({ vbe.mode_info().y_resolution }, 800);
assert_eq!(vbe.mode_info().x_char_size, 8);
assert_eq!(vbe.mode_info().y_char_size, 16);
assert_eq!(vbe.mode_info().number_of_planes, 1);
assert_eq!(vbe.mode_info().bpp, 32);
assert_eq!(vbe.mode_info().number_of_banks, 1);
Expand Down
4 changes: 2 additions & 2 deletions multiboot2/src/memory_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl MemoryMapTag {
#[cfg(feature = "builder")]
#[must_use]
pub fn new(areas: &[MemoryArea]) -> Box<Self> {
let header = TagHeader::new(Self::ID, 0);
let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */);
let entry_size = (size_of::<MemoryArea>() as u32).to_ne_bytes();
let entry_version = 0_u32.to_ne_bytes();
let areas = {
Expand Down Expand Up @@ -294,7 +294,7 @@ impl EFIMemoryMapTag {
#[cfg(feature = "builder")]
#[must_use]
pub fn new_from_map(desc_size: u32, desc_version: u32, efi_mmap: &[u8]) -> Box<Self> {
let header = TagHeader::new(Self::ID, 0);
let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */);
assert_ne!(desc_size, 0);
let desc_size = desc_size.to_ne_bytes();
let desc_version = desc_version.to_ne_bytes();
Expand Down
10 changes: 9 additions & 1 deletion multiboot2/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl ModuleTag {
#[cfg(feature = "builder")]
#[must_use]
pub fn new(start: u32, end: u32, cmdline: &str) -> Box<Self> {
let header = TagHeader::new(Self::ID, 0);
let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */);
assert!(end > start, "must have a size");

let start = start.to_ne_bytes();
Expand Down Expand Up @@ -64,8 +64,16 @@ impl ModuleTag {
}

/// The size of the module/the BLOB in memory.
///
/// # Panics
/// Panics if the tag reports an end address below the start address,
/// which can only happen for oddly formed tags.
#[must_use]
pub const fn module_size(&self) -> u32 {
assert!(
self.mod_end >= self.mod_start,
"the module end address should not be below its start address"
);
self.mod_end - self.mod_start
}
}
Expand Down
2 changes: 1 addition & 1 deletion multiboot2/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl NetworkTag {
#[cfg(feature = "builder")]
#[must_use]
pub fn new(dhcp_pack: &[u8]) -> Box<Self> {
let header = TagHeader::new(Self::ID, 0);
let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */);
new_boxed(header, &[dhcp_pack])
}
}
Expand Down
Loading