Skip to content

Commit ace45d5

Browse files
authored
Merge pull request #115 from rust-osdev/multiboot2-str-safety
Multiboot2: More Safety for Getters of Strings from Tags (Prepare Release v0.14.0)
2 parents 6ed0ca4 + 417689b commit ace45d5

10 files changed

+200
-45
lines changed

multiboot2/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Multiboot2-compliant bootloaders, like GRUB. It supports all tags from the speci
66
including full support for the sections of ELF-64. This library is `no_std` and can be
77
used in a Multiboot2-kernel.
88
"""
9-
version = "0.13.3"
9+
version = "0.14.0"
1010
authors = [
1111
"Philipp Oppermann <[email protected]>",
1212
"Calvin Lee <[email protected]>",

multiboot2/Changelog.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
# CHANGELOG for crate `multiboot2`
22

3-
## 0.13.3 (2022-05-03)
3+
## 0.14.0 (2022-05-xx)
4+
- **BREAKING CHANGES** \
5+
This version includes a few small breaking changes that brings more safety when parsing strings from the
6+
multiboot information structure.
7+
- `BootLoaderNameTag::name` now returns a Result instead of just the value
8+
- `CommandLineTag::command_line` now returns a Result instead of just the value
9+
- `ModuleTag::cmdline` now returns a Result instead of just the value
10+
- `RsdpV1Tag::signature` now returns a Result instead of an Option
11+
- `RsdpV1Tag::oem_id` now returns a Result instead of an Option
12+
- `RsdpV2Tag::signature` now returns a Result instead of an Option
13+
- `RsdpV2Tag::oem_id` now returns a Result instead of an Option
14+
- internal code improvements
15+
16+
17+
## 0.13.3 (2022-06-03)
418
- impl `Send` for `BootInformation`
519

620
## 0.13.2 (2022-05-02)

multiboot2/src/boot_loader_name.rs

+47-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::TagType;
2+
use core::str::Utf8Error;
23

34
/// This tag contains the name of the bootloader that is booting the kernel.
45
///
@@ -9,11 +10,14 @@ use crate::TagType;
910
pub struct BootLoaderNameTag {
1011
typ: TagType,
1112
size: u32,
13+
/// Null-terminated UTF-8 string
1214
string: u8,
1315
}
1416

1517
impl BootLoaderNameTag {
1618
/// Read the name of the bootloader that is booting the kernel.
19+
/// This is an null-terminated UTF-8 string. If this returns `Err` then perhaps the memory
20+
/// is invalid or the bootloader doesn't follow the spec.
1721
///
1822
/// # Examples
1923
///
@@ -23,11 +27,49 @@ impl BootLoaderNameTag {
2327
/// assert_eq!("GRUB 2.02~beta3-5", name);
2428
/// }
2529
/// ```
26-
pub fn name(&self) -> &str {
30+
pub fn name(&self) -> Result<&str, Utf8Error> {
2731
use core::{mem, slice, str};
28-
unsafe {
29-
let strlen = self.size as usize - mem::size_of::<BootLoaderNameTag>();
30-
str::from_utf8_unchecked(slice::from_raw_parts((&self.string) as *const u8, strlen))
31-
}
32+
// strlen without null byte
33+
let strlen = self.size as usize - mem::size_of::<BootLoaderNameTag>();
34+
let bytes = unsafe { slice::from_raw_parts((&self.string) as *const u8, strlen) };
35+
str::from_utf8(bytes)
36+
}
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use crate::TagType;
42+
43+
const MSG: &str = "hello";
44+
45+
/// Returns the tag structure in bytes in native endian format.
46+
fn get_bytes() -> std::vec::Vec<u8> {
47+
// size is: 4 bytes for tag + 4 bytes for size + length of null-terminated string
48+
let size = (4 + 4 + MSG.as_bytes().len() + 1) as u32;
49+
[
50+
&((TagType::BootLoaderName as u32).to_ne_bytes()),
51+
&size.to_ne_bytes(),
52+
MSG.as_bytes(),
53+
// Null Byte
54+
&[0],
55+
]
56+
.iter()
57+
.flat_map(|bytes| bytes.iter())
58+
.copied()
59+
.collect()
60+
}
61+
62+
/// Tests to parse a string with a terminating null byte from the tag (as the spec defines).
63+
#[test]
64+
fn test_parse_str() {
65+
let tag = get_bytes();
66+
let tag = unsafe {
67+
tag.as_ptr()
68+
.cast::<super::BootLoaderNameTag>()
69+
.as_ref()
70+
.unwrap()
71+
};
72+
assert_eq!({ tag.typ }, TagType::BootLoaderName);
73+
assert_eq!(tag.name().expect("must be valid UTF-8"), MSG);
3274
}
3375
}

multiboot2/src/command_line.rs

+51-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
//! Module for [CommandLineTag].
2+
13
use crate::TagType;
4+
use core::mem;
5+
use core::slice;
6+
use core::str;
27

38
/// This tag contains the command line string.
49
///
@@ -9,11 +14,14 @@ use crate::TagType;
914
pub struct CommandLineTag {
1015
typ: TagType,
1116
size: u32,
17+
/// Null-terminated UTF-8 string
1218
string: u8,
1319
}
1420

1521
impl CommandLineTag {
1622
/// Read the command line string that is being passed to the booting kernel.
23+
/// This is an null-terminated UTF-8 string. If this returns `Err` then perhaps the memory
24+
/// is invalid or the bootloader doesn't follow the spec.
1725
///
1826
/// # Examples
1927
///
@@ -23,11 +31,48 @@ impl CommandLineTag {
2331
/// assert_eq!("/bootarg", command_line);
2432
/// }
2533
/// ```
26-
pub fn command_line(&self) -> &str {
27-
use core::{mem, slice, str};
28-
unsafe {
29-
let strlen = self.size as usize - mem::size_of::<CommandLineTag>();
30-
str::from_utf8_unchecked(slice::from_raw_parts((&self.string) as *const u8, strlen))
31-
}
34+
pub fn command_line(&self) -> Result<&str, str::Utf8Error> {
35+
// strlen without null byte
36+
let strlen = self.size as usize - mem::size_of::<CommandLineTag>();
37+
let bytes = unsafe { slice::from_raw_parts((&self.string) as *const u8, strlen) };
38+
str::from_utf8(bytes)
39+
}
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
use crate::TagType;
45+
46+
const MSG: &str = "hello";
47+
48+
/// Returns the tag structure in bytes in native endian format.
49+
fn get_bytes() -> std::vec::Vec<u8> {
50+
// size is: 4 bytes for tag + 4 bytes for size + length of null-terminated string
51+
let size = (4 + 4 + MSG.as_bytes().len() + 1) as u32;
52+
[
53+
&((TagType::Cmdline as u32).to_ne_bytes()),
54+
&size.to_ne_bytes(),
55+
MSG.as_bytes(),
56+
// Null Byte
57+
&[0],
58+
]
59+
.iter()
60+
.flat_map(|bytes| bytes.iter())
61+
.copied()
62+
.collect()
63+
}
64+
65+
/// Tests to parse a string with a terminating null byte from the tag (as the spec defines).
66+
#[test]
67+
fn test_parse_str() {
68+
let tag = get_bytes();
69+
let tag = unsafe {
70+
tag.as_ptr()
71+
.cast::<super::CommandLineTag>()
72+
.as_ref()
73+
.unwrap()
74+
};
75+
assert_eq!({ tag.typ }, TagType::Cmdline);
76+
assert_eq!(tag.command_line().expect("must be valid UTF-8"), MSG);
3277
}
3378
}

multiboot2/src/elf_sections.rs

+2
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ impl ElfSection {
190190
use core::{slice, str};
191191

192192
let name_ptr = unsafe { self.string_table().offset(self.get().name_index() as isize) };
193+
194+
// strlen without null byte
193195
let strlen = {
194196
let mut len = 0;
195197
while unsafe { *name_ptr.offset(len) } != 0 {

multiboot2/src/framebuffer.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::Reader;
33
use core::slice;
44

55
/// The VBE Framebuffer information Tag.
6-
#[derive(Debug, PartialEq)]
6+
#[derive(Debug, PartialEq, Eq)]
77
pub struct FramebufferTag<'a> {
88
/// Contains framebuffer physical address.
99
///
@@ -29,7 +29,7 @@ pub struct FramebufferTag<'a> {
2929
}
3030

3131
/// The type of framebuffer.
32-
#[derive(Debug, PartialEq)]
32+
#[derive(Debug, PartialEq, Eq)]
3333
pub enum FramebufferType<'a> {
3434
/// Indexed color.
3535
Indexed {
@@ -55,7 +55,7 @@ pub enum FramebufferType<'a> {
5555
}
5656

5757
/// An RGB color type field.
58-
#[derive(Debug, PartialEq)]
58+
#[derive(Debug, PartialEq, Eq)]
5959
pub struct FramebufferField {
6060
/// Color field position.
6161
pub position: u8,
@@ -65,7 +65,7 @@ pub struct FramebufferField {
6565
}
6666

6767
/// A framebuffer color descriptor in the palette.
68-
#[derive(Clone, Copy, Debug, PartialEq)]
68+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6969
#[repr(C, packed)] // only repr(C) would add unwanted padding at the end
7070
pub struct FramebufferColor {
7171
/// The Red component of the color.

multiboot2/src/lib.rs

+20-5
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,14 @@ impl fmt::Debug for BootInformation {
352352
"boot_loader_name_tag",
353353
&self
354354
.boot_loader_name_tag()
355-
.map(|x| x.name())
355+
.and_then(|x| x.name().ok())
356356
.unwrap_or("<unknown>"),
357357
)
358358
.field(
359359
"command_line",
360360
&self
361361
.command_line_tag()
362-
.map(|x| x.command_line())
362+
.and_then(|x| x.command_line().ok())
363363
.unwrap_or(""),
364364
)
365365
.field("memory_areas", &self.memory_map_tag())
@@ -532,7 +532,13 @@ mod tests {
532532
assert!(bi.elf_sections_tag().is_none());
533533
assert!(bi.memory_map_tag().is_none());
534534
assert!(bi.module_tags().next().is_none());
535-
assert_eq!("name", bi.boot_loader_name_tag().unwrap().name());
535+
assert_eq!(
536+
"name",
537+
bi.boot_loader_name_tag()
538+
.expect("tag must be present")
539+
.name()
540+
.expect("must be valid utf8")
541+
);
536542
assert!(bi.command_line_tag().is_none());
537543
}
538544

@@ -1231,9 +1237,18 @@ mod tests {
12311237
assert!(bi.module_tags().next().is_none());
12321238
assert_eq!(
12331239
"GRUB 2.02~beta3-5",
1234-
bi.boot_loader_name_tag().unwrap().name()
1240+
bi.boot_loader_name_tag()
1241+
.expect("tag must be present")
1242+
.name()
1243+
.expect("must be valid utf-8")
1244+
);
1245+
assert_eq!(
1246+
"",
1247+
bi.command_line_tag()
1248+
.expect("tag must present")
1249+
.command_line()
1250+
.expect("must be valid utf-8")
12351251
);
1236-
assert_eq!("", bi.command_line_tag().unwrap().command_line());
12371252

12381253
// Test the Framebuffer tag
12391254
let fbi = bi.framebuffer_tag().unwrap();

multiboot2/src/module.rs

+48-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::tag_type::{Tag, TagIter, TagType};
22
use core::fmt::{Debug, Formatter};
3+
use core::str::Utf8Error;
34

45
/// This tag indicates to the kernel what boot module was loaded along with
56
/// the kernel image, and where it can be found.
@@ -10,25 +11,24 @@ pub struct ModuleTag {
1011
size: u32,
1112
mod_start: u32,
1213
mod_end: u32,
13-
/// Begin of the command line string.
14+
/// Null-terminated UTF-8 string
1415
cmdline_str: u8,
1516
}
1617

1718
impl ModuleTag {
18-
// The multiboot specification defines the module str as valid utf-8 (zero terminated string),
19-
// therefore this function produces defined behavior
20-
/// Get the cmdline of the module. If the GRUB configuration contains
21-
/// `module2 /foobar/some_boot_module --test cmdline-option`, then this method
19+
/// Returns the cmdline of the module.
20+
/// This is an null-terminated UTF-8 string. If this returns `Err` then perhaps the memory
21+
/// is invalid or the bootloader doesn't follow the spec.
22+
///
23+
/// For example: If the GRUB configuration contains
24+
/// `module2 /foobar/some_boot_module --test cmdline-option` then this method
2225
/// will return `--test cmdline-option`.
23-
pub fn cmdline(&self) -> &str {
26+
pub fn cmdline(&self) -> Result<&str, Utf8Error> {
2427
use core::{mem, slice, str};
28+
// strlen without null byte
2529
let strlen = self.size as usize - mem::size_of::<ModuleTag>();
26-
unsafe {
27-
str::from_utf8_unchecked(slice::from_raw_parts(
28-
&self.cmdline_str as *const u8,
29-
strlen,
30-
))
31-
}
30+
let bytes = unsafe { slice::from_raw_parts((&self.cmdline_str) as *const u8, strlen) };
31+
str::from_utf8(bytes)
3232
}
3333

3434
/// Start address of the module.
@@ -89,3 +89,39 @@ impl<'a> Debug for ModuleIter<'a> {
8989
list.finish()
9090
}
9191
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use crate::TagType;
96+
97+
const MSG: &str = "hello";
98+
99+
/// Returns the tag structure in bytes in native endian format.
100+
fn get_bytes() -> std::vec::Vec<u8> {
101+
// size is: 4 bytes for tag + 4 bytes for size + length of null-terminated string
102+
// 4 bytes mod_start + 4 bytes mod_end
103+
let size = (4 + 4 + 4 + 4 + MSG.as_bytes().len() + 1) as u32;
104+
[
105+
&((TagType::Module as u32).to_ne_bytes()),
106+
&size.to_ne_bytes(),
107+
&0_u32.to_ne_bytes(),
108+
&0_u32.to_ne_bytes(),
109+
MSG.as_bytes(),
110+
// Null Byte
111+
&[0],
112+
]
113+
.iter()
114+
.flat_map(|bytes| bytes.iter())
115+
.copied()
116+
.collect()
117+
}
118+
119+
/// Tests to parse a string with a terminating null byte from the tag (as the spec defines).
120+
#[test]
121+
fn test_parse_str() {
122+
let tag = get_bytes();
123+
let tag = unsafe { tag.as_ptr().cast::<super::ModuleTag>().as_ref().unwrap() };
124+
assert_eq!({ tag.typ }, TagType::Module);
125+
assert_eq!(tag.cmdline().expect("must be valid UTF-8"), MSG);
126+
}
127+
}

0 commit comments

Comments
 (0)