diff options
author | 2025-03-12 16:44:02 +0000 | |
---|---|---|
committer | 2025-03-20 16:33:35 -0700 | |
commit | b2d23dd55d0f1d16beb43a247d62bf09a8060665 (patch) | |
tree | e684811a36c48b5337ce34f9e9e872b267d1b569 | |
parent | 6f3beba5e01780c30364e34be15335da51c4e0e5 (diff) |
Add helper classes for reading and writing SDC files.
Secure dex metadata companion (SDC) file is a file type that augments a
secure dex metadata (SDM) file with additional metadata. It is generated
on device when the SDM file is being installed.
Bug: 377474232
Test: m test-art-host-gtest-art_runtime_tests
Change-Id: I13cf86db9cdcca85e5b59bd992a4deddeeb57501
-rw-r--r-- | runtime/Android.bp | 2 | ||||
-rw-r--r-- | runtime/oat/sdc_file.cc | 113 | ||||
-rw-r--r-- | runtime/oat/sdc_file.h | 89 | ||||
-rw-r--r-- | runtime/oat/sdc_file_test.cc | 167 |
4 files changed, 371 insertions, 0 deletions
diff --git a/runtime/Android.bp b/runtime/Android.bp index 892f1b0a19..c0a65f27a7 100644 --- a/runtime/Android.bp +++ b/runtime/Android.bp @@ -397,6 +397,7 @@ cc_defaults { "oat/oat_file_assistant_context.cc", "oat/oat_file_manager.cc", "oat/oat_quick_method_header.cc", + "oat/sdc_file.cc", "oat/stack_map.cc", "object_lock.cc", "offsets.cc", @@ -1129,6 +1130,7 @@ art_cc_defaults { "native_stack_dump_test.cc", "oat/oat_file_assistant_test.cc", "oat/oat_file_test.cc", + "oat/sdc_file_test.cc", "parsed_options_test.cc", "prebuilt_tools_test.cc", "proxy_test.cc", diff --git a/runtime/oat/sdc_file.cc b/runtime/oat/sdc_file.cc new file mode 100644 index 0000000000..35a759e576 --- /dev/null +++ b/runtime/oat/sdc_file.cc @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2025 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 "sdc_file.h" + +#include <memory> +#include <regex> +#include <string> +#include <string_view> +#include <unordered_map> +#include <vector> + +#include "android-base/file.h" +#include "android-base/parseint.h" +#include "android-base/scopeguard.h" +#include "base/macros.h" +#include "base/utils.h" + +namespace art HIDDEN { + +using ::android::base::ParseInt; +using ::android::base::ReadFileToString; +using ::android::base::WriteStringToFd; + +std::unique_ptr<SdcReader> SdcReader::Load(const std::string filename, std::string* error_msg) { + std::unique_ptr<SdcReader> reader(new SdcReader()); + + // The sdc file is supposed to be small, so read fully into memory for simplicity. + if (!ReadFileToString(filename, &reader->content_)) { + *error_msg = ART_FORMAT("Failed to load sdc file '{}': {}", filename, strerror(errno)); + return nullptr; + } + + std::vector<std::string_view> lines; + Split(reader->content_, '\n', &lines); + std::unordered_map<std::string_view, std::string_view> map; + for (std::string_view line : lines) { + size_t pos = line.find('='); + if (pos == std::string_view::npos || pos == 0) { + *error_msg = ART_FORMAT("Malformed line '{}' in sdc file '{}'", line, filename); + return nullptr; + } + if (!map.try_emplace(line.substr(0, pos), line.substr(pos + 1)).second) { + *error_msg = ART_FORMAT("Duplicate key '{}' in sdc file '{}'", line.substr(0, pos), filename); + return nullptr; + } + } + + decltype(map)::iterator it; + if ((it = map.find("sdm-timestamp")) == map.end()) { + *error_msg = ART_FORMAT("Missing key 'sdm-timestamp' in sdc file '{}'", filename); + return nullptr; + } + if (!ParseInt(std::string(it->second), &reader->sdm_timestamp_, /*min=*/1l)) { + *error_msg = ART_FORMAT("Invalid 'sdm-timestamp' {}", it->second); + return nullptr; + } + + if ((it = map.find("apex-versions")) == map.end()) { + *error_msg = ART_FORMAT("Missing key 'apex-versions' in sdc file '{}'", filename); + return nullptr; + } + if (!std::regex_match(it->second.begin(), it->second.end(), std::regex("[0-9/]*"))) { + *error_msg = ART_FORMAT("Invalid 'apex-versions' {}", it->second); + return nullptr; + } + reader->apex_versions_ = it->second; + + if (map.size() > 2) { + *error_msg = ART_FORMAT("Malformed sdc file '{}'. Unrecognized keys", filename); + return nullptr; + } + + return reader; +} + +bool SdcWriter::Save(std::string* error_msg) { + auto cleanup = android::base::make_scope_guard([this] { (void)file_.FlushClose(); }); + if (sdm_timestamp_ <= 0) { + *error_msg = ART_FORMAT("Invalid 'sdm-timestamp' {}", sdm_timestamp_); + return false; + } + DCHECK_EQ(file_.GetLength(), 0); + std::string content = + ART_FORMAT("sdm-timestamp={}\napex-versions={}\n", sdm_timestamp_, apex_versions_); + if (!WriteStringToFd(content, file_.Fd())) { + *error_msg = ART_FORMAT("Failed to write sdc file '{}': {}", file_.GetPath(), strerror(errno)); + return false; + } + int res = file_.FlushClose(); + if (res != 0) { + *error_msg = + ART_FORMAT("Failed to flush close sdc file '{}': {}", file_.GetPath(), strerror(-res)); + return false; + } + cleanup.Disable(); + return true; +} + +} // namespace art diff --git a/runtime/oat/sdc_file.h b/runtime/oat/sdc_file.h new file mode 100644 index 0000000000..18c34a3527 --- /dev/null +++ b/runtime/oat/sdc_file.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025 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. + */ + +#ifndef ART_RUNTIME_OAT_SDC_FILE_H_ +#define ART_RUNTIME_OAT_SDC_FILE_H_ + +#include <ctime> +#include <memory> +#include <string> +#include <string_view> +#include <utility> + +#include "base/macros.h" +#include "base/os.h" + +namespace art HIDDEN { + +// A helper class to read a secure dex metadata companion (SDC) file. +// +// Secure dex metadata companion (SDC) file is a file type that augments a secure dex metadata (SDM) +// file with additional metadata. +// +// 1. There may be exactly one SDC file accompanying each SDM file. An SDC file without a +// corresponding SDM file, or with a mismatching SDM timestamp, is garbage. +// 2. They are always local on device. +// 3. They are only read and written by the ART module. +// 4. A later version of the ART module must be able to understand the contents. +// +// It is a text file in the format of: +// key1=value1\n +// key2=value2\n +// ... +// Repeated keys are not allowed. This is an extensible format, so versioning is not needed. +class SdcReader { + public: + static std::unique_ptr<SdcReader> Load(const std::string filename, std::string* error_msg); + + // The mtime of the SDM file on device. + // This is for detecting obsolete SDC files. + time_t GetSdmTimestamp() const { return sdm_timestamp_; } + + // The value of `Runtime::GetApexVersions` at the time where the SDM file was first seen on + // device. This is for detecting samegrade placebos. + std::string_view GetApexVersions() const { return apex_versions_; } + + private: + SdcReader() = default; + + std::string content_; + time_t sdm_timestamp_; + std::string_view apex_versions_; +}; + +// A helper class to write a secure dex metadata companion (SDC) file. +class EXPORT SdcWriter { + public: + // Takes ownership of the file. + explicit SdcWriter(File&& file) : file_(std::move(file)) {} + + // See `SdcReader::GetSdmTimestamp`. + void SetSdmTimestamp(time_t value) { sdm_timestamp_ = value; } + + // See `SdcReader::GetApexVersions`. + void SetApexVersions(std::string_view value) { apex_versions_ = value; } + + bool Save(std::string* error_msg); + + private: + File file_; + time_t sdm_timestamp_ = 0; + std::string apex_versions_; +}; + +} // namespace art + +#endif // ART_RUNTIME_OAT_SDC_FILE_H_ diff --git a/runtime/oat/sdc_file_test.cc b/runtime/oat/sdc_file_test.cc new file mode 100644 index 0000000000..f3bf1bc36d --- /dev/null +++ b/runtime/oat/sdc_file_test.cc @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2025 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 "sdc_file.h" + +#include <memory> +#include <string> + +#include "android-base/file.h" +#include "base/common_art_test.h" +#include "base/macros.h" +#include "base/os.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace art HIDDEN { + +using ::android::base::ReadFileToString; +using ::android::base::WriteStringToFile; +using ::testing::HasSubstr; +using ::testing::StartsWith; + +class SdcFileTestBase : public CommonArtTest { + protected: + void SetUp() override { + CommonArtTest::SetUp(); + + scratch_dir_ = std::make_unique<ScratchDir>(); + test_file_ = scratch_dir_->GetPath() + "test.sdc"; + } + + void TearDown() override { + scratch_dir_.reset(); + CommonArtTest::TearDown(); + } + + std::unique_ptr<ScratchDir> scratch_dir_; + std::string test_file_; +}; + +class SdcReaderTest : public SdcFileTestBase {}; + +TEST_F(SdcReaderTest, Success) { + ASSERT_TRUE( + WriteStringToFile("sdm-timestamp=987654321\napex-versions=/12345678/12345679\n", test_file_)); + + std::string error_msg; + std::unique_ptr<SdcReader> reader = SdcReader::Load(test_file_, &error_msg); + ASSERT_NE(reader, nullptr) << error_msg; + + EXPECT_EQ(reader->GetApexVersions(), "/12345678/12345679"); + EXPECT_EQ(reader->GetSdmTimestamp(), 987654321l); +} + +TEST_F(SdcReaderTest, NotFound) { + std::string error_msg; + std::unique_ptr<SdcReader> reader = SdcReader::Load(test_file_, &error_msg); + ASSERT_EQ(reader, nullptr); + + EXPECT_THAT(error_msg, StartsWith("Failed to load sdc file")); +} + +TEST_F(SdcReaderTest, MissingApexVersions) { + ASSERT_TRUE(WriteStringToFile("sdm-timestamp=987654321\n", test_file_)); + + std::string error_msg; + std::unique_ptr<SdcReader> reader = SdcReader::Load(test_file_, &error_msg); + ASSERT_EQ(reader, nullptr); + + EXPECT_THAT(error_msg, StartsWith("Missing key 'apex-versions' in sdc file")); +} + +TEST_F(SdcReaderTest, InvalidSdmTimestamp) { + ASSERT_TRUE(WriteStringToFile("sdm-timestamp=0\napex-versions=/12345678/12345679\n", test_file_)); + + std::string error_msg; + std::unique_ptr<SdcReader> reader = SdcReader::Load(test_file_, &error_msg); + ASSERT_EQ(reader, nullptr); + + EXPECT_THAT(error_msg, HasSubstr("Invalid 'sdm-timestamp'")); +} + +TEST_F(SdcReaderTest, InvalidApexVersions) { + ASSERT_TRUE(WriteStringToFile("sdm-timestamp=987654321\napex-versions=abc\n", test_file_)); + + std::string error_msg; + std::unique_ptr<SdcReader> reader = SdcReader::Load(test_file_, &error_msg); + ASSERT_EQ(reader, nullptr); + + EXPECT_THAT(error_msg, HasSubstr("Invalid 'apex-versions'")); +} + +TEST_F(SdcReaderTest, UnrecognizedKey) { + ASSERT_TRUE(WriteStringToFile( + "sdm-timestamp=987654321\napex-versions=/12345678/12345679\nwrong-key=12345678\n", + test_file_)); + + std::string error_msg; + std::unique_ptr<SdcReader> reader = SdcReader::Load(test_file_, &error_msg); + ASSERT_EQ(reader, nullptr); + + EXPECT_THAT(error_msg, HasSubstr("Unrecognized keys")); +} + +class SdcWriterTest : public SdcFileTestBase {}; + +TEST_F(SdcWriterTest, Success) { + std::unique_ptr<File> file(OS::CreateEmptyFileWriteOnly(test_file_.c_str())); + ASSERT_NE(file, nullptr); + SdcWriter writer(std::move(*file)); + + writer.SetApexVersions("/12345678/12345679"); + writer.SetSdmTimestamp(987654321l); + + std::string error_msg; + ASSERT_TRUE(writer.Save(&error_msg)) << error_msg; + + std::string content; + ASSERT_TRUE(ReadFileToString(test_file_, &content)); + + EXPECT_EQ(content, "sdm-timestamp=987654321\napex-versions=/12345678/12345679\n"); +} + +TEST_F(SdcWriterTest, SaveFailed) { + ASSERT_TRUE(WriteStringToFile("", test_file_)); + + std::unique_ptr<File> file(OS::OpenFileForReading(test_file_.c_str())); + ASSERT_NE(file, nullptr); + SdcWriter writer( + File(file->Release(), file->GetPath(), /*check_usage=*/false, /*read_only_mode=*/false)); + + writer.SetApexVersions("/12345678/12345679"); + writer.SetSdmTimestamp(987654321l); + + std::string error_msg; + EXPECT_FALSE(writer.Save(&error_msg)); + + EXPECT_THAT(error_msg, StartsWith("Failed to write sdc file")); +} + +TEST_F(SdcWriterTest, InvalidSdmTimestamp) { + std::unique_ptr<File> file(OS::CreateEmptyFileWriteOnly(test_file_.c_str())); + ASSERT_NE(file, nullptr); + SdcWriter writer(std::move(*file)); + + writer.SetApexVersions("/12345678/12345679"); + + std::string error_msg; + EXPECT_FALSE(writer.Save(&error_msg)); + + EXPECT_THAT(error_msg, StartsWith("Invalid 'sdm-timestamp'")); +} + +} // namespace art |