Rework postinstall unittests to pass on Android.

Postinstall unittests were creating and mounting an image on each test
run. This patch adds several test scripts to one of the pre-generated
images and uses that image during postinstall testing instead.

To workaround problems with mount/umount of loop devices on Android,
this patch rewrites the `losetup` logic to make the appropriate
syscalls and create the loop device with mknod if it doesn't exists.

The tests require some extra SELinux policies to run in enforcing mode.

Bug: 26955860
TEST=Ran all Postinstall unittests.

Change-Id: I47a56b80b97596bc65ffe30cbc8118f05faff0ae
diff --git a/payload_consumer/filesystem_verifier_action_unittest.cc b/payload_consumer/filesystem_verifier_action_unittest.cc
index cdf7fc0..1b5d32a 100644
--- a/payload_consumer/filesystem_verifier_action_unittest.cc
+++ b/payload_consumer/filesystem_verifier_action_unittest.cc
@@ -153,7 +153,8 @@
 
   // Attach loop devices to the files
   string a_dev;
-  test_utils::ScopedLoopbackDeviceBinder a_dev_releaser(a_loop_file, &a_dev);
+  test_utils::ScopedLoopbackDeviceBinder a_dev_releaser(
+      a_loop_file, false, &a_dev);
   if (!(a_dev_releaser.is_bound())) {
     ADD_FAILURE();
     return false;
diff --git a/payload_consumer/postinstall_runner_action.cc b/payload_consumer/postinstall_runner_action.cc
index d57ef4e..9ebef53 100644
--- a/payload_consumer/postinstall_runner_action.cc
+++ b/payload_consumer/postinstall_runner_action.cc
@@ -87,9 +87,16 @@
       utils::MakeTempDirectory("au_postint_mount.XXXXXX", &fs_mount_dir_));
 #endif  // __ANDROID__
 
-  string abs_path = base::FilePath(fs_mount_dir_)
-                        .AppendASCII(partition.postinstall_path)
-                        .value();
+  base::FilePath postinstall_path(partition.postinstall_path);
+  if (postinstall_path.IsAbsolute()) {
+    LOG(ERROR) << "Invalid absolute path passed to postinstall, use a relative"
+                  "path instead: "
+               << partition.postinstall_path;
+    return CompletePostinstall(ErrorCode::kPostinstallRunnerError);
+  }
+
+  string abs_path =
+      base::FilePath(fs_mount_dir_).Append(postinstall_path).value();
   if (!base::StartsWith(
           abs_path, fs_mount_dir_, base::CompareCase::SENSITIVE)) {
     LOG(ERROR) << "Invalid relative postinstall path: "
@@ -171,6 +178,7 @@
   }
 
   ScopedActionCompleter completer(processor_, this);
+  completer.set_code(error_code);
 
   if (error_code != ErrorCode::kSuccess) {
     LOG(ERROR) << "Postinstall action failed.";
@@ -186,8 +194,6 @@
   if (HasOutputPipe()) {
     SetOutputObject(install_plan_);
   }
-
-  completer.set_code(ErrorCode::kSuccess);
 }
 
 }  // namespace chromeos_update_engine
diff --git a/payload_consumer/postinstall_runner_action.h b/payload_consumer/postinstall_runner_action.h
index b4defae..08dedd2 100644
--- a/payload_consumer/postinstall_runner_action.h
+++ b/payload_consumer/postinstall_runner_action.h
@@ -34,14 +34,14 @@
   explicit PostinstallRunnerAction(BootControlInterface* boot_control)
       : PostinstallRunnerAction(boot_control, nullptr) {}
 
-  void PerformAction();
+  void PerformAction() override;
 
   // Note that there's no support for terminating this action currently.
-  void TerminateProcessing() { CHECK(false); }
+  void TerminateProcessing()  override { CHECK(false); }
 
   // Debugging/logging
   static std::string StaticType() { return "PostinstallRunnerAction"; }
-  std::string Type() const { return StaticType(); }
+  std::string Type() const override { return StaticType(); }
 
  private:
   friend class PostinstallRunnerActionTest;
diff --git a/payload_consumer/postinstall_runner_action_unittest.cc b/payload_consumer/postinstall_runner_action_unittest.cc
index 85535d7..01d8d8f 100644
--- a/payload_consumer/postinstall_runner_action_unittest.cc
+++ b/payload_consumer/postinstall_runner_action_unittest.cc
@@ -39,36 +39,12 @@
 #include "update_engine/common/utils.h"
 
 using brillo::MessageLoop;
-using chromeos_update_engine::test_utils::System;
-using chromeos_update_engine::test_utils::WriteFileString;
+using chromeos_update_engine::test_utils::ScopedLoopbackDeviceBinder;
 using std::string;
-using std::unique_ptr;
 using std::vector;
 
 namespace chromeos_update_engine {
 
-class PostinstallRunnerActionTest : public ::testing::Test {
- protected:
-  void SetUp() override {
-    loop_.SetAsCurrent();
-    async_signal_handler_.Init();
-    subprocess_.Init(&async_signal_handler_);
-  }
-
-  // DoTest with various combinations of do_losetup, err_code and
-  // powerwash_required.
-  void DoTest(bool do_losetup, int err_code, bool powerwash_required);
-
- protected:
-  static const char* kImageMountPointTemplate;
-
-  base::MessageLoopForIO base_loop_;
-  brillo::BaseMessageLoop loop_{&base_loop_};
-  brillo::AsynchronousSignalHandler async_signal_handler_;
-  Subprocess subprocess_;
-  FakeBootControl fake_boot_control_;
-};
-
 class PostinstActionProcessorDelegate : public ActionProcessorDelegate {
  public:
   PostinstActionProcessorDelegate()
@@ -86,168 +62,89 @@
       code_set_ = true;
     }
   }
+
   ErrorCode code_;
   bool code_set_;
 };
 
-TEST_F(PostinstallRunnerActionTest, RunAsRootSimpleTest) {
-  DoTest(true, 0, false);
-}
+class PostinstallRunnerActionTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    loop_.SetAsCurrent();
+    async_signal_handler_.Init();
+    subprocess_.Init(&async_signal_handler_);
+    ASSERT_TRUE(utils::MakeTempDirectory(
+        "postinstall_runner_action_unittest-XXXXXX", &working_dir_));
+    // We use a test-specific powerwash marker file, to avoid race conditions.
+    powerwash_marker_file_ = working_dir_ + "/factory_install_reset";
+    // These tests use the postinstall files generated by "generate_images.sh"
+    // stored in the "disk_ext2_ue_settings.img" image.
+    postinstall_image_ = test_utils::GetBuildArtifactsPath()
+                             .Append("gen/disk_ext2_ue_settings.img")
+                             .value();
 
-TEST_F(PostinstallRunnerActionTest, RunAsRootPowerwashRequiredTest) {
-  DoTest(true, 0, true);
-}
+    ASSERT_EQ(0U, getuid()) << "Run these tests as root.";
+  }
 
-TEST_F(PostinstallRunnerActionTest, RunAsRootCantMountTest) {
-  DoTest(false, 0, true);
-}
+  void TearDown() override {
+    EXPECT_TRUE(base::DeleteFile(base::FilePath(working_dir_), true));
+  }
 
-TEST_F(PostinstallRunnerActionTest, RunAsRootErrScriptTest) {
-  DoTest(true, 1, false);
-}
+  // Setup an action processor and run the PostinstallRunnerAction with a single
+  // partition |device_path|, running the |postinstall_program| command from
+  // there.
+  void RunPosinstallAction(const string& device_path,
+                           const string& postinstall_program,
+                           bool powerwash_required);
 
-TEST_F(PostinstallRunnerActionTest, RunAsRootFirmwareBErrScriptTest) {
-  DoTest(true, 3, false);
-}
+ protected:
+  base::MessageLoopForIO base_loop_;
+  brillo::BaseMessageLoop loop_{&base_loop_};
+  brillo::AsynchronousSignalHandler async_signal_handler_;
+  Subprocess subprocess_;
 
-TEST_F(PostinstallRunnerActionTest, RunAsRootFirmwareROErrScriptTest) {
-  DoTest(true, 4, false);
-}
+  // A temporary working directory used for the test.
+  string working_dir_;
+  string powerwash_marker_file_;
 
-const char* PostinstallRunnerActionTest::kImageMountPointTemplate =
-    "au_destination-XXXXXX";
+  // The path to the postinstall sample image.
+  string postinstall_image_;
 
-void PostinstallRunnerActionTest::DoTest(
-    bool do_losetup,
-    int err_code,
+  FakeBootControl fake_boot_control_;
+  PostinstActionProcessorDelegate delegate_;
+};
+
+void PostinstallRunnerActionTest::RunPosinstallAction(
+    const string& device_path,
+    const string& postinstall_program,
     bool powerwash_required) {
-  ASSERT_EQ(0U, getuid()) << "Run me as root. Ideally don't run other tests "
-                          << "as root, tho.";
-  // True if the post-install action is expected to succeed.
-  bool should_succeed = do_losetup && !err_code;
-
-  string orig_cwd;
-  {
-    vector<char> buf(1000);
-    ASSERT_EQ(buf.data(), getcwd(buf.data(), buf.size()));
-    orig_cwd = string(buf.data(), strlen(buf.data()));
-  }
-
-  // Create a unique named working directory and chdir into it.
-  string cwd;
-  ASSERT_TRUE(utils::MakeTempDirectory(
-          "postinstall_runner_action_unittest-XXXXXX",
-          &cwd));
-  ASSERT_EQ(0, test_utils::Chdir(cwd));
-
-  // Create a 10MiB sparse file to be used as image; format it as ext2.
-  ASSERT_EQ(0, System(
-          "dd if=/dev/zero of=image.dat seek=10485759 bs=1 count=1 "
-          "status=none"));
-  ASSERT_EQ(0, System("mkfs.ext2 -F image.dat"));
-
-  // Create a uniquely named image mount point, mount the image.
-  ASSERT_EQ(0, System(string("mkdir -p ") + kStatefulPartition));
-  string mountpoint;
-  ASSERT_TRUE(utils::MakeTempDirectory(
-          string(kStatefulPartition) + "/" + kImageMountPointTemplate,
-          &mountpoint));
-  ASSERT_EQ(0, System(string("mount -o loop image.dat ") + mountpoint));
-
-  // Generate a fake postinst script inside the image.
-  string script = (err_code ?
-                   base::StringPrintf("#!/bin/bash\nexit %d", err_code) :
-                   base::StringPrintf(
-                       "#!/bin/bash\n"
-                       "mount | grep au_postint_mount | grep ext2\n"
-                       "if [ $? -eq 0 ]; then\n"
-                       "  touch %s/postinst_called\n"
-                       "fi\n",
-                       cwd.c_str()));
-  const string script_file_name = mountpoint + "/postinst";
-  ASSERT_TRUE(WriteFileString(script_file_name, script));
-  ASSERT_EQ(0, System(string("chmod a+x ") + script_file_name));
-
-  // Unmount image; do not remove the uniquely named directory as it will be
-  // reused during the test.
-  ASSERT_TRUE(utils::UnmountFilesystem(mountpoint));
-
-  // get a loop device we can use for the install device
-  string dev = "/dev/null";
-
-  unique_ptr<test_utils::ScopedLoopbackDeviceBinder> loop_releaser;
-  if (do_losetup) {
-    loop_releaser.reset(new test_utils::ScopedLoopbackDeviceBinder(
-            cwd + "/image.dat", &dev));
-  }
-
-  // We use a test-specific powerwash marker file, to avoid race conditions.
-  string powerwash_marker_file = mountpoint + "/factory_install_reset";
-  LOG(INFO) << ">>> powerwash_marker_file=" << powerwash_marker_file;
-
   ActionProcessor processor;
   ObjectFeederAction<InstallPlan> feeder_action;
   InstallPlan::Partition part;
   part.name = "part";
-  part.target_path = dev;
+  part.target_path = device_path;
   part.run_postinstall = true;
-  part.postinstall_path = kPostinstallDefaultScript;
+  part.postinstall_path = postinstall_program;
   InstallPlan install_plan;
   install_plan.partitions = {part};
-  install_plan.download_url = "http://devserver:8080/update";
+  install_plan.download_url = "http://127.0.0.1:8080/update";
   install_plan.powerwash_required = powerwash_required;
   feeder_action.set_obj(install_plan);
   PostinstallRunnerAction runner_action(&fake_boot_control_,
-                                        powerwash_marker_file.c_str());
+                                        powerwash_marker_file_.c_str());
   BondActions(&feeder_action, &runner_action);
   ObjectCollectorAction<InstallPlan> collector_action;
   BondActions(&runner_action, &collector_action);
-  PostinstActionProcessorDelegate delegate;
   processor.EnqueueAction(&feeder_action);
   processor.EnqueueAction(&runner_action);
   processor.EnqueueAction(&collector_action);
-  processor.set_delegate(&delegate);
+  processor.set_delegate(&delegate_);
 
   loop_.PostTask(FROM_HERE,
                  base::Bind([&processor] { processor.StartProcessing(); }));
   loop_.Run();
   ASSERT_FALSE(processor.IsRunning());
-
-  EXPECT_TRUE(delegate.code_set_);
-  EXPECT_EQ(should_succeed, delegate.code_ == ErrorCode::kSuccess);
-  if (should_succeed)
-    EXPECT_EQ(install_plan, collector_action.object());
-
-  const base::FilePath kPowerwashMarkerPath(powerwash_marker_file);
-  string actual_cmd;
-  if (should_succeed && powerwash_required) {
-    EXPECT_TRUE(base::ReadFileToString(kPowerwashMarkerPath, &actual_cmd));
-    EXPECT_EQ(kPowerwashCommand, actual_cmd);
-  } else {
-    EXPECT_FALSE(
-        base::ReadFileToString(kPowerwashMarkerPath, &actual_cmd));
-  }
-
-  if (err_code == 2)
-    EXPECT_EQ(ErrorCode::kPostinstallBootedFromFirmwareB, delegate.code_);
-
-  struct stat stbuf;
-  int rc = lstat((string(cwd) + "/postinst_called").c_str(), &stbuf);
-  if (should_succeed)
-    ASSERT_EQ(0, rc);
-  else
-    ASSERT_LT(rc, 0);
-
-  if (do_losetup) {
-    loop_releaser.reset(nullptr);
-  }
-
-  // Remove unique stateful directory.
-  ASSERT_EQ(0, System(string("rm -fr ") + mountpoint));
-
-  // Remove the temporary work directory.
-  ASSERT_EQ(0, test_utils::Chdir(orig_cwd));
-  ASSERT_EQ(0, System(string("rm -fr ") + cwd));
+  EXPECT_TRUE(delegate_.code_set_);
 }
 
 // Death tests don't seem to be working on Hardy
@@ -258,4 +155,78 @@
                "postinstall_runner_action.h:.*] Check failed");
 }
 
+// Test that postinstall succeeds in the simple case of running the default
+// /postinst command which only exits 0.
+TEST_F(PostinstallRunnerActionTest, RunAsRootSimpleTest) {
+  ScopedLoopbackDeviceBinder loop(postinstall_image_, false, nullptr);
+  RunPosinstallAction(loop.dev(), kPostinstallDefaultScript, false);
+  EXPECT_EQ(ErrorCode::kSuccess, delegate_.code_);
+
+  // Since powerwash_required was false, this should not trigger a powerwash.
+  EXPECT_FALSE(utils::FileExists(powerwash_marker_file_.c_str()));
+}
+
+TEST_F(PostinstallRunnerActionTest, RunAsRootRunSymlinkFileTest) {
+  ScopedLoopbackDeviceBinder loop(postinstall_image_, false, nullptr);
+  RunPosinstallAction(loop.dev(), "bin/postinst_link", false);
+  EXPECT_EQ(ErrorCode::kSuccess, delegate_.code_);
+}
+
+TEST_F(PostinstallRunnerActionTest, RunAsRootPowerwashRequiredTest) {
+  ScopedLoopbackDeviceBinder loop(postinstall_image_, false, nullptr);
+  // Run a simple postinstall program but requiring a powerwash.
+  RunPosinstallAction(loop.dev(), "bin/postinst_example", true);
+  EXPECT_EQ(ErrorCode::kSuccess, delegate_.code_);
+
+  // Check that the powerwash marker file was set.
+  string actual_cmd;
+  EXPECT_TRUE(base::ReadFileToString(base::FilePath(powerwash_marker_file_),
+                                     &actual_cmd));
+  EXPECT_EQ(kPowerwashCommand, actual_cmd);
+}
+
+// Runs postinstall from a partition file that doesn't mount, so it should
+// fail.
+TEST_F(PostinstallRunnerActionTest, RunAsRootCantMountTest) {
+  RunPosinstallAction("/dev/null", kPostinstallDefaultScript, false);
+  EXPECT_EQ(ErrorCode::kPostinstallRunnerError, delegate_.code_);
+
+  // In case of failure, Postinstall should not signal a powerwash even if it
+  // was requested.
+  EXPECT_FALSE(utils::FileExists(powerwash_marker_file_.c_str()));
+}
+
+// Check that the failures from the postinstall script cause the action to
+// fail.
+TEST_F(PostinstallRunnerActionTest, RunAsRootErrScriptTest) {
+  ScopedLoopbackDeviceBinder loop(postinstall_image_, false, nullptr);
+  RunPosinstallAction(loop.dev(), "bin/postinst_fail1", false);
+  EXPECT_EQ(ErrorCode::kPostinstallRunnerError, delegate_.code_);
+}
+
+// The exit code 3 and 4 are a specials cases that would be reported back to
+// UMA with a different error code. Test those cases are properly detected.
+TEST_F(PostinstallRunnerActionTest, RunAsRootFirmwareBErrScriptTest) {
+  ScopedLoopbackDeviceBinder loop(postinstall_image_, false, nullptr);
+  RunPosinstallAction(loop.dev(), "bin/postinst_fail3", false);
+  EXPECT_EQ(ErrorCode::kPostinstallBootedFromFirmwareB, delegate_.code_);
+}
+
+// Check that you can't specify an absolute path.
+TEST_F(PostinstallRunnerActionTest, RunAsRootAbsolutePathNotAllowedTest) {
+  ScopedLoopbackDeviceBinder loop(postinstall_image_, false, nullptr);
+  RunPosinstallAction(loop.dev(), "/etc/../bin/sh", false);
+  EXPECT_EQ(ErrorCode::kPostinstallRunnerError, delegate_.code_);
+}
+
+#ifdef __ANDROID__
+// Check that the postinstall file is relabeled to the postinstall label.
+// SElinux labels are only set on Android.
+TEST_F(PostinstallRunnerActionTest, RunAsRootCheckFileContextsTest) {
+  ScopedLoopbackDeviceBinder loop(postinstall_image_, false, nullptr);
+  RunPosinstallAction(loop.dev(), "bin/self_check_context", false);
+  EXPECT_EQ(ErrorCode::kSuccess, delegate_.code_);
+}
+#endif  // __ANDROID__
+
 }  // namespace chromeos_update_engine