update_engine: Invalidate firmware from Omaha

Currently if Omaha triggers an update invalidation, the update engine
only invalidates kernel + root updates, but not firmware.

Add the firmware invalidation to Omaha-triggered invalidations, too.

BUG=b:275530794
TEST=manually updated firmware and issued invalidation from Omaha
TEST=cros_run_unit_tests --board=<BOARD> --packages update_engine

Change-Id: I9d0001333b3c0b65d0e57a908ab34b364f3f22d3
Reviewed-on: https://chromium-review.googlesource.com/c/aosp/platform/system/update_engine/+/4622720
Reviewed-by: Hung-Te Lin <[email protected]>
Commit-Queue: Artyom Chen <[email protected]>
Tested-by: Artyom Chen <[email protected]>
Reviewed-by: Jae Hoon Kim <[email protected]>
diff --git a/BUILD.gn b/BUILD.gn
index 2dbedac..8ebc94e 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -262,6 +262,7 @@
   all_dependent_pkg_deps = [
     "dbus-1",
     "expat",
+    "libcrossystem",
     "libcurl",
     "libdebugd-client",
     "libmetrics",
@@ -596,6 +597,7 @@
     pkg_deps = [
       "libbrillo-test",
       "libchrome-test",
+      "libcrossystem-test",
       "libdebugd-client-test",
       "libpower_manager-client-test",
       "libsession_manager-client-test",
diff --git a/common/fake_hardware.h b/common/fake_hardware.h
index 4fe9c5f..44bcccb 100644
--- a/common/fake_hardware.h
+++ b/common/fake_hardware.h
@@ -274,6 +274,8 @@
     return rootfs_verification_enabled_;
   }
 
+  bool ResetFWTryNextSlot() override { return true; };
+
  private:
   bool is_official_build_{true};
   bool is_normal_boot_mode_{true};
diff --git a/common/hardware_interface.h b/common/hardware_interface.h
index 7422220..03355b5 100644
--- a/common/hardware_interface.h
+++ b/common/hardware_interface.h
@@ -187,6 +187,10 @@
   // Returns true if rootfs verification is enabled.
   // e.g. "dm_verity.dev_wait" is set to 1 in the kernel commandline.
   virtual bool IsRootfsVerificationEnabled() const = 0;
+
+  // Resets a RW firmware partition slot to try on next boot to a current slot.
+  // Returns false on failure, true on success.
+  virtual bool ResetFWTryNextSlot() = 0;
 };
 
 }  // namespace chromeos_update_engine
diff --git a/cros/hardware_chromeos.cc b/cros/hardware_chromeos.cc
index 2b900a3..c95cd09 100644
--- a/cros/hardware_chromeos.cc
+++ b/cros/hardware_chromeos.cc
@@ -27,6 +27,7 @@
 #include <brillo/blkdev_utils/lvm.h>
 #include <brillo/key_value_store.h>
 #include <debugd/dbus-constants.h>
+#include <libcrossystem/crossystem.h>
 #include <vboot/crossystem.h>
 
 extern "C" {
@@ -106,6 +107,22 @@
 constexpr char kEnrollmentRecoveryRequired[] = "EnrollmentRecoveryRequired";
 
 constexpr char kConsumerSegment[] = "IsConsumerSegment";
+
+// Firmware slot to try next (A or B).
+constexpr char kFWTryNextFlag[] = "fw_try_next";
+
+// Current main firmware.
+constexpr char kMainFWActFlag[] = "mainfw_act";
+
+// Firmware boot result this boot.
+constexpr char kFWResultFlag[] = "fw_result";
+
+// Number of times to try to boot `kFWTryNextFlag` slot.
+constexpr char kFWTryCountFlag[] = "fw_try_count";
+
+// Firmware partition slots.
+constexpr char kFWSlotA[] = "A";
+constexpr char kFWSlotB[] = "B";
 }  // namespace
 
 namespace chromeos_update_engine {
@@ -128,6 +145,7 @@
   LoadConfig("" /* root_prefix */, IsNormalBootMode());
   debugd_proxy_.reset(
       new org::chromium::debugdProxy(DBusConnection::Get()->GetDBus()));
+  crossystem_.reset(new crossystem::Crossystem());
 }
 
 bool HardwareChromeOS::IsOfficialBuild() const {
@@ -556,4 +574,69 @@
   return kernel_cmd_line.find("dm_verity.dev_wait=1") != std::string::npos;
 }
 
+bool HardwareChromeOS::ResetFWTryNextSlot() {
+  const std::optional<std::string> main_fw_act = GetMainFWAct();
+  const int fw_try_count = 0;
+
+  if (!main_fw_act) {
+    return false;
+  }
+
+  return SetFWTryNextSlot(*main_fw_act) && SetFWResultSuccessful() &&
+         SetFWTryCount(fw_try_count);
+}
+
+bool HardwareChromeOS::SetFWTryNextSlot(base::StringPiece target_slot) {
+  DCHECK(crossystem_);
+
+  if (target_slot != kFWSlotA && target_slot != kFWSlotB) {
+    LOG(ERROR) << "Invalid target_slot " << target_slot;
+    return false;
+  }
+
+  if (!crossystem_->VbSetSystemPropertyString(kFWTryNextFlag,
+                                              target_slot.data())) {
+    LOG(ERROR) << "Unable to set " << kFWTryNextFlag << " to "
+               << target_slot.data();
+    return false;
+  }
+
+  return true;
+}
+
+std::optional<std::string> HardwareChromeOS::GetMainFWAct() const {
+  DCHECK(crossystem_);
+
+  const std::optional<std::string> main_fw_act =
+      crossystem_->VbGetSystemPropertyString(kMainFWActFlag);
+  if (!main_fw_act) {
+    LOG(ERROR) << "Unable to get a current FW slot from " << kMainFWActFlag;
+    return std::nullopt;
+  }
+
+  return *main_fw_act;
+}
+
+bool HardwareChromeOS::SetFWResultSuccessful() {
+  DCHECK(crossystem_);
+
+  if (!crossystem_->VbSetSystemPropertyString(kFWResultFlag, "success")) {
+    LOG(ERROR) << "Unable to set " << kFWResultFlag << " to success";
+    return false;
+  }
+
+  return true;
+}
+
+bool HardwareChromeOS::SetFWTryCount(int count) {
+  DCHECK(crossystem_);
+
+  if (!crossystem_->VbSetSystemPropertyInt(kFWTryCountFlag, count)) {
+    LOG(ERROR) << "Unable to set " << kFWTryCountFlag << " to " << count;
+    return false;
+  }
+
+  return true;
+}
+
 }  // namespace chromeos_update_engine
diff --git a/cros/hardware_chromeos.h b/cros/hardware_chromeos.h
index 471b34c..3b1c7be 100644
--- a/cros/hardware_chromeos.h
+++ b/cros/hardware_chromeos.h
@@ -24,6 +24,7 @@
 #include <base/time/time.h>
 #include <debugd/dbus-proxies.h>
 #include <gtest/gtest_prod.h>  // for FRIEND_TEST
+#include <libcrossystem/crossystem.h>
 
 #include "update_engine/common/error_code.h"
 #include "update_engine/common/hardware_interface.h"
@@ -86,11 +87,37 @@
     non_volatile_path_ = path;
   }
 
+  bool ResetFWTryNextSlot() override;
+
  private:
   friend class HardwareChromeOSTest;
   FRIEND_TEST(HardwareChromeOSTest, GeneratePowerwashCommandCheck);
   FRIEND_TEST(HardwareChromeOSTest,
               GeneratePowerwashCommandWithRollbackDataCheck);
+  FRIEND_TEST(HardwareChromeOSFWTest,
+              ResetsFWTryNextSlotProperlyIfValidMainFwAct);
+  FRIEND_TEST(HardwareChromeOSTest, ResetFWTryNextSlotFailsIfInvalidMainFwAct);
+  FRIEND_TEST(HardwareChromeOSTest, ResetFWTryNextSlotFailsIfMissingMainFwAct);
+  FRIEND_TEST(HardwareChromeOSTest,
+              ResetFWTryNextSlotFailsIfSettingResultFlagFails);
+  FRIEND_TEST(HardwareChromeOSTest,
+              ResetFWTryNextSlotFailsIfSettingTryCountFails);
+
+  // Returns a currently active firmware slot.
+  // `kFWSlotA` or `kFWSlotB` most of the time, though can be "recovery" or
+  // "error".
+  std::optional<std::string> GetMainFWAct() const;
+  // Sets a RW firmware partition slot to try on next boot to
+  // |target_slot|.
+  // Expects only `kFWSlotA` or `kFWSlotB` as |target_slot|.
+  // Returns false on failure.
+  bool SetFWTryNextSlot(base::StringPiece target_slot);
+  // Marks current RW firmware boot result as success.
+  // Returns false on failure.
+  bool SetFWResultSuccessful();
+  // Sets a number of times to try a next boot RW partition slot to |count|.
+  // Returns false on failure.
+  bool SetFWTryCount(int count);
 
   // Load the update manager config flags (is_oobe_enabled flag) from the
   // appropriate location based on whether we are in a normal mode boot (as
@@ -107,6 +134,7 @@
   base::FilePath non_volatile_path_;
 
   std::unique_ptr<org::chromium::debugdProxyInterface> debugd_proxy_;
+  std::unique_ptr<crossystem::Crossystem> crossystem_;
 };
 
 }  // namespace chromeos_update_engine
diff --git a/cros/hardware_chromeos_unittest.cc b/cros/hardware_chromeos_unittest.cc
index 391d683..fc769f8 100644
--- a/cros/hardware_chromeos_unittest.cc
+++ b/cros/hardware_chromeos_unittest.cc
@@ -17,12 +17,15 @@
 #include "update_engine/cros/hardware_chromeos.h"
 
 #include <memory>
+#include <utility>
 
 #include <base/files/file_util.h>
 #include <base/files/scoped_temp_dir.h>
 #include <base/json/json_string_value_serializer.h>
 #include <brillo/file_utils.h>
 #include <gtest/gtest.h>
+#include <libcrossystem/crossystem.h>
+#include <libcrossystem/crossystem_fake.h>
 
 #include "update_engine/common/constants.h"
 #include "update_engine/common/fake_hardware.h"
@@ -75,6 +78,12 @@
   void SetUp() override {
     ASSERT_TRUE(root_dir_.CreateUniqueTempDir());
     FakeSystemState::CreateInstance();
+
+    auto fake_crossystem_impl =
+        std::make_unique<crossystem::fake::CrossystemFake>();
+    auto crossystem = std::make_unique<crossystem::Crossystem>(
+        std::move(fake_crossystem_impl));
+    hardware_.crossystem_ = std::move(crossystem);
   }
 
   void WriteStatefulConfig(const string& config) {
@@ -378,4 +387,84 @@
   EXPECT_FALSE(hardware_.IsConsumerSegmentSet(root.get()));
 }
 
+struct FWTestCase {
+  std::string mainfw_act;
+  std::string fw_try_next;
+};
+
+class HardwareChromeOSFWTest : public HardwareChromeOSTest,
+                               public testing::WithParamInterface<FWTestCase> {
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    /* no prefix */,
+    HardwareChromeOSFWTest,
+    testing::Values(FWTestCase{.mainfw_act = "A", .fw_try_next = "B"},
+                    FWTestCase{.mainfw_act = "B", .fw_try_next = "A"}));
+
+TEST_P(HardwareChromeOSFWTest, ResetsFWTryNextSlotProperlyIfValidMainFwAct) {
+  auto fw_test_case = GetParam();
+
+  hardware_.crossystem_->VbSetSystemPropertyInt("fw_try_count", 5);
+  hardware_.crossystem_->VbSetSystemPropertyString("mainfw_act",
+                                                   fw_test_case.mainfw_act);
+  hardware_.crossystem_->VbSetSystemPropertyString("fw_try_next",
+                                                   fw_test_case.fw_try_next);
+  hardware_.crossystem_->VbSetSystemPropertyString("fw_result", "unknown");
+
+  bool result = hardware_.ResetFWTryNextSlot();
+
+  ASSERT_TRUE(result);
+  EXPECT_EQ(hardware_.crossystem_->VbGetSystemPropertyString("mainfw_act"),
+            fw_test_case.mainfw_act);
+  EXPECT_EQ(hardware_.crossystem_->VbGetSystemPropertyString("fw_try_next"),
+            hardware_.crossystem_->VbGetSystemPropertyString("mainfw_act"));
+  EXPECT_EQ(hardware_.crossystem_->VbGetSystemPropertyInt("fw_try_count"), 0);
+  EXPECT_EQ(hardware_.crossystem_->VbGetSystemPropertyString("fw_result"),
+            "success");
+}
+
+TEST_F(HardwareChromeOSTest, ResetFWTryNextSlotFailsIfInvalidMainFwAct) {
+  hardware_.crossystem_->VbSetSystemPropertyString("mainfw_act", "recovery");
+
+  bool result = hardware_.ResetFWTryNextSlot();
+
+  ASSERT_FALSE(result);
+}
+
+TEST_F(HardwareChromeOSTest, ResetFWTryNextSlotFailsIfMissingMainFwAct) {
+  auto fake_crossystem = std::make_unique<crossystem::fake::CrossystemFake>();
+  fake_crossystem->UnsetSystemPropertyValue("mainfw_act");
+  hardware_.crossystem_ =
+      std::make_unique<crossystem::Crossystem>(std::move(fake_crossystem));
+
+  bool result = hardware_.ResetFWTryNextSlot();
+
+  ASSERT_FALSE(result);
+}
+
+TEST_F(HardwareChromeOSTest, ResetFWTryNextSlotFailsIfSettingResultFlagFails) {
+  auto fake_crossystem = std::make_unique<crossystem::fake::CrossystemFake>();
+  fake_crossystem->SetSystemPropertyReadOnlyStatus("fw_result", true);
+  hardware_.crossystem_ =
+      std::make_unique<crossystem::Crossystem>(std::move(fake_crossystem));
+  hardware_.crossystem_->VbSetSystemPropertyString("mainfw_act", "A");
+
+  bool result = hardware_.ResetFWTryNextSlot();
+
+  ASSERT_FALSE(result);
+}
+
+TEST_F(HardwareChromeOSTest, ResetFWTryNextSlotFailsIfSettingTryCountFails) {
+  auto fake_crossystem = std::make_unique<crossystem::fake::CrossystemFake>();
+  fake_crossystem->SetSystemPropertyReadOnlyStatus("fw_try_count", true);
+  hardware_.crossystem_ =
+      std::make_unique<crossystem::Crossystem>(std::move(fake_crossystem));
+  hardware_.crossystem_->VbSetSystemPropertyString("mainfw_act", "A");
+
+  bool result = hardware_.ResetFWTryNextSlot();
+
+  ASSERT_FALSE(result);
+}
+
 }  // namespace chromeos_update_engine
diff --git a/cros/update_attempter.cc b/cros/update_attempter.cc
index c022a90..d049827 100644
--- a/cros/update_attempter.cc
+++ b/cros/update_attempter.cc
@@ -1728,6 +1728,11 @@
         << "Could not delete update completed markers. Continuing anyway.";
     success = false;
   }
+  LOG(INFO) << "Invalidating firmware update.";
+  if (!SystemState::Get()->hardware()->ResetFWTryNextSlot()) {
+    LOG(WARNING) << "Could not reset firmware slot. Continuing anyway.";
+    success = false;
+  }
 
   SystemState::Get()->metrics_reporter()->ReportInvalidatedUpdate(success);
 }