| // Copyright 2019 The Chromium OS 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 "biod/cros_fp_updater.h" |
| |
| #include <fcntl.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| |
| #include <string> |
| #include <utility> |
| |
| #include <base/command_line.h> |
| #include <base/logging.h> |
| #include <base/process/launch.h> |
| #include <base/time/time.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/strings/string_split.h> |
| #include <chromeos/ec/ec_commands.h> |
| #include <cros_config/cros_config_interface.h> |
| |
| #include "biod/cros_fp_device.h" |
| #include "biod/cros_fp_firmware.h" |
| #include "biod/ec_command.h" |
| #include "biod/update_reason.h" |
| |
| namespace { |
| |
| constexpr base::TimeDelta kBootSplashScreenLaunchTimeout = |
| base::TimeDelta::FromSeconds(10); |
| |
| constexpr char kFlashromPath[] = "/usr/sbin/flashrom"; |
| constexpr char kRebootFile[] = "/tmp/force_reboot_after_fw_update"; |
| |
| constexpr char kUpdateDisableFile[] = "/opt/google/biod/fw/.disable_fp_updater"; |
| constexpr char kFirmwareLegacyBoardPattern[] = "*_fp"; |
| constexpr char kFirmwareGlobSuffix[] = "_*.bin"; |
| |
| bool UpdateImage(const biod::CrosFpDeviceUpdate& ec_dev, |
| const biod::CrosFpBootUpdateCtrl& boot_ctrl, |
| const biod::CrosFpFirmware& fw, |
| enum ec_current_image image) { |
| if (boot_ctrl.TriggerBootUpdateSplash()) { |
| DLOG(INFO) << "Successfully launched update splash screen."; |
| } else { |
| DLOG(ERROR) << "Failed to launch boot update splash screen, continuing."; |
| } |
| if (!ec_dev.Flash(fw, image)) { |
| LOG(ERROR) << "Failed to flash " |
| << biod::CrosFpDeviceUpdate::EcCurrentImageToString(image) |
| << ", aborting."; |
| return false; |
| } |
| |
| // If we updated the FW, we need to reboot (b/119222361). |
| // We only reboot if we succeed, since we do not want to |
| // create a reboot loop. |
| if (boot_ctrl.ScheduleReboot()) { |
| DLOG(INFO) << "Successfully scheduled reboot after update."; |
| } else { |
| DLOG(ERROR) << "Failed to schedule reboot after update, continuing."; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| namespace biod { |
| |
| constexpr char kCrosConfigFPPath[] = "/fingerprint"; |
| constexpr char kCrosConfigFPBoard[] = "board"; |
| constexpr char kCrosConfigFPLocation[] = "sensor-location"; |
| |
| std::string CrosFpDeviceUpdate::EcCurrentImageToString( |
| enum ec_current_image image) { |
| switch (image) { |
| case EC_IMAGE_UNKNOWN: |
| return "UNKNOWN"; |
| case EC_IMAGE_RO: |
| return "RO"; |
| case EC_IMAGE_RW: |
| return "RW"; |
| default: |
| return "INVALID"; |
| } |
| NOTREACHED(); |
| } |
| |
| bool CrosFpDeviceUpdate::GetVersion(CrosFpDevice::EcVersion* ecver) const { |
| DCHECK(ecver != nullptr); |
| |
| auto fd = base::ScopedFD(open(CrosFpDevice::kCrosFpPath, O_RDWR | O_CLOEXEC)); |
| if (!fd.is_valid()) { |
| LOG(ERROR) << "Failed to open fingerprint device, while fetching version."; |
| return false; |
| } |
| |
| if (!biod::CrosFpDevice::GetVersion(fd, ecver)) { |
| LOG(ERROR) << "Failed to read fingerprint version."; |
| return false; |
| } |
| return true; |
| } |
| |
| bool CrosFpDeviceUpdate::IsFlashProtectEnabled(bool* status) const { |
| DCHECK(status != nullptr); |
| |
| auto fd = base::ScopedFD(open(CrosFpDevice::kCrosFpPath, O_RDWR | O_CLOEXEC)); |
| if (!fd.is_valid()) { |
| LOG(ERROR) << "Failed to open fingerprint device, while fetching " |
| "flashprotect status."; |
| return false; |
| } |
| |
| biod::EcCommand<struct ec_params_flash_protect, |
| struct ec_response_flash_protect> |
| fp_cmd(EC_CMD_FLASH_PROTECT, EC_VER_FLASH_PROTECT); |
| fp_cmd.Req()->mask = 0; |
| fp_cmd.Req()->flags = 0; |
| if (!fp_cmd.Run(fd.get())) { |
| LOG(ERROR) << "Failed to fetch fingerprint flashprotect flags."; |
| return false; |
| } |
| *status = fp_cmd.Resp()->flags & EC_FLASH_PROTECT_RO_NOW; |
| return true; |
| } |
| |
| bool CrosFpDeviceUpdate::Flash(const CrosFpFirmware& fw, |
| enum ec_current_image image) const { |
| DCHECK(image == EC_IMAGE_RO || image == EC_IMAGE_RW); |
| |
| std::string image_str = EcCurrentImageToString(image); |
| |
| LOG(INFO) << "Flashing " << image_str << " of FPMCU."; |
| |
| base::CommandLine cmd{base::FilePath(kFlashromPath)}; |
| cmd.AppendSwitch("fast-verify"); |
| cmd.AppendSwitchASCII("programmer", "ec:type=fp"); |
| cmd.AppendSwitchASCII("image", "EC_" + image_str); |
| |
| // The write switch does not work with --write=<PATH> syntax. |
| // It must appear as --write <PATH>. |
| cmd.AppendSwitch("write"); |
| cmd.AppendArgPath(fw.GetPath()); |
| |
| DLOG(INFO) << "Launching '" << cmd.GetCommandLineString() << "'."; |
| |
| // TODO(b/130026657): Impose timeout on flashrom. |
| std::string cmd_output; |
| bool status = base::GetAppOutputAndError(cmd, &cmd_output); |
| const auto lines = base::SplitStringPiece( |
| cmd_output, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| for (const auto line : lines) { |
| LOG(INFO) << cmd.GetProgram().BaseName().value() << ": " << line; |
| } |
| if (!status) { |
| LOG(ERROR) << "FPMCU flash utility failed."; |
| return false; |
| } |
| return true; |
| } |
| |
| // Show splashscreen about critical update to the user so they don't |
| // reboot in the middle, potentially during RO update. |
| bool CrosFpBootUpdateCtrl::TriggerBootUpdateSplash() const { |
| LOG(INFO) << "Launching update splash screen."; |
| |
| int exit_code; |
| base::CommandLine cmd{base::FilePath("chromeos-boot-alert")}; |
| cmd.AppendArg("update_firmware"); |
| |
| DLOG(INFO) << "Launching '" << cmd.GetCommandLineString() << "'."; |
| |
| // libchrome does not include a wrapper for capturing a process output |
| // and having an active timeout. |
| // Since boot splash screen can hang forever, it is more important |
| // to have a dedicated timeout in this process launch than to log |
| // the launch process's output. |
| // TODO(b/130026657): Capture stdout/stderr and forward to logger. |
| base::LaunchOptions opt; |
| auto p = base::LaunchProcess(cmd, opt); |
| if (!p.WaitForExitWithTimeout(kBootSplashScreenLaunchTimeout, &exit_code)) { |
| LOG(ERROR) << "Update splash screen launcher timeout met."; |
| return false; |
| } |
| if (exit_code != EXIT_SUCCESS) { |
| LOG(ERROR) << "Update splash screen launcher exited with bad status."; |
| return false; |
| } |
| return true; |
| } |
| |
| bool CrosFpBootUpdateCtrl::ScheduleReboot() const { |
| LOG(INFO) << "Scheduling post update reboot."; |
| |
| // Trigger a file create. |
| base::File file(base::FilePath(kRebootFile), |
| base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); |
| if (!file.IsValid()) { |
| LOG(ERROR) << "Failed to schedule post update reboot: " |
| << base::File::ErrorToString(file.error_details()); |
| return false; |
| } |
| return true; |
| } |
| |
| namespace updater { |
| |
| constexpr char kFirmwareDir[] = "/opt/google/biod/fw"; |
| |
| // FindFirmwareFile searches |directory| for a single firmware file |
| // that matches the |board_name|+|kFirmwareGlobSuffix| file pattern. |
| // If a single matching firmware file is found is found, |
| // its path is written to |file|. Otherwise, |file| will be untouched. |
| FindFirmwareFileStatus FindFirmwareFile(const base::FilePath& directory, |
| const std::string& board_name, |
| base::FilePath* file) { |
| if (!base::DirectoryExists(directory)) { |
| return FindFirmwareFileStatus::kNoDirectory; |
| } |
| |
| std::string glob(board_name + std::string(kFirmwareGlobSuffix)); |
| base::FileEnumerator fw_bin_list(directory, false, |
| base::FileEnumerator::FileType::FILES, glob); |
| |
| // Find provided firmware file |
| base::FilePath fw_bin = fw_bin_list.Next(); |
| if (fw_bin.empty()) { |
| return FindFirmwareFileStatus::kFileNotFound; |
| } |
| LOG(INFO) << "Found firmware file '" << fw_bin.value() << "'."; |
| |
| // Ensure that there are no other firmware files |
| bool extra_fw_files = false; |
| for (base::FilePath fw_extra = fw_bin_list.Next(); !fw_extra.empty(); |
| fw_extra = fw_bin_list.Next()) { |
| extra_fw_files = true; |
| LOG(ERROR) << "Found firmware file '" << fw_extra.value() << "'."; |
| } |
| if (extra_fw_files) { |
| return FindFirmwareFileStatus::kMultipleFiles; |
| } |
| |
| *file = fw_bin; |
| return FindFirmwareFileStatus::kFoundFile; |
| } |
| |
| FindFirmwareFileStatus FindFirmwareFile( |
| const base::FilePath& directory, |
| brillo::CrosConfigInterface* cros_config, |
| base::FilePath* file) { |
| std::string board_name; |
| if (cros_config->GetString(kCrosConfigFPPath, kCrosConfigFPBoard, |
| &board_name)) { |
| LOG(INFO) << "Identified fingerprint board name as '" << board_name << "'."; |
| } else { |
| LOG(WARNING) << "Fingerprint board name is unavailable, continuing with " |
| "legacy update."; |
| board_name = kFirmwareLegacyBoardPattern; |
| } |
| |
| return FindFirmwareFile(directory, board_name, file); |
| } |
| |
| std::string FindFirmwareFileStatusToString(FindFirmwareFileStatus status) { |
| switch (status) { |
| case FindFirmwareFileStatus::kFoundFile: |
| return "Firmware file found."; |
| case FindFirmwareFileStatus::kNoDirectory: |
| return "Firmware directory does not exist."; |
| case FindFirmwareFileStatus::kFileNotFound: |
| return "Firmware file not found."; |
| case FindFirmwareFileStatus::kMultipleFiles: |
| return "More than one firmware file was found."; |
| } |
| |
| NOTREACHED(); |
| return "Unknown find firmware file status encountered."; |
| } |
| |
| bool UpdateDisallowed() { |
| return base::PathExists(base::FilePath(kUpdateDisableFile)); |
| } |
| |
| // Since /fingerprint/sensor-location is an optional field, the only information |
| // that is relevant to the updater is if fingerprint is explicitly not |
| // supported. |
| bool FingerprintUnsupported(brillo::CrosConfigInterface* cros_config) { |
| std::string fingerprint_location; |
| if (cros_config->GetString(kCrosConfigFPPath, kCrosConfigFPLocation, |
| &fingerprint_location)) { |
| if (fingerprint_location == "none") { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| UpdateResult DoUpdate(const CrosFpDeviceUpdate& ec_dev, |
| const CrosFpBootUpdateCtrl& boot_ctrl, |
| const CrosFpFirmware& fw) { |
| bool attempted = false; |
| UpdateResult result = {UpdateStatus::kUpdateNotNecessary, |
| UpdateReason::kNone}; |
| |
| // Grab the new firmware file's versions. |
| CrosFpFirmware::ImageVersion fw_version = fw.GetVersion(); |
| |
| // Grab the FPMCU's current firmware version and current active image. |
| CrosFpDevice::EcVersion ecver; |
| if (!ec_dev.GetVersion(&ecver)) { |
| result.status = UpdateStatus::kUpdateFailedGetVersion; |
| return result; |
| } |
| |
| // If write protection is not enabled, the RO firmware should |
| // be updated first, as this allows for re-keying (dev->premp->mp) |
| // and non-forward compatible changes. |
| bool flashprotect_enabled; |
| if (!ec_dev.IsFlashProtectEnabled(&flashprotect_enabled)) { |
| result.status = UpdateStatus::kUpdateFailedFlashProtect; |
| return result; |
| } |
| if (!flashprotect_enabled) { |
| LOG(INFO) << "Flashprotect is disabled."; |
| if (ecver.ro_version != fw_version.ro_version) { |
| result.reason |= UpdateReason::kMismatchROVersion; |
| attempted = true; |
| LOG(INFO) << "FPMCU RO firmware mismatch, updating."; |
| if (!UpdateImage(ec_dev, boot_ctrl, fw, EC_IMAGE_RO)) { |
| result.status = UpdateStatus::kUpdateFailedRO; |
| return result; |
| } |
| } else { |
| LOG(INFO) << "FPMCU RO firmware is up to date."; |
| } |
| } else { |
| LOG(INFO) << "FPMCU RO firmware is protected: no update."; |
| } |
| |
| // The firmware should be updated if RO is active (i.e. RW is corrupted) or if |
| // the firmware version available on the rootfs is different from the RW. |
| bool active_image_ro = ecver.current_image != EC_IMAGE_RW; |
| bool rw_mismatch = ecver.rw_version != fw_version.rw_version; |
| if (active_image_ro) { |
| result.reason |= UpdateReason::kActiveImageRO; |
| } |
| if (rw_mismatch) { |
| result.reason |= UpdateReason::kMismatchRWVersion; |
| } |
| if (active_image_ro || rw_mismatch) { |
| attempted = true; |
| LOG(INFO) |
| << "FPMCU RW firmware mismatch or failed RW boot detected, updating."; |
| if (!UpdateImage(ec_dev, boot_ctrl, fw, EC_IMAGE_RW)) { |
| result.status = UpdateStatus::kUpdateFailedRW; |
| return result; |
| } |
| } else { |
| LOG(INFO) << "FPMCU RW firmware is up to date."; |
| } |
| |
| result.status = attempted ? UpdateStatus::kUpdateSucceeded |
| : UpdateStatus::kUpdateNotNecessary; |
| return result; |
| } |
| |
| } // namespace updater |
| |
| } // namespace biod |