blob: f412d9b059184b3b0d98983a2d709f3ad6921682 [file] [log] [blame]
// 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