| // 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/updater/cros_fp_updater.h" |
| |
| #include <fcntl.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| |
| #include <string> |
| #include <utility> |
| |
| #include <base/check.h> |
| #include <base/command_line.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/logging.h> |
| #include <base/notreached.h> |
| #include <base/optional.h> |
| #include <base/process/launch.h> |
| #include <base/strings/string_split.h> |
| #include <base/time/time.h> |
| #include <chromeos/ec/ec_commands.h> |
| #include <cros_config/cros_config_interface.h> |
| #include <libec/ec_command.h> |
| |
| #include "biod/biod_config.h" |
| #include "biod/biod_system.h" |
| #include "biod/cros_fp_device.h" |
| #include "biod/cros_fp_firmware.h" |
| #include "biod/updater/update_reason.h" |
| #include "biod/updater/update_status.h" |
| #include "biod/updater/update_utils.h" |
| #include "biod/utils.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"; |
| |
| 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 { |
| |
| 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(); |
| } |
| |
| base::Optional<CrosFpDeviceInterface::EcVersion> |
| CrosFpDeviceUpdate::GetVersion() const { |
| 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 base::nullopt; |
| } |
| |
| auto version = biod::CrosFpDevice::GetVersion(fd); |
| if (!version) { |
| LOG(ERROR) << "Failed to read fingerprint version."; |
| return base::nullopt; |
| } |
| return version; |
| } |
| |
| 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; |
| } |
| |
| ec::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("noverify-all"); |
| 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()); |
| |
| LOG(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 { |
| |
| 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. |
| auto ecver = ec_dev.GetVersion(); |
| if (!ecver) { |
| result.status = UpdateStatus::kUpdateFailedGetVersion; |
| return result; |
| } |
| |
| BiodSystem system; |
| LOG(INFO) << "Hardware write protect: " |
| << (system.HardwareWriteProtectIsEnabled() ? "enabled" |
| : "disabled"); |
| |
| // 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."; |
| |
| // TODO(b/119131962): Remove when flashrom is fixed. |
| if (!system.HardwareWriteProtectIsEnabled()) { |
| LOG(ERROR) << "FPMCU software write protect enabled while system hardware" |
| " write is protect disabled. Updating will fail due to " |
| "http://go/flashrom-fpmcu-wp-bug."; |
| } |
| } |
| |
| // 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 |