diff --git a/multiboot2-common/CHANGELOG.md b/multiboot2-common/CHANGELOG.md index 319b3401..28ab5572 100644 --- a/multiboot2-common/CHANGELOG.md +++ b/multiboot2-common/CHANGELOG.md @@ -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 diff --git a/multiboot2-common/src/boxed.rs b/multiboot2-common/src/boxed.rs index 4cdbccc6..4e1a81a9 100644 --- a/multiboot2-common/src/boxed.rs +++ b/multiboot2-common/src/boxed.rs @@ -39,6 +39,12 @@ pub fn new_boxed + ?Sized>( let tag_size = size_of::() + 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 @@ -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::>(header, &[&[0_u8; 256]]); + } + #[test] fn test_clone_tag() { // A 5-byte payload, so that the reported tag size (13) is no diff --git a/multiboot2-common/src/lib.rs b/multiboot2-common/src/lib.rs index bf7f1f16..27f5565e 100644 --- a/multiboot2-common/src/lib.rs +++ b/multiboot2-common/src/lib.rs @@ -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); } @@ -457,9 +461,8 @@ impl DynSizedStructure { 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)); diff --git a/multiboot2-common/src/tag.rs b/multiboot2-common/src/tag.rs index 6ffadd5b..f6c1d69e 100644 --- a/multiboot2-common/src/tag.rs +++ b/multiboot2-common/src/tag.rs @@ -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::()` 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 diff --git a/multiboot2-common/src/test_utils.rs b/multiboot2-common/src/test_utils.rs index 238cf234..257e17d6 100644 --- a/multiboot2-common/src/test_utils.rs +++ b/multiboot2-common/src/test_utils.rs @@ -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(); } } @@ -111,6 +111,7 @@ unsafe impl MaybeDynSized for DummyDstTag { const BASE_SIZE: usize = size_of::(); fn dst_len(header: &Self::Header) -> Self::Metadata { + assert!(header.size as usize >= Self::BASE_SIZE); header.size as usize - Self::BASE_SIZE } } diff --git a/multiboot2-header/src/header.rs b/multiboot2-header/src/header.rs index a79162ed..7c725da8 100644 --- a/multiboot2-header/src/header.rs +++ b/multiboot2-header/src/header.rs @@ -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); } } diff --git a/multiboot2-header/src/tags.rs b/multiboot2-header/src/tags.rs index 88cdb5b7..30ab3153 100644 --- a/multiboot2-header/src/tags.rs +++ b/multiboot2-header/src/tags.rs @@ -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(); } } diff --git a/multiboot2/CHANGELOG.md b/multiboot2/CHANGELOG.md index 9076fbfd..9af0b481 100644 --- a/multiboot2/CHANGELOG.md +++ b/multiboot2/CHANGELOG.md @@ -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 diff --git a/multiboot2/src/boot_information.rs b/multiboot2/src/boot_information.rs index 11a7eaaf..55f0eeaa 100644 --- a/multiboot2/src/boot_information.rs +++ b/multiboot2/src/boot_information.rs @@ -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 @@ -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(); } } @@ -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> { - let tag = self.get_tag::(); - tag.map(|t| { - assert!((t.entry_size() * t.shndx()) <= t.header().size); - t.sections() - }) + self.get_tag::().map(|t| t.sections()) } /// Returns the first [`ElfSectionsTag`], if present. diff --git a/multiboot2/src/builder.rs b/multiboot2/src/builder.rs index 9e406786..3c9ed53a 100644 --- a/multiboot2/src/builder.rs +++ b/multiboot2/src/builder.rs @@ -388,7 +388,6 @@ mod tests { VBEControlInfo::default(), VBEModeInfo::default(), )) - // Currently causes UB. .framebuffer(FramebufferTag::new( 0x1000, 1, diff --git a/multiboot2/src/command_line.rs b/multiboot2/src/command_line.rs index 6ecebd07..4559f07b 100644 --- a/multiboot2/src/command_line.rs +++ b/multiboot2/src/command_line.rs @@ -25,7 +25,7 @@ impl CommandLineTag { #[cfg(feature = "builder")] #[must_use] pub fn new(command_line: &str) -> Box { - 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]) diff --git a/multiboot2/src/efi.rs b/multiboot2/src/efi.rs index c573dde2..a27e416e 100644 --- a/multiboot2/src/efi.rs +++ b/multiboot2/src/efi.rs @@ -19,8 +19,6 @@ pub struct EFISdt32Tag { } impl EFISdt32Tag { - const BASE_SIZE: usize = size_of::() + size_of::(); - /// Create a new tag to pass the EFI32 System Table pointer. #[must_use] pub fn new(pointer: u32) -> Self { @@ -42,7 +40,9 @@ impl EFISdt32Tag { unsafe impl MaybeDynSized for EFISdt32Tag { type Header = TagHeader; - const BASE_SIZE: usize = size_of::(); + // Spec size (12), excluding the trailing padding that `size_of::()` + // (16) would add. + const BASE_SIZE: usize = size_of::() + size_of::(); } impl Tag for EFISdt32Tag { @@ -100,8 +100,6 @@ pub struct EFIImageHandle32Tag { } impl EFIImageHandle32Tag { - const BASE_SIZE: usize = size_of::() + size_of::(); - /// Constructs a new tag. #[must_use] pub fn new(pointer: u32) -> Self { @@ -123,7 +121,7 @@ impl EFIImageHandle32Tag { unsafe impl MaybeDynSized for EFIImageHandle32Tag { type Header = TagHeader; - const BASE_SIZE: usize = size_of::(); + const BASE_SIZE: usize = size_of::() + size_of::(); } impl Tag for EFIImageHandle32Tag { @@ -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!(::BASE_SIZE, 12); + assert_eq!(::BASE_SIZE, 12); + } + #[test] fn test_build_eftsdt32() { let tag = EFISdt32Tag::new(ADDR.try_into().unwrap()); diff --git a/multiboot2/src/elf_sections.rs b/multiboot2/src/elf_sections.rs index d6c93a0b..066754c6 100644 --- a/multiboot2/src/elf_sections.rs +++ b/multiboot2/src/elf_sections.rs @@ -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 { - 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(); diff --git a/multiboot2/src/framebuffer.rs b/multiboot2/src/framebuffer.rs index 1d5f1ac7..20c9bf86 100644 --- a/multiboot2/src/framebuffer.rs +++ b/multiboot2/src/framebuffer.rs @@ -102,7 +102,7 @@ impl FramebufferTag { bpp: u8, buffer_type: FramebufferType, ) -> Box { - 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(); diff --git a/multiboot2/src/image_load_addr.rs b/multiboot2/src/image_load_addr.rs index d2d4d66c..83530d12 100644 --- a/multiboot2/src/image_load_addr.rs +++ b/multiboot2/src/image_load_addr.rs @@ -15,8 +15,6 @@ pub struct ImageLoadPhysAddrTag { } impl ImageLoadPhysAddrTag { - const BASE_SIZE: usize = size_of::() + size_of::(); - /// Constructs a new tag. #[must_use] pub fn new(load_base_addr: u32) -> Self { @@ -37,7 +35,7 @@ impl ImageLoadPhysAddrTag { unsafe impl MaybeDynSized for ImageLoadPhysAddrTag { type Header = TagHeader; - const BASE_SIZE: usize = size_of::(); + const BASE_SIZE: usize = size_of::() + size_of::(); } impl Tag for ImageLoadPhysAddrTag { @@ -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!(::BASE_SIZE, 12); + } + #[test] fn test_build_load_addr() { let tag = ImageLoadPhysAddrTag::new(ADDR); diff --git a/multiboot2/src/lib.rs b/multiboot2/src/lib.rs index d24d6033..da6add73 100644 --- a/multiboot2/src/lib.rs +++ b/multiboot2/src/lib.rs @@ -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); diff --git a/multiboot2/src/memory_map.rs b/multiboot2/src/memory_map.rs index d99d3920..602b3809 100644 --- a/multiboot2/src/memory_map.rs +++ b/multiboot2/src/memory_map.rs @@ -37,7 +37,7 @@ impl MemoryMapTag { #[cfg(feature = "builder")] #[must_use] pub fn new(areas: &[MemoryArea]) -> Box { - let header = TagHeader::new(Self::ID, 0); + let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */); let entry_size = (size_of::() as u32).to_ne_bytes(); let entry_version = 0_u32.to_ne_bytes(); let areas = { @@ -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 { - 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(); diff --git a/multiboot2/src/module.rs b/multiboot2/src/module.rs index 3c2aa975..1a472373 100644 --- a/multiboot2/src/module.rs +++ b/multiboot2/src/module.rs @@ -25,7 +25,7 @@ impl ModuleTag { #[cfg(feature = "builder")] #[must_use] pub fn new(start: u32, end: u32, cmdline: &str) -> Box { - 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(); @@ -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 } } diff --git a/multiboot2/src/network.rs b/multiboot2/src/network.rs index 772dfbd1..e7ad425c 100644 --- a/multiboot2/src/network.rs +++ b/multiboot2/src/network.rs @@ -20,7 +20,7 @@ impl NetworkTag { #[cfg(feature = "builder")] #[must_use] pub fn new(dhcp_pack: &[u8]) -> Box { - let header = TagHeader::new(Self::ID, 0); + let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */); new_boxed(header, &[dhcp_pack]) } } diff --git a/multiboot2/src/rsdp.rs b/multiboot2/src/rsdp.rs index 59121d0f..cf59d8a8 100644 --- a/multiboot2/src/rsdp.rs +++ b/multiboot2/src/rsdp.rs @@ -48,8 +48,6 @@ impl RsdpV1Tag { /// Signature of RSDP v1. pub const SIGNATURE: [u8; 8] = *b"RSD PTR "; - const BASE_SIZE: usize = size_of::() + 16 + 4; - /// Constructs a new tag. #[must_use] pub fn new(oem_id: [u8; 6], revision: u8, rsdt_address: u32) -> Self { @@ -117,7 +115,7 @@ impl RsdpV1Tag { unsafe impl MaybeDynSized for RsdpV1Tag { type Header = TagHeader; - const BASE_SIZE: usize = size_of::(); + const BASE_SIZE: usize = size_of::() + 16 + 4; } impl Tag for RsdpV1Tag { @@ -147,9 +145,6 @@ impl RsdpV2Tag { /// Signature of RSDP v2. pub const SIGNATURE: [u8; 8] = *b"RSD PTR "; - const BASE_SIZE: usize = - size_of::() + 16 + 2 * size_of::() + size_of::() + 4; - /// Constructs a new tag. #[must_use] pub fn new( @@ -211,12 +206,12 @@ impl RsdpV2Tag { // SAFETY: `self` is a valid reference, and we only read the // initialized raw representation of the fixed-size layout. let bytes = - unsafe { slice::from_raw_parts((self as *const Self).cast::(), size_of::()) }; - let length = self.length as usize; - if length != Self::BASE_SIZE - size_of::() { + unsafe { slice::from_raw_parts((self as *const Self).cast::(), Self::BASE_SIZE) }; + let rsdp_length = self.length as usize; + if rsdp_length != Self::BASE_SIZE - size_of::() { return false; } - let ext_end = size_of::() + length; + let ext_end = size_of::() + rsdp_length; if ext_end > bytes.len() { return false; } @@ -255,7 +250,10 @@ impl RsdpV2Tag { unsafe impl MaybeDynSized for RsdpV2Tag { type Header = TagHeader; - const BASE_SIZE: usize = size_of::(); + // Spec size (44), excluding the trailing padding that `size_of::()` + // (48) would add. + const BASE_SIZE: usize = + size_of::() + 16 + 2 * size_of::() + size_of::() + 4; } impl Tag for RsdpV2Tag { @@ -268,6 +266,17 @@ impl Tag for RsdpV2Tag { mod tests { use super::*; + /// The tags must report the spec-mandated size, not the padded Rust type + /// size. The trailing padding is uninitialized memory for + /// stack-constructed values and must never be read. + #[test] + fn base_size_excludes_trailing_padding() { + assert_eq!(::BASE_SIZE, 28); + assert_eq!(size_of::(), 32); + assert_eq!(::BASE_SIZE, 44); + assert_eq!(size_of::(), 48); + } + #[test] fn v1_new_computes_valid_checksum() { let tag = RsdpV1Tag::new(*b"ABCDEF", 1, 0x1234_5678); diff --git a/multiboot2/src/smbios.rs b/multiboot2/src/smbios.rs index d0786512..347dc073 100644 --- a/multiboot2/src/smbios.rs +++ b/multiboot2/src/smbios.rs @@ -23,7 +23,7 @@ impl SmbiosTag { #[cfg(feature = "builder")] #[must_use] pub fn new(major: u8, minor: u8, tables: &[u8]) -> Box { - let header = TagHeader::new(Self::ID, 0); + let header = TagHeader::new(Self::ID, 0 /* filled by new_boxed */); let reserved = [0, 0, 0, 0, 0, 0]; new_boxed(header, &[&[major, minor], &reserved, tables]) } diff --git a/multiboot2/src/tag.rs b/multiboot2/src/tag.rs index 4dfc9fbe..5ccf8e8f 100644 --- a/multiboot2/src/tag.rs +++ b/multiboot2/src/tag.rs @@ -40,6 +40,6 @@ unsafe impl Header for TagHeader { } fn set_size(&mut self, total_size: usize) { - self.size = total_size as u32 + self.size = u32::try_from(total_size).unwrap() } } diff --git a/multiboot2/src/vbe_info.rs b/multiboot2/src/vbe_info.rs index fefc659a..933da18b 100644 --- a/multiboot2/src/vbe_info.rs +++ b/multiboot2/src/vbe_info.rs @@ -216,11 +216,17 @@ pub struct VBEModeInfo { /// Bytes per scan line pub pitch: u16, - /// Horizontal and vertical resolution in pixels or characters. - pub resolution: (u16, u16), + /// Horizontal resolution in pixels or characters. + pub x_resolution: u16, - /// Character cell width and height in pixels. - pub character_size: (u8, u8), + /// Vertical resolution in pixels or characters. + pub y_resolution: u16, + + /// Character cell width in pixels. + pub x_char_size: u8, + + /// Character cell height in pixels. + pub y_char_size: u8, /// Number of memory planes. pub number_of_planes: u8, @@ -293,8 +299,10 @@ impl fmt::Debug for VBEModeInfo { .field("window_b_segment", &{ self.window_b_segment }) .field("window_function_ptr", &{ self.window_function_ptr }) .field("pitch", &{ self.pitch }) - .field("resolution", &{ self.resolution }) - .field("character_size", &self.character_size) + .field("x_resolution", &{ self.x_resolution }) + .field("y_resolution", &{ self.y_resolution }) + .field("x_char_size", &self.x_char_size) + .field("y_char_size", &self.y_char_size) .field("number_of_planes", &self.number_of_planes) .field("bpp", &self.bpp) .field("number_of_banks", &self.number_of_banks) @@ -325,8 +333,10 @@ impl Default for VBEModeInfo { window_b_segment: 0, window_function_ptr: 0, pitch: 0, - resolution: (0, 0), - character_size: (0, 0), + x_resolution: 0, + y_resolution: 0, + x_char_size: 0, + y_char_size: 0, number_of_planes: 0, bpp: 0, number_of_banks: 0,