update_engine: Enterprise channel downgrade

Powerwash and roll back when an enrolled user downgrades the channel:
- If the admin downgrades channel, check for ChannelDowngradebehavior
policy.
- If the user downgrades the channel, powerwash based on given boolean.

Add the "rollback" flag to the powerwash file to try to preserve some
data.

Note that this change is not affecting users yet: The
ChannelDowngradeBehavior policy is not available in DPanel and the UI
does not support chosing powerwash on channel downgrade for enrolled
users.

BUG=chromium:1122531
TEST=FEATURES=test emerge-amd64-generic update_engine
TEST=Set policy with YAPS and test on device

Change-Id: I2f02a6e752eed083b57484766f8e7ecc2eed7aca
Reviewed-on: https://chromium-review.googlesource.com/c/aosp/platform/system/update_engine/+/2397890
Tested-by: Miriam Polzer <mpolzer@google.com>
Commit-Queue: Miriam Polzer <mpolzer@google.com>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
Reviewed-by: Jae Hoon Kim <kimjae@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 59aa004..b1719e2 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -533,6 +533,7 @@
       "update_boot_flags_action_unittest.cc",
       "update_manager/boxed_value_unittest.cc",
       "update_manager/chromeos_policy_unittest.cc",
+      "update_manager/enterprise_device_policy_impl_unittest.cc",
       "update_manager/evaluation_context_unittest.cc",
       "update_manager/generic_variables_unittest.cc",
       "update_manager/prng_unittest.cc",
diff --git a/mock_update_attempter.h b/mock_update_attempter.h
index cc05648..d502222 100644
--- a/mock_update_attempter.h
+++ b/mock_update_attempter.h
@@ -30,17 +30,20 @@
  public:
   using UpdateAttempter::UpdateAttempter;
 
-  MOCK_METHOD10(Update,
-                void(const std::string& app_version,
-                     const std::string& omaha_url,
-                     const std::string& target_channel,
-                     const std::string& lts_tag,
-                     const std::string& target_version_prefix,
-                     bool rollback_allowed,
-                     bool rollback_data_save_requested,
-                     int rollback_allowed_milestones,
-                     bool obey_proxies,
-                     bool interactive));
+  MOCK_METHOD(void,
+              Update,
+              (const std::string& app_version,
+               const std::string& omaha_url,
+               const std::string& target_channel,
+               const std::string& lts_tag,
+               const std::string& target_version_prefix,
+               bool rollback_allowed,
+               bool rollback_data_save_requested,
+               int rollback_allowed_milestones,
+               bool rollback_on_channel_downgrade,
+               bool obey_proxies,
+               bool interactive),
+              (override));
 
   MOCK_METHOD1(GetStatus, bool(update_engine::UpdateEngineStatus* out_status));
 
diff --git a/omaha_request_params.h b/omaha_request_params.h
index aad9290..5d30d58 100644
--- a/omaha_request_params.h
+++ b/omaha_request_params.h
@@ -302,6 +302,10 @@
   void set_is_powerwash_allowed(bool powerwash_allowed) {
     mutable_image_props_.is_powerwash_allowed = powerwash_allowed;
   }
+  bool is_powerwash_allowed() {
+    return mutable_image_props_.is_powerwash_allowed;
+  }
+
   void set_device_requisition(const std::string& requisition) {
     device_requisition_ = requisition;
   }
diff --git a/omaha_response_handler_action.cc b/omaha_response_handler_action.cc
index 040f8e7..92e0a72 100644
--- a/omaha_response_handler_action.cc
+++ b/omaha_response_handler_action.cc
@@ -188,10 +188,12 @@
   }
 
   // Powerwash if either the response requires it or the parameters indicated
-  // powerwash and we are downgrading the version.
+  // powerwash (usually because there was a channel downgrade) and we are
+  // downgrading the version. Enterprise rollback, indicated by
+  // |response.is_rollback| is dealt with separately above.
   if (response.powerwash_required) {
     install_plan_.powerwash_required = true;
-  } else if (params->ShouldPowerwash()) {
+  } else if (params->ShouldPowerwash() && !response.is_rollback) {
     base::Version new_version(response.version);
     base::Version current_version(params->app_version());
 
@@ -205,6 +207,10 @@
                    << " Current version number: " << params->app_version();
     } else if (new_version < current_version) {
       install_plan_.powerwash_required = true;
+      // Always try to preserve enrollment and wifi data for enrolled devices.
+      install_plan_.rollback_data_save_requested =
+          system_state_ && system_state_->device_policy() &&
+          system_state_->device_policy()->IsEnterpriseEnrolled();
     }
   }
 
diff --git a/omaha_response_handler_action_unittest.cc b/omaha_response_handler_action_unittest.cc
index 04cfa73..4e421b0 100644
--- a/omaha_response_handler_action_unittest.cc
+++ b/omaha_response_handler_action_unittest.cc
@@ -532,6 +532,132 @@
 }
 
 TEST_F(OmahaResponseHandlerActionTest,
+       ChangeToMoreStableChannelButSameVersionTest) {
+  OmahaResponse in;
+  in.update_exists = true;
+  in.version = "12345.0.0.0";
+  in.packages.push_back({.payload_urls = {"https://ChannelDownVersionUp"},
+                         .size = 1,
+                         .hash = kPayloadHashHex});
+  in.more_info_url = "http://more/info";
+
+  // Create a uniquely named test directory.
+  base::ScopedTempDir tempdir;
+  ASSERT_TRUE(tempdir.CreateUniqueTempDir());
+
+  OmahaRequestParams params(&fake_system_state_);
+  fake_system_state_.fake_hardware()->SetIsOfficialBuild(false);
+  params.set_root(tempdir.GetPath().value());
+  params.set_current_channel("beta-channel");
+  EXPECT_TRUE(params.SetTargetChannel("stable-channel", true, nullptr));
+  params.UpdateDownloadChannel();
+  params.set_app_version("12345.0.0.0");
+
+  fake_system_state_.set_request_params(&params);
+  InstallPlan install_plan;
+  EXPECT_TRUE(DoTest(in, "", &install_plan));
+  EXPECT_FALSE(install_plan.powerwash_required);
+  EXPECT_FALSE(install_plan.rollback_data_save_requested);
+}
+
+// On an enrolled device, the rollback data restore should be attempted when
+// doing a powerwash and channel downgrade.
+TEST_F(OmahaResponseHandlerActionTest,
+       ChangeToMoreStableChannelEnrolledDataRestore) {
+  OmahaResponse in;
+  in.update_exists = true;
+  in.version = "12345.96.0.0";
+  in.packages.push_back({.payload_urls = {"https://ChannelDownEnrolled"},
+                         .size = 1,
+                         .hash = kPayloadHashHex});
+  in.more_info_url = "http://more/info";
+
+  // Create a uniquely named test directory.
+  base::ScopedTempDir tempdir;
+  ASSERT_TRUE(tempdir.CreateUniqueTempDir());
+
+  OmahaRequestParams params(&fake_system_state_);
+  fake_system_state_.fake_hardware()->SetIsOfficialBuild(true);
+  params.set_root(tempdir.GetPath().value());
+  params.set_current_channel("beta-channel");
+  EXPECT_TRUE(params.SetTargetChannel("stable-channel", true, nullptr));
+  params.UpdateDownloadChannel();
+  params.set_app_version("12347.48.0.0");
+
+  testing::NiceMock<policy::MockDevicePolicy> mock_device_policy;
+  EXPECT_CALL(mock_device_policy, IsEnterpriseEnrolled())
+      .WillOnce(Return(true));
+  fake_system_state_.set_device_policy(&mock_device_policy);
+
+  fake_system_state_.set_request_params(&params);
+  InstallPlan install_plan;
+  EXPECT_TRUE(DoTest(in, "", &install_plan));
+  EXPECT_TRUE(install_plan.rollback_data_save_requested);
+}
+
+// Never attempt rollback data restore if the device is not enrolled.
+TEST_F(OmahaResponseHandlerActionTest,
+       ChangeToMoreStableChannelUnenrolledNoDataRestore) {
+  OmahaResponse in;
+  in.update_exists = true;
+  in.version = "12345.96.0.0";
+  in.packages.push_back({.payload_urls = {"https://ChannelDownEnrolled"},
+                         .size = 1,
+                         .hash = kPayloadHashHex});
+  in.more_info_url = "http://more/info";
+
+  // Create a uniquely named test directory.
+  base::ScopedTempDir tempdir;
+  ASSERT_TRUE(tempdir.CreateUniqueTempDir());
+
+  OmahaRequestParams params(&fake_system_state_);
+  fake_system_state_.fake_hardware()->SetIsOfficialBuild(true);
+  params.set_root(tempdir.GetPath().value());
+  params.set_current_channel("beta-channel");
+  EXPECT_TRUE(params.SetTargetChannel("stable-channel", true, nullptr));
+  params.UpdateDownloadChannel();
+  params.set_app_version("12347.48.0.0");
+
+  testing::NiceMock<policy::MockDevicePolicy> mock_device_policy;
+  EXPECT_CALL(mock_device_policy, IsEnterpriseEnrolled())
+      .WillOnce(Return(false));
+  fake_system_state_.set_device_policy(&mock_device_policy);
+
+  fake_system_state_.set_request_params(&params);
+  InstallPlan install_plan;
+  EXPECT_TRUE(DoTest(in, "", &install_plan));
+  EXPECT_FALSE(install_plan.rollback_data_save_requested);
+}
+
+// Never attempt rollback data restore if powerwash is not allowed.
+TEST_F(OmahaResponseHandlerActionTest,
+       ChangeToMoreStableChannelNoPowerwashNoDataRestore) {
+  OmahaResponse in;
+  in.update_exists = true;
+  in.version = "12345.96.0.0";
+  in.packages.push_back(
+      {.payload_urls = {"https://URL"}, .size = 1, .hash = kPayloadHashHex});
+  in.more_info_url = "http://more/info";
+
+  // Create a uniquely named test directory.
+  base::ScopedTempDir tempdir;
+  ASSERT_TRUE(tempdir.CreateUniqueTempDir());
+
+  OmahaRequestParams params(&fake_system_state_);
+  fake_system_state_.fake_hardware()->SetIsOfficialBuild(true);
+  params.set_root(tempdir.GetPath().value());
+  params.set_current_channel("beta-channel");
+  EXPECT_TRUE(params.SetTargetChannel("stable-channel", false, nullptr));
+  params.UpdateDownloadChannel();
+  params.set_app_version("12347.48.0.0");
+
+  fake_system_state_.set_request_params(&params);
+  InstallPlan install_plan;
+  EXPECT_TRUE(DoTest(in, "", &install_plan));
+  EXPECT_FALSE(install_plan.rollback_data_save_requested);
+}
+
+TEST_F(OmahaResponseHandlerActionTest,
        ChangeToLessStableVersionAndChannelTest) {
   OmahaResponse in;
   in.update_exists = true;
diff --git a/payload_consumer/postinstall_runner_action.cc b/payload_consumer/postinstall_runner_action.cc
index e8fa81b..91c3a64 100644
--- a/payload_consumer/postinstall_runner_action.cc
+++ b/payload_consumer/postinstall_runner_action.cc
@@ -63,9 +63,8 @@
   // that retains a small amount of system state such as enrollment and
   // network configuration. In both cases all user accounts are deleted.
   if (install_plan_.powerwash_required || install_plan_.is_rollback) {
-    bool save_rollback_data =
-        install_plan_.is_rollback && install_plan_.rollback_data_save_requested;
-    if (hardware_->SchedulePowerwash(save_rollback_data)) {
+    if (hardware_->SchedulePowerwash(
+            install_plan_.rollback_data_save_requested)) {
       powerwash_scheduled_ = true;
     } else {
       return CompletePostinstall(ErrorCode::kPostinstallPowerwashError);
diff --git a/update_attempter.cc b/update_attempter.cc
index 24562e2..14d5837 100644
--- a/update_attempter.cc
+++ b/update_attempter.cc
@@ -252,6 +252,7 @@
                              bool rollback_allowed,
                              bool rollback_data_save_requested,
                              int rollback_allowed_milestones,
+                             bool rollback_on_channel_downgrade,
                              bool obey_proxies,
                              bool interactive) {
   // This is normally called frequently enough so it's appropriate to use as a
@@ -290,6 +291,7 @@
                              rollback_allowed,
                              rollback_data_save_requested,
                              rollback_allowed_milestones,
+                             rollback_on_channel_downgrade,
                              obey_proxies,
                              interactive)) {
     return;
@@ -366,6 +368,7 @@
                                             bool rollback_allowed,
                                             bool rollback_data_save_requested,
                                             int rollback_allowed_milestones,
+                                            bool rollback_on_channel_downgrade,
                                             bool obey_proxies,
                                             bool interactive) {
   http_response_code_ = 0;
@@ -424,11 +427,9 @@
     LOG(INFO) << "No target channel mandated by policy.";
   } else {
     LOG(INFO) << "Setting target channel as mandated: " << target_channel;
-    // Pass in false for powerwash_allowed until we add it to the policy
-    // protobuf.
     string error_message;
     if (!omaha_request_params_->SetTargetChannel(
-            target_channel, false, &error_message)) {
+            target_channel, rollback_on_channel_downgrade, &error_message)) {
       LOG(ERROR) << "Setting the channel failed: " << error_message;
     }
 
@@ -1114,6 +1115,7 @@
            params.rollback_allowed,
            params.rollback_data_save_requested,
            params.rollback_allowed_milestones,
+           params.rollback_on_channel_downgrade,
            /*obey_proxies=*/false,
            params.interactive);
     // Always clear the forced app_version and omaha_url after an update attempt
diff --git a/update_attempter.h b/update_attempter.h
index abd0bd4..6c93150 100644
--- a/update_attempter.h
+++ b/update_attempter.h
@@ -90,6 +90,7 @@
                       bool rollback_allowed,
                       bool rollback_data_save_requested,
                       int rollback_allowed_milestones,
+                      bool rollback_on_channel_downgrade,
                       bool obey_proxies,
                       bool interactive);
 
@@ -284,6 +285,8 @@
   FRIEND_TEST(UpdateAttempterTest, RollbackAfterInstall);
   FRIEND_TEST(UpdateAttempterTest, RollbackAllowed);
   FRIEND_TEST(UpdateAttempterTest, RollbackAllowedSetAndReset);
+  FRIEND_TEST(UpdateAttempterTest, ChannelDowngradeNoRollback);
+  FRIEND_TEST(UpdateAttempterTest, ChannelDowngradeRollback);
   FRIEND_TEST(UpdateAttempterTest, RollbackMetricsNotRollbackFailure);
   FRIEND_TEST(UpdateAttempterTest, RollbackMetricsNotRollbackSuccess);
   FRIEND_TEST(UpdateAttempterTest, RollbackMetricsRollbackFailure);
@@ -376,6 +379,7 @@
                              bool rollback_allowed,
                              bool rollback_data_save_requested,
                              int rollback_allowed_milestones,
+                             bool rollback_on_channel_downgrade,
                              bool obey_proxies,
                              bool interactive);
 
diff --git a/update_attempter_unittest.cc b/update_attempter_unittest.cc
index 354416e..edcb67b 100644
--- a/update_attempter_unittest.cc
+++ b/update_attempter_unittest.cc
@@ -25,6 +25,7 @@
 #include <unordered_set>
 
 #include <base/files/file_util.h>
+#include <base/files/scoped_temp_dir.h>
 #include <base/message_loop/message_loop.h>
 #include <brillo/message_loops/base_message_loop.h>
 #include <brillo/message_loops/message_loop.h>
@@ -179,6 +180,7 @@
               bool rollback_allowed,
               bool rollback_data_save_requested,
               int rollback_allowed_milestones,
+              bool rollback_on_channel_downgrade,
               bool obey_proxies,
               bool interactive) override {
     update_called_ = true;
@@ -191,6 +193,7 @@
                               rollback_allowed,
                               rollback_data_save_requested,
                               rollback_allowed_milestones,
+                              rollback_on_channel_downgrade,
                               obey_proxies,
                               interactive);
       return;
@@ -427,7 +430,7 @@
 void UpdateAttempterTest::SessionIdTestChange() {
   EXPECT_NE(UpdateStatus::UPDATED_NEED_REBOOT, attempter_.status());
   const auto old_session_id = attempter_.session_id_;
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_NE(old_session_id, attempter_.session_id_);
   ScheduleQuitMainLoop();
 }
@@ -798,7 +801,7 @@
     EXPECT_CALL(*processor_, StartProcessing());
   }
 
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   loop_.PostTask(FROM_HERE,
                  base::Bind(&UpdateAttempterTest::UpdateTestVerify,
                             base::Unretained(this)));
@@ -998,7 +1001,7 @@
   fake_system_state_.set_p2p_manager(&mock_p2p_manager);
   mock_p2p_manager.fake().SetP2PEnabled(false);
   EXPECT_CALL(mock_p2p_manager, PerformHousekeeping()).Times(0);
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_FALSE(actual_using_p2p_for_downloading_);
   EXPECT_FALSE(actual_using_p2p_for_sharing());
   ScheduleQuitMainLoop();
@@ -1020,7 +1023,7 @@
   mock_p2p_manager.fake().SetEnsureP2PRunningResult(false);
   mock_p2p_manager.fake().SetPerformHousekeepingResult(false);
   EXPECT_CALL(mock_p2p_manager, PerformHousekeeping()).Times(0);
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_FALSE(actual_using_p2p_for_downloading());
   EXPECT_FALSE(actual_using_p2p_for_sharing());
   ScheduleQuitMainLoop();
@@ -1043,7 +1046,7 @@
   mock_p2p_manager.fake().SetEnsureP2PRunningResult(true);
   mock_p2p_manager.fake().SetPerformHousekeepingResult(false);
   EXPECT_CALL(mock_p2p_manager, PerformHousekeeping());
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_FALSE(actual_using_p2p_for_downloading());
   EXPECT_FALSE(actual_using_p2p_for_sharing());
   ScheduleQuitMainLoop();
@@ -1065,7 +1068,7 @@
   mock_p2p_manager.fake().SetEnsureP2PRunningResult(true);
   mock_p2p_manager.fake().SetPerformHousekeepingResult(true);
   EXPECT_CALL(mock_p2p_manager, PerformHousekeeping());
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_TRUE(actual_using_p2p_for_downloading());
   EXPECT_TRUE(actual_using_p2p_for_sharing());
   ScheduleQuitMainLoop();
@@ -1097,6 +1100,7 @@
                     false,
                     /*rollback_allowed_milestones=*/0,
                     false,
+                    false,
                     /*interactive=*/true);
   EXPECT_FALSE(actual_using_p2p_for_downloading());
   EXPECT_TRUE(actual_using_p2p_for_sharing());
@@ -1127,7 +1131,7 @@
   attempter_.policy_provider_.reset(
       new policy::PolicyProvider(std::move(device_policy)));
 
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_EQ(scatter_factor_in_seconds, attempter_.scatter_factor_.InSeconds());
 
   ScheduleQuitMainLoop();
@@ -1165,7 +1169,7 @@
   attempter_.policy_provider_.reset(
       new policy::PolicyProvider(std::move(device_policy)));
 
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_EQ(scatter_factor_in_seconds, attempter_.scatter_factor_.InSeconds());
 
   // Make sure the file still exists.
@@ -1181,7 +1185,7 @@
   // However, if the count is already 0, it's not decremented. Test that.
   initial_value = 0;
   EXPECT_TRUE(fake_prefs.SetInt64(kPrefsUpdateCheckCount, initial_value));
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   EXPECT_TRUE(fake_prefs.Exists(kPrefsUpdateCheckCount));
   EXPECT_TRUE(fake_prefs.GetInt64(kPrefsUpdateCheckCount, &new_value));
   EXPECT_EQ(initial_value, new_value);
@@ -1237,6 +1241,7 @@
                     false,
                     /*rollback_allowed_milestones=*/0,
                     false,
+                    false,
                     /*interactive=*/true);
   EXPECT_EQ(scatter_factor_in_seconds, attempter_.scatter_factor_.InSeconds());
 
@@ -1289,7 +1294,7 @@
   FakePrefs fake_prefs;
   SetUpStagingTest(kValidStagingSchedule, &fake_prefs);
 
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   // Check that prefs have the correct values.
   int64_t update_count;
   EXPECT_TRUE(fake_prefs.GetInt64(kPrefsUpdateCheckCount, &update_count));
@@ -1346,8 +1351,17 @@
   FakePrefs fake_prefs;
   SetUpStagingTest(kValidStagingSchedule, &fake_prefs);
 
-  attempter_.Update(
-      "", "", "", "", "", false, false, 0, false, /* interactive = */ true);
+  attempter_.Update("",
+                    "",
+                    "",
+                    "",
+                    "",
+                    false,
+                    false,
+                    0,
+                    false,
+                    false,
+                    /* interactive = */ true);
   CheckStagingOff();
 
   ScheduleQuitMainLoop();
@@ -1367,8 +1381,17 @@
   FakePrefs fake_prefs;
   SetUpStagingTest(kValidStagingSchedule, &fake_prefs);
 
-  attempter_.Update(
-      "", "", "", "", "", false, false, 0, false, /* interactive = */ true);
+  attempter_.Update("",
+                    "",
+                    "",
+                    "",
+                    "",
+                    false,
+                    false,
+                    0,
+                    false,
+                    false,
+                    /* interactive = */ true);
   CheckStagingOff();
 
   ScheduleQuitMainLoop();
@@ -1697,23 +1720,33 @@
 
 TEST_F(UpdateAttempterTest, TargetVersionPrefixSetAndReset) {
   attempter_.CalculateUpdateParams(
-      "", "", "", "", "1234", false, false, 4, false, false);
+      /*app_version=*/"",
+      /*omaha_url=*/"",
+      /*target_channel=*/"",
+      /*lts_tag=*/"",
+      /*target_version_prefix=*/"1234",
+      /*rollback_allowed=*/false,
+      /*rollback_data_save_requested=*/false,
+      /*rollback_allowed_milestones=*/4,
+      /*rollback_on_channel_downgrade=*/false,
+      /*obey_proxies=*/false,
+      /*interactive=*/false);
   EXPECT_EQ("1234",
             fake_system_state_.request_params()->target_version_prefix());
 
   attempter_.CalculateUpdateParams(
-      "", "", "", "", "", false, 4, false, false, false);
+      "", "", "", "", "", false, false, 4, false, false, false);
   EXPECT_TRUE(
       fake_system_state_.request_params()->target_version_prefix().empty());
 }
 
 TEST_F(UpdateAttempterTest, TargetChannelHintSetAndReset) {
   attempter_.CalculateUpdateParams(
-      "", "", "", "hint", "", false, false, 4, false, false);
+      "", "", "", "hint", "", false, false, 4, false, false, false);
   EXPECT_EQ("hint", fake_system_state_.request_params()->lts_tag());
 
   attempter_.CalculateUpdateParams(
-      "", "", "", "", "", false, 4, false, false, false);
+      "", "", "", "", "", false, false, 4, false, false, false);
   EXPECT_TRUE(fake_system_state_.request_params()->lts_tag().empty());
 }
 
@@ -1726,6 +1759,7 @@
                                    /*rollback_allowed=*/true,
                                    /*rollback_data_save_requested=*/false,
                                    /*rollback_allowed_milestones=*/4,
+                                   /*rollback_on_channel_downgrade=*/false,
                                    false,
                                    false);
   EXPECT_TRUE(fake_system_state_.request_params()->rollback_allowed());
@@ -1740,6 +1774,7 @@
                                    /*rollback_allowed=*/false,
                                    /*rollback_data_save_requested=*/false,
                                    /*rollback_allowed_milestones=*/4,
+                                   /*rollback_on_channel_downgrade=*/false,
                                    false,
                                    false);
   EXPECT_FALSE(fake_system_state_.request_params()->rollback_allowed());
@@ -1747,6 +1782,42 @@
             fake_system_state_.request_params()->rollback_allowed_milestones());
 }
 
+TEST_F(UpdateAttempterTest, ChannelDowngradeNoRollback) {
+  base::ScopedTempDir tempdir;
+  ASSERT_TRUE(tempdir.CreateUniqueTempDir());
+  fake_system_state_.request_params()->set_root(tempdir.GetPath().value());
+  attempter_.CalculateUpdateParams(/*app_version=*/"",
+                                   /*omaha_url=*/"",
+                                   /*target_channel=*/"stable-channel",
+                                   /*lts_tag=*/"",
+                                   /*target_version_prefix=*/"",
+                                   /*rollback_allowed=*/false,
+                                   /*rollback_data_save_requested=*/false,
+                                   /*rollback_allowed_milestones=*/4,
+                                   /*rollback_on_channel_downgrade=*/false,
+                                   /*obey_proxies=*/false,
+                                   /*interactive=*/false);
+  EXPECT_FALSE(fake_system_state_.request_params()->is_powerwash_allowed());
+}
+
+TEST_F(UpdateAttempterTest, ChannelDowngradeRollback) {
+  base::ScopedTempDir tempdir;
+  ASSERT_TRUE(tempdir.CreateUniqueTempDir());
+  fake_system_state_.request_params()->set_root(tempdir.GetPath().value());
+  attempter_.CalculateUpdateParams(/*app_version=*/"",
+                                   /*omaha_url=*/"",
+                                   /*target_channel=*/"stable-channel",
+                                   /*lts_tag=*/"",
+                                   /*target_version_prefix=*/"",
+                                   /*rollback_allowed=*/false,
+                                   /*rollback_data_save_requested=*/false,
+                                   /*rollback_allowed_milestones=*/4,
+                                   /*rollback_on_channel_downgrade=*/true,
+                                   /*obey_proxies=*/false,
+                                   /*interactive=*/false);
+  EXPECT_TRUE(fake_system_state_.request_params()->is_powerwash_allowed());
+}
+
 TEST_F(UpdateAttempterTest, UpdateDeferredByPolicyTest) {
   // Construct an OmahaResponseHandlerAction that has processed an InstallPlan,
   // but the update is being deferred by the Policy.
@@ -1861,7 +1932,7 @@
               SetRollbackHappened(false))
       .Times(expected_reset ? 1 : 0);
   attempter_.policy_provider_ = std::move(mock_policy_provider);
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
   ScheduleQuitMainLoop();
 }
 
@@ -2202,7 +2273,7 @@
         .WillOnce(Return(false));
   attempter_.policy_provider_.reset(
       new policy::PolicyProvider(std::move(device_policy)));
-  attempter_.Update("", "", "", "", "", false, false, 0, false, false);
+  attempter_.Update("", "", "", "", "", false, false, 0, false, false, false);
 
   EXPECT_EQ(token, attempter_.omaha_request_params_->autoupdate_token());
   ScheduleQuitMainLoop();
diff --git a/update_manager/android_things_policy.cc b/update_manager/android_things_policy.cc
index 6362a73..c4fa75a 100644
--- a/update_manager/android_things_policy.cc
+++ b/update_manager/android_things_policy.cc
@@ -63,6 +63,7 @@
   result->rollback_allowed = false;
   result->rollback_data_save_requested = false;
   result->rollback_allowed_milestones = -1;
+  result->rollback_on_channel_downgrade = false;
   result->interactive = false;
 
   // Build a list of policies to consult.  Note that each policy may modify the
diff --git a/update_manager/chromeos_policy.cc b/update_manager/chromeos_policy.cc
index 85cc3ae..4c651b7 100644
--- a/update_manager/chromeos_policy.cc
+++ b/update_manager/chromeos_policy.cc
@@ -221,6 +221,7 @@
   result->target_version_prefix.clear();
   result->rollback_allowed = false;
   result->rollback_allowed_milestones = -1;
+  result->rollback_on_channel_downgrade = false;
   result->interactive = false;
 
   EnoughSlotsAbUpdatesPolicyImpl enough_slots_ab_updates_policy;
diff --git a/update_manager/default_policy.cc b/update_manager/default_policy.cc
index cc13c44..7ca414b 100644
--- a/update_manager/default_policy.cc
+++ b/update_manager/default_policy.cc
@@ -44,6 +44,7 @@
   result->target_version_prefix.clear();
   result->rollback_allowed = false;
   result->rollback_allowed_milestones = -1;  // No version rolls should happen.
+  result->rollback_on_channel_downgrade = false;
   result->interactive = false;
 
   // Ensure that the minimum interval is set. If there's no clock, this defaults
diff --git a/update_manager/enterprise_device_policy_impl.cc b/update_manager/enterprise_device_policy_impl.cc
index fed50a9..8fc79ca 100644
--- a/update_manager/enterprise_device_policy_impl.cc
+++ b/update_manager/enterprise_device_policy_impl.cc
@@ -117,14 +117,23 @@
     if (rollback_allowed_milestones_p)
       result->rollback_allowed_milestones = *rollback_allowed_milestones_p;
 
-    // Determine whether a target channel is dictated by policy.
+    // Determine whether a target channel is dictated by policy and whether we
+    // should rollback in case that channel is more stable.
     const bool* release_channel_delegated_p =
         ec->GetValue(dp_provider->var_release_channel_delegated());
     if (release_channel_delegated_p && !(*release_channel_delegated_p)) {
       const string* release_channel_p =
           ec->GetValue(dp_provider->var_release_channel());
-      if (release_channel_p)
+      if (release_channel_p) {
         result->target_channel = *release_channel_p;
+        const ChannelDowngradeBehavior* channel_downgrade_behavior_p =
+            ec->GetValue(dp_provider->var_channel_downgrade_behavior());
+        if (channel_downgrade_behavior_p &&
+            *channel_downgrade_behavior_p ==
+                ChannelDowngradeBehavior::kRollback) {
+          result->rollback_on_channel_downgrade = true;
+        }
+      }
     }
 
     const string* release_lts_tag_p =
diff --git a/update_manager/enterprise_device_policy_impl_unittest.cc b/update_manager/enterprise_device_policy_impl_unittest.cc
new file mode 100644
index 0000000..5b25602
--- /dev/null
+++ b/update_manager/enterprise_device_policy_impl_unittest.cc
@@ -0,0 +1,65 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "update_engine/update_manager/enterprise_device_policy_impl.h"
+
+#include <memory>
+
+#include "update_engine/update_manager/policy_test_utils.h"
+
+namespace chromeos_update_manager {
+
+class UmEnterpriseDevicePolicyImplTest : public UmPolicyTestBase {
+ protected:
+  UmEnterpriseDevicePolicyImplTest() : UmPolicyTestBase() {
+    policy_ = std::make_unique<EnterpriseDevicePolicyImpl>();
+  }
+
+  void SetUpDefaultState() override {
+    UmPolicyTestBase::SetUpDefaultState();
+
+    fake_state_.device_policy_provider()->var_device_policy_is_loaded()->reset(
+        new bool(true));
+  }
+};
+
+TEST_F(UmEnterpriseDevicePolicyImplTest, ChannelDowngradeBehaviorNoRollback) {
+  fake_state_.device_policy_provider()->var_release_channel_delegated()->reset(
+      new bool(false));
+  fake_state_.device_policy_provider()->var_release_channel()->reset(
+      new std::string("stable-channel"));
+
+  UpdateCheckParams result;
+  ExpectPolicyStatus(
+      EvalStatus::kContinue, &Policy::UpdateCheckAllowed, &result);
+  EXPECT_FALSE(result.rollback_on_channel_downgrade);
+}
+
+TEST_F(UmEnterpriseDevicePolicyImplTest, ChannelDowngradeBehaviorRollback) {
+  fake_state_.device_policy_provider()->var_release_channel_delegated()->reset(
+      new bool(false));
+  fake_state_.device_policy_provider()->var_release_channel()->reset(
+      new std::string("stable-channel"));
+  fake_state_.device_policy_provider()->var_channel_downgrade_behavior()->reset(
+      new ChannelDowngradeBehavior(ChannelDowngradeBehavior::kRollback));
+
+  UpdateCheckParams result;
+  ExpectPolicyStatus(
+      EvalStatus::kContinue, &Policy::UpdateCheckAllowed, &result);
+  EXPECT_TRUE(result.rollback_on_channel_downgrade);
+}
+
+}  // namespace chromeos_update_manager
diff --git a/update_manager/policy.h b/update_manager/policy.h
index 9194c38..4b3bfc7 100644
--- a/update_manager/policy.h
+++ b/update_manager/policy.h
@@ -58,6 +58,9 @@
   // (e.g. no device policy is available yet), in this case no version
   // roll-forward should happen.
   int rollback_allowed_milestones;
+  // Whether a rollback with data save should be initiated on channel
+  // downgrade (e.g. beta to stable).
+  bool rollback_on_channel_downgrade{false};
   // A target channel, if so imposed by policy; otherwise, an empty string.
   std::string target_channel;
   // Specifies if the channel hint, e.g. LTS (Long Term Support) updates.