diff options
Diffstat (limited to 'tools')
86 files changed, 8017 insertions, 1101 deletions
diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp index fecc7b3cbf37..d02fd83df6af 100644 --- a/tools/aapt/Command.cpp +++ b/tools/aapt/Command.cpp @@ -1028,7 +1028,6 @@ int doDump(Bundle* bundle) // These permissions are required by services implementing services // the system binds to (IME, Accessibility, PrintServices, etc.) bool hasBindDeviceAdminPermission = false; - bool hasBindInputMethodPermission = false; bool hasBindAccessibilityServicePermission = false; bool hasBindPrintServicePermission = false; bool hasBindNfcServicePermission = false; @@ -1757,7 +1756,6 @@ int doDump(Bundle* bundle) hasMetaHostPaymentCategory = false; hasMetaOffHostPaymentCategory = false; hasBindDeviceAdminPermission = false; - hasBindInputMethodPermission = false; hasBindAccessibilityServicePermission = false; hasBindPrintServicePermission = false; hasBindNfcServicePermission = false; @@ -1871,9 +1869,7 @@ int doDump(Bundle* bundle) String8 permission = AaptXml::getAttribute(tree, PERMISSION_ATTR, &error); if (error == "") { - if (permission == "android.permission.BIND_INPUT_METHOD") { - hasBindInputMethodPermission = true; - } else if (permission == + if (permission == "android.permission.BIND_ACCESSIBILITY_SERVICE") { hasBindAccessibilityServicePermission = true; } else if (permission == diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp index b9de11b0026b..47750fc11a6e 100644 --- a/tools/aapt/ResourceTable.cpp +++ b/tools/aapt/ResourceTable.cpp @@ -2970,14 +2970,6 @@ status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& } e->setNameIndex(keyStrings.add(e->getName(), true)); - // If this entry has no values for other configs, - // and is the default config, then it is special. Otherwise - // we want to add it with the config info. - ConfigDescription* valueConfig = NULL; - if (N != 1 || config == nullConfig) { - valueConfig = &config; - } - status_t err = e->prepareFlatten(&valueStrings, this, &configTypeName, &config); if (err != NO_ERROR) { diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index 7b94e718fd0e..34a1b112d880 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -15,7 +15,7 @@ $(aapt2_results): .KATI_IMPLICIT_OUTPUTS := $(aapt2_results)-nocache $(aapt2_results): $(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests -$(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests --gtest_output=xml:$@ > /dev/null 2>&1 -$(call declare-0p-target,$(aapt2_results)) +$(call declare-1p-target,$(aapt2_results)) aapt2_results := diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp index 23f6c88aad91..3787f3b96f08 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -51,8 +51,10 @@ std::optional<ResourceName> ToResourceName(const android::ResTable::resource_nam util::Utf16ToUtf8(StringPiece16(name_in.package, name_in.packageLen)); std::optional<ResourceNamedTypeRef> type; + std::string converted; if (name_in.type) { - type = ParseResourceNamedType(util::Utf16ToUtf8(StringPiece16(name_in.type, name_in.typeLen))); + converted = util::Utf16ToUtf8(StringPiece16(name_in.type, name_in.typeLen)); + type = ParseResourceNamedType(converted); } else if (name_in.type8) { type = ParseResourceNamedType(StringPiece(name_in.type8, name_in.typeLen)); } else { @@ -85,9 +87,10 @@ std::optional<ResourceName> ToResourceName(const android::AssetManager2::Resourc name_out.package = std::string(name_in.package, name_in.package_len); std::optional<ResourceNamedTypeRef> type; + std::string converted; if (name_in.type16) { - type = - ParseResourceNamedType(util::Utf16ToUtf8(StringPiece16(name_in.type16, name_in.type_len))); + converted = util::Utf16ToUtf8(StringPiece16(name_in.type16, name_in.type_len)); + type = ParseResourceNamedType(converted); } else if (name_in.type) { type = ParseResourceNamedType(StringPiece(name_in.type, name_in.type_len)); } else { diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp index 8ea43abff895..34e8edb0a47f 100644 --- a/tools/aapt2/SdkConstants.cpp +++ b/tools/aapt2/SdkConstants.cpp @@ -27,7 +27,7 @@ namespace aapt { static ApiVersion sDevelopmentSdkLevel = 10000; static const auto sDevelopmentSdkCodeNames = - std::unordered_set<StringPiece>({"Q", "R", "S", "Sv2", "Tiramisu"}); + std::unordered_set<StringPiece>({"Q", "R", "S", "Sv2", "Tiramisu", "UpsideDownCake"}); static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = { {0x021c, 1}, diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index d8c76e297ec5..0170c4a4c54b 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -270,6 +270,8 @@ class LinkCommand : public Command { "Changes the name of the target package for overlay. Most useful\n" "when used in conjunction with --rename-manifest-package.", &options_.manifest_fixer_options.rename_overlay_target_package); + AddOptionalFlag("--rename-overlay-category", "Changes the category for the overlay.", + &options_.manifest_fixer_options.rename_overlay_category); AddOptionalFlagList("-0", "File suffix not to compress.", &options_.extensions_to_not_compress); AddOptionalSwitch("--no-compress", "Do not compress any resources.", diff --git a/tools/aapt2/format/Container.cpp b/tools/aapt2/format/Container.cpp index 9cef7b3d2ce3..1ff6c4996b91 100644 --- a/tools/aapt2/format/Container.cpp +++ b/tools/aapt2/format/Container.cpp @@ -76,7 +76,7 @@ bool ContainerWriter::AddResTableEntry(const pb::ResourceTable& table) { coded_out.WriteLittleEndian32(kResTable); // Write the aligned size. - const ::google::protobuf::uint64 size = table.ByteSize(); + const size_t size = table.ByteSizeLong(); const int padding = CalculatePaddingForAlignment(size); coded_out.WriteLittleEndian64(size); @@ -109,7 +109,7 @@ bool ContainerWriter::AddResFileEntry(const pb::internal::CompiledFile& file, coded_out.WriteLittleEndian32(kResFile); // Write the aligned size. - const ::google::protobuf::uint32 header_size = file.ByteSize(); + const size_t header_size = file.ByteSizeLong(); const int header_padding = CalculatePaddingForAlignment(header_size); const ::google::protobuf::uint64 data_size = in->TotalSize(); const int data_padding = CalculatePaddingForAlignment(data_size); diff --git a/tools/aapt2/io/Util.h b/tools/aapt2/io/Util.h index 5cb8206db23c..1b48a288d255 100644 --- a/tools/aapt2/io/Util.h +++ b/tools/aapt2/io/Util.h @@ -131,8 +131,7 @@ class ProtoInputStreamReader { template <typename T> bool ReadMessage(T *message) { ZeroCopyInputAdaptor adapter(in_); google::protobuf::io::CodedInputStream coded_stream(&adapter); - coded_stream.SetTotalBytesLimit(std::numeric_limits<int32_t>::max(), - coded_stream.BytesUntilTotalBytesLimit()); + coded_stream.SetTotalBytesLimit(std::numeric_limits<int32_t>::max()); return message->ParseFromCodedStream(&coded_stream); } diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index 948b11b6795e..42191912775a 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -449,13 +449,18 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, manifest_action["attribution"]["inherit-from"]; manifest_action["original-package"]; manifest_action["overlay"].Action([&](xml::Element* el) -> bool { - if (!options_.rename_overlay_target_package) { - return true; + if (options_.rename_overlay_target_package) { + if (xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "targetPackage")) { + attr->value = options_.rename_overlay_target_package.value(); + } } - - if (xml::Attribute* attr = - el->FindAttribute(xml::kSchemaAndroid, "targetPackage")) { - attr->value = options_.rename_overlay_target_package.value(); + if (options_.rename_overlay_category) { + if (xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "category")) { + attr->value = options_.rename_overlay_category.value(); + } else { + el->attributes.push_back(xml::Attribute{xml::kSchemaAndroid, "category", + options_.rename_overlay_category.value()}); + } } return true; }); diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h index d5d1d1770e1c..a8707d9d8623 100644 --- a/tools/aapt2/link/ManifestFixer.h +++ b/tools/aapt2/link/ManifestFixer.h @@ -48,6 +48,9 @@ struct ManifestFixerOptions { // <overlay>. std::optional<std::string> rename_overlay_target_package; + // The category to use instead of the one defined in 'android:category' in <overlay>. + std::optional<std::string> rename_overlay_category; + // The version name to set if 'android:versionName' is not defined in <manifest> or if // replace_version is set. std::optional<std::string> version_name_default; diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 432f10bdab97..098d0be7f87d 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -351,6 +351,54 @@ TEST_F(ManifestFixerTest, EXPECT_THAT(attr->value, StrEq("com.android")); } +TEST_F(ManifestFixerTest, AddOverlayCategory) { + ManifestFixerOptions options; + options.rename_overlay_category = std::string("category"); + + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <overlay android:targetName="Customization" android:targetPackage="android" /> + </manifest>)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); + + xml::Element* overlay_el = manifest_el->FindChild({}, "overlay"); + ASSERT_THAT(overlay_el, NotNull()); + + xml::Attribute* attr = overlay_el->FindAttribute(xml::kSchemaAndroid, "category"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("category")); +} + +TEST_F(ManifestFixerTest, OverrideOverlayCategory) { + ManifestFixerOptions options; + options.rename_overlay_category = std::string("category"); + + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <overlay android:targetName="Customization" + android:targetPackage="android" + android:category="yrogetac"/> + </manifest>)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); + + xml::Element* overlay_el = manifest_el->FindChild({}, "overlay"); + ASSERT_THAT(overlay_el, NotNull()); + + xml::Attribute* attr = overlay_el->FindAttribute(xml::kSchemaAndroid, "category"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("category")); +} + TEST_F(ManifestFixerTest, UseDefaultVersionNameAndCode) { ManifestFixerOptions options; options.version_name_default = std::string("Beta"); diff --git a/tools/bit/adb.cpp b/tools/bit/adb.cpp index f521a63255e1..201028ba900a 100644 --- a/tools/bit/adb.cpp +++ b/tools/bit/adb.cpp @@ -73,7 +73,7 @@ string get_system_property(const string& name, int* err) { Command cmd("adb"); - cmd.AddArg("shell"); + cmd.AddArg("exec-out"); cmd.AddArg("getprop"); cmd.AddArg(name); @@ -278,7 +278,7 @@ run_instrumentation_test(const string& packageName, const string& runner, const InstrumentationCallbacks* callbacks) { Command cmd("adb"); - cmd.AddArg("shell"); + cmd.AddArg("exec-out"); cmd.AddArg("am"); cmd.AddArg("instrument"); cmd.AddArg("-w"); diff --git a/tools/bit/main.cpp b/tools/bit/main.cpp index fd184f50091a..0d48070fd0c6 100644 --- a/tools/bit/main.cpp +++ b/tools/bit/main.cpp @@ -52,24 +52,22 @@ struct Target { int testPassCount; int testFailCount; + int testIgnoreCount; int unknownFailureCount; // unknown failure == "Process crashed", etc. - bool actionsWithNoTests; Target(bool b, bool i, bool t, const string& p); }; Target::Target(bool b, bool i, bool t, const string& p) - :build(b), - install(i), - test(t), - pattern(p), - testActionCount(0), - testPassCount(0), - testFailCount(0), - unknownFailureCount(0), - actionsWithNoTests(false) -{ -} + : build(b), + install(i), + test(t), + pattern(p), + testActionCount(0), + testPassCount(0), + testFailCount(0), + testIgnoreCount(0), + unknownFailureCount(0) {} /** * Command line options. @@ -188,13 +186,12 @@ struct TestAction { // The number of tests that failed int failCount; + + // The number of tests that were ignored (because of @Ignore) + int ignoreCount; }; -TestAction::TestAction() - :passCount(0), - failCount(0) -{ -} +TestAction::TestAction() : passCount(0), failCount(0), ignoreCount(0) {} /** * Record for an activity that is going to be launched. @@ -278,7 +275,7 @@ TestResults::OnTestStatus(TestStatus& status) line << " of " << testCount; } } - line << ": " << m_currentAction->target->name << ':' << className << "\\#" << testName; + line << ": " << m_currentAction->target->name << ':' << className << "#" << testName; print_one_line("%s", line.str().c_str()); } else if ((resultCode == -1) || (resultCode == -2)) { // test failed @@ -286,9 +283,9 @@ TestResults::OnTestStatus(TestStatus& status) // all as "failures". m_currentAction->failCount++; m_currentAction->target->testFailCount++; - printf("%s\n%sFailed: %s:%s\\#%s%s\n", g_escapeClearLine, g_escapeRedBold, - m_currentAction->target->name.c_str(), className.c_str(), - testName.c_str(), g_escapeEndColor); + printf("%s\n%sFailed: %s:%s#%s%s\n", g_escapeClearLine, g_escapeRedBold, + m_currentAction->target->name.c_str(), className.c_str(), testName.c_str(), + g_escapeEndColor); bool stackFound; string stack = get_bundle_string(results, &stackFound, "stack", NULL); @@ -300,6 +297,13 @@ TestResults::OnTestStatus(TestStatus& status) } else if (stackFound) { printf("%s\n", stack.c_str()); } + } else if (resultCode == -3) { + // test ignored + m_currentAction->ignoreCount++; + m_currentAction->target->testIgnoreCount++; + printf("%s\n%sIgnored: %s:%s#%s%s\n", g_escapeClearLine, g_escapeYellowBold, + m_currentAction->target->name.c_str(), className.c_str(), testName.c_str(), + g_escapeEndColor); } } @@ -403,11 +407,14 @@ print_usage(FILE* out) { fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs all the\n"); fprintf(out, " tests in the ProtoOutputStreamBoolTest class.\n"); fprintf(out, "\n"); - fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest\\#testWrite\n"); + fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest#testWrite\n"); fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs the testWrite\n"); fprintf(out, " test method on that class.\n"); fprintf(out, "\n"); - fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest\\#testWrite,.ProtoOutputStreamBoolTest\\#testRepeated\n"); + fprintf(out, + " bit " + "CtsProtoTestCases:.ProtoOutputStreamBoolTest#testWrite,.ProtoOutputStreamBoolTest#" + "testRepeated\n"); fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs the testWrite\n"); fprintf(out, " and testRepeated test methods on that class.\n"); fprintf(out, "\n"); @@ -450,6 +457,35 @@ print_usage(FILE* out) { fprintf(out, "\n"); } +/** + * Prints a possibly color-coded summary of test results. Example output: + * + * "34 passed, 0 failed, 1 ignored\n" + */ +static void print_results(int passed, int failed, int ignored) { + char const* nothing = ""; + char const* cp = nothing; + char const* cf = nothing; + char const* ci = nothing; + + if (failed > 0) { + cf = g_escapeRedBold; + } else if (passed > 0 || ignored > 0) { + cp = passed > 0 ? g_escapeGreenBold : nothing; + ci = ignored > 0 ? g_escapeYellowBold : nothing; + } else { + cp = g_escapeYellowBold; + cf = g_escapeYellowBold; + } + + if (ignored > 0) { + printf("%s%d passed%s, %s%d failed%s, %s%d ignored%s\n", cp, passed, g_escapeEndColor, cf, + failed, g_escapeEndColor, ci, ignored, g_escapeEndColor); + } else { + printf("%s%d passed%s, %s%d failed%s\n", cp, passed, g_escapeEndColor, cf, failed, + g_escapeEndColor); + } +} /** * Sets the appropriate flag* variables. If there is a problem with the @@ -812,7 +848,7 @@ run_phases(vector<Target*> targets, const Options& options) // Stop & Sync if (!options.noRestart) { - err = run_adb("shell", "stop", NULL); + err = run_adb("exec-out", "stop", NULL); check_error(err); } err = run_adb("remount", NULL); @@ -831,9 +867,9 @@ run_phases(vector<Target*> targets, const Options& options) } else { print_status("Restarting the runtime"); - err = run_adb("shell", "setprop", "sys.boot_completed", "0", NULL); + err = run_adb("exec-out", "setprop", "sys.boot_completed", "0", NULL); check_error(err); - err = run_adb("shell", "start", NULL); + err = run_adb("exec-out", "start", NULL); check_error(err); } @@ -846,7 +882,7 @@ run_phases(vector<Target*> targets, const Options& options) sleep(2); } sleep(1); - err = run_adb("shell", "wm", "dismiss-keyguard", NULL); + err = run_adb("exec-out", "wm", "dismiss-keyguard", NULL); check_error(err); } } @@ -863,7 +899,7 @@ run_phases(vector<Target*> targets, const Options& options) continue; } // TODO: if (!apk.file.fileInfo.exists || apk.file.HasChanged()) - err = run_adb("shell", "mkdir", "-p", dir.c_str(), NULL); + err = run_adb("exec-out", "mkdir", "-p", dir.c_str(), NULL); check_error(err); err = run_adb("push", pushed.file.filename.c_str(), pushed.dest.c_str(), NULL); check_error(err); @@ -945,9 +981,9 @@ run_phases(vector<Target*> targets, const Options& options) } } if (runAll) { - err = run_adb("shell", installedPath.c_str(), NULL); + err = run_adb("exec-out", installedPath.c_str(), NULL); } else { - err = run_adb("shell", installedPath.c_str(), filterArg.c_str(), NULL); + err = run_adb("exec-out", installedPath.c_str(), filterArg.c_str(), NULL); } if (err == 0) { target->testPassCount++; @@ -1035,22 +1071,10 @@ run_phases(vector<Target*> targets, const Options& options) err = run_instrumentation_test(action.packageName, action.runner, action.className, &testResults); check_error(err); - if (action.passCount == 0 && action.failCount == 0) { - action.target->actionsWithNoTests = true; - } int total = action.passCount + action.failCount; printf("%sRan %d test%s for %s. ", g_escapeClearLine, total, total > 1 ? "s" : "", action.target->name.c_str()); - if (action.passCount == 0 && action.failCount == 0) { - printf("%s%d passed, %d failed%s\n", g_escapeYellowBold, action.passCount, - action.failCount, g_escapeEndColor); - } else if (action.failCount > 0) { - printf("%d passed, %s%d failed%s\n", action.passCount, g_escapeRedBold, - action.failCount, g_escapeEndColor); - } else { - printf("%s%d passed%s, %d failed\n", g_escapeGreenBold, action.passCount, - g_escapeEndColor, action.failCount); - } + print_results(action.passCount, action.failCount, action.ignoreCount); if (!testResults.IsSuccess()) { printf("\n%sTest didn't finish successfully: %s%s\n", g_escapeRedBold, testResults.GetErrorMessage().c_str(), g_escapeEndColor); @@ -1073,7 +1097,7 @@ run_phases(vector<Target*> targets, const Options& options) const ActivityAction& action = activityActions[0]; string componentName = action.packageName + "/" + action.className; - err = run_adb("shell", "am", "start", componentName.c_str(), NULL); + err = run_adb("exec-out", "am", "start", componentName.c_str(), NULL); check_error(err); } @@ -1147,17 +1171,11 @@ run_phases(vector<Target*> targets, const Options& options) printf(" %sUnknown failure, see above message.%s\n", g_escapeRedBold, g_escapeEndColor); hasErrors = true; - } else if (target->actionsWithNoTests) { - printf(" %s%d passed, %d failed%s\n", g_escapeYellowBold, - target->testPassCount, target->testFailCount, g_escapeEndColor); - hasErrors = true; - } else if (target->testFailCount > 0) { - printf(" %d passed, %s%d failed%s\n", target->testPassCount, - g_escapeRedBold, target->testFailCount, g_escapeEndColor); - hasErrors = true; } else { - printf(" %s%d passed%s, %d failed\n", g_escapeGreenBold, - target->testPassCount, g_escapeEndColor, target->testFailCount); + printf(" %s%s ", target->name.c_str(), + padding.c_str() + target->name.length()); + print_results(target->testPassCount, target->testFailCount, + target->testIgnoreCount); } } } diff --git a/tools/codegen/Android.bp b/tools/codegen/Android.bp index e53ba3e18a86..a1df878df12e 100644 --- a/tools/codegen/Android.bp +++ b/tools/codegen/Android.bp @@ -9,7 +9,7 @@ package { java_binary_host { name: "codegen_cli", - manifest: "manifest.txt", + main_class: "com.android.codegen.MainKt", srcs: [ "src/**/*.kt", ], diff --git a/tools/codegen/manifest.txt b/tools/codegen/manifest.txt deleted file mode 100644 index 6e1018ba6b55..000000000000 --- a/tools/codegen/manifest.txt +++ /dev/null @@ -1 +0,0 @@ -Main-class: com.android.codegen.MainKt diff --git a/tools/fonts/Android.bp b/tools/fonts/Android.bp index eeb9e3ceda1e..f8629f9bd0b8 100644 --- a/tools/fonts/Android.bp +++ b/tools/fonts/Android.bp @@ -24,12 +24,7 @@ package { python_defaults { name: "fonts_python_defaults", version: { - py2: { - enabled: false, - embedded_launcher: false, - }, py3: { - enabled: true, embedded_launcher: true, }, }, diff --git a/tools/lint/OWNERS b/tools/lint/OWNERS index 7c0451900e32..33e237d306fc 100644 --- a/tools/lint/OWNERS +++ b/tools/lint/OWNERS @@ -2,4 +2,8 @@ brufino@google.com jsharkey@google.com per-file *CallingSettingsNonUserGetterMethods* = file:/packages/SettingsProvider/OWNERS +per-file *RegisterReceiverFlagDetector* = jacobhobbie@google.com +# Android lint in the Android platform maintainers +colefaust@google.com +farivar@google.com diff --git a/tools/lint/README.md b/tools/lint/README.md index b534b62cb395..b235ad60c799 100644 --- a/tools/lint/README.md +++ b/tools/lint/README.md @@ -1,15 +1,44 @@ -# Android Framework Lint Checker +# Android Lint Checks for AOSP -Custom lint checks written here are going to be executed for modules that opt in to those (e.g. any +Custom Android Lint checks are written here to be executed against java modules +in AOSP. These checks are broken down into two subdirectories: + +1. [Global Checks](#android-global-lint-checker) +2. [Framework Checks](#android-framework-lint-checker) + +# [Android Global Lint Checker](/global) +Checks written here are executed for the entire tree. The `AndroidGlobalLintChecker` +build target produces a jar file that is included in the overall build output +(`AndroidGlobalLintChecker.jar`). This file is then downloaded as a prebuilt under the +`prebuilts/cmdline-tools` subproject, and included by soong with all invocations of lint. + +## How to add new global lint checks +1. Write your detector with its issues and put it into + `global/checks/src/main/java/com/google/android/lint`. +2. Add your detector's issues into `AndroidGlobalIssueRegistry`'s `issues` + field. +3. Write unit tests for your detector in one file and put it into + `global/checks/test/java/com/google/android/lint`. +4. Have your change reviewed and merged. Once your change is merged, + obtain a build number from a successful build that includes your change. +5. Run `prebuilts/cmdline-tools/update-android-global-lint-checker.sh + <build_number>`. The script will create a commit that you can upload for + approval to the `prebuilts/cmdline-tools` subproject. +6. Done! Your lint check should be applied in lint report builds across the + entire tree! + +# [Android Framework Lint Checker](/framework) + +Checks written here are going to be executed for modules that opt in to those (e.g. any `services.XXX` module) and results will be automatically reported on CLs on gerrit. -## How to add new lint checks +## How to add new framework lint checks 1. Write your detector with its issues and put it into - `checks/src/main/java/com/google/android/lint`. + `framework/checks/src/main/java/com/google/android/lint`. 2. Add your detector's issues into `AndroidFrameworkIssueRegistry`'s `issues` field. 3. Write unit tests for your detector in one file and put it into - `checks/test/java/com/google/android/lint`. + `framework/checks/test/java/com/google/android/lint`. 4. Done! Your lint checks should be applied in lint report builds for modules that include `AndroidFrameworkLintChecker`. @@ -44,7 +73,11 @@ m out/soong/.intermediates/frameworks/base/services/autofill/services.autofill/a environment variable with the id of the lint. For example: `ANDROID_LINT_CHECK=UnusedTokenOfOriginalCallingIdentity m out/[...]/lint-report.html` -## Create or update a baseline +# How to apply automatic fixes suggested by lint + +See [lint_fix](fix/README.md) + +# Create or update a baseline Baseline files can be used to silence known errors (and warnings) that are deemed to be safe. When there is a lint-baseline.xml file in the root folder of the java library, soong will @@ -75,9 +108,10 @@ locally change the soong code in [lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75) adding `cmd.Flag("--nowarn")` and running lint again. -## Documentation +# Documentation - [Android Lint Docs](https://googlesamples.github.io/android-custom-lint-rules/) +- [Lint Check Unit Testing](https://googlesamples.github.io/android-custom-lint-rules/api-guide/unit-testing.md.html) - [Android Lint source files](https://source.corp.google.com/studio-main/tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/) - [PSI source files](https://github.com/JetBrains/intellij-community/tree/master/java/java-psi-api/src/com/intellij/psi) - [UAST source files](https://upsource.jetbrains.com/idea-ce/structure/idea-ce-7b9b8cc138bbd90aec26433f82cd2c6838694003/uast/uast-common/src/org/jetbrains/uast) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt deleted file mode 100644 index 8011b36c9a8f..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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. - */ - -package com.google.android.lint - -import com.android.tools.lint.detector.api.AnnotationInfo -import com.android.tools.lint.detector.api.AnnotationOrigin -import com.android.tools.lint.detector.api.AnnotationUsageInfo -import com.android.tools.lint.detector.api.AnnotationUsageType -import com.android.tools.lint.detector.api.ConstantEvaluator -import com.android.tools.lint.detector.api.Category -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Implementation -import com.android.tools.lint.detector.api.Issue -import com.android.tools.lint.detector.api.JavaContext -import com.android.tools.lint.detector.api.Scope -import com.android.tools.lint.detector.api.Severity -import com.android.tools.lint.detector.api.SourceCodeScanner -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiMethod -import org.jetbrains.uast.UElement - -/** - * Lint Detector that ensures that any method overriding a method annotated - * with @EnforcePermission is also annotated with the exact same annotation. - * The intent is to surface the effective permission checks to the service - * implementations. - */ -class EnforcePermissionDetector : Detector(), SourceCodeScanner { - - val ENFORCE_PERMISSION = "android.annotation.EnforcePermission" - - override fun applicableAnnotations(): List<String> { - return listOf(ENFORCE_PERMISSION) - } - - private fun areAnnotationsEquivalent( - context: JavaContext, - anno1: PsiAnnotation, - anno2: PsiAnnotation - ): Boolean { - if (anno1.qualifiedName != anno2.qualifiedName) { - return false - } - val attr1 = anno1.parameterList.attributes - val attr2 = anno2.parameterList.attributes - if (attr1.size != attr2.size) { - return false - } - for (i in attr1.indices) { - if (attr1[i].name != attr2[i].name) { - return false - } - val v1 = ConstantEvaluator.evaluate(context, attr1[i].value) - val v2 = ConstantEvaluator.evaluate(context, attr2[i].value) - if (v1 != v2) { - return false - } - } - return true - } - - override fun visitAnnotationUsage( - context: JavaContext, - element: UElement, - annotationInfo: AnnotationInfo, - usageInfo: AnnotationUsageInfo - ) { - if (usageInfo.type == AnnotationUsageType.EXTENDS) { - val newClass = element.sourcePsi?.parent?.parent as PsiClass - val extendedClass: PsiClass = usageInfo.referenced as PsiClass - val newAnnotation = newClass.getAnnotation(ENFORCE_PERMISSION) - val extendedAnnotation = extendedClass.getAnnotation(ENFORCE_PERMISSION)!! - - val location = context.getLocation(element) - val newClassName = newClass.qualifiedName - val extendedClassName = extendedClass.qualifiedName - if (newAnnotation == null) { - val msg = "The class $newClassName extends the class $extendedClassName which " + - "is annotated with @EnforcePermission. The same annotation must be used " + - "on $newClassName." - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (!areAnnotationsEquivalent(context, newAnnotation, extendedAnnotation)) { - val msg = "The class $newClassName is annotated with ${newAnnotation.text} " + - "which differs from the parent class $extendedClassName: " + - "${extendedAnnotation.text}. The same annotation must be used for " + - "both classes." - context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) - } - } else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE && - annotationInfo.origin == AnnotationOrigin.METHOD) { - val overridingMethod = element.sourcePsi as PsiMethod - val overriddenMethod = usageInfo.referenced as PsiMethod - val overridingAnnotation = overridingMethod.getAnnotation(ENFORCE_PERMISSION) - val overriddenAnnotation = overriddenMethod.getAnnotation(ENFORCE_PERMISSION)!! - - val location = context.getLocation(element) - val overridingClass = overridingMethod.parent as PsiClass - val overriddenClass = overriddenMethod.parent as PsiClass - val overridingName = "${overridingClass.name}.${overridingMethod.name}" - val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}" - if (overridingAnnotation == null) { - val msg = "The method $overridingName overrides the method $overriddenName which " + - "is annotated with @EnforcePermission. The same annotation must be used " + - "on $overridingName" - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (!areAnnotationsEquivalent( - context, overridingAnnotation, overriddenAnnotation)) { - val msg = "The method $overridingName is annotated with " + - "${overridingAnnotation.text} which differs from the overridden " + - "method $overriddenName: ${overriddenAnnotation.text}. The same " + - "annotation must be used for both methods." - context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) - } - } - } - - companion object { - val EXPLANATION = """ - The @EnforcePermission annotation is used to indicate that the underlying binder code - has already verified the caller's permissions before calling the appropriate method. The - verification code is usually generated by the AIDL compiler, which also takes care of - annotating the generated Java code. - - In order to surface that information to platform developers, the same annotation must be - used on the implementation class or methods. - """ - - val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create( - id = "MissingEnforcePermissionAnnotation", - briefDescription = "Missing @EnforcePermission annotation on Binder method", - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 6, - severity = Severity.ERROR, - implementation = Implementation( - EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - - val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create( - id = "MismatchingEnforcePermissionAnnotation", - briefDescription = "Incorrect @EnforcePermission annotation on Binder method", - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 6, - severity = Severity.ERROR, - implementation = Implementation( - EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - } -} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt deleted file mode 100644 index f5f4ebee24e0..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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. - */ - -package com.google.android.lint - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile -import com.android.tools.lint.checks.infrastructure.TestLintTask -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Issue - -@Suppress("UnstableApiUsage") -class EnforcePermissionDetectorTest : LintDetectorTest() { - override fun getDetector(): Detector = EnforcePermissionDetector() - - override fun getIssues(): List<Issue> = listOf( - EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, - EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION - ) - - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - fun testDoesNotDetectIssuesCorrectAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public class TestClass1 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - import android.annotation.EnforcePermission; - public class TestClass2 extends IFooMethod.Stub { - @Override - @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testDetectIssuesMismatchingAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public class TestClass3 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \ -annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ -which differs from the parent class IFoo.Stub: \ -@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \ -same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation] -public class TestClass3 extends IFoo.Stub { - ~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMismatchingAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - public class TestClass4 extends IFooMethod.Stub { - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \ -annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ -which differs from the overridden method Stub.testMethod: \ -@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \ -annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] - public void testMethod() {} - ~~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMissingAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - public class TestClass5 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \ -the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \ -used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation] -public class TestClass5 extends IFoo.Stub { - ~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMissingAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - public class TestClass6 extends IFooMethod.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \ -overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \ -annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation] - public void testMethod() {} - ~~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - /* Stubs */ - - private val interfaceIFooStub: TestFile = java( - """ - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public interface IFoo { - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public static abstract class Stub implements IFoo { - @Override - public void testMethod() {} - } - public void testMethod(); - } - """ - ).indented() - - private val interfaceIFooMethodStub: TestFile = java( - """ - public interface IFooMethod { - public static abstract class Stub implements IFooMethod { - @Override - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod() {} - } - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod(); - } - """ - ).indented() - - private val manifestPermissionStub: TestFile = java( - """ - package android.Manifest; - class permission { - public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; - public static final String INTERNET = "android.permission.INTERNET"; - } - """ - ).indented() - - private val enforcePermissionAnnotationStub: TestFile = java( - """ - package android.annotation; - public @interface EnforcePermission {} - """ - ).indented() - - private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub, - manifestPermissionStub, enforcePermissionAnnotationStub) - - // Substitutes "backslash + new line" with an empty string to imitate line continuation - private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") -} diff --git a/tools/lint/common/Android.bp b/tools/lint/common/Android.bp new file mode 100644 index 000000000000..898f88b8759c --- /dev/null +++ b/tools/lint/common/Android.bp @@ -0,0 +1,29 @@ +// 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. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "AndroidCommonLint", + srcs: ["src/main/java/**/*.kt"], + libs: ["lint_api"], + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt new file mode 100644 index 000000000000..0ef165f1523b --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package com.google.android.lint + +import com.google.android.lint.model.Method + +const val CLASS_STUB = "Stub" +const val CLASS_CONTEXT = "android.content.Context" +const val CLASS_ACTIVITY_MANAGER_SERVICE = "com.android.server.am.ActivityManagerService" +const val CLASS_ACTIVITY_MANAGER_INTERNAL = "android.app.ActivityManagerInternal" + +// Enforce permission APIs +val ENFORCE_PERMISSION_METHODS = listOf( + Method(CLASS_CONTEXT, "checkPermission"), + Method(CLASS_CONTEXT, "checkCallingPermission"), + Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"), + Method(CLASS_CONTEXT, "enforcePermission"), + Method(CLASS_CONTEXT, "enforceCallingPermission"), + Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"), + Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"), + Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") +) + +const val ANNOTATION_PERMISSION_METHOD = "android.annotation.PermissionMethod" +const val ANNOTATION_PERMISSION_NAME = "android.annotation.PermissionName" +const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult" diff --git a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt new file mode 100644 index 000000000000..9a7f8fa53d87 --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package com.google.android.lint + +import com.android.tools.lint.detector.api.getUMethod +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter +import org.jetbrains.uast.UQualifiedReferenceExpression + +fun isPermissionMethodCall(callExpression: UCallExpression): Boolean { + val method = callExpression.resolve()?.getUMethod() ?: return false + return hasPermissionMethodAnnotation(method) +} + +fun hasPermissionMethodAnnotation(method: UMethod): Boolean = + getPermissionMethodAnnotation(method) != null + +fun getPermissionMethodAnnotation(method: UMethod?): UAnnotation? = method?.uAnnotations + ?.firstOrNull { it.qualifiedName == ANNOTATION_PERMISSION_METHOD } + +fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any { + it.hasQualifiedName(ANNOTATION_PERMISSION_NAME) +} + +/** + * Attempts to return a CallExpression from a QualifiedReferenceExpression (or returns it directly if passed directly) + * @param callOrReferenceCall expected to be UCallExpression or UQualifiedReferenceExpression + * @return UCallExpression, if available + */ +fun findCallExpression(callOrReferenceCall: UElement?): UCallExpression? = + when (callOrReferenceCall) { + is UCallExpression -> callOrReferenceCall + is UQualifiedReferenceExpression -> callOrReferenceCall.selector as? UCallExpression + else -> null + } diff --git a/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt new file mode 100644 index 000000000000..3939b6109eaa --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package com.google.android.lint.model + +/** + * Data class to represent a Method + */ +data class Method(val clazz: String, val name: String) { + override fun toString(): String { + return "$clazz#$name" + } +} diff --git a/tools/lint/fix/Android.bp b/tools/lint/fix/Android.bp new file mode 100644 index 000000000000..43f21221ae5a --- /dev/null +++ b/tools/lint/fix/Android.bp @@ -0,0 +1,33 @@ +// 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. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +python_binary_host { + name: "lint_fix", + main: "soong_lint_fix.py", + srcs: ["soong_lint_fix.py"], +} + +python_library_host { + name: "soong_lint_fix", + srcs: ["soong_lint_fix.py"], +} diff --git a/tools/lint/fix/README.md b/tools/lint/fix/README.md new file mode 100644 index 000000000000..a5ac2be1c18a --- /dev/null +++ b/tools/lint/fix/README.md @@ -0,0 +1,30 @@ +# Refactoring the platform with lint +Inspiration: go/refactor-the-platform-with-lint\ +**Special Thanks: brufino@, azharaa@, for the prior work that made this all possible** + +## What is this? + +It's a python script that runs the framework linter, +and then (optionally) copies modified files back into the source tree.\ +Why python, you ask? Because python is cool ¯\_(ツ)_/¯. + +Incidentally, this exposes a much simpler way to run individual lint checks +against individual modules, so it's useful beyond applying fixes. + +## Why? + +Lint is not allowed to modify source files directly via lint's `--apply-suggestions` flag. +As a compromise, soong zips up the (potentially) modified sources and leaves them in an intermediate +directory. This script runs the lint, unpacks those files, and copies them back into the tree. + +## How do I run it? +**WARNING: You probably want to commit/stash any changes to your working tree before doing this...** + +``` +source build/envsetup.sh +lunch cf_x86_64_phone-userdebug # or any lunch target +m lint_fix +lint_fix -h +``` + +The script's help output explains things that are omitted here. diff --git a/tools/lint/fix/soong_lint_fix.py b/tools/lint/fix/soong_lint_fix.py new file mode 100644 index 000000000000..cd4d778d1dec --- /dev/null +++ b/tools/lint/fix/soong_lint_fix.py @@ -0,0 +1,173 @@ +# 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. + +import argparse +import json +import os +import shutil +import subprocess +import sys +import zipfile + +ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP") +ANDROID_PRODUCT_OUT = os.environ.get("ANDROID_PRODUCT_OUT") +PRODUCT_OUT = ANDROID_PRODUCT_OUT.removeprefix(f"{ANDROID_BUILD_TOP}/") + +SOONG_UI = "build/soong/soong_ui.bash" +PATH_PREFIX = "out/soong/.intermediates" +PATH_SUFFIX = "android_common/lint" +FIX_ZIP = "suggested-fixes.zip" + +class SoongLintFix: + """ + This class creates a command line tool that will + apply lint fixes to the platform via the necessary + combination of soong and shell commands. + + It breaks up these operations into a few "private" methods + that are intentionally exposed so experimental code can tweak behavior. + + The entry point, `run`, will apply lint fixes using the + intermediate `suggested-fixes` directory that soong creates during its + invocation of lint. + + Basic usage: + ``` + from soong_lint_fix import SoongLintFix + + SoongLintFix().run() + ``` + """ + def __init__(self): + self._parser = _setup_parser() + self._args = None + self._kwargs = None + self._path = None + self._target = None + + + def run(self, additional_setup=None, custom_fix=None): + """ + Run the script + """ + self._setup() + self._find_module() + self._lint() + + if not self._args.no_fix: + self._fix() + + if self._args.print: + self._print() + + def _setup(self): + self._args = self._parser.parse_args() + env = os.environ.copy() + if self._args.check: + env["ANDROID_LINT_CHECK"] = self._args.check + if self._args.lint_module: + env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._args.lint_module + + self._kwargs = { + "env": env, + "executable": "/bin/bash", + "shell": True, + } + + os.chdir(ANDROID_BUILD_TOP) + + + def _find_module(self): + print("Refreshing soong modules...") + try: + os.mkdir(ANDROID_PRODUCT_OUT) + except OSError: + pass + subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs) + print("done.") + + with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f: + module_info = json.load(f) + + if self._args.module not in module_info: + sys.exit(f"Module {self._args.module} not found!") + + module_path = module_info[self._args.module]["path"][0] + print(f"Found module {module_path}/{self._args.module}.") + + self._path = f"{PATH_PREFIX}/{module_path}/{self._args.module}/{PATH_SUFFIX}" + self._target = f"{self._path}/lint-report.txt" + + + def _lint(self): + print("Cleaning up any old lint results...") + try: + os.remove(f"{self._target}") + os.remove(f"{self._path}/{FIX_ZIP}") + except FileNotFoundError: + pass + print("done.") + + print(f"Generating {self._target}") + subprocess.call(f"{SOONG_UI} --make-mode {self._target}", **self._kwargs) + print("done.") + + + def _fix(self): + print("Copying suggested fixes to the tree...") + with zipfile.ZipFile(f"{self._path}/{FIX_ZIP}") as zip: + for name in zip.namelist(): + if name.startswith("out") or not name.endswith(".java"): + continue + with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst: + shutil.copyfileobj(src, dst) + print("done.") + + + def _print(self): + print("### lint-report.txt ###", end="\n\n") + with open(self._target, "r") as f: + print(f.read()) + + +def _setup_parser(): + parser = argparse.ArgumentParser(description=""" + This is a python script that applies lint fixes to the platform: + 1. Set up the environment, etc. + 2. Run lint on the specified target. + 3. Copy the modified files, from soong's intermediate directory, back into the tree. + + **Gotcha**: You must have run `source build/envsetup.sh` and `lunch` first. + """, formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument('module', + help='The soong build module to run ' + '(e.g. framework-minus-apex or services.core.unboosted)') + + parser.add_argument('--check', + help='Which lint to run. Passed to the ANDROID_LINT_CHECK environment variable.') + + parser.add_argument('--lint-module', + help='Specific lint module to run. Passed to the ANDROID_LINT_CHECK_EXTRA_MODULES environment variable.') + + parser.add_argument('--no-fix', action='store_true', + help='Just build and run the lint, do NOT apply the fixes.') + + parser.add_argument('--print', action='store_true', + help='Print the contents of the generated lint-report.txt at the end.') + + return parser + +if __name__ == "__main__": + SoongLintFix().run()
\ No newline at end of file diff --git a/tools/lint/Android.bp b/tools/lint/framework/Android.bp index 17547ef8b561..30a6daaef2a4 100644 --- a/tools/lint/Android.bp +++ b/tools/lint/framework/Android.bp @@ -1,4 +1,4 @@ -// Copyright (C) 2021 The Android Open Source Project +// 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. @@ -29,6 +29,10 @@ java_library_host { "auto_service_annotations", "lint_api", ], + static_libs: [ + "AndroidCommonLint", + ], + kotlincflags: ["-Xjvm-default=all"], } java_test_host { @@ -42,5 +46,19 @@ java_test_host { ], test_options: { unit_test: true, + tradefed_options: [ + { + // lint bundles in some classes that were built with older versions + // of libraries, and no longer load. Since tradefed tries to load + // all classes in the jar to look for tests, it crashes loading them. + // Exclude these classes from tradefed's search. + name: "exclude-paths", + value: "org/apache", + }, + { + name: "exclude-paths", + value: "META-INF", + }, + ], }, } diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt new file mode 100644 index 000000000000..935badecf8d5 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.google.android.lint.parcel.SaferParcelChecker +import com.google.auto.service.AutoService + +@AutoService(IssueRegistry::class) +@Suppress("UnstableApiUsage") +class AndroidFrameworkIssueRegistry : IssueRegistry() { + override val issues = listOf( + CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, + CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, + CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, + CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, + CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE, + CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED, + SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, + // TODO: Currently crashes due to OOM issue + // PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, + PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE, + PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD, + ) + + override val api: Int + get() = CURRENT_API + + override val minApi: Int + get() = 8 + + override val vendor: Vendor = Vendor( + vendorName = "Android", + feedbackUrl = "http://b/issues/new?component=315013", + contact = "brufino@google.com" + ) +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt index 930378b168b2..0c375c358e61 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt @@ -33,6 +33,7 @@ import org.jetbrains.uast.UBlockExpression import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UDeclarationsExpression import org.jetbrains.uast.UElement +import org.jetbrains.uast.UIfExpression import org.jetbrains.uast.ULocalVariable import org.jetbrains.uast.USimpleNameReferenceExpression import org.jetbrains.uast.UTryExpression @@ -52,10 +53,10 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { private val tokensMap = mutableMapOf<String, Token>() override fun getApplicableUastTypes(): List<Class<out UElement?>> = - listOf(ULocalVariable::class.java, UCallExpression::class.java) + listOf(ULocalVariable::class.java, UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler = - TokenUastHandler(context) + TokenUastHandler(context) /** File analysis starts with a clear map */ override fun beforeCheckFile(context: Context) { @@ -70,9 +71,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { override fun afterCheckFile(context: Context) { for (token in tokensMap.values) { context.report( - ISSUE_UNUSED_TOKEN, - token.location, - getIncidentMessageUnusedToken(token.variableName) + ISSUE_UNUSED_TOKEN, + token.location, + getIncidentMessageUnusedToken(token.variableName) ) } tokensMap.clear() @@ -96,9 +97,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { val variableName = node.getName() if (!node.isFinal) { context.report( - ISSUE_NON_FINAL_TOKEN, - location, - getIncidentMessageNonFinalToken(variableName) + ISSUE_NON_FINAL_TOKEN, + location, + getIncidentMessageNonFinalToken(variableName) ) } // If there exists an unused variable with the same name in the map, we can imply that @@ -106,9 +107,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { val oldToken = tokensMap[variableName] if (oldToken != null) { context.report( - ISSUE_UNUSED_TOKEN, - oldToken.location, - getIncidentMessageUnusedToken(oldToken.variableName) + ISSUE_UNUSED_TOKEN, + oldToken.location, + getIncidentMessageUnusedToken(oldToken.variableName) ) } // If there exists a token in the same scope as the current new token, it means that @@ -117,56 +118,84 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { val firstCallToken = findFirstTokenInScope(node) if (firstCallToken != null) { context.report( - ISSUE_NESTED_CLEAR_IDENTITY_CALLS, - createNestedLocation(firstCallToken, location), - getIncidentMessageNestedClearIdentityCallsPrimary( - firstCallToken.variableName, - variableName - ) + ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + createNestedLocation(firstCallToken, location), + getIncidentMessageNestedClearIdentityCallsPrimary( + firstCallToken.variableName, + variableName + ) ) } // If the next statement in the tree is not a try-finally statement, we need to report // the "clearCallingIdentity() is not followed by try-finally" issue val finallyClause = (getNextStatementOfLocalVariable(node) as? UTryExpression) - ?.finallyClause + ?.finallyClause if (finallyClause == null) { context.report( - ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, - location, - getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName) + ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, + location, + getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName) ) } tokensMap[variableName] = Token( - variableName, - node.sourcePsi?.getUseScope(), - location, - finallyClause + variableName, + node.sourcePsi?.getUseScope(), + location, + finallyClause ) } - /** - * For every method(): - * - Checks use of caller-aware methods issue - * For every call of Binder.restoreCallingIdentity(token): - * - Checks for restoreCallingIdentity() not in the finally block issue - * - Removes token from tokensMap if token is within the scope of the method - */ override fun visitCallExpression(node: UCallExpression) { + when { + isMethodCall(node, Method.BINDER_CLEAR_CALLING_IDENTITY) -> { + checkClearCallingIdentityCall(node) + } + isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY) -> { + checkRestoreCallingIdentityCall(node) + } + isCallerAwareMethod(node) -> checkCallerAwareMethod(node) + } + } + + private fun checkClearCallingIdentityCall(node: UCallExpression) { + var firstNonQualifiedParent = getFirstNonQualifiedParent(node) + // if the call expression is inside a ternary, and the ternary is assigned + // to a variable, then we are still technically assigning + // any result of clearCallingIdentity to a variable + if (firstNonQualifiedParent is UIfExpression && firstNonQualifiedParent.isTernary) { + firstNonQualifiedParent = firstNonQualifiedParent.uastParent + } + if (firstNonQualifiedParent !is ULocalVariable) { + context.report( + ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE, + context.getLocation(node), + getIncidentMessageResultOfClearIdentityCallNotStoredInVariable( + node.getQualifiedParentOrThis().asRenderString() + ) + ) + } + } + + private fun checkCallerAwareMethod(node: UCallExpression) { val token = findFirstTokenInScope(node) - if (isCallerAwareMethod(node) && token != null) { + if (token != null) { context.report( - ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, - context.getLocation(node), - getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( - token.variableName, - node.asRenderString() - ) + ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, + context.getLocation(node), + getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( + token.variableName, + node.asRenderString() + ) ) - return } - if (!isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY)) return - val first = node.valueArguments[0].skipParenthesizedExprDown() - val arg = first as? USimpleNameReferenceExpression ?: return + } + + /** + * - Checks for restoreCallingIdentity() not in the finally block issue + * - Removes token from tokensMap if token is within the scope of the method + */ + private fun checkRestoreCallingIdentityCall(node: UCallExpression) { + val arg = node.valueArguments[0] as? USimpleNameReferenceExpression ?: return val variableName = arg.identifier val originalScope = tokensMap[variableName]?.scope ?: return val psi = arg.sourcePsi ?: return @@ -174,26 +203,31 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { // token declaration. If not within the scope, no action is needed because the token is // irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity() if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return - // - We do not report "restore identity call not in finally" issue when there is no + // We do not report "restore identity call not in finally" issue when there is no // finally block because that case is already handled by "clear identity call not // followed by try-finally" issue - // - UCallExpression can be a child of UQualifiedReferenceExpression, i.e. - // receiver.selector, so to get the call's immediate parent we need to get the topmost - // parent qualified reference expression and access its parent if (tokensMap[variableName]?.finallyBlock != null && - skipParenthesizedExprUp(node.getQualifiedParentOrThis().uastParent) != - tokensMap[variableName]?.finallyBlock) { + getFirstNonQualifiedParent(node) != + tokensMap[variableName]?.finallyBlock + ) { context.report( - ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, - context.getLocation(node), - getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName) + ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + context.getLocation(node), + getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName) ) } tokensMap.remove(variableName) } + private fun getFirstNonQualifiedParent(expression: UCallExpression): UElement? { + // UCallExpression can be a child of UQualifiedReferenceExpression, i.e. + // receiver.selector, so to get the call's immediate parent we need to get the topmost + // parent qualified reference expression and access its parent + return skipParenthesizedExprUp(expression.getQualifiedParentOrThis().uastParent) + } + private fun isCallerAwareMethod(expression: UCallExpression): Boolean = - callerAwareMethods.any { method -> isMethodCall(expression, method) } + callerAwareMethods.any { method -> isMethodCall(expression, method) } private fun isMethodCall( expression: UCallExpression, @@ -201,12 +235,12 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { ): Boolean { val psiMethod = expression.resolve() ?: return false return psiMethod.getName() == method.methodName && - context.evaluator.methodMatches( - psiMethod, - method.className, - /* allowInherit */ true, - *method.args - ) + context.evaluator.methodMatches( + psiMethod, + method.className, + /* allowInherit */ true, + *method.args + ) } /** @@ -255,7 +289,7 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { return declarations[indexInDeclarations + 1] } val enclosingBlock = node - .getParentOfType<UBlockExpression>(strict = true) ?: return null + .getParentOfType<UBlockExpression>(strict = true) ?: return null val expressions = enclosingBlock.expressions val indexInBlock = expressions.indexOf(declarationsExpression as UElement) return if (indexInBlock == -1) null else expressions.getOrNull(indexInBlock + 1) @@ -301,12 +335,12 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { secondCallTokenLocation: Location ): Location { return cloneLocation(secondCallTokenLocation) - .withSecondary( - cloneLocation(firstCallToken.location), - getIncidentMessageNestedClearIdentityCallsSecondary( - firstCallToken.variableName - ) + .withSecondary( + cloneLocation(firstCallToken.location), + getIncidentMessageNestedClearIdentityCallsSecondary( + firstCallToken.variableName ) + ) } private fun cloneLocation(location: Location): Location { @@ -347,20 +381,20 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { const val CLASS_USER_HANDLE = "android.os.UserHandle" private val callerAwareMethods = listOf( - Method.BINDER_GET_CALLING_PID, - Method.BINDER_GET_CALLING_UID, - Method.BINDER_GET_CALLING_UID_OR_THROW, - Method.BINDER_GET_CALLING_USER_HANDLE, - Method.USER_HANDLE_GET_CALLING_APP_ID, - Method.USER_HANDLE_GET_CALLING_USER_ID + Method.BINDER_GET_CALLING_PID, + Method.BINDER_GET_CALLING_UID, + Method.BINDER_GET_CALLING_UID_OR_THROW, + Method.BINDER_GET_CALLING_USER_HANDLE, + Method.USER_HANDLE_GET_CALLING_APP_ID, + Method.USER_HANDLE_GET_CALLING_USER_ID ) /** Issue: unused token from Binder.clearCallingIdentity() */ @JvmField val ISSUE_UNUSED_TOKEN: Issue = Issue.create( - id = "UnusedTokenOfOriginalCallingIdentity", - briefDescription = "Unused token of Binder.clearCallingIdentity()", - explanation = """ + id = "UnusedTokenOfOriginalCallingIdentity", + briefDescription = "Unused token of Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but have not used the returned token to \ restore the identity. @@ -370,26 +404,26 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { `token` is the result of `Binder.clearCallingIdentity()` """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageUnusedToken(variableName: String) = "`$variableName` has " + - "not been used to restore the calling identity. Introduce a `try`-`finally` " + - "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " + - "in `finally` or remove `$variableName`." + "not been used to restore the calling identity. Introduce a `try`-`finally` " + + "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " + + "in `finally` or remove `$variableName`." /** Issue: non-final token from Binder.clearCallingIdentity() */ @JvmField val ISSUE_NON_FINAL_TOKEN: Issue = Issue.create( - id = "NonFinalTokenOfOriginalCallingIdentity", - briefDescription = "Non-final token of Binder.clearCallingIdentity()", - explanation = """ + id = "NonFinalTokenOfOriginalCallingIdentity", + briefDescription = "Non-final token of Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but have not made the returned token `final`. @@ -397,47 +431,47 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { which can cause problems when restoring the identity with \ `Binder.restoreCallingIdentity(token)`. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageNonFinalToken(variableName: String) = "`$variableName` is " + - "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " + - "`$variableName`." + "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " + + "`$variableName`." /** Issue: nested calls of Binder.clearCallingIdentity() */ @JvmField val ISSUE_NESTED_CLEAR_IDENTITY_CALLS: Issue = Issue.create( - id = "NestedClearCallingIdentityCalls", - briefDescription = "Nested calls of Binder.clearCallingIdentity()", - explanation = """ + id = "NestedClearCallingIdentityCalls", + briefDescription = "Nested calls of Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()` twice without restoring identity with the \ result of the first call. Make sure to restore the identity after each clear identity call. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageNestedClearIdentityCallsPrimary( firstCallVariableName: String, secondCallVariableName: String ): String = "The calling identity has already been cleared and returned into " + - "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " + - "restoring the calling identity with " + - "`Binder.restoreCallingIdentity($firstCallVariableName)`." + "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " + + "restoring the calling identity with " + + "`Binder.restoreCallingIdentity($firstCallVariableName)`." private fun getIncidentMessageNestedClearIdentityCallsSecondary( firstCallVariableName: String @@ -446,10 +480,10 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { /** Issue: Binder.clearCallingIdentity() is not followed by `try-finally` statement */ @JvmField val ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY: Issue = Issue.create( - id = "ClearIdentityCallNotFollowedByTryFinally", - briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " + - "statement", - explanation = """ + id = "ClearIdentityCallNotFollowedByTryFinally", + briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " + + "statement", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but the next statement is not a `try` \ statement. @@ -472,30 +506,30 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { code with your identity that was originally intended to run with the calling \ application's identity. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageClearIdentityCallNotFollowedByTryFinally( variableName: String ): String = "You cleared the calling identity and returned the result into " + - "`$variableName`, but the next statement is not a `try`-`finally` statement. " + - "Define a `try`-`finally` block after `$variableName` declaration to ensure a " + - "safe restore of the calling identity by calling " + - "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " + - "of the `finally` block." + "`$variableName`, but the next statement is not a `try`-`finally` statement. " + + "Define a `try`-`finally` block after `$variableName` declaration to ensure a " + + "safe restore of the calling identity by calling " + + "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " + + "of the `finally` block." /** Issue: Binder.restoreCallingIdentity() is not in finally block */ @JvmField val ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK: Issue = Issue.create( - id = "RestoreIdentityCallNotInFinallyBlock", - briefDescription = "Binder.restoreCallingIdentity() is not in finally block", - explanation = """ + id = "RestoreIdentityCallNotInFinallyBlock", + briefDescription = "Binder.restoreCallingIdentity() is not in finally block", + explanation = """ You are restoring the original calling identity with \ `Binder.restoreCallingIdentity()`, but the call is not an immediate child of \ the `finally` block of the `try` statement. @@ -516,28 +550,28 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { `finally` block, you may run code with your identity that was originally \ intended to run with the calling application's identity. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageRestoreIdentityCallNotInFinallyBlock( variableName: String ): String = "`Binder.restoreCallingIdentity($variableName)` is not an immediate child of " + - "the `finally` block of the try statement after `$variableName` declaration. " + - "Surround the call with `finally` block and call it unconditionally." + "the `finally` block of the try statement after `$variableName` declaration. " + + "Surround the call with `finally` block and call it unconditionally." /** Issue: Use of caller-aware methods after Binder.clearCallingIdentity() */ @JvmField val ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY: Issue = Issue.create( - id = "UseOfCallerAwareMethodsWithClearedIdentity", - briefDescription = "Use of caller-aware methods after " + - "Binder.clearCallingIdentity()", - explanation = """ + id = "UseOfCallerAwareMethodsWithClearedIdentity", + briefDescription = "Use of caller-aware methods after " + + "Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but used one of the methods below before \ restoring the identity. These methods will use your own identity instead of \ @@ -556,22 +590,59 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { UserHandle.getCallingUserId() ``` """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( variableName: String, methodName: String ): String = "You cleared the original identity with `Binder.clearCallingIdentity()` " + - "and returned into `$variableName`, so `$methodName` will be using your own " + - "identity instead of the caller's. Either explicitly query your own identity or " + - "move it after restoring the identity with " + - "`Binder.restoreCallingIdentity($variableName)`." + "and returned into `$variableName`, so `$methodName` will be using your own " + + "identity instead of the caller's. Either explicitly query your own identity or " + + "move it after restoring the identity with " + + "`Binder.restoreCallingIdentity($variableName)`." + + /** Issue: Result of Binder.clearCallingIdentity() is not stored in a variable */ + @JvmField + val ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE: Issue = Issue.create( + id = "ResultOfClearIdentityCallNotStoredInVariable", + briefDescription = "Result of Binder.clearCallingIdentity() is not stored in a " + + "variable", + explanation = """ + You cleared the original calling identity with \ + `Binder.clearCallingIdentity()`, but did not store the result of the method \ + call in a variable. You need to store the result in a variable and restore it later. + + Use the following pattern for running operations with your own identity: + + ``` + final long token = Binder.clearCallingIdentity(); + try { + // Code using your own identity + } finally { + Binder.restoreCallingIdentity(token); + } + ``` + """, + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + private fun getIncidentMessageResultOfClearIdentityCallNotStoredInVariable( + methodName: String + ): String = "You cleared the original identity with `$methodName` but did not store the " + + "result in a variable. You need to store the result in a variable and restore it " + + "later." } } diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt index fe567da7c017..fe567da7c017 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt new file mode 100644 index 000000000000..48540b1da565 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt @@ -0,0 +1,515 @@ +/* + * 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. + */ + +package com.google.android.lint + +import com.android.tools.lint.client.api.UastParser +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Context +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.android.tools.lint.detector.api.interprocedural.CallGraph +import com.android.tools.lint.detector.api.interprocedural.CallGraphResult +import com.android.tools.lint.detector.api.interprocedural.searchForPaths +import com.intellij.psi.PsiAnonymousClass +import com.intellij.psi.PsiMethod +import java.util.LinkedList +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter +import org.jetbrains.uast.USimpleNameReferenceExpression +import org.jetbrains.uast.visitor.AbstractUastVisitor + +/** + * A lint checker to detect potential package visibility issues for system's APIs. APIs working + * in the system_server and taking the package name as a parameter may have chance to reveal + * package existence status on the device, and break the + * <a href="https://developer.android.com/about/versions/11/privacy/package-visibility"> + * Package Visibility</a> that we introduced in Android 11. + * <p> + * Take an example of the API `boolean setFoo(String packageName)`, a malicious app may have chance + * to detect package existence state on the device from the result of the API, if there is no + * package visibility filtering rule or uid identify checks applying to the parameter of the + * package name. + */ +class PackageVisibilityDetector : Detector(), SourceCodeScanner { + + // Enables call graph analysis + override fun isCallGraphRequired(): Boolean = true + + override fun analyzeCallGraph( + context: Context, + callGraph: CallGraphResult + ) { + val systemServerApiNodes = callGraph.callGraph.nodes.filter(::isSystemServerApi) + val sinkMethodNodes = callGraph.callGraph.nodes.filter { + // TODO(b/228285232): Remove enforce permission sink methods + isNodeInList(it, ENFORCE_PERMISSION_METHODS) || isNodeInList(it, APPOPS_METHODS) + } + val parser = context.client.getUastParser(context.project) + analyzeApisContainPackageNameParameters( + context, parser, systemServerApiNodes, sinkMethodNodes) + } + + /** + * Looking for API contains package name parameters, report the lint issue if the API does not + * invoke any sink methods. + */ + private fun analyzeApisContainPackageNameParameters( + context: Context, + parser: UastParser, + systemServerApiNodes: List<CallGraph.Node>, + sinkMethodNodes: List<CallGraph.Node> + ) { + for (apiNode in systemServerApiNodes) { + val apiMethod = apiNode.getUMethod() ?: continue + val pkgNameParamIndexes = apiMethod.uastParameters.mapIndexedNotNull { index, param -> + if (Parameter(param) in PACKAGE_NAME_PATTERNS && apiNode.isArgumentInUse(index)) { + index + } else { + null + } + }.takeIf(List<Int>::isNotEmpty) ?: continue + + for (pkgNameParamIndex in pkgNameParamIndexes) { + // Trace the call path of the method's argument, pass the lint checks if a sink + // method is found + if (traceArgumentCallPath( + apiNode, pkgNameParamIndex, PACKAGE_NAME_SINK_METHOD_LIST)) { + continue + } + // Pass the check if one of the sink methods is invoked + if (hasValidPath( + searchForPaths( + sources = listOf(apiNode), + isSink = { it in sinkMethodNodes }, + getNeighbors = { node -> node.edges.map { it.node!! } } + ) + ) + ) continue + + // Report issue + val reportElement = apiMethod.uastParameters[pkgNameParamIndex] as UElement + val location = parser.createLocation(reportElement) + context.report( + ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, + location, + getMsgPackageNameNoPackageVisibilityFilters(apiMethod, pkgNameParamIndex) + ) + } + } + } + + /** + * Returns {@code true} if the method associated with the given node is a system server's + * public API that extends from Stub class. + */ + private fun isSystemServerApi( + node: CallGraph.Node + ): Boolean { + val method = node.getUMethod() ?: return false + if (!method.hasModifierProperty("public") || + method.uastBody == null || + method.containingClass is PsiAnonymousClass) { + return false + } + val className = method.containingClass?.qualifiedName ?: return false + if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) { + return false + } + return (method.containingClass ?: return false).supers + .filter { it.name == CLASS_STUB } + .filter { it.qualifiedName !in BYPASS_STUBS } + .any { it.findMethodBySignature(method, /* checkBases */ true) != null } + } + + /** + * Returns {@code true} if the list contains the node of the call graph. + */ + private fun isNodeInList( + node: CallGraph.Node, + filters: List<Method> + ): Boolean { + val method = node.getUMethod() ?: return false + return Method(method) in filters + } + + /** + * Trace the call paths of the argument of the method in the start entry. Return {@code true} + * if one of methods in the sink call list is invoked. + * Take an example of the call path: + * foo(packageName) -> a(packageName) -> b(packageName) -> filterAppAccess() + * It returns {@code true} if the filterAppAccess() is in the sink call list. + */ + private fun traceArgumentCallPath( + apiNode: CallGraph.Node, + pkgNameParamIndex: Int, + sinkList: List<Method> + ): Boolean { + val startEntry = TraceEntry(apiNode, pkgNameParamIndex) + val traceQueue = LinkedList<TraceEntry>().apply { add(startEntry) } + val allVisits = mutableSetOf<TraceEntry>().apply { add(startEntry) } + while (!traceQueue.isEmpty()) { + val entry = traceQueue.poll() + val entryNode = entry.node + val entryMethod = entryNode.getUMethod() ?: continue + val entryArgumentName = entryMethod.uastParameters[entry.argumentIndex].name + for (outEdge in entryNode.edges) { + val outNode = outEdge.node ?: continue + val outMethod = outNode.getUMethod() ?: continue + val outArgumentIndex = + outEdge.call?.findArgumentIndex( + entryArgumentName, outMethod.uastParameters.size) + val sinkMethod = findInSinkList(outMethod, sinkList) + if (sinkMethod == null) { + if (outArgumentIndex == null) { + // Path is not relevant to the sink method and argument + continue + } + // Path is relevant to the argument, add a new trace entry if never visit before + val newEntry = TraceEntry(outNode, outArgumentIndex) + if (newEntry !in allVisits) { + traceQueue.add(newEntry) + allVisits.add(newEntry) + } + continue + } + if (sinkMethod.matchArgument && outArgumentIndex == null) { + // The sink call is required to match the argument, but not found + continue + } + if (sinkMethod.checkCaller && + entryMethod.isInClearCallingIdentityScope(outEdge.call!!)) { + // The sink call is in the scope of Binder.clearCallingIdentify + continue + } + // A sink method is matched + return true + } + } + return false + } + + /** + * Returns the UMethod associated with the given node of call graph. + */ + private fun CallGraph.Node.getUMethod(): UMethod? = this.target.element as? UMethod + + /** + * Returns the system module name (e.g. com.android.server.pm) of the method of the + * call graph node. + */ + private fun CallGraph.Node.getModuleName(): String? { + val method = getUMethod() ?: return null + val className = method.containingClass?.qualifiedName ?: return null + if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) { + return null + } + val dotPos = className.indexOf(".", SYSTEM_PACKAGE_PREFIX.length) + if (dotPos == -1) { + return SYSTEM_PACKAGE_PREFIX + } + return className.substring(0, dotPos) + } + + /** + * Return {@code true} if the argument in the method's body is in-use. + */ + private fun CallGraph.Node.isArgumentInUse(argIndex: Int): Boolean { + val method = getUMethod() ?: return false + val argumentName = method.uastParameters[argIndex].name + var foundArg = false + val methodVisitor = object : AbstractUastVisitor() { + override fun visitSimpleNameReferenceExpression( + node: USimpleNameReferenceExpression + ): Boolean { + if (node.identifier == argumentName) { + foundArg = true + } + return true + } + } + method.uastBody?.accept(methodVisitor) + return foundArg + } + + /** + * Given an argument name, returns the index of argument in the call expression. + */ + private fun UCallExpression.findArgumentIndex( + argumentName: String, + parameterSize: Int + ): Int? { + if (valueArgumentCount == 0 || parameterSize == 0) { + return null + } + var match = false + val argVisitor = object : AbstractUastVisitor() { + override fun visitSimpleNameReferenceExpression( + node: USimpleNameReferenceExpression + ): Boolean { + if (node.identifier == argumentName) { + match = true + } + return true + } + override fun visitCallExpression(node: UCallExpression): Boolean { + return true + } + } + valueArguments.take(parameterSize).forEachIndexed { index, argument -> + argument.accept(argVisitor) + if (match) { + return index + } + } + return null + } + + /** + * Given a UMethod, returns a method from the sink method list. + */ + private fun findInSinkList( + uMethod: UMethod, + sinkCallList: List<Method> + ): Method? { + return sinkCallList.find { + it == Method(uMethod) || + it == Method(uMethod.containingClass?.qualifiedName ?: "", "*") + } + } + + /** + * Returns {@code true} if the call expression is in the scope of the + * Binder.clearCallingIdentify. + */ + private fun UMethod.isInClearCallingIdentityScope(call: UCallExpression): Boolean { + var isInScope = false + val methodVisitor = object : AbstractUastVisitor() { + private var clearCallingIdentity = 0 + override fun visitCallExpression(node: UCallExpression): Boolean { + if (call == node && clearCallingIdentity != 0) { + isInScope = true + return true + } + val visitMethod = Method(node.resolve() ?: return false) + if (visitMethod == METHOD_CLEAR_CALLING_IDENTITY) { + clearCallingIdentity++ + } else if (visitMethod == METHOD_RESTORE_CALLING_IDENTITY) { + clearCallingIdentity-- + } + return false + } + } + accept(methodVisitor) + return isInScope + } + + /** + * Checks the module name of the start node and the last node that invokes the sink method + * (e.g. checkPermission) in a path, returns {@code true} if one of the paths has the same + * module name for both nodes. + */ + private fun hasValidPath(paths: Collection<List<CallGraph.Node>>): Boolean { + for (pathNodes in paths) { + if (pathNodes.size < VALID_CALL_PATH_NODES_SIZE) { + continue + } + val startModule = pathNodes[0].getModuleName() ?: continue + val lastCallModule = pathNodes[pathNodes.size - 2].getModuleName() ?: continue + if (startModule == lastCallModule) { + return true + } + } + return false + } + + /** + * A data class to represent the method. + */ + private data class Method( + val clazz: String, + val name: String + ) { + // Used by traceArgumentCallPath to indicate that the method is required to match the + // argument name + var matchArgument = true + + // Used by traceArgumentCallPath to indicate that the method is required to check whether + // the Binder.clearCallingIdentity is invoked. + var checkCaller = false + + constructor( + clazz: String, + name: String, + matchArgument: Boolean = true, + checkCaller: Boolean = false + ) : this(clazz, name) { + this.matchArgument = matchArgument + this.checkCaller = checkCaller + } + + constructor( + method: PsiMethod + ) : this(method.containingClass?.qualifiedName ?: "", method.name) + + constructor( + method: com.google.android.lint.model.Method + ) : this(method.clazz, method.name) + } + + /** + * A data class to represent the parameter of the method. The parameter name is converted to + * lower case letters for comparison. + */ + private data class Parameter private constructor( + val typeName: String, + val parameterName: String + ) { + constructor(uParameter: UParameter) : this( + uParameter.type.canonicalText, + uParameter.name.lowercase() + ) + + companion object { + fun create(typeName: String, parameterName: String) = + Parameter(typeName, parameterName.lowercase()) + } + } + + /** + * A data class wraps a method node of the call graph and an index that indicates an + * argument of the method to record call trace information. + */ + private data class TraceEntry( + val node: CallGraph.Node, + val argumentIndex: Int + ) + + companion object { + private const val SYSTEM_PACKAGE_PREFIX = "com.android.server." + // A valid call path list needs to contain a start node and a sink node + private const val VALID_CALL_PATH_NODES_SIZE = 2 + + private const val CLASS_STRING = "java.lang.String" + private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager" + private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager" + private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager" + private const val CLASS_BINDER = "android.os.Binder" + private const val CLASS_PACKAGE_MANAGER_INTERNAL = + "android.content.pm.PackageManagerInternal" + + // Patterns of package name parameter + private val PACKAGE_NAME_PATTERNS = setOf( + Parameter.create(CLASS_STRING, "packageName"), + Parameter.create(CLASS_STRING, "callingPackage"), + Parameter.create(CLASS_STRING, "callingPackageName"), + Parameter.create(CLASS_STRING, "pkgName"), + Parameter.create(CLASS_STRING, "callingPkg"), + Parameter.create(CLASS_STRING, "pkg") + ) + + // Package manager APIs + private val PACKAGE_NAME_SINK_METHOD_LIST = listOf( + Method(CLASS_PACKAGE_MANAGER_INTERNAL, "filterAppAccess", matchArgument = false), + Method(CLASS_PACKAGE_MANAGER_INTERNAL, "canQueryPackage"), + Method(CLASS_PACKAGE_MANAGER_INTERNAL, "isSameApp"), + Method(CLASS_PACKAGE_MANAGER, "*", checkCaller = true), + Method(CLASS_IPACKAGE_MANAGER, "*", checkCaller = true), + Method(CLASS_PACKAGE_MANAGER, "getPackagesForUid", matchArgument = false), + Method(CLASS_IPACKAGE_MANAGER, "getPackagesForUid", matchArgument = false) + ) + + // AppOps APIs which include uid and package visibility filters checks + private val APPOPS_METHODS = listOf( + Method(CLASS_APPOPS_MANAGER, "noteOp"), + Method(CLASS_APPOPS_MANAGER, "noteOpNoThrow"), + Method(CLASS_APPOPS_MANAGER, "noteOperation"), + Method(CLASS_APPOPS_MANAGER, "noteProxyOp"), + Method(CLASS_APPOPS_MANAGER, "noteProxyOpNoThrow"), + Method(CLASS_APPOPS_MANAGER, "startOp"), + Method(CLASS_APPOPS_MANAGER, "startOpNoThrow"), + Method(CLASS_APPOPS_MANAGER, "FinishOp"), + Method(CLASS_APPOPS_MANAGER, "finishProxyOp"), + Method(CLASS_APPOPS_MANAGER, "checkPackage") + ) + + // Enforce permission APIs + private val ENFORCE_PERMISSION_METHODS = + com.google.android.lint.ENFORCE_PERMISSION_METHODS + .map(PackageVisibilityDetector::Method) + + private val BYPASS_STUBS = listOf( + "android.content.pm.IPackageDataObserver.Stub", + "android.content.pm.IPackageDeleteObserver.Stub", + "android.content.pm.IPackageDeleteObserver2.Stub", + "android.content.pm.IPackageInstallObserver2.Stub", + "com.android.internal.app.IAppOpsCallback.Stub", + + // TODO(b/228285637): Do not bypass PackageManagerService API + "android.content.pm.IPackageManager.Stub", + "android.content.pm.IPackageManagerNative.Stub" + ) + + private val METHOD_CLEAR_CALLING_IDENTITY = + Method(CLASS_BINDER, "clearCallingIdentity") + private val METHOD_RESTORE_CALLING_IDENTITY = + Method(CLASS_BINDER, "restoreCallingIdentity") + + private fun getMsgPackageNameNoPackageVisibilityFilters( + method: UMethod, + argumentIndex: Int + ): String = "Api: ${method.name} contains a package name parameter: " + + "${method.uastParameters[argumentIndex].name} does not apply " + + "package visibility filtering rules." + + private val EXPLANATION = """ + APIs working in the system_server and taking the package name as a parameter may have + chance to reveal package existence status on the device, and break the package + visibility that we introduced in Android 11. + (https://developer.android.com/about/versions/11/privacy/package-visibility) + + Take an example of the API `boolean setFoo(String packageName)`, a malicious app may + have chance to get package existence state on the device from the result of the API, + if there is no package visibility filtering rule or uid identify checks applying to + the parameter of the package name. + + To resolve it, you could apply package visibility filtering rules to the package name + via PackageManagerInternal.filterAppAccess API, before starting to use the package name. + If the parameter is a calling package name, use the PackageManager API such as + PackageManager.getPackagesForUid to verify the calling identify. + """ + + val ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS = Issue.create( + id = "ApiMightLeakAppVisibility", + briefDescription = "Api takes package name parameter doesn't apply " + + "package visibility filters", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 1, + severity = Severity.WARNING, + implementation = Implementation( + PackageVisibilityDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt new file mode 100644 index 000000000000..e12ec3d4a77c --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt @@ -0,0 +1,199 @@ +/* + * 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. + */ + +package com.google.android.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.android.tools.lint.detector.api.getUMethod +import com.intellij.psi.PsiType +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.UReturnExpression +import org.jetbrains.uast.getContainingUMethod + +/** + * Stops incorrect usage of {@link PermissionMethod} + * TODO: add tests once re-enabled (b/240445172, b/247542171) + */ +class PermissionMethodDetector : Detector(), SourceCodeScanner { + + override fun getApplicableUastTypes(): List<Class<out UElement>> = + listOf(UAnnotation::class.java, UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = + PermissionMethodHandler(context) + + private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (hasPermissionMethodAnnotation(node)) return + if (onlyCallsPermissionMethod(node)) { + val location = context.getLocation(node.javaPsi.modifierList) + val fix = fix() + .annotate(ANNOTATION_PERMISSION_METHOD) + .range(location) + .autoFix() + .build() + + context.report( + ISSUE_CAN_BE_PERMISSION_METHOD, + location, + "Annotate method with @PermissionMethod", + fix + ) + } + } + + override fun visitAnnotation(node: UAnnotation) { + if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return + val method = node.getContainingUMethod() ?: return + + if (!isPermissionMethodReturnType(method)) { + context.report( + ISSUE_PERMISSION_METHOD_USAGE, + context.getLocation(node), + """ + Methods annotated with `@PermissionMethod` should return `void`, \ + `boolean`, or `@PackageManager.PermissionResult int`." + """.trimIndent() + ) + } + + if (method.returnType == PsiType.INT && + method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) } + ) { + context.report( + ISSUE_PERMISSION_METHOD_USAGE, + context.getLocation(node), + """ + Methods annotated with `@PermissionMethod` that return `int` should \ + also be annotated with `@PackageManager.PermissionResult.`" + """.trimIndent() + ) + } + } + } + + companion object { + + private val EXPLANATION_PERMISSION_METHOD_USAGE = """ + `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \ + Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \ + `void` and potentially throw `SecurityException`. + """.trimIndent() + + @JvmField + val ISSUE_PERMISSION_METHOD_USAGE = Issue.create( + id = "PermissionMethodUsage", + briefDescription = "@PermissionMethod used incorrectly", + explanation = EXPLANATION_PERMISSION_METHOD_USAGE, + category = Category.CORRECTNESS, + priority = 5, + severity = Severity.ERROR, + implementation = Implementation( + PermissionMethodDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = true + ) + + private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """ + Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \ + be annotated with @PermissionMethod. For example: + ``` + void wrapperHelper() { + // Context.enforceCallingPermission is annotated with @PermissionMethod + context.enforceCallingPermission(SOME_PERMISSION) + } + ``` + """.trimIndent() + + @JvmField + val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create( + id = "CanBePermissionMethod", + briefDescription = "Method can be annotated with @PermissionMethod", + explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + PermissionMethodDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = false + ) + + private fun isPermissionMethodReturnType(method: UMethod): Boolean = + listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType) + + /** + * Identifies methods that... + * DO call other methods annotated with @PermissionMethod + * DO NOT do anything else + */ + private fun onlyCallsPermissionMethod(method: UMethod): Boolean { + val body = method.uastBody as? UBlockExpression ?: return false + if (body.expressions.isEmpty()) return false + for (expression in body.expressions) { + when (expression) { + is UQualifiedReferenceExpression -> { + if (!isPermissionMethodCall(expression.selector)) return false + } + is UReturnExpression -> { + if (!isPermissionMethodCall(expression.returnExpression)) return false + } + is UCallExpression -> { + if (!isPermissionMethodCall(expression)) return false + } + is UIfExpression -> { + if (expression.thenExpression !is UReturnExpression) return false + if (!isPermissionMethodCall(expression.condition)) return false + } + else -> return false + } + } + return true + } + + private fun isPermissionMethodCall(expression: UExpression?): Boolean { + return when (expression) { + is UQualifiedReferenceExpression -> + return isPermissionMethodCall(expression.selector) + is UCallExpression -> { + val calledMethod = expression.resolve()?.getUMethod() ?: return false + return hasPermissionMethodAnnotation(calledMethod) + } + else -> false + } + } + + private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations + .any { it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) } + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt new file mode 100644 index 000000000000..06c098df385d --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt @@ -0,0 +1,229 @@ +/* + * 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. + */ + +package com.google.android.lint.parcel + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Location +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiCallExpression +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiIntersectionType +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeParameter +import com.intellij.psi.PsiWildcardType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UVariable + +/** + * Subclass this class and override {@link #getBoundingClass} to report an unsafe Parcel API issue + * with a fix that migrates towards the new safer API by appending an argument in the form of + * {@code com.package.ItemType.class} coming from the result of the overridden method. + */ +abstract class CallMigrator( + val method: Method, + private val rejects: Set<String> = emptySet(), +) { + open fun report(context: JavaContext, call: UCallExpression, method: PsiMethod) { + val location = context.getLocation(call) + val itemType = filter(getBoundingClass(context, call, method)) + val fix = (itemType as? PsiClassType)?.let { type -> + getParcelFix(location, this.method.name, getArgumentSuffix(type)) + } + val message = "Unsafe `${this.method.className}.${this.method.name}()` API usage" + context.report(SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, call, location, message, fix) + } + + protected open fun getArgumentSuffix(type: PsiClassType) = + ", ${type.rawType().canonicalText}.class" + + protected open fun getBoundingClass( + context: JavaContext, + call: UCallExpression, + method: PsiMethod, + ): PsiType? = null + + protected fun getItemType(type: PsiType, container: String): PsiClassType? { + val supers = getParentTypes(type).mapNotNull { it as? PsiClassType } + val containerType = supers.firstOrNull { it.rawType().canonicalText == container } + ?: return null + val itemType = containerType.parameters.getOrNull(0) ?: return null + // TODO: Expand to other types, see PsiTypeVisitor + return when (itemType) { + is PsiClassType -> itemType + is PsiWildcardType -> itemType.bound as PsiClassType + else -> null + } + } + + /** + * Tries to obtain the type expected by the "receiving" end given a certain {@link UElement}. + * + * This could be an assignment, an argument passed to a method call, to a constructor call, a + * type cast, etc. If no receiving end is found, the type of the UExpression itself is returned. + */ + protected fun getReceivingType(expression: UElement): PsiType? { + val parent = expression.uastParent + var type = when (parent) { + is UCallExpression -> { + val i = parent.valueArguments.indexOf(expression) + val psiCall = parent.sourcePsi as? PsiCallExpression ?: return null + val typeSubstitutor = psiCall.resolveMethodGenerics().substitutor + val method = psiCall.resolveMethod()!! + method.getSignature(typeSubstitutor).parameterTypes[i] + } + is UVariable -> parent.type + is UExpression -> parent.getExpressionType() + else -> null + } + if (type == null && expression is UExpression) { + type = expression.getExpressionType() + } + return type + } + + protected fun filter(type: PsiType?): PsiType? { + // It's important that PsiIntersectionType case is above the one that check the type in + // rejects, because for intersect types, the canonicalText is one of the terms. + if (type is PsiIntersectionType) { + return type.conjuncts.mapNotNull(this::filter).firstOrNull() + } + if (type == null || type.canonicalText in rejects) { + return null + } + if (type is PsiClassType && type.resolve() is PsiTypeParameter) { + return null + } + return type + } + + private fun getParentTypes(type: PsiType): Set<PsiType> = + type.superTypes.flatMap(::getParentTypes).toSet() + type + + protected fun getParcelFix(location: Location, method: String, arguments: String) = + LintFix + .create() + .name("Migrate to safer Parcel.$method() API") + .replace() + .range(location) + .pattern("$method\\s*\\(((?:.|\\n)*)\\)") + .with("\\k<1>$arguments") + .autoFix() + .build() +} + +/** + * This class derives the type to be appended by inferring the generic type of the {@code container} + * type (eg. "java.util.List") of the {@code argument}-th argument. + */ +class ContainerArgumentMigrator( + method: Method, + private val argument: Int, + private val container: String, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + val firstParamType = call.valueArguments[argument].getExpressionType() ?: return null + return getItemType(firstParamType, container)!! + } + + /** + * We need to insert a casting construct in the class parameter. For example: + * (Class<Foo<Bar>>) (Class<?>) Foo.class. + * This is needed for when the arguments of the conflict (eg. when there is List<Foo<Bar>> and + * class type is Class<Foo?). + */ + override fun getArgumentSuffix(type: PsiClassType): String { + if (type.parameters.isNotEmpty()) { + val rawType = type.rawType() + return ", (Class<${type.canonicalText}>) (Class<?>) ${rawType.canonicalText}.class" + } + return super.getArgumentSuffix(type) + } +} + +/** + * This class derives the type to be appended by inferring the generic type of the {@code container} + * type (eg. "java.util.List") of the return type of the method. + */ +class ContainerReturnMigrator( + method: Method, + private val container: String, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + val type = getReceivingType(call.uastParent!!) ?: return null + return getItemType(type, container) + } +} + +/** + * This class derives the type to be appended by inferring the expected type for the method result. + */ +class ReturnMigrator( + method: Method, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + return getReceivingType(call.uastParent!!) + } +} + +/** + * This class appends the class loader and the class object by deriving the type from the method + * result. + */ +class ReturnMigratorWithClassLoader( + method: Method, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + return getReceivingType(call.uastParent!!) + } + + override fun getArgumentSuffix(type: PsiClassType): String = + "${type.rawType().canonicalText}.class.getClassLoader(), " + + "${type.rawType().canonicalText}.class" + +} + +/** + * This class derives the type to be appended by inferring the expected array type + * for the method result. + */ +class ArrayReturnMigrator( + method: Method, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + val type = getReceivingType(call.uastParent!!) + return (type as? PsiArrayType)?.componentType + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt new file mode 100644 index 000000000000..0826e8e74431 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package com.google.android.lint.parcel + +data class Method( + val params: List<String>, + val clazz: String, + val name: String, + val parameters: List<String> +) { + constructor( + clazz: String, + name: String, + parameters: List<String> + ) : this( + listOf(), clazz, name, parameters + ) + + val signature: String + get() { + val prefix = if (params.isEmpty()) "" else "${params.joinToString(", ", "<", ">")} " + return "$prefix$clazz.$name(${parameters.joinToString()})" + } + + val className: String by lazy { + clazz.split(".").last() + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt new file mode 100644 index 000000000000..f92826316be4 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt @@ -0,0 +1,126 @@ +/* + * 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. + */ + +package com.google.android.lint.parcel + +import com.android.tools.lint.detector.api.* +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiSubstitutor +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeParameter +import org.jetbrains.uast.UCallExpression +import java.util.* + +@Suppress("UnstableApiUsage") +class SaferParcelChecker : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List<String> = + MIGRATORS + .map(CallMigrator::method) + .map(Method::name) + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!isAtLeastT(context)) return + val signature = getSignature(method) + val migrator = MIGRATORS.firstOrNull { it.method.signature == signature } ?: return + migrator.report(context, node, method) + } + + private fun getSignature(method: PsiMethod): String { + val name = UastLintUtils.getQualifiedName(method) + val signature = method.getSignature(PsiSubstitutor.EMPTY) + val parameters = + signature.parameterTypes.joinToString(transform = PsiType::getCanonicalText) + val types = signature.typeParameters.map(PsiTypeParameter::getName) + val prefix = if (types.isEmpty()) "" else types.joinToString(", ", "<", ">") + " " + return "$prefix$name($parameters)" + } + + private fun isAtLeastT(context: Context): Boolean { + val project = if (context.isGlobalAnalysis()) context.mainProject else context.project + return project.isAndroidProject && project.minSdkVersion.featureLevel >= 33 + } + + companion object { + @JvmField + val ISSUE_UNSAFE_API_USAGE: Issue = Issue.create( + id = "UnsafeParcelApi", + briefDescription = "Use of unsafe deserialization API", + explanation = """ + You are using a deprecated deserialization API that doesn't accept the expected class as\ + a parameter. This means that unexpected classes could be instantiated and\ + unexpected code executed. + + Please migrate to the safer alternative that takes an extra Class<T> parameter. + """, + category = Category.SECURITY, + priority = 8, + severity = Severity.WARNING, + + implementation = Implementation( + SaferParcelChecker::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + // Parcel + private val PARCEL_METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf()) + private val PARCEL_METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader")) + + // Bundle + private val BUNDLE_METHOD_GET_SERIALIZABLE = Method("android.os.Bundle", "getSerializable", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE = Method(listOf("T"), "android.os.Bundle", "getParcelable", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST = Method(listOf("T"), "android.os.Bundle", "getParcelableArrayList", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY = Method("android.os.Bundle", "getParcelableArray", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY = Method(listOf("T"), "android.os.Bundle", "getSparseParcelableArray", listOf("java.lang.String")) + + // Intent + private val INTENT_METHOD_GET_SERIALIZABLE_EXTRA = Method("android.content.Intent", "getSerializableExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA = Method("android.content.Intent", "getParcelableArrayExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableArrayListExtra", listOf("java.lang.String")) + + // TODO: Write migrators for methods below + private val PARCEL_METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader")) + + private val MIGRATORS = listOf( + ReturnMigrator(PARCEL_METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")), + ContainerArgumentMigrator(PARCEL_METHOD_READ_LIST, 0, "java.util.List"), + ContainerReturnMigrator(PARCEL_METHOD_READ_ARRAY_LIST, "java.util.Collection"), + ContainerReturnMigrator(PARCEL_METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"), + ContainerArgumentMigrator(PARCEL_METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"), + ReturnMigratorWithClassLoader(PARCEL_METHOD_READ_SERIALIZABLE), + ArrayReturnMigrator(PARCEL_METHOD_READ_ARRAY, setOf("java.lang.Object")), + ArrayReturnMigrator(PARCEL_METHOD_READ_PARCELABLE_ARRAY, setOf("android.os.Parcelable")), + + ReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE, setOf("android.os.Parcelable")), + ContainerReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST, "java.util.Collection", setOf("android.os.Parcelable")), + ArrayReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY, setOf("android.os.Parcelable")), + ContainerReturnMigrator(BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY, "android.util.SparseArray", setOf("android.os.Parcelable")), + ReturnMigrator(BUNDLE_METHOD_GET_SERIALIZABLE, setOf("java.io.Serializable")), + + ReturnMigrator(INTENT_METHOD_GET_PARCELABLE_EXTRA, setOf("android.os.Parcelable")), + ContainerReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA, "java.util.Collection", setOf("android.os.Parcelable")), + ArrayReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA, setOf("android.os.Parcelable")), + ReturnMigrator(INTENT_METHOD_GET_SERIALIZABLE_EXTRA, setOf("java.io.Serializable")), + ) + } +} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt index e1a5c613dee1..d90f3e31baf9 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt @@ -27,12 +27,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { override fun getDetector(): Detector = CallingIdentityTokenDetector() override fun getIssues(): List<Issue> = listOf( - CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, - CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, - CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, - CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, - CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, - CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY + CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, + CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, + CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, + CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, + CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE ) override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) @@ -41,8 +42,8 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { fun testDoesNotDetectIssuesInCorrectScenario() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -62,22 +63,29 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } finally { restoreCallingIdentity(token3); } + final Long token4 = true ? Binder.clearCallingIdentity() : null; + try { + } finally { + if (token4 != null) { + restoreCallingIdentity(token4); + } + } } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expectClean() + .run() + .expectClean() } /** Unused token issue tests */ fun testDetectsUnusedTokens() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -101,12 +109,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token1 has not been used to \ restore the calling identity. Introduce a try-finally after the \ declaration and call Binder.restoreCallingIdentity(token1) in finally or \ @@ -127,13 +135,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 3 warnings """.addLineContinuation() - ) + ) } fun testDetectsUnusedTokensInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 { @@ -152,12 +160,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token has not been used to \ restore the calling identity. Introduce a try-finally after the \ declaration and call Binder.restoreCallingIdentity(token) in finally or \ @@ -166,13 +174,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } fun testDoesNotDetectUsedTokensInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 { @@ -192,17 +200,17 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expectClean() + .run() + .expectClean() } fun testDetectsUnusedTokensWithSimilarNamesInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 { @@ -220,12 +228,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token has not been used to \ restore the calling identity. Introduce a try-finally after the \ declaration and call Binder.restoreCallingIdentity(token) in finally or \ @@ -240,15 +248,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 2 warnings """.addLineContinuation() - ) + ) } /** Non-final token issue tests */ fun testDetectsNonFinalTokens() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -271,12 +279,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token1 is a non-final token from \ Binder.clearCallingIdentity(). Add final keyword to token1. \ [NonFinalTokenOfOriginalCallingIdentity] @@ -294,7 +302,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 3 warnings """.addLineContinuation() - ) + ) } /** Nested clearCallingIdentity() calls issue tests */ @@ -302,8 +310,8 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { fun testDetectsNestedClearCallingIdentityCalls() { // Pattern: clear - clear - clear - restore - restore - restore lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -326,12 +334,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:7: Warning: The calling identity has already \ been cleared and returned into token1. Move token2 declaration after \ restoring the calling identity with Binder.restoreCallingIdentity(token1). \ @@ -348,15 +356,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { src/test/pkg/TestClass1.java:5: Location of the token1 declaration. 0 errors, 2 warnings """.addLineContinuation() - ) + ) } /** clearCallingIdentity() not followed by try-finally issue tests */ fun testDetectsClearIdentityCallNotFollowedByTryFinally() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder{ @@ -397,12 +405,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: You cleared the calling identity \ and returned the result into token, but the next statement is not a \ try-finally statement. Define a try-finally block after token declaration \ @@ -445,15 +453,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 5 warnings """.addLineContinuation() - ) + ) } /** restoreCallingIdentity() call not in finally block issue tests */ fun testDetectsRestoreCallingIdentityCallNotInFinally() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -482,12 +490,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:10: Warning: \ Binder.restoreCallingIdentity(token) is not an immediate child of the \ finally block of the try statement after token declaration. Surround the c\ @@ -511,13 +519,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 3 warnings """.addLineContinuation() - ) + ) } fun testDetectsRestoreCallingIdentityCallNotInFinallyInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -560,12 +568,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:11: Warning: \ Binder.restoreCallingIdentity(token1) is not an immediate child of the \ finally block of the try statement after token1 declaration. Surround the \ @@ -596,15 +604,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 4 warnings """.addLineContinuation() - ) + ) } /** Use of caller-aware methods after clearCallingIdentity() issue tests */ fun testDetectsUseOfCallerAwareMethodsWithClearedIdentityIssuesInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; import android.os.UserHandle; @@ -632,12 +640,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:8: Warning: You cleared the original identity \ with Binder.clearCallingIdentity() and returned into token, so \ getCallingPid() will be using your own identity instead of the \ @@ -736,13 +744,58 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 12 warnings """.addLineContinuation() - ) + ) + } + + /** Result of Binder.clearCallingIdentity() is not stored in a variable issue tests */ + + fun testDetectsResultOfClearIdentityCallNotStoredInVariable() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 extends Binder { + private void testMethod() { + Binder.clearCallingIdentity(); + android.os.Binder.clearCallingIdentity(); + clearCallingIdentity(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: You cleared the original identity \ + with Binder.clearCallingIdentity() but did not store the result in a \ + variable. You need to store the result in a variable and restore it later. \ + [ResultOfClearIdentityCallNotStoredInVariable] + Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:6: Warning: You cleared the original identity \ + with android.os.Binder.clearCallingIdentity() but did not store the result \ + in a variable. You need to store the result in a variable and restore it \ + later. [ResultOfClearIdentityCallNotStoredInVariable] + android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:7: Warning: You cleared the original identity \ + with clearCallingIdentity() but did not store the result in a variable. \ + You need to store the result in a variable and restore it later. \ + [ResultOfClearIdentityCallNotStoredInVariable] + clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 3 warnings + """.addLineContinuation() + ) } /** Stubs for classes used for testing */ private val binderStub: TestFile = java( - """ + """ package android.os; public class Binder { public static final native long clearCallingIdentity() { @@ -767,7 +820,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ).indented() private val userHandleStub: TestFile = java( - """ + """ package android.os; import android.annotation.AppIdInt; import android.annotation.UserIdInt; @@ -792,7 +845,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ).indented() private val userIdIntStub: TestFile = java( - """ + """ package android.annotation; public @interface UserIdInt { } @@ -800,7 +853,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ).indented() private val appIdIntStub: TestFile = java( - """ + """ package android.annotation; public @interface AppIdInt { } diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt index e72f38416310..e72f38416310 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt new file mode 100644 index 000000000000..a70644ab8532 --- /dev/null +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt @@ -0,0 +1,271 @@ +/* + * 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. + */ + +package com.google.android.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class PackageVisibilityDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = PackageVisibilityDetector() + + override fun getIssues(): MutableList<Issue> = mutableListOf( + PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun testDetectIssuesParameterDoesNotApplyPackageVisibilityFilters() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + @Override + public boolean hasPackage(String packageName) { + return packageName != null; + } + } + """).indented(), *stubs + ).run().expect( + """ + src/com/android/server/lint/test/TestClass.java:6: Warning: \ + Api: hasPackage contains a package name parameter: packageName does not apply \ + package visibility filtering rules. \ + [ApiMightLeakAppVisibility] + public boolean hasPackage(String packageName) { + ~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testDoesNotDetectIssuesApiInvokesAppOps() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.app.AppOpsManager; + import android.os.Binder; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + private AppOpsManager mAppOpsManager; + + @Override + public boolean hasPackage(String packageName) { + checkPackage(packageName); + return packageName != null; + } + + private void checkPackage(String packageName) { + mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName); + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + fun testDoesNotDetectIssuesApiInvokesEnforcePermission() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.content.Context; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + private Context mContext; + + @Override + public boolean hasPackage(String packageName) { + enforcePermission(); + return packageName != null; + } + + private void enforcePermission() { + mContext.checkCallingPermission( + android.Manifest.permission.ACCESS_INPUT_FLINGER); + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + fun testDoesNotDetectIssuesApiInvokesPackageManager() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.content.pm.PackageInfo; + import android.content.pm.PackageManager; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + private PackageManager mPackageManager; + + @Override + public boolean hasPackage(String packageName) { + return getPackageInfo(packageName) != null; + } + + private PackageInfo getPackageInfo(String packageName) { + try { + return mPackageManager.getPackageInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + fun testDetectIssuesApiInvokesPackageManagerAndClearCallingIdentify() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.content.pm.PackageInfo; + import android.content.pm.PackageManager; + import android.internal.test.IFoo;import android.os.Binder; + + public class TestClass extends IFoo.Stub { + private PackageManager mPackageManager; + + @Override + public boolean hasPackage(String packageName) { + return getPackageInfo(packageName) != null; + } + + private PackageInfo getPackageInfo(String packageName) { + long token = Binder.clearCallingIdentity(); + try { + try { + return mPackageManager.getPackageInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } finally{ + Binder.restoreCallingIdentity(token); + } + } + } + """).indented(), *stubs + ).run().expect( + """ + src/com/android/server/lint/test/TestClass.java:10: Warning: \ + Api: hasPackage contains a package name parameter: packageName does not apply \ + package visibility filtering rules. \ + [ApiMightLeakAppVisibility] + public boolean hasPackage(String packageName) { + ~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testDoesNotDetectIssuesApiNotSystemPackagePrefix() { + lint().files(java( + """ + package com.test.not.system.prefix; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + @Override + public boolean hasPackage(String packageName) { + return packageName != null; + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + private val contextStub: TestFile = java( + """ + package android.content; + + public abstract class Context { + public abstract int checkCallingPermission(String permission); + } + """ + ).indented() + + private val appOpsManagerStub: TestFile = java( + """ + package android.app; + + public class AppOpsManager { + public void checkPackage(int uid, String packageName) { + } + } + """ + ).indented() + + private val packageManagerStub: TestFile = java( + """ + package android.content.pm; + import android.content.pm.PackageInfo; + + public abstract class PackageManager { + public static class NameNotFoundException extends AndroidException { + } + + public abstract PackageInfo getPackageInfo(String packageName, int flags) + throws NameNotFoundException; + } + """ + ).indented() + + private val packageInfoStub: TestFile = java( + """ + package android.content.pm; + public class PackageInfo {} + """ + ).indented() + + private val binderStub: TestFile = java( + """ + package android.os; + + public class Binder { + public static final native long clearCallingIdentity(); + public static final native void restoreCallingIdentity(long token); + public static final native int getCallingUid(); + } + """ + ).indented() + + private val interfaceIFooStub: TestFile = java( + """ + package android.internal.test; + import android.os.Binder; + + public interface IFoo { + boolean hasPackage(String packageName); + public abstract static class Stub extends Binder implements IFoo { + } + } + """ + ).indented() + + private val stubs = arrayOf(contextStub, appOpsManagerStub, packageManagerStub, + packageInfoStub, binderStub, interfaceIFooStub) + + // Substitutes "backslash + new line" with an empty string to imitate line continuation + private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") +} diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt new file mode 100644 index 000000000000..e686695ca804 --- /dev/null +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt @@ -0,0 +1,823 @@ +/* + * 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. + */ + +package com.google.android.lint.parcel + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestMode +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class SaferParcelCheckerTest : LintDetectorTest() { + override fun getDetector(): Detector = SaferParcelChecker() + + override fun getIssues(): List<Issue> = listOf( + SaferParcelChecker.ISSUE_UNSAFE_API_USAGE + ) + + override fun lint(): TestLintTask = + super.lint() + .allowMissingSdk(true) + // We don't do partial analysis in the platform + .skipTestModes(TestMode.PARTIAL) + + /** Parcel Tests */ + + fun testParcelDetectUnsafeReadSerializable() { + lint() + .files( + java( + """ + package test.pkg; + import android.os.Parcel; + import java.io.Serializable; + + public class TestClass { + private TestClass(Parcel p) { + Serializable ans = p.readSerializable(); + } + } + """ + ).indented(), + *includes + ) + .expectIdenticalTestModeOutput(false) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readSerializable() \ + API usage [UnsafeParcelApi] + Serializable ans = p.readSerializable(); + ~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadSerializable() { + lint() + .files( + java( + """ + package test.pkg; + import android.os.Parcel; + import java.io.Serializable; + + public class TestClass { + private TestClass(Parcel p) { + String ans = p.readSerializable(null, String.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + ArrayList ans = p.readArrayList(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Unsafe Parcel.readArrayList() API \ + usage [UnsafeParcelApi] + ArrayList ans = p.readArrayList(null); + ~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + ArrayList<Intent> ans = p.readArrayList(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + p.readList(list, null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readList() API usage \ + [UnsafeParcelApi] + p.readList(list, null); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testDParceloesNotDetectSafeReadList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + p.readList(list, null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent ans = p.readParcelable(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelable() API \ + usage [UnsafeParcelApi] + Intent ans = p.readParcelable(null); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent ans = p.readParcelable(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadParcelableList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + List<Intent> ans = p.readParcelableList(list, null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \ + API usage [UnsafeParcelApi] + List<Intent> ans = p.readParcelableList(list, null); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadParcelableList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + List<Intent> ans = + p.readParcelableList(list, null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadSparseArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import android.util.SparseArray; + + public class TestClass { + private TestClass(Parcel p) { + SparseArray<Intent> ans = p.readSparseArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\ + usage [UnsafeParcelApi] + SparseArray<Intent> ans = p.readSparseArray(null); + ~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadSparseArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import android.util.SparseArray; + + public class TestClass { + private TestClass(Parcel p) { + SparseArray<Intent> ans = + p.readSparseArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadSArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readArray() API\ + usage [UnsafeParcelApi] + Intent[] ans = p.readArray(null); + ~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadParcelableSArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readParcelableArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelableArray() API\ + usage [UnsafeParcelApi] + Intent[] ans = p.readParcelableArray(null); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readParcelableArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + /** Bundle Tests */ + + fun testBundleDetectUnsafeGetParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent ans = b.getParcelable("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelable() API usage [UnsafeParcelApi] + Intent ans = b.getParcelable("key"); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent ans = b.getParcelable("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetParcelableArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + ArrayList<Intent> ans = b.getParcelableArrayList("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi] + ArrayList<Intent> ans = b.getParcelableArrayList("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelableArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + ArrayList<Intent> ans = b.getParcelableArrayList("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent[] ans = b.getParcelableArray("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArray() API usage [UnsafeParcelApi] + Intent[] ans = b.getParcelableArray("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent[] ans = b.getParcelableArray("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetSparseParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + SparseArray<Intent> ans = b.getSparseParcelableArray("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi] + SparseArray<Intent> ans = b.getSparseParcelableArray("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetSparseParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + SparseArray<Intent> ans = b.getSparseParcelableArray("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + /** Intent Tests */ + + fun testIntentDetectUnsafeGetParcelableExtra() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + + public class TestClass { + private TestClass(Intent i) { + Intent ans = i.getParcelableExtra("name"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Unsafe Intent.getParcelableExtra() API usage [UnsafeParcelApi] + Intent ans = i.getParcelableExtra("name"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testIntentDoesNotDetectSafeGetParcelableExtra() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + + public class TestClass { + private TestClass(Intent i) { + Intent ans = i.getParcelableExtra("name", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + + /** Stubs for classes used for testing */ + + + private val includes = + arrayOf( + manifest().minSdk("33"), + java( + """ + package android.os; + import java.util.ArrayList; + import java.util.List; + import java.util.Map; + import java.util.HashMap; + + public final class Parcel { + // Deprecated + public Object[] readArray(ClassLoader loader) { return null; } + public ArrayList readArrayList(ClassLoader loader) { return null; } + public HashMap readHashMap(ClassLoader loader) { return null; } + public void readList(List outVal, ClassLoader loader) {} + public void readMap(Map outVal, ClassLoader loader) {} + public <T extends Parcelable> T readParcelable(ClassLoader loader) { return null; } + public Parcelable[] readParcelableArray(ClassLoader loader) { return null; } + public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) { return null; } + public <T extends Parcelable> List<T> readParcelableList(List<T> list, ClassLoader cl) { return null; } + public Serializable readSerializable() { return null; } + public <T> SparseArray<T> readSparseArray(ClassLoader loader) { return null; } + + // Replacements + public <T> T[] readArray(ClassLoader loader, Class<T> clazz) { return null; } + public <T> ArrayList<T> readArrayList(ClassLoader loader, Class<? extends T> clazz) { return null; } + public <K, V> HashMap<K,V> readHashMap(ClassLoader loader, Class<? extends K> clazzKey, Class<? extends V> clazzValue) { return null; } + public <T> void readList(List<? super T> outVal, ClassLoader loader, Class<T> clazz) {} + public <K, V> void readMap(Map<? super K, ? super V> outVal, ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {} + public <T> T readParcelable(ClassLoader loader, Class<T> clazz) { return null; } + public <T> T[] readParcelableArray(ClassLoader loader, Class<T> clazz) { return null; } + public <T> Parcelable.Creator<T> readParcelableCreator(ClassLoader loader, Class<T> clazz) { return null; } + public <T> List<T> readParcelableList(List<T> list, ClassLoader cl, Class<T> clazz) { return null; } + public <T> T readSerializable(ClassLoader loader, Class<T> clazz) { return null; } + public <T> SparseArray<T> readSparseArray(ClassLoader loader, Class<? extends T> clazz) { return null; } + } + """ + ).indented(), + java( + """ + package android.os; + import java.util.ArrayList; + import java.util.List; + import java.util.Map; + import java.util.HashMap; + + public final class Bundle { + // Deprecated + public <T extends Parcelable> T getParcelable(String key) { return null; } + public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { return null; } + public Parcelable[] getParcelableArray(String key) { return null; } + public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { return null; } + + // Replacements + public <T> T getParcelable(String key, Class<T> clazz) { return null; } + public <T> ArrayList<T> getParcelableArrayList(String key, Class<? extends T> clazz) { return null; } + public <T> T[] getParcelableArray(String key, Class<T> clazz) { return null; } + public <T> SparseArray<T> getSparseParcelableArray(String key, Class<? extends T> clazz) { return null; } + + } + """ + ).indented(), + java( + """ + package android.os; + public interface Parcelable {} + """ + ).indented(), + java( + """ + package android.content; + public class Intent implements Parcelable, Cloneable { + // Deprecated + public <T extends Parcelable> T getParcelableExtra(String name) { return null; } + + // Replacements + public <T> T getParcelableExtra(String name, Class<T> clazz) { return null; } + + } + """ + ).indented(), + java( + """ + package android.util; + public class SparseArray<E> implements Cloneable {} + """ + ).indented(), + ) + + // Substitutes "backslash + new line" with an empty string to imitate line continuation + private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") +} diff --git a/tools/lint/global/Android.bp b/tools/lint/global/Android.bp new file mode 100644 index 000000000000..bedb7bd78a29 --- /dev/null +++ b/tools/lint/global/Android.bp @@ -0,0 +1,65 @@ +// 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. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "AndroidGlobalLintChecker", + srcs: ["checks/src/main/java/**/*.kt"], + plugins: ["auto_service_plugin"], + libs: [ + "auto_service_annotations", + "lint_api", + ], + static_libs: ["AndroidCommonLint"], + kotlincflags: ["-Xjvm-default=all"], + dist: { + targets: ["droid"], + }, +} + +java_test_host { + name: "AndroidGlobalLintCheckerTest", + srcs: ["checks/src/test/java/**/*.kt"], + static_libs: [ + "AndroidGlobalLintChecker", + "junit", + "lint", + "lint_tests", + ], + test_options: { + unit_test: true, + tradefed_options: [ + { + // lint bundles in some classes that were built with older versions + // of libraries, and no longer load. Since tradefed tries to load + // all classes in the jar to look for tests, it crashes loading them. + // Exclude these classes from tradefed's search. + name: "exclude-paths", + value: "org/apache", + }, + { + name: "exclude-paths", + value: "META-INF", + }, + ], + }, +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt index a6fd9bba6192..a20266a9b140 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -19,21 +19,19 @@ package com.google.android.lint import com.android.tools.lint.client.api.IssueRegistry import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.detector.api.CURRENT_API +import com.google.android.lint.aidl.EnforcePermissionDetector +import com.google.android.lint.aidl.EnforcePermissionHelperDetector +import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector import com.google.auto.service.AutoService @AutoService(IssueRegistry::class) @Suppress("UnstableApiUsage") -class AndroidFrameworkIssueRegistry : IssueRegistry() { +class AndroidGlobalIssueRegistry : IssueRegistry() { override val issues = listOf( - CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, - CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, - CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, - CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, - CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, - CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, - CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED, EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, - EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION, + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, + SimpleManualPermissionEnforcementDetector.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT, ) override val api: Int @@ -45,6 +43,6 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() { override val vendor: Vendor = Vendor( vendorName = "Android", feedbackUrl = "http://b/issues/new?component=315013", - contact = "brufino@google.com" + contact = "repsonsible-apis@google.com" ) -} +}
\ No newline at end of file diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt new file mode 100644 index 000000000000..ab6d871d6ea6 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod + +/** + * Abstract class for detectors that look for methods implementing + * generated AIDL interface stubs + */ +abstract class AidlImplementationDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List<Class<out UElement?>> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + val interfaceName = getContainingAidlInterface(context, node) + .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return + val body = (node.uastBody as? UBlockExpression) ?: return + visitAidlMethod(context, node, interfaceName, body) + } + } + + abstract fun visitAidlMethod( + context: JavaContext, + node: UMethod, + interfaceName: String, + body: UBlockExpression, + ) +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt new file mode 100644 index 000000000000..dcfbe953f955 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission" +const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission" +const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced" + +val AIDL_PERMISSION_ANNOTATIONS = listOf( + ANNOTATION_ENFORCE_PERMISSION, + ANNOTATION_REQUIRES_NO_PERMISSION, + ANNOTATION_PERMISSION_MANUALLY_ENFORCED +) + +const val BINDER_CLASS = "android.os.Binder" +const val IINTERFACE_INTERFACE = "android.os.IInterface" + +const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission" + +/** + * If a non java (e.g. c++) backend is enabled, the @EnforcePermission + * annotation cannot be used. At time of writing, the mechanism + * is not implemented for non java backends. + * TODO: b/242564874 (have lint know which interfaces have the c++ backend enabled) + * rather than hard coding this list? + */ +val EXCLUDED_CPP_INTERFACES = listOf( + "AdbTransportType", + "FingerprintAndPairDevice", + "IAdbCallback", + "IAdbManager", + "PairDevice", + "IStatsBootstrapAtomService", + "StatsBootstrapAtom", + "StatsBootstrapAtomValue", + "FixedSizeArrayExample", + "PlaybackTrackMetadata", + "RecordTrackMetadata", + "SinkMetadata", + "SourceMetadata", + "IUpdateEngineStable", + "IUpdateEngineStableCallback", + "AudioCapabilities", + "ConfidenceLevel", + "ModelParameter", + "ModelParameterRange", + "Phrase", + "PhraseRecognitionEvent", + "PhraseRecognitionExtra", + "PhraseSoundModel", + "Properties", + "RecognitionConfig", + "RecognitionEvent", + "RecognitionMode", + "RecognitionStatus", + "SoundModel", + "SoundModelType", + "Status", + "IThermalService", + "IPowerManager", + "ITunerResourceManager" +) diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt new file mode 100644 index 000000000000..0baac2c7aacf --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt @@ -0,0 +1,226 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.AnnotationInfo +import com.android.tools.lint.detector.api.AnnotationOrigin +import com.android.tools.lint.detector.api.AnnotationUsageInfo +import com.android.tools.lint.detector.api.AnnotationUsageType +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.ConstantEvaluator +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiArrayInitializerMemberValue +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.toUElement + +/** + * Lint Detector that ensures that any method overriding a method annotated + * with @EnforcePermission is also annotated with the exact same annotation. + * The intent is to surface the effective permission checks to the service + * implementations. + * + * This is done with 2 mechanisms: + * 1. Visit any annotation usage, to ensure that any derived class will have + * the correct annotation on each methods. This is for the top to bottom + * propagation. + * 2. Visit any annotation, to ensure that if a method is annotated, it has + * its ancestor also annotated. This is to avoid having an annotation on a + * Java method without the corresponding annotation on the AIDL interface. + */ +class EnforcePermissionDetector : Detector(), SourceCodeScanner { + + override fun applicableAnnotations(): List<String> { + return listOf(ANNOTATION_ENFORCE_PERMISSION) + } + + override fun getApplicableUastTypes(): List<Class<out UElement>> { + return listOf(UAnnotation::class.java) + } + + private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> { + if (elem is PsiArrayInitializerMemberValue) + return elem.getInitializers().map { it as PsiElement }.toTypedArray() + return elem.getChildren() + } + + private fun areAnnotationsEquivalent( + context: JavaContext, + anno1: PsiAnnotation, + anno2: PsiAnnotation + ): Boolean { + if (anno1.qualifiedName != anno2.qualifiedName) { + return false + } + val attr1 = anno1.parameterList.attributes + val attr2 = anno2.parameterList.attributes + if (attr1.size != attr2.size) { + return false + } + for (i in attr1.indices) { + if (attr1[i].name != attr2[i].name) { + return false + } + val value1 = attr1[i].value ?: return false + val value2 = attr2[i].value ?: return false + // Try to compare values directly with each other. + val v1 = ConstantEvaluator.evaluate(context, value1) + val v2 = ConstantEvaluator.evaluate(context, value2) + if (v1 != null && v2 != null) { + if (v1 != v2) { + return false + } + } else { + val children1 = annotationValueGetChildren(value1) + val children2 = annotationValueGetChildren(value2) + if (children1.size != children2.size) { + return false + } + for (j in children1.indices) { + val c1 = ConstantEvaluator.evaluate(context, children1[j]) + val c2 = ConstantEvaluator.evaluate(context, children2[j]) + if (c1 != c2) { + return false + } + } + } + } + return true + } + + private fun compareMethods( + context: JavaContext, + element: UElement, + overridingMethod: PsiMethod, + overriddenMethod: PsiMethod, + checkEquivalence: Boolean = true + ) { + // If method is not from a Stub subclass, this method shouldn't use @EP at all. + // This is handled by EnforcePermissionHelperDetector. + if (!isContainedInSubclassOfStub(context, overridingMethod.toUElement() as? UMethod)) { + return + } + val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val location = context.getLocation(element) + val overridingClass = overridingMethod.parent as PsiClass + val overriddenClass = overriddenMethod.parent as PsiClass + val overridingName = "${overridingClass.name}.${overridingMethod.name}" + val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}" + if (overridingAnnotation == null) { + val msg = "The method $overridingName overrides the method $overriddenName which " + + "is annotated with @EnforcePermission. The same annotation must be used " + + "on $overridingName" + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (overriddenAnnotation == null) { + val msg = "The method $overridingName overrides the method $overriddenName which " + + "is not annotated with @EnforcePermission. The same annotation must be " + + "used on $overriddenName. Did you forget to annotate the AIDL definition?" + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (checkEquivalence && !areAnnotationsEquivalent( + context, overridingAnnotation, overriddenAnnotation)) { + val msg = "The method $overridingName is annotated with " + + "${overridingAnnotation.text} which differs from the overridden " + + "method $overriddenName: ${overriddenAnnotation.text}. The same " + + "annotation must be used for both methods." + context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) + } + } + + override fun visitAnnotationUsage( + context: JavaContext, + element: UElement, + annotationInfo: AnnotationInfo, + usageInfo: AnnotationUsageInfo + ) { + if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE && + annotationInfo.origin == AnnotationOrigin.METHOD) { + val overridingMethod = element.sourcePsi as PsiMethod + val overriddenMethod = usageInfo.referenced as PsiMethod + compareMethods(context, element, overridingMethod, overriddenMethod) + } + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitAnnotation(node: UAnnotation) { + if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) { + return + } + val method = node.uastParent as? UMethod ?: return + val overridingMethod = method as PsiMethod + val parents = overridingMethod.findSuperMethods() + for (overriddenMethod in parents) { + // The equivalence check can be skipped, if both methods are + // annotated, it will be verified by visitAnnotationUsage. + compareMethods(context, method, overridingMethod, + overriddenMethod, checkEquivalence = false) + } + } + } + } + + companion object { + val EXPLANATION = """ + The @EnforcePermission annotation is used to indicate that the underlying binder code + has already verified the caller's permissions before calling the appropriate method. The + verification code is usually generated by the AIDL compiler, which also takes care of + annotating the generated Java code. + + In order to surface that information to platform developers, the same annotation must be + used on the implementation class or methods. + """ + + val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MissingEnforcePermissionAnnotation", + briefDescription = "Missing @EnforcePermission annotation on Binder method", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MismatchingEnforcePermissionAnnotation", + briefDescription = "Incorrect @EnforcePermission annotation on Binder method", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt new file mode 100644 index 000000000000..25d208db14ec --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt @@ -0,0 +1,384 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationBooleanValue +import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValues +import com.android.tools.lint.detector.api.findSelector +import com.android.tools.lint.detector.api.getUMethod +import com.google.android.lint.findCallExpression +import com.google.android.lint.getPermissionMethodAnnotation +import com.google.android.lint.hasPermissionNameAnnotation +import com.google.android.lint.isPermissionMethodCall +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiType +import org.jetbrains.kotlin.psi.psiUtil.parameterIndex +import org.jetbrains.uast.UBinaryExpression +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UExpressionList +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UThrowExpression +import org.jetbrains.uast.UastBinaryOperator +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.skipParenthesizedExprDown +import org.jetbrains.uast.visitor.AbstractUastVisitor + +/** + * Helper class that facilitates the creation of lint auto fixes + */ +data class EnforcePermissionFix( + val manualCheckLocations: List<Location>, + val permissionNames: List<String>, + val errorLevel: Boolean, + val anyOf: Boolean, +) { + fun toLintFix(context: JavaContext, node: UMethod): LintFix { + val methodLocation = context.getLocation(node) + val replaceOrRemoveFixes = manualCheckLocations.mapIndexed { index, manualCheckLocation -> + if (index == 0) { + // Replace the first manual check with a call to the helper method + getHelperMethodFix(node, manualCheckLocation, false) + } else { + // Remove all subsequent manual checks + LintFix.create() + .replace() + .reformat(true) + .range(manualCheckLocation) + .with("") + .autoFix() + .build() + } + } + + // Annotate the method with @EnforcePermission(...) + val annotateFix = LintFix.create() + .annotate(annotation) + .range(methodLocation) + .autoFix() + .build() + + return LintFix.create().composite(annotateFix, *replaceOrRemoveFixes.toTypedArray()) + } + + private val annotation: String + get() { + val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" } + + val attributeName = + if (permissionNames.size > 1) { + if (anyOf) "anyOf" else "allOf" + } else null + + val annotationParameter = + if (attributeName != null) "$attributeName={$quotedPermissions}" + else quotedPermissions + + return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)" + } + + companion object { + /** + * Walks the expressions in a block, looking for simple permission checks. + * + * As soon as something other than a permission check is encountered, stop looking, + * as some other business logic is happening that prevents an automated fix. + */ + fun fromBlockExpression( + context: JavaContext, + blockExpression: UBlockExpression + ): EnforcePermissionFix? { + try { + val singleFixes = mutableListOf<EnforcePermissionFix>() + for (expression in blockExpression.expressions) { + val fix = fromExpression(context, expression) ?: break + singleFixes.add(fix) + } + return compose(singleFixes) + } catch (e: AnyOfAllOfException) { + return null + } + } + + /** + * Conditionally constructs EnforcePermissionFix from any UExpression + * + * @return EnforcePermissionFix if the expression boils down to a permission check, + * else null + */ + fun fromExpression( + context: JavaContext, + expression: UExpression + ): EnforcePermissionFix? { + val trimmedExpression = expression.skipParenthesizedExprDown() + if (trimmedExpression is UIfExpression) { + return fromIfExpression(context, trimmedExpression) + } + findCallExpression(trimmedExpression)?.let { + return fromCallExpression(context, it) + } + return null + } + + /** + * Conditionally constructs EnforcePermissionFix from a UCallExpression + * + * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null + */ + fun fromCallExpression( + context: JavaContext, + callExpression: UCallExpression + ): EnforcePermissionFix? { + val method = callExpression.resolve()?.getUMethod() ?: return null + val annotation = getPermissionMethodAnnotation(method) ?: return null + val returnsVoid = method.returnType == PsiType.VOID + val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false + val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false + return EnforcePermissionFix( + listOf(getPermissionCheckLocation(context, callExpression)), + getPermissionCheckValues(callExpression), + errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf), + anyOf, + ) + } + + /** + * Conditionally constructs EnforcePermissionFix from a UCallExpression + * + * @return EnforcePermissionFix IF AND ONLY IF: + * * The condition of the if statement compares the return value of a + * PermissionMethod to one of the PackageManager.PermissionResult values + * * The expression inside the if statement does nothing but throw SecurityException + */ + fun fromIfExpression( + context: JavaContext, + ifExpression: UIfExpression + ): EnforcePermissionFix? { + val condition = ifExpression.condition.skipParenthesizedExprDown() + if (condition !is UBinaryExpression) return null + + val maybeLeftCall = findCallExpression(condition.leftOperand) + val maybeRightCall = findCallExpression(condition.rightOperand) + + val (callExpression, comparison) = + if (maybeLeftCall is UCallExpression) { + Pair(maybeLeftCall, condition.rightOperand) + } else if (maybeRightCall is UCallExpression) { + Pair(maybeRightCall, condition.leftOperand) + } else return null + + val permissionMethodAnnotation = getPermissionMethodAnnotation( + callExpression.resolve()?.getUMethod()) ?: return null + + val equalityCheck = + when (comparison.findSelector().asSourceString() + .filterNot(Char::isWhitespace)) { + "PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS + "PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS + else -> return null + } + + if (condition.operator != equalityCheck) return null + + val throwExpression: UThrowExpression? = + ifExpression.thenExpression as? UThrowExpression + ?: (ifExpression.thenExpression as? UBlockExpression) + ?.expressions?.firstOrNull() + as? UThrowExpression + + + val thrownClass = (throwExpression?.thrownExpression?.getExpressionType() + as? PsiClassType)?.resolve() ?: return null + if (!context.evaluator.inheritsFrom( + thrownClass, "java.lang.SecurityException")){ + return null + } + + val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false + val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false + + return EnforcePermissionFix( + listOf(context.getLocation(ifExpression)), + getPermissionCheckValues(callExpression), + errorLevel = isErrorLevel(throws = true, orSelf = orSelf), + anyOf = anyOf + ) + } + + + fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix? { + if (individuals.isEmpty()) return null + val anyOfs = individuals.filter(EnforcePermissionFix::anyOf) + // anyOf/allOf should be consistent. If we encounter some @PermissionMethods that are anyOf + // and others that aren't, we don't know what to do. + if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) { + throw AnyOfAllOfException() + } + return EnforcePermissionFix( + individuals.flatMap(EnforcePermissionFix::manualCheckLocations), + individuals.flatMap(EnforcePermissionFix::permissionNames), + errorLevel = individuals.all(EnforcePermissionFix::errorLevel), + anyOf = anyOfs.isNotEmpty() + ) + } + + /** + * Given a permission check, get its proper location + * so that a lint fix can remove the entire expression + */ + private fun getPermissionCheckLocation( + context: JavaContext, + callExpression: UCallExpression + ): + Location { + val javaPsi = callExpression.javaPsi!! + return Location.create( + context.file, + javaPsi.containingFile?.text, + javaPsi.textRange.startOffset, + // unfortunately the element doesn't include the ending semicolon + javaPsi.textRange.endOffset + 1 + ) + } + + /** + * Given a @PermissionMethod, find arguments annotated with @PermissionName + * and pull out the permission value(s) being used. Also evaluates nested calls + * to @PermissionMethod(s) in the given method's body. + */ + @Throws(AnyOfAllOfException::class) + private fun getPermissionCheckValues( + callExpression: UCallExpression + ): List<String> { + if (!isPermissionMethodCall(callExpression)) return emptyList() + + val result = mutableSetOf<String>() // protect against duplicate permission values + val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice + val bfsQueue = ArrayDeque(listOf(callExpression)) + + var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL + + // Bread First Search - evaluating nested @PermissionMethod(s) in the available + // source code for @PermissionName(s). + while (bfsQueue.isNotEmpty()) { + val currentCallExpression = bfsQueue.removeFirst() + visitedCalls.add(currentCallExpression) + val currentPermissions = findPermissions(currentCallExpression) + result.addAll(currentPermissions) + + val currentAnnotation = getPermissionMethodAnnotation( + currentCallExpression.resolve()?.getUMethod()) + val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false + + // anyOf/allOf should be consistent. If we encounter a nesting of @PermissionMethods + // where we start in an anyOf state and switch to allOf, or vice versa, + // we don't know what to do. + if (anyOfAllOfState == AnyOfAllOfState.INITIAL) { + if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF + else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF + } + + if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) { + throw AnyOfAllOfException() + } + + if (anyOfAllOfState == AnyOfAllOfState.ANY_OF && + !currentAnyOf && currentPermissions.size > 1) { + throw AnyOfAllOfException() + } + + currentCallExpression.resolve()?.getUMethod() + ?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue)) + } + + return result.toList() + } + + private enum class AnyOfAllOfState { + INITIAL, + ANY_OF, + ALL_OF + } + + /** + * Adds visited permission method calls to the provided + * queue in support of the BFS traversal happening while + * this is used + */ + private class PermissionCheckValuesVisitor( + val visitedCalls: Set<UCallExpression>, + val bfsQueue: ArrayDeque<UCallExpression> + ) : AbstractUastVisitor() { + override fun visitCallExpression(node: UCallExpression): Boolean { + if (isPermissionMethodCall(node) && node !in visitedCalls) { + bfsQueue.add(node) + } + return false + } + } + + private fun findPermissions( + callExpression: UCallExpression, + ): List<String> { + val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod()) + + val hardCodedPermissions = (getAnnotationStringValues(annotation, "value") + ?: emptyArray()) + .toList() + + val indices = callExpression.resolve()?.getUMethod() + ?.uastParameters + ?.filter(::hasPermissionNameAnnotation) + ?.mapNotNull { it.sourcePsi?.parameterIndex() } + ?: emptyList() + + val argPermissions = indices + .flatMap { i -> + when (val argument = callExpression.getArgumentForParameter(i)) { + null -> listOf(null) + is UExpressionList -> // varargs e.g. someMethod(String...) + argument.expressions.map(UExpression::evaluateString) + else -> listOf(argument.evaluateString()) + } + } + .filterNotNull() + + return hardCodedPermissions + argPermissions + } + + /** + * If we detect that the PermissionMethod enforces that permission is granted, + * AND is of the "orSelf" variety, we are very confident that this is a behavior + * preserving migration to @EnforcePermission. Thus, the incident should be ERROR + * level. + */ + private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf + } +} +/** + * anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission - + * meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf, + * we don't know which to apply. + */ +class AnyOfAllOfException : Exception() { + override val message: String = "anyOf/allOf permission methods cannot be mixed" +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt new file mode 100644 index 000000000000..df13af516514 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt @@ -0,0 +1,149 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.google.android.lint.findCallExpression +import com.intellij.psi.PsiElement +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UDeclarationsExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.skipParenthesizedExprDown + +class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List<Class<out UElement?>> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (context.evaluator.isAbstract(node)) return + if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return + + if (!isContainedInSubclassOfStub(context, node)) { + context.report( + ISSUE_MISUSING_ENFORCE_PERMISSION, + node, + context.getLocation(node), + "The class of ${node.name} does not inherit from an AIDL generated Stub class" + ) + return + } + + val targetExpression = getHelperMethodCallSourceString(node) + val message = + "Method must start with $targetExpression or super.${node.name}(), if applicable" + + val firstExpression = (node.uastBody as? UBlockExpression) + ?.expressions?.firstOrNull() + + if (firstExpression == null) { + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + message, + ) + return + } + + val firstExpressionSource = firstExpression.skipParenthesizedExprDown() + .asSourceString() + .filterNot(Char::isWhitespace) + + if (firstExpressionSource != targetExpression && + firstExpressionSource != "super.$targetExpression") { + // calling super.<methodName>() is also legal + val directSuper = context.evaluator.getSuperMethod(node) + val firstCall = findCallExpression(firstExpression)?.resolve() + if (directSuper != null && firstCall == directSuper) return + + val locationTarget = getLocationTarget(firstExpression) + val expressionLocation = context.getLocation(locationTarget) + + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + message, + getHelperMethodFix(node, expressionLocation), + ) + } + } + } + + companion object { + private const val HELPER_SUFFIX = "_enforcePermission" + + private const val EXPLANATION = """ + The @EnforcePermission annotation can only be used on methods whose class extends from + the Stub class generated by the AIDL compiler. When @EnforcePermission is applied, the + AIDL compiler generates a Stub method to do the permission check called yourMethodName$HELPER_SUFFIX. + + yourMethodName$HELPER_SUFFIX must be executed before any other operation. To do that, you can + either call it directly, or call it indirectly via super.yourMethodName(). + """ + + val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create( + id = "MissingEnforcePermissionHelper", + briefDescription = """Missing permission-enforcing method call in AIDL method + |annotated with @EnforcePermission""".trimMargin(), + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionHelperDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + val ISSUE_MISUSING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MisusingEnforcePermissionAnnotation", + briefDescription = "@EnforcePermission cannot be used here", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + /** + * handles an edge case with UDeclarationsExpression, where sourcePsi is null, + * resulting in an incorrect Location if used directly + */ + private fun getLocationTarget(firstExpression: UExpression): PsiElement? { + if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi + if (firstExpression is UDeclarationsExpression) { + return firstExpression.declarations.firstOrNull()?.sourcePsi + } + return null + } + } +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt new file mode 100644 index 000000000000..d41fee3fc0dc --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt @@ -0,0 +1,96 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Location +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiReferenceList +import org.jetbrains.uast.UMethod + +/** + * Given a UMethod, determine if this method is + * the entrypoint to an interface generated by AIDL, + * returning the interface name if so, otherwise returning null + */ +fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? { + if (!isContainedInSubclassOfStub(context, node)) return null + for (superMethod in node.findSuperMethods()) { + for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements + ?: continue) { + if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) { + return superMethod.containingClass?.name + } + } + } + return null +} + +fun isContainedInSubclassOfStub(context: JavaContext, node: UMethod?): Boolean { + var superClass = node?.containingClass?.superClass + while (superClass != null) { + if (isStub(context, superClass)) return true + superClass = superClass.superClass + } + return false +} + +fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean { + if (psiClass == null) return false + if (psiClass.name != "Stub") return false + if (!context.evaluator.isStatic(psiClass)) return false + if (!context.evaluator.isAbstract(psiClass)) return false + + if (!hasSingleAncestor(psiClass.extendsList, BINDER_CLASS)) return false + + val parent = psiClass.parent as? PsiClass ?: return false + if (!hasSingleAncestor(parent.extendsList, IINTERFACE_INTERFACE)) return false + + val parentName = parent.qualifiedName ?: return false + if (!hasSingleAncestor(psiClass.implementsList, parentName)) return false + + return true +} + +private fun hasSingleAncestor(references: PsiReferenceList?, qualifiedName: String) = + references != null && + references.referenceElements.size == 1 && + references.referenceElements[0].qualifiedName == qualifiedName + +fun getHelperMethodCallSourceString(node: UMethod) = "${node.name}$AIDL_PERMISSION_HELPER_SUFFIX()" + +fun getHelperMethodFix( + node: UMethod, + manualCheckLocation: Location, + prepend: Boolean = true +): LintFix { + val helperMethodSource = getHelperMethodCallSourceString(node) + val indent = " ".repeat(manualCheckLocation.start?.column ?: 0) + val newText = "$helperMethodSource;${if (prepend) "\n\n$indent" else ""}" + + val fix = LintFix.create() + .replace() + .range(manualCheckLocation) + .with(newText) + .reformat(true) + .autoFix() + + if (prepend) fix.beginning() + + return fix.build() +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt new file mode 100644 index 000000000000..c7be36efd991 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt @@ -0,0 +1,92 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Incident +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UMethod + +/** + * Looks for methods implementing generated AIDL interface stubs + * that can have simple permission checks migrated to + * @EnforcePermission annotations + */ +@Suppress("UnstableApiUsage") +class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() { + override fun visitAidlMethod( + context: JavaContext, + node: UMethod, + interfaceName: String, + body: UBlockExpression + ) { + val enforcePermissionFix = EnforcePermissionFix.fromBlockExpression(context, body) ?: return + val lintFix = enforcePermissionFix.toLintFix(context, node) + val message = + "$interfaceName permission check ${ + if (enforcePermissionFix.errorLevel) "should" else "can" + } be converted to @EnforcePermission annotation" + + val incident = Incident( + ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT, + enforcePermissionFix.manualCheckLocations.last(), + message, + lintFix + ) + + // TODO(b/265014041): turn on errors once all code that would cause one is fixed + // if (enforcePermissionFix.errorLevel) { + // incident.overrideSeverity(Severity.ERROR) + // } + + context.report(incident) + } + + companion object { + + private val EXPLANATION = """ + Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission + annotation to declare the permissions to be enforced. The verification code is then + generated by the AIDL compiler, which also takes care of annotating the generated java + code. + + This reduces the risk of bugs around these permission checks (that often become vulnerabilities). + It also enables easier auditing and review. + + Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto) + """.trimIndent() + + @JvmField + val ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT = Issue.create( + id = "SimpleManualPermissionEnforcement", + briefDescription = "Manual permission check can be @EnforcePermission annotation", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + SimpleManualPermissionEnforcementDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + ) + } +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt new file mode 100644 index 000000000000..f2930d9faac7 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt @@ -0,0 +1,557 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class EnforcePermissionDetectorCodegenTest : LintDetectorTest() { + override fun getDetector(): Detector = EnforcePermissionDetector() + + override fun getIssues(): List<Issue> = listOf( + EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun test_generated_IProtected() { + lint().files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtected extends android.os.IInterface + { + /** Default implementation for IProtected. */ + public static class Default implements android.aidl.tests.permission.IProtected + { + @Override public void PermissionProtected() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtected interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) { + return ((android.aidl.tests.permission.IProtected)iin); + } + return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_PermissionProtected: + { + return "PermissionProtected"; + } + case TRANSACTION_MultiplePermissionsAll: + { + return "MultiplePermissionsAll"; + } + case TRANSACTION_MultiplePermissionsAny: + { + return "MultiplePermissionsAny"; + } + case TRANSACTION_NonManifestPermission: + { + return "NonManifestPermission"; + } + case TRANSACTION_SetGranted: + { + return "SetGranted"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_PermissionProtected: + { + this.PermissionProtected(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAll: + { + this.MultiplePermissionsAll(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAny: + { + this.MultiplePermissionsAny(); + reply.writeNoException(); + break; + } + case TRANSACTION_NonManifestPermission: + { + this.NonManifestPermission(); + reply.writeNoException(); + break; + } + case TRANSACTION_SetGranted: + { + java.util.List<java.lang.String> _arg0; + _arg0 = data.createStringArrayList(); + data.enforceNoDataAvail(); + this.SetGranted(_arg0); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtected + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void PermissionProtected() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStringList(permissions); + boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for PermissionProtected */ + protected void PermissionProtected_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source); + } + static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for MultiplePermissionsAll */ + protected void MultiplePermissionsAll_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); + /** Helper method to enforce permissions for MultiplePermissionsAny */ + protected void MultiplePermissionsAny_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); + /** Helper method to enforce permissions for NonManifestPermission */ + protected void NonManifestPermission_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source); + } + static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); + /** @hide */ + public int getMaxTransactionId() + { + return 4; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void PermissionProtected() throws android.os.RemoteException; + @android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAll() throws android.os.RemoteException; + @android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAny() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) + public void NonManifestPermission() throws android.os.RemoteException; + // Used by the integration tests to dynamically set permissions that are considered granted. + @android.annotation.RequiresNoPermission + public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun test_generated_IProtectedInterface() { + lint().files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtectedInterface extends android.os.IInterface + { + /** Default implementation for IProtectedInterface. */ + public static class Default implements android.aidl.tests.permission.IProtectedInterface + { + @Override public void Method1() throws android.os.RemoteException + { + } + @Override public void Method2() throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) { + return ((android.aidl.tests.permission.IProtectedInterface)iin); + } + return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_Method1: + { + return "Method1"; + } + case TRANSACTION_Method2: + { + return "Method2"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_Method1: + { + this.Method1(); + reply.writeNoException(); + break; + } + case TRANSACTION_Method2: + { + this.Method2(); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtectedInterface + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void Method1() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void Method2() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for Method1 */ + protected void Method1_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for Method2 */ + protected void Method2_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + /** @hide */ + public int getMaxTransactionId() + { + return 1; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method1() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method2() throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + /* Stubs */ + + private val manifestPermissionStub: TestFile = java( + """ + package android.Manifest; + class permission { + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String INTERNET = "android.permission.INTERNET"; + } + """ + ).indented() + + private val enforcePermissionAnnotationStub: TestFile = java( + """ + package android.annotation; + public @interface EnforcePermission {} + """ + ).indented() + + private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub) +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt new file mode 100644 index 000000000000..75b00737a168 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt @@ -0,0 +1,425 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class EnforcePermissionDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = EnforcePermissionDetector() + + override fun getIssues(): List<Issue> = listOf( + EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass2 extends IFooMethod.Stub { + @Override + @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAllOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass11 extends IFooMethod.Stub { + @Override + @EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAll() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAllLiteralOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass111 extends IFooMethod.Stub { + @Override + @EnforcePermission(allOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAllLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAnyOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass12 extends IFooMethod.Stub { + @Override + @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAny() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAnyLiteralOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass121 extends IFooMethod.Stub { + @Override + @EnforcePermission(anyOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAnyLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDetectIssuesMismatchingAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass4 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ + which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesEmptyAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass41 extends IFooMethod.Stub { + @android.annotation.EnforcePermission + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass41.java:4: Error: The method TestClass41.testMethod is annotated with @android.annotation.EnforcePermission \ + which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAnyAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass9 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) + public void testMethodAny() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass9.java:4: Error: The method TestClass9.testMethodAny is annotated with \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \ + which differs from the overridden method Stub.testMethodAny: \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAny() {} + ~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAnyLiteralAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass91 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass91.java:4: Error: The method TestClass91.testMethodAnyLiteral is annotated with \ + @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \ + which differs from the overridden method Stub.testMethodAnyLiteral: \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAnyLiteral() {} + ~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAllAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass10 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) + public void testMethodAll() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass10.java:4: Error: The method TestClass10.testMethodAll is annotated with \ + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \ + which differs from the overridden method Stub.testMethodAll: \ + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAll() {} + ~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAllLiteralAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass101 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) + public void testMethodAllLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass101.java:4: Error: The method TestClass101.testMethodAllLiteral is annotated with \ + @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \ + which differs from the overridden method Stub.testMethodAllLiteral: \ + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAllLiteral() {} + ~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMissingAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass6 extends IFooMethod.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod overrides the method Stub.testMethod which is annotated with @EnforcePermission. \ + The same annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesExtraAnnotationMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass7 extends IBar.Stub { + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod overrides the method Stub.testMethod which is not annotated with @EnforcePermission. \ + The same annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? [MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMissingAnnotationOnMethodWhenClassIsCalledDefault() { + lint().files(java( + """ + package test.pkg; + public class Default extends IFooMethod.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/Default.java:3: Error: The method Default.testMethod \ + overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same annotation must be used on Default.testMethod [MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation() + ) + } + + fun testDoesDetectIssuesShortStringsNotAllowed() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass121 extends IFooMethod.Stub { + @Override + @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass121.java:6: Error: The method \ + TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) \ + which differs from the overridden method Stub.testMethodAnyLiteral: \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAnyLiteral() {} + ~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation() + ) + } + + /* Stubs */ + + // A service with permission annotation on the method. + private val interfaceIFooMethodStub: TestFile = java( + """ + public interface IFooMethod extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements IFooMethod { + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod() {} + @Override + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAny() {} + @Override + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + @Override + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAll() {} + @Override + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAllLiteral() {} + } + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod(); + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAny() {} + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAll() {} + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAllLiteral() {} + } + """ + ).indented() + + // A service without any permission annotation. + private val interfaceIBarStub: TestFile = java( + """ + public interface IBar extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements IBar { + @Override + public void testMethod() {} + } + public void testMethod(); + } + """ + ).indented() + + private val manifestPermissionStub: TestFile = java( + """ + package android.Manifest; + class permission { + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String NFC = "android.permission.NFC"; + public static final String INTERNET = "android.permission.INTERNET"; + } + """ + ).indented() + + private val enforcePermissionAnnotationStub: TestFile = java( + """ + package android.annotation; + public @interface EnforcePermission {} + """ + ).indented() + + private val stubs = arrayOf(interfaceIFooMethodStub, interfaceIBarStub, + manifestPermissionStub, enforcePermissionAnnotationStub) + + // Substitutes "backslash + new line" with an empty string to imitate line continuation + private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt new file mode 100644 index 000000000000..5a63bb4084d2 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt @@ -0,0 +1,557 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestMode +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class EnforcePermissionHelperDetectorCodegenTest : LintDetectorTest() { + override fun getDetector(): Detector = EnforcePermissionHelperDetector() + + override fun getIssues(): List<Issue> = listOf( + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun test_generated_IProtected() { + lint().testModes(TestMode.DEFAULT).files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtected extends android.os.IInterface + { + /** Default implementation for IProtected. */ + public static class Default implements android.aidl.tests.permission.IProtected + { + @Override public void PermissionProtected() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtected interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) { + return ((android.aidl.tests.permission.IProtected)iin); + } + return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_PermissionProtected: + { + return "PermissionProtected"; + } + case TRANSACTION_MultiplePermissionsAll: + { + return "MultiplePermissionsAll"; + } + case TRANSACTION_MultiplePermissionsAny: + { + return "MultiplePermissionsAny"; + } + case TRANSACTION_NonManifestPermission: + { + return "NonManifestPermission"; + } + case TRANSACTION_SetGranted: + { + return "SetGranted"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_PermissionProtected: + { + this.PermissionProtected(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAll: + { + this.MultiplePermissionsAll(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAny: + { + this.MultiplePermissionsAny(); + reply.writeNoException(); + break; + } + case TRANSACTION_NonManifestPermission: + { + this.NonManifestPermission(); + reply.writeNoException(); + break; + } + case TRANSACTION_SetGranted: + { + java.util.List<java.lang.String> _arg0; + _arg0 = data.createStringArrayList(); + data.enforceNoDataAvail(); + this.SetGranted(_arg0); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtected + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void PermissionProtected() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStringList(permissions); + boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for PermissionProtected */ + protected void PermissionProtected_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source); + } + static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for MultiplePermissionsAll */ + protected void MultiplePermissionsAll_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); + /** Helper method to enforce permissions for MultiplePermissionsAny */ + protected void MultiplePermissionsAny_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); + /** Helper method to enforce permissions for NonManifestPermission */ + protected void NonManifestPermission_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source); + } + static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); + /** @hide */ + public int getMaxTransactionId() + { + return 4; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void PermissionProtected() throws android.os.RemoteException; + @android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAll() throws android.os.RemoteException; + @android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAny() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) + public void NonManifestPermission() throws android.os.RemoteException; + // Used by the integration tests to dynamically set permissions that are considered granted. + @android.annotation.RequiresNoPermission + public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun test_generated_IProtectedInterface() { + lint().files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtectedInterface extends android.os.IInterface + { + /** Default implementation for IProtectedInterface. */ + public static class Default implements android.aidl.tests.permission.IProtectedInterface + { + @Override public void Method1() throws android.os.RemoteException + { + } + @Override public void Method2() throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) { + return ((android.aidl.tests.permission.IProtectedInterface)iin); + } + return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_Method1: + { + return "Method1"; + } + case TRANSACTION_Method2: + { + return "Method2"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_Method1: + { + this.Method1(); + reply.writeNoException(); + break; + } + case TRANSACTION_Method2: + { + this.Method2(); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtectedInterface + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void Method1() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void Method2() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for Method1 */ + protected void Method1_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for Method2 */ + protected void Method2_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + /** @hide */ + public int getMaxTransactionId() + { + return 1; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method1() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method2() throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + /* Stubs */ + + private val manifestPermissionStub: TestFile = java( + """ + package android.Manifest; + class permission { + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String INTERNET = "android.permission.INTERNET"; + } + """ + ).indented() + + private val enforcePermissionAnnotationStub: TestFile = java( + """ + package android.annotation; + public @interface EnforcePermission {} + """ + ).indented() + + private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub) +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt new file mode 100644 index 000000000000..10a6e1da91dc --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt @@ -0,0 +1,443 @@ +/* +* 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. +*/ + +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask + +class EnforcePermissionHelperDetectorTest : LintDetectorTest() { + override fun getDetector() = EnforcePermissionHelperDetector() + override fun getIssues() = listOf( + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, + EnforcePermissionHelperDetector.ISSUE_MISUSING_ENFORCE_PERMISSION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testFirstExpressionIsFunctionCall() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...: + @@ -8 +8 + + test_enforcePermission(); + + + """ + ) + } + + fun testFirstExpressionIsVariableDeclaration() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + String foo = "bar"; + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...: + @@ -8 +8 + + test_enforcePermission(); + + + """ + ) + } + + fun testMethodIsEmpty() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException {} + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testOkay() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testHelperWithoutSuperPrefix_Okay() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInterfaceDefaultMethod_notStubAncestor_error() { + lint().files( + java( + """ + public interface IProtected extends android.os.IInterface { + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + default void PermissionProtected() throws android.os.RemoteException { + String foo = "bar"; + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/IProtected.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation] + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testInheritance_callSuper_okay() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInheritance_callHelper_okay() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInheritance_missingCallInChain_error() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + doStuff(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/Bar.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testInheritance_missingCall_error() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + doStuff(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/Baz.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testRandomClass_notStubAncestor_error() { + lint().files( + java( + """ + public class Foo { + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + void PermissionProtected() throws android.os.RemoteException { + String foo = "bar"; + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation] + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + ^ + 1 errors, 0 warnings + """ + ) + } + + companion object { + val stubs = arrayOf(aidlStub, contextStub, binderStub) + } +} + + + diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt new file mode 100644 index 000000000000..6b8e72cf9222 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt @@ -0,0 +1,843 @@ +/* + * 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. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = SimpleManualPermissionEnforcementDetector() + override fun getIssues(): List<Issue> = listOf( + SimpleManualPermissionEnforcementDetector + .ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testClass_orSelfFalse_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testClass_enforcesFalse_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAnonClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 8: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testConstantEvaluation() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + } + } + """ + ).indented(), + *stubs, + manifestStub + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 8: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAllOf() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission( + "android.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + - mContext.enforceCallingOrSelfPermission( + - "android.permission.WRITE_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAllOf_mixedOrSelf_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingPermission( + "android.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + - mContext.enforceCallingPermission( + - "android.permission.WRITE_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAllOf_mixedEnforces_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + mContext.checkCallingOrSelfPermission( + "android.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.checkCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + - mContext.checkCallingOrSelfPermission( + - "android.permission.WRITE_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testPrecedingExpressions() { + lint().files( + java( + """ + import android.os.Binder; + import android.test.ITest; + public class Foo extends ITest.Stub { + private mContext Context; + @Override + public void test() throws android.os.RemoteException { + long uid = Binder.getCallingUid(); + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testPermissionHelper() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(orSelf = true) + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:14: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helper(); + ~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 14: Annotate with @EnforcePermission: + @@ -12 +12 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -14 +15 + - helper(); + + test_enforcePermission(); + """ + ) + } + + fun testPermissionHelper_orSelfNotBubbledUp_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helper(); + ~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 14: Annotate with @EnforcePermission: + @@ -12 +12 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -14 +15 + - helper(); + + test_enforcePermission(); + """ + ) + } + + fun testPermissionHelperAllOf() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(orSelf = true) + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:16: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 16: Annotate with @EnforcePermission: + @@ -13 +13 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"}) + @@ -15 +16 + - helper(); + - mContext.enforceCallingOrSelfPermission("FOO", "foo"); + + test_enforcePermission(); + """ + ) + } + + + fun testPermissionHelperNested() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(orSelf = true) + private void helperHelper() { + helper("android.permission.WRITE_CONTACTS"); + } + + @android.annotation.PermissionMethod(orSelf = true) + private void helper(@android.annotation.PermissionName String extraPermission) { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helperHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:19: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helperHelper(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 19: Annotate with @EnforcePermission: + @@ -17 +17 + + @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"}) + @@ -19 +20 + - helperHelper(); + + test_enforcePermission(); + """ + ) + } + + fun testIfExpression() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + - != PackageManager.PERMISSION_GRANTED) { + - throw new SecurityException("yikes!"); + - } + + test_enforcePermission(); + """ + ) + } + + fun testIfExpression_orSelfFalse_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + - != PackageManager.PERMISSION_GRANTED) { + - throw new SecurityException("yikes!"); + - } + + test_enforcePermission(); + """ + ) + } + + fun testIfExpression_otherSideEffect_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + doSomethingElse(); + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testIfExpression_inlinedWithSideEffect_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (somethingElse() && mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + + private boolean somethingElse() { + return true; + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testAnyOf_hardCodedAndVarArgs() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(anyOf = true) + private void helperHelper() { + helper("FOO", "BAR"); + } + + @android.annotation.PermissionMethod(anyOf = true, value = {"BAZ", "BUZZ"}) + private void helper(@android.annotation.PermissionName String... extraPermissions) {} + + @Override + public void test() throws android.os.RemoteException { + helperHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:17: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helperHelper(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 17: Annotate with @EnforcePermission: + @@ -15 +15 + + @android.annotation.EnforcePermission(anyOf={"BAZ", "BUZZ", "FOO", "BAR"}) + @@ -17 +18 + - helperHelper(); + + test_enforcePermission(); + """ + ) + } + + + fun testAnyOfAllOf_mixedConsecutiveCalls_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod + private void allOfhelper() { + mContext.enforceCallingOrSelfPermission("FOO"); + mContext.enforceCallingOrSelfPermission("BAR"); + } + + @android.annotation.PermissionMethod(anyOf = true, permissions = {"BAZ", "BUZZ"}) + private void anyOfHelper() {} + + @Override + public void test() throws android.os.RemoteException { + allOfhelper(); + anyOfHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testAnyOfAllOf_mixedNestedCalls_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.annotation.PermissionName; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(anyOf = true) + private void anyOfCheck(@PermissionName String... permissions) { + allOfCheck("BAZ", "BUZZ"); + } + + @android.annotation.PermissionMethod + private void allOfCheck(@PermissionName String... permissions) {} + + @Override + public void test() throws android.os.RemoteException { + anyOfCheck("FOO", "BAR"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + companion object { + val stubs = arrayOf( + aidlStub, + contextStub, + binderStub, + permissionMethodStub, + permissionNameStub + ) + } +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt new file mode 100644 index 000000000000..2ec8fddbb4e9 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt @@ -0,0 +1,88 @@ +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFile + +val aidlStub: TestFile = java( + """ + package android.test; + public interface ITest extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements android.test.ITest { + protected void test_enforcePermission() throws SecurityException {} + } + public void test() throws android.os.RemoteException; + } + """ +).indented() + +val contextStub: TestFile = java( + """ + package android.content; + public class Context { + @android.annotation.PermissionMethod(orSelf = true) + public void enforceCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {} + @android.annotation.PermissionMethod + public void enforceCallingPermission(@android.annotation.PermissionName String permission, String message) {} + @android.annotation.PermissionMethod(orSelf = true) + public int checkCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {} + @android.annotation.PermissionMethod + public int checkCallingPermission(@android.annotation.PermissionName String permission, String message) {} + } + """ +).indented() + +val binderStub: TestFile = java( + """ + package android.os; + public class Binder { + public static int getCallingUid() {} + } + """ +).indented() + +val permissionMethodStub: TestFile = java( + """ + package android.annotation; + + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({METHOD}) + public @interface PermissionMethod {} + """ +).indented() + +val permissionNameStub: TestFile = java( + """ + package android.annotation; + + import static java.lang.annotation.ElementType.FIELD; + import static java.lang.annotation.ElementType.LOCAL_VARIABLE; + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.ElementType.PARAMETER; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) + public @interface PermissionName {} + """ +).indented() + +val manifestStub: TestFile = java( + """ + package android; + + public final class Manifest { + public static final class permission { + public static final String READ_CONTACTS="android.permission.READ_CONTACTS"; + } + } + """.trimIndent() +)
\ No newline at end of file diff --git a/tools/localedata/OWNERS b/tools/localedata/OWNERS new file mode 100644 index 000000000000..2501679784d6 --- /dev/null +++ b/tools/localedata/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 24949 +include platform/external/icu:/OWNERS diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py index ca1847af7d06..81ac897deae0 100755 --- a/tools/localedata/extract_icu_data.py +++ b/tools/localedata/extract_icu_data.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright 2016 The Android Open Source Project. All Rights Reserved. # @@ -61,7 +61,7 @@ def read_likely_subtags(input_file_name): # would be chosen.) } for line in input_file: - line = unicode(line, 'UTF-8').strip(u' \n\uFEFF').encode('UTF-8') + line = line.strip(u' \n\uFEFF') if line.startswith('//'): continue if '{' in line and '}' in line: @@ -118,26 +118,26 @@ def pack_to_uint32(locale): def dump_script_codes(all_scripts): """Dump the SCRIPT_CODES table.""" - print 'const char SCRIPT_CODES[][4] = {' + print('const char SCRIPT_CODES[][4] = {') for index, script in enumerate(all_scripts): - print " /* %-2d */ {'%c', '%c', '%c', '%c'}," % ( - index, script[0], script[1], script[2], script[3]) - print '};' - print + print(" /* %-2d */ {'%c', '%c', '%c', '%c'}," % ( + index, script[0], script[1], script[2], script[3])) + print('};') + print() def dump_script_data(likely_script_dict, all_scripts): """Dump the script data.""" - print - print 'const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({' + print() + print('const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({') for locale in sorted(likely_script_dict.keys()): script = likely_script_dict[locale] - print ' {0x%08Xu, %2du}, // %s -> %s' % ( + print(' {0x%08Xu, %2du}, // %s -> %s' % ( pack_to_uint32(locale), all_scripts.index(script), locale.replace('_', '-'), - script) - print '});' + script)) + print('});') def pack_to_uint64(locale): @@ -152,13 +152,13 @@ def pack_to_uint64(locale): def dump_representative_locales(representative_locales): """Dump the set of representative locales.""" - print - print 'std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({' + print() + print('std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({') for locale in sorted(representative_locales): - print ' 0x%08XLLU, // %s' % ( + print(' 0x%08XLLU, // %s' % ( pack_to_uint64(locale), - locale) - print '});' + locale)) + print('});') def read_and_dump_likely_data(icu_data_dir): @@ -220,30 +220,30 @@ def get_likely_script(locale, likely_script_dict): def dump_parent_data(script_organized_dict): """Dump information for parents of locales.""" sorted_scripts = sorted(script_organized_dict.keys()) - print + print() for script in sorted_scripts: parent_dict = script_organized_dict[script] print ('const std::unordered_map<uint32_t, uint32_t> %s_PARENTS({' % escape_script_variable_name(script.upper())) for locale in sorted(parent_dict.keys()): parent = parent_dict[locale] - print ' {0x%08Xu, 0x%08Xu}, // %s -> %s' % ( + print(' {0x%08Xu, 0x%08Xu}, // %s -> %s' % ( pack_to_uint32(locale), pack_to_uint32(parent), locale.replace('_', '-'), - parent.replace('_', '-')) - print '});' - print - - print 'const struct {' - print ' const char script[4];' - print ' const std::unordered_map<uint32_t, uint32_t>* map;' - print '} SCRIPT_PARENTS[] = {' + parent.replace('_', '-'))) + print('});') + print() + + print('const struct {') + print(' const char script[4];') + print(' const std::unordered_map<uint32_t, uint32_t>* map;') + print('} SCRIPT_PARENTS[] = {') for script in sorted_scripts: - print " {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % ( + print(" {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % ( script[0], script[1], script[2], script[3], - escape_script_variable_name(script.upper())) - print '};' + escape_script_variable_name(script.upper()))) + print('};') def dump_parent_tree_depth(parent_dict): @@ -256,8 +256,8 @@ def dump_parent_tree_depth(parent_dict): depth += 1 max_depth = max(max_depth, depth) assert max_depth < 5 # Our algorithms assume small max_depth - print - print 'const size_t MAX_PARENT_DEPTH = %d;' % max_depth + print() + print('const size_t MAX_PARENT_DEPTH = %d;' % max_depth) def read_and_dump_parent_data(icu_data_dir, likely_script_dict): @@ -281,8 +281,8 @@ def main(): source_root, 'external', 'icu', 'icu4c', 'source', 'data') - print '// Auto-generated by %s' % sys.argv[0] - print + print('// Auto-generated by %s' % sys.argv[0]) + print() likely_script_dict = read_and_dump_likely_data(icu_data_dir) read_and_dump_parent_data(icu_data_dir, likely_script_dict) diff --git a/tools/locked_region_code_injection/Android.bp b/tools/locked_region_code_injection/Android.bp index 6efd1f64d8fe..954b816a52bf 100644 --- a/tools/locked_region_code_injection/Android.bp +++ b/tools/locked_region_code_injection/Android.bp @@ -12,10 +12,27 @@ java_binary_host { manifest: "manifest.txt", srcs: ["src/**/*.java"], static_libs: [ - "asm-9.2", - "asm-commons-9.2", - "asm-tree-9.2", - "asm-analysis-9.2", - "guava-21.0", + "guava", + "ow2-asm", + "ow2-asm-analysis", + "ow2-asm-commons", + "ow2-asm-tree", + ], +} + +java_library_host { + name: "lockedregioncodeinjection_input", + manifest: "test/manifest.txt", + srcs: ["test/*/*.java"], + static_libs: [ + "guava", + "ow2-asm", + "ow2-asm-analysis", + "ow2-asm-commons", + "ow2-asm-tree", + "hamcrest-library", + "hamcrest", + "platform-test-annotations", + "junit", ], } diff --git a/tools/locked_region_code_injection/OWNERS b/tools/locked_region_code_injection/OWNERS new file mode 100644 index 000000000000..bd43f1736ca5 --- /dev/null +++ b/tools/locked_region_code_injection/OWNERS @@ -0,0 +1,4 @@ +# Everyone in frameworks/base is included by default +shayba@google.com +shombert@google.com +timmurray@google.com diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java index 81a077324e6c..2067bb4ef2fe 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java @@ -13,37 +13,51 @@ */ package lockedregioncodeinjection; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +import static com.google.common.base.Preconditions.checkElementIndex; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.commons.TryCatchBlockSorter; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.TryCatchBlockNode; import org.objectweb.asm.tree.analysis.Analyzer; import org.objectweb.asm.tree.analysis.AnalyzerException; import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.Frame; -import static com.google.common.base.Preconditions.checkElementIndex; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; /** - * This visitor does two things: + * This visitor operates on two kinds of targets. For a legacy target, it does the following: * - * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and insert the corresponding pre + * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and inserts the corresponding pre * and post methods calls should it matches one of the given target type in the Configuration. * * 2. Find all methods that are synchronized and insert pre method calls in the beginning and post * method calls just before all return instructions. + * + * For a scoped target, it does the following: + * + * 1. Finds all the MONITOR_ENTER instructions in the byte code. If the target of the opcode is + * named in a --scope switch, then the pre method is invoked ON THE TARGET immediately after + * MONITOR_ENTER opcode completes. + * + * 2. Finds all the MONITOR_EXIT instructions in the byte code. If the target of the opcode is + * named in a --scope switch, then the post method is invoked ON THE TARGET immediately before + * MONITOR_EXIT opcode completes. */ class LockFindingClassVisitor extends ClassVisitor { private String className = null; @@ -73,12 +87,16 @@ class LockFindingClassVisitor extends ClassVisitor { class LockFindingMethodVisitor extends MethodVisitor { private String owner; private MethodVisitor chain; + private final String className; + private final String methodName; public LockFindingMethodVisitor(String owner, MethodNode mn, MethodVisitor chain) { super(Utils.ASM_VERSION, mn); assert owner != null; this.owner = owner; this.chain = chain; + className = owner; + methodName = mn.name; } @SuppressWarnings("unchecked") @@ -93,6 +111,12 @@ class LockFindingClassVisitor extends ClassVisitor { for (LockTarget t : targets) { if (t.getTargetDesc().equals("L" + owner + ";")) { ownerMonitor = t; + if (ownerMonitor.getScoped()) { + final String emsg = String.format( + "scoped targets do not support synchronized methods in %s.%s()", + className, methodName); + throw new RuntimeException(emsg); + } } } } @@ -118,9 +142,11 @@ class LockFindingClassVisitor extends ClassVisitor { AbstractInsnNode s = instructions.getFirst(); MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPreOwner(), ownerMonitor.getPreMethod(), "()V", false); - insertMethodCallBefore(mn, frameMap, handlersMap, s, 0, call); + insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, 0, call); } + boolean anyDup = false; + for (int i = 0; i < instructions.size(); i++) { AbstractInsnNode s = instructions.get(i); @@ -131,9 +157,15 @@ class LockFindingClassVisitor extends ClassVisitor { LockTargetState state = (LockTargetState) operand; for (int j = 0; j < state.getTargets().size(); j++) { LockTarget target = state.getTargets().get(j); - MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, - target.getPreOwner(), target.getPreMethod(), "()V", false); - insertMethodCallAfter(mn, frameMap, handlersMap, s, i, call); + MethodInsnNode call = methodCall(target, true); + if (target.getScoped()) { + TypeInsnNode cast = typeCast(target); + i += insertInvokeAcquire(mn, frameMap, handlersMap, s, i, + call, cast); + anyDup = true; + } else { + i += insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call); + } } } } @@ -144,8 +176,9 @@ class LockFindingClassVisitor extends ClassVisitor { if (operand instanceof LockTargetState) { LockTargetState state = (LockTargetState) operand; for (int j = 0; j < state.getTargets().size(); j++) { - // The instruction after a monitor_exit should be a label for the end of the implicit - // catch block that surrounds the synchronized block to call monitor_exit when an exception + // The instruction after a monitor_exit should be a label for + // the end of the implicit catch block that surrounds the + // synchronized block to call monitor_exit when an exception // occurs. checkState(instructions.get(i + 1).getType() == AbstractInsnNode.LABEL, "Expected to find label after monitor exit"); @@ -161,9 +194,16 @@ class LockFindingClassVisitor extends ClassVisitor { "Expected label to be the end of monitor exit's try block"); LockTarget target = state.getTargets().get(j); - MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, - target.getPostOwner(), target.getPostMethod(), "()V", false); - insertMethodCallAfter(mn, frameMap, handlersMap, label, labelIndex, call); + MethodInsnNode call = methodCall(target, false); + if (target.getScoped()) { + TypeInsnNode cast = typeCast(target); + i += insertInvokeRelease(mn, frameMap, handlersMap, s, i, + call, cast); + anyDup = true; + } else { + insertMethodCallAfter(mn, frameMap, handlersMap, label, + labelIndex, call); + } } } } @@ -174,16 +214,116 @@ class LockFindingClassVisitor extends ClassVisitor { MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPostOwner(), ownerMonitor.getPostMethod(), "()V", false); - insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call); + insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, i, call); i++; // Skip ahead. Otherwise, we will revisit this instruction again. } } + + if (anyDup) { + mn.maxStack++; + } + super.visitEnd(); mn.accept(chain); } + + // Insert a call to a monitor pre handler. The node and the index identify the + // monitorenter call itself. Insert DUP immediately prior to the MONITORENTER. + // Insert the typecast and call (in that order) after the MONITORENTER. + public int insertInvokeAcquire(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call, TypeInsnNode cast) { + InsnList instructions = mn.instructions; + + // Insert a DUP right before MONITORENTER, to capture the object being locked. + // Note that the object will be typed as java.lang.Object. + instructions.insertBefore(node, new InsnNode(Opcodes.DUP)); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + // Insert the call right after the MONITORENTER. These entries are pushed after + // MONITORENTER so they are inserted in reverse order. MONITORENTER should be + // the target of a try/catch block, which means it must be immediately + // followed by a label (which is part of the try/catch block definition). + // Move forward past the label so the invocation in inside the proper block. + // Throw an error if the next instruction is not a label. + node = node.getNext(); + if (!(node instanceof LabelNode)) { + throw new RuntimeException(String.format("invalid bytecode sequence in %s.%s()", + className, methodName)); + } + node = node.getNext(); + index = instructions.indexOf(node); + + instructions.insertBefore(node, cast); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 3; + } + + // Insert instructions completely before the current opcode. This is slightly + // different from insertMethodCallBefore(), which inserts the call before MONITOREXIT + // but inserts the start and end labels after MONITOREXIT. + public int insertInvokeRelease(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call, TypeInsnNode cast) { + InsnList instructions = mn.instructions; + + instructions.insertBefore(node, new InsnNode(Opcodes.DUP)); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, cast); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 3; + } + } + + public static MethodInsnNode methodCall(LockTarget target, boolean pre) { + String spec = "()V"; + if (!target.getScoped()) { + if (pre) { + return new MethodInsnNode( + Opcodes.INVOKESTATIC, target.getPreOwner(), target.getPreMethod(), spec); + } else { + return new MethodInsnNode( + Opcodes.INVOKESTATIC, target.getPostOwner(), target.getPostMethod(), spec); + } + } else { + if (pre) { + return new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, target.getPreOwner(), target.getPreMethod(), spec); + } else { + return new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, target.getPostOwner(), target.getPostMethod(), spec); + } + } } - public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap, + public static TypeInsnNode typeCast(LockTarget target) { + if (!target.getScoped()) { + return null; + } else { + // preOwner and postOwner return the same string for scoped targets. + return new TypeInsnNode(Opcodes.CHECKCAST, target.getPreOwner()); + } + } + + /** + * Insert a method call before the beginning or end of a synchronized method. + */ + public static void insertMethodCallBeforeSync(MethodNode mn, List<Frame> frameMap, List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, MethodInsnNode call) { List<TryCatchBlockNode> handlers = handlersMap.get(index); @@ -226,6 +366,22 @@ class LockFindingClassVisitor extends ClassVisitor { updateCatchHandler(mn, handlers, start, end, handlersMap); } + // Insert instructions completely before the current opcode. This is slightly different from + // insertMethodCallBeforeSync(), which inserts the call before MONITOREXIT but inserts the + // start and end labels after MONITOREXIT. + public int insertMethodCallBefore(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call) { + InsnList instructions = mn.instructions; + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 1; + } + + @SuppressWarnings("unchecked") public static void updateCatchHandler(MethodNode mn, List<TryCatchBlockNode> handlers, LabelNode start, LabelNode end, List<List<TryCatchBlockNode>> handlersMap) { diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java index c5e59e3dc64d..5f6240327a7f 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java @@ -21,14 +21,28 @@ package lockedregioncodeinjection; public class LockTarget { public static final LockTarget NO_TARGET = new LockTarget("", null, null); + // The lock which must be instrumented, in Java internal form (L<path>;). private final String targetDesc; + // The methods to be called when the lock is taken (released). For non-scoped locks, + // these are fully qualified static methods. For scoped locks, these are the + // unqualified names of a member method of the target lock. private final String pre; private final String post; + // If true, the pre and post methods are virtual on the target class. The pre and post methods + // are both called while the lock is held. If this field is false then the pre and post methods + // take no parameters and the post method is called after the lock is released. This is legacy + // behavior. + private final boolean scoped; - public LockTarget(String targetDesc, String pre, String post) { + public LockTarget(String targetDesc, String pre, String post, boolean scoped) { this.targetDesc = targetDesc; this.pre = pre; this.post = post; + this.scoped = scoped; + } + + public LockTarget(String targetDesc, String pre, String post) { + this(targetDesc, pre, post, false); } public String getTargetDesc() { @@ -40,7 +54,11 @@ public class LockTarget { } public String getPreOwner() { - return pre.substring(0, pre.lastIndexOf('.')); + if (scoped) { + return targetDesc.substring(1, targetDesc.length() - 1); + } else { + return pre.substring(0, pre.lastIndexOf('.')); + } } public String getPreMethod() { @@ -52,10 +70,23 @@ public class LockTarget { } public String getPostOwner() { - return post.substring(0, post.lastIndexOf('.')); + if (scoped) { + return targetDesc.substring(1, targetDesc.length() - 1); + } else { + return post.substring(0, post.lastIndexOf('.')); + } } public String getPostMethod() { return post.substring(post.lastIndexOf('.') + 1); } + + public boolean getScoped() { + return scoped; + } + + @Override + public String toString() { + return targetDesc + ":" + pre + ":" + post; + } } diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java index 99d841829132..5df0160abd8c 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java @@ -13,10 +13,11 @@ */ package lockedregioncodeinjection; -import java.util.List; import org.objectweb.asm.Type; import org.objectweb.asm.tree.analysis.BasicValue; +import java.util.List; + public class LockTargetState extends BasicValue { private final List<LockTarget> lockTargets; @@ -31,4 +32,10 @@ public class LockTargetState extends BasicValue { public List<LockTarget> getTargets() { return lockTargets; } + + @Override + public String toString() { + return "LockTargetState(" + getType().getDescriptor() + + ", " + lockTargets.size() + ")"; + } } diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java index 828cce72dda9..d22ea2338ff7 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java @@ -21,7 +21,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Collections; +import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; @@ -36,6 +36,7 @@ public class Main { String legacyTargets = null; String legacyPreMethods = null; String legacyPostMethods = null; + List<LockTarget> targets = new ArrayList<>(); for (int i = 0; i < args.length; i++) { if ("-i".equals(args[i].trim())) { i++; @@ -52,23 +53,25 @@ public class Main { } else if ("--post".equals(args[i].trim())) { i++; legacyPostMethods = args[i].trim(); + } else if ("--scoped".equals(args[i].trim())) { + i++; + targets.add(Utils.getScopedTarget(args[i].trim())); } - } - // TODO(acleung): Better help message than asserts. - assert inJar != null; - assert outJar != null; + if (inJar == null) { + throw new RuntimeException("missing input jar path"); + } + if (outJar == null) { + throw new RuntimeException("missing output jar path"); + } assert legacyTargets == null || (legacyPreMethods != null && legacyPostMethods != null); ZipFile zipSrc = new ZipFile(inJar); ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outJar)); - List<LockTarget> targets = null; if (legacyTargets != null) { - targets = Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods, - legacyPostMethods); - } else { - targets = Collections.emptyList(); + targets.addAll(Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods, + legacyPostMethods)); } Enumeration<? extends ZipEntry> srcEntries = zipSrc.entries(); diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java index f1e84b1d8a33..bfef10611e6c 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java @@ -20,7 +20,7 @@ import java.util.List; public class Utils { - public static final int ASM_VERSION = Opcodes.ASM7; + public static final int ASM_VERSION = Opcodes.ASM9; /** * Reads a comma separated configuration similar to the Jack definition. @@ -44,4 +44,27 @@ public class Utils { return config; } + + /** + * Returns a single {@link LockTarget} from a string. The target is a comma-separated list of + * the target class, the request method, the release method, and a boolean which is true if this + * is a scoped target and false if this is a legacy target. The boolean is optional and + * defaults to true. + */ + public static LockTarget getScopedTarget(String arg) { + String[] c = arg.split(","); + if (c.length == 3) { + return new LockTarget(c[0], c[1], c[2], true); + } else if (c.length == 4) { + if (c[3].equals("true")) { + return new LockTarget(c[0], c[1], c[2], true); + } else if (c[3].equals("false")) { + return new LockTarget(c[0], c[1], c[2], false); + } else { + System.err.println("illegal target parameter \"" + c[3] + "\""); + } + } + // Fall through + throw new RuntimeException("invalid scoped target format"); + } } diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java index 31fa0bf63416..28f00b9c897a 100644 --- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java @@ -17,7 +17,10 @@ import org.junit.Assert; import org.junit.Test; /** - * To run the unit tests: + * To run the unit tests, first build the two necessary artifacts. Do this explicitly as they are + * not generally retained by a normal "build all". After lunching a target: + * m lockedregioncodeinjection + * m lockedregioncodeinjection_input * * <pre> * <code> @@ -29,31 +32,25 @@ import org.junit.Test; * mkdir -p out * rm -fr out/* * - * # Make booster - * javac -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar src/*/*.java -d out/ - * pushd out - * jar cfe lockedregioncodeinjection.jar lockedregioncodeinjection.Main */*.class - * popd - * - * # Make unit tests. - * javac -cp lib/junit-4.12.jar test/*/*.java -d out/ - * - * pushd out - * jar cfe test_input.jar lockedregioncodeinjection.Test */*.class - * popd + * # Paths to the build artifacts. These assume linux-x86; YMMV. + * ROOT=$TOP/out/host/linux-x86 + * EXE=$ROOT/bin/lockedregioncodeinjection + * INPUT=$ROOT/frameworkd/lockedregioncodeinjection_input.jar * * # Run tool on unit tests. - * java -ea -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar:out/lockedregioncodeinjection.jar \ - * lockedregioncodeinjection.Main \ - * -i out/test_input.jar -o out/test_output.jar \ + * $EXE -i $INPUT -o out/test_output.jar \ * --targets 'Llockedregioncodeinjection/TestTarget;' \ * --pre 'lockedregioncodeinjection/TestTarget.boost' \ * --post 'lockedregioncodeinjection/TestTarget.unboost' * * # Run unit tests. - * java -ea -cp lib/hamcrest-core-1.3.jar:lib/junit-4.12.jar:out/test_output.jar \ + * java -ea -cp out/test_output.jar \ * org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain * </code> + * OR + * <code> + * bash test/unit-test.sh + * </code> * </pre> */ public class TestMain { @@ -64,7 +61,9 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); - Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.invokeCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); synchronized (t) { Assert.assertEquals(TestTarget.boostCount, 1); @@ -75,6 +74,8 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -84,12 +85,16 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); t.synchronizedCall(); Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -99,12 +104,16 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); t.synchronizedCallReturnInt(); Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -253,4 +262,125 @@ public class TestMain { Assert.assertEquals(TestTarget.invokeCount, 1); } + @Test + public void testScopedTarget() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(0, target.scopedLock().mLevel); + + synchronized (target.scopedLock()) { + Assert.assertEquals(1, target.scopedLock().mLevel); + } + Assert.assertEquals(0, target.scopedLock().mLevel); + + synchronized (target.scopedLock()) { + synchronized (target.scopedLock()) { + Assert.assertEquals(2, target.scopedLock().mLevel); + } + } + Assert.assertEquals(0, target.scopedLock().mLevel); + } + + @Test + public void testScopedExceptionHandling() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(0, target.scopedLock().mLevel); + + boolean handled; + + // 1: an exception inside the block properly releases the lock. + handled = false; + try { + synchronized (target.scopedLock()) { + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + Assert.assertEquals(1, target.scopedLock().mLevel); + throw new RuntimeException(); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + + // 2: An exception inside the monitor enter function + handled = false; + target.throwOnEnter(true); + try { + synchronized (target.scopedLock()) { + // The exception was thrown inside monitorEnter(), so the code should + // never reach this point. + Assert.assertEquals(0, 1); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + + // 3: An exception inside the monitor exit function + handled = false; + target.throwOnEnter(true); + try { + synchronized (target.scopedLock()) { + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + Assert.assertEquals(1, target.scopedLock().mLevel); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + } + + // Provide an in-class type conversion for the scoped target. + private Object untypedLock(TestScopedTarget target) { + return target.scopedLock(); + } + + @Test + public void testScopedLockTyping() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(target.scopedLock().mLevel, 0); + + // Scoped lock injection works on the static type of an object. In general, it is + // a very bad idea to do type conversion on scoped locks, but the general rule is + // that conversions within a single method are recognized by the lock injection + // tool and injection occurs. Conversions outside a single method are not + // recognized and injection does not occur. + + // 1. Conversion occurs outside the class. The visible type of the lock is Object + // in this block, so no injection takes place on 'untypedLock', even though the + // dynamic type is TestScopedLock. + synchronized (target.untypedLock()) { + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + + // 2. Conversion occurs inside the class but in another method. The visible type + // of the lock is Object in this block, so no injection takes place on + // 'untypedLock', even though the dynamic type is TestScopedLock. + synchronized (untypedLock(target)) { + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + + // 3. Conversion occurs inside the method. The compiler can determine the type of + // the lock within a single function, so injection does take place here. + Object untypedLock = target.scopedLock(); + synchronized (untypedLock) { + Assert.assertEquals(1, target.scopedLock().mLevel); + Assert.assertEquals(true, untypedLock instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + } } diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java new file mode 100644 index 000000000000..7441d2b1da48 --- /dev/null +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 lockedregioncodeinjection; + +public class TestScopedLock { + public int mEntered = 0; + public int mExited = 0; + + public int mLevel = 0; + private final TestScopedTarget mTarget; + + TestScopedLock(TestScopedTarget target) { + mTarget = target; + } + + void monitorEnter() { + mLevel++; + mEntered++; + mTarget.enter(mLevel); + } + + void monitorExit() { + mLevel--; + mExited++; + mTarget.exit(mLevel); + } +} diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java new file mode 100644 index 000000000000..c80975e9c6b3 --- /dev/null +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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 lockedregioncodeinjection; + +public class TestScopedTarget { + + public final TestScopedLock mLock;; + + private boolean mNextEnterThrows = false; + private boolean mNextExitThrows = false; + + TestScopedTarget() { + mLock = new TestScopedLock(this); + } + + TestScopedLock scopedLock() { + return mLock; + } + + Object untypedLock() { + return mLock; + } + + void enter(int level) { + if (mNextEnterThrows) { + mNextEnterThrows = false; + throw new RuntimeException(); + } + } + + void exit(int level) { + if (mNextExitThrows) { + mNextExitThrows = false; + throw new RuntimeException(); + } + } + + void throwOnEnter(boolean b) { + mNextEnterThrows = b; + } + + void throwOnExit(boolean b) { + mNextExitThrows = b; + } +} diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java index d1c8f340a598..e3ba6a77e3b7 100644 --- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java @@ -19,8 +19,17 @@ public class TestTarget { public static int invokeCount = 0; public static boolean nextUnboostThrows = false; + // If this is not null, then this is the lock under test. The lock must not be held when boost() + // or unboost() are called. + public static Object mLock = null; + public static int boostCountLocked = 0; + public static int unboostCountLocked = 0; + public static void boost() { boostCount++; + if (mLock != null && Thread.currentThread().holdsLock(mLock)) { + boostCountLocked++; + } } public static void unboost() { @@ -29,6 +38,9 @@ public class TestTarget { throw new RuntimeException(); } unboostCount++; + if (mLock != null && Thread.currentThread().holdsLock(mLock)) { + unboostCountLocked++; + } } public static void invoke() { diff --git a/tools/locked_region_code_injection/test/manifest.txt b/tools/locked_region_code_injection/test/manifest.txt new file mode 100644 index 000000000000..2314c188721c --- /dev/null +++ b/tools/locked_region_code_injection/test/manifest.txt @@ -0,0 +1 @@ +Main-Class: org.junit.runner.JUnitCore diff --git a/tools/locked_region_code_injection/test/unit-test.sh b/tools/locked_region_code_injection/test/unit-test.sh new file mode 100755 index 000000000000..9fa6f39af14a --- /dev/null +++ b/tools/locked_region_code_injection/test/unit-test.sh @@ -0,0 +1,98 @@ +#! /bin/bash +# + +# Copyright (C) 2023 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. + +# This script runs the tests for the lockedregioninjectioncode. See +# TestMain.java for the invocation. The script expects that a full build has +# already been done and artifacts are in $TOP/out. + +# Compute the default top of the workspace. The following code copies the +# strategy of croot. (croot cannot be usd directly because it is a function and +# functions are not carried over into subshells.) This gives the correct answer +# if run from inside a workspace. If run from outside a workspace, supply TOP +# on the command line. +TOPFILE=build/make/core/envsetup.mk +TOP=$(dirname $(realpath $0)) +while [[ ! $TOP = / && ! -f $TOP/$TOPFILE ]]; do + TOP=$(dirname $TOP) +done +# TOP is "/" if this script is located outside a workspace. + +# If the user supplied a top directory, use it instead +if [[ -n $1 ]]; then + TOP=$1 + shift +fi +if [[ -z $TOP || $TOP = / ]]; then + echo "usage: $0 <workspace-root>" + exit 1 +elif [[ ! -d $TOP ]]; then + echo "$TOP is not a directory" + exit 1 +elif [[ ! -d $TOP/prebuilts/misc/common ]]; then + echo "$TOP does not look like w workspace" + exit 1 +fi +echo "Using workspace $TOP" + +# Pick up the current java compiler. The lunch target is not very important, +# since most, if not all, will use the same host binaries. +pushd $TOP > /dev/null +. build/envsetup.sh > /dev/null 2>&1 +lunch redfin-userdebug > /dev/null 2>&1 +popd > /dev/null + +# Bail on any error +set -o pipefail +trap 'exit 1' ERR + +# Create the two sources +pushd $TOP > /dev/null +m lockedregioncodeinjection +m lockedregioncodeinjection_input +popd > /dev/null + +# Create a temporary directory outside of the workspace. +OUT=$TOP/out/host/test/lockedregioncodeinjection +echo + +# Clean the directory +if [[ -d $OUT ]]; then rm -r $OUT; fi +mkdir -p $OUT + +ROOT=$TOP/out/host/linux-x86 +EXE=$ROOT/bin/lockedregioncodeinjection +INP=$ROOT/framework/lockedregioncodeinjection_input.jar + +# Run tool on unit tests. +$EXE \ + -i $INP -o $OUT/test_output.jar \ + --targets 'Llockedregioncodeinjection/TestTarget;' \ + --pre 'lockedregioncodeinjection/TestTarget.boost' \ + --post 'lockedregioncodeinjection/TestTarget.unboost' \ + --scoped 'Llockedregioncodeinjection/TestScopedLock;,monitorEnter,monitorExit' + +# Run unit tests. +java -ea -cp $OUT/test_output.jar \ + org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain + +# Extract the class files and decompile them for possible post-analysis. +pushd $OUT > /dev/null +jar -x --file test_output.jar lockedregioncodeinjection +for class in lockedregioncodeinjection/*.class; do + javap -c -v $class > ${class%.class}.asm +done +popd > /dev/null + +echo "artifacts are in $OUT" diff --git a/tools/preload-check/AndroidTest.xml b/tools/preload-check/AndroidTest.xml index a0645d5b1051..d486f0f914c6 100644 --- a/tools/preload-check/AndroidTest.xml +++ b/tools/preload-check/AndroidTest.xml @@ -14,7 +14,7 @@ limitations under the License. --> <configuration description="Config for PreloadCheck"> - <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> <option name="push" value="preload-check-device.jar->/data/local/tmp/preload-check-device.jar" /> </target_preparer> diff --git a/tools/preload-check/OWNERS b/tools/preload-check/OWNERS new file mode 100644 index 000000000000..e71c7332ca90 --- /dev/null +++ b/tools/preload-check/OWNERS @@ -0,0 +1 @@ +include /ZYGOTE_OWNERS diff --git a/tools/preload/loadclass/LoadClass.java b/tools/preload/loadclass/LoadClass.java index a71b6a8b145e..3f6658ab8c65 100644 --- a/tools/preload/loadclass/LoadClass.java +++ b/tools/preload/loadclass/LoadClass.java @@ -14,8 +14,8 @@ * limitations under the License. */ -import android.util.Log; import android.os.Debug; +import android.util.Log; /** * Loads a class, runs the garbage collector, and prints showmap output. @@ -28,7 +28,7 @@ class LoadClass { System.loadLibrary("android_runtime"); if (registerNatives() < 0) { - throw new RuntimeException("Error registering natives."); + throw new RuntimeException("Error registering natives."); } Debug.startAllocCounting(); @@ -46,7 +46,7 @@ class LoadClass { } } - System.gc(); + Runtime.getRuntime().gc(); int allocCount = Debug.getGlobalAllocCount(); int allocSize = Debug.getGlobalAllocSize(); @@ -73,7 +73,7 @@ class LoadClass { response.append(',').append(freedCount); response.append(',').append(freedSize); response.append(',').append(nativeHeapSize); - + System.out.println(response.toString()); } diff --git a/tools/processors/intdef_mappings/Android.bp b/tools/processors/intdef_mappings/Android.bp index 82a5dac21160..7059c52ddc76 100644 --- a/tools/processors/intdef_mappings/Android.bp +++ b/tools/processors/intdef_mappings/Android.bp @@ -7,27 +7,33 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -java_plugin { - name: "intdef-annotation-processor", - - processor_class: "android.processor.IntDefProcessor", +java_library_host { + name: "libintdef-annotation-processor", srcs: [ ":framework-annotations", "src/**/*.java", - "src/**/*.kt" + "src/**/*.kt", ], use_tools_jar: true, } +java_plugin { + name: "intdef-annotation-processor", + + processor_class: "android.processor.IntDefProcessor", + + static_libs: ["libintdef-annotation-processor"], +} + java_test_host { name: "intdef-annotation-processor-test", srcs: [ "test/**/*.java", - "test/**/*.kt" - ], + "test/**/*.kt", + ], java_resource_dirs: ["test/resources"], static_libs: [ @@ -35,7 +41,7 @@ java_test_host { "truth-prebuilt", "junit", "guava", - "intdef-annotation-processor" + "libintdef-annotation-processor", ], test_suites: ["general-tests"], diff --git a/tools/processors/staledataclass/Android.bp b/tools/processors/staledataclass/Android.bp index 1e5097662a0a..2169c49a91a5 100644 --- a/tools/processors/staledataclass/Android.bp +++ b/tools/processors/staledataclass/Android.bp @@ -22,17 +22,13 @@ java_plugin { static_libs: [ "codegen-version-info", ], - // The --add-modules/exports flags below don't work for kotlinc yet, so pin this module to Java language level 8 (see b/139342589): - java_version: "1.8", - openjdk9: { - javacflags: [ - "--add-modules=jdk.compiler", - "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", - "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", - "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", - "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", - ], - }, + javacflags: [ + "--add-modules=jdk.compiler", + "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + ], use_tools_jar: true, } diff --git a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt index 2e60f64b21e8..1cef5b0c8dfb 100644 --- a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt +++ b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE") package android.processor.staledataclass @@ -97,7 +98,7 @@ class StaleDataclassProcessor: AbstractProcessor() { private fun elemToString(elem: Element): String { return buildString { - append(elem.modifiers.joinToString(" ") { it.name.toLowerCase() }) + append(elem.modifiers.joinToString(" ") { it.name.lowercase() }) append(" ") append(elem.annotationMirrors.joinToString(" ", transform = { annotationToString(it) })) append(" ") diff --git a/tools/processors/view_inspector/Android.bp b/tools/processors/view_inspector/Android.bp index ea9974f06a64..877a1d2b5fb3 100644 --- a/tools/processors/view_inspector/Android.bp +++ b/tools/processors/view_inspector/Android.bp @@ -7,10 +7,8 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -java_plugin { - name: "view-inspector-annotation-processor", - - processor_class: "android.processor.view.inspector.PlatformInspectableProcessor", +java_library_host { + name: "libview-inspector-annotation-processor", srcs: ["src/java/**/*.java"], java_resource_dirs: ["src/resources"], @@ -23,6 +21,16 @@ java_plugin { use_tools_jar: true, } +java_plugin { + name: "view-inspector-annotation-processor", + + processor_class: "android.processor.view.inspector.PlatformInspectableProcessor", + + static_libs: [ + "libview-inspector-annotation-processor", + ], +} + java_test_host { name: "view-inspector-annotation-processor-test", @@ -32,7 +40,7 @@ java_test_host { static_libs: [ "junit", "guava", - "view-inspector-annotation-processor" + "libview-inspector-annotation-processor", ], test_suites: ["general-tests"], diff --git a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt index a52c8042582b..451e514b8b33 100644 --- a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt +++ b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt @@ -29,7 +29,7 @@ object CodeUtils { */ fun hash(position: String, messageString: String, logLevel: LogLevel, logGroup: LogGroup): Int { return (position + messageString + logLevel.name + logGroup.name) - .map { c -> c.toInt() }.reduce { h, c -> h * 31 + c } + .map { c -> c.code }.reduce { h, c -> h * 31 + c } } fun checkWildcardStaticImported(code: CompilationUnit, className: String, fileName: String) { diff --git a/tools/sdkparcelables/Android.bp b/tools/sdkparcelables/Android.bp index 6ebacd8a0b14..6503a1f3a5f8 100644 --- a/tools/sdkparcelables/Android.bp +++ b/tools/sdkparcelables/Android.bp @@ -14,7 +14,7 @@ java_binary_host { "src/**/*.kt", ], static_libs: [ - "asm-9.2", + "ow2-asm", ], } diff --git a/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt index 0fb062f280e3..0b619488c49c 100644 --- a/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt +++ b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt @@ -39,7 +39,7 @@ fun main(args: Array<String>) { kotlin.system.exitProcess(2) } - val ancestorCollector = AncestorCollector(Opcodes.ASM7, null) + val ancestorCollector = AncestorCollector(Opcodes.ASM9, null) for (entry in zipFile.entries()) { if (entry.name.endsWith(".class")) { diff --git a/tools/stringslint/stringslint.py b/tools/stringslint/stringslint.py deleted file mode 100644 index 15088fc81e88..000000000000 --- a/tools/stringslint/stringslint.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python3 -#-*- coding: utf-8 -*- - -# Copyright (C) 2018 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. - -""" -Enforces common Android string best-practices. It ignores lint messages from -a previous strings file, if provided. - -Usage: stringslint.py strings.xml -Usage: stringslint.py strings.xml old_strings.xml - -In general: -* Errors signal issues that must be fixed before submitting, and are only - used when there are no false-positives. -* Warnings signal issues that might need to be fixed, but need manual - inspection due to risk of false-positives. -* Info signal issues that should be fixed to match best-practices, such - as providing comments to aid translation. -""" - -import re, sys, codecs -import lxml.etree as ET - -BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) - -def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): - # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes - codes = [] - if reset: codes.append("0") - else: - if not fg is None: codes.append("3%d" % (fg)) - if not bg is None: - if not bright: codes.append("4%d" % (bg)) - else: codes.append("10%d" % (bg)) - if bold: codes.append("1") - elif dim: codes.append("2") - else: codes.append("22") - return "\033[%sm" % (";".join(codes)) - -warnings = None - -def warn(tag, msg, actual, expected, color=YELLOW): - global warnings - key = "%s:%d" % (tag.attrib["name"], hash(msg)) - value = "%sLine %d: '%s':%s %s" % (format(fg=color, bold=True), - tag.sourceline, - tag.attrib["name"], - format(reset=True), - msg) - if not actual is None: value += "\n\tActual: %s%s%s" % (format(dim=True), - actual, - format(reset=True)) - if not expected is None: value += "\n\tExample: %s%s%s" % (format(dim=True), - expected, - format(reset=True)) - warnings[key] = value - - -def error(tag, msg, actual, expected): - warn(tag, msg, actual, expected, RED) - -def info(tag, msg, actual, expected): - warn(tag, msg, actual, expected, CYAN) - -# Escaping logic borrowed from https://stackoverflow.com/a/24519338 -ESCAPE_SEQUENCE_RE = re.compile(r''' - ( \\U........ # 8-digit hex escapes - | \\u.... # 4-digit hex escapes - | \\x.. # 2-digit hex escapes - | \\[0-7]{1,3} # Octal escapes - | \\N\{[^}]+\} # Unicode characters by name - | \\[\\'"abfnrtv] # Single-character escapes - )''', re.UNICODE | re.VERBOSE) - -def decode_escapes(s): - def decode_match(match): - return codecs.decode(match.group(0), 'unicode-escape') - - s = re.sub(r"\n\s*", " ", s) - s = ESCAPE_SEQUENCE_RE.sub(decode_match, s) - s = re.sub(r"%(\d+\$)?[a-z]", "____", s) - s = re.sub(r"\^\d+", "____", s) - s = re.sub(r"<br/?>", "\n", s) - s = re.sub(r"</?[a-z]+>", "", s) - return s - -def sample_iter(tag): - if not isinstance(tag, ET._Comment) and re.match("{.*xliff.*}g", tag.tag) and "example" in tag.attrib: - yield tag.attrib["example"] - elif tag.text: - yield decode_escapes(tag.text) - for e in tag: - for v in sample_iter(e): - yield v - if e.tail: - yield decode_escapes(e.tail) - -def lint(path): - global warnings - warnings = {} - - with open(path) as f: - raw = f.read() - if len(raw.strip()) == 0: - return warnings - tree = ET.fromstring(bytes(raw, encoding='utf-8')) - root = tree #tree.getroot() - - last_comment = None - for child in root: - # TODO: handle plurals - if isinstance(child, ET._Comment): - last_comment = child - elif child.tag == "string": - # We always consume comment - comment = last_comment - last_comment = None - - # Prepare string for analysis - text = "".join(child.itertext()) - sample = "".join(sample_iter(child)).strip().strip("'\"") - - # Validate comment - if comment is None: - info(child, "Missing string comment to aid translation", - None, None) - continue - if "do not translate" in comment.text.lower(): - continue - if "translatable" in child.attrib and child.attrib["translatable"].lower() == "false": - continue - - misspelled_attributes = [ - ("translateable", "translatable"), - ] - for misspelling, expected in misspelled_attributes: - if misspelling in child.attrib: - error(child, "Misspelled <string> attribute.", misspelling, expected) - - limit = re.search("CHAR[ _-]LIMIT=(\d+|NONE|none)", comment.text) - if limit is None: - info(child, "Missing CHAR LIMIT to aid translation", - repr(comment), "<!-- Description of string [CHAR LIMIT=32] -->") - elif re.match("\d+", limit.group(1)): - limit = int(limit.group(1)) - if len(sample) > limit: - warn(child, "Expanded string length is larger than CHAR LIMIT", - sample, None) - - # Look for common mistakes/substitutions - if "'" in text: - error(child, "Turned quotation mark glyphs are more polished", - text, "This doesn\u2019t need to \u2018happen\u2019 today") - if '"' in text and not text.startswith('"') and text.endswith('"'): - error(child, "Turned quotation mark glyphs are more polished", - text, "This needs to \u201chappen\u201d today") - if "..." in text: - error(child, "Ellipsis glyph is more polished", - text, "Loading\u2026") - if "wi-fi" in text.lower(): - error(child, "Non-breaking glyph is more polished", - text, "Wi\u2011Fi") - if "wifi" in text.lower(): - error(child, "Using non-standard spelling", - text, "Wi\u2011Fi") - if re.search("\d-\d", text): - warn(child, "Ranges should use en dash glyph", - text, "You will find this material in chapters 8\u201312") - if "--" in text: - warn(child, "Phrases should use em dash glyph", - text, "Upon discovering errors\u2014all 124 of them\u2014they recalled.") - if ". " in text: - warn(child, "Only use single space between sentences", - text, "First idea. Second idea.") - if re.match(r"^[A-Z\s]{5,}$", text): - warn(child, "Actions should use android:textAllCaps in layout; ignore if acronym", - text, "Refresh data") - if " phone " in text and "product" not in child.attrib: - warn(child, "Strings mentioning phones should have variants for tablets", - text, None) - - # When more than one substitution, require indexes - if len(re.findall("%[^%]", text)) > 1: - if len(re.findall("%[^\d]", text)) > 0: - error(child, "Substitutions must be indexed", - text, "Add %1$s to %2$s") - - # Require xliff substitutions - for gc in child.iter(): - badsub = False - if gc.tail and re.search("%[^%]", gc.tail): badsub = True - if re.match("{.*xliff.*}g", gc.tag): - if "id" not in gc.attrib: - error(child, "Substitutions must define id attribute", - None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") - if "example" not in gc.attrib: - error(child, "Substitutions must define example attribute", - None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") - else: - if gc.text and re.search("%[^%]", gc.text): badsub = True - if badsub: - error(child, "Substitutions must be inside xliff tags", - text, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") - - return warnings - -if len(sys.argv) > 2: - before = lint(sys.argv[2]) -else: - before = {} -after = lint(sys.argv[1]) - -for b in before: - if b in after: - del after[b] - -if len(after) > 0: - for a in sorted(after.keys()): - print(after[a]) - print() - sys.exit(1) diff --git a/tools/stringslint/stringslint_sha.sh b/tools/stringslint/stringslint_sha.sh index bd0569873197..009a1f284bf0 100755 --- a/tools/stringslint/stringslint_sha.sh +++ b/tools/stringslint/stringslint_sha.sh @@ -1,5 +1,3 @@ #!/bin/bash -LOCAL_DIR="$( dirname ${BASH_SOURCE} )" -git show --name-only --pretty=format: $1 | grep values/strings.xml | while read file; do - python3 $LOCAL_DIR/stringslint.py <(git show $1:$file) <(git show $1^:$file) -done + +# NOTE: this script has been deprecated and replaced by AyeAye checks directly in Gerrit diff --git a/tools/traceinjection/Android.bp b/tools/traceinjection/Android.bp index 39d1b1c2defd..d627fb99d882 100644 --- a/tools/traceinjection/Android.bp +++ b/tools/traceinjection/Android.bp @@ -12,11 +12,11 @@ java_binary_host { manifest: "manifest.txt", srcs: ["src/**/*.java"], static_libs: [ - "asm-9.2", - "asm-commons-9.2", - "asm-tree-9.2", - "asm-analysis-9.2", - "guava-21.0", + "ow2-asm", + "ow2-asm-commons", + "ow2-asm-tree", + "ow2-asm-analysis", + "guava", ], } diff --git a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java index 863f976b8aff..67c5561f348d 100644 --- a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java +++ b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java @@ -28,7 +28,7 @@ public class TraceInjectionClassVisitor extends ClassVisitor { private final TraceInjectionConfiguration mParams; public TraceInjectionClassVisitor(ClassVisitor classVisitor, TraceInjectionConfiguration params) { - super(Opcodes.ASM7, classVisitor); + super(Opcodes.ASM9, classVisitor); mParams = params; } diff --git a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java index c2bbddcb5668..91e987dc72ee 100644 --- a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java +++ b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java @@ -61,7 +61,7 @@ public class TraceInjectionMethodAdapter extends AdviceAdapter { public TraceInjectionMethodAdapter(MethodVisitor methodVisitor, int access, String name, String descriptor, TraceInjectionConfiguration params) { - super(Opcodes.ASM7, methodVisitor, access, name, descriptor); + super(Opcodes.ASM9, methodVisitor, access, name, descriptor); mParams = params; mIsConstructor = "<init>".equals(name); } @@ -157,7 +157,7 @@ public class TraceInjectionMethodAdapter extends AdviceAdapter { class TracingAnnotationVisitor extends AnnotationVisitor { TracingAnnotationVisitor(AnnotationVisitor annotationVisitor) { - super(Opcodes.ASM7, annotationVisitor); + super(Opcodes.ASM9, annotationVisitor); } @Override diff --git a/tools/validatekeymaps/OWNERS b/tools/validatekeymaps/OWNERS index 0313a40f7270..4c20c4dc9d35 100644 --- a/tools/validatekeymaps/OWNERS +++ b/tools/validatekeymaps/OWNERS @@ -1,2 +1 @@ -michaelwr@google.com -svv@google.com +include /INPUT_OWNERS |