#include "host/libs/config/data_image.h" #include #include #include "blkid.h" #include "common/libs/fs/shared_buf.h" #include "common/libs/utils/files.h" #include "common/libs/utils/result.h" #include "common/libs/utils/subprocess.h" #include "host/libs/config/mbr.h" #include "host/libs/vm_manager/gem5_manager.h" namespace cuttlefish { namespace { const std::string kDataPolicyUseExisting = "use_existing"; const std::string kDataPolicyCreateIfMissing = "create_if_missing"; const std::string kDataPolicyAlwaysCreate = "always_create"; const std::string kDataPolicyResizeUpTo= "resize_up_to"; const int FSCK_ERROR_CORRECTED = 1; const int FSCK_ERROR_CORRECTED_REQUIRES_REBOOT = 2; // Currently the Cuttlefish bootloaders are built only for x86 (32-bit), // ARM (QEMU only, 32-bit) and AArch64 (64-bit), and U-Boot will hard-code // these search paths. Install all bootloaders to one of these paths. // NOTE: For now, just ignore the 32-bit ARM version, as Debian doesn't // build an EFI monolith for this architecture. const std::string kBootPathIA32 = "EFI/BOOT/BOOTIA32.EFI"; const std::string kBootPathAA64 = "EFI/BOOT/BOOTAA64.EFI"; const std::string kM5 = ""; // These are the paths Debian installs the monoliths to. If another distro // uses an alternative monolith path, add it to this table const std::pair kGrubBlobTable[] = { {"/usr/lib/grub/i386-efi/monolithic/grubia32.efi", kBootPathIA32}, {"/usr/lib/grub/arm64-efi/monolithic/grubaa64.efi", kBootPathAA64}, }; // M5 checkpoint required binary file const std::pair kM5BlobTable[] = { {"/tmp/m5", kM5}, }; bool ForceFsckImage(const CuttlefishConfig& config, const std::string& data_image) { std::string fsck_path; if (config.userdata_format() == "f2fs") { fsck_path = HostBinaryPath("fsck.f2fs"); } else if (config.userdata_format() == "ext4") { fsck_path = "/sbin/e2fsck"; } int fsck_status = execute({fsck_path, "-y", "-f", data_image}); if (fsck_status & ~(FSCK_ERROR_CORRECTED|FSCK_ERROR_CORRECTED_REQUIRES_REBOOT)) { LOG(ERROR) << "`" << fsck_path << " -y -f " << data_image << "` failed with code " << fsck_status; return false; } return true; } bool NewfsMsdos(const std::string& data_image, int data_image_mb, int offset_num_mb) { off_t image_size_bytes = static_cast(data_image_mb) << 20; off_t offset_size_bytes = static_cast(offset_num_mb) << 20; image_size_bytes -= offset_size_bytes; off_t image_size_sectors = image_size_bytes / 512; auto newfs_msdos_path = HostBinaryPath("newfs_msdos"); return execute({newfs_msdos_path, "-F", "32", "-m", "0xf8", "-o", "0", "-c", "8", "-h", "255", "-u", "63", "-S", "512", "-s", std::to_string(image_size_sectors), "-C", std::to_string(data_image_mb) + "M", "-@", std::to_string(offset_size_bytes), data_image}) == 0; } bool ResizeImage(const CuttlefishConfig& config, const std::string& data_image, int data_image_mb) { auto file_mb = FileSize(data_image) >> 20; if (file_mb > data_image_mb) { LOG(ERROR) << data_image << " is already " << file_mb << " MB, will not " << "resize down."; return false; } else if (file_mb == data_image_mb) { LOG(INFO) << data_image << " is already the right size"; return true; } else { off_t raw_target = static_cast(data_image_mb) << 20; auto fd = SharedFD::Open(data_image, O_RDWR); if (fd->Truncate(raw_target) != 0) { LOG(ERROR) << "`truncate --size=" << data_image_mb << "M " << data_image << "` failed:" << fd->StrError(); return false; } bool fsck_success = ForceFsckImage(config, data_image); if (!fsck_success) { return false; } std::string resize_path; if (config.userdata_format() == "f2fs") { resize_path = HostBinaryPath("resize.f2fs"); } else if (config.userdata_format() == "ext4") { resize_path = "/sbin/resize2fs"; } int resize_status = execute({resize_path, data_image}); if (resize_status != 0) { LOG(ERROR) << "`" << resize_path << " " << data_image << "` failed with code " << resize_status; return false; } fsck_success = ForceFsckImage(config, data_image); if (!fsck_success) { return false; } } return true; } } // namespace bool CreateBlankImage( const std::string& image, int num_mb, const std::string& image_fmt) { LOG(DEBUG) << "Creating " << image; off_t image_size_bytes = static_cast(num_mb) << 20; // The newfs_msdos tool with the mandatory -C option will do the same // as below to zero the image file, so we don't need to do it here if (image_fmt != "sdcard") { auto fd = SharedFD::Open(image, O_CREAT | O_TRUNC | O_RDWR, 0666); if (fd->Truncate(image_size_bytes) != 0) { LOG(ERROR) << "`truncate --size=" << num_mb << "M " << image << "` failed:" << fd->StrError(); return false; } } if (image_fmt == "ext4") { if (execute({"/sbin/mkfs.ext4", image}) != 0) { return false; } } else if (image_fmt == "f2fs") { auto make_f2fs_path = cuttlefish::HostBinaryPath("make_f2fs"); if (execute({make_f2fs_path, "-t", image_fmt, image, "-C", "utf8", "-O", "compression,extra_attr,project_quota", "-g", "android"}) != 0) { return false; } } else if (image_fmt == "sdcard") { // Reserve 1MB in the image for the MBR and padding, to simulate what // other OSes do by default when partitioning a drive off_t offset_size_bytes = 1 << 20; image_size_bytes -= offset_size_bytes; if (!NewfsMsdos(image, num_mb, 1)) { LOG(ERROR) << "Failed to create SD-Card filesystem"; return false; } // Write the MBR after the filesystem is formatted, as the formatting tools // don't consistently preserve the image contents MasterBootRecord mbr = { .partitions = {{ .partition_type = 0xC, .first_lba = (std::uint32_t) offset_size_bytes / SECTOR_SIZE, .num_sectors = (std::uint32_t) image_size_bytes / SECTOR_SIZE, }}, .boot_signature = {0x55, 0xAA}, }; auto fd = SharedFD::Open(image, O_RDWR); if (WriteAllBinary(fd, &mbr) != sizeof(MasterBootRecord)) { LOG(ERROR) << "Writing MBR to " << image << " failed:" << fd->StrError(); return false; } } else if (image_fmt != "none") { LOG(WARNING) << "Unknown image format '" << image_fmt << "' for " << image << ", treating as 'none'."; } return true; } std::string GetFsType(const std::string& path) { std::string fs_type; blkid_cache cache; if (blkid_get_cache(&cache, NULL) < 0) { LOG(INFO) << "blkid_get_cache failed"; return fs_type; } blkid_dev dev = blkid_get_dev(cache, path.c_str(), BLKID_DEV_NORMAL); if (!dev) { LOG(INFO) << "blkid_get_dev failed"; blkid_put_cache(cache); return fs_type; } const char *type, *value; blkid_tag_iterate iter = blkid_tag_iterate_begin(dev); while (blkid_tag_next(iter, &type, &value) == 0) { if (!strcmp(type, "TYPE")) { fs_type = value; } } blkid_tag_iterate_end(iter); blkid_put_cache(cache); return fs_type; } struct DataImageTag {}; class FixedDataImagePath : public DataImagePath { public: INJECT(FixedDataImagePath(ANNOTATED(DataImageTag, std::string) path)) : path_(path) {} const std::string& Path() const override { return path_; } private: std::string path_; }; fruit::Component FixedDataImagePathComponent( const std::string* path) { return fruit::createComponent() .bind() .bindInstance>(*path); } class InitializeDataImageImpl : public InitializeDataImage { public: INJECT(InitializeDataImageImpl(const CuttlefishConfig& config, DataImagePath& data_path)) : config_(config), data_path_(data_path) {} // SetupFeature std::string Name() const override { return "InitializeDataImageImpl"; } bool Enabled() const override { return true; } private: std::unordered_set Dependencies() const override { return {}; } bool Setup() override { auto action = ChooseAction(); if (!action.ok()) { LOG(ERROR) << "Failed to select a userdata processing action: " << action.error(); return false; } auto result = EvaluateAction(*action); if (!result.ok()) { LOG(ERROR) << "Failed to evaluate userdata action: " << result.error(); return false; } return true; } private: enum class DataImageAction { kNoAction, kCreateImage, kResizeImage }; Result ChooseAction() { if (config_.data_policy() == kDataPolicyAlwaysCreate) { return DataImageAction::kCreateImage; } if (!FileHasContent(data_path_.Path())) { if (config_.data_policy() == kDataPolicyUseExisting) { return CF_ERR("A data image must exist to use -data_policy=" << kDataPolicyUseExisting); } else if (config_.data_policy() == kDataPolicyResizeUpTo) { return CF_ERR(data_path_.Path() << " does not exist, but resizing was requested"); } return DataImageAction::kCreateImage; } if (GetFsType(data_path_.Path()) != config_.userdata_format()) { CF_EXPECT(config_.data_policy() == kDataPolicyResizeUpTo, "Changing the fs format is incompatible with -data_policy=" << kDataPolicyResizeUpTo); return DataImageAction::kCreateImage; } if (config_.data_policy() == kDataPolicyResizeUpTo) { return DataImageAction::kResizeImage; } return DataImageAction::kNoAction; } Result EvaluateAction(DataImageAction action) { switch (action) { case DataImageAction::kNoAction: LOG(DEBUG) << data_path_.Path() << " exists. Not creating it."; return {}; case DataImageAction::kCreateImage: { RemoveFile(data_path_.Path()); CF_EXPECT(config_.blank_data_image_mb() != 0, "Expected `-blank_data_image_mb` to be set for " "image creation."); CF_EXPECT( CreateBlankImage(data_path_.Path(), config_.blank_data_image_mb(), config_.userdata_format()), "Failed to create a blank image at \"" << data_path_.Path() << "\" with size " << config_.blank_data_image_mb() << " and format \"" << config_.userdata_format() << "\""); return {}; } case DataImageAction::kResizeImage: { CF_EXPECT(config_.blank_data_image_mb() != 0, "Expected `-blank_data_image_mb` to be set for " "image resizing."); CF_EXPECT(ResizeImage(config_, data_path_.Path(), config_.blank_data_image_mb()), "Failed to resize \"" << data_path_.Path() << "\" to " << config_.blank_data_image_mb() << " MB"); return {}; } } } const CuttlefishConfig& config_; DataImagePath& data_path_; }; fruit::Component, InitializeDataImage> InitializeDataImageComponent() { return fruit::createComponent() .addMultibinding() .bind(); } struct MiscImageTag {}; class FixedMiscImagePath : public MiscImagePath { public: INJECT(FixedMiscImagePath(ANNOTATED(MiscImageTag, std::string) path)) : path_(path) {} const std::string& Path() const override { return path_; } private: std::string path_; }; class InitializeMiscImageImpl : public InitializeMiscImage { public: INJECT(InitializeMiscImageImpl(MiscImagePath& misc_path)) : misc_path_(misc_path) {} // SetupFeature std::string Name() const override { return "InitializeMiscImageImpl"; } bool Enabled() const override { return true; } private: std::unordered_set Dependencies() const override { return {}; } bool Setup() override { bool misc_exists = FileHasContent(misc_path_.Path()); if (misc_exists) { LOG(DEBUG) << "misc partition image: use existing at \"" << misc_path_.Path() << "\""; return true; } LOG(DEBUG) << "misc partition image: creating empty at \"" << misc_path_.Path() << "\""; if (!CreateBlankImage(misc_path_.Path(), 1 /* mb */, "none")) { LOG(ERROR) << "Failed to create misc image"; return false; } return true; } private: MiscImagePath& misc_path_; }; fruit::Component FixedMiscImagePathComponent( const std::string* path) { return fruit::createComponent() .bind() .bindInstance>(*path); } fruit::Component, InitializeMiscImage> InitializeMiscImageComponent() { return fruit::createComponent() .addMultibinding() .bind(); } struct EspImageTag {}; struct KernelPathTag {}; struct InitRamFsTag {}; struct RootFsTag {}; struct ConfigTag {}; class InitializeEspImageImpl : public InitializeEspImage { public: INJECT(InitializeEspImageImpl(ANNOTATED(EspImageTag, std::string) esp_image, ANNOTATED(KernelPathTag, std::string) kernel_path, ANNOTATED(InitRamFsTag, std::string) initramfs_path, ANNOTATED(RootFsTag, std::string) rootfs_path, ANNOTATED(ConfigTag, const CuttlefishConfig *) config)) : esp_image_(esp_image), kernel_path_(kernel_path), initramfs_path_(initramfs_path), rootfs_path_(rootfs_path), config_(config){} // SetupFeature std::string Name() const override { return "InitializeEspImageImpl"; } std::unordered_set Dependencies() const override { return {}; } bool Enabled() const override { return !rootfs_path_.empty(); } protected: bool Setup() override { bool esp_exists = FileHasContent(esp_image_); if (esp_exists) { LOG(DEBUG) << "esp partition image: use existing"; return true; } LOG(DEBUG) << "esp partition image: creating default"; // newfs_msdos won't make a partition smaller than 257 mb // this should be enough for anybody.. auto tmp_esp_image = esp_image_ + ".tmp"; if (!NewfsMsdos(tmp_esp_image, 257 /* mb */, 0 /* mb (offset) */)) { LOG(ERROR) << "Failed to create filesystem for " << tmp_esp_image; return false; } // For licensing and build reproducibility reasons, pick up the bootloaders // from the host Linux distribution (if present) and pack them into the // automatically generated ESP. If the user wants their own bootloaders, // they can use -esp_image=/path/to/esp.img to override, so we don't need // to accommodate customizations of this packing process. int success; const std::pair *kBlobTable; std::size_t size; // Skip GRUB on Gem5 if (config_->vm_manager() != vm_manager::Gem5Manager::name()){ // Currently we only support Debian based distributions, and GRUB is built // for those distros to always load grub.cfg from EFI/debian/grub.cfg, and // nowhere else. If you want to add support for other distros, make the // extra directories below and copy the initial grub.cfg there as well auto mmd = HostBinaryPath("mmd"); success = execute({mmd, "-i", tmp_esp_image, "EFI", "EFI/BOOT", "EFI/debian"}); if (success != 0) { LOG(ERROR) << "Failed to create directories in " << tmp_esp_image; return false; } size = sizeof(kGrubBlobTable)/sizeof(const std::pair); kBlobTable = kGrubBlobTable; } else { size = sizeof(kM5BlobTable)/sizeof(const std::pair); kBlobTable = kM5BlobTable; } // The grub binaries are small, so just copy all the architecture blobs // we can find, which minimizes complexity. If the user removed the grub bin // package from their system, the ESP will be empty and Other OS will not be // supported auto mcopy = HostBinaryPath("mcopy"); bool copied = false; for (int i=0; ivm_manager() != vm_manager::Gem5Manager::name()){ auto grub_cfg = DefaultHostArtifactsPath("etc/grub/grub.cfg"); CHECK(FileExists(grub_cfg)) << "Missing file " << grub_cfg << "!"; success = execute({mcopy, "-i", tmp_esp_image, "-s", grub_cfg, "::EFI/debian/"}); if (success != 0) { LOG(ERROR) << "Failed to copy " << grub_cfg << " to " << tmp_esp_image; return false; } } if (!kernel_path_.empty()) { success = execute( {mcopy, "-i", tmp_esp_image, "-s", kernel_path_, "::vmlinuz"}); if (success != 0) { LOG(ERROR) << "Failed to copy " << kernel_path_ << " to " << tmp_esp_image; return false; } if (!initramfs_path_.empty()) { success = execute({mcopy, "-i", tmp_esp_image, "-s", initramfs_path_, "::initrd.img"}); if (success != 0) { LOG(ERROR) << "Failed to copy " << initramfs_path_ << " to " << tmp_esp_image; return false; } } } if (!cuttlefish::RenameFile(tmp_esp_image, esp_image_)) { LOG(ERROR) << "Renaming " << tmp_esp_image << " to " << esp_image_ << " failed"; return false; } return true; } private: std::string esp_image_; std::string kernel_path_; std::string initramfs_path_; std::string rootfs_path_; const CuttlefishConfig* config_; }; fruit::Component, InitializeEspImage> InitializeEspImageComponent( const std::string* esp_image, const std::string* kernel_path, const std::string* initramfs_path, const std::string* rootfs_path, const CuttlefishConfig* config) { return fruit::createComponent() .addMultibinding() .bind() .bindInstance>(*esp_image) .bindInstance>(*kernel_path) .bindInstance>( *initramfs_path) .bindInstance>(*rootfs_path) .bindInstance>(*config); } } // namespace cuttlefish