artd: delete optimized artifacts.
Bug: 225827974
Test: adb shell pm art delete-optimized-artifacts com.google.android.youtube
Ignore-AOSP-First: ART Services
Change-Id: I4d168ae17d67422139258f1968c717765e1ffeab
diff --git a/artd/Android.bp b/artd/Android.bp
index d4e2d47..292f09b 100644
--- a/artd/Android.bp
+++ b/artd/Android.bp
@@ -27,6 +27,7 @@
defaults: ["art_defaults"],
srcs: [
"artd.cc",
+ "path_utils.cc",
],
shared_libs: [
"libarttools",
@@ -35,6 +36,7 @@
],
static_libs: [
"artd-aidl-ndk",
+ "libc++fs",
],
}
@@ -64,8 +66,13 @@
art_cc_defaults {
name: "art_artd_tests_defaults",
defaults: ["artd_defaults"],
+ static_libs: [
+ "libcap",
+ "libgmock",
+ ],
srcs: [
"artd_test.cc",
+ "path_utils_test.cc",
],
}
diff --git a/artd/artd.cc b/artd/artd.cc
index 98693cb..e471954 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -19,8 +19,11 @@
#include <stdlib.h>
#include <unistd.h>
+#include <cstdint>
+#include <filesystem>
#include <memory>
#include <string>
+#include <system_error>
#include <utility>
#include <vector>
@@ -29,6 +32,7 @@
#include "android-base/logging.h"
#include "android-base/properties.h"
#include "android-base/result.h"
+#include "android-base/stringprintf.h"
#include "android-base/strings.h"
#include "android/binder_auto_utils.h"
#include "android/binder_manager.h"
@@ -36,6 +40,7 @@
#include "base/array_ref.h"
#include "base/file_utils.h"
#include "oat_file_assistant.h"
+#include "path_utils.h"
#include "runtime.h"
#include "tools/tools.h"
@@ -50,6 +55,7 @@
using ::android::base::GetBoolProperty;
using ::android::base::Result;
using ::android::base::Split;
+using ::android::base::StringPrintf;
using ::ndk::ScopedAStatus;
constexpr const char* kServiceName = "artd";
@@ -92,6 +98,28 @@
return !GetBoolProperty("odsign.verification.success", /*default_value=*/false);
}
+// Deletes a file. Returns the size of the deleted file, or 0 if the deleted file is empty or an
+// error occurs.
+int64_t GetSizeAndDeleteFile(const std::string& path) {
+ std::error_code ec;
+ int64_t size = std::filesystem::file_size(path, ec);
+ if (ec) {
+ // It is okay if the file does not exist. We don't have to log it.
+ if (ec.value() != ENOENT) {
+ LOG(ERROR) << StringPrintf(
+ "Failed to get the file size of '%s': %s", path.c_str(), ec.message().c_str());
+ }
+ return 0;
+ }
+
+ if (!std::filesystem::remove(path, ec)) {
+ LOG(ERROR) << StringPrintf("Failed to remove '%s': %s", path.c_str(), ec.message().c_str());
+ return 0;
+ }
+
+ return size;
+}
+
} // namespace
ScopedAStatus Artd::isAlive(bool* _aidl_return) {
@@ -100,9 +128,18 @@
}
ScopedAStatus Artd::deleteArtifacts(const ArtifactsPath& in_artifactsPath, int64_t* _aidl_return) {
- (void)in_artifactsPath;
- (void)_aidl_return;
- return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ Result<std::string> oat_path = BuildOatPath(in_artifactsPath);
+ if (!oat_path.ok()) {
+ return ScopedAStatus::fromExceptionCodeWithMessage(EX_ILLEGAL_STATE,
+ oat_path.error().message().c_str());
+ }
+
+ *_aidl_return = 0;
+ *_aidl_return += GetSizeAndDeleteFile(*oat_path);
+ *_aidl_return += GetSizeAndDeleteFile(OatPathToVdexPath(*oat_path));
+ *_aidl_return += GetSizeAndDeleteFile(OatPathToArtPath(*oat_path));
+
+ return ScopedAStatus::ok();
}
ScopedAStatus Artd::getOptimizationStatus(const std::string& in_dexFile,
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index 14bccc2..f16a58e 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -16,26 +16,94 @@
#include "artd.h"
+#include <sys/capability.h>
+
+#include <filesystem>
+#include <functional>
#include <memory>
+#include "android-base/file.h"
+#include "android-base/logging.h"
+#include "android-base/scopeguard.h"
#include "android/binder_interface_utils.h"
#include "base/common_art_test.h"
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace art {
namespace artd {
namespace {
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::make_scope_guard;
+using ::android::base::ScopeGuard;
+using ::testing::_;
+using ::testing::ContainsRegex;
+using ::testing::HasSubstr;
+using ::testing::MockFunction;
+
+// A wrapper of `cap_t` that automatically calls `cap_free`.
+class ScopedCap {
+ public:
+ explicit ScopedCap(cap_t cap) : cap_(cap) { CHECK_NE(cap, nullptr); }
+
+ ScopedCap(ScopedCap&& other) : cap_(std::exchange(other.cap_, nullptr)) {}
+
+ ~ScopedCap() {
+ if (cap_ != nullptr) {
+ CHECK_EQ(cap_free(cap_), 0);
+ }
+ }
+
+ cap_t Get() const { return cap_; }
+
+ private:
+ cap_t cap_;
+};
+
+// Temporarily drops all root capabilities when the test is run as root. This is a noop otherwise.
+ScopeGuard<std::function<void()>> ScopedUnroot() {
+ ScopedCap old_cap(cap_get_proc());
+ ScopedCap new_cap(cap_dup(old_cap.Get()));
+ CHECK_EQ(cap_clear_flag(new_cap.Get(), CAP_EFFECTIVE), 0);
+ CHECK_EQ(cap_set_proc(new_cap.Get()), 0);
+ // `old_cap` is actually not shared with anyone else, but we have to wrap it with a `shared_ptr`
+ // because `std::function` requires captures to be copyable.
+ return make_scope_guard([old_cap = std::make_shared<ScopedCap>(std::move(old_cap))]() {
+ CHECK_EQ(cap_set_proc(old_cap->Get()), 0);
+ });
+}
+
+// Temporarily drops all permission on a file/directory.
+ScopeGuard<std::function<void()>> ScopedInaccessible(const std::string& path) {
+ std::filesystem::perms old_perms = std::filesystem::status(path).permissions();
+ std::filesystem::permissions(path, std::filesystem::perms::none);
+ return make_scope_guard([=]() { std::filesystem::permissions(path, old_perms); });
+}
+
+ScopeGuard<std::function<void()>> ScopedSetLogger(android::base::LogFunction&& logger) {
+ android::base::LogFunction old_logger = android::base::SetLogger(std::move(logger));
+ return make_scope_guard([old_logger = std::move(old_logger)]() mutable {
+ android::base::SetLogger(std::move(old_logger));
+ });
+}
+
class ArtdTest : public CommonArtTest {
protected:
void SetUp() override {
CommonArtTest::SetUp();
artd_ = ndk::SharedRefBase::make<Artd>();
+ scratch_dir_ = std::make_unique<ScratchDir>();
}
- void TearDown() override { CommonArtTest::TearDown(); }
+ void TearDown() override {
+ scratch_dir_.reset();
+ CommonArtTest::TearDown();
+ }
std::shared_ptr<Artd> artd_;
+ std::unique_ptr<ScratchDir> scratch_dir_;
+ MockFunction<android::base::LogFunction> mock_logger_;
};
TEST_F(ArtdTest, isAlive) {
@@ -44,6 +112,130 @@
EXPECT_TRUE(result);
}
+TEST_F(ArtdTest, deleteArtifacts) {
+ std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+ std::filesystem::create_directories(oat_dir);
+ android::base::WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes.
+ android::base::WriteStringToFile("ab", oat_dir + "/b.vdex"); // 2 bytes.
+ android::base::WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte.
+
+ int64_t result = -1;
+ EXPECT_TRUE(artd_
+ ->deleteArtifacts(
+ ArtifactsPath{
+ .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+ .isa = "arm64",
+ .isInDalvikCache = false,
+ },
+ &result)
+ .isOk());
+ EXPECT_EQ(result, 4 + 2 + 1);
+
+ EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
+ EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.vdex"));
+ EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
+}
+
+TEST_F(ArtdTest, deleteArtifactsMissingFile) {
+ // Missing VDEX file.
+ std::string oat_dir = dalvik_cache_ + "/arm64";
+ std::filesystem::create_directories(oat_dir);
+ android::base::WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex"); // 4 bytes.
+ android::base::WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art"); // 1 byte.
+
+ auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+ EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
+
+ int64_t result = -1;
+ EXPECT_TRUE(artd_
+ ->deleteArtifacts(
+ ArtifactsPath{
+ .dexPath = "/a/b.apk",
+ .isa = "arm64",
+ .isInDalvikCache = true,
+ },
+ &result)
+ .isOk());
+ EXPECT_EQ(result, 4 + 1);
+
+ EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.dex"));
+ EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.art"));
+}
+
+TEST_F(ArtdTest, deleteArtifactsNoFile) {
+ auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+ EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
+
+ int64_t result = -1;
+ EXPECT_TRUE(artd_
+ ->deleteArtifacts(
+ ArtifactsPath{
+ .dexPath = android_data_ + "/a/b.apk",
+ .isa = "arm64",
+ .isInDalvikCache = false,
+ },
+ &result)
+ .isOk());
+ EXPECT_EQ(result, 0);
+}
+
+TEST_F(ArtdTest, deleteArtifactsPermissionDenied) {
+ std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+ std::filesystem::create_directories(oat_dir);
+ android::base::WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes.
+ android::base::WriteStringToFile("ab", oat_dir + "/b.vdex"); // 2 bytes.
+ android::base::WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte.
+
+ auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+ EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(3);
+
+ auto scoped_inaccessible = ScopedInaccessible(oat_dir);
+ auto scoped_unroot = ScopedUnroot();
+
+ int64_t result = -1;
+ EXPECT_TRUE(artd_
+ ->deleteArtifacts(
+ ArtifactsPath{
+ .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+ .isa = "arm64",
+ .isInDalvikCache = false,
+ },
+ &result)
+ .isOk());
+ EXPECT_EQ(result, 0);
+}
+
+TEST_F(ArtdTest, deleteArtifactsFileIsDir) {
+ // VDEX file is a directory.
+ std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+ std::filesystem::create_directories(oat_dir);
+ std::filesystem::create_directories(oat_dir + "/b.vdex");
+ android::base::WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes.
+ android::base::WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte.
+
+ auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+ EXPECT_CALL(mock_logger_,
+ Call(_, _, _, _, _, ContainsRegex(R"re(Failed to get the file size.*b\.vdex)re")))
+ .Times(1);
+
+ int64_t result = -1;
+ EXPECT_TRUE(artd_
+ ->deleteArtifacts(
+ ArtifactsPath{
+ .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+ .isa = "arm64",
+ .isInDalvikCache = false,
+ },
+ &result)
+ .isOk());
+ EXPECT_EQ(result, 4 + 1);
+
+ // The directory is kept because getting the file size failed.
+ EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
+ EXPECT_TRUE(std::filesystem::exists(oat_dir + "/b.vdex"));
+ EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
+}
+
} // namespace
} // namespace artd
} // namespace art
diff --git a/artd/path_utils.cc b/artd/path_utils.cc
new file mode 100644
index 0000000..c4d9031
--- /dev/null
+++ b/artd/path_utils.cc
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 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 "path_utils.h"
+
+#include <filesystem>
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/errors.h"
+#include "android-base/result.h"
+#include "android-base/strings.h"
+#include "arch/instruction_set.h"
+#include "base/file_utils.h"
+#include "oat_file_assistant.h"
+
+namespace art {
+namespace artd {
+
+namespace {
+
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::EndsWith;
+using ::android::base::Error;
+using ::android::base::Result;
+
+Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) {
+ if (path_str.empty()) {
+ return Errorf("Path is empty");
+ }
+ std::filesystem::path path(path_str);
+ if (!path.is_absolute()) {
+ return Errorf("Path '{}' is not an absolute path", path_str);
+ }
+ if (path.lexically_normal() != path_str) {
+ return Errorf("Path '{}' is not in normal form", path_str);
+ }
+ return {};
+}
+
+Result<void> ValidateDexPath(const std::string& dex_path) {
+ OR_RETURN(ValidateAbsoluteNormalPath(dex_path));
+ if (!EndsWith(dex_path, ".apk") && !EndsWith(dex_path, ".jar")) {
+ return Errorf("Dex path '{}' has an invalid extension", dex_path);
+ }
+ return {};
+}
+
+} // namespace
+
+Result<std::string> BuildOatPath(const ArtifactsPath& artifacts_path) {
+ OR_RETURN(ValidateDexPath(artifacts_path.dexPath));
+
+ InstructionSet isa = GetInstructionSetFromString(artifacts_path.isa.c_str());
+ if (isa == InstructionSet::kNone) {
+ return Errorf("Instruction set '{}' is invalid", artifacts_path.isa.c_str());
+ }
+
+ std::string error_msg;
+ std::string path;
+ if (artifacts_path.isInDalvikCache) {
+ // Apps' OAT files are never in ART APEX data.
+ if (!OatFileAssistant::DexLocationToOatFilename(
+ artifacts_path.dexPath, isa, /*deny_art_apex_data_files=*/true, &path, &error_msg)) {
+ return Error() << error_msg;
+ }
+ return path;
+ } else {
+ if (!OatFileAssistant::DexLocationToOdexFilename(
+ artifacts_path.dexPath, isa, &path, &error_msg)) {
+ return Error() << error_msg;
+ }
+ return path;
+ }
+}
+
+std::string OatPathToVdexPath(const std::string& oat_path) {
+ return ReplaceFileExtension(oat_path, "vdex");
+}
+
+std::string OatPathToArtPath(const std::string& oat_path) {
+ return ReplaceFileExtension(oat_path, "art");
+}
+
+} // namespace artd
+} // namespace art
diff --git a/artd/path_utils.h b/artd/path_utils.h
new file mode 100644
index 0000000..970143a
--- /dev/null
+++ b/artd/path_utils.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 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_ARTD_PATH_UTILS_H_
+#define ART_ARTD_PATH_UTILS_H_
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/result.h"
+
+namespace art {
+namespace artd {
+
+// Returns the absolute path to the OAT file built from the `ArtifactsPath`.
+android::base::Result<std::string> BuildOatPath(
+ const aidl::com::android::server::art::ArtifactsPath& artifacts_path);
+
+// Returns the path to the VDEX file that corresponds to the OAT file.
+std::string OatPathToVdexPath(const std::string& oat_path);
+
+// Returns the path to the ART file that corresponds to the OAT file.
+std::string OatPathToArtPath(const std::string& oat_path);
+
+} // namespace artd
+} // namespace art
+
+#endif // ART_ARTD_PATH_UTILS_H_
diff --git a/artd/path_utils_test.cc b/artd/path_utils_test.cc
new file mode 100644
index 0000000..9ce40c5
--- /dev/null
+++ b/artd/path_utils_test.cc
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2022 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 "path_utils.h"
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/result-gmock.h"
+#include "base/common_art_test.h"
+#include "gtest/gtest.h"
+
+namespace art {
+namespace artd {
+namespace {
+
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::testing::HasError;
+using ::android::base::testing::HasValue;
+using ::android::base::testing::WithMessage;
+
+class PathUtilsTest : public CommonArtTest {};
+
+TEST_F(PathUtilsTest, BuildOatPath) {
+ EXPECT_THAT(
+ BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
+ HasValue("/a/oat/arm64/b.odex"));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathDalvikCache) {
+ EXPECT_THAT(
+ BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = true}),
+ HasValue(android_data_ + "/dalvik-cache/arm64/a@b.apk@classes.dex"));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathEmptyDexPath) {
+ EXPECT_THAT(BuildOatPath(ArtifactsPath{.dexPath = "", .isa = "arm64", .isInDalvikCache = false}),
+ HasError(WithMessage("Path is empty")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathRelativeDexPath) {
+ EXPECT_THAT(
+ BuildOatPath(ArtifactsPath{.dexPath = "a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
+ HasError(WithMessage("Path 'a/b.apk' is not an absolute path")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathNonNormalDexPath) {
+ EXPECT_THAT(BuildOatPath(ArtifactsPath{
+ .dexPath = "/a/c/../b.apk", .isa = "arm64", .isInDalvikCache = false}),
+ HasError(WithMessage("Path '/a/c/../b.apk' is not in normal form")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathInvalidDexExtension) {
+ EXPECT_THAT(BuildOatPath(ArtifactsPath{
+ .dexPath = "/a/b.invalid", .isa = "arm64", .isInDalvikCache = false}),
+ HasError(WithMessage("Dex path '/a/b.invalid' has an invalid extension")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathInvalidIsa) {
+ EXPECT_THAT(BuildOatPath(
+ ArtifactsPath{.dexPath = "/a/b.apk", .isa = "invalid", .isInDalvikCache = false}),
+ HasError(WithMessage("Instruction set 'invalid' is invalid")));
+}
+
+TEST_F(PathUtilsTest, OatPathToVdexPath) {
+ EXPECT_EQ(OatPathToVdexPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.vdex");
+}
+
+TEST_F(PathUtilsTest, OatPathToArtPath) {
+ EXPECT_EQ(OatPathToArtPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.art");
+}
+
+} // namespace
+} // namespace artd
+} // namespace art