Skip to content

bzImage / initrd support (v2) #670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- Documentation for Logger API Requests in `docs/api_requests/logger.md`.
- Documentation for Actions API Requests in `docs/api_requests/actions.md`.
- Documentation for MMDS in `docs/mmds.md`.
- Support for booting from bzImage-format kernels
- Support for loading initrd as well as kernel

### Changed

Expand Down
2 changes: 2 additions & 0 deletions api_server/src/request/boot_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ mod tests {
fn test_into_parsed_request() {
let body = BootSourceConfig {
kernel_image_path: String::from("/foo/bar"),
initrd_path: None,
boot_args: Some(String::from("foobar")),
};
let same_body = BootSourceConfig {
kernel_image_path: String::from("/foo/bar"),
initrd_path: None,
boot_args: Some(String::from("foobar")),
};
let (sender, receiver) = oneshot::channel();
Expand Down
3 changes: 3 additions & 0 deletions api_server/swagger/firecracker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ definitions:
kernel_image_path:
type: string
description: Host level path to the kernel image used to boot the guest
initrd_path:
type: string
description: Host level path to the initrd image used to boot the guest
boot_args:
type: string
description: Kernel boot arguments
Expand Down
143 changes: 140 additions & 3 deletions kernel/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ pub enum Error {
CommandLineOverflow,
InvalidElfMagicNumber,
InvalidEntryAddress,
InvalidLinuxMagicNumber,
InvalidProgramHeaderSize,
InvalidProgramHeaderOffset,
InvalidProgramHeaderAddress,
LinuxNot64Bit,
ReadElfHeader,
ReadInitrd,
ReadKernelImage,
ReadLinuxHeader,
ReadProgramHeader,
SeekInitrd,
SeekKernelEnd,
SeekKernelStart,
SeekLinuxHeader,
SeekElfStart,
SeekProgramHeader,
}
Expand All @@ -44,7 +51,7 @@ pub type Result<T> = std::result::Result<T, Error>;
/// * `kernel_image` - Input vmlinux image.
///
/// Returns the entry address of the kernel.
pub fn load_kernel<F>(guest_mem: &GuestMemory, kernel_image: &mut F) -> Result<GuestAddress>
fn load_elf64<F>(guest_mem: &GuestMemory, kernel_image: &mut F) -> Result<GuestAddress>
where
F: Read + Seek,
{
Expand Down Expand Up @@ -111,6 +118,66 @@ where
Ok(GuestAddress(ehdr.e_entry as usize))
}

/// Loads a kernel from a 64-bit vmlinuz bzImage to a slice
///
/// # Arguments
///
/// * `guest_mem` - The guest memory region the kernel is written to.
/// * `kernel_image` - Input vmlinuz image.
///
/// Returns the entry address of the kernel.
fn load_bzimage<F>(guest_mem: &GuestMemory, kernel_image: &mut F) -> Result<GuestAddress>
where
F: Read + Seek,
{
const BZIMAGE_HEADER_OFFSET: u64 = 0x1f1;
const BZIMAGE_HEADER_MAGIC: u32 = 0x53726448;
const BZIMAGE_LOAD_ADDRESS: usize = 0x100000;
const BZIMAGE_64BIT_ENTRY_ADDRESS: usize = BZIMAGE_LOAD_ADDRESS + 0x200;

let mut header: x86_64::bootparam::setup_header = Default::default();

/* Read and check the header. */
kernel_image
.seek(SeekFrom::Start(BZIMAGE_HEADER_OFFSET))
.map_err(|_| Error::SeekLinuxHeader)?;
unsafe {
// read_struct is safe when reading a POD struct. It can be used and dropped without issue.
sys_util::read_struct(kernel_image, &mut header).map_err(|_| Error::ReadLinuxHeader)?;
}
if header.header != BZIMAGE_HEADER_MAGIC {
return Err(Error::InvalidLinuxMagicNumber);
}
if (header.xloadflags as u32 & x86_64::bootparam::XLF_KERNEL_64) == 0 {
return Err(Error::LinuxNot64Bit);
}

/* Find the protected-mode code in the file. */
let load_bytes = header.syssize as usize * 16;
let load_offset = if header.setup_sects == 0 {
5 * 512
} else {
(header.setup_sects as u64 + 1) * 512
};

/* Load it at the default bzImage load address. */
kernel_image
.seek(SeekFrom::Start(load_offset))
.map_err(|_| Error::SeekKernelStart)?;
guest_mem
.read_to_memory(GuestAddress(BZIMAGE_LOAD_ADDRESS), kernel_image, load_bytes)
.map_err(|_| Error::ReadKernelImage)?;

Ok(GuestAddress(BZIMAGE_64BIT_ENTRY_ADDRESS))
}

pub fn load_kernel<F>(guest_mem: &GuestMemory, kernel_image: &mut F) -> Result<GuestAddress>
where
F: Read + Seek,
{
load_elf64(guest_mem, kernel_image).or_else(|_| load_bzimage(guest_mem, kernel_image))
}

/// Writes the command line string to the given memory slice.
///
/// # Arguments
Expand Down Expand Up @@ -142,6 +209,37 @@ pub fn load_cmdline(
Ok(())
}

/// Loads the initrd from a file into the given memory slice.
///
/// * `guest_mem` - The guest memory region the initrd is written to.
/// * `initrd_image` - Input initrd image.
///
/// Returns the entry address of the initrd and its length as a tuple
pub fn load_initrd<F>(
guest_mem: &GuestMemory,
initrd_image: &mut F,
) -> Result<(GuestAddress, usize)>
where
F: Read + Seek,
{
// This works for bzImage kernels because they load at 1MiB and have 16MiB init space.
// In practice works for many ELF kernels too because they load at 16MiB.
// Ought to have an allocator in guest_mem to find an empty spot.
const INITRD_LOAD_ADDRESS: usize = 0x3000000; // 48 MiB

let load_bytes = initrd_image
.seek(SeekFrom::End(0))
.map_err(|_| Error::SeekInitrd)? as usize;
initrd_image
.seek(SeekFrom::Start(0))
.map_err(|_| Error::SeekInitrd)?;
guest_mem
.read_to_memory(GuestAddress(INITRD_LOAD_ADDRESS), initrd_image, load_bytes)
.map_err(|_| Error::ReadInitrd)?;

Ok((GuestAddress(INITRD_LOAD_ADDRESS), load_bytes))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -220,7 +318,7 @@ mod tests {
bad_image[0x1] = 0x33;
assert_eq!(
Err(Error::InvalidElfMagicNumber),
load_kernel(&gm, &mut Cursor::new(&bad_image))
load_elf64(&gm, &mut Cursor::new(&bad_image))
);
}

Expand All @@ -232,7 +330,7 @@ mod tests {
bad_image[0x5] = 2;
assert_eq!(
Err(Error::BigEndianElfOnLittle),
load_kernel(&gm, &mut Cursor::new(&bad_image))
load_elf64(&gm, &mut Cursor::new(&bad_image))
);
}

Expand All @@ -244,6 +342,45 @@ mod tests {
bad_image[0x20] = 0x10;
assert_eq!(
Err(Error::InvalidProgramHeaderOffset),
load_elf64(&gm, &mut Cursor::new(&bad_image))
);
}

// bare skeleton of a bzImage-compatible file.
fn make_bz() -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("test_bzImage.bin"));
v
}

#[test]
fn load_bz() {
let gm = create_guest_mem();
let image = make_bz();
assert_eq!(
Ok(GuestAddress(0x100200)),
load_kernel(&gm, &mut Cursor::new(&image))
);
}

#[test]
fn bad_bz_magic() {
let gm = create_guest_mem();
let mut bad_image = make_bz();
bad_image[0x203] = 0x0;
assert_eq!(
Err(Error::InvalidLinuxMagicNumber),
load_kernel(&gm, &mut Cursor::new(&bad_image))
);
}

#[test]
fn bad_bz_32bit() {
let gm = create_guest_mem();
let mut bad_image = make_bz();
bad_image[0x236] = 0x2;
assert_eq!(
Err(Error::LinuxNot64Bit),
load_kernel(&gm, &mut Cursor::new(&bad_image))
);
}
Expand Down
Binary file added kernel/src/loader/test_bzImage.bin
Binary file not shown.
9 changes: 5 additions & 4 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Tests can be added in any (existing or new) sub-directory of `tests/`, in files
named `test_*.py`.

Fixtures can be used to quickly build Firecracker microvm integration tests
that run on all microvm images in `s3://spec.firecracker/microvm-images/`.
that run on all microvm images in `s3://spec.ccfc.min/img/`.

For example, the test below makes use of the `test_microvm_any` fixture and will
be run on every microvm image in the bucket, each as a separate test case.
Expand All @@ -102,14 +102,15 @@ To see what fixtures are available, inspect `conftest.py`.

## Adding Microvm Images

Simply place the microvm image under `s3://spec.firecracker/microvm-images/`.
Simply place the microvm image under `s3://spec.ccfc.min/img/`.
The layout is:

``` tree
s3://<bucket-url>/microvm-images/
s3://<bucket-name>/img/
<microvm_test_image_folder_n>/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're touching/fixing this file, please also update the S3 bucket name from spec.firecracker to spec.ccfc.min, also the tree has changed from:

s3://<bucket-url>/microvm-images/

to

s3://<bucket-name>/img/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, updated.

kernel/
<optional_kernel_name.>vmlinux.bin
<optional_initrd_name.>initrd.img
fsfiles/
<rootfs_name>rootfs.ext4
<other_fsfile_n>
Expand All @@ -119,7 +120,7 @@ s3://<bucket-url>/microvm-images/
...
```

Then, tag `<microvm_test_image_folder_n>` with:
Then, tag `<microvm_test_image_folder_n>/` with:

``` json
TagSet = [{"key": "capability:<cap_name>", "value": ""}, ...]
Expand Down
28 changes: 23 additions & 5 deletions tests/framework/microvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def __init__(
self._kernel_path = os.path.join(self._path, MICROVM_KERNEL_RELPATH)
self._fsfiles_path = os.path.join(self._path, MICROVM_FSFILES_RELPATH)
self._kernel_file = ''
self._rootfs_file = ''
self._initrd_file = None
self._rootfs_file = None

# The binaries this microvm will use to start.
self._fc_binary_path = fc_binary_path
Expand Down Expand Up @@ -156,6 +157,16 @@ def kernel_file(self, path):
"""Set the path to the kernel file."""
self._kernel_file = path

@property
def initrd_file(self):
"""Return the name of the initrd file used by this microVM to boot."""
return self._initrd_file

@initrd_file.setter
def initrd_file(self, path):
"""Set the path to the initrd file."""
self._initrd_file = path

@property
def rootfs_file(self):
"""Return the path to the image this microVM can boot into."""
Expand Down Expand Up @@ -204,6 +215,7 @@ def setup(self):
<microvm_uuid>/
kernel/
<kernel_file_n>
<initrd_file_n>
....
fsfiles/
<fsfile_n>
Expand Down Expand Up @@ -328,12 +340,18 @@ def basic_config(
)

# Add a kernel to start booting from.
response = self.boot.put(
kernel_image_path=self.create_jailed_resource(self.kernel_file)
)
if self.initrd_file is not None:
response = self.boot.put(
initrd_path=self.create_jailed_resource(self.initrd_file),
kernel_image_path=self.create_jailed_resource(self.kernel_file)
)
else:
response = self.boot.put(
kernel_image_path=self.create_jailed_resource(self.kernel_file)
)
assert self._api_session.is_good_response(response.status_code)

if add_root_device:
if add_root_device and self.rootfs_file is not None:
# Add the root file system with rw permissions.
response = self.drive.put(
drive_id='rootfs',
Expand Down
5 changes: 4 additions & 1 deletion tests/framework/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,15 @@ def get(cls):
@staticmethod
def create_json(
boot_args=None,
kernel_image_path=None
kernel_image_path=None,
initrd_path=None
):
"""Compose the json associated to this type of API request."""
datax = {}
if kernel_image_path is not None:
datax['kernel_image_path'] = kernel_image_path
if initrd_path is not None:
datax['initrd_path'] = initrd_path
if boot_args is not None:
datax['boot_args'] = boot_args
return datax
Expand Down
4 changes: 4 additions & 0 deletions tests/framework/s3fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class MicrovmImageS3Fetcher:
MICROVM_IMAGE_KERNEL_RELPATH = 'kernel/'
MICROVM_IMAGE_BLOCKDEV_RELPATH = 'fsfiles/'
MICROVM_IMAGE_KERNEL_FILE_SUFFIX = r'vmlinux.bin'
MICROVM_IMAGE_INITRD_FILE_SUFFIX = r'initrd.img'
MICROVM_IMAGE_ROOTFS_FILE_SUFFIX = r'rootfs.ext4'
MICROVM_IMAGE_SSH_KEY_SUFFIX = r'.id_rsa'

Expand Down Expand Up @@ -166,6 +167,9 @@ def get_microvm_image(self, microvm_image_name, microvm):
if resource_key.endswith(self.MICROVM_IMAGE_KERNEL_FILE_SUFFIX):
microvm.kernel_file = microvm_dest_path

if resource_key.endswith(self.MICROVM_IMAGE_INITRD_FILE_SUFFIX):
microvm.initrd_file = microvm_dest_path

if resource_key.endswith(self.MICROVM_IMAGE_ROOTFS_FILE_SUFFIX):
microvm.rootfs_file = microvm_dest_path

Expand Down
Loading