/* * Copyright (C) 2021 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "vendor_boot_img_utils.h" using android::base::borrowed_fd; using android::base::ErrnoError; using android::base::GetExecutableDirectory; using android::base::ReadFdToString; using android::base::Result; using testing::AllOf; using testing::Each; using testing::Eq; using testing::HasSubstr; using testing::Not; using testing::Property; using std::string_literals::operator""s; // Expect that the Result returned by |expr| is successful, and value matches |result_matcher|. #define EXPECT_RESULT(expr, result_matcher) \ EXPECT_THAT(expr, AllOf(Property(&decltype(expr)::ok, Eq(true)), \ Property(&decltype(expr)::value, result_matcher))) // Expect that the Result returned by |expr| fails, and error message matches |error_matcher|. #define EXPECT_ERROR(expr, error_matcher) \ do { \ EXPECT_THAT( \ expr, \ AllOf(Property(&decltype(expr)::ok, Eq(false)), \ Property(&decltype(expr)::error, \ Property(&decltype(expr)::error_type::message, error_matcher)))); \ } while (0) namespace { // Wrapper of fstat. Result FileSize(borrowed_fd fd, std::filesystem::path path) { struct stat sb; if (fstat(fd.get(), &sb) == -1) return ErrnoError() << "fstat(" << path << ")"; return sb.st_size; } // Seek to beginning then read the whole file. Result ReadStartOfFdToString(borrowed_fd fd, std::filesystem::path path) { if (lseek(fd.get(), 0, SEEK_SET) != 0) return ErrnoError() << "lseek(" << path << ", 0, SEEK_SET)"; std::string content; if (!android::base::ReadFdToString(fd, &content)) return ErrnoError() << "read(" << path << ")"; return content; } // Round |value| up to page boundary. inline uint32_t round_up(uint32_t value, uint32_t page_size) { return (value + page_size - 1) / page_size * page_size; } // Match is successful if |arg| is a zero-padded version of |expected|. MATCHER_P(IsPadded, expected, (negation ? "is" : "isn't") + " zero-padded of expected value"s) { if (arg.size() < expected.size()) return false; if (0 != memcmp(arg.data(), expected.data(), expected.size())) return false; auto remainder = std::string_view(arg).substr(expected.size()); for (char e : remainder) if (e != '\0') return false; return true; } // Same as Eq, but don't print the content to avoid spam. MATCHER_P(MemEq, expected, (negation ? "is" : "isn't") + " expected value"s) { if (arg.size() != expected.size()) return false; return 0 == memcmp(arg.data(), expected.data(), expected.size()); } // Expect that |arg| and |expected| has the same AVB footer. MATCHER_P(HasSameAvbFooter, expected, (negation ? "has" : "does not have") + "expected AVB footer"s) { if (expected.size() < AVB_FOOTER_SIZE || arg.size() < AVB_FOOTER_SIZE) return false; return std::string_view(expected).substr(expected.size() - AVB_FOOTER_SIZE) == std::string_view(arg).substr(arg.size() - AVB_FOOTER_SIZE); } // A lazy handle of a file. struct TestFileHandle { virtual ~TestFileHandle() = default; // Lazily call OpenImpl(), cache result in open_result_. android::base::Result Open() { if (!open_result_.has_value()) open_result_ = OpenImpl(); return open_result_.value(); } // The original size at the time when the file is opened. If the file has been modified, // this field is NOT updated. uint64_t size() { CHECK(open_result_.has_value()); return size_; } // The current size of the file. If the file has been modified since opened, // this is updated. Result fsize() { CHECK(open_result_.has_value()); return FileSize(fd_, abs_path_); } borrowed_fd fd() { CHECK(open_result_.has_value()); return fd_; } Result Read() { CHECK(open_result_.has_value()); return ReadStartOfFdToString(fd_, abs_path_); } private: std::filesystem::path abs_path_; uint64_t size_; std::optional> open_result_; borrowed_fd fd_{-1}; // Opens |rel_path_| as a readonly fd, pass it to Transform, and store result to // |borrowed_fd_|. android::base::Result OpenImpl() { android::base::unique_fd read_fd(TEMP_FAILURE_RETRY( open(abs_path_.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_BINARY))); if (!read_fd.ok()) return ErrnoError() << "open(" << abs_path_ << ")"; auto size = FileSize(read_fd, abs_path_); if (!size.ok()) return size.error(); size_ = *size; auto borrowed_fd = Transform(abs_path_, std::move(read_fd)); if (!borrowed_fd.ok()) return borrowed_fd.error(); fd_ = borrowed_fd.value(); return {}; } protected: // |rel_path| is the relative path under test data directory. TestFileHandle(const std::filesystem::path& rel_path) : abs_path_(std::move(std::filesystem::path(GetExecutableDirectory()) / rel_path)) {} // Given |read_fd|, the readonly fd on the test file, return an fd that's suitable for client // to use. Implementation is responsible for managing the lifetime of the returned fd. virtual android::base::Result Transform(const std::filesystem::path& abs_path, android::base::unique_fd read_fd) = 0; }; // A TestFileHandle where the file is readonly. struct ReadOnlyTestFileHandle : TestFileHandle { ReadOnlyTestFileHandle(const std::filesystem::path& rel_path) : TestFileHandle(rel_path) {} private: android::base::unique_fd owned_fd_; android::base::Result Transform(const std::filesystem::path&, android::base::unique_fd read_fd) override { owned_fd_ = std::move(read_fd); return owned_fd_; } }; // A TestFileHandle where the test file is copies, hence read-writable. struct ReadWriteTestFileHandle : TestFileHandle { ReadWriteTestFileHandle(const std::filesystem::path& rel_path) : TestFileHandle(rel_path) {} private: std::unique_ptr temp_file_; android::base::Result Transform(const std::filesystem::path& abs_path, android::base::unique_fd read_fd) override { // Make a copy to avoid writing to test data. Test files are small, so it is okay // to read the whole file. auto content = ReadStartOfFdToString(read_fd, abs_path); if (!content.ok()) return content.error(); temp_file_ = std::make_unique(); if (temp_file_->fd == -1) return ErrnoError() << "copy " << abs_path << ": open temp file failed"; if (!android::base::WriteStringToFd(*content, temp_file_->fd)) return ErrnoError() << "copy " << abs_path << ": write temp file failed"; return temp_file_->fd; } }; class RepackVendorBootImgTestEnv : public ::testing::Environment { public: virtual void SetUp() { OpenTestFile("test_dtb.img", &dtb, &dtb_content); OpenTestFile("test_bootconfig.img", &bootconfig, &bootconfig_content); OpenTestFile("test_vendor_ramdisk_none.img", &none, &none_content); OpenTestFile("test_vendor_ramdisk_platform.img", &platform, &platform_content); OpenTestFile("test_vendor_ramdisk_replace.img", &replace, &replace_content); } std::unique_ptr dtb; std::string dtb_content; std::unique_ptr bootconfig; std::string bootconfig_content; std::unique_ptr none; std::string none_content; std::unique_ptr platform; std::string platform_content; std::unique_ptr replace; std::string replace_content; private: void OpenTestFile(const char* rel_path, std::unique_ptr* handle, std::string* content) { *handle = std::make_unique(rel_path); ASSERT_RESULT_OK((*handle)->Open()); auto content_res = (*handle)->Read(); ASSERT_RESULT_OK(content_res); *content = *content_res; } }; RepackVendorBootImgTestEnv* env = nullptr; struct RepackVendorBootImgTestParam { std::string vendor_boot_file_name; uint32_t expected_header_version; friend std::ostream& operator<<(std::ostream& os, const RepackVendorBootImgTestParam& param) { return os << param.vendor_boot_file_name; } }; class RepackVendorBootImgTest : public ::testing::TestWithParam { public: virtual void SetUp() { vboot = std::make_unique(GetParam().vendor_boot_file_name); ASSERT_RESULT_OK(vboot->Open()); } std::unique_ptr vboot; }; TEST_P(RepackVendorBootImgTest, InvalidSize) { EXPECT_ERROR(replace_vendor_ramdisk(vboot->fd(), vboot->size() + 1, "default", env->replace->fd(), env->replace->size()), HasSubstr("Size of vendor boot does not match")); EXPECT_ERROR(replace_vendor_ramdisk(vboot->fd(), vboot->size(), "default", env->replace->fd(), env->replace->size() + 1), HasSubstr("Size of new vendor ramdisk does not match")); } TEST_P(RepackVendorBootImgTest, ReplaceUnknown) { auto res = replace_vendor_ramdisk(vboot->fd(), vboot->size(), "unknown", env->replace->fd(), env->replace->size()); if (GetParam().expected_header_version == 3) { EXPECT_ERROR(res, Eq("Require vendor boot header V4 but is V3")); } else if (GetParam().expected_header_version == 4) { EXPECT_ERROR(res, Eq("Vendor ramdisk 'unknown' not found")); } } TEST_P(RepackVendorBootImgTest, ReplaceDefault) { auto old_content = vboot->Read(); ASSERT_RESULT_OK(old_content); ASSERT_RESULT_OK(replace_vendor_ramdisk(vboot->fd(), vboot->size(), "default", env->replace->fd(), env->replace->size())); EXPECT_RESULT(vboot->fsize(), vboot->size()) << "File size should not change after repack"; auto new_content_res = vboot->Read(); ASSERT_RESULT_OK(new_content_res); std::string_view new_content(*new_content_res); auto hdr = reinterpret_cast(new_content.data()); ASSERT_EQ(0, memcmp(VENDOR_BOOT_MAGIC, hdr->magic, VENDOR_BOOT_MAGIC_SIZE)); ASSERT_EQ(GetParam().expected_header_version, hdr->header_version); EXPECT_EQ(hdr->vendor_ramdisk_size, env->replace->size()); EXPECT_EQ(hdr->dtb_size, env->dtb->size()); auto o = round_up(sizeof(vendor_boot_img_hdr_v3), hdr->page_size); auto p = round_up(hdr->vendor_ramdisk_size, hdr->page_size); auto q = round_up(hdr->dtb_size, hdr->page_size); EXPECT_THAT(new_content.substr(o, p), IsPadded(env->replace_content)); EXPECT_THAT(new_content.substr(o + p, q), IsPadded(env->dtb_content)); if (hdr->header_version < 4) return; auto hdr_v4 = static_cast(hdr); EXPECT_EQ(hdr_v4->vendor_ramdisk_table_entry_num, 1); EXPECT_EQ(hdr_v4->vendor_ramdisk_table_size, 1 * hdr_v4->vendor_ramdisk_table_entry_size); EXPECT_GE(hdr_v4->vendor_ramdisk_table_entry_size, sizeof(vendor_ramdisk_table_entry_v4)); auto entry = reinterpret_cast(&new_content[o + p + q]); EXPECT_EQ(entry->ramdisk_offset, 0); EXPECT_EQ(entry->ramdisk_size, hdr_v4->vendor_ramdisk_size); EXPECT_EQ(entry->ramdisk_type, VENDOR_RAMDISK_TYPE_NONE); EXPECT_EQ(hdr_v4->bootconfig_size, env->bootconfig->size()); auto r = round_up(hdr_v4->vendor_ramdisk_table_size, hdr_v4->page_size); auto s = round_up(hdr_v4->bootconfig_size, hdr_v4->page_size); EXPECT_THAT(new_content.substr(o + p + q + r, s), IsPadded(env->bootconfig_content)); EXPECT_THAT(new_content, HasSameAvbFooter(*old_content)); } INSTANTIATE_TEST_SUITE_P( RepackVendorBootImgTest, RepackVendorBootImgTest, ::testing::Values(RepackVendorBootImgTestParam{"vendor_boot_v3.img", 3}, RepackVendorBootImgTestParam{"vendor_boot_v4_with_frag.img", 4}, RepackVendorBootImgTestParam{"vendor_boot_v4_without_frag.img", 4}), [](const auto& info) { return android::base::StringReplace(info.param.vendor_boot_file_name, ".", "_", false); }); std::string_view GetRamdiskName(const vendor_ramdisk_table_entry_v4* entry) { auto ramdisk_name = reinterpret_cast(entry->ramdisk_name); return std::string_view(ramdisk_name, strnlen(ramdisk_name, VENDOR_RAMDISK_NAME_SIZE)); } class RepackVendorBootImgTestV4 : public ::testing::TestWithParam { public: virtual void SetUp() { vboot = std::make_unique("vendor_boot_v4_with_frag.img"); ASSERT_RESULT_OK(vboot->Open()); } std::unique_ptr vboot; }; TEST_P(RepackVendorBootImgTestV4, Replace) { uint32_t replace_ramdisk_type = GetParam(); std::string replace_ramdisk_name; std::string expect_new_ramdisk_content; uint32_t expect_none_size = env->none->size(); uint32_t expect_platform_size = env->platform->size(); switch (replace_ramdisk_type) { case VENDOR_RAMDISK_TYPE_NONE: replace_ramdisk_name = "none_ramdisk"; expect_new_ramdisk_content = env->replace_content + env->platform_content; expect_none_size = env->replace->size(); break; case VENDOR_RAMDISK_TYPE_PLATFORM: replace_ramdisk_name = "platform_ramdisk"; expect_new_ramdisk_content = env->none_content + env->replace_content; expect_platform_size = env->replace->size(); break; default: LOG(FATAL) << "Ramdisk type " << replace_ramdisk_type << " is not supported by this test."; } auto old_content = vboot->Read(); ASSERT_RESULT_OK(old_content); ASSERT_RESULT_OK(replace_vendor_ramdisk(vboot->fd(), vboot->size(), replace_ramdisk_name, env->replace->fd(), env->replace->size())); EXPECT_RESULT(vboot->fsize(), vboot->size()) << "File size should not change after repack"; auto new_content_res = vboot->Read(); ASSERT_RESULT_OK(new_content_res); std::string_view new_content(*new_content_res); auto hdr = reinterpret_cast(new_content.data()); ASSERT_EQ(0, memcmp(VENDOR_BOOT_MAGIC, hdr->magic, VENDOR_BOOT_MAGIC_SIZE)); ASSERT_EQ(4, hdr->header_version); EXPECT_EQ(hdr->vendor_ramdisk_size, expect_none_size + expect_platform_size); EXPECT_EQ(hdr->dtb_size, env->dtb->size()); EXPECT_EQ(hdr->bootconfig_size, env->bootconfig->size()); auto o = round_up(sizeof(vendor_boot_img_hdr_v3), hdr->page_size); auto p = round_up(hdr->vendor_ramdisk_size, hdr->page_size); auto q = round_up(hdr->dtb_size, hdr->page_size); auto r = round_up(hdr->vendor_ramdisk_table_size, hdr->page_size); auto s = round_up(hdr->bootconfig_size, hdr->page_size); EXPECT_THAT(new_content.substr(o, p), IsPadded(expect_new_ramdisk_content)); EXPECT_THAT(new_content.substr(o + p, q), IsPadded(env->dtb_content)); // Check changes in table. EXPECT_EQ(hdr->vendor_ramdisk_table_entry_num, 2); EXPECT_EQ(hdr->vendor_ramdisk_table_size, 2 * hdr->vendor_ramdisk_table_entry_size); EXPECT_GE(hdr->vendor_ramdisk_table_entry_size, sizeof(vendor_ramdisk_table_entry_v4)); auto entry_none = reinterpret_cast(&new_content[o + p + q]); EXPECT_EQ(entry_none->ramdisk_offset, 0); EXPECT_EQ(entry_none->ramdisk_size, expect_none_size); EXPECT_EQ(entry_none->ramdisk_type, VENDOR_RAMDISK_TYPE_NONE); EXPECT_EQ(GetRamdiskName(entry_none), "none_ramdisk"); auto entry_platform = reinterpret_cast( &new_content[o + p + q + hdr->vendor_ramdisk_table_entry_size]); EXPECT_EQ(entry_platform->ramdisk_offset, expect_none_size); EXPECT_EQ(entry_platform->ramdisk_size, expect_platform_size); EXPECT_EQ(entry_platform->ramdisk_type, VENDOR_RAMDISK_TYPE_PLATFORM); EXPECT_EQ(GetRamdiskName(entry_platform), "platform_ramdisk"); EXPECT_THAT(new_content.substr(o + p + q + r, s), IsPadded(env->bootconfig_content)); EXPECT_THAT(new_content, HasSameAvbFooter(*old_content)); } INSTANTIATE_TEST_SUITE_P(RepackVendorBootImgTest, RepackVendorBootImgTestV4, ::testing::Values(VENDOR_RAMDISK_TYPE_NONE, VENDOR_RAMDISK_TYPE_PLATFORM), [](const auto& info) { return info.param == VENDOR_RAMDISK_TYPE_NONE ? "none" : "platform"; }); } // namespace int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); env = static_cast( testing::AddGlobalTestEnvironment(new RepackVendorBootImgTestEnv)); return RUN_ALL_TESTS(); }