diff options
Diffstat (limited to 'tools')
26 files changed, 1789 insertions, 69 deletions
diff --git a/tools/aapt/Android.bp b/tools/aapt/Android.bp index f36739730775..a594e5bf0ce1 100644 --- a/tools/aapt/Android.bp +++ b/tools/aapt/Android.bp @@ -71,8 +71,6 @@ cc_library_host_static { cflags: [ "-Wno-format-y2k", "-DSTATIC_ANDROIDFW_FOR_TOOLS", - // Allow implicit fallthroughs in AaptAssets.cpp until they are fixed. - "-Wno-error=implicit-fallthrough", ], srcs: [ diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp index a571aee61546..e0040e486a23 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -534,17 +534,18 @@ Maybe<int> ParseSdkVersion(const StringPiece& str) { } // Try parsing the code name. - std::pair<StringPiece, int> entry = GetDevelopmentSdkCodeNameAndVersion(); - if (entry.first == trimmed_str) { - return entry.second; + Maybe<int> entry = GetDevelopmentSdkCodeNameVersion(trimmed_str); + if (entry) { + return entry.value(); } // Try parsing codename from "[codename].[preview_sdk_fingerprint]" value. const StringPiece::const_iterator begin = std::begin(trimmed_str); const StringPiece::const_iterator end = std::end(trimmed_str); const StringPiece::const_iterator codename_end = std::find(begin, end, '.'); - if (codename_end != end && entry.first == trimmed_str.substr(begin, codename_end)) { - return entry.second; + entry = GetDevelopmentSdkCodeNameVersion(trimmed_str.substr(begin, codename_end)); + if (entry) { + return entry.value(); } return {}; } diff --git a/tools/aapt2/ResourceUtils_test.cpp b/tools/aapt2/ResourceUtils_test.cpp index 3b77135a09eb..c016cb44af00 100644 --- a/tools/aapt2/ResourceUtils_test.cpp +++ b/tools/aapt2/ResourceUtils_test.cpp @@ -214,14 +214,15 @@ TEST(ResourceUtilsTest, ItemsWithWhitespaceAreParsedCorrectly) { } TEST(ResourceUtilsTest, ParseSdkVersionWithCodename) { - const android::StringPiece codename = - GetDevelopmentSdkCodeNameAndVersion().first; - const int version = GetDevelopmentSdkCodeNameAndVersion().second; + EXPECT_THAT(ResourceUtils::ParseSdkVersion("Q"), Eq(Maybe<int>(10000))); + EXPECT_THAT( + ResourceUtils::ParseSdkVersion("Q.fingerprint"), + Eq(Maybe<int>(10000))); - EXPECT_THAT(ResourceUtils::ParseSdkVersion(codename), Eq(Maybe<int>(version))); + EXPECT_THAT(ResourceUtils::ParseSdkVersion("R"), Eq(Maybe<int>(10000))); EXPECT_THAT( - ResourceUtils::ParseSdkVersion(codename.to_string() + ".fingerprint"), - Eq(Maybe<int>(version))); + ResourceUtils::ParseSdkVersion("R.fingerprint"), + Eq(Maybe<int>(10000))); } TEST(ResourceUtilsTest, StringBuilderWhitespaceRemoval) { diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp index f4b0124abcda..b4b6ff1daaaa 100644 --- a/tools/aapt2/SdkConstants.cpp +++ b/tools/aapt2/SdkConstants.cpp @@ -18,15 +18,17 @@ #include <algorithm> #include <string> -#include <unordered_map> +#include <unordered_set> #include <vector> using android::StringPiece; namespace aapt { -static const char* sDevelopmentSdkCodeName = "Q"; static ApiVersion sDevelopmentSdkLevel = 10000; +static const auto sDevelopmentSdkCodeNames = std::unordered_set<StringPiece>({ + "Q", "R" +}); static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = { {0x021c, 1}, @@ -72,8 +74,9 @@ ApiVersion FindAttributeSdkLevel(const ResourceId& id) { return iter->second; } -std::pair<StringPiece, ApiVersion> GetDevelopmentSdkCodeNameAndVersion() { - return std::make_pair(StringPiece(sDevelopmentSdkCodeName), sDevelopmentSdkLevel); +Maybe<ApiVersion> GetDevelopmentSdkCodeNameVersion(const StringPiece& code_name) { + return (sDevelopmentSdkCodeNames.find(code_name) == sDevelopmentSdkCodeNames.end()) + ? Maybe<ApiVersion>() : sDevelopmentSdkLevel; } } // namespace aapt diff --git a/tools/aapt2/SdkConstants.h b/tools/aapt2/SdkConstants.h index 580daabbd577..a00d978565ad 100644 --- a/tools/aapt2/SdkConstants.h +++ b/tools/aapt2/SdkConstants.h @@ -58,7 +58,7 @@ enum : ApiVersion { }; ApiVersion FindAttributeSdkLevel(const ResourceId& id); -std::pair<android::StringPiece, ApiVersion> GetDevelopmentSdkCodeNameAndVersion(); +Maybe<ApiVersion> GetDevelopmentSdkCodeNameVersion(const android::StringPiece& code_name); } // namespace aapt diff --git a/tools/aapt2/cmd/Convert_test.cpp b/tools/aapt2/cmd/Convert_test.cpp index 3c0fe370c516..ddc146cd27f4 100644 --- a/tools/aapt2/cmd/Convert_test.cpp +++ b/tools/aapt2/cmd/Convert_test.cpp @@ -123,8 +123,7 @@ TEST_F(ConvertTest, DuplicateEntriesWrittenOnce) { void* cookie = nullptr; - ZipString prefix("res/theme/10"); - int32_t result = StartIteration(handle, &cookie, &prefix, nullptr); + int32_t result = StartIteration(handle, &cookie, "res/theme/10", ""); // If this is -5, that means we've found a duplicate entry and this test has failed EXPECT_THAT(result, Eq(0)); @@ -145,4 +144,4 @@ TEST_F(ConvertTest, DuplicateEntriesWrittenOnce) { EXPECT_THAT(count, Eq(1)); } -} // namespace aapt
\ No newline at end of file +} // namespace aapt diff --git a/tools/aapt2/io/ZipArchive.cpp b/tools/aapt2/io/ZipArchive.cpp index f6aaa1280a61..a692ba5d26c0 100644 --- a/tools/aapt2/io/ZipArchive.cpp +++ b/tools/aapt2/io/ZipArchive.cpp @@ -114,7 +114,7 @@ std::unique_ptr<ZipFileCollection> ZipFileCollection::Create( } void* cookie = nullptr; - result = StartIteration(collection->handle_, &cookie, nullptr, nullptr); + result = StartIteration(collection->handle_, &cookie); if (result != 0) { if (out_error) *out_error = ErrorCodeString(result); return {}; diff --git a/tools/hiddenapi/generate_hiddenapi_lists.py b/tools/hiddenapi/generate_hiddenapi_lists.py index c856cc36d6f6..e883c6bed755 100755 --- a/tools/hiddenapi/generate_hiddenapi_lists.py +++ b/tools/hiddenapi/generate_hiddenapi_lists.py @@ -29,6 +29,7 @@ FLAG_GREYLIST = "greylist" FLAG_BLACKLIST = "blacklist" FLAG_GREYLIST_MAX_O = "greylist-max-o" FLAG_GREYLIST_MAX_P = "greylist-max-p" +FLAG_GREYLIST_MAX_Q = "greylist-max-q" FLAG_CORE_PLATFORM_API = "core-platform-api" FLAG_PUBLIC_API = "public-api" FLAG_SYSTEM_API = "system-api" @@ -41,6 +42,7 @@ FLAGS_API_LIST = [ FLAG_BLACKLIST, FLAG_GREYLIST_MAX_O, FLAG_GREYLIST_MAX_P, + FLAG_GREYLIST_MAX_Q, ] ALL_FLAGS = FLAGS_API_LIST + [ FLAG_CORE_PLATFORM_API, diff --git a/tools/lock_agent/Android.bp b/tools/lock_agent/Android.bp new file mode 100644 index 000000000000..c54e5b57ccc5 --- /dev/null +++ b/tools/lock_agent/Android.bp @@ -0,0 +1,61 @@ +cc_library { + name: "liblockagent", + host_supported: false, + srcs: ["agent.cpp"], + static_libs: [ + "libbase_ndk", + "slicer_ndk_no_rtti", + ], + shared_libs: [ + "libz", // for slicer (using adler32). + "liblog", + ], + sdk_version: "current", + stl: "c++_static", + include_dirs: [ + // NDK headers aren't available in platform NDK builds. + "libnativehelper/include_jni", + ], + header_libs: [ + "libopenjdkjvmti_headers", + ], + compile_multilib: "both", +} + +cc_binary_host { + name: "lockagenttest", + srcs: ["agent.cpp"], + static_libs: [ + "libbase", + "libz", + "slicer", + ], + include_dirs: [ + // NDK headers aren't available in platform NDK builds. + "libnativehelper/include_jni", + ], + header_libs: [ + "libopenjdkjvmti_headers", + ], +} + +java_library { + name: "lockagent", + srcs: ["java/**/*.java"], + dex_preopt: { + enabled: false, + }, + optimize: { + enabled: false, + }, + installable: true, +} + +sh_binary { + name: "start_with_lockagent", + src: "start_with_lockagent.sh", + required: [ + "liblockagent", + "lockagent", + ], +} diff --git a/tools/lock_agent/agent.cpp b/tools/lock_agent/agent.cpp new file mode 100644 index 000000000000..59bfa2bf849b --- /dev/null +++ b/tools/lock_agent/agent.cpp @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2019 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 <cstring> +#include <iostream> +#include <memory> +#include <sstream> + +#include <jni.h> + +#include <jvmti.h> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/macros.h> +#include <android-base/unique_fd.h> + +#include <fcntl.h> +#include <sys/stat.h> + +// We need dladdr. +#if !defined(__APPLE__) && !defined(_WIN32) +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#define DEFINED_GNU_SOURCE +#endif +#include <dlfcn.h> +#ifdef DEFINED_GNU_SOURCE +#undef _GNU_SOURCE +#undef DEFINED_GNU_SOURCE +#endif +#endif + +// Slicer's headers have code that triggers these warnings. b/65298177 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-parameter" +#pragma clang diagnostic ignored "-Wsign-compare" + +#include <slicer/dex_ir.h> +#include <slicer/code_ir.h> +#include <slicer/dex_bytecode.h> +#include <slicer/dex_ir_builder.h> +#include <slicer/writer.h> +#include <slicer/reader.h> + +#pragma clang diagnostic pop + +namespace { + +JavaVM* gJavaVM = nullptr; + +// Converts a class name to a type descriptor +// (ex. "java.lang.String" to "Ljava/lang/String;") +std::string classNameToDescriptor(const char* className) { + std::stringstream ss; + ss << "L"; + for (auto p = className; *p != '\0'; ++p) { + ss << (*p == '.' ? '/' : *p); + } + ss << ";"; + return ss.str(); +} + +using namespace dex; +using namespace lir; + +bool transform(std::shared_ptr<ir::DexFile> dexIr) { + bool modified = false; + + std::unique_ptr<ir::Builder> builder; + + for (auto& method : dexIr->encoded_methods) { + // Do not look into abstract/bridge/native/synthetic methods. + if ((method->access_flags & (kAccAbstract | kAccBridge | kAccNative | kAccSynthetic)) + != 0) { + continue; + } + + struct HookVisitor: public Visitor { + HookVisitor(std::unique_ptr<ir::Builder>* b, std::shared_ptr<ir::DexFile> d_ir, + CodeIr* c_ir) : + b(b), dIr(d_ir), cIr(c_ir) { + } + + bool Visit(Bytecode* bytecode) override { + if (bytecode->opcode == OP_MONITOR_ENTER) { + prepare(); + addCall(bytecode, OP_INVOKE_STATIC_RANGE, hookType, "preLock", voidType, + objectType, reinterpret_cast<VReg*>(bytecode->operands[0])->reg); + myModified = true; + return true; + } + if (bytecode->opcode == OP_MONITOR_EXIT) { + prepare(); + addCall(bytecode, OP_INVOKE_STATIC_RANGE, hookType, "postLock", voidType, + objectType, reinterpret_cast<VReg*>(bytecode->operands[0])->reg); + myModified = true; + return true; + } + return false; + } + + void prepare() { + if (*b == nullptr) { + *b = std::unique_ptr<ir::Builder>(new ir::Builder(dIr)); + } + if (voidType == nullptr) { + voidType = (*b)->GetType("V"); + hookType = (*b)->GetType("Lcom/android/lock_checker/LockHook;"); + objectType = (*b)->GetType("Ljava/lang/Object;"); + } + } + + void addInst(lir::Instruction* instructionAfter, Opcode opcode, + const std::list<Operand*>& operands) { + auto instruction = cIr->Alloc<Bytecode>(); + + instruction->opcode = opcode; + + for (auto it = operands.begin(); it != operands.end(); it++) { + instruction->operands.push_back(*it); + } + + cIr->instructions.InsertBefore(instructionAfter, instruction); + } + + void addCall(lir::Instruction* instructionAfter, Opcode opcode, ir::Type* type, + const char* methodName, ir::Type* returnType, + const std::vector<ir::Type*>& types, const std::list<int>& regs) { + auto proto = (*b)->GetProto(returnType, (*b)->GetTypeList(types)); + auto method = (*b)->GetMethodDecl((*b)->GetAsciiString(methodName), proto, type); + + VRegList* paramRegs = cIr->Alloc<VRegList>(); + for (auto it = regs.begin(); it != regs.end(); it++) { + paramRegs->registers.push_back(*it); + } + + addInst(instructionAfter, opcode, + { paramRegs, cIr->Alloc<Method>(method, method->orig_index) }); + } + + void addCall(lir::Instruction* instructionAfter, Opcode opcode, ir::Type* type, + const char* methodName, ir::Type* returnType, ir::Type* paramType, + u4 paramVReg) { + auto proto = (*b)->GetProto(returnType, (*b)->GetTypeList( { paramType })); + auto method = (*b)->GetMethodDecl((*b)->GetAsciiString(methodName), proto, type); + + VRegRange* args = cIr->Alloc<VRegRange>(paramVReg, 1); + + addInst(instructionAfter, opcode, + { args, cIr->Alloc<Method>(method, method->orig_index) }); + } + + std::unique_ptr<ir::Builder>* b; + std::shared_ptr<ir::DexFile> dIr; + CodeIr* cIr; + ir::Type* voidType = nullptr; + ir::Type* hookType = nullptr; + ir::Type* objectType = nullptr; + bool myModified = false; + }; + + CodeIr c(method.get(), dexIr); + HookVisitor visitor(&builder, dexIr, &c); + + for (auto it = c.instructions.begin(); it != c.instructions.end(); ++it) { + lir::Instruction* fi = *it; + fi->Accept(&visitor); + } + + if (visitor.myModified) { + modified = true; + c.Assemble(); + } + } + + return modified; +} + +std::pair<dex::u1*, size_t> maybeTransform(const char* name, size_t classDataLen, + const unsigned char* classData, dex::Writer::Allocator* allocator) { + // Isolate byte code of class class. This is needed as Android usually gives us more + // than the class we need. + dex::Reader reader(classData, classDataLen); + + dex::u4 index = reader.FindClassIndex(classNameToDescriptor(name).c_str()); + CHECK_NE(index, kNoIndex); + reader.CreateClassIr(index); + std::shared_ptr<ir::DexFile> ir = reader.GetIr(); + + if (!transform(ir)) { + return std::make_pair(nullptr, 0); + } + + size_t new_size; + dex::Writer writer(ir); + dex::u1* newClassData = writer.CreateImage(allocator, &new_size); + return std::make_pair(newClassData, new_size); +} + +void transformHook(jvmtiEnv* jvmtiEnv, JNIEnv* env ATTRIBUTE_UNUSED, + jclass classBeingRedefined ATTRIBUTE_UNUSED, jobject loader, const char* name, + jobject protectionDomain ATTRIBUTE_UNUSED, jint classDataLen, + const unsigned char* classData, jint* newClassDataLen, unsigned char** newClassData) { + // Even reading the classData array is expensive as the data is only generated when the + // memory is touched. Hence call JvmtiAgent#shouldTransform to check if we need to transform + // the class. + + // Skip bootclasspath classes. TODO: Make this configurable. + if (loader == nullptr) { + return; + } + + // Do not look into java.* classes. Should technically be filtered by above, but when that's + // configurable have this. + if (strncmp("java", name, 4) == 0) { + return; + } + + // Do not look into our Java classes. + if (strncmp("com/android/lock_checker", name, 24) == 0) { + return; + } + + class JvmtiAllocator: public dex::Writer::Allocator { + public: + explicit JvmtiAllocator(::jvmtiEnv* jvmti) : + jvmti_(jvmti) { + } + + void* Allocate(size_t size) override { + unsigned char* res = nullptr; + jvmti_->Allocate(size, &res); + return res; + } + + void Free(void* ptr) override { + jvmti_->Deallocate(reinterpret_cast<unsigned char*>(ptr)); + } + + private: + ::jvmtiEnv* jvmti_; + }; + JvmtiAllocator allocator(jvmtiEnv); + std::pair<dex::u1*, size_t> result = maybeTransform(name, classDataLen, classData, + &allocator); + + if (result.second > 0) { + *newClassData = result.first; + *newClassDataLen = static_cast<jint>(result.second); + } +} + +void dataDumpRequestHook(jvmtiEnv* jvmtiEnv ATTRIBUTE_UNUSED) { + if (gJavaVM == nullptr) { + LOG(ERROR) << "No JavaVM for dump"; + return; + } + JNIEnv* env; + if (gJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + LOG(ERROR) << "Could not get env for dump"; + return; + } + jclass lockHookClass = env->FindClass("com/android/lock_checker/LockHook"); + if (lockHookClass == nullptr) { + env->ExceptionClear(); + LOG(ERROR) << "Could not find LockHook class"; + return; + } + jmethodID dumpId = env->GetStaticMethodID(lockHookClass, "dump", "()V"); + if (dumpId == nullptr) { + env->ExceptionClear(); + LOG(ERROR) << "Could not find LockHook.dump"; + return; + } + env->CallStaticVoidMethod(lockHookClass, dumpId); + env->ExceptionClear(); +} + +// A function for dladdr to search. +extern "C" __attribute__ ((visibility ("default"))) void lock_agent_tag_fn() { +} + +bool fileExists(const std::string& path) { + struct stat statBuf; + int rc = stat(path.c_str(), &statBuf); + return rc == 0; +} + +std::string findLockAgentJar() { + // Check whether the jar is located next to the agent's so. +#ifndef __APPLE__ + { + Dl_info info; + if (dladdr(reinterpret_cast<const void*>(&lock_agent_tag_fn), /* out */ &info) != 0) { + std::string lockAgentSoPath = info.dli_fname; + std::string dir = android::base::Dirname(lockAgentSoPath); + std::string lockAgentJarPath = dir + "/" + "lockagent.jar"; + if (fileExists(lockAgentJarPath)) { + return lockAgentJarPath; + } + } else { + LOG(ERROR) << "dladdr failed"; + } + } +#endif + + std::string sysFrameworkPath = "/system/framework/lockagent.jar"; + if (fileExists(sysFrameworkPath)) { + return sysFrameworkPath; + } + + std::string relPath = "lockagent.jar"; + if (fileExists(relPath)) { + return relPath; + } + + return ""; +} + +void prepareHook(jvmtiEnv* env) { + // Inject the agent Java code. + { + std::string path = findLockAgentJar(); + if (path.empty()) { + LOG(FATAL) << "Could not find lockagent.jar"; + } + LOG(INFO) << "Will load Java parts from " << path; + jvmtiError res = env->AddToBootstrapClassLoaderSearch(path.c_str()); + if (res != JVMTI_ERROR_NONE) { + LOG(FATAL) << "Could not add lockagent from " << path << " to boot classpath: " << res; + } + } + + jvmtiCapabilities caps; + memset(&caps, 0, sizeof(caps)); + caps.can_retransform_classes = 1; + + if (env->AddCapabilities(&caps) != JVMTI_ERROR_NONE) { + LOG(FATAL) << "Could not add caps"; + } + + jvmtiEventCallbacks cb; + memset(&cb, 0, sizeof(cb)); + cb.ClassFileLoadHook = transformHook; + cb.DataDumpRequest = dataDumpRequestHook; + + if (env->SetEventCallbacks(&cb, sizeof(cb)) != JVMTI_ERROR_NONE) { + LOG(FATAL) << "Could not set cb"; + } + + if (env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr) + != JVMTI_ERROR_NONE) { + LOG(FATAL) << "Could not enable events"; + } + if (env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_DATA_DUMP_REQUEST, nullptr) + != JVMTI_ERROR_NONE) { + LOG(FATAL) << "Could not enable events"; + } +} + +jint attach(JavaVM* vm, char* options ATTRIBUTE_UNUSED, void* reserved ATTRIBUTE_UNUSED) { + gJavaVM = vm; + + jvmtiEnv* env; + jint jvmError = vm->GetEnv(reinterpret_cast<void**>(&env), JVMTI_VERSION_1_2); + if (jvmError != JNI_OK) { + return jvmError; + } + + prepareHook(env); + + return JVMTI_ERROR_NONE; +} + +extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved) { + return attach(vm, options, reserved); +} + +extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { + return attach(vm, options, reserved); +} + +int locktest_main(int argc, char *argv[]) { + if (argc != 3) { + LOG(FATAL) << "Need two arguments: dex-file class-name"; + } + struct stat statBuf; + int rc = stat(argv[1], &statBuf); + if (rc != 0) { + PLOG(FATAL) << "Could not get file size for " << argv[1]; + } + std::unique_ptr<char[]> data(new char[statBuf.st_size]); + { + android::base::unique_fd fd(open(argv[1], O_RDONLY)); + if (fd.get() == -1) { + PLOG(FATAL) << "Could not open file " << argv[1]; + } + if (!android::base::ReadFully(fd.get(), data.get(), statBuf.st_size)) { + PLOG(FATAL) << "Could not read file " << argv[1]; + } + } + + class NewDeleteAllocator: public dex::Writer::Allocator { + public: + explicit NewDeleteAllocator() { + } + + void* Allocate(size_t size) override { + return new char[size]; + } + + void Free(void* ptr) override { + delete[] reinterpret_cast<char*>(ptr); + } + }; + NewDeleteAllocator allocator; + + std::pair<dex::u1*, size_t> result = maybeTransform(argv[2], statBuf.st_size, + reinterpret_cast<unsigned char*>(data.get()), &allocator); + + if (result.second == 0) { + LOG(INFO) << "No transformation"; + return 0; + } + + std::string newName(argv[1]); + newName.append(".new"); + + { + android::base::unique_fd fd( + open(newName.c_str(), O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR)); + if (fd.get() == -1) { + PLOG(FATAL) << "Could not open file " << newName; + } + if (!android::base::WriteFully(fd.get(), result.first, result.second)) { + PLOG(FATAL) << "Could not write file " << newName; + } + } + LOG(INFO) << "Transformed file written to " << newName; + + return 0; +} + +} // namespace + +int main(int argc, char *argv[]) { + return locktest_main(argc, argv); +} diff --git a/tools/lock_agent/java/com/android/lock_checker/LockHook.java b/tools/lock_agent/java/com/android/lock_checker/LockHook.java new file mode 100644 index 000000000000..95b318101316 --- /dev/null +++ b/tools/lock_agent/java/com/android/lock_checker/LockHook.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.lock_checker; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.util.Log; +import android.util.LogWriter; + +import com.android.internal.os.SomeArgs; +import com.android.internal.util.StatLogger; + +import dalvik.system.AnnotatedStackTraceElement; + +import libcore.util.HexEncoding; + +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Entry class for lock inversion infrastructure. The agent will inject calls to preLock + * and postLock, and the hook will call the checker, and store violations. + */ +public class LockHook { + private static final String TAG = "LockHook"; + + private static final Charset sFilenameCharset = Charset.forName("UTF-8"); + + private static final HandlerThread sHandlerThread; + private static final WtfHandler sHandler; + + private static final AtomicInteger sTotalObtainCount = new AtomicInteger(); + private static final AtomicInteger sTotalReleaseCount = new AtomicInteger(); + private static final AtomicInteger sDeepestNest = new AtomicInteger(); + + /** + * Whether to do the lock check on this thread. + */ + private static final ThreadLocal<Boolean> sDoCheck = ThreadLocal.withInitial(() -> true); + + interface Stats { + int ON_THREAD = 0; + } + + static final StatLogger sStats = new StatLogger(new String[] { "on-thread", }); + + private static final ConcurrentLinkedQueue<Object> sViolations = new ConcurrentLinkedQueue<>(); + private static final int MAX_VIOLATIONS = 50; + + private static final LockChecker[] sCheckers; + + static { + sHandlerThread = new HandlerThread("LockHook:wtf", Process.THREAD_PRIORITY_BACKGROUND); + sHandlerThread.start(); + sHandler = new WtfHandler(sHandlerThread.getLooper()); + + sCheckers = new LockChecker[] { new OnThreadLockChecker() }; + } + + static <T> boolean shouldDumpStacktrace(StacktraceHasher hasher, Map<String, T> dumpedSet, + T val, AnnotatedStackTraceElement[] st, int from, int to) { + final String stacktraceHash = hasher.stacktraceHash(st, from, to); + if (dumpedSet.containsKey(stacktraceHash)) { + return false; + } + dumpedSet.put(stacktraceHash, val); + return true; + } + + static void updateDeepestNest(int nest) { + for (;;) { + final int knownDeepest = sDeepestNest.get(); + if (knownDeepest >= nest) { + return; + } + if (sDeepestNest.compareAndSet(knownDeepest, nest)) { + return; + } + } + } + + static void wtf(String message) { + sHandler.wtf(message); + } + + static void doCheckOnThisThread(boolean check) { + sDoCheck.set(check); + } + + /** + * This method is called when a lock is about to be held. (Except if it's a + * synchronized, the lock is already held.) + */ + public static void preLock(Object lock) { + if (Thread.currentThread() != sHandlerThread && sDoCheck.get()) { + sDoCheck.set(false); + try { + sTotalObtainCount.incrementAndGet(); + for (LockChecker checker : sCheckers) { + checker.pre(lock); + } + } finally { + sDoCheck.set(true); + } + } + } + + /** + * This method is called when a lock is about to be released. + */ + public static void postLock(Object lock) { + if (Thread.currentThread() != sHandlerThread && sDoCheck.get()) { + sDoCheck.set(false); + try { + sTotalReleaseCount.incrementAndGet(); + for (LockChecker checker : sCheckers) { + checker.post(lock); + } + } finally { + sDoCheck.set(true); + } + } + } + + private static class WtfHandler extends Handler { + private static final int MSG_WTF = 1; + + WtfHandler(Looper looper) { + super(looper); + } + + public void wtf(String msg) { + sDoCheck.set(false); + SomeArgs args = SomeArgs.obtain(); + args.arg1 = msg; + obtainMessage(MSG_WTF, args).sendToTarget(); + sDoCheck.set(true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_WTF: + SomeArgs args = (SomeArgs) msg.obj; + Log.wtf(TAG, (String) args.arg1); + args.recycle(); + break; + } + } + } + + /** + * Generates a hash for a given stacktrace of a {@link Throwable}. + */ + static class StacktraceHasher { + private byte[] mLineNumberBuffer = new byte[4]; + private final MessageDigest mHash; + + StacktraceHasher() { + try { + mHash = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public String stacktraceHash(Throwable t) { + mHash.reset(); + for (StackTraceElement e : t.getStackTrace()) { + hashStackTraceElement(e); + } + return HexEncoding.encodeToString(mHash.digest()); + } + + public String stacktraceHash(AnnotatedStackTraceElement[] annotatedStack, int from, + int to) { + mHash.reset(); + for (int i = from; i <= to; i++) { + hashStackTraceElement(annotatedStack[i].getStackTraceElement()); + } + return HexEncoding.encodeToString(mHash.digest()); + } + + private void hashStackTraceElement(StackTraceElement e) { + if (e.getFileName() != null) { + mHash.update(sFilenameCharset.encode(e.getFileName()).array()); + } else { + if (e.getClassName() != null) { + mHash.update(sFilenameCharset.encode(e.getClassName()).array()); + } + if (e.getMethodName() != null) { + mHash.update(sFilenameCharset.encode(e.getMethodName()).array()); + } + } + + final int line = e.getLineNumber(); + mLineNumberBuffer[0] = (byte) ((line >> 24) & 0xff); + mLineNumberBuffer[1] = (byte) ((line >> 16) & 0xff); + mLineNumberBuffer[2] = (byte) ((line >> 8) & 0xff); + mLineNumberBuffer[3] = (byte) ((line >> 0) & 0xff); + mHash.update(mLineNumberBuffer); + } + } + + static void addViolation(Object o) { + sViolations.offer(o); + while (sViolations.size() > MAX_VIOLATIONS) { + sViolations.poll(); + } + } + + /** + * Dump stats to the given PrintWriter. + */ + public static void dump(PrintWriter pw, String indent) { + final int oc = LockHook.sTotalObtainCount.get(); + final int rc = LockHook.sTotalReleaseCount.get(); + final int dn = LockHook.sDeepestNest.get(); + pw.print("Lock stats: oc="); + pw.print(oc); + pw.print(" rc="); + pw.print(rc); + pw.print(" dn="); + pw.print(dn); + pw.println(); + + for (LockChecker checker : sCheckers) { + pw.print(indent); + pw.print(" "); + checker.dump(pw); + pw.println(); + } + + sStats.dump(pw, indent); + + pw.print(indent); + pw.println("Violations:"); + for (Object v : sViolations) { + pw.print(indent); // This won't really indent a multiline string, + // though. + pw.println(v); + } + } + + /** + * Dump stats to logcat. + */ + public static void dump() { + // Dump to logcat. + PrintWriter out = new PrintWriter(new LogWriter(Log.WARN, TAG), true); + dump(out, ""); + out.close(); + } + + interface LockChecker { + void pre(Object lock); + + void post(Object lock); + + int getNumDetected(); + + int getNumDetectedUnique(); + + String getCheckerName(); + + void dump(PrintWriter pw); + } +} diff --git a/tools/lock_agent/java/com/android/lock_checker/OnThreadLockChecker.java b/tools/lock_agent/java/com/android/lock_checker/OnThreadLockChecker.java new file mode 100644 index 000000000000..0f3a28598741 --- /dev/null +++ b/tools/lock_agent/java/com/android/lock_checker/OnThreadLockChecker.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.lock_checker; + +import android.util.Log; + +import dalvik.system.AnnotatedStackTraceElement; +import dalvik.system.VMStack; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + + +class OnThreadLockChecker implements LockHook.LockChecker { + private static final String TAG = "LockCheckOnThread"; + + private static final boolean SKIP_RECURSIVE = true; + + private final Thread mChecker; + + private final AtomicInteger mNumDetected = new AtomicInteger(); + + private final AtomicInteger mNumDetectedUnique = new AtomicInteger(); + + // Queue for possible violations, to handle them on the sChecker thread. + private final LinkedBlockingQueue<Violation> mQueue = new LinkedBlockingQueue<>(); + + // The stack of locks held on the current thread. + private final ThreadLocal<List<Object>> mHeldLocks = ThreadLocal + .withInitial(() -> new ArrayList<>(10)); + + // A cached stacktrace hasher for each thread. The hasher caches internal objects and is not + // thread-safe. + private final ThreadLocal<LockHook.StacktraceHasher> mStacktraceHasher = ThreadLocal + .withInitial(() -> new LockHook.StacktraceHasher()); + + // A map of stacktrace hashes we have seen. + private final ConcurrentMap<String, Boolean> mDumpedStacktraceHashes = + new ConcurrentHashMap<>(); + + OnThreadLockChecker() { + mChecker = new Thread(() -> checker()); + mChecker.setName(TAG); + mChecker.setPriority(Thread.MIN_PRIORITY); + mChecker.start(); + } + + private static class LockPair { + // Consider WeakReference. It will require also caching the String + // description for later reporting, though. + Object mFirst; + Object mSecond; + + private int mCachedHashCode; + + LockPair(Object first, Object second) { + mFirst = first; + mSecond = second; + computeHashCode(); + } + + public void set(Object newFirst, Object newSecond) { + mFirst = newFirst; + mSecond = newSecond; + computeHashCode(); + } + + private void computeHashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mFirst == null) ? 0 : System.identityHashCode(mFirst)); + result = prime * result + ((mSecond == null) ? 0 : System.identityHashCode(mSecond)); + mCachedHashCode = result; + } + + @Override + public int hashCode() { + return mCachedHashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + LockPair other = (LockPair) obj; + return mFirst == other.mFirst && mSecond == other.mSecond; + } + } + + private static class OrderData { + final int mTid; + final String mThreadName; + final AnnotatedStackTraceElement[] mStack; + + OrderData(int tid, String threadName, AnnotatedStackTraceElement[] stack) { + this.mTid = tid; + this.mThreadName = threadName; + this.mStack = stack; + } + } + + private static ConcurrentMap<LockPair, OrderData> sLockOrderMap = new ConcurrentHashMap<>(); + + @Override + public void pre(Object lock) { + handlePre(Thread.currentThread(), lock); + } + + @Override + public void post(Object lock) { + handlePost(Thread.currentThread(), lock); + } + + private void handlePre(Thread self, Object lock) { + List<Object> heldLocks = mHeldLocks.get(); + + LockHook.updateDeepestNest(heldLocks.size() + 1); + + heldLocks.add(lock); + if (heldLocks.size() == 1) { + return; + } + + // Data about this location. Cached and lazily initialized. + AnnotatedStackTraceElement[] annotatedStack = null; + OrderData orderData = null; + + // Reused tmp pair; + LockPair tmp = new LockPair(lock, lock); + + int size = heldLocks.size() - 1; + for (int i = 0; i < size; i++) { + Object alreadyHeld = heldLocks.get(i); + if (SKIP_RECURSIVE && lock == alreadyHeld) { + return; + } + + // Check if we've already seen alreadyHeld -> lock. + tmp.set(alreadyHeld, lock); + if (sLockOrderMap.containsKey(tmp)) { + continue; // Already seen. + } + + // Note: could insert the OrderData now. This would mean we only + // report one instance for each order violation, but it avoids + // the expensive hashing in handleViolation for duplicate stacks. + + // Locking alreadyHeld -> lock, check whether the inverse exists. + tmp.set(lock, alreadyHeld); + + // We technically need a critical section here. Add synchronized and + // skip + // instrumenting this class. For now, a concurrent hash map is good + // enough. + + OrderData oppositeData = sLockOrderMap.getOrDefault(tmp, null); + if (oppositeData != null) { + if (annotatedStack == null) { + annotatedStack = VMStack.getAnnotatedThreadStackTrace(self); + } + postViolation(self, alreadyHeld, lock, annotatedStack, oppositeData); + continue; + } + + // Enter our occurrence. + if (annotatedStack == null) { + annotatedStack = VMStack.getAnnotatedThreadStackTrace(self); + } + if (orderData == null) { + orderData = new OrderData((int) self.getId(), self.getName(), annotatedStack); + } + sLockOrderMap.putIfAbsent(new LockPair(alreadyHeld, lock), orderData); + + // Check again whether we might have raced with the opposite. + oppositeData = sLockOrderMap.getOrDefault(tmp, null); + if (oppositeData != null) { + postViolation(self, alreadyHeld, lock, annotatedStack, oppositeData); + } + } + } + + private void handlePost(Thread self, Object lock) { + List<Object> heldLocks = mHeldLocks.get(); + if (heldLocks.isEmpty()) { + Log.wtf("LockCheckMine", "Empty thread list on post()"); + return; + } + int index = heldLocks.size() - 1; + if (heldLocks.get(index) != lock) { + Log.wtf("LockCheckMine", "post(" + Violation.describeLock(lock) + ") vs [..., " + + Violation.describeLock(heldLocks.get(index)) + "]"); + return; + } + heldLocks.remove(index); + } + + private static class Violation { + int mSelfTid; + String mSelfName; + Object mAlreadyHeld; + Object mLock; + AnnotatedStackTraceElement[] mStack; + OrderData mOppositeData; + + Violation(Thread self, Object alreadyHeld, Object lock, + AnnotatedStackTraceElement[] stack, OrderData oppositeData) { + this.mSelfTid = (int) self.getId(); + this.mSelfName = self.getName(); + this.mAlreadyHeld = alreadyHeld; + this.mLock = lock; + this.mStack = stack; + this.mOppositeData = oppositeData; + } + + private static String getAnnotatedStackString(AnnotatedStackTraceElement[] stackTrace, + int skip, String extra, int prefixAfter, String prefix) { + StringBuilder sb = new StringBuilder(); + for (int i = skip; i < stackTrace.length; i++) { + AnnotatedStackTraceElement element = stackTrace[i]; + sb.append(" ").append(i >= prefixAfter ? prefix : "").append("at ") + .append(element.getStackTraceElement()).append('\n'); + if (i == skip && extra != null) { + sb.append(" ").append(extra).append('\n'); + } + if (element.getHeldLocks() != null) { + for (Object held : element.getHeldLocks()) { + sb.append(" ").append(i >= prefixAfter ? prefix : "") + .append(describeLocking(held, "locked")).append('\n'); + } + } + } + return sb.toString(); + } + + private static String describeLocking(Object lock, String action) { + return String.format("- %s %s", action, describeLock(lock)); + } + + private static int getTo(AnnotatedStackTraceElement[] stack, Object searchFor) { + // Extract the range of the annotated stack. + int to = stack.length - 1; + for (int i = 0; i < stack.length; i++) { + Object[] locks = stack[i].getHeldLocks(); + if (locks != null) { + for (Object heldLock : locks) { + if (heldLock == searchFor) { + to = i; + break; + } + } + } + } + return to; + } + + private static String describeLock(Object lock) { + return String.format("<0x%08x> (a %s)", System.identityHashCode(lock), + lock.getClass().getName()); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Lock inversion detected!\n"); + sb.append(" Locked "); + sb.append(describeLock(mLock)); + sb.append(" -> "); + sb.append(describeLock(mAlreadyHeld)); + sb.append(" on thread ").append(mOppositeData.mTid).append(" (") + .append(mOppositeData.mThreadName).append(")"); + sb.append(" at:\n"); + sb.append(getAnnotatedStackString(mOppositeData.mStack, 4, + describeLocking(mAlreadyHeld, "will lock"), getTo(mOppositeData.mStack, mLock) + + 1, " | ")); + sb.append(" Locking "); + sb.append(describeLock(mAlreadyHeld)); + sb.append(" -> "); + sb.append(describeLock(mLock)); + sb.append(" on thread ").append(mSelfTid).append(" (").append(mSelfName).append(")"); + sb.append(" at:\n"); + sb.append(getAnnotatedStackString(mStack, 4, describeLocking(mLock, "will lock"), + getTo(mStack, mAlreadyHeld) + 1, " | ")); + + return sb.toString(); + } + } + + private void postViolation(Thread self, Object alreadyHeld, Object lock, + AnnotatedStackTraceElement[] annotatedStack, OrderData oppositeData) { + mQueue.offer(new Violation(self, alreadyHeld, lock, annotatedStack, oppositeData)); + } + + private void handleViolation(Violation v) { + mNumDetected.incrementAndGet(); + // Extract the range of the annotated stack. + int to = Violation.getTo(v.mStack, v.mAlreadyHeld); + + if (LockHook.shouldDumpStacktrace(mStacktraceHasher.get(), mDumpedStacktraceHashes, + Boolean.TRUE, v.mStack, 0, to)) { + mNumDetectedUnique.incrementAndGet(); + LockHook.wtf(v.toString()); + LockHook.addViolation(v); + } + } + + private void checker() { + LockHook.doCheckOnThisThread(false); + + for (;;) { + try { + Violation v = mQueue.take(); + handleViolation(v); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + @Override + public int getNumDetected() { + return mNumDetected.get(); + } + + @Override + public int getNumDetectedUnique() { + return mNumDetectedUnique.get(); + } + + @Override + public String getCheckerName() { + return "Standard LockChecker"; + } + + @Override + public void dump(PrintWriter pw) { + pw.print(getCheckerName()); + pw.print(": d="); + pw.print(getNumDetected()); + pw.print(" du="); + pw.print(getNumDetectedUnique()); + } +} diff --git a/tools/lock_agent/start_with_lockagent.sh b/tools/lock_agent/start_with_lockagent.sh new file mode 100755 index 000000000000..953922230a11 --- /dev/null +++ b/tools/lock_agent/start_with_lockagent.sh @@ -0,0 +1,5 @@ +#!/system/bin/sh +APP=$1 +shift +$APP -Xplugin:libopenjdkjvmti.so -agentpath:liblockagent.so $@ + diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java index edb9a49f4106..828cce72dda9 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java @@ -13,6 +13,9 @@ */ package lockedregioncodeinjection; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + import java.io.BufferedInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -24,8 +27,6 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; public class Main { public static void main(String[] args) throws IOException { @@ -74,6 +75,7 @@ public class Main { while (srcEntries.hasMoreElements()) { ZipEntry entry = srcEntries.nextElement(); ZipEntry newEntry = new ZipEntry(entry.getName()); + newEntry.setTime(entry.getTime()); zos.putNextEntry(newEntry); BufferedInputStream bis = new BufferedInputStream(zipSrc.getInputStream(entry)); diff --git a/tools/preload-check/Android.bp b/tools/preload-check/Android.bp new file mode 100644 index 000000000000..2488341bfd97 --- /dev/null +++ b/tools/preload-check/Android.bp @@ -0,0 +1,22 @@ +// Copyright (C) 2019 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. + +java_test_host { + name: "PreloadCheck", + srcs: ["src/**/*.java"], + java_resources: [":preloaded-classes-blacklist"], + libs: ["tradefed"], + test_suites: ["general-tests"], + required: ["preload-check-device"], +} diff --git a/tools/preload-check/AndroidTest.xml b/tools/preload-check/AndroidTest.xml new file mode 100644 index 000000000000..a0645d5b1051 --- /dev/null +++ b/tools/preload-check/AndroidTest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 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. +--> +<configuration description="Config for PreloadCheck"> + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <option name="cleanup" value="true" /> + <option name="push" value="preload-check-device.jar->/data/local/tmp/preload-check-device.jar" /> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="class" value="com.android.preload.check.PreloadCheck" /> + </test> +</configuration> diff --git a/tools/preload-check/TEST_MAPPING b/tools/preload-check/TEST_MAPPING new file mode 100644 index 000000000000..d09805ec08b6 --- /dev/null +++ b/tools/preload-check/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "PreloadCheck" + } + ] +} diff --git a/tools/preload-check/device/Android.bp b/tools/preload-check/device/Android.bp new file mode 100644 index 000000000000..7782b0d378ae --- /dev/null +++ b/tools/preload-check/device/Android.bp @@ -0,0 +1,27 @@ +// Copyright (C) 2019 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. + +java_test_helper_library { + name: "preload-check-device", + host_supported: false, + device_supported: true, + compile_dex: true, + + sdk_version: "current", + srcs: ["src/**/*.java"], + test_suites: ["general-tests"], + dex_preopt: { + enabled: false, + }, +} diff --git a/tools/preload-check/device/src/com/android/preload/check/Initialized.java b/tools/preload-check/device/src/com/android/preload/check/Initialized.java new file mode 100644 index 000000000000..81c074c04d4c --- /dev/null +++ b/tools/preload-check/device/src/com/android/preload/check/Initialized.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.preload.check; + +/** + * Test that the given boot classpath class is initialized. + */ +public class Initialized { + public static void main(String[] args) throws Exception { + Util.assertInitialized(args[0], null); + System.out.println("OK"); + } +} diff --git a/tools/preload-check/device/src/com/android/preload/check/IntegrityCheck.java b/tools/preload-check/device/src/com/android/preload/check/IntegrityCheck.java new file mode 100644 index 000000000000..1c1e927c54c4 --- /dev/null +++ b/tools/preload-check/device/src/com/android/preload/check/IntegrityCheck.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.preload.check; + +/** + * Test that a helper class is first seen as uninitialized, then initialized after forced. + */ +public class IntegrityCheck { + public static void main(String[] args) throws Exception { + ClassLoader loader = IntegrityCheck.class.getClassLoader(); + + Util.assertNotInitialized("com.android.preload.check.IntegrityCheck$StatusHelper", loader); + + Class.forName("com.android.preload.check.IntegrityCheck$StatusHelper", + /* initialize */ true, loader); + + Util.assertInitialized("com.android.preload.check.IntegrityCheck$StatusHelper", loader); + + System.out.println("OK"); + } + + @SuppressWarnings("unused") + private static class StatusHelper { + private final static Object defer = new Object(); + } +} diff --git a/tools/preload-check/device/src/com/android/preload/check/NotInitialized.java b/tools/preload-check/device/src/com/android/preload/check/NotInitialized.java new file mode 100644 index 000000000000..c3d2c7737c7d --- /dev/null +++ b/tools/preload-check/device/src/com/android/preload/check/NotInitialized.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.preload.check; + +/** + * Test that the given boot classpath class is not initialized. + */ +public class NotInitialized { + public static void main(String[] args) throws Exception { + Util.assertNotInitialized(args[0], null); + System.out.println("OK"); + } +} diff --git a/tools/preload-check/device/src/com/android/preload/check/NotInitializedRegex.java b/tools/preload-check/device/src/com/android/preload/check/NotInitializedRegex.java new file mode 100644 index 000000000000..d942bad9b6a8 --- /dev/null +++ b/tools/preload-check/device/src/com/android/preload/check/NotInitializedRegex.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.preload.check; + +import dalvik.system.DexFile; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Test boot classpath classes that satisfy a given regular expression to be not initialized. + * Optionally check that at least one class was matched. + */ +public class NotInitializedRegex { + /** + * First arg (mandatory): regular exception. Second arg (optional): boolean to denote a + * required match. + */ + public static void main(String[] args) throws Exception { + Matcher m = Pattern.compile(args[0]).matcher(""); + boolean requiresMatch = args.length > 1 ? Boolean.parseBoolean(args[1]) : false; + + Collection<DexFile> dexFiles = Util.getBootDexFiles(); + int matched = 0, notMatched = 0; + for (DexFile dexFile : dexFiles) { + Enumeration<String> entries = dexFile.entries(); + while (entries.hasMoreElements()) { + String entry = entries.nextElement(); + m.reset(entry); + if (m.matches()) { + System.out.println(entry + ": match"); + matched++; + check(entry); + } else { + System.out.println(entry + ": no match"); + notMatched++; + } + } + } + System.out.println("Matched: " + matched + " Not-Matched: " + notMatched); + if (requiresMatch && matched == 0) { + throw new RuntimeException("Did not find match"); + } + System.out.println("OK"); + } + + private static void check(String name) { + Util.assertNotInitialized(name, null); + } +} diff --git a/tools/preload-check/device/src/com/android/preload/check/Util.java b/tools/preload-check/device/src/com/android/preload/check/Util.java new file mode 100644 index 000000000000..fccea0a0c107 --- /dev/null +++ b/tools/preload-check/device/src/com/android/preload/check/Util.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.preload.check; + +import dalvik.system.DexFile; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +public class Util { + private static Field statusField; + + static { + try { + Class<?> klass = Class.class; + statusField = klass.getDeclaredField("status"); + statusField.setAccessible(true); + } catch (Throwable t) { + throw new RuntimeException(t); + } + // Reset the framework's kill handler. + Thread.setDefaultUncaughtExceptionHandler(null); + } + + public static Collection<DexFile> getBootDexFiles() throws Exception { + Class<?> vmClassLoaderClass = Class.forName("java.lang.VMClassLoader"); + Method getResources = vmClassLoaderClass.getDeclaredMethod("getResources", String.class); + getResources.setAccessible(true); + LinkedList<DexFile> res = new LinkedList<>(); + for (int i = 1;; i++) { + try { + String name = "classes" + (i > 1 ? String.valueOf(i) : "") + ".dex"; + @SuppressWarnings("unchecked") + List<URL> urls = (List<URL>) getResources.invoke(null, name); + if (urls.isEmpty()) { + break; + } + for (URL url : urls) { + // Make temp copy, so we can use public API. Would be nice to use in-memory, but + // those are unstable. + String tmp = "/data/local/tmp/tmp.dex"; + try (BufferedInputStream in = new BufferedInputStream(url.openStream()); + BufferedOutputStream out = new BufferedOutputStream( + new FileOutputStream(tmp))) { + byte[] buf = new byte[4096]; + for (;;) { + int r = in.read(buf); + if (r == -1) { + break; + } + out.write(buf, 0, r); + } + } + try { + res.add(new DexFile(tmp)); + } catch (Exception dexError) { + dexError.printStackTrace(System.out); + } + new File(tmp).delete(); + } + } catch (Exception ignored) { + break; + } + } + return res; + } + + public static boolean isInitialized(Class<?> klass) throws Exception { + Object val = statusField.get(klass); + if (val == null || !(val instanceof Integer)) { + throw new IllegalStateException(String.valueOf(val)); + } + int intVal = (int)val; + intVal = (intVal >> (32-4)) & 0xf; + return intVal >= 14; + } + + public static void assertTrue(boolean val, String msg) { + if (!val) { + throw new RuntimeException(msg); + } + } + + public static void assertInitializedState(String className, boolean expected, + ClassLoader loader) { + boolean initialized; + try { + Class<?> klass = Class.forName(className, /* initialize */ false, loader); + initialized = isInitialized(klass); + } catch (Throwable t) { + throw new RuntimeException(t); + } + assertTrue(expected == initialized, className); + } + + public static void assertNotInitialized(String className, ClassLoader loader) { + assertInitializedState(className, false, loader); + } + + public static void assertInitialized(String className, ClassLoader loader) { + assertInitializedState(className, true, loader); + } +} diff --git a/tools/preload-check/src/com/android/preload/check/PreloadCheck.java b/tools/preload-check/src/com/android/preload/check/PreloadCheck.java new file mode 100644 index 000000000000..00fd414e3ee2 --- /dev/null +++ b/tools/preload-check/src/com/android/preload/check/PreloadCheck.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2019 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. + */ + +package com.android.preload.check; + +import static org.junit.Assert.assertTrue; + +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.IDeviceTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +@RunWith(DeviceJUnit4ClassRunner.class) +public class PreloadCheck implements IDeviceTest { + private ITestDevice mTestDevice; + + private static final String TEST_CLASSPATH = "/data/local/tmp/preload-check-device.jar"; + + @Override + public void setDevice(ITestDevice testDevice) { + mTestDevice = testDevice; + } + + @Override + public ITestDevice getDevice() { + return mTestDevice; + } + + /** + * Test that checks work as expected. + */ + @Test + public void testStatus() throws Exception { + run("com.android.preload.check.IntegrityCheck"); + } + + /** + * b/130206915. + */ + @Test + public void testAsyncTask() throws Exception { + run("com.android.preload.check.NotInitialized", "android.os.AsyncTask"); + } + + /** + * Just a check for something we expect to see initialized. + */ + @Test + public void testAnimator() throws Exception { + run("com.android.preload.check.Initialized", "android.animation.Animator"); + } + + /** + * Test the classes mentioned in the embedded preloaded-classes blacklist. + */ + @Test + public void testBlackList() throws Exception { + StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new InputStreamReader(getClass() + .getResourceAsStream("/preloaded-classes-blacklist")))) { + String s; + while ((s = br.readLine()) != null) { + s = s.trim(); + if (s.startsWith("#") || s.isEmpty()) { + continue; + } + try { + run("com.android.preload.check.NotInitialized", s); + } catch (Throwable t) { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(t.getMessage()); + } + } + } + if (sb.length() > 0) { + throw new RuntimeException(sb.toString()); + } + } + + /** + * Test the classes ending in NoPreloadHolder are not initialized. + */ + @Test + public void testNoPreloadHolder() throws Exception { + run("com.android.preload.check.NotInitializedRegex", ".*NoPreloadHolder$", "true"); + } + + private void run(String cmd, String... args) throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append("app_process ") + .append("-cp ").append(TEST_CLASSPATH) + .append(" /system/bin ") + .append(cmd); + for (String arg : args) { + sb.append(' ').append(escape(arg)); + } + String res = mTestDevice.executeShellCommand(sb.toString()); + assertTrue(sb.toString() + "\n===\n" + res, res.trim().endsWith("OK")); + } + + private static String escape(String input) { + if (input.indexOf('$') == -1) { + return input; + } + return input.replace("$", "\\$"); + } +} diff --git a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java index ef2914610f80..5a5703ed520c 100644 --- a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java +++ b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java @@ -20,12 +20,13 @@ import static javax.lang.model.element.ElementKind.PACKAGE; import static javax.tools.Diagnostic.Kind.ERROR; import static javax.tools.Diagnostic.Kind.WARNING; -import android.annotation.UnsupportedAppUsage; - import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.sun.tools.javac.code.Type; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -69,6 +70,7 @@ public class SignatureBuilder { public SignatureBuilderException(String message) { super(message); } + public void report(Element offendingElement) { mMessager.printMessage(ERROR, getMessage(), offendingElement); } @@ -153,7 +155,7 @@ public class SignatureBuilder { /** * Get the signature for an executable, either a method or a constructor. * - * @param name "<init>" for constructor, else the method name + * @param name "<init>" for constructor, else the method name * @param method The executable element in question. */ private String getExecutableSignature(CharSequence name, ExecutableElement method) @@ -191,8 +193,13 @@ public class SignatureBuilder { return sig.toString(); } - public String buildSignature(Element element) { - UnsupportedAppUsage uba = element.getAnnotation(UnsupportedAppUsage.class); + /** + * Creates the signature for an annotated element. + * + * @param annotationType type of annotation being processed. + * @param element element for which we want to create a signature. + */ + public String buildSignature(Class<? extends Annotation> annotationType, Element element) { try { String signature; switch (element.getKind()) { @@ -208,18 +215,35 @@ public class SignatureBuilder { default: return null; } - // if we have an expected signature on the annotation, warn if it doesn't match. - if (!Strings.isNullOrEmpty(uba.expectedSignature())) { - if (!signature.equals(uba.expectedSignature())) { - mMessager.printMessage( - WARNING, - String.format("Expected signature doesn't match generated signature.\n" - + " Expected: %s\n Generated: %s", - uba.expectedSignature(), signature), - element); + // Obtain annotation objects + Annotation annotation = element.getAnnotation(annotationType); + if (annotation == null) { + throw new IllegalStateException( + "Element doesn't have any UnsupportedAppUsage annotation"); + } + try { + Method expectedSignatureMethod = annotationType.getMethod("expectedSignature"); + // If we have an expected signature on the annotation, warn if it doesn't match. + String expectedSignature = expectedSignatureMethod.invoke(annotation).toString(); + if (!Strings.isNullOrEmpty(expectedSignature)) { + if (!signature.equals(expectedSignature)) { + mMessager.printMessage( + WARNING, + String.format( + "Expected signature doesn't match generated signature.\n" + + " Expected: %s\n Generated: %s", + expectedSignature, signature), + element); + } } + return signature; + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + "Annotation type does not have expectedSignature parameter", e); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Could not get expectedSignature parameter for annotation", e); } - return signature; } catch (SignatureBuilderException problem) { problem.report(element); return null; diff --git a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java index d368136c7081..5bb956a1fea2 100644 --- a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java +++ b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java @@ -18,9 +18,8 @@ package android.processor.unsupportedappusage; import static javax.tools.StandardLocation.CLASS_OUTPUT; -import android.annotation.UnsupportedAppUsage; - import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; import com.sun.tools.javac.model.JavacElements; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Pair; @@ -28,6 +27,7 @@ import com.sun.tools.javac.util.Position; import java.io.IOException; import java.io.PrintStream; +import java.lang.annotation.Annotation; import java.net.URLEncoder; import java.util.Map; import java.util.Set; @@ -47,14 +47,14 @@ import javax.lang.model.element.TypeElement; /** * Annotation processor for {@link UnsupportedAppUsage} annotations. * - * This processor currently outputs two things: - * 1. A greylist.txt containing dex signatures of all annotated elements. - * 2. A CSV file with a mapping of dex signatures to corresponding source positions. + * This processor currently outputs a CSV file with a mapping of dex signatures to corresponding + * source positions. * - * The first will be used at a later stage of the build to add access flags to the dex file. The - * second is used for automating updates to the annotations themselves. + * This is used for automating updates to the annotations themselves. */ -@SupportedAnnotationTypes({"android.annotation.UnsupportedAppUsage"}) +@SupportedAnnotationTypes({"android.annotation.UnsupportedAppUsage", + "dalvik.annotation.compat.UnsupportedAppUsage" +}) public class UnsupportedAppUsageProcessor extends AbstractProcessor { // Package name for writing output. Output will be written to the "class output" location within @@ -62,6 +62,13 @@ public class UnsupportedAppUsageProcessor extends AbstractProcessor { private static final String PACKAGE = "unsupportedappusage"; private static final String INDEX_CSV = "unsupportedappusage_index.csv"; + private static final ImmutableSet<Class<? extends Annotation>> SUPPORTED_ANNOTATIONS = + ImmutableSet.of(android.annotation.UnsupportedAppUsage.class, + dalvik.annotation.compat.UnsupportedAppUsage.class); + private static final ImmutableSet<String> SUPPORTED_ANNOTATION_NAMES = + SUPPORTED_ANNOTATIONS.stream().map(annotation -> annotation.getCanonicalName()).collect( + ImmutableSet.toImmutableSet()); + @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latest(); @@ -92,8 +99,7 @@ public class UnsupportedAppUsageProcessor extends AbstractProcessor { private AnnotationMirror getUnsupportedAppUsageAnnotationMirror(Element e) { for (AnnotationMirror m : e.getAnnotationMirrors()) { TypeElement type = (TypeElement) m.getAnnotationType().asElement(); - if (type.getQualifiedName().toString().equals( - UnsupportedAppUsage.class.getCanonicalName())) { + if (SUPPORTED_ANNOTATION_NAMES.contains(type.getQualifiedName().toString())) { return m; } } @@ -133,12 +139,12 @@ public class UnsupportedAppUsageProcessor extends AbstractProcessor { /** * Maps an annotated element to the source position of the @UnsupportedAppUsage annotation * attached to it. It returns CSV in the format: - * dex-signature,filename,start-line,start-col,end-line,end-col + * dex-signature,filename,start-line,start-col,end-line,end-col * * The positions refer to the annotation itself, *not* the annotated member. This can therefore * be used to read just the annotation from the file, and to perform in-place edits on it. * - * @param signature the dex signature for the element. + * @param signature the dex signature for the element. * @param annotatedElement The annotated element * @return A single line of CSV text */ @@ -164,28 +170,34 @@ public class UnsupportedAppUsageProcessor extends AbstractProcessor { */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { - Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith( - UnsupportedAppUsage.class); - if (annotated.size() == 0) { - return true; - } - // build signatures for each annotated member, and put them in a map of signature to member Map<String, Element> signatureMap = new TreeMap<>(); SignatureBuilder sb = new SignatureBuilder(processingEnv.getMessager()); - for (Element e : annotated) { - String sig = sb.buildSignature(e); - if (sig != null) { - signatureMap.put(sig, e); + for (Class<? extends Annotation> supportedAnnotation : SUPPORTED_ANNOTATIONS) { + Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith( + supportedAnnotation); + if (annotated.size() == 0) { + continue; + } + // Build signatures for each annotated member and put them in a map from signature to + // member. + for (Element e : annotated) { + String sig = sb.buildSignature(supportedAnnotation, e); + if (sig != null) { + signatureMap.put(sig, e); + } } } - try { - writeToFile(INDEX_CSV, - getCsvHeaders(), - signatureMap.entrySet() - .stream() - .map(e -> getAnnotationIndex(e.getKey() ,e.getValue()))); - } catch (IOException e) { - throw new RuntimeException("Failed to write output", e); + + if (!signatureMap.isEmpty()) { + try { + writeToFile(INDEX_CSV, + getCsvHeaders(), + signatureMap.entrySet() + .stream() + .map(e -> getAnnotationIndex(e.getKey(), e.getValue()))); + } catch (IOException e) { + throw new RuntimeException("Failed to write output", e); + } } return true; } |