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