diff --git a/multiboot2-common/CHANGELOG.md b/multiboot2-common/CHANGELOG.md index f627a209..17130847 100644 --- a/multiboot2-common/CHANGELOG.md +++ b/multiboot2-common/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Added the `raw_type!` macro that generates an ABI-safe `#[repr(transparent)]` + newtype plus a corresponding high-level open-set enum, including all + conversions between them and the underlying integer. + ## v0.5.0 (2026-08-24) - Fixed undefined behavior in `DynSizedStructure::cast`: the target size is now diff --git a/multiboot2-common/src/lib.rs b/multiboot2-common/src/lib.rs index 0c039964..d3c076f6 100644 --- a/multiboot2-common/src/lib.rs +++ b/multiboot2-common/src/lib.rs @@ -255,6 +255,7 @@ pub mod test_utils; mod boxed; mod bytes_ref; mod iter; +mod raw; mod tag; #[cfg(feature = "alloc")] diff --git a/multiboot2-common/src/raw.rs b/multiboot2-common/src/raw.rs new file mode 100644 index 00000000..6265b929 --- /dev/null +++ b/multiboot2-common/src/raw.rs @@ -0,0 +1,309 @@ +//! Module for the `raw_type` macro. + +/// Defines a pair of an ABI-compatible raw newtype and a corresponding +/// convenient, high-level open-set enum, along with all conversions between +/// the newtype, the enum, and the underlying integer. +/// +/// The newtype behaves like the plain integer (`Copy`, comparisons, and +/// conversions) but carries the semantics of the enum: [`Debug`] prints the +/// variant name together with the raw value (e.g. `Foo(0)`) and [`Display`] +/// prints just the variant name (e.g. `Foo`); values without a specified +/// semantic print as `Custom(x)`. It is safe to use in `#[repr(C)]` +/// structures parsed from raw memory, as every bit pattern is valid for it. The enum assigns each specified value to a +/// variant; all other values are mapped to the automatically added `Custom` +/// variant, which carries the raw integer. By convention, the newtype +/// carries the name of the enum plus a `Raw` suffix. +/// +/// [`Debug`]: core::fmt::Debug +/// [`Display`]: core::fmt::Display +/// +/// # Example +/// +/// ``` +/// multiboot2_common::raw_type! { +/// /// ABI compatible representation of a demo type. +/// pub struct DemoTypeRaw(u32); +/// +/// /// The type of a demo item. +/// /// +/// /// This is a higher level abstraction for [`DemoTypeRaw`]. +/// pub enum DemoType { +/// /// The first defined type. +/// Foo = 0, +/// /// The second defined type. +/// Bar = 1, +/// } +/// } +/// +/// let raw = DemoTypeRaw::new(1); +/// assert_eq!(raw, DemoType::Bar); +/// assert_eq!(DemoType::from(DemoTypeRaw::new(42)), DemoType::Custom(42)); +/// ``` +#[macro_export] +macro_rules! raw_type { + ( + $(#[$raw_attr:meta])* + $raw_vis:vis struct $Raw:ident($int:ty); + + $(#[$enum_attr:meta])* + $enum_vis:vis enum $Enum:ident { + $( + $(#[$variant_attr:meta])* + $Variant:ident = $value:literal, + )+ + } + ) => { + $(#[$raw_attr])* + #[repr(transparent)] + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + $raw_vis struct $Raw($int); + + impl $Raw { + /// Constructs a new instance from the raw binary value. + #[must_use] + pub const fn new(val: $int) -> Self { + Self(val) + } + + /// Returns the raw binary value. + #[must_use] + pub const fn get(self) -> $int { + self.0 + } + } + + impl ::core::fmt::Debug for $Raw { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Debug::fmt(&$Enum::from_val(self.0), f) + } + } + + impl ::core::fmt::Display for $Raw { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Display::fmt(&$Enum::from_val(self.0), f) + } + } + + $(#[$enum_attr])* + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + $enum_vis enum $Enum { + $( + $(#[$variant_attr])* + $Variant, + )+ + /// Value without a specified semantic in the specification. + Custom($int), + } + + impl $Enum { + /// Returns the raw binary value. + #[must_use] + pub const fn val(self) -> $int { + match self { + $(Self::$Variant => $value,)+ + Self::Custom(val) => val, + } + } + + /// Constructs the variant corresponding to the raw binary value. + /// + /// Values without a specified semantic are mapped to + /// [`Self::Custom`]. + #[must_use] + pub const fn from_val(val: $int) -> Self { + match val { + $($value => Self::$Variant,)+ + val => Self::Custom(val), + } + } + } + + impl ::core::fmt::Debug for $Enum { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + $(Self::$Variant => f.debug_tuple(stringify!($Variant)).field(&$value).finish(),)+ + Self::Custom(val) => f.debug_tuple("Custom").field(val).finish(), + } + } + } + + impl ::core::fmt::Display for $Enum { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + $(Self::$Variant => f.write_str(stringify!($Variant)),)+ + Self::Custom(val) => write!(f, "Custom({val})"), + } + } + } + + impl ::core::convert::From<$int> for $Raw { + fn from(val: $int) -> Self { + Self::new(val) + } + } + + impl ::core::convert::From<$Raw> for $int { + fn from(raw: $Raw) -> Self { + raw.get() + } + } + + impl ::core::convert::From<$int> for $Enum { + fn from(val: $int) -> Self { + Self::from_val(val) + } + } + + impl ::core::convert::From<$Enum> for $int { + fn from(val: $Enum) -> Self { + val.val() + } + } + + impl ::core::convert::From<$Raw> for $Enum { + fn from(raw: $Raw) -> Self { + Self::from_val(raw.get()) + } + } + + impl ::core::convert::From<$Enum> for $Raw { + fn from(val: $Enum) -> Self { + Self::new(val.val()) + } + } + + impl ::core::cmp::PartialEq<$Enum> for $Raw { + fn eq(&self, other: &$Enum) -> bool { + self.0 == other.val() + } + } + + impl ::core::cmp::PartialEq<$Raw> for $Enum { + fn eq(&self, other: &$Raw) -> bool { + self.val() == other.0 + } + } + + impl ::core::cmp::PartialEq<$int> for $Raw { + fn eq(&self, other: &$int) -> bool { + self.0 == *other + } + } + + impl ::core::cmp::PartialEq<$Raw> for $int { + fn eq(&self, other: &$Raw) -> bool { + *self == other.0 + } + } + + impl ::core::cmp::PartialEq<$int> for $Enum { + fn eq(&self, other: &$int) -> bool { + self.val() == *other + } + } + + impl ::core::cmp::PartialEq<$Enum> for $int { + fn eq(&self, other: &$Enum) -> bool { + *self == other.val() + } + } + }; +} + +#[cfg(test)] +mod tests { + use std::collections::{BTreeSet, HashSet}; + + crate::raw_type! { + /// ABI compatible representation of a test type. + pub struct TestRaw(u16); + + /// The type of a test item. + /// + /// This is a higher level abstraction for [`TestRaw`]. + pub enum TestType { + /// The first defined value. + Foo = 0, + /// A defined value with a gap to the previous one. + Bar = 42, + } + } + + // The newtype must be binary compatible with the underlying integer. + const _: () = assert!(size_of::() == size_of::()); + const _: () = assert!(align_of::() == align_of::()); + + #[test] + fn test_const_constructors_and_getters() { + const RAW: TestRaw = TestRaw::new(42); + const VAL: u16 = RAW.get(); + const TYP: TestType = TestType::from_val(VAL); + assert_eq!(VAL, 42); + assert_eq!(TYP, TestType::Bar); + assert_eq!(TYP.val(), 42); + } + + /// Every raw value must be constructible and must round-trip through + /// the newtype and the enum, including values unknown to the + /// specification. + #[test] + fn test_roundtrip() { + for val in [0_u16, 42, 1337, u16::MAX] { + let raw = TestRaw::from(val); + let typ = TestType::from(raw); + assert_eq!(u16::from(raw), val); + assert_eq!(u16::from(typ), val); + assert_eq!(TestRaw::from(typ), raw); + } + } + + #[test] + fn test_from_val() { + assert_eq!(TestType::from_val(0), TestType::Foo); + assert_eq!(TestType::from_val(42), TestType::Bar); + assert_eq!(TestType::from_val(7), TestType::Custom(7)); + assert_eq!(TestType::Foo.val(), 0); + assert_eq!(TestType::Bar.val(), 42); + assert_eq!(TestType::Custom(7).val(), 7); + } + + /// All three representations must be comparable with each other. + #[test] + fn test_partial_eq() { + assert_eq!(TestRaw::new(42), TestType::Bar); + assert_eq!(TestType::Bar, TestRaw::new(42)); + assert_eq!(TestRaw::new(42), 42); + assert_eq!(42, TestRaw::new(42)); + assert_eq!(TestType::Bar, 42); + assert_eq!(42, TestType::Bar); + assert_eq!(TestRaw::new(7), TestType::Custom(7)); + assert_ne!(TestRaw::new(0), TestType::Bar); + } + + /// The types must debug-print the semantic of their value together + /// with the raw value. + #[test] + fn test_debug() { + assert_eq!(format!("{:?}", TestRaw::new(0)), "Foo(0)"); + assert_eq!(format!("{:?}", TestRaw::new(7)), "Custom(7)"); + assert_eq!(format!("{:?}", TestType::Bar), "Bar(42)"); + } + + /// The types must display-print just the semantic of their value. + #[test] + fn test_display() { + assert_eq!(format!("{}", TestRaw::new(0)), "Foo"); + assert_eq!(format!("{}", TestRaw::new(7)), "Custom(7)"); + assert_eq!(format!("{}", TestType::Bar), "Bar"); + } + + /// Both types must be usable in ordered and hashed collections. + #[test] + fn test_ord_and_hash() { + let set = BTreeSet::from([TestType::Bar, TestType::Foo, TestType::Bar]); + assert!(set.iter().zip(set.iter().skip(1)).all(|(a, b)| a < b)); + + let set = HashSet::from([TestRaw::new(0), TestRaw::new(1)]); + assert_eq!(set.len(), 2); + } +} diff --git a/multiboot2-common/src/test_utils.rs b/multiboot2-common/src/test_utils.rs index c534eefc..5cd9d4c1 100644 --- a/multiboot2-common/src/test_utils.rs +++ b/multiboot2-common/src/test_utils.rs @@ -15,6 +15,8 @@ use core::ops::Deref; #[repr(C, align(8))] pub struct AlignedBytes(pub [u8; N]); +const _: () = assert!(align_of::>() == crate::ALIGNMENT); + impl AlignedBytes { /// Creates a new type. #[must_use] @@ -116,14 +118,10 @@ impl Tag for DummyDstTag { #[cfg(test)] mod tests { - use crate::ALIGNMENT; - use super::*; #[test] fn abi() { - assert_eq!(align_of::>(), ALIGNMENT); - let bytes = AlignedBytes([0]); assert_eq!(bytes.as_ptr().align_offset(8), 0); assert_eq!((&raw const bytes[0]).align_offset(8), 0); diff --git a/multiboot2-header/CHANGELOG.md b/multiboot2-header/CHANGELOG.md index c1e6dad2..df07893a 100644 --- a/multiboot2-header/CHANGELOG.md +++ b/multiboot2-header/CHANGELOG.md @@ -2,6 +2,33 @@ ## Unreleased +- **Breaking:** `InformationRequestHeaderTag::new()` now takes + `&[MbiTagType]` and `requests()` returns an iterator over `MbiTagType`. + The `MbiTagTypeId` re-export was renamed to `MbiTagTypeRaw`. +- Fixed undefined behavior when parsing a header containing a tag with a type + unknown to the specification. `HeaderTagHeader` now stores the new + `HeaderTagTypeRaw` newtype; the `typ()` getters keep returning + `HeaderTagType`, which gained a `Custom` variant. **Breaking:** the now + meaningless `HeaderTagType::count()` was removed. +- Fixed undefined behavior when parsing a header with an architecture unknown + to the specification. `Multiboot2BasicHeader` now stores the new + `HeaderTagISARaw` newtype; the `arch()` getters keep returning + `HeaderTagISA`, which gained a `Custom` variant. +- Fixed undefined behavior when parsing a `RelocatableHeaderTag` with a + placement preference unknown to the specification. The tag now stores the + new `RelocatableHeaderTagPreferenceRaw` newtype; `preference()` keeps + returning `RelocatableHeaderTagPreference`, which gained a `Custom` variant. +- Fixed undefined behavior when parsing a header tag whose flags field holds a + value other than 0 or 1. `HeaderTagHeader` now stores the new + `HeaderTagFlagRaw` newtype; the `flags()` getters keep returning + `HeaderTagFlag`, which gained a `Custom` variant. +- Fixed undefined behavior and a spec violation in `ConsoleHeaderTagFlags`: + the enum discriminants did not match the specification. **Breaking:** + `ConsoleRequired` now serializes to `1` (was `0`) and `EgaTextSupported` to + `2` (was `1`), matching the example C code of the specification; the enum + gained a `Custom` variant and the tag stores the new + `ConsoleHeaderTagFlagsRaw` newtype. + ## v0.10.0 (2026-08-24) - Fixed `Header::load` rejecting a valid header whose end tag has a non-zero diff --git a/multiboot2-header/examples/minimal.rs b/multiboot2-header/examples/minimal.rs index b9c9032a..1675a7d6 100644 --- a/multiboot2-header/examples/minimal.rs +++ b/multiboot2-header/examples/minimal.rs @@ -18,10 +18,7 @@ fn main() { )) .information_request_tag(InformationRequestHeaderTag::new( HeaderTagFlag::Required, - &[ - MbiTagType::Cmdline.into(), - MbiTagType::BootLoaderName.into(), - ], + &[MbiTagType::Cmdline, MbiTagType::BootLoaderName], )) .build(); diff --git a/multiboot2-header/src/address.rs b/multiboot2-header/src/address.rs index 50f54ff1..543423cb 100644 --- a/multiboot2-header/src/address.rs +++ b/multiboot2-header/src/address.rs @@ -42,6 +42,8 @@ pub struct AddressHeaderTag { bss_end_addr: u32, } +const _: () = assert!(size_of::() == 2 + 2 + 4 + 4 + 4 + 4 + 4); + impl AddressHeaderTag { /// Constructs a new tag. #[must_use] @@ -115,13 +117,3 @@ impl Tag for AddressHeaderTag { type IDType = HeaderTagType; const ID: HeaderTagType = HeaderTagType::Address; } - -#[cfg(test)] -mod tests { - use crate::AddressHeaderTag; - - #[test] - fn test_assert_size() { - assert_eq!(size_of::(), 2 + 2 + 4 + 4 + 4 + 4 + 4); - } -} diff --git a/multiboot2-header/src/builder.rs b/multiboot2-header/src/builder.rs index 405003ed..183f2d35 100644 --- a/multiboot2-header/src/builder.rs +++ b/multiboot2-header/src/builder.rs @@ -176,28 +176,28 @@ mod tests { .information_request_tag(InformationRequestHeaderTag::new( Optional, &[ - MbiTagType::Cmdline.into(), - MbiTagType::BootLoaderName.into(), - MbiTagType::Module.into(), - MbiTagType::BasicMeminfo.into(), - MbiTagType::Bootdev.into(), - MbiTagType::Mmap.into(), - MbiTagType::Vbe.into(), - MbiTagType::Framebuffer.into(), - MbiTagType::ElfSections.into(), - MbiTagType::Apm.into(), - MbiTagType::Efi32.into(), - MbiTagType::Efi64.into(), - MbiTagType::Smbios.into(), - MbiTagType::AcpiV1.into(), - MbiTagType::AcpiV2.into(), - MbiTagType::Network.into(), - MbiTagType::EfiMmap.into(), - MbiTagType::EfiBs.into(), - MbiTagType::Efi32Ih.into(), - MbiTagType::Efi64Ih.into(), - MbiTagType::LoadBaseAddr.into(), - MbiTagType::Custom(0x1337).into(), + MbiTagType::Cmdline, + MbiTagType::BootLoaderName, + MbiTagType::Module, + MbiTagType::BasicMeminfo, + MbiTagType::Bootdev, + MbiTagType::Mmap, + MbiTagType::Vbe, + MbiTagType::Framebuffer, + MbiTagType::ElfSections, + MbiTagType::Apm, + MbiTagType::Efi32, + MbiTagType::Efi64, + MbiTagType::Smbios, + MbiTagType::AcpiV1, + MbiTagType::AcpiV2, + MbiTagType::Network, + MbiTagType::EfiMmap, + MbiTagType::EfiBs, + MbiTagType::Efi32Ih, + MbiTagType::Efi64Ih, + MbiTagType::LoadBaseAddr, + MbiTagType::Custom(0x1337), ], )) .address_tag(AddressHeaderTag::new( diff --git a/multiboot2-header/src/console.rs b/multiboot2-header/src/console.rs index 3f649420..6b773978 100644 --- a/multiboot2-header/src/console.rs +++ b/multiboot2-header/src/console.rs @@ -1,14 +1,24 @@ use crate::{HeaderTagFlag, HeaderTagHeader, HeaderTagType}; use multiboot2_common::{MaybeDynSized, Tag}; -/// Possible flags for [`ConsoleHeaderTag`]. -#[repr(u32)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ConsoleHeaderTagFlags { - /// Console required. - ConsoleRequired = 0, - /// EGA text support. - EgaTextSupported = 1, +multiboot2_common::raw_type! { + /// ABI compatible representation of the console flags of the + /// [`ConsoleHeaderTag`]. + /// + /// This type matches the binary representation (`u32`). + pub struct ConsoleHeaderTagFlagsRaw(u32); + + /// The console flags of the [`ConsoleHeaderTag`]. + /// + /// This is a higher level abstraction for [`ConsoleHeaderTagFlagsRaw`]. + pub enum ConsoleHeaderTagFlags { + /// At least one of the consoles supported by the bootloader must be + /// present and information about it must be available in the boot + /// information. + ConsoleRequired = 1, + /// The OS image has EGA text support. + EgaTextSupported = 2, + } } /// Tells that a console must be available in MBI. @@ -17,7 +27,7 @@ pub enum ConsoleHeaderTagFlags { #[repr(C, align(8))] pub struct ConsoleHeaderTag { header: HeaderTagHeader, - console_flags: ConsoleHeaderTagFlags, + console_flags: ConsoleHeaderTagFlagsRaw, } impl ConsoleHeaderTag { @@ -28,7 +38,7 @@ impl ConsoleHeaderTag { HeaderTagHeader::new(HeaderTagType::ConsoleFlags, flags, Self::BASE_SIZE as u32); Self { header, - console_flags, + console_flags: ConsoleHeaderTagFlagsRaw::new(console_flags.val()), } } @@ -53,7 +63,7 @@ impl ConsoleHeaderTag { /// Returns the [`ConsoleHeaderTagFlags`]. #[must_use] pub const fn console_flags(&self) -> ConsoleHeaderTagFlags { - self.console_flags + ConsoleHeaderTagFlags::from_val(self.console_flags.get()) } } @@ -67,3 +77,43 @@ impl Tag for ConsoleHeaderTag { type IDType = HeaderTagType; const ID: HeaderTagType = HeaderTagType::ConsoleFlags; } + +#[cfg(test)] +mod tests { + use super::*; + use crate::GenericHeaderTag; + use core::borrow::Borrow; + use multiboot2_common::test_utils::AlignedBytes; + + /// The console flag values must match the values mandated by the + /// specification. + #[test] + fn console_flags_match_spec_values() { + assert_eq!(ConsoleHeaderTagFlags::ConsoleRequired.val(), 1); + assert_eq!(ConsoleHeaderTagFlags::EgaTextSupported.val(), 2); + } + + /// A tag with a console flags value unknown to the specification must be + /// parsable without undefined behavior. + #[test] + fn unknown_console_flags_are_not_ub() { + #[rustfmt::skip] + let bytes = AlignedBytes::new([ + /* typ = console flags */ + 4, 0, + /* flags */ + 0, 0, + /* size */ + 12, 0, 0, 0, + /* console_flags = 3 (unknown) */ + 3, 0, 0, 0, + /* padding to alignment */ + 0, 0, 0, 0, + ]); + let tag = GenericHeaderTag::ref_from_slice(bytes.borrow()) + .unwrap() + .cast::(); + + assert_eq!(tag.console_flags(), ConsoleHeaderTagFlags::Custom(3)); + } +} diff --git a/multiboot2-header/src/end.rs b/multiboot2-header/src/end.rs index 42c1f7e5..a868d0d2 100644 --- a/multiboot2-header/src/end.rs +++ b/multiboot2-header/src/end.rs @@ -8,6 +8,8 @@ pub struct EndHeaderTag { header: HeaderTagHeader, } +const _: () = assert!(size_of::() == 2 + 2 + 4); + impl Default for EndHeaderTag { fn default() -> Self { Self::new() @@ -55,13 +57,3 @@ impl Tag for EndHeaderTag { type IDType = HeaderTagType; const ID: HeaderTagType = HeaderTagType::End; } - -#[cfg(test)] -mod tests { - use crate::EndHeaderTag; - - #[test] - fn test_assert_size() { - assert_eq!(size_of::(), 2 + 2 + 4); - } -} diff --git a/multiboot2-header/src/header.rs b/multiboot2-header/src/header.rs index 5245acdf..7739ee45 100644 --- a/multiboot2-header/src/header.rs +++ b/multiboot2-header/src/header.rs @@ -1,8 +1,8 @@ use crate::{ AddressHeaderTag, ConsoleHeaderTag, EfiBootServiceHeaderTag, EntryAddressHeaderTag, EntryEfi32HeaderTag, EntryEfi64HeaderTag, FramebufferHeaderTag, HeaderTagHeader, HeaderTagISA, - HeaderTagType, InformationRequestHeaderTag, ModuleAlignHeaderTag, RelocatableHeaderTag, - TagIter, + HeaderTagISARaw, HeaderTagType, InformationRequestHeaderTag, ModuleAlignHeaderTag, + RelocatableHeaderTag, TagIter, }; use core::fmt::{Debug, Formatter}; use core::ptr::NonNull; @@ -78,7 +78,7 @@ impl<'a> Header<'a> { // The spec only requires the end tag to have type 0 and size 8; it // does not constrain the flags field. - typ == HeaderTagType::End as u16 && size == size_of::() + typ == HeaderTagType::End.val() && size == size_of::() }) } @@ -355,13 +355,15 @@ pub enum LoadError { pub struct Multiboot2BasicHeader { /// Must be the value of [`MAGIC`]. header_magic: u32, - arch: HeaderTagISA, + arch: HeaderTagISARaw, length: u32, checksum: u32, // Followed by dynamic amount of dynamically sized header tags. // At minimum, the end tag. } +const _: () = assert!(size_of::() == 4 + 4 + 4 + 4); + impl Multiboot2BasicHeader { #[cfg(feature = "builder")] /// Constructor for the basic header. @@ -370,7 +372,7 @@ impl Multiboot2BasicHeader { let checksum = Self::calc_checksum(magic, arch, length); Self { header_magic: magic, - arch, + arch: HeaderTagISARaw::new(arch.val()), length, checksum, } @@ -386,7 +388,7 @@ impl Multiboot2BasicHeader { u32, /* expected checksum */ ), > { - let check = Self::calc_checksum(self.header_magic, self.arch, self.length); + let check = Self::calc_checksum(self.header_magic, self.arch(), self.length); if check == self.checksum { Ok(()) } else { @@ -397,7 +399,7 @@ impl Multiboot2BasicHeader { /// Calculates the checksum as described in the spec. #[must_use] pub const fn calc_checksum(magic: u32, arch: HeaderTagISA, length: u32) -> u32 { - (0x100000000 - magic as u64 - arch as u64 - length as u64) as u32 + (0x100000000 - magic as u64 - arch.val() as u64 - length as u64) as u32 } /// Returns the header magic. @@ -409,7 +411,7 @@ impl Multiboot2BasicHeader { /// Returns the [`HeaderTagISA`]. #[must_use] pub const fn arch(&self) -> HeaderTagISA { - self.arch + HeaderTagISA::from_val(self.arch.get()) } /// Returns the length. @@ -432,7 +434,7 @@ impl DynSizedHeader for Multiboot2BasicHeader { fn set_size(&mut self, total_size: usize) { self.length = total_size as u32; - self.checksum = Self::calc_checksum(self.header_magic, self.arch, total_size as u32); + self.checksum = Self::calc_checksum(self.header_magic, self.arch(), total_size as u32); } } @@ -461,7 +463,7 @@ mod tests { // Aligned magic buffer[0..4].copy_from_slice(&MAGIC.to_le_bytes()); // Architecture - buffer[4..8].copy_from_slice(&(HeaderTagISA::I386 as u32).to_le_bytes()); + buffer[4..8].copy_from_slice(&HeaderTagISA::I386.val().to_le_bytes()); // Total size buffer[8..12].copy_from_slice(&24_u32.to_le_bytes()); // Checksum @@ -474,11 +476,6 @@ mod tests { buffer[20..24].copy_from_slice(&8_u32.to_le_bytes()); } - #[test] - fn test_assert_size() { - assert_eq!(size_of::(), 4 + 4 + 4 + 4); - } - #[test] fn find_header_handles_short_buffers() { let bytes = AlignedBytes::new([0; 16]); @@ -552,6 +549,72 @@ mod tests { assert!(header.is_ok()); } + /// A tag with a flags value unknown to the specification must be + /// parsable without undefined behavior, preserving the value. + #[test] + fn load_accepts_unknown_flags_value() { + use crate::HeaderTagFlag; + + let mut bytes = AlignedBytes::new([0; 24]); + write_minimal_valid_header_tag(&mut bytes.0); + // End tag: flags value without a specified meaning. + bytes.0[18..20].copy_from_slice(&0xFFFF_u16.to_le_bytes()); + + // SAFETY: The test buffer is aligned and contains a valid + // header layout. + let header = unsafe { Header::load(bytes.as_ptr().cast()) }.unwrap(); + + let flags = header.iter().next().unwrap().header().flags(); + assert_eq!(flags, HeaderTagFlag::Custom(0xFFFF)); + } + + /// A header with an architecture unknown to the specification must be + /// parsable without undefined behavior. + #[test] + fn load_accepts_unknown_architecture() { + let mut bytes = AlignedBytes::new([0; 24]); + write_minimal_valid_header_tag(&mut bytes.0); + // Architecture unknown to the specification. + bytes.0[4..8].copy_from_slice(&3_u32.to_le_bytes()); + let checksum = Multiboot2BasicHeader::calc_checksum(MAGIC, HeaderTagISA::Custom(3), 24); + bytes.0[12..16].copy_from_slice(&checksum.to_le_bytes()); + + // SAFETY: The test buffer is aligned and contains a valid + // header layout. + let header = unsafe { Header::load(bytes.as_ptr().cast()) }.unwrap(); + + assert_eq!(header.arch(), HeaderTagISA::Custom(3)); + // The Debug implementation must also cope with unknown values. + let debug = format!("{header:?}"); + assert!(debug.contains("Custom(3)")); + } + + /// A header containing a tag with a type unknown to the specification + /// must be parsable and iterable without undefined behavior. + #[test] + fn load_accepts_unknown_tag_type() { + let mut bytes = AlignedBytes::new([0; 32]); + let checksum = Multiboot2BasicHeader::calc_checksum(MAGIC, HeaderTagISA::I386, 32); + bytes.0[0..4].copy_from_slice(&MAGIC.to_le_bytes()); + bytes.0[8..12].copy_from_slice(&32_u32.to_le_bytes()); + bytes.0[12..16].copy_from_slice(&checksum.to_le_bytes()); + // Tag with a type unknown to the specification. + bytes.0[16..18].copy_from_slice(&0x1337_u16.to_le_bytes()); + bytes.0[20..24].copy_from_slice(&8_u32.to_le_bytes()); + // End tag. + bytes.0[28..32].copy_from_slice(&8_u32.to_le_bytes()); + + // SAFETY: The test buffer is aligned and contains a valid + // header layout. + let header = unsafe { Header::load(bytes.as_ptr().cast()) }.unwrap(); + + let tag = header.iter().next().unwrap(); + assert_eq!(tag.header().typ(), HeaderTagType::Custom(0x1337)); + // The Debug implementation must also cope with unknown types. + let debug = format!("{header:?}"); + assert!(debug.contains("Custom(4919)")); + } + #[test] fn load_rejects_missing_end_tag() { let mut bytes = AlignedBytes::new([0; 16]); @@ -572,10 +635,10 @@ mod tests { let mut bytes = AlignedBytes::new([0; 32]); let checksum = Multiboot2BasicHeader::calc_checksum(MAGIC, HeaderTagISA::I386, 32); bytes.0[0..4].copy_from_slice(&MAGIC.to_le_bytes()); - bytes.0[4..8].copy_from_slice(&(HeaderTagISA::I386 as u32).to_le_bytes()); + bytes.0[4..8].copy_from_slice(&HeaderTagISA::I386.val().to_le_bytes()); bytes.0[8..12].copy_from_slice(&32_u32.to_le_bytes()); bytes.0[12..16].copy_from_slice(&checksum.to_le_bytes()); - bytes.0[16..18].copy_from_slice(&(HeaderTagType::InformationRequest as u16).to_le_bytes()); + bytes.0[16..18].copy_from_slice(&HeaderTagType::InformationRequest.val().to_le_bytes()); bytes.0[20..24].copy_from_slice(&24_u32.to_le_bytes()); // SAFETY: The test buffer is aligned and contains a valid diff --git a/multiboot2-header/src/information_request.rs b/multiboot2-header/src/information_request.rs index 3a1eee06..7404a72e 100644 --- a/multiboot2-header/src/information_request.rs +++ b/multiboot2-header/src/information_request.rs @@ -1,12 +1,12 @@ -use crate::{HeaderTagFlag, HeaderTagHeader}; -use crate::{HeaderTagType, MbiTagTypeId}; +use crate::{HeaderTagFlag, HeaderTagHeader, HeaderTagType}; +use crate::{MbiTagType, MbiTagTypeRaw}; use core::fmt; use core::fmt::{Debug, Formatter}; #[cfg(feature = "builder")] use multiboot2_common::new_boxed; use multiboot2_common::{MaybeDynSized, Tag}; #[cfg(feature = "builder")] -use {alloc::boxed::Box, core::slice}; +use {alloc::boxed::Box, alloc::vec::Vec}; /// Specifies which tag types the bootloader should provide /// inside the mbi. @@ -14,21 +14,20 @@ use {alloc::boxed::Box, core::slice}; #[repr(C, align(8))] pub struct InformationRequestHeaderTag { header: HeaderTagHeader, - requests: [MbiTagTypeId], + requests: [MbiTagTypeRaw], } impl InformationRequestHeaderTag { /// Creates a new object. #[cfg(feature = "builder")] #[must_use] - pub fn new(flags: HeaderTagFlag, requests: &[MbiTagTypeId]) -> Box { + pub fn new(flags: HeaderTagFlag, requests: &[MbiTagType]) -> Box { let header = HeaderTagHeader::new(HeaderTagType::InformationRequest, flags, 0); - // SAFETY: The memory we are using is valid. - let requests = unsafe { - let ptr = &raw const *requests; - slice::from_raw_parts(ptr.cast::(), size_of_val(requests)) - }; - new_boxed(header, &[requests]) + let requests = requests + .iter() + .flat_map(|request| request.val().to_ne_bytes()) + .collect::>(); + new_boxed(header, &[requests.as_slice()]) } /// Returns the [`HeaderTagType`]. @@ -49,10 +48,19 @@ impl InformationRequestHeaderTag { self.header.size() } - /// Returns the requests as array - #[must_use] - pub const fn requests(&self) -> &[MbiTagTypeId] { - &self.requests + /// Returns an iterator over the requested tag types. + pub fn requests(&self) -> impl Iterator + '_ { + self.requests.iter().map(|&raw| MbiTagType::from(raw)) + } +} + +/// Debug-formats the requests of an [`InformationRequestHeaderTag`] as a list +/// of [`MbiTagType`]. +struct RequestsDebug<'a>(&'a InformationRequestHeaderTag); + +impl Debug for RequestsDebug<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.requests()).finish() } } @@ -62,7 +70,7 @@ impl Debug for InformationRequestHeaderTag { .field("type", &self.typ()) .field("flags", &self.flags()) .field("size", &self.size()) - .field("requests", &self.requests()) + .field("requests", &RequestsDebug(self)) .finish() } } @@ -74,8 +82,8 @@ impl MaybeDynSized for InformationRequestHeaderTag { fn dst_len(header: &Self::Header) -> Self::Metadata { let dst_size = header.size() as usize - Self::BASE_SIZE; - assert_eq!(dst_size % size_of::(), 0); - dst_size / size_of::() + assert_eq!(dst_size % size_of::(), 0); + dst_size / size_of::() } } @@ -88,37 +96,35 @@ impl Tag for InformationRequestHeaderTag { #[cfg(feature = "builder")] mod tests { use super::*; - use crate::MbiTagType; #[test] fn creation() { - // Main objective here is to satisfy Miri. - let _ir = InformationRequestHeaderTag::new( - HeaderTagFlag::Optional, - &[ - MbiTagType::Cmdline.into(), - MbiTagType::BootLoaderName.into(), - MbiTagType::Module.into(), - MbiTagType::BasicMeminfo.into(), - MbiTagType::Bootdev.into(), - MbiTagType::Mmap.into(), - MbiTagType::Vbe.into(), - MbiTagType::Framebuffer.into(), - MbiTagType::ElfSections.into(), - MbiTagType::Apm.into(), - MbiTagType::Efi32.into(), - MbiTagType::Efi64.into(), - MbiTagType::Smbios.into(), - MbiTagType::AcpiV1.into(), - MbiTagType::AcpiV2.into(), - MbiTagType::Network.into(), - MbiTagType::EfiMmap.into(), - MbiTagType::EfiBs.into(), - MbiTagType::Efi32Ih.into(), - MbiTagType::Efi64Ih.into(), - MbiTagType::LoadBaseAddr.into(), - MbiTagType::Custom(0x1337).into(), - ], - ); + let requests = [ + MbiTagType::Cmdline, + MbiTagType::BootLoaderName, + MbiTagType::Module, + MbiTagType::BasicMeminfo, + MbiTagType::Bootdev, + MbiTagType::Mmap, + MbiTagType::Vbe, + MbiTagType::Framebuffer, + MbiTagType::ElfSections, + MbiTagType::Apm, + MbiTagType::Efi32, + MbiTagType::Efi64, + MbiTagType::Smbios, + MbiTagType::AcpiV1, + MbiTagType::AcpiV2, + MbiTagType::Network, + MbiTagType::EfiMmap, + MbiTagType::EfiBs, + MbiTagType::Efi32Ih, + MbiTagType::Efi64Ih, + MbiTagType::LoadBaseAddr, + MbiTagType::Custom(0x1337), + ]; + // Statement also is a good test for Miri. + let ir = InformationRequestHeaderTag::new(HeaderTagFlag::Optional, &requests); + assert!(ir.requests().eq(requests.iter().copied())); } } diff --git a/multiboot2-header/src/lib.rs b/multiboot2-header/src/lib.rs index 2cb76cf3..3a8c4ab3 100644 --- a/multiboot2-header/src/lib.rs +++ b/multiboot2-header/src/lib.rs @@ -103,4 +103,4 @@ pub use self::uefi_bs::*; pub use builder::Builder; /// Re-export of [`multiboot2::TagType`] from `multiboot2`-crate. -pub use multiboot2::{TagType as MbiTagType, TagTypeId as MbiTagTypeId}; +pub use multiboot2::{TagType as MbiTagType, TagTypeRaw as MbiTagTypeRaw}; diff --git a/multiboot2-header/src/module_align.rs b/multiboot2-header/src/module_align.rs index 2055d7b7..ce0c43c6 100644 --- a/multiboot2-header/src/module_align.rs +++ b/multiboot2-header/src/module_align.rs @@ -8,6 +8,8 @@ pub struct ModuleAlignHeaderTag { header: HeaderTagHeader, } +const _: () = assert!(size_of::() == 2 + 2 + 4); + impl ModuleAlignHeaderTag { /// Constructs a new tag. #[must_use] @@ -46,13 +48,3 @@ impl Tag for ModuleAlignHeaderTag { type IDType = HeaderTagType; const ID: HeaderTagType = HeaderTagType::ModuleAlign; } - -#[cfg(test)] -mod tests { - use crate::ModuleAlignHeaderTag; - - #[test] - fn test_assert_size() { - assert_eq!(size_of::(), 2 + 2 + 4); - } -} diff --git a/multiboot2-header/src/relocatable.rs b/multiboot2-header/src/relocatable.rs index 5fba477a..9c7b7eb8 100644 --- a/multiboot2-header/src/relocatable.rs +++ b/multiboot2-header/src/relocatable.rs @@ -3,18 +3,27 @@ use core::fmt; use core::fmt::{Debug, Formatter}; use multiboot2_common::{MaybeDynSized, Tag}; -/// Specifies the bootloader's preferred placement for a relocatable image. -#[repr(u32)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum RelocatableHeaderTagPreference { - /// Let the bootloader choose the image location. - None = 0, - /// Load the image at the lowest possible address that is not below - /// `min_addr`. - Low = 1, - /// Load the image at the highest possible address that does not end above - /// `max_addr`. - High = 2, +multiboot2_common::raw_type! { + /// ABI compatible representation of the placement preference of the + /// relocatable header tag. + /// + /// This type matches the binary representation (`u32`). + pub struct RelocatableHeaderTagPreferenceRaw(u32); + + /// The bootloader's preferred placement for a relocatable image. + /// + /// This is a higher level abstraction for + /// [`RelocatableHeaderTagPreferenceRaw`]. + pub enum RelocatableHeaderTagPreference { + /// Let the bootloader choose the image location. + None = 0, + /// Load the image at the lowest possible address that is not below + /// `min_addr`. + Low = 1, + /// Load the image at the highest possible address that does not end above + /// `max_addr`. + High = 2, + } } /// This tag indicates that the image is relocatable. @@ -32,9 +41,11 @@ pub struct RelocatableHeaderTag { max_addr: u32, /// Image alignment in memory, e.g. 4096. align: u32, - preference: RelocatableHeaderTagPreference, + preference: RelocatableHeaderTagPreferenceRaw, } +const _: () = assert!(size_of::() == 2 + 2 + 4 + 4 + 4 + 4 + 4); + impl RelocatableHeaderTag { /// Constructs a new tag. #[must_use] @@ -52,7 +63,7 @@ impl RelocatableHeaderTag { min_addr, max_addr, align, - preference, + preference: RelocatableHeaderTagPreferenceRaw::new(preference.val()), } } @@ -95,7 +106,7 @@ impl RelocatableHeaderTag { /// Return the preference. #[must_use] pub const fn preference(&self) -> RelocatableHeaderTagPreference { - self.preference + RelocatableHeaderTagPreference::from_val(self.preference.get()) } } @@ -127,10 +138,37 @@ impl Tag for RelocatableHeaderTag { #[cfg(test)] mod tests { - use crate::RelocatableHeaderTag; + use super::*; + use crate::GenericHeaderTag; + use core::borrow::Borrow; + use multiboot2_common::test_utils::AlignedBytes; + /// A tag with a placement preference unknown to the specification must + /// be parsable without undefined behavior. #[test] - fn test_assert_size() { - assert_eq!(size_of::(), 2 + 2 + 4 + 4 + 4 + 4 + 4); + fn unknown_preference_is_not_ub() { + #[rustfmt::skip] + let bytes = AlignedBytes::new([ + /* typ = relocatable */ + 10, 0, + /* flags */ + 0, 0, + /* size */ + 24, 0, 0, 0, + /* min_addr, max_addr, align */ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + /* preference = 99 (unknown) */ + 99, 0, 0, 0, + ]); + let tag = GenericHeaderTag::ref_from_slice(bytes.borrow()) + .unwrap() + .cast::(); + + assert_eq!(tag.preference(), RelocatableHeaderTagPreference::Custom(99)); + // The Debug implementation must also cope with unknown values. + let debug = format!("{tag:?}"); + assert!(debug.contains("Custom(99)")); } } diff --git a/multiboot2-header/src/tags.rs b/multiboot2-header/src/tags.rs index f330214f..87fdab8c 100644 --- a/multiboot2-header/src/tags.rs +++ b/multiboot2-header/src/tags.rs @@ -1,71 +1,86 @@ -//! Definition for all types of "Multiboot2 header tags". The values are taken from the example C -//! code at the end of the official Multiboot2 spec. These tags follow in memory right after -//! [`crate::Multiboot2BasicHeader`]. +//! Definition for all types of "Multiboot2 header tags". These tags follow in +//! memory right after [`crate::Multiboot2BasicHeader`]. use multiboot2_common::Header; -/// ISA/ARCH in Multiboot2 header. -#[repr(u32)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum HeaderTagISA { - /// Spec: "means 32-bit (protected) mode of i386". - /// Caution: This is confusing. If you use the EFI64-tag - /// on an UEFI system, the machine will boot into `64-bit long mode`. - /// Therefore this tag should be understood as "arch=x86|x86_64". - I386 = 0, - /// 32-bit MIPS - MIPS32 = 4, -} +multiboot2_common::raw_type! { + /// ABI compatible representation of the ISA/ARCH of a Multiboot2 header. + /// + /// This type matches the binary representation (`u32`) and stands in the + /// `arch` property of [`crate::Multiboot2BasicHeader`]. + pub struct HeaderTagISARaw(u32); -/// Possible types for header tags of a Multiboot2 header. -/// -/// The names and values are taken from the example C code at the bottom of the -/// Multiboot2 specification. This value stands in the `typ` property of -/// [`HeaderTagHeader`]. -#[repr(u16)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum HeaderTagType { - /// Type for [`crate::EndHeaderTag`]. - End = 0, - /// Type for [`crate::InformationRequestHeaderTag`]. - InformationRequest = 1, - /// Type for [`crate::AddressHeaderTag`]. - Address = 2, - /// Type for [`crate::EntryAddressHeaderTag`]. - EntryAddress = 3, - /// Type for [`crate::ConsoleHeaderTag`]. - ConsoleFlags = 4, - /// Type for [`crate::FramebufferHeaderTag`]. - Framebuffer = 5, - /// Type for [`crate::ModuleAlignHeaderTag`]. - ModuleAlign = 6, - /// Type for [`crate::EfiBootServiceHeaderTag`]. - EfiBS = 7, - /// Type for [`crate::EntryEfi32HeaderTag`]. - EntryAddressEFI32 = 8, - /// Type for [`crate::EntryEfi64HeaderTag`]. - EntryAddressEFI64 = 9, - /// Type for [`crate::RelocatableHeaderTag`]. - Relocatable = 10, + /// The ISA/ARCH a Multiboot2 image targets. + /// + /// This is a higher level abstraction for [`HeaderTagISARaw`]. + pub enum HeaderTagISA { + /// Spec: "means 32-bit (protected) mode of i386". + /// Caution: This is confusing. If you use the EFI64-tag + /// on an UEFI system, the machine will boot into `64-bit long mode`. + /// Therefore this tag should be understood as "arch=x86|x86_64". + I386 = 0, + /// 32-bit MIPS + MIPS32 = 4, + } } -impl HeaderTagType { - /// Returns the number of possible variants. - #[must_use] - pub const fn count() -> u32 { - 11 +multiboot2_common::raw_type! { + /// ABI compatible representation of the type of a Multiboot2 header tag. + /// + /// This type matches the binary representation (`u16`) and stands in the + /// `typ` property of [`HeaderTagHeader`]. + pub struct HeaderTagTypeRaw(u16); + + /// The type of a Multiboot2 header tag. + /// + /// This is a higher level abstraction for [`HeaderTagTypeRaw`]. + pub enum HeaderTagType { + /// Type for [`crate::EndHeaderTag`]. + End = 0, + /// Type for [`crate::InformationRequestHeaderTag`]. + InformationRequest = 1, + /// Type for [`crate::AddressHeaderTag`]. + Address = 2, + /// Type for [`crate::EntryAddressHeaderTag`]. + EntryAddress = 3, + /// Type for [`crate::ConsoleHeaderTag`]. + ConsoleFlags = 4, + /// Type for [`crate::FramebufferHeaderTag`]. + Framebuffer = 5, + /// Type for [`crate::ModuleAlignHeaderTag`]. + ModuleAlign = 6, + /// Type for [`crate::EfiBootServiceHeaderTag`]. + EfiBS = 7, + /// Type for [`crate::EntryEfi32HeaderTag`]. + EntryAddressEFI32 = 8, + /// Type for [`crate::EntryEfi64HeaderTag`]. + EntryAddressEFI64 = 9, + /// Type for [`crate::RelocatableHeaderTag`]. + Relocatable = 10, } } -/// Flags for Multiboot2 header tags. -#[repr(u16)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum HeaderTagFlag { - /// The bootloader must provide this tag. If this is not possible, the - /// bootloader will fail to load the kernel. - Required = 0, - /// The bootloader should provide the tag if possible. - Optional = 1, +multiboot2_common::raw_type! { + /// ABI compatible representation of the flags of a Multiboot2 header + /// tag. + /// + /// This type matches the binary representation (`u16`) and stands in the + /// `flags` property of [`HeaderTagHeader`]. + pub struct HeaderTagFlagRaw(u16); + + /// Flags of a Multiboot2 header tag. + /// + /// These flags tell whether a tag is required or optional for the + /// bootloader. + /// + /// This is a higher level abstraction for [`HeaderTagFlagRaw`]. + pub enum HeaderTagFlag { + /// The bootloader must provide this tag. If this is not possible, the + /// bootloader will fail to load the kernel. + Required = 0, + /// The bootloader should provide the tag if possible. + Optional = 1, + } } /// The common header that all header tags share. Specific tags may have @@ -73,29 +88,35 @@ pub enum HeaderTagFlag { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(C, align(8))] pub struct HeaderTagHeader { - typ: HeaderTagType, /* u16 */ - flags: HeaderTagFlag, /* u16 */ + typ: HeaderTagTypeRaw, /* u16 */ + flags: HeaderTagFlagRaw, /* u16 */ size: u32, // Followed by optional additional tag-specific fields. } +const _: () = assert!(size_of::() == 2 + 2 + 4); + impl HeaderTagHeader { /// Creates a new header. #[must_use] pub const fn new(typ: HeaderTagType, flags: HeaderTagFlag, size: u32) -> Self { - Self { typ, flags, size } + Self { + typ: HeaderTagTypeRaw::new(typ.val()), + flags: HeaderTagFlagRaw::new(flags.val()), + size, + } } /// Returns the [`HeaderTagType`]. #[must_use] pub const fn typ(&self) -> HeaderTagType { - self.typ + HeaderTagType::from_val(self.typ.get()) } /// Returns the [`HeaderTagFlag`]s. #[must_use] pub const fn flags(&self) -> HeaderTagFlag { - self.flags + HeaderTagFlag::from_val(self.flags.get()) } /// Returns the size. @@ -114,13 +135,3 @@ impl Header for HeaderTagHeader { self.size = total_size as u32; } } - -#[cfg(test)] -mod tests { - use crate::HeaderTagHeader; - - #[test] - fn test_assert_size() { - assert_eq!(size_of::(), 2 + 2 + 4); - } -} diff --git a/multiboot2-header/src/uefi_bs.rs b/multiboot2-header/src/uefi_bs.rs index 241f62a9..29a88787 100644 --- a/multiboot2-header/src/uefi_bs.rs +++ b/multiboot2-header/src/uefi_bs.rs @@ -9,6 +9,8 @@ pub struct EfiBootServiceHeaderTag { header: HeaderTagHeader, } +const _: () = assert!(size_of::() == 2 + 2 + 4); + impl EfiBootServiceHeaderTag { /// Constructs a new tag. #[must_use] @@ -46,13 +48,3 @@ impl Tag for EfiBootServiceHeaderTag { type IDType = HeaderTagType; const ID: HeaderTagType = HeaderTagType::EfiBS; } - -#[cfg(test)] -mod tests { - use crate::EfiBootServiceHeaderTag; - - #[test] - fn test_assert_size() { - assert_eq!(size_of::(), 2 + 2 + 4); - } -} diff --git a/multiboot2/CHANGELOG.md b/multiboot2/CHANGELOG.md index d19e4a8a..e8697e0c 100644 --- a/multiboot2/CHANGELOG.md +++ b/multiboot2/CHANGELOG.md @@ -2,6 +2,21 @@ ## Unreleased +- **Breaking:** Renamed `TagTypeId` to `TagTypeRaw`; it is now generated by the + `raw_type!` macro from `multiboot2-common` and gained more conversions. + `TagHeader::new()` now takes `impl Into`. +- **Breaking:** Renamed `MemoryAreaTypeId` to `MemoryAreaTypeRaw` (also + generated by `raw_type!`). `MemoryArea::typ()` now returns `MemoryAreaType` + and `MemoryArea::new()` takes `impl Into`. +- Fixed undefined behavior when parsing a `FramebufferTag` with a framebuffer + type unknown to the specification. **Breaking:** the `FramebufferTypeId` enum + was replaced by the `FramebufferTypeRaw` newtype and the open-set + `FramebufferKind` enum; `UnknownFramebufferType` is now exported. +- Fixed undefined behavior when reading a `VBEModeInfo` with a reserved or + OEM-defined memory model. **Breaking:** the `memory_model` field is now + typed as the new `VBEMemoryModelRaw` newtype; `VBEMemoryModel` gained a + `Custom` variant. + ## v0.26.0 / v0.26.1 (2026-08-24) - Fixed `BootdevTag` and `ApmTag` reporting a tag size that included Rust struct diff --git a/multiboot2/src/boot_information.rs b/multiboot2/src/boot_information.rs index d01826d8..3d7ab125 100644 --- a/multiboot2/src/boot_information.rs +++ b/multiboot2/src/boot_information.rs @@ -372,7 +372,7 @@ impl<'a> BootInformation<'a> { /// /// ```no_run /// use std::mem; - /// use multiboot2::{BootInformation, BootInformationHeader, parse_slice_as_string, StringError, TagHeader, TagType, TagTypeId}; /// + /// use multiboot2::{BootInformation, BootInformationHeader, parse_slice_as_string, StringError, TagHeader, TagType, TagTypeRaw}; /// /// use multiboot2_common::{MaybeDynSized, Tag}; /// /// #[repr(C)] diff --git a/multiboot2/src/boot_loader_name.rs b/multiboot2/src/boot_loader_name.rs index e7967f3c..0cc61f08 100644 --- a/multiboot2/src/boot_loader_name.rs +++ b/multiboot2/src/boot_loader_name.rs @@ -99,7 +99,7 @@ mod tests { use multiboot2_common::test_utils::AlignedBytes; #[rustfmt::skip] - fn get_bytes() -> AlignedBytes<16> { + const fn get_bytes() -> AlignedBytes<16> { AlignedBytes::new([ TagType::BootLoaderName.val() as u8, 0, 0, 0, 14, 0, 0, 0, diff --git a/multiboot2/src/command_line.rs b/multiboot2/src/command_line.rs index 3678e596..e54d4f0c 100644 --- a/multiboot2/src/command_line.rs +++ b/multiboot2/src/command_line.rs @@ -93,7 +93,7 @@ mod tests { use multiboot2_common::test_utils::AlignedBytes; #[rustfmt::skip] - fn get_bytes() -> AlignedBytes<16> { + const fn get_bytes() -> AlignedBytes<16> { AlignedBytes::new([ TagType::Cmdline.val() as u8, 0, 0, 0, 14, 0, 0, 0, diff --git a/multiboot2/src/framebuffer.rs b/multiboot2/src/framebuffer.rs index 31029b31..dc86a491 100644 --- a/multiboot2/src/framebuffer.rs +++ b/multiboot2/src/framebuffer.rs @@ -81,16 +81,12 @@ pub struct FramebufferTag { /// Contains number of bits per pixel. bpp: u8, - /// The type of framebuffer. See [`FramebufferTypeId`]. - // TODO: Strictly speaking this causes UB for invalid values. However, no - // sane bootloader puts something illegal there at the moment. When we - // refactor this (newtype pattern?), we should also streamline other - // parts in the code base accordingly. - framebuffer_type: FramebufferTypeId, + /// The type of framebuffer. See [`FramebufferKind`]. + framebuffer_type: FramebufferTypeRaw, _padding: u16, - /// This optional data and its meaning depend on the [`FramebufferTypeId`]. + /// This optional data and its meaning depends on [`FramebufferTypeRaw`]. buffer: [u8], } @@ -122,7 +118,7 @@ impl FramebufferTag { &width, &height, &[bpp], - &[buffer_type_id as u8], + &[buffer_type_id.get()], &padding, &optional_buffer, ], @@ -167,13 +163,8 @@ impl FramebufferTag { pub fn buffer_type(&self) -> Result, UnknownFramebufferType> { let mut reader = Reader::new(&self.buffer); - // TODO: We should use the newtype pattern instead or so to properly - // solve this. - let fb_type_raw = self.framebuffer_type as u8; - let fb_type = FramebufferTypeId::try_from(fb_type_raw)?; - - match fb_type { - FramebufferTypeId::Indexed => { + match FramebufferKind::from(self.framebuffer_type) { + FramebufferKind::Indexed => { // TODO we can create a struct for this and implement // DynSizedStruct for it to leverage the already existing // functionality @@ -197,7 +188,7 @@ impl FramebufferTag { }; Ok(FramebufferType::Indexed { palette }) } - FramebufferTypeId::RGB => { + FramebufferKind::RGB => { let red_pos = reader.read_next_u8(); // These refer to the bit positions of the LSB of each field let red_mask = reader.read_next_u8(); // And then the length of the field from LSB to MSB let green_pos = reader.read_next_u8(); @@ -219,7 +210,8 @@ impl FramebufferTag { }, }) } - FramebufferTypeId::Text => Ok(FramebufferType::Text), + FramebufferKind::Text => Ok(FramebufferType::Text), + FramebufferKind::Custom(val) => Err(UnknownFramebufferType(val)), } } } @@ -273,37 +265,34 @@ impl PartialEq for FramebufferTag { } } -/// ABI-compatible framebuffer type. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(u8)] -#[expect(clippy::upper_case_acronyms)] -pub enum FramebufferTypeId { - Indexed = 0, - RGB = 1, - Text = 2, - // spec says: there may be more variants in the future -} - -impl TryFrom for FramebufferTypeId { - type Error = UnknownFramebufferType; +multiboot2_common::raw_type! { + /// ABI compatible representation of the framebuffer type of the + /// framebuffer tag. + /// + /// This type matches the binary representation (`u8`). + pub struct FramebufferTypeRaw(u8); - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::Indexed), - 1 => Ok(Self::RGB), - 2 => Ok(Self::Text), - val => Err(UnknownFramebufferType(val)), - } + /// The kind of framebuffer described by the framebuffer tag according to + /// the Multiboot2 spec. + /// + /// This is a higher level abstraction for [`FramebufferTypeRaw`]. Unlike + /// [`FramebufferType`], it only describes the kind of framebuffer and + /// not its payload. + #[allow(clippy::upper_case_acronyms)] + pub enum FramebufferKind { + /// Indexed color. + Indexed = 0, + /// Direct RGB color. + RGB = 1, + /// EGA Text. + Text = 2, + // spec says: there may be more variants in the future } } -impl From> for FramebufferTypeId { +impl From> for FramebufferTypeRaw { fn from(value: FramebufferType) -> Self { - match value { - FramebufferType::Indexed { .. } => Self::Indexed, - FramebufferType::RGB { .. } => Self::RGB, - FramebufferType::Text => Self::Text, - } + value.id() } } @@ -336,13 +325,13 @@ pub enum FramebufferType<'a> { impl FramebufferType<'_> { #[must_use] - #[cfg(feature = "builder")] - const fn id(&self) -> FramebufferTypeId { - match self { - FramebufferType::Indexed { .. } => FramebufferTypeId::Indexed, - FramebufferType::RGB { .. } => FramebufferTypeId::RGB, - FramebufferType::Text => FramebufferTypeId::Text, - } + const fn id(&self) -> FramebufferTypeRaw { + let kind = match self { + FramebufferType::Indexed { .. } => FramebufferKind::Indexed, + FramebufferType::RGB { .. } => FramebufferKind::RGB, + FramebufferType::Text => FramebufferKind::Text, + }; + FramebufferTypeRaw::new(kind.val()) } #[must_use] @@ -404,7 +393,9 @@ pub struct FramebufferColor { pub blue: u8, } -/// Error when an unknown [`FramebufferTypeId`] is found. +const _: () = assert!(size_of::() == 3); + +/// Error when an unknown framebuffer type is found. #[derive(Debug, Copy, Clone, PartialEq, Eq, Error)] #[error("Unknown framebuffer type {0}")] pub struct UnknownFramebufferType(u8); @@ -416,12 +407,6 @@ mod tests { use core::borrow::Borrow; use multiboot2_common::test_utils::AlignedBytes; - // Compile time test - #[test] - fn test_size() { - assert_eq!(size_of::(), 3) - } - #[test] #[cfg(feature = "builder")] fn create_new() { @@ -478,6 +463,36 @@ mod tests { dbg!(tag); } + /// A tag with a framebuffer type unknown to the specification must be + /// parsable without undefined behavior and report the unknown type as + /// an error. + #[test] + fn unknown_framebuffer_type_is_not_ub() { + #[rustfmt::skip] + let bytes = AlignedBytes::new([ + /* typ = framebuffer */ + 8, 0, 0, 0, + /* size = base size */ + 32, 0, 0, 0, + /* address */ + 0, 0, 0, 0, 0, 0, 0, 0, + /* pitch, width, height */ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + /* bpp, type = 0x40 (unknown), padding */ + 0, 0x40, 0, 0, + ]); + let tag = GenericInfoTag::ref_from_slice(bytes.borrow()) + .unwrap() + .cast::(); + + assert_eq!(tag.buffer_type(), Err(UnknownFramebufferType(0x40))); + // The Debug implementation must also cope with unknown values. + let debug = format!("{tag:?}"); + assert!(debug.contains("UnknownFramebufferType")); + } + #[test] #[should_panic(expected = "indexed framebuffer palette must fit in the tag")] fn indexed_palette_must_fit_in_tag() { diff --git a/multiboot2/src/lib.rs b/multiboot2/src/lib.rs index 545f50a2..c3db6997 100644 --- a/multiboot2/src/lib.rs +++ b/multiboot2/src/lib.rs @@ -107,11 +107,14 @@ pub use elf_sections::{ ElfSectionExt, ElfSectionFlags, ElfSectionIter, ElfSectionType, ElfSectionsTag, }; pub use end::EndTag; -pub use framebuffer::{FramebufferColor, FramebufferField, FramebufferTag, FramebufferType}; +pub use framebuffer::{ + FramebufferColor, FramebufferField, FramebufferKind, FramebufferTag, FramebufferType, + FramebufferTypeRaw, UnknownFramebufferType, +}; pub use image_load_addr::ImageLoadPhysAddrTag; pub use memory_map::{ BasicMemoryInfoTag, EFIMemoryAreaType, EFIMemoryAttribute, EFIMemoryDesc, EFIMemoryMapTag, - MemoryArea, MemoryAreaType, MemoryAreaTypeId, MemoryMapTag, + MemoryArea, MemoryAreaType, MemoryAreaTypeRaw, MemoryMapTag, }; pub use module::{ModuleIter, ModuleTag}; pub use network::NetworkTag; @@ -119,11 +122,11 @@ pub use ptr_meta::Pointee; pub use rsdp::{RsdpV1Tag, RsdpV2Tag}; pub use smbios::SmbiosTag; pub use tag::TagHeader; -pub use tag_type::{TagType, TagTypeId}; +pub use tag_type::{TagType, TagTypeRaw}; pub use util::{StringError, parse_slice_as_string}; pub use vbe_info::{ VBECapabilities, VBEControlInfo, VBEDirectColorAttributes, VBEField, VBEInfoTag, - VBEMemoryModel, VBEModeAttributes, VBEModeInfo, VBEWindowAttributes, + VBEMemoryModel, VBEMemoryModelRaw, VBEModeAttributes, VBEModeInfo, VBEWindowAttributes, }; /// Magic number that a Multiboot2-compliant bootloader will use to identify @@ -1233,7 +1236,7 @@ mod tests { fn get_repeated_custom_tags_from_mbi() { #[repr(C, align(8))] struct CustomTag { - tag: TagTypeId, + tag: TagTypeRaw, size: u32, foo: u32, } @@ -1329,7 +1332,7 @@ mod tests { #[repr(C)] #[derive(crate::Pointee)] struct CustomTag { - tag: TagTypeId, + tag: TagTypeRaw, size: u32, name: [u8], } @@ -1408,7 +1411,7 @@ mod tests { assert_eq!(tag.name(), Ok("hello")); } - /// Tests that `get_tag` can consume multiple types that implement `Into` + /// Tests that `get_tag` can consume multiple types that implement `Into` #[test] fn get_tag_into_variants() { let bytes = AlignedBytes([ diff --git a/multiboot2/src/memory_map.rs b/multiboot2/src/memory_map.rs index 4f2a385f..6b68d86d 100644 --- a/multiboot2/src/memory_map.rs +++ b/multiboot2/src/memory_map.rs @@ -6,7 +6,7 @@ pub use uefi_raw::table::boot::MemoryDescriptor as EFIMemoryDesc; pub use uefi_raw::table::boot::MemoryType as EFIMemoryAreaType; use crate::tag::TagHeader; -use crate::{TagType, TagTypeId}; +use crate::{TagType, TagTypeRaw}; use core::fmt::{Debug, Formatter}; use core::marker::PhantomData; use multiboot2_common::{MaybeDynSized, Tag}; @@ -99,17 +99,17 @@ impl Tag for MemoryMapTag { pub struct MemoryArea { base_addr: u64, length: u64, - typ: MemoryAreaTypeId, + typ: MemoryAreaTypeRaw, _reserved: u32, } impl MemoryArea { /// Create a new MemoryArea. - pub fn new(base_addr: u64, length: u64, typ: impl Into) -> Self { + pub fn new(base_addr: u64, length: u64, typ: impl Into) -> Self { Self { base_addr, length, - typ: typ.into(), + typ: MemoryAreaTypeRaw::new(typ.into().val()), _reserved: 0, } } @@ -134,8 +134,8 @@ impl MemoryArea { /// The type of the memory region. #[must_use] - pub const fn typ(&self) -> MemoryAreaTypeId { - self.typ + pub const fn typ(&self) -> MemoryAreaType { + MemoryAreaType::from_val(self.typ.get()) } } @@ -149,99 +149,37 @@ impl Debug for MemoryArea { } } -/// ABI-friendly version of [`MemoryAreaType`]. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(C)] -pub struct MemoryAreaTypeId(u32); - -impl From for MemoryAreaTypeId { - fn from(value: u32) -> Self { - Self(value) - } -} - -impl From for u32 { - fn from(value: MemoryAreaTypeId) -> Self { - value.0 - } -} - -impl Debug for MemoryAreaTypeId { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - let mt = MemoryAreaType::from(*self); - Debug::fmt(&mt, f) - } -} - -/// Abstraction over defined memory types for the memory map as well as custom -/// ones. Types 1 to 5 are defined in the Multiboot2 spec and correspond to the -/// entry types of e820 memory maps. -/// -/// This is not binary compatible with the Multiboot2 spec. Please use -/// [`MemoryAreaTypeId`] instead. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum MemoryAreaType { - /// Available memory free to be used by the OS. - Available, /* 1 */ - - /// A reserved area that must not be used. - Reserved, /* 2, */ - - /// Usable memory holding ACPI information. - AcpiAvailable, /* 3, */ +multiboot2_common::raw_type! { + /// ABI compatible representation of the type of a memory area. + /// + /// This type matches the binary representation (`u32`). + pub struct MemoryAreaTypeRaw(u32); - /// Reserved memory which needs to be preserved on hibernation. - /// Also called NVS in spec, which stands for "Non-Volatile Sleep/Storage", - /// which is part of ACPI specification. - ReservedHibernate, /* 4, */ + /// The type of a memory area of the memory map. + /// + /// Types 1 to 5 are defined in the Multiboot2 spec and correspond to the + /// entry types of e820 memory maps; other values are mapped to + /// [`MemoryAreaType::Custom`]. + /// + /// This is a higher level abstraction for [`MemoryAreaTypeRaw`] and not + /// binary compatible with the Multiboot2 spec. + pub enum MemoryAreaType { + /// Available memory free to be used by the OS. + Available = 1, - /// Memory which is occupied by defective RAM modules. - Defective, /* = 5, */ + /// A reserved area that must not be used. + Reserved = 2, - /// Custom memory map type. - Custom(u32), -} + /// Usable memory holding ACPI information. + AcpiAvailable = 3, -impl From for MemoryAreaType { - fn from(value: MemoryAreaTypeId) -> Self { - match value.0 { - 1 => Self::Available, - 2 => Self::Reserved, - 3 => Self::AcpiAvailable, - 4 => Self::ReservedHibernate, - 5 => Self::Defective, - val => Self::Custom(val), - } - } -} - -impl From for MemoryAreaTypeId { - fn from(value: MemoryAreaType) -> Self { - let integer = match value { - MemoryAreaType::Available => 1, - MemoryAreaType::Reserved => 2, - MemoryAreaType::AcpiAvailable => 3, - MemoryAreaType::ReservedHibernate => 4, - MemoryAreaType::Defective => 5, - MemoryAreaType::Custom(val) => val, - }; - integer.into() - } -} - -impl PartialEq for MemoryAreaTypeId { - fn eq(&self, other: &MemoryAreaType) -> bool { - let val: Self = (*other).into(); - let val: u32 = val.0; - self.0.eq(&val) - } -} + /// Reserved memory which needs to be preserved on hibernation. + /// Also called NVS in spec, which stands for "Non-Volatile Sleep/Storage", + /// which is part of ACPI specification. + ReservedHibernate = 4, -impl PartialEq for MemoryAreaType { - fn eq(&self, other: &MemoryAreaTypeId) -> bool { - let val: MemoryAreaTypeId = (*self).into(); - let val: u32 = val.0; - other.0.eq(&val) + /// Memory which is occupied by defective RAM modules. + Defective = 5, } } @@ -407,7 +345,7 @@ impl Debug for EFIMemoryMapTag { impl MaybeDynSized for EFIMemoryMapTag { type Header = TagHeader; - const BASE_SIZE: usize = size_of::() + 3 * size_of::(); + const BASE_SIZE: usize = size_of::() + 3 * size_of::(); fn dst_len(header: &TagHeader) -> usize { assert!(header.size as usize >= Self::BASE_SIZE); diff --git a/multiboot2/src/module.rs b/multiboot2/src/module.rs index 5052e246..8c57f498 100644 --- a/multiboot2/src/module.rs +++ b/multiboot2/src/module.rs @@ -139,7 +139,7 @@ mod tests { use multiboot2_common::test_utils::AlignedBytes; #[rustfmt::skip] - fn get_bytes() -> AlignedBytes<24> { + const fn get_bytes() -> AlignedBytes<24> { AlignedBytes::new([ TagType::Module.val() as u8, 0, 0, 0, 22, 0, 0, 0, diff --git a/multiboot2/src/network.rs b/multiboot2/src/network.rs index d7d77f9b..e264f292 100644 --- a/multiboot2/src/network.rs +++ b/multiboot2/src/network.rs @@ -1,6 +1,6 @@ //! Module for [`NetworkTag`]. -use crate::{TagHeader, TagType, TagTypeId}; +use crate::{TagHeader, TagType, TagTypeRaw}; use multiboot2_common::{MaybeDynSized, Tag}; use ptr_meta::Pointee; #[cfg(feature = "builder")] @@ -10,7 +10,7 @@ use {alloc::boxed::Box, multiboot2_common::new_boxed}; #[derive(Debug, Pointee)] #[repr(C, align(8))] pub struct NetworkTag { - typ: TagTypeId, + typ: TagTypeRaw, size: u32, dhcpack: [u8], } diff --git a/multiboot2/src/smbios.rs b/multiboot2/src/smbios.rs index ee158608..ec85f63c 100644 --- a/multiboot2/src/smbios.rs +++ b/multiboot2/src/smbios.rs @@ -83,7 +83,7 @@ mod tests { use multiboot2_common::test_utils::AlignedBytes; #[rustfmt::skip] - fn get_bytes() -> AlignedBytes<32> { + const fn get_bytes() -> AlignedBytes<32> { AlignedBytes::new([ TagType::Smbios.val() as u8, 0, 0, 0, 25, 0, 0, 0, diff --git a/multiboot2/src/tag.rs b/multiboot2/src/tag.rs index 574be7b2..9f489718 100644 --- a/multiboot2/src/tag.rs +++ b/multiboot2/src/tag.rs @@ -1,6 +1,6 @@ //! Module for the base tag definition [`TagHeader`]. -use crate::TagTypeId; +use crate::{TagType, TagTypeRaw}; use core::fmt::Debug; use multiboot2_common::Header; @@ -16,7 +16,7 @@ pub struct TagHeader { /// The ABI-compatible [`TagType`]. /// /// [`TagType`]: crate::TagType - pub typ: TagTypeId, /* u32 */ + pub typ: TagTypeRaw, /* u32 */ /// The total size of the tag including the header. pub size: u32, // Followed by optional additional tag specific fields. @@ -24,9 +24,9 @@ pub struct TagHeader { impl TagHeader { /// Creates a new header. - pub fn new(typ: impl Into, size: u32) -> Self { + pub fn new(typ: impl Into, size: u32) -> Self { Self { - typ: typ.into(), + typ: TagTypeRaw::new(typ.into().val()), size, } } diff --git a/multiboot2/src/tag_type.rs b/multiboot2/src/tag_type.rs index e19b8d2c..b2247fa1 100644 --- a/multiboot2/src/tag_type.rs +++ b/multiboot2/src/tag_type.rs @@ -1,368 +1,118 @@ //! Module for tag types. //! -//! The relevant exports of this module are [`TagTypeId`] and [`TagType`]. +//! The relevant exports of this module are [`TagTypeRaw`] and [`TagType`]. -use core::fmt::{Debug, Formatter}; -use core::hash::Hash; - -/// Serialized form of [`TagType`] that matches the binary representation -/// (`u32`). -/// -/// The abstraction corresponds to the `typ`/`type` field of a Multiboot2 -/// [`TagHeader`]. This type can easily be created from or converted to -/// [`TagType`]. -/// -/// [`TagHeader`]: crate::TagHeader -#[repr(transparent)] -#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Ord, Hash)] -pub struct TagTypeId(u32); - -impl TagTypeId { - /// Constructor. - #[must_use] - pub const fn new(val: u32) -> Self { - Self(val) - } -} - -impl Debug for TagTypeId { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - let tag_type = TagType::from(*self); - Debug::fmt(&tag_type, f) - } -} - -/// Higher level abstraction for [`TagTypeId`] that assigns each possible value -/// to a specific semantic according to the specification. -/// -/// Additionally, it allows to use the [`TagType::Custom`] variant. It is -/// **not binary compatible** with [`TagTypeId`]. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum TagType { - /// Tag `0`: Marks the end of the tags. - End, - /// Tag `1`: Additional command line string. - /// For example `''` or `'--my-custom-option foo --provided by_grub`, if - /// your GRUB config contains `multiboot2 /boot/multiboot2-binary.elf --my-custom-option foo --provided by_grub` - Cmdline, - /// Tag `2`: Name of the bootloader, e.g. 'GRUB 2.04-1ubuntu44.2' - BootLoaderName, - /// Tag `3`: Additional Multiboot modules, which are BLOBs provided in - /// memory. For example an initial ram disk with essential drivers. - Module, - /// Tag `4`: `mem_lower` and `mem_upper` indicate the amount of lower and - /// upper memory, respectively, in kilobytes. Lower memory starts at - /// address 0, and upper memory starts at address 1 megabyte. The maximum - /// possible value for lower memory is 640 kilobytes. The value returned - /// for upper memory is maximally the address of the first upper memory - /// hole minus 1 megabyte. It is not guaranteed to be this value. +multiboot2_common::raw_type! { + /// ABI compatible representation of the type of a boot information tag. /// - /// This tag may not be provided by some bootloaders on EFI platforms if - /// EFI boot services are enabled and available for the loaded image (EFI - /// boot services not terminated tag exists in Multiboot2 information - /// structure). - BasicMeminfo, - /// Tag `5`: This tag indicates which BIOS disk device the bootloader - /// loaded the OS image from. If the OS image was not loaded from a BIOS - /// disk, then this tag must not be present. The operating system may use - /// this field as a hint for determining its own root device, but is not - /// required to. - Bootdev, - /// Tag `6`: Memory map. The map provided is guaranteed to list all - /// standard RAM that should be available for normal use. This type however - /// includes the regions occupied by kernel, mbi, segments and modules. - /// Kernel must take care not to overwrite these regions. + /// This type matches the binary representation (`u32`) and corresponds + /// to the `typ`/`type` field of a Multiboot2 [`TagHeader`]. It can + /// easily be created from or converted to [`TagType`]. /// - /// This tag may not be provided by some bootloaders on EFI platforms if - /// EFI boot services are enabled and available for the loaded image (EFI - /// boot services not terminated tag exists in Multiboot2 information - /// structure). - Mmap, - /// Tag `7`: Contains the VBE control information returned by the VBE - /// Function `0x00` and VBE mode information returned by the VBE Function - /// `0x01`, respectively. Note that VBE 3.0 defines another protected mode - /// interface which is incompatible with the old one. If you want to use - /// the new protected mode interface, you will have to find the table - /// yourself. - Vbe, - /// Tag `8`: Framebuffer. - Framebuffer, - /// Tag `9`: This tag contains section header table from an ELF kernel, the - /// size of each entry, number of entries, and the string table used as the - /// index of names. They correspond to the `shdr_*` entries (`shdr_num`, - /// etc.) in the Executable and Linkable Format (ELF) specification in the - /// program header. - ElfSections, - /// Tag `10`: APM table. See Advanced Power Management (APM) BIOS Interface - /// Specification, for more information. - Apm, - /// Tag `11`: This tag contains pointer to i386 EFI system table. - Efi32, - /// Tag `12`: This tag contains pointer to amd64 EFI system table. - Efi64, - /// Tag `13`: This tag contains a copy of SMBIOS tables as well as their - /// version. - Smbios, - /// Tag `14`: Also called "AcpiOld" in other multiboot2 implementations. - AcpiV1, - /// Tag `15`: Refers to version 2 and later of Acpi. - /// Also called "AcpiNew" in other multiboot2 implementations. - AcpiV2, - /// Tag `16`: This tag contains network information in the format specified - /// as DHCP. It may be either a real DHCP reply or just the configuration - /// info in the same format. This tag appears once - /// per card. - Network, - /// Tag `17`: This tag contains EFI memory map as per EFI specification. - /// This tag may not be provided by some bootloaders on EFI platforms if - /// EFI boot services are enabled and available for the loaded image (EFI - /// boot services not terminated tag exists in Multiboot2 information - /// structure). - EfiMmap, - /// Tag `18`: This tag indicates ExitBootServices wasn't called. - EfiBs, - /// Tag `19`: This tag contains pointer to EFI i386 image handle. Usually - /// it is bootloader image handle. - Efi32Ih, - /// Tag `20`: This tag contains pointer to EFI amd64 image handle. Usually - /// it is bootloader image handle. - Efi64Ih, - /// Tag `21`: This tag contains image load base physical address. The spec - /// tells *"It is provided only if image has relocatable header tag."* but - /// experience showed that this is not true for at least GRUB 2. - LoadBaseAddr, - /// Custom tag types `> 21`. The Multiboot2 spec doesn't explicitly allow - /// or disallow them. Bootloader and OS developers are free to use custom - /// tags. - Custom(u32), -} - -impl TagType { - /// Convenient wrapper to get the underlying `u32` representation of the tag. - #[must_use] - pub fn val(&self) -> u32 { - u32::from(*self) - } -} - -/// Relevant `From` implementations for conversions between `u32`, -/// [`TagTypeId`], and [`TagType`]. -mod primitive_conversion_impls { - use super::*; - use core::mem::transmute; - - impl From for TagTypeId { - fn from(value: u32) -> Self { - // SAFETY: the type has repr(transparent) - unsafe { transmute(value) } - } - } - - impl From for u32 { - fn from(value: TagTypeId) -> Self { - value.0 as _ - } - } - - impl From for TagType { - fn from(value: u32) -> Self { - match value { - 0 => Self::End, - 1 => Self::Cmdline, - 2 => Self::BootLoaderName, - 3 => Self::Module, - 4 => Self::BasicMeminfo, - 5 => Self::Bootdev, - 6 => Self::Mmap, - 7 => Self::Vbe, - 8 => Self::Framebuffer, - 9 => Self::ElfSections, - 10 => Self::Apm, - 11 => Self::Efi32, - 12 => Self::Efi64, - 13 => Self::Smbios, - 14 => Self::AcpiV1, - 15 => Self::AcpiV2, - 16 => Self::Network, - 17 => Self::EfiMmap, - 18 => Self::EfiBs, - 19 => Self::Efi32Ih, - 20 => Self::Efi64Ih, - 21 => Self::LoadBaseAddr, - c => Self::Custom(c), - } - } - } - - impl From for u32 { - fn from(value: TagType) -> Self { - match value { - TagType::End => 0, - TagType::Cmdline => 1, - TagType::BootLoaderName => 2, - TagType::Module => 3, - TagType::BasicMeminfo => 4, - TagType::Bootdev => 5, - TagType::Mmap => 6, - TagType::Vbe => 7, - TagType::Framebuffer => 8, - TagType::ElfSections => 9, - TagType::Apm => 10, - TagType::Efi32 => 11, - TagType::Efi64 => 12, - TagType::Smbios => 13, - TagType::AcpiV1 => 14, - TagType::AcpiV2 => 15, - TagType::Network => 16, - TagType::EfiMmap => 17, - TagType::EfiBs => 18, - TagType::Efi32Ih => 19, - TagType::Efi64Ih => 20, - TagType::LoadBaseAddr => 21, - TagType::Custom(c) => c, - } - } - } -} - -/// `From` implementations for conversions between [`TagTypeId`] and -/// [`TagType`]. -mod intermediate_conversion_impls { - use super::*; - - impl From for TagType { - fn from(value: TagTypeId) -> Self { - let value = u32::from(value); - Self::from(value) - } - } - - impl From for TagTypeId { - fn from(value: TagType) -> Self { - let value = u32::from(value); - Self::from(value) - } - } -} + /// [`TagHeader`]: crate::TagHeader + pub struct TagTypeRaw(u32); -/// Implements `PartialEq` between [`TagTypeId`] and [`TagType`]. Two values are -/// equal if their `u32` representation is equal. Additionally, `u32` can be -/// compared with [`TagTypeId`]. -mod partial_eq_impls { - use super::*; - - impl PartialEq for TagType { - fn eq(&self, other: &TagTypeId) -> bool { - let this = u32::from(*self); - let that = u32::from(*other); - this == that - } - } - - // each compare/equal direction must be implemented manually - impl PartialEq for TagTypeId { - fn eq(&self, other: &TagType) -> bool { - other.eq(self) - } - } - - impl PartialEq for TagTypeId { - fn eq(&self, other: &u32) -> bool { - let this = u32::from(*self); - this == *other - } - } - - impl PartialEq for u32 { - fn eq(&self, other: &TagTypeId) -> bool { - other.eq(self) - } - } - - impl PartialEq for TagType { - fn eq(&self, other: &u32) -> bool { - let this = u32::from(*self); - this == *other - } - } - - impl PartialEq for u32 { - fn eq(&self, other: &TagType) -> bool { - other.eq(self) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_hashset() { - let mut set = std::collections::HashSet::new(); - set.insert(TagType::Cmdline); - set.insert(TagType::ElfSections); - set.insert(TagType::BootLoaderName); - set.insert(TagType::LoadBaseAddr); - set.insert(TagType::LoadBaseAddr); - assert_eq!(set.len(), 4); - println!("{set:#?}"); - } - - #[test] - fn test_btreeset() { - let mut set = std::collections::BTreeSet::new(); - set.insert(TagType::Cmdline); - set.insert(TagType::ElfSections); - set.insert(TagType::BootLoaderName); - set.insert(TagType::LoadBaseAddr); - set.insert(TagType::LoadBaseAddr); - assert_eq!(set.len(), 4); - for (current, next) in set.iter().zip(set.iter().skip(1)) { - assert!(current < next); - } - println!("{set:#?}"); - } - - /// Tests for equality when one type is u32 and the other the enum representation. - #[test] - fn test_partial_eq_u32() { - assert_eq!(21, TagType::LoadBaseAddr); - assert_eq!(TagType::LoadBaseAddr, 21); - assert_eq!(21, TagTypeId(21)); - assert_eq!(TagTypeId(21), 21); - assert_eq!(42, TagType::Custom(42)); - assert_eq!(TagType::Custom(42), 42); - } - - /// Tests the construction of [`TagTypeId`] from primitive `u32` values. - #[test] - #[expect(non_snake_case)] - fn test_TagTypeId() { - assert_eq!(size_of::(), size_of::()); - assert_eq!(align_of::(), align_of::()); - - for i in 0..50_u32 { - let val: TagTypeId = i.into(); - let val2: TagType = val.into(); - assert_eq!(val, val2); - } - - let tag_custom: u32 = 0x1337; - let tag_custom: TagTypeId = tag_custom.into(); - let tag_custom: TagType = tag_custom.into(); - matches!(tag_custom, TagType::Custom(0x1337)); - } - - /// Tests the construction of [`TagTypeId`] from primitive `u32` values for - /// specified and custom tags. - #[test] - fn test_from_and_to_tag_type_id() { - for i in 0..1_000 { - let tag_type_id = TagTypeId::new(i); - let tag_type_from_id = TagType::from(tag_type_id); - let tag_type_from_u16 = TagType::from(i); - assert_eq!(tag_type_from_id, tag_type_from_u16) - } + /// The type of a boot information tag. + /// + /// This assigns each possible value a specific semantic according to the + /// Multiboot2 spec. Custom tag types `> 21` are mapped to + /// [`TagType::Custom`]; the spec doesn't explicitly allow or disallow + /// them. + /// + /// This is a higher level abstraction for [`TagTypeRaw`] and **not + /// binary compatible** with it. + pub enum TagType { + /// Tag `0`: Marks the end of the tags. + End = 0, + /// Tag `1`: Additional command line string. + /// For example `''` or `'--my-custom-option foo --provided by_grub`, if + /// your GRUB config contains `multiboot2 /boot/multiboot2-binary.elf --my-custom-option foo --provided by_grub` + Cmdline = 1, + /// Tag `2`: Name of the bootloader, e.g. 'GRUB 2.04-1ubuntu44.2' + BootLoaderName = 2, + /// Tag `3`: Additional Multiboot modules, which are BLOBs provided in + /// memory. For example an initial ram disk with essential drivers. + Module = 3, + /// Tag `4`: `mem_lower` and `mem_upper` indicate the amount of lower and + /// upper memory, respectively, in kilobytes. Lower memory starts at + /// address 0, and upper memory starts at address 1 megabyte. The maximum + /// possible value for lower memory is 640 kilobytes. The value returned + /// for upper memory is maximally the address of the first upper memory + /// hole minus 1 megabyte. It is not guaranteed to be this value. + /// + /// This tag may not be provided by some bootloaders on EFI platforms if + /// EFI boot services are enabled and available for the loaded image (EFI + /// boot services not terminated tag exists in Multiboot2 information + /// structure). + BasicMeminfo = 4, + /// Tag `5`: This tag indicates which BIOS disk device the bootloader + /// loaded the OS image from. If the OS image was not loaded from a BIOS + /// disk, then this tag must not be present. The operating system may use + /// this field as a hint for determining its own root device, but is not + /// required to. + Bootdev = 5, + /// Tag `6`: Memory map. The map provided is guaranteed to list all + /// standard RAM that should be available for normal use. This type however + /// includes the regions occupied by kernel, mbi, segments and modules. + /// Kernel must take care not to overwrite these regions. + /// + /// This tag may not be provided by some bootloaders on EFI platforms if + /// EFI boot services are enabled and available for the loaded image (EFI + /// boot services not terminated tag exists in Multiboot2 information + /// structure). + Mmap = 6, + /// Tag `7`: Contains the VBE control information returned by the VBE + /// Function `0x00` and VBE mode information returned by the VBE Function + /// `0x01`, respectively. Note that VBE 3.0 defines another protected mode + /// interface which is incompatible with the old one. If you want to use + /// the new protected mode interface, you will have to find the table + /// yourself. + Vbe = 7, + /// Tag `8`: Framebuffer. + Framebuffer = 8, + /// Tag `9`: This tag contains section header table from an ELF kernel, the + /// size of each entry, number of entries, and the string table used as the + /// index of names. They correspond to the `shdr_*` entries (`shdr_num`, + /// etc.) in the Executable and Linkable Format (ELF) specification in the + /// program header. + ElfSections = 9, + /// Tag `10`: APM table. See Advanced Power Management (APM) BIOS Interface + /// Specification, for more information. + Apm = 10, + /// Tag `11`: This tag contains pointer to i386 EFI system table. + Efi32 = 11, + /// Tag `12`: This tag contains pointer to amd64 EFI system table. + Efi64 = 12, + /// Tag `13`: This tag contains a copy of SMBIOS tables as well as their + /// version. + Smbios = 13, + /// Tag `14`: Also called "AcpiOld" in other multiboot2 implementations. + AcpiV1 = 14, + /// Tag `15`: Refers to version 2 and later of Acpi. + /// Also called "AcpiNew" in other multiboot2 implementations. + AcpiV2 = 15, + /// Tag `16`: This tag contains network information in the format specified + /// as DHCP. It may be either a real DHCP reply or just the configuration + /// info in the same format. This tag appears once + /// per card. + Network = 16, + /// Tag `17`: This tag contains EFI memory map as per EFI specification. + /// This tag may not be provided by some bootloaders on EFI platforms if + /// EFI boot services are enabled and available for the loaded image (EFI + /// boot services not terminated tag exists in Multiboot2 information + /// structure). + EfiMmap = 17, + /// Tag `18`: This tag indicates ExitBootServices wasn't called. + EfiBs = 18, + /// Tag `19`: This tag contains pointer to EFI i386 image handle. Usually + /// it is bootloader image handle. + Efi32Ih = 19, + /// Tag `20`: This tag contains pointer to EFI amd64 image handle. Usually + /// it is bootloader image handle. + Efi64Ih = 20, + /// Tag `21`: This tag contains image load base physical address. The spec + /// tells *"It is provided only if image has relocatable header tag."* but + /// experience showed that this is not true for at least GRUB 2. + LoadBaseAddr = 21, } } diff --git a/multiboot2/src/vbe_info.rs b/multiboot2/src/vbe_info.rs index 2d22d0ba..678f1e6e 100644 --- a/multiboot2/src/vbe_info.rs +++ b/multiboot2/src/vbe_info.rs @@ -142,6 +142,8 @@ pub struct VBEControlInfo { oem_data: [u8; 256], } +const _: () = assert!(size_of::() == 512); + impl fmt::Debug for VBEControlInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("VBEControlInfo") @@ -228,7 +230,7 @@ pub struct VBEModeInfo { pub number_of_banks: u8, /// Memory model type - pub memory_model: VBEMemoryModel, + pub memory_model: VBEMemoryModelRaw, /// Bank size (Measured in Kilobytes.) pub bank_size: u8, @@ -275,6 +277,8 @@ pub struct VBEModeInfo { reserved1: [u8; 206], } +const _: () = assert!(size_of::() == 256); + impl fmt::Debug for VBEModeInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("VBEModeInfo") @@ -443,18 +447,61 @@ bitflags! { } } -/// The MemoryModel field specifies the general type of memory organization used in modes. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(u8)] -#[expect(missing_docs)] -pub enum VBEMemoryModel { - #[default] - Text = 0x00, - CGAGraphics = 0x01, - HerculesGraphics = 0x02, - Planar = 0x03, - PackedPixel = 0x04, - Unchained = 0x05, - DirectColor = 0x06, - YUV = 0x07, +multiboot2_common::raw_type! { + /// ABI compatible representation of the VBE memory model. + /// + /// This type matches the binary representation (`u8`). + #[derive(Default)] + pub struct VBEMemoryModelRaw(u8); + + /// The general type of memory organization used in a VBE mode. + /// + /// Reserved values (`0x08..=0x0F`) and OEM-defined values + /// (`0x10..=0xFF`) of the VBE spec are mapped to + /// [`VBEMemoryModel::Custom`]. + /// + /// This is a higher level abstraction for [`VBEMemoryModelRaw`]. + #[derive(Default)] + #[allow(clippy::upper_case_acronyms)] + pub enum VBEMemoryModel { + /// Text mode. + #[default] + Text = 0x00, + /// CGA graphics. + CGAGraphics = 0x01, + /// Hercules graphics. + HerculesGraphics = 0x02, + /// Planar. + Planar = 0x03, + /// Packed pixel. + PackedPixel = 0x04, + /// Non-chain 4, 256 color. + Unchained = 0x05, + /// Direct color. + DirectColor = 0x06, + /// YUV. + YUV = 0x07, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A mode info block with a memory model unknown to the specification + /// must be readable without undefined behavior. + #[test] + fn unknown_memory_model_is_not_ub() { + let mut bytes = [0_u8; size_of::()]; + // Offset of `memory_model` within the packed struct. + bytes[27] = 0x42; + + // SAFETY: The struct is `repr(C, packed)`, one byte aligned, and + // since the fix every bit pattern is a valid instance. + let mode_info = unsafe { core::ptr::read_unaligned(bytes.as_ptr().cast::()) }; + + assert_eq!(mode_info.memory_model, VBEMemoryModel::Custom(0x42)); + let debug = format!("{mode_info:?}"); + assert!(debug.contains("Custom(66)")); + } }