| // Copyright 2017 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "src/virtualization/bin/vmm/linux.h" | |
| #include <endian.h> | |
| #include <fcntl.h> | |
| #include <sys/stat.h> | |
| #include <zircon/boot/e820.h> | |
| #include <fbl/unique_fd.h> | |
| #include "src/lib/fxl/strings/string_printf.h" | |
| #include "src/virtualization/bin/vmm/bits.h" | |
| #include "src/virtualization/bin/vmm/guest.h" | |
| __BEGIN_CDECLS; | |
| #include <libfdt.h> | |
| __END_CDECLS; | |
| #if __aarch64__ | |
| // This address works for direct-mapping of host memory. This address is chosen | |
| // to ensure that we do not collide with the mapping of the host kernel. | |
| static constexpr uintptr_t kKernelOffset = 0x2080000; | |
| #elif __x86_64__ | |
| static constexpr uintptr_t kKernelOffset = 0x200000; | |
| #include "src/virtualization/bin/vmm/arch/x64/acpi.h" | |
| #include "src/virtualization/bin/vmm/arch/x64/e820.h" | |
| #endif | |
| static constexpr uint8_t kLoaderTypeUnspecified = 0xff; // Unknown bootloader | |
| static constexpr uint16_t kMinBootProtocol = 0x200; // bzImage boot protocol | |
| static constexpr uint16_t kBootFlagMagic = 0xaa55; | |
| static constexpr uint32_t kHeaderMagic = 0x53726448; | |
| static constexpr uintptr_t kEntryOffset = 0x200; | |
| __UNUSED static constexpr uintptr_t kE820MapOffset = 0x02d0; | |
| __UNUSED static constexpr size_t kMaxE820Entries = 128; | |
| __UNUSED static constexpr size_t kSectorSize = 512; | |
| static constexpr uint16_t kMzSignature = 0x5a4d; // MZ | |
| static constexpr uint32_t kMzMagic = 0x644d5241; // ARM\x64 | |
| struct SetupData { | |
| enum Type : uint32_t { | |
| Dtb = 2, | |
| }; | |
| uint64_t next; | |
| Type type; | |
| uint32_t len; | |
| uint8_t data[0]; | |
| } __PACKED; | |
| static constexpr char kDtbPath[] = "/pkg/data/board.dtb"; | |
| static constexpr uintptr_t kRamdiskOffset = 0x4000000; | |
| static constexpr uintptr_t kDtbOffset = kRamdiskOffset - (PAGE_SIZE * 2); | |
| static constexpr uintptr_t kDtbOverlayOffset = kDtbOffset - (PAGE_SIZE * 2); | |
| static constexpr uintptr_t kDtbBootParamsOffset = kDtbOffset + sizeof(SetupData); | |
| // clang-format off | |
| // For the Linux x86 boot protocol, see: | |
| // https://www.kernel.org/doc/Documentation/x86/boot.txt | |
| // https://www.kernel.org/doc/Documentation/x86/zero-page.txt | |
| enum Bp8 : uintptr_t { | |
| VIDEO_MODE = 0x0006, // Original video mode | |
| VIDEO_COLS = 0x0007, // Original video cols | |
| VIDEO_LINES = 0x000e, // Original video lines | |
| E820_COUNT = 0x01e8, // Number of entries in e820 map | |
| SETUP_SECTS = 0x01f1, // Size of real mode kernel in sectors | |
| LOADER_TYPE = 0x0210, // Type of bootloader | |
| LOADFLAGS = 0x0211, // Boot protocol flags | |
| RELOCATABLE = 0x0234, // Whether the kernel relocatable | |
| }; | |
| enum Bp16 : uintptr_t { | |
| BOOTFLAG = 0x01fe, // Bootflag, should match BOOT_FLAG_MAGIC | |
| VERSION = 0x0206, // Boot protocol version | |
| XLOADFLAGS = 0x0236, // Extended boot protocol flags | |
| }; | |
| enum Bp32 : uintptr_t { | |
| SYSSIZE = 0x01f4, // Size of protected-mode code in units of 16 bytes | |
| HEADER = 0x0202, // Header, should match HEADER_MAGIC | |
| RAMDISK_IMAGE = 0x0218, // RAM disk image address | |
| RAMDISK_SIZE = 0x021c, // RAM disk image size | |
| COMMAND_LINE = 0x0228, // Pointer to command line args string | |
| KERNEL_ALIGN = 0x0230, // Kernel alignment | |
| }; | |
| enum Bp64 : uintptr_t { | |
| SETUP_DATA = 0x0250, // Physical address to linked list of SetupData | |
| }; | |
| enum Lf : uint8_t { | |
| LOAD_HIGH = 1u << 0, // Protected mode code loads at 0x100000 | |
| }; | |
| enum Xlf : uint16_t { | |
| KERNEL_64 = 1u << 0, // Has legacy 64-bit entry point at 0x200 | |
| CAN_BE_LOADED_ABOVE_4G = 1u << 1, // Kernel/boot_params/cmdline/ramdisk can be above 4G | |
| }; | |
| // clang-format on | |
| static uint8_t& bp(const PhysMem& phys_mem, Bp8 off) { | |
| return *phys_mem.as<uint8_t>(kKernelOffset + off); | |
| } | |
| static uint16_t& bp(const PhysMem& phys_mem, Bp16 off) { | |
| return *phys_mem.as<uint16_t>(kKernelOffset + off); | |
| } | |
| static uint32_t& bp(const PhysMem& phys_mem, Bp32 off) { | |
| return *phys_mem.as<uint32_t>(kKernelOffset + off); | |
| } | |
| static uint64_t& bp(const PhysMem& phys_mem, Bp64 off) { | |
| return *phys_mem.as<uint64_t>(kKernelOffset + off); | |
| } | |
| static bool is_boot_params(const PhysMem& phys_mem) { | |
| return bp(phys_mem, BOOTFLAG) == kBootFlagMagic && bp(phys_mem, HEADER) == kHeaderMagic; | |
| } | |
| // MZ header used to boot ARM64 kernels. | |
| // | |
| // See: https://www.kernel.org/doc/Documentation/arm64/booting.txt. | |
| struct MzHeader { | |
| uint32_t code0; | |
| uint32_t code1; | |
| uint64_t kernel_off; | |
| uint64_t kernel_len; | |
| uint64_t flags; | |
| uint64_t reserved0; | |
| uint64_t reserved1; | |
| uint64_t reserved2; | |
| uint32_t magic; | |
| uint32_t pe_off; | |
| } __PACKED; | |
| static_assert(sizeof(MzHeader) == 64, ""); | |
| static bool is_mz(const MzHeader* header) { | |
| return (header->code0 & UINT16_MAX) == kMzSignature && header->kernel_len > sizeof(MzHeader) && | |
| header->magic == kMzMagic && header->pe_off >= sizeof(MzHeader); | |
| } | |
| static inline bool is_within(uintptr_t x, uintptr_t addr, uintptr_t size) { | |
| return x >= addr && x < addr + size; | |
| } | |
| static zx_status_t read_fd(const int fd, const PhysMem& phys_mem, const uintptr_t off, | |
| size_t* file_size) { | |
| struct stat stat; | |
| ssize_t ret = fstat(fd, &stat); | |
| if (ret < 0) { | |
| FX_LOGS(ERROR) << "Failed to stat file"; | |
| return ZX_ERR_IO; | |
| } | |
| ret = read(fd, phys_mem.as<void>(off, stat.st_size), stat.st_size); | |
| if (ret != stat.st_size) { | |
| FX_LOGS(ERROR) << "Failed to read file"; | |
| return ZX_ERR_IO; | |
| } | |
| *file_size = stat.st_size; | |
| return ZX_OK; | |
| } | |
| zx_status_t load_kernel(const std::string& kernel_path, const PhysMem& phys_mem, | |
| const uintptr_t kernel_off) { | |
| fbl::unique_fd fd(open(kernel_path.c_str(), O_RDONLY)); | |
| if (!fd) { | |
| FX_LOGS(ERROR) << "Failed to open kernel image " << kernel_path; | |
| return ZX_ERR_IO; | |
| } | |
| size_t kernel_size; | |
| read_fd(fd.get(), phys_mem, kernel_off, &kernel_size); | |
| if (is_within(kDtbOffset, kernel_off, kernel_size)) { | |
| FX_LOGS(ERROR) << "Kernel location overlaps DTB location"; | |
| return ZX_ERR_OUT_OF_RANGE; | |
| } | |
| return ZX_OK; | |
| } | |
| static zx_status_t read_device_tree(const int fd, const PhysMem& phys_mem, const uintptr_t off, | |
| const uintptr_t limit, void** dtb, size_t* dtb_size) { | |
| zx_status_t status = read_fd(fd, phys_mem, off, dtb_size); | |
| if (status != ZX_OK) { | |
| FX_LOGS(ERROR) << "Failed to read device tree"; | |
| return status; | |
| } | |
| if (off + *dtb_size > limit) { | |
| FX_LOGS(ERROR) << "Device tree is too large"; | |
| return ZX_ERR_OUT_OF_RANGE; | |
| } | |
| *dtb = phys_mem.as<void>(off, *dtb_size); | |
| int ret = fdt_check_header(*dtb); | |
| if (ret != 0) { | |
| FX_LOGS(ERROR) << "Invalid device tree " << ret; | |
| return ZX_ERR_IO_DATA_INTEGRITY; | |
| } | |
| return ZX_OK; | |
| } | |
| static zx_status_t read_boot_params(const PhysMem& phys_mem, uintptr_t* guest_ip) { | |
| // Validate kernel configuration. | |
| uint16_t xloadflags = bp(phys_mem, XLOADFLAGS); | |
| if (~xloadflags & (KERNEL_64 | CAN_BE_LOADED_ABOVE_4G)) { | |
| FX_LOGS(ERROR) << "Unsupported Linux kernel"; | |
| return ZX_ERR_NOT_SUPPORTED; | |
| } | |
| uint16_t protocol = bp(phys_mem, VERSION); | |
| uint8_t loadflags = bp(phys_mem, LOADFLAGS); | |
| if (protocol < kMinBootProtocol || !(loadflags & LOAD_HIGH)) { | |
| FX_LOGS(ERROR) << "Linux kernel is not a bzImage"; | |
| return ZX_ERR_NOT_SUPPORTED; | |
| } | |
| if (bp(phys_mem, RELOCATABLE) == 0) { | |
| FX_LOGS(ERROR) << "Linux kernel is not relocatable"; | |
| return ZX_ERR_NOT_SUPPORTED; | |
| } | |
| uint64_t kernel_align = bp(phys_mem, KERNEL_ALIGN); | |
| if (kKernelOffset % kernel_align != 0) { | |
| FX_LOGS(ERROR) << "Linux kernel has unsupported alignment"; | |
| return ZX_ERR_NOT_SUPPORTED; | |
| } | |
| // Calculate the offset to the protected mode kernel. | |
| uint8_t setup_sects = bp(phys_mem, SETUP_SECTS); | |
| if (setup_sects == 0) { | |
| // 0 here actually means 4, see boot.txt. | |
| setup_sects = 4; | |
| } | |
| uintptr_t setup_off = (setup_sects + 1) * kSectorSize; | |
| *guest_ip = kKernelOffset + kEntryOffset + setup_off; | |
| return ZX_OK; | |
| } | |
| static zx_status_t write_boot_params(const PhysMem& phys_mem, const DevMem& dev_mem, | |
| const std::string& cmdline, const int dtb_overlay_fd, | |
| const size_t initrd_size) { | |
| // Set type of bootloader. | |
| bp(phys_mem, LOADER_TYPE) = kLoaderTypeUnspecified; | |
| // Zero video, columns and lines to skip early video init. | |
| bp(phys_mem, VIDEO_MODE) = 0; | |
| bp(phys_mem, VIDEO_COLS) = 0; | |
| bp(phys_mem, VIDEO_LINES) = 0; | |
| // Set the address and size of the initial RAM disk. | |
| if (initrd_size > 0) { | |
| bp(phys_mem, RAMDISK_IMAGE) = kRamdiskOffset; | |
| bp(phys_mem, RAMDISK_SIZE) = static_cast<uint32_t>(initrd_size); | |
| } | |
| // Copy the command line string. | |
| size_t cmdline_len = cmdline.size() + 1; | |
| if (phys_mem.size() < PAGE_SIZE || cmdline_len > PAGE_SIZE) { | |
| FX_LOGS(ERROR) << "Command line is too long"; | |
| return ZX_ERR_OUT_OF_RANGE; | |
| } | |
| uint32_t cmdline_off = phys_mem.size() - PAGE_SIZE; | |
| memcpy(phys_mem.as<void>(cmdline_off, cmdline_len), cmdline.c_str(), cmdline_len); | |
| bp(phys_mem, COMMAND_LINE) = cmdline_off; | |
| // If specified, load a device tree overlay. | |
| if (dtb_overlay_fd >= 0) { | |
| void* dtb; | |
| size_t dtb_size; | |
| zx_status_t status = read_device_tree(dtb_overlay_fd, phys_mem, kDtbBootParamsOffset, | |
| kRamdiskOffset, &dtb, &dtb_size); | |
| if (status != ZX_OK) { | |
| FX_LOGS(ERROR) << "Failed to read device tree"; | |
| return status; | |
| } | |
| auto setup_data = phys_mem.as<SetupData>(kDtbOffset); | |
| setup_data->type = SetupData::Dtb; | |
| setup_data->len = dtb_size; | |
| bp(phys_mem, SETUP_DATA) = kDtbOffset; | |
| } | |
| #if __x86_64__ | |
| // Setup e820 memory map. | |
| E820Map e820_map(phys_mem.size(), dev_mem); | |
| for (const auto& range : dev_mem) { | |
| e820_map.AddReservedRegion(range.addr, range.size); | |
| } | |
| size_t e820_entries = e820_map.size(); | |
| if (e820_entries > kMaxE820Entries) { | |
| FX_LOGS(ERROR) << "Not enough space for e820 memory map"; | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| bp(phys_mem, E820_COUNT) = static_cast<uint8_t>(e820_entries); | |
| const size_t e820_size = e820_entries * sizeof(e820entry_t); | |
| e820entry_t* e820_addr = phys_mem.as<e820entry_t>(kKernelOffset + kE820MapOffset, e820_size); | |
| e820_map.copy(e820_addr); | |
| #endif | |
| return ZX_OK; | |
| } | |
| static zx_status_t read_mz(const PhysMem& phys_mem, uintptr_t* guest_ip) { | |
| MzHeader* mz_header = phys_mem.as<MzHeader>(kKernelOffset); | |
| if (!is_mz(mz_header)) { | |
| return ZX_ERR_NOT_SUPPORTED; | |
| } | |
| *guest_ip = kKernelOffset; | |
| return ZX_OK; | |
| } | |
| static void device_tree_error_msg(const char* property_name) { | |
| FX_LOGS(ERROR) << "Failed to add \"" << property_name << "\" to device " | |
| << "tree, space must be reserved in the device tree"; | |
| } | |
| static zx_status_t add_memory_entry(void* dtb, int memory_off, zx_gpaddr_t addr, size_t size) { | |
| uint64_t entry[2]; | |
| entry[0] = htobe64(addr); | |
| entry[1] = htobe64(size); | |
| int ret = fdt_appendprop(dtb, memory_off, "reg", entry, sizeof(entry)); | |
| if (ret < 0) { | |
| device_tree_error_msg("reg"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| return ZX_OK; | |
| } | |
| static zx_status_t load_device_tree(const int dtb_fd, | |
| const fuchsia::virtualization::GuestConfig& cfg, | |
| const PhysMem& phys_mem, const DevMem& dev_mem, | |
| const std::vector<PlatformDevice*>& devices, | |
| const std::string& cmdline, const int dtb_overlay_fd, | |
| const size_t initrd_size) { | |
| void* dtb; | |
| size_t dtb_size; | |
| zx_status_t status = | |
| read_device_tree(dtb_fd, phys_mem, kDtbOffset, kRamdiskOffset, &dtb, &dtb_size); | |
| if (status != ZX_OK) { | |
| FX_LOGS(ERROR) << "Failed to read device tree"; | |
| return status; | |
| } | |
| // If specified, load a device tree overlay. | |
| if (dtb_overlay_fd >= 0) { | |
| void* dtb_overlay; | |
| status = read_device_tree(dtb_overlay_fd, phys_mem, kDtbOverlayOffset, kDtbOffset, &dtb_overlay, | |
| &dtb_size); | |
| if (status != ZX_OK) { | |
| FX_LOGS(ERROR) << "Failed to read device tree overlay"; | |
| return status; | |
| } | |
| int ret = fdt_overlay_apply(dtb, dtb_overlay); | |
| if (ret != 0) { | |
| FX_LOGS(ERROR) << "Failed to apply device tree overlay " << ret; | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| } | |
| int off = fdt_path_offset(dtb, "/chosen"); | |
| if (off < 0) { | |
| FX_LOGS(ERROR) << "Failed to find \"/chosen\" in device tree"; | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| // Add command line to device tree. | |
| int ret = fdt_setprop_string(dtb, off, "bootargs", cmdline.c_str()); | |
| if (ret != 0) { | |
| device_tree_error_msg("bootargs"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| if (initrd_size > 0) { | |
| // Add the memory range of the initial RAM disk. | |
| ret = fdt_setprop_u64(dtb, off, "linux,initrd-start", kRamdiskOffset); | |
| if (ret != 0) { | |
| device_tree_error_msg("linux,initrd-start"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| ret = fdt_setprop_u64(dtb, off, "linux,initrd-end", kRamdiskOffset + initrd_size); | |
| if (ret != 0) { | |
| device_tree_error_msg("linux,initrd-end"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| } | |
| // Add CPUs to device tree. | |
| int cpus_off = fdt_path_offset(dtb, "/cpus"); | |
| if (cpus_off < 0) { | |
| FX_LOGS(ERROR) << "Failed to find \"/cpus\" in device tree"; | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| for (int cpu = cfg.cpus() - 1; cpu >= 0; --cpu) { | |
| std::string name = fxl::StringPrintf("cpu@%d", cpu); | |
| int cpu_off = fdt_add_subnode(dtb, cpus_off, name.c_str()); | |
| if (cpu_off < 0) { | |
| device_tree_error_msg("cpu"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| ret = fdt_setprop_string(dtb, cpu_off, "device_type", "cpu"); | |
| if (ret != 0) { | |
| device_tree_error_msg("device_type"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| ret = fdt_setprop_string(dtb, cpu_off, "compatible", "arm,armv8"); | |
| if (ret != 0) { | |
| device_tree_error_msg("compatible"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| ret = fdt_setprop_u32(dtb, cpu_off, "reg", static_cast<uint32_t>(cpu)); | |
| if (ret != 0) { | |
| device_tree_error_msg("reg"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| ret = fdt_setprop_string(dtb, cpu_off, "enable-method", "psci"); | |
| if (ret != 0) { | |
| device_tree_error_msg("enable-method"); | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| } | |
| // Add memory to device tree. | |
| int root_off = fdt_path_offset(dtb, "/"); | |
| if (root_off < 0) { | |
| FX_LOGS(ERROR) << "Failed to find root node in device tree"; | |
| return ZX_ERR_BAD_STATE; | |
| } | |
| auto yield = [&status, dtb, root_off](zx_gpaddr_t addr, size_t size) { | |
| if (status != ZX_OK) { | |
| return; | |
| } | |
| std::string name = fxl::StringPrintf("memory@%lx", addr); | |
| int memory_off = fdt_add_subnode(dtb, root_off, name.c_str()); | |
| if (memory_off < 0) { | |
| status = ZX_ERR_BAD_STATE; | |
| return; | |
| } | |
| int ret = fdt_setprop_string(dtb, memory_off, "device_type", "memory"); | |
| if (ret != 0) { | |
| status = ZX_ERR_BAD_STATE; | |
| return; | |
| } | |
| status = add_memory_entry(dtb, memory_off, addr, size); | |
| }; | |
| for (const fuchsia::virtualization::MemorySpec& spec : cfg.memory()) { | |
| // Do not use device memory when yielding normal memory. | |
| if (spec.policy != fuchsia::virtualization::MemoryPolicy::HOST_DEVICE) { | |
| dev_mem.YieldInverseRange(spec.base, spec.size, yield); | |
| if (status != ZX_OK) { | |
| return status; | |
| } | |
| } | |
| } | |
| // Add all platform devices to device tree. | |
| for (auto device : devices) { | |
| status = device->ConfigureDtb(dtb); | |
| if (status != ZX_OK) { | |
| return status; | |
| } | |
| } | |
| return ZX_OK; | |
| } | |
| static std::string linux_cmdline(std::string cmdline) { | |
| #if __x86_64__ | |
| return fxl::StringPrintf("acpi_rsdp=%#lx %s", kAcpiOffset, cmdline.c_str()); | |
| #else | |
| return cmdline; | |
| #endif | |
| } | |
| zx_status_t setup_linux(const fuchsia::virtualization::GuestConfig& cfg, const PhysMem& phys_mem, | |
| const DevMem& dev_mem, const std::vector<PlatformDevice*>& devices, | |
| uintptr_t* guest_ip, uintptr_t* boot_ptr) { | |
| // Read the kernel image. | |
| zx_status_t status = load_kernel(cfg.kernel_path(), phys_mem, kKernelOffset); | |
| if (status != ZX_OK) { | |
| return status; | |
| } | |
| size_t initrd_size = 0; | |
| if (cfg.has_ramdisk_path()) { | |
| fbl::unique_fd initrd_fd(open(cfg.ramdisk_path().c_str(), O_RDONLY)); | |
| if (!initrd_fd) { | |
| FX_LOGS(ERROR) << "Failed to open initial RAM disk " << cfg.ramdisk_path(); | |
| return ZX_ERR_IO; | |
| } | |
| status = read_fd(initrd_fd.get(), phys_mem, kRamdiskOffset, &initrd_size); | |
| if (status != ZX_OK) { | |
| FX_LOGS(ERROR) << "Failed to read initial RAM disk " << cfg.ramdisk_path(); | |
| return status; | |
| } | |
| } | |
| fbl::unique_fd dtb_overlay_fd; | |
| if (cfg.has_dtb_overlay_path()) { | |
| dtb_overlay_fd.reset(open(cfg.dtb_overlay_path().c_str(), O_RDONLY)); | |
| if (!dtb_overlay_fd) { | |
| FX_LOGS(ERROR) << "Failed to open device tree overlay " << cfg.dtb_overlay_path(); | |
| return ZX_ERR_IO; | |
| } | |
| } | |
| const std::string cmdline = linux_cmdline(cfg.cmdline()); | |
| if (is_boot_params(phys_mem)) { | |
| status = read_boot_params(phys_mem, guest_ip); | |
| if (status != ZX_OK) { | |
| return status; | |
| } | |
| status = write_boot_params(phys_mem, dev_mem, cmdline, dtb_overlay_fd.get(), initrd_size); | |
| if (status != ZX_OK) { | |
| return status; | |
| } | |
| *boot_ptr = kKernelOffset; | |
| } else { | |
| status = read_mz(phys_mem, guest_ip); | |
| if (status != ZX_OK) { | |
| return status; | |
| } | |
| fbl::unique_fd dtb_fd(open(kDtbPath, O_RDONLY)); | |
| if (!dtb_fd) { | |
| FX_LOGS(ERROR) << "Failed to open device tree " << kDtbPath; | |
| return ZX_ERR_IO; | |
| } | |
| status = load_device_tree(dtb_fd.get(), cfg, phys_mem, dev_mem, devices, cmdline, | |
| dtb_overlay_fd.get(), initrd_size); | |
| if (status != ZX_OK) { | |
| return status; | |
| } | |
| *boot_ptr = kDtbOffset; | |
| } | |
| return ZX_OK; | |
| } |