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.

(cherry picked from commit cbc2274c4160805bf726df872390112654816ca7)

Change-Id: Ib4644572e2813615c89e4c6eef632e895cf0b90c
diff --git a/common/test_utils.cc b/common/test_utils.cc
index 77a9141..aba92e9 100644
--- a/common/test_utils.cc
+++ b/common/test_utils.cc
@@ -18,8 +18,12 @@
 
 #include <dirent.h>
 #include <errno.h>
+#include <fcntl.h>
+#include <linux/loop.h>
+#include <linux/major.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <sys/ioctl.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/xattr.h>
@@ -137,27 +141,76 @@
   return utils::WriteFile(path.c_str(), data.data(), data.size());
 }
 
-// Binds provided |filename| to an unused loopback device, whose name is written
-// to the string pointed to by |lo_dev_name_p|. Returns true on success, false
-// otherwise (along with corresponding test failures), in which case the content
-// of |lo_dev_name_p| is unknown.
-bool BindToUnusedLoopDevice(const string& filename, string* lo_dev_name_p) {
-  CHECK(lo_dev_name_p);
+bool BindToUnusedLoopDevice(const string& filename,
+                            bool writable,
+                            string* out_lo_dev_name) {
+  CHECK(out_lo_dev_name);
+  // Get the next available loop-device.
+  int control_fd =
+      HANDLE_EINTR(open("/dev/loop-control", O_RDWR | O_LARGEFILE));
+  TEST_AND_RETURN_FALSE_ERRNO(control_fd >= 0);
+  int loop_number = ioctl(control_fd, LOOP_CTL_GET_FREE);
+  IGNORE_EINTR(close(control_fd));
+  *out_lo_dev_name = StringPrintf("/dev/loop%d", loop_number);
 
-  // Bind to an unused loopback device, sanity check the device name.
-  lo_dev_name_p->clear();
-  if (!(utils::ReadPipe("losetup --show -f " + filename, lo_dev_name_p) &&
-        base::StartsWith(*lo_dev_name_p, "/dev/loop",
-                         base::CompareCase::SENSITIVE))) {
-    ADD_FAILURE();
+  // Double check that the loop exists and is free.
+  int loop_device_fd =
+      HANDLE_EINTR(open(out_lo_dev_name->c_str(), O_RDWR | O_LARGEFILE));
+  if (loop_device_fd == -1 && errno == ENOENT) {
+    // Workaround the case when the loop device doesn't exist.
+    TEST_AND_RETURN_FALSE_ERRNO(mknod(out_lo_dev_name->c_str(),
+                                      S_IFBLK | 0660,
+                                      makedev(LOOP_MAJOR, loop_number)) == 0);
+    loop_device_fd =
+        HANDLE_EINTR(open(out_lo_dev_name->c_str(), O_RDWR | O_LARGEFILE));
+  }
+  TEST_AND_RETURN_FALSE_ERRNO(loop_device_fd != -1);
+  ScopedFdCloser loop_device_fd_closer(&loop_device_fd);
+
+  struct loop_info64 device_info;
+  if (ioctl(loop_device_fd, LOOP_GET_STATUS64, &device_info) != -1 ||
+      errno != ENXIO) {
+    PLOG(ERROR) << "Loop device " << out_lo_dev_name->c_str()
+                << " already in use";
     return false;
   }
 
-  // Strip anything from the first newline char.
-  size_t newline_pos = lo_dev_name_p->find('\n');
-  if (newline_pos != string::npos)
-    lo_dev_name_p->erase(newline_pos);
+  // Open our data file and assign it to the loop device.
+  int data_fd = open(filename.c_str(),
+                     (writable ? O_RDWR : O_RDONLY) | O_LARGEFILE | O_CLOEXEC);
+  TEST_AND_RETURN_FALSE_ERRNO(data_fd >= 0);
+  ScopedFdCloser data_fd_closer(&data_fd);
+  TEST_AND_RETURN_FALSE_ERRNO(ioctl(loop_device_fd, LOOP_SET_FD, data_fd) == 0);
 
+  memset(&device_info, 0, sizeof(device_info));
+  device_info.lo_offset = 0;
+  device_info.lo_sizelimit = 0;  // 0 means whole file.
+  device_info.lo_flags = (writable ? 0 : LO_FLAGS_READ_ONLY);
+  device_info.lo_number = loop_number;
+  strncpy(reinterpret_cast<char*>(device_info.lo_file_name),
+          base::FilePath(filename).BaseName().value().c_str(),
+          LO_NAME_SIZE - 1);
+  device_info.lo_file_name[LO_NAME_SIZE - 1] = '\0';
+  TEST_AND_RETURN_FALSE_ERRNO(
+      ioctl(loop_device_fd, LOOP_SET_STATUS64, &device_info) == 0);
+  return true;
+}
+
+bool UnbindLoopDevice(const string& lo_dev_name) {
+  int loop_device_fd =
+      HANDLE_EINTR(open(lo_dev_name.c_str(), O_RDWR | O_LARGEFILE));
+  if (loop_device_fd == -1 && errno == ENOENT)
+    return true;
+  TEST_AND_RETURN_FALSE_ERRNO(loop_device_fd != -1);
+  ScopedFdCloser loop_device_fd_closer(&loop_device_fd);
+
+  struct loop_info64 device_info;
+  // Check if the device is bound before trying to unbind it.
+  int get_stat_err = ioctl(loop_device_fd, LOOP_GET_STATUS64, &device_info);
+  if (get_stat_err == -1 && errno == ENXIO)
+    return true;
+
+  TEST_AND_RETURN_FALSE_ERRNO(ioctl(loop_device_fd, LOOP_CLR_FD) == 0);
   return true;
 }
 
@@ -258,7 +311,8 @@
   dir_remover_.reset(new ScopedDirRemover(*mnt_path));
 
   string loop_dev;
-  loop_binder_.reset(new ScopedLoopbackDeviceBinder(file_path, &loop_dev));
+  loop_binder_.reset(
+      new ScopedLoopbackDeviceBinder(file_path, true, &loop_dev));
 
   EXPECT_TRUE(utils::MountFilesystem(loop_dev, *mnt_path, flags, "", nullptr));
   unmounter_.reset(new ScopedFilesystemUnmounter(*mnt_path));
diff --git a/common/test_utils.h b/common/test_utils.h
index 7be027a..2c8a6de 100644
--- a/common/test_utils.h
+++ b/common/test_utils.h
@@ -57,8 +57,15 @@
 bool WriteFileVector(const std::string& path, const brillo::Blob& data);
 bool WriteFileString(const std::string& path, const std::string& data);
 
-bool BindToUnusedLoopDevice(const std::string &filename,
-                            std::string* lo_dev_name_ptr);
+// Binds provided |filename| to an unused loopback device, whose name is written
+// to the string pointed to by |out_lo_dev_name|. The new loop device will be
+// read-only unless |writable| is set to true. Returns true on success, false
+// otherwise (along with corresponding test failures), in which case the content
+// of |out_lo_dev_name| is unknown.
+bool BindToUnusedLoopDevice(const std::string& filename,
+                            bool writable,
+                            std::string* out_lo_dev_name);
+bool UnbindLoopDevice(const std::string& lo_dev_name);
 
 // Returns true iff a == b
 bool ExpectVectorsEq(const brillo::Blob& a, const brillo::Blob& b);
@@ -120,8 +127,10 @@
 
 class ScopedLoopbackDeviceBinder {
  public:
-  ScopedLoopbackDeviceBinder(const std::string& file, std::string* dev) {
-    is_bound_ = BindToUnusedLoopDevice(file, &dev_);
+  ScopedLoopbackDeviceBinder(const std::string& file,
+                             bool writable,
+                             std::string* dev) {
+    is_bound_ = BindToUnusedLoopDevice(file, writable, &dev_);
     EXPECT_TRUE(is_bound_);
 
     if (is_bound_ && dev)
@@ -133,15 +142,8 @@
       return;
 
     for (int retry = 0; retry < 5; retry++) {
-      std::vector<std::string> args;
-      args.push_back("/sbin/losetup");
-      args.push_back("-d");
-      args.push_back(dev_);
-      int return_code = 0;
-      EXPECT_TRUE(Subprocess::SynchronousExec(args, &return_code, nullptr));
-      if (return_code == 0) {
+      if (UnbindLoopDevice(dev_))
         return;
-      }
       sleep(1);
     }
     ADD_FAILURE();
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
diff --git a/sample_images/generate_images.sh b/sample_images/generate_images.sh
index 17bb11c..c9936c1 100755
--- a/sample_images/generate_images.sh
+++ b/sample_images/generate_images.sh
@@ -124,6 +124,49 @@
 EOF
 }
 
+add_files_postinstall() {
+  local mntdir="$1"
+
+  sudo mkdir -p "${mntdir}"/bin >/dev/null
+
+  # A postinstall bash program.
+  sudo tee "${mntdir}"/bin/postinst_example >/dev/null <<EOF
+#!/etc/../bin/sh
+echo "I'm a postinstall program and I know how to write to stdout"
+echo "My call was $@"
+exit 0
+EOF
+
+  # A symlink to another program. This should also work.
+  sudo ln -s "postinst_example" "${mntdir}"/bin/postinst_link
+
+  sudo tee "${mntdir}"/bin/postinst_fail3 >/dev/null <<EOF
+#!/etc/../bin/sh
+exit 3
+EOF
+
+  sudo tee "${mntdir}"/bin/postinst_fail1 >/dev/null <<EOF
+#!/etc/../bin/sh
+exit 1
+EOF
+
+  # A postinstall bash program.
+  sudo tee "${mntdir}"/bin/self_check_context >/dev/null <<EOF
+#!/etc/../bin/sh
+echo "This is my context:"
+ls -lZ "\$0" | grep -F ' u:object_r:postinstall_file:s0 ' || exit 5
+exit 0
+EOF
+
+  sudo tee "${mntdir}"/postinst >/dev/null <<EOF
+#!/etc/../bin/sh
+echo "postinst"
+exit 0
+EOF
+
+  sudo chmod +x "${mntdir}"/postinst "${mntdir}"/bin/*
+}
+
 # generate_fs <filename> <kind> <size> [block_size] [block_groups]
 generate_fs() {
   local filename="$1"
@@ -152,6 +195,7 @@
   case "${kind}" in
     ue_settings)
       add_files_ue_settings "${mntdir}" "${block_size}"
+      add_files_postinstall "${mntdir}" "${block_size}"
       ;;
     default)
       add_files_default "${mntdir}" "${block_size}"
@@ -179,7 +223,7 @@
   generate_image disk_ext2_1k default 16777216 1024
   generate_image disk_ext2_4k default 16777216 4096
   generate_image disk_ext2_4k_empty empty $((1024 * 4096)) 4096
-  generate_image disk_ext2_ue_settings ue_settings 16777216 4096
+  generate_image disk_ext2_ue_settings ue_settings $((1024 * 4096)) 4096
 
   # Generate the tarball and delete temporary images.
   echo "Packing tar file sample_images.tar.bz2"
diff --git a/sample_images/sample_images.tar.bz2 b/sample_images/sample_images.tar.bz2
index fe1a80a..d77ae93 100644
--- a/sample_images/sample_images.tar.bz2
+++ b/sample_images/sample_images.tar.bz2
Binary files differ