Fix Core Platform API JNI check

Fix an inverted flag around kIsTargetBuild that broke check.

Simplify SharedObjectKind's so there are only values - part of the ART
module and other. Being part of the module is equivalent to
DexDomain::CorePlatform.

Add a test for Core Platform API checks for native code. The test is
derived from 674-hiddenapi, and separate because the existing test is
already complex.

Add 2030-core-platform-api-jni to the same knownfailures as
674-hiddenapi since the newer test likely to have the same failures.

Bug: 128517070
Bug: 144502743
Test: art/test/run-test --host 2030
Test: art/test/run-test 2030
Test: erroneous warnings gone from master
Change-Id: I1a365a28b89bd46b3e403f6deb736f350c4baf10
diff --git a/runtime/hidden_api_jni.cc b/runtime/hidden_api_jni.cc
index 074c4c3..2e32224 100644
--- a/runtime/hidden_api_jni.cc
+++ b/runtime/hidden_api_jni.cc
@@ -25,6 +25,7 @@
 #include <mutex>
 
 #include "android-base/logging.h"
+#include "android-base/thread_annotations.h"
 
 #include "unwindstack/Regs.h"
 #include "unwindstack/RegsGetLocal.h"
@@ -45,7 +46,7 @@
 
 // The maximum number of frames to back trace through when performing Core Platform API checks of
 // native code.
-static constexpr size_t kMaxFrames = 3;
+static constexpr size_t kMaxFramesForHiddenApiJniCheck = 3;
 
 static std::mutex gUnwindingMutex;
 
@@ -73,25 +74,16 @@
 };
 
 static UnwindHelper& GetUnwindHelper() {
-  static UnwindHelper helper(kMaxFrames);
+  static UnwindHelper helper(kMaxFramesForHiddenApiJniCheck);
   return helper;
 }
 
 }  // namespace
 
-enum class SharedObjectKind {
-  kRuntime = 0,
-  kApexModule = 1,
-  kOther = 2
-};
-
 std::ostream& operator<<(std::ostream& os, SharedObjectKind kind) {
   switch (kind) {
-    case SharedObjectKind::kRuntime:
-      os << "Runtime";
-      break;
-    case SharedObjectKind::kApexModule:
-      os << "APEX Module";
+    case SharedObjectKind::kArtModule:
+      os << "ART module";
       break;
     case SharedObjectKind::kOther:
       os << "Other";
@@ -118,12 +110,9 @@
     return SharedObjectKind::kOther;
   }
 
-  bool HasCache() const {
-    return memory_type_table_.Size() != 0;
-  }
-
   void BuildCache() {
-    DCHECK(!HasCache());
+    std::lock_guard<std::mutex> guard(mutex_);
+    DCHECK_EQ(memory_type_table_.Size(), 0u);
     art::MemoryTypeTable<SharedObjectKind>::Builder builder;
     builder_ = &builder;
     libjavacore_loaded_ = false;
@@ -141,7 +130,18 @@
     builder_ = nullptr;
   }
 
+  void SetLibraryPathClassifier(JniLibraryPathClassifier* fc_classifier) {
+    std::lock_guard<std::mutex> guard(mutex_);
+    fc_classifier_ = fc_classifier;
+  }
+
+  bool HasLibraryPathClassifier() const {
+    std::lock_guard<std::mutex> guard(mutex_);
+    return fc_classifier_ != nullptr;
+  }
+
   void DropCache() {
+    const std::lock_guard<std::mutex> guard(mutex_);
     memory_type_table_ = {};
   }
 
@@ -149,6 +149,7 @@
   CodeRangeCache() {}
 
   bool Find(uintptr_t address, SharedObjectKind* kind) const {
+    std::lock_guard<std::mutex> guard(mutex_);
     const art::MemoryTypeRange<SharedObjectKind>* range = memory_type_table_.Lookup(address);
     if (range == nullptr) {
       return false;
@@ -169,7 +170,14 @@
       }
       uintptr_t start = info->dlpi_addr + phdr.p_vaddr;
       const uintptr_t limit = art::RoundUp(start + phdr.p_memsz, art::kPageSize);
-      SharedObjectKind kind = GetKind(info->dlpi_name, start, limit);
+      SharedObjectKind kind = GetKind(info->dlpi_name);
+      if (cache->fc_classifier_ != nullptr) {
+        std::optional<SharedObjectKind> maybe_kind =
+            cache->fc_classifier_->Classify(info->dlpi_name);
+        if (maybe_kind.has_value()) {
+          kind = maybe_kind.value();
+        }
+      }
       art::MemoryTypeRange<SharedObjectKind> range{start, limit, kind};
       if (!builder->Add(range)) {
         LOG(WARNING) << "Overlapping/invalid range found in ELF headers: " << range;
@@ -191,25 +199,29 @@
     return 0;
   }
 
-  static SharedObjectKind GetKind(const char* so_name, uintptr_t start, uintptr_t limit) {
-    uintptr_t runtime_method = reinterpret_cast<uintptr_t>(CodeRangeCache::GetKind);
-    if (runtime_method >= start && runtime_method < limit) {
-      return SharedObjectKind::kRuntime;
-    }
-    return art::LocationIsOnApex(so_name) ? SharedObjectKind::kApexModule
-                                          : SharedObjectKind::kOther;
+  static SharedObjectKind GetKind(const char* so_name) {
+    return art::LocationIsOnArtModule(so_name) ? SharedObjectKind::kArtModule
+                                               : SharedObjectKind::kOther;
   }
 
-  art::MemoryTypeTable<SharedObjectKind> memory_type_table_;
-
   // Table builder, only valid during BuildCache().
-  art::MemoryTypeTable<SharedObjectKind>::Builder* builder_;
+  art::MemoryTypeTable<SharedObjectKind>::Builder* builder_ GUARDED_BY(mutex_) = nullptr;
+
+  // Table for mapping PC addresses to their shared object files.
+  art::MemoryTypeTable<SharedObjectKind> memory_type_table_ GUARDED_BY(mutex_);
+
+  // Classifier used to override shared object classifications during tests.
+  JniLibraryPathClassifier* fc_classifier_ GUARDED_BY(mutex_) = nullptr;
 
   // Sanity checking state.
   bool libjavacore_loaded_;
   bool libnativehelper_loaded_;
   bool libopenjdk_loaded_;
 
+  // Mutex to protect fc_classifier_ and related state during testing. Outside of testing we
+  // only generate the |memory_type_table_| once.
+  mutable std::mutex mutex_;
+
   static constexpr std::string_view kLibjavacore = "libjavacore.so";
   static constexpr std::string_view kLibnativehelper = "libnativehelper.so";
   static constexpr std::string_view kLibopenjdk = art::kIsDebugBuild ? "libopenjdkd.so"
@@ -231,21 +243,39 @@
   CorePlatformApiCookie cookie =
       bit_cast<CorePlatformApiCookie, uint32_t>(self->CorePlatformApiCookie());
   bool is_core_platform_api_approved = false;  // Default value for non-device testing.
-  if (!kIsTargetBuild) {
-    // On target device, if policy says enforcement is disabled, then treat all callers as
-    // approved.
+  const bool is_under_test = CodeRangeCache::GetSingleton().HasLibraryPathClassifier();
+  if (kIsTargetBuild || is_under_test) {
+    // On target device (or running tests). If policy says enforcement is disabled,
+    // then treat all callers as approved.
     auto policy = Runtime::Current()->GetCorePlatformApiEnforcementPolicy();
     if (policy == hiddenapi::EnforcementPolicy::kDisabled) {
       is_core_platform_api_approved = true;
     } else if (cookie.depth == 0) {
-      // On target device, only check the caller at depth 0 (the outermost entry into JNI
-      // interface).
+      // On target device, only check the caller at depth 0 which corresponds to the outermost
+      // entry into the JNI interface. When performing the check here, we note that |*this| is
+      // stack allocated at entry points to JNI field and method resolution |*methods. We can use
+      // the address of |this| to find the callers frame.
       DCHECK_EQ(cookie.approved, false);
-      void* caller_pc = CaptureCallerPc();
+      void* caller_pc = nullptr;
+      {
+        std::lock_guard<std::mutex> guard(gUnwindingMutex);
+        unwindstack::Unwinder* unwinder = GetUnwindHelper().Unwinder();
+        std::unique_ptr<unwindstack::Regs> regs(unwindstack::Regs::CreateFromLocal());
+        RegsGetLocal(regs.get());
+        unwinder->SetRegs(regs.get());
+        unwinder->Unwind();
+        for (auto it = unwinder->frames().begin(); it != unwinder->frames().end(); ++it) {
+          // Unwind to frame above the tlsJniStackMarker. The stack markers should be on the first
+          // frame calling JNI methods.
+          if (it->sp > reinterpret_cast<uint64_t>(this)) {
+            caller_pc = reinterpret_cast<void*>(it->pc);
+            break;
+          }
+        }
+      }
       if (caller_pc != nullptr) {
         SharedObjectKind kind = CodeRangeCache::GetSingleton().GetSharedObjectKind(caller_pc);
-        is_core_platform_api_approved = ((kind == SharedObjectKind::kRuntime) ||
-                                         (kind == SharedObjectKind::kApexModule));
+        is_core_platform_api_approved = (kind == SharedObjectKind::kArtModule);
       }
     }
   }
@@ -279,30 +309,15 @@
   return cookie.approved;
 }
 
-void* ScopedCorePlatformApiCheck::CaptureCallerPc() {
-  std::lock_guard<std::mutex> guard(gUnwindingMutex);
-  unwindstack::Unwinder* unwinder = GetUnwindHelper().Unwinder();
-  std::unique_ptr<unwindstack::Regs> regs(unwindstack::Regs::CreateFromLocal());
-  RegsGetLocal(regs.get());
-  unwinder->SetRegs(regs.get());
-  unwinder->Unwind();
-  for (auto it = unwinder->frames().begin(); it != unwinder->frames().end(); ++it) {
-    // Unwind to frame above the tlsJniStackMarker. The stack markers should be on the first frame
-    // calling JNI methods.
-    if (it->sp > reinterpret_cast<uint64_t>(this)) {
-      return reinterpret_cast<void*>(it->pc);
-    }
-  }
-  return nullptr;
-}
-
-void JniInitializeNativeCallerCheck() {
+void JniInitializeNativeCallerCheck(JniLibraryPathClassifier* classifier) {
   // This method should be called only once and before there are multiple runtime threads.
-  DCHECK(!CodeRangeCache::GetSingleton().HasCache());
+  CodeRangeCache::GetSingleton().DropCache();
+  CodeRangeCache::GetSingleton().SetLibraryPathClassifier(classifier);
   CodeRangeCache::GetSingleton().BuildCache();
 }
 
 void JniShutdownNativeCallerCheck() {
+  CodeRangeCache::GetSingleton().SetLibraryPathClassifier(nullptr);
   CodeRangeCache::GetSingleton().DropCache();
 }
 
@@ -322,7 +337,7 @@
   return false;
 }
 
-void JniInitializeNativeCallerCheck() {}
+void JniInitializeNativeCallerCheck(JniLibraryPathClassifier* f ATTRIBUTE_UNUSED) {}
 
 void JniShutdownNativeCallerCheck() {}
 
diff --git a/runtime/hidden_api_jni.h b/runtime/hidden_api_jni.h
index a084378..15de6ef 100644
--- a/runtime/hidden_api_jni.h
+++ b/runtime/hidden_api_jni.h
@@ -17,6 +17,8 @@
 #ifndef ART_RUNTIME_HIDDEN_API_JNI_H_
 #define ART_RUNTIME_HIDDEN_API_JNI_H_
 
+#include <optional>
+
 #include "base/macros.h"
 
 namespace art {
@@ -37,16 +39,25 @@
   static bool IsCurrentCallerApproved(Thread* self);
 
  private:
-  // Captures calling PC for frame above the frame allocating the current ScopedCorePlatformApiCheck
-  // instance.
-  void* CaptureCallerPc();
-
   // Instances should only be stack allocated, copy and assignment not useful.
   DISALLOW_ALLOCATION();
   DISALLOW_COPY_AND_ASSIGN(ScopedCorePlatformApiCheck);
 };
 
-void JniInitializeNativeCallerCheck();
+// Kind of memory page from mapped shared object files.
+enum class SharedObjectKind {
+  kArtModule = 0,   // Part of the ART module.
+  kOther = 1        // Neither of the above.
+};
+
+
+class JniLibraryPathClassifier {
+ public:
+  virtual std::optional<SharedObjectKind> Classify(const char* so_name) = 0;
+  virtual ~JniLibraryPathClassifier() {}
+};
+
+void JniInitializeNativeCallerCheck(JniLibraryPathClassifier* fc = nullptr);
 void JniShutdownNativeCallerCheck();
 
 }  // namespace hiddenapi
diff --git a/runtime/jni/jni_internal.cc b/runtime/jni/jni_internal.cc
index 882e10f..887ad9a 100644
--- a/runtime/jni/jni_internal.cc
+++ b/runtime/jni/jni_internal.cc
@@ -88,9 +88,17 @@
 template<typename T>
 ALWAYS_INLINE static bool ShouldDenyAccessToMember(T* member, Thread* self)
     REQUIRES_SHARED(Locks::mutator_lock_) {
-  if (hiddenapi::ScopedCorePlatformApiCheck::IsCurrentCallerApproved(self)) {
+  const bool native_caller_trusted =
+      hiddenapi::ScopedCorePlatformApiCheck::IsCurrentCallerApproved(self);
+  if (native_caller_trusted) {
+    // A trusted caller is in the same domain as the ART module so is assumed to always have
+    // access to the APIs that the module provides.
     return false;
   }
+
+  // Construct AccessContext from the first calling class on stack.
+  // If the calling class cannot be determined, e.g. unattached threads,
+  // we conservatively assume the caller is trusted.
   return hiddenapi::ShouldDenyAccessToMember(
       member,
       [&]() REQUIRES_SHARED(Locks::mutator_lock_) {
diff --git a/test/2030-core-platform-api-jni/build b/test/2030-core-platform-api-jni/build
new file mode 100644
index 0000000..330a6de
--- /dev/null
+++ b/test/2030-core-platform-api-jni/build
@@ -0,0 +1,38 @@
+#!/bin/bash
+#
+# Copyright 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.
+
+set -e
+
+# Build the jars twice. First with applying hiddenapi, creating a boot jar, then
+# a second time without to create a normal jar. We need to do this because we
+# want to load the jar once as an app module and once as a member of the boot
+# class path. The DexFileVerifier would fail on the former as it does not allow
+# hidden API access flags in dex files. DexFileVerifier is not invoked on boot
+# class path dex files, so the boot jar loads fine in the latter case.
+
+export USE_HIDDENAPI=true
+./default-build "$@"
+
+# Move the jar file into the resource folder to be bundled with the test.
+mkdir res
+mv ${TEST_NAME}.jar res/boot.jar
+
+# Clear all intermediate files otherwise default-build would either skip
+# compilation or fail rebuilding.
+rm -rf classes*
+
+export USE_HIDDENAPI=false
+./default-build "$@"
diff --git a/test/2030-core-platform-api-jni/check b/test/2030-core-platform-api-jni/check
new file mode 100644
index 0000000..c319a0a
--- /dev/null
+++ b/test/2030-core-platform-api-jni/check
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# 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.
+
+# Remove pid and date from the log messages.
+grep -vE '^dalvikvm(32|64) E [^]]+]' "$2" \
+    | grep -v JNI_OnLoad \
+    | grep -v JNI_OnUnload \
+    > "$2.tmp"
+
+./default-check "$1" "$2.tmp"
diff --git a/test/2030-core-platform-api-jni/core-platform-api-jni.cc b/test/2030-core-platform-api-jni/core-platform-api-jni.cc
new file mode 100644
index 0000000..525982f
--- /dev/null
+++ b/test/2030-core-platform-api-jni/core-platform-api-jni.cc
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hidden_api.h"
+#include "hidden_api_jni.h"
+#include "jni.h"
+#include "runtime.h"
+
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/ScopedLocalRef.h"
+#include "nativehelper/ScopedUtfChars.h"
+
+class TestLibraryPathClassifier : public art::hiddenapi::JniLibraryPathClassifier {
+ public:
+  std::optional<art::hiddenapi::SharedObjectKind> Classify(const char* so_path) override {
+    // so_path is the path to a shared object. We have the filename minus suffix (expected to be
+    // .so).
+    const char* last_separator = strrchr(so_path, '/');
+    std::string_view filename = (last_separator != nullptr) ? last_separator + 1 : so_path;
+    if (filename == so_name_) {
+      return so_kind_;
+    }
+    return {};
+  }
+
+  void Configure(const char* so_file, art::hiddenapi::SharedObjectKind kind) {
+    so_name_ = so_file;
+    so_kind_ = kind;
+  }
+
+ private:
+  std::string so_name_;
+  art::hiddenapi::SharedObjectKind so_kind_ = art::hiddenapi::SharedObjectKind::kOther;
+};
+
+static TestLibraryPathClassifier* GetLibraryPathClassifier() {
+  static TestLibraryPathClassifier g_classifier;
+  return &g_classifier;
+}
+
+static void InstallLibraryPathClassifier(JNIEnv* env,
+                                         jstring j_library_path,
+                                         art::hiddenapi::SharedObjectKind kind) {
+  ScopedUtfChars library_path(env, j_library_path);
+  const char* last_separator = strrchr(library_path.c_str(), '/');
+  const char* library_so = (last_separator != nullptr) ? last_separator + 1 : library_path.c_str();
+  GetLibraryPathClassifier()->Configure(library_so, kind);
+  art::hiddenapi::JniInitializeNativeCallerCheck(GetLibraryPathClassifier());
+}
+
+extern "C" JNIEXPORT void JNICALL Java_Main_treatAsArtModule(JNIEnv* env,
+                                                             jclass /*klass*/,
+                                                             jstring library_name) {
+  InstallLibraryPathClassifier(env, library_name, art::hiddenapi::SharedObjectKind::kArtModule);
+}
+
+extern "C" JNIEXPORT void JNICALL Java_Main_treatAsOtherLibrary(JNIEnv* env,
+                                                                jclass /*klass*/,
+                                                                jstring library_name) {
+  InstallLibraryPathClassifier(env, library_name, art::hiddenapi::SharedObjectKind::kOther);
+}
diff --git a/test/2030-core-platform-api-jni/expected.txt b/test/2030-core-platform-api-jni/expected.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/2030-core-platform-api-jni/expected.txt
diff --git a/test/2030-core-platform-api-jni/hiddenapi-flags.csv b/test/2030-core-platform-api-jni/hiddenapi-flags.csv
new file mode 100644
index 0000000..42626f7
--- /dev/null
+++ b/test/2030-core-platform-api-jni/hiddenapi-flags.csv
@@ -0,0 +1,108 @@
+LNullaryConstructorBlacklistAndCorePlatformApi;-><init>()V,blacklist,core-platform-api
+LNullaryConstructorBlacklist;-><init>()V,blacklist
+LNullaryConstructorDarkGreylist;-><init>()V,greylist-max-o
+LNullaryConstructorLightGreylist;-><init>()V,greylist
+LParentClass;->fieldPackageBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldPackageBlacklist:I,blacklist
+LParentClass;->fieldPackageDarkGreylist:I,greylist-max-o
+LParentClass;->fieldPackageLightGreylist:I,greylist
+LParentClass;->fieldPackageStaticBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldPackageStaticBlacklist:I,blacklist
+LParentClass;->fieldPackageStaticDarkGreylist:I,greylist-max-o
+LParentClass;->fieldPackageStaticLightGreylist:I,greylist
+LParentClass;->fieldPrivateBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldPrivateBlacklist:I,blacklist
+LParentClass;->fieldPrivateDarkGreylist:I,greylist-max-o
+LParentClass;->fieldPrivateLightGreylist:I,greylist
+LParentClass;->fieldPrivateStaticBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldPrivateStaticBlacklist:I,blacklist
+LParentClass;->fieldPrivateStaticDarkGreylist:I,greylist-max-o
+LParentClass;->fieldPrivateStaticLightGreylist:I,greylist
+LParentClass;->fieldProtectedBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldProtectedBlacklist:I,blacklist
+LParentClass;->fieldProtectedDarkGreylist:I,greylist-max-o
+LParentClass;->fieldProtectedLightGreylist:I,greylist
+LParentClass;->fieldProtectedStaticBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldProtectedStaticBlacklist:I,blacklist
+LParentClass;->fieldProtectedStaticDarkGreylist:I,greylist-max-o
+LParentClass;->fieldProtectedStaticLightGreylist:I,greylist
+LParentClass;->fieldPublicBlacklistAndCorePlatformApiB:I,blacklist,core-platform-api
+LParentClass;->fieldPublicBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldPublicBlacklistB:I,blacklist
+LParentClass;->fieldPublicBlacklist:I,blacklist
+LParentClass;->fieldPublicDarkGreylistB:I,greylist-max-o
+LParentClass;->fieldPublicDarkGreylist:I,greylist-max-o
+LParentClass;->fieldPublicLightGreylistB:I,greylist
+LParentClass;->fieldPublicLightGreylist:I,greylist
+LParentClass;->fieldPublicStaticBlacklistAndCorePlatformApiB:I,blacklist,core-platform-api
+LParentClass;->fieldPublicStaticBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentClass;->fieldPublicStaticBlacklistB:I,blacklist
+LParentClass;->fieldPublicStaticBlacklist:I,blacklist
+LParentClass;->fieldPublicStaticDarkGreylistB:I,greylist-max-o
+LParentClass;->fieldPublicStaticDarkGreylist:I,greylist-max-o
+LParentClass;->fieldPublicStaticLightGreylistB:I,greylist
+LParentClass;->fieldPublicStaticLightGreylist:I,greylist
+LParentClass;-><init>(DB)V,greylist-max-o
+LParentClass;-><init>(DC)V,blacklist
+LParentClass;-><init>(DI)V,blacklist,core-platform-api
+LParentClass;-><init>(DZ)V,greylist
+LParentClass;-><init>(FB)V,greylist-max-o
+LParentClass;-><init>(FC)V,blacklist
+LParentClass;-><init>(FI)V,blacklist,core-platform-api
+LParentClass;-><init>(FZ)V,greylist
+LParentClass;-><init>(IB)V,greylist-max-o
+LParentClass;-><init>(IC)V,blacklist
+LParentClass;-><init>(II)V,blacklist,core-platform-api
+LParentClass;-><init>(IZ)V,greylist
+LParentClass;-><init>(JB)V,greylist-max-o
+LParentClass;-><init>(JC)V,blacklist
+LParentClass;-><init>(JI)V,blacklist,core-platform-api
+LParentClass;-><init>(JZ)V,greylist
+LParentClass;->methodPackageBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodPackageBlacklist()I,blacklist
+LParentClass;->methodPackageDarkGreylist()I,greylist-max-o
+LParentClass;->methodPackageLightGreylist()I,greylist
+LParentClass;->methodPackageStaticBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodPackageStaticBlacklist()I,blacklist
+LParentClass;->methodPackageStaticDarkGreylist()I,greylist-max-o
+LParentClass;->methodPackageStaticLightGreylist()I,greylist
+LParentClass;->methodPrivateBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodPrivateBlacklist()I,blacklist
+LParentClass;->methodPrivateDarkGreylist()I,greylist-max-o
+LParentClass;->methodPrivateLightGreylist()I,greylist
+LParentClass;->methodPrivateStaticBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodPrivateStaticBlacklist()I,blacklist
+LParentClass;->methodPrivateStaticDarkGreylist()I,greylist-max-o
+LParentClass;->methodPrivateStaticLightGreylist()I,greylist
+LParentClass;->methodProtectedBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodProtectedBlacklist()I,blacklist
+LParentClass;->methodProtectedDarkGreylist()I,greylist-max-o
+LParentClass;->methodProtectedLightGreylist()I,greylist
+LParentClass;->methodProtectedStaticBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodProtectedStaticBlacklist()I,blacklist
+LParentClass;->methodProtectedStaticDarkGreylist()I,greylist-max-o
+LParentClass;->methodProtectedStaticLightGreylist()I,greylist
+LParentClass;->methodPublicBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodPublicBlacklist()I,blacklist
+LParentClass;->methodPublicDarkGreylist()I,greylist-max-o
+LParentClass;->methodPublicLightGreylist()I,greylist
+LParentClass;->methodPublicStaticBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentClass;->methodPublicStaticBlacklist()I,blacklist
+LParentClass;->methodPublicStaticDarkGreylist()I,greylist-max-o
+LParentClass;->methodPublicStaticLightGreylist()I,greylist
+LParentInterface;->fieldPublicStaticBlacklistAndCorePlatformApi:I,blacklist,core-platform-api
+LParentInterface;->fieldPublicStaticBlacklist:I,blacklist
+LParentInterface;->fieldPublicStaticDarkGreylist:I,greylist-max-o
+LParentInterface;->fieldPublicStaticLightGreylist:I,greylist
+LParentInterface;->methodPublicBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentInterface;->methodPublicBlacklist()I,blacklist
+LParentInterface;->methodPublicDarkGreylist()I,greylist-max-o
+LParentInterface;->methodPublicDefaultBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentInterface;->methodPublicDefaultBlacklist()I,blacklist
+LParentInterface;->methodPublicDefaultDarkGreylist()I,greylist-max-o
+LParentInterface;->methodPublicDefaultLightGreylist()I,greylist
+LParentInterface;->methodPublicLightGreylist()I,greylist
+LParentInterface;->methodPublicStaticBlacklistAndCorePlatformApi()I,blacklist,core-platform-api
+LParentInterface;->methodPublicStaticBlacklist()I,blacklist
+LParentInterface;->methodPublicStaticDarkGreylist()I,greylist-max-o
+LParentInterface;->methodPublicStaticLightGreylist()I,greylist
diff --git a/test/2030-core-platform-api-jni/info.txt b/test/2030-core-platform-api-jni/info.txt
new file mode 100644
index 0000000..94aac1e
--- /dev/null
+++ b/test/2030-core-platform-api-jni/info.txt
@@ -0,0 +1,16 @@
+Test whether hidden API access flags are being enforced. The test is composed of
+two JARs. The first (parent) defines methods and fields and the second (child)
+tries to access them with reflection/JNI/MethodHandles or link against them.
+Note that the first is compiled twice - once with and once without hidden access
+flags.
+
+The test then proceeds to exercise the following combinations of class loading:
+(a) Both parent and child dex loaded with PathClassLoader, parent's class loader
+    is the child's class loader's parent. Access flags should not be enforced as
+    the parent does not belong to boot class path.
+(b) Parent is appended to boot class path, child is loaded with PathClassLoader.
+    In this situation child should not be able to access hidden methods/fields
+    of the parent.
+(c) Both parent and child are appended to boot class path. Restrictions should
+    not apply as hidden APIs are accessible within the boundaries of the boot
+    class path.
diff --git a/test/2030-core-platform-api-jni/run b/test/2030-core-platform-api-jni/run
new file mode 100755
index 0000000..2babeef
--- /dev/null
+++ b/test/2030-core-platform-api-jni/run
@@ -0,0 +1,19 @@
+#!/bin/bash
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Make verification soft fail so that we can re-verify boot classpath
+# methods at runtime.
+exec ${RUN} $@ --verify-soft-fail
\ No newline at end of file
diff --git a/test/2030-core-platform-api-jni/src-art/Main.java b/test/2030-core-platform-api-jni/src-art/Main.java
new file mode 100644
index 0000000..d6601db
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src-art/Main.java
@@ -0,0 +1,225 @@
+/*
+ * 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.
+ */
+
+import dalvik.system.PathClassLoader;
+import java.io.File;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.util.Arrays;
+
+public class Main {
+  // This needs to be kept in sync with DexDomain in ChildClass.
+  enum DexDomain {
+    CorePlatform,
+    Platform,
+    Application
+  }
+
+  public static void main(String[] args) throws Exception {
+    System.loadLibrary(args[0]);
+    prepareNativeLibFileName(args[0]);
+
+    // Enable hidden API checks in case they are disabled by default.
+    init();
+
+    // TODO there are sequential depencies between these test cases, and bugs
+    // in the production code may lead to subsequent tests to erroneously pass,
+    // or test the wrong thing. We rely on not deduping hidden API warnings
+    // here for the same reasons), meaning the code under test and production
+    // code are running in different configurations. Each test should be run in
+    // a fresh process to ensure that they are working correctly and not
+    // accidentally interfering with each other.
+    // As a side effect, we also cannot test Platform->Platform and later
+    // Platform->CorePlatform as the former succeeds in verifying linkage usages
+    // that should fail in the latter.
+    // We also cannot use InMemoryDexClassLoader because it runs verification in
+    // a background thread and being able to dynamically change the configuration
+    // (like list of exemptions) would require proper thread synchronization.
+
+    // Run test with both parent and child dex files loaded with class loaders.
+    // The expectation is that hidden members in parent should be visible to
+    // the child.
+    doTest(DexDomain.Application, DexDomain.Application, false);
+    doUnloading();
+
+    // Now append parent dex file to boot class path and run again. This time
+    // the child dex file should not be able to access private APIs of the
+    // parent.
+    int parentIdx = appendToBootClassLoader(DEX_PARENT_BOOT, /* isCorePlatform */ false);
+    doTest(DexDomain.Platform, DexDomain.Application, false);
+    doUnloading();
+
+    // Now run the same test again, but with the blacklist exmemptions list set
+    // to "L" which matches everything.
+    doTest(DexDomain.Platform, DexDomain.Application, true);
+    doUnloading();
+
+    // Repeat the two tests above, only with parent being a core-platform dex file.
+    setDexDomain(parentIdx, /* isCorePlatform */ true);
+    doTest(DexDomain.CorePlatform, DexDomain.Application, false);
+    doUnloading();
+
+    doTest(DexDomain.CorePlatform, DexDomain.Application, true);
+    doUnloading();
+
+    // Append child to boot class path, first as a platform dex file.
+    // It should not be allowed to access non-public, non-core platform API members.
+    int childIdx = appendToBootClassLoader(DEX_CHILD, /* isCorePlatform */ false);
+    doTest(DexDomain.CorePlatform, DexDomain.Platform, false);
+    doUnloading();
+
+    // And finally change child to core-platform dex. With both in the boot classpath
+    // and both core-platform, access should be granted.
+    setDexDomain(childIdx, /* isCorePlatform */ true);
+    doTest(DexDomain.CorePlatform, DexDomain.CorePlatform, false);
+    doUnloading();
+  }
+
+  private static void doTest(DexDomain parentDomain, DexDomain childDomain,
+                             boolean whitelistAllApis) throws Exception {
+    // Load parent dex if it is not in boot class path.
+    ClassLoader parentLoader = null;
+    if (parentDomain == DexDomain.Application) {
+      parentLoader = new PathClassLoader(DEX_PARENT, ClassLoader.getSystemClassLoader());
+    } else {
+      parentLoader = BOOT_CLASS_LOADER;
+    }
+
+    // Load child dex if it is not in boot class path.
+    ClassLoader childLoader = null;
+    if (childDomain == DexDomain.Application) {
+      childLoader = new PathClassLoader(DEX_CHILD, parentLoader);
+    } else {
+      if (parentLoader != BOOT_CLASS_LOADER) {
+        throw new IllegalStateException(
+            "DeclaringClass must be in parent class loader of CallingClass");
+      }
+      childLoader = BOOT_CLASS_LOADER;
+    }
+
+    // Create a unique copy of the native library. Each shared library can only
+    // be loaded once, but for some reason even classes from a class loader
+    // cannot register their native methods against symbols in a shared library
+    // loaded by their parent class loader.
+    String nativeLibCopy = createNativeLibCopy(parentDomain, childDomain, whitelistAllApis);
+
+    // Set exemptions to "L" (matches all classes) if we are testing whitelisting.
+    setWhitelistAll(whitelistAllApis);
+
+    // Invoke ChildClass.runTest
+    Class<?> childClass = Class.forName("ChildClass", true, childLoader);
+    Method runTestMethod = childClass.getDeclaredMethod(
+        "runTest", String.class, Integer.TYPE, Integer.TYPE, Integer.TYPE, Boolean.TYPE,
+        MethodHandle.class);
+
+    final MethodHandle nativeLibraryLoadHooks[] = {
+      OTHER_LIBRARY_LOAD_HOOK,
+      ART_MODULE_LOAD_HOOK,
+    };
+
+    for (MethodHandle mh : nativeLibraryLoadHooks) {
+      int nativeDomainOrdinal;
+      if (mh == OTHER_LIBRARY_LOAD_HOOK) {
+        nativeDomainOrdinal = DexDomain.Application.ordinal();
+      } else {
+        nativeDomainOrdinal = DexDomain.CorePlatform.ordinal();
+      }
+      runTestMethod.invoke(null, nativeLibCopy,
+                           parentDomain.ordinal(), childDomain.ordinal(), nativeDomainOrdinal,
+                           whitelistAllApis, mh);
+    }
+  }
+
+  // Routine which tries to figure out the absolute path of our native library.
+  private static void prepareNativeLibFileName(String arg) throws Exception {
+    String libName = System.mapLibraryName(arg);
+    Method libPathsMethod = Runtime.class.getDeclaredMethod("getLibPaths");
+    libPathsMethod.setAccessible(true);
+    String[] libPaths = (String[]) libPathsMethod.invoke(Runtime.getRuntime());
+    nativeLibFileName = null;
+    for (String p : libPaths) {
+      String candidate = p + libName;
+      if (new File(candidate).exists()) {
+        nativeLibFileName = candidate;
+        break;
+      }
+    }
+    if (nativeLibFileName == null) {
+      throw new IllegalStateException("Didn't find " + libName + " in " +
+          Arrays.toString(libPaths));
+    }
+  }
+
+  // Copy native library to a new file with a unique name so it does not
+  // conflict with other loaded instance of the same binary file.
+  private static String createNativeLibCopy(DexDomain parentDomain, DexDomain childDomain,
+      boolean whitelistAllApis) throws Exception {
+    String tempFileName = System.mapLibraryName(
+        "hiddenapitest_" + (parentDomain.ordinal()) + (childDomain.ordinal()) +
+        (whitelistAllApis ? "1" : "0"));
+    File tempFile = new File(System.getenv("DEX_LOCATION"), tempFileName);
+    Files.copy(new File(nativeLibFileName).toPath(), tempFile.toPath());
+    return tempFile.getAbsolutePath();
+  }
+
+  private static void doUnloading() {
+    // Do multiple GCs to prevent rare flakiness if some other thread is
+    // keeping the classloader live.
+    for (int i = 0; i < 5; ++i) {
+       Runtime.getRuntime().gc();
+    }
+  }
+
+  private static MethodHandle getLoadLibraryHook(String methodName) {
+    try {
+      return MethodHandles.publicLookup().findStatic(Main.class, methodName,
+                                                     LOAD_HOOK_METHOD_TYPE);
+    } catch (Throwable t) {
+      System.err.println("Initialization error for " + methodName + " " + t);
+      return null;
+    }
+  }
+
+  private static String nativeLibFileName;
+
+  private static final MethodType LOAD_HOOK_METHOD_TYPE = MethodType.methodType(void.class,
+                                                                                String.class);
+  private static final MethodHandle OTHER_LIBRARY_LOAD_HOOK =
+      getLoadLibraryHook("treatAsOtherLibrary");
+  private static final MethodHandle ART_MODULE_LOAD_HOOK =
+      getLoadLibraryHook("treatAsArtModule");
+
+  private static final String DEX_PARENT =
+      new File(System.getenv("DEX_LOCATION"), "2030-core-platform-api-jni.jar").getAbsolutePath();
+  private static final String DEX_PARENT_BOOT =
+      new File(new File(System.getenv("DEX_LOCATION"), "res"), "boot.jar").getAbsolutePath();
+  private static final String DEX_CHILD =
+      new File(System.getenv("DEX_LOCATION"), "2030-core-platform-api-jni-ex.jar").getAbsolutePath();
+
+  private static ClassLoader BOOT_CLASS_LOADER = Object.class.getClassLoader();
+
+  private static native int appendToBootClassLoader(String dexPath, boolean isCorePlatform);
+  private static native void setDexDomain(int index, boolean isCorePlatform);
+  private static native void init();
+  private static native void setWhitelistAll(boolean value);
+
+  public static native void treatAsArtModule(String library);
+  public static native void treatAsOtherLibrary(String library);
+}
diff --git a/test/2030-core-platform-api-jni/src-ex/ChildClass.java b/test/2030-core-platform-api-jni/src-ex/ChildClass.java
new file mode 100644
index 0000000..75fed41
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src-ex/ChildClass.java
@@ -0,0 +1,282 @@
+/*
+ * 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.
+ */
+
+import dalvik.system.VMRuntime;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodType;
+import java.util.function.Consumer;
+
+public class ChildClass {
+  enum PrimitiveType {
+    TInteger('I', Integer.TYPE, Integer.valueOf(0)),
+    TLong('J', Long.TYPE, Long.valueOf(0)),
+    TFloat('F', Float.TYPE, Float.valueOf(0)),
+    TDouble('D', Double.TYPE, Double.valueOf(0)),
+    TBoolean('Z', Boolean.TYPE, Boolean.valueOf(false)),
+    TByte('B', Byte.TYPE, Byte.valueOf((byte) 0)),
+    TShort('S', Short.TYPE, Short.valueOf((short) 0)),
+    TCharacter('C', Character.TYPE, Character.valueOf('0'));
+
+    PrimitiveType(char shorty, Class klass, Object value) {
+      mShorty = shorty;
+      mClass = klass;
+      mDefaultValue = value;
+    }
+
+    public char mShorty;
+    public Class mClass;
+    public Object mDefaultValue;
+  }
+
+  enum Hiddenness {
+    Whitelist(PrimitiveType.TShort),
+    LightGreylist(PrimitiveType.TBoolean),
+    DarkGreylist(PrimitiveType.TByte),
+    Blacklist(PrimitiveType.TCharacter),
+    BlacklistAndCorePlatformApi(PrimitiveType.TInteger);
+
+    Hiddenness(PrimitiveType type) { mAssociatedType = type; }
+    public PrimitiveType mAssociatedType;
+  }
+
+  enum Visibility {
+    Public(PrimitiveType.TInteger),
+    Package(PrimitiveType.TFloat),
+    Protected(PrimitiveType.TLong),
+    Private(PrimitiveType.TDouble);
+
+    Visibility(PrimitiveType type) { mAssociatedType = type; }
+    public PrimitiveType mAssociatedType;
+  }
+
+  enum Behaviour {
+    Granted,
+    Warning,
+    Denied,
+  }
+
+  // This needs to be kept in sync with DexDomain in Main.
+  enum DexDomain {
+    CorePlatform,
+    Platform,
+    Application
+  }
+
+  private static final boolean booleanValues[] = new boolean[] { false, true };
+
+  public static void runTest(String libFileName, int parentDomainOrdinal,
+                             int childDomainOrdinal, int childNativeDomainOrdinal,
+                             boolean everythingWhitelisted,
+                             MethodHandle postSystemLoadHook) throws Throwable {
+    System.load(libFileName);
+
+    // Configure domain of native library.
+    postSystemLoadHook.invokeExact(libFileName);
+
+    parentDomain = DexDomain.values()[parentDomainOrdinal];
+    childDomain = DexDomain.values()[childDomainOrdinal];
+    childNativeDomain = DexDomain.values()[childNativeDomainOrdinal];
+
+    configMessage = "parentDomain=" + parentDomain.name() +
+            ", childDomain=" + childDomain.name() +
+            ", childNativeDomain=" + childNativeDomain.name() +
+            ", everythingWhitelisted=" + everythingWhitelisted;
+
+    // Check expectations about loading into boot class path.
+    boolean isParentInBoot = (ParentClass.class.getClassLoader().getParent() == null);
+    boolean expectedParentInBoot = (parentDomain != DexDomain.Application);
+    if (isParentInBoot != expectedParentInBoot) {
+      throw new RuntimeException("Expected ParentClass " +
+                                 (expectedParentInBoot ? "" : "not ") + "in boot class path");
+    }
+    boolean isChildInBoot = (ChildClass.class.getClassLoader().getParent() == null);
+    boolean expectedChildInBoot = (childDomain != DexDomain.Application);
+    if (isChildInBoot != expectedChildInBoot) {
+      throw new RuntimeException("Expected ChildClass " + (expectedChildInBoot ? "" : "not ") +
+                                 "in boot class path");
+    }
+    ChildClass.everythingWhitelisted = everythingWhitelisted;
+
+    boolean isSameBoot = (isParentInBoot == isChildInBoot);
+    boolean isDebuggable = VMRuntime.getRuntime().isJavaDebuggable();
+
+    // For compat reasons, meta-reflection should still be usable by apps if hidden api check
+    // hardening is disabled (i.e. target SDK is Q or earlier). The only configuration where this
+    // workaround used to work is for ChildClass in the Application domain and ParentClass in the
+    // Platform domain, so only test that configuration with hidden api check hardening disabled.
+    boolean testHiddenApiCheckHardeningDisabled =
+        (childDomain == DexDomain.Application) && (parentDomain == DexDomain.Platform);
+
+    // Run meaningful combinations of access flags.
+    for (Hiddenness hiddenness : Hiddenness.values()) {
+      Behaviour expected;
+      final boolean invokesMemberCallback;
+      // Warnings are now disabled whenever access is granted, even for
+      // greylisted APIs. This is the behaviour for release builds.
+      if (everythingWhitelisted || hiddenness == Hiddenness.Whitelist) {
+        expected = Behaviour.Granted;
+        invokesMemberCallback = false;
+      } else if (parentDomain == DexDomain.CorePlatform && childDomain == DexDomain.Platform) {
+        expected = (hiddenness == Hiddenness.BlacklistAndCorePlatformApi)
+                ? Behaviour.Granted : Behaviour.Denied;
+        invokesMemberCallback = false;
+      } else if (isSameBoot) {
+        expected = Behaviour.Granted;
+        invokesMemberCallback = false;
+      } else if (hiddenness == Hiddenness.Blacklist ||
+                 hiddenness == Hiddenness.BlacklistAndCorePlatformApi) {
+        expected = Behaviour.Denied;
+        invokesMemberCallback = true;
+      } else {
+        expected = Behaviour.Warning;
+        invokesMemberCallback = true;
+      }
+
+      if (childNativeDomain == DexDomain.CorePlatform) {
+          // Native code that is part of the Core Platform (it's in the ART module). This code is
+          // assumed to have access to all methods and fields.
+          expected = Behaviour.Granted;
+      }
+
+      for (boolean isStatic : booleanValues) {
+        String suffix = (isStatic ? "Static" : "") + hiddenness.name();
+
+        for (Visibility visibility : Visibility.values()) {
+          // Test methods and fields
+          for (Class klass : new Class<?>[] { ParentClass.class, ParentInterface.class }) {
+            String baseName = visibility.name() + suffix;
+            checkField(klass, "field" + baseName, isStatic, visibility, expected,
+                invokesMemberCallback, testHiddenApiCheckHardeningDisabled);
+            checkMethod(klass, "method" + baseName, isStatic, visibility, expected,
+                invokesMemberCallback, testHiddenApiCheckHardeningDisabled);
+          }
+
+          // Check whether one can use a class constructor.
+          checkConstructor(ParentClass.class, visibility, hiddenness, expected,
+                testHiddenApiCheckHardeningDisabled);
+        }
+      }
+    }
+  }
+
+  private static void checkField(Class<?> klass, String name, boolean isStatic,
+      Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback,
+      boolean testHiddenApiCheckHardeningDisabled) throws Exception {
+
+    boolean isPublic = (visibility == Visibility.Public);
+    boolean canDiscover = (behaviour != Behaviour.Denied);
+
+    if (klass.isInterface() && (!isStatic || !isPublic)) {
+      // Interfaces only have public static fields.
+      return;
+    }
+
+    // Test discovery with JNI.
+
+    if (JNI.canDiscoverField(klass, name, isStatic) != canDiscover) {
+      throwDiscoveryException(klass, name, true, "JNI", canDiscover);
+    }
+
+    if (canDiscover) {
+      if (!JNI.canGetField(klass, name, isStatic)) {
+        throwAccessException(klass, name, true, "getIntField");
+      }
+      if (!JNI.canSetField(klass, name, isStatic)) {
+        throwAccessException(klass, name, true, "setIntField");
+      }
+    }
+  }
+
+  private static void checkMethod(Class<?> klass, String name, boolean isStatic,
+      Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback,
+      boolean testHiddenApiCheckHardeningDisabled) throws Exception {
+
+    boolean isPublic = (visibility == Visibility.Public);
+    if (klass.isInterface() && !isPublic) {
+      // All interface members are public.
+      return;
+    }
+
+    boolean canDiscover = (behaviour != Behaviour.Denied);
+
+    // Test discovery with JNI.
+
+    if (JNI.canDiscoverMethod(klass, name, isStatic) != canDiscover) {
+      throwDiscoveryException(klass, name, false, "JNI", canDiscover);
+    }
+
+    // Finish here if we could not discover the method.
+
+    if (canDiscover) {
+      // Test whether we can invoke the method. This skips non-static interface methods.
+      if (!klass.isInterface() || isStatic) {
+        if (!JNI.canInvokeMethodA(klass, name, isStatic)) {
+          throwAccessException(klass, name, false, "CallMethodA");
+        }
+        if (!JNI.canInvokeMethodV(klass, name, isStatic)) {
+          throwAccessException(klass, name, false, "CallMethodV");
+        }
+      }
+    }
+  }
+
+  private static void checkConstructor(Class<?> klass, Visibility visibility, Hiddenness hiddenness,
+      Behaviour behaviour, boolean testHiddenApiCheckHardeningDisabled) throws Exception {
+
+    boolean isPublic = (visibility == Visibility.Public);
+    String signature = "(" + visibility.mAssociatedType.mShorty +
+                             hiddenness.mAssociatedType.mShorty + ")V";
+    String fullName = "<init>" + signature;
+    Class<?> args[] = new Class[] { visibility.mAssociatedType.mClass,
+                                    hiddenness.mAssociatedType.mClass };
+    Object initargs[] = new Object[] { visibility.mAssociatedType.mDefaultValue,
+                                       hiddenness.mAssociatedType.mDefaultValue };
+    MethodType methodType = MethodType.methodType(void.class, args);
+
+    boolean canDiscover = (behaviour != Behaviour.Denied);
+
+    // Test discovery with JNI.
+
+    if (JNI.canDiscoverConstructor(klass, signature) != canDiscover) {
+      throwDiscoveryException(klass, fullName, false, "JNI", canDiscover);
+    }
+  }
+
+  private static void throwDiscoveryException(Class<?> klass, String name, boolean isField,
+      String fn, boolean canAccess) {
+    throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() +
+        "." + name + " to " + (canAccess ? "" : "not ") + "be discoverable with " + fn + ". " +
+        configMessage);
+  }
+
+  private static void throwAccessException(Class<?> klass, String name, boolean isField,
+      String fn) {
+    throw new RuntimeException("Expected to be able to access " + (isField ? "field " : "method ") +
+        klass.getName() + "." + name + " using " + fn + ". " + configMessage);
+  }
+
+  private static void throwModifiersException(Class<?> klass, String name, boolean isField) {
+    throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() +
+        "." + name + " to not expose hidden modifiers");
+  }
+
+  private static DexDomain parentDomain;
+  private static DexDomain childDomain;
+  private static DexDomain childNativeDomain;
+  private static boolean everythingWhitelisted;
+
+  private static String configMessage;
+}
diff --git a/test/2030-core-platform-api-jni/src-ex/JNI.java b/test/2030-core-platform-api-jni/src-ex/JNI.java
new file mode 100644
index 0000000..5dfb296
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src-ex/JNI.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+public class JNI {
+  public static native boolean canDiscoverField(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canGetField(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canSetField(Class<?> klass, String name, boolean isStatic);
+
+  public static native boolean canDiscoverMethod(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canInvokeMethodA(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canInvokeMethodV(Class<?> klass, String name, boolean isStatic);
+
+  public static native boolean canDiscoverConstructor(Class<?> klass, String signature);
+  public static native boolean canInvokeConstructorA(Class<?> klass, String signature);
+  public static native boolean canInvokeConstructorV(Class<?> klass, String signature);
+}
diff --git a/test/2030-core-platform-api-jni/src/DummyClass.java b/test/2030-core-platform-api-jni/src/DummyClass.java
new file mode 100644
index 0000000..afba747
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/DummyClass.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+public class DummyClass implements ParentInterface {
+  public int methodPublicWhitelist() { return 1; }
+  public int methodPublicLightGreylist() { return 2; }
+  public int methodPublicDarkGreylist() { return 3; }
+  public int methodPublicBlacklist() { return 4; }
+  public int methodPublicBlacklistAndCorePlatformApi() { return 5; }
+
+  public static ParentInterface getInterfaceInstance() {
+    return new DummyClass();
+  }
+}
diff --git a/test/2030-core-platform-api-jni/src/NullaryConstructorBlacklist.java b/test/2030-core-platform-api-jni/src/NullaryConstructorBlacklist.java
new file mode 100644
index 0000000..5bf6278
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/NullaryConstructorBlacklist.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+public class NullaryConstructorBlacklist {
+  public NullaryConstructorBlacklist() { x = 22; }
+  public NullaryConstructorBlacklist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/2030-core-platform-api-jni/src/NullaryConstructorBlacklistAndCorePlatformApi.java b/test/2030-core-platform-api-jni/src/NullaryConstructorBlacklistAndCorePlatformApi.java
new file mode 100644
index 0000000..86af29e
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/NullaryConstructorBlacklistAndCorePlatformApi.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class NullaryConstructorBlacklistAndCorePlatformApi {
+  public NullaryConstructorBlacklistAndCorePlatformApi() { x = 22; }
+  public NullaryConstructorBlacklistAndCorePlatformApi(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/2030-core-platform-api-jni/src/NullaryConstructorDarkGreylist.java b/test/2030-core-platform-api-jni/src/NullaryConstructorDarkGreylist.java
new file mode 100644
index 0000000..c25a767
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/NullaryConstructorDarkGreylist.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+public class NullaryConstructorDarkGreylist {
+  public NullaryConstructorDarkGreylist() { x = 22; }
+  public NullaryConstructorDarkGreylist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/2030-core-platform-api-jni/src/NullaryConstructorLightGreylist.java b/test/2030-core-platform-api-jni/src/NullaryConstructorLightGreylist.java
new file mode 100644
index 0000000..d5dac8b
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/NullaryConstructorLightGreylist.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+public class NullaryConstructorLightGreylist {
+  public NullaryConstructorLightGreylist() { x = 22; }
+  public NullaryConstructorLightGreylist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/2030-core-platform-api-jni/src/NullaryConstructorWhitelist.java b/test/2030-core-platform-api-jni/src/NullaryConstructorWhitelist.java
new file mode 100644
index 0000000..d101907
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/NullaryConstructorWhitelist.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+public class NullaryConstructorWhitelist {
+  public NullaryConstructorWhitelist() { x = 22; }
+  public NullaryConstructorWhitelist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/2030-core-platform-api-jni/src/ParentClass.java b/test/2030-core-platform-api-jni/src/ParentClass.java
new file mode 100644
index 0000000..1442392
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/ParentClass.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+public class ParentClass {
+  public ParentClass() {}
+
+  // INSTANCE FIELD
+
+  public int fieldPublicWhitelist = 211;
+  int fieldPackageWhitelist = 212;
+  protected int fieldProtectedWhitelist = 213;
+  private int fieldPrivateWhitelist = 214;
+  public int fieldPublicWhitelistB = 215;
+
+  public int fieldPublicLightGreylist = 221;
+  int fieldPackageLightGreylist = 222;
+  protected int fieldProtectedLightGreylist = 223;
+  private int fieldPrivateLightGreylist = 224;
+  public int fieldPublicLightGreylistB = 225;
+
+  public int fieldPublicDarkGreylist = 231;
+  int fieldPackageDarkGreylist = 232;
+  protected int fieldProtectedDarkGreylist = 233;
+  private int fieldPrivateDarkGreylist = 234;
+  public int fieldPublicDarkGreylistB = 235;
+
+  public int fieldPublicBlacklist = 241;
+  int fieldPackageBlacklist = 242;
+  protected int fieldProtectedBlacklist = 243;
+  private int fieldPrivateBlacklist = 244;
+  public int fieldPublicBlacklistB = 245;
+
+  public int fieldPublicBlacklistAndCorePlatformApi = 251;
+  int fieldPackageBlacklistAndCorePlatformApi = 252;
+  protected int fieldProtectedBlacklistAndCorePlatformApi = 253;
+  private int fieldPrivateBlacklistAndCorePlatformApi = 254;
+  public int fieldPublicBlacklistAndCorePlatformApiB = 255;
+
+  // STATIC FIELD
+
+  public static int fieldPublicStaticWhitelist = 111;
+  static int fieldPackageStaticWhitelist = 112;
+  protected static int fieldProtectedStaticWhitelist = 113;
+  private static int fieldPrivateStaticWhitelist = 114;
+  public static int fieldPublicStaticWhitelistB = 115;
+
+  public static int fieldPublicStaticLightGreylist = 121;
+  static int fieldPackageStaticLightGreylist = 122;
+  protected static int fieldProtectedStaticLightGreylist = 123;
+  private static int fieldPrivateStaticLightGreylist = 124;
+  public static int fieldPublicStaticLightGreylistB = 125;
+
+  public static int fieldPublicStaticDarkGreylist = 131;
+  static int fieldPackageStaticDarkGreylist = 132;
+  protected static int fieldProtectedStaticDarkGreylist = 133;
+  private static int fieldPrivateStaticDarkGreylist = 134;
+  public static int fieldPublicStaticDarkGreylistB = 135;
+
+  public static int fieldPublicStaticBlacklist = 141;
+  static int fieldPackageStaticBlacklist = 142;
+  protected static int fieldProtectedStaticBlacklist = 143;
+  private static int fieldPrivateStaticBlacklist = 144;
+  public static int fieldPublicStaticBlacklistB = 145;
+
+  public static int fieldPublicStaticBlacklistAndCorePlatformApi = 151;
+  static int fieldPackageStaticBlacklistAndCorePlatformApi = 152;
+  protected static int fieldProtectedStaticBlacklistAndCorePlatformApi = 153;
+  private static int fieldPrivateStaticBlacklistAndCorePlatformApi = 154;
+  public static int fieldPublicStaticBlacklistAndCorePlatformApiB = 155;
+
+  // INSTANCE METHOD
+
+  public int methodPublicWhitelist() { return 411; }
+  int methodPackageWhitelist() { return 412; }
+  protected int methodProtectedWhitelist() { return 413; }
+  private int methodPrivateWhitelist() { return 414; }
+
+  public int methodPublicLightGreylist() { return 421; }
+  int methodPackageLightGreylist() { return 422; }
+  protected int methodProtectedLightGreylist() { return 423; }
+  private int methodPrivateLightGreylist() { return 424; }
+
+  public int methodPublicDarkGreylist() { return 431; }
+  int methodPackageDarkGreylist() { return 432; }
+  protected int methodProtectedDarkGreylist() { return 433; }
+  private int methodPrivateDarkGreylist() { return 434; }
+
+  public int methodPublicBlacklist() { return 441; }
+  int methodPackageBlacklist() { return 442; }
+  protected int methodProtectedBlacklist() { return 443; }
+  private int methodPrivateBlacklist() { return 444; }
+
+  public int methodPublicBlacklistAndCorePlatformApi() { return 451; }
+  int methodPackageBlacklistAndCorePlatformApi() { return 452; }
+  protected int methodProtectedBlacklistAndCorePlatformApi() { return 453; }
+  private int methodPrivateBlacklistAndCorePlatformApi() { return 454; }
+
+  // STATIC METHOD
+
+  public static int methodPublicStaticWhitelist() { return 311; }
+  static int methodPackageStaticWhitelist() { return 312; }
+  protected static int methodProtectedStaticWhitelist() { return 313; }
+  private static int methodPrivateStaticWhitelist() { return 314; }
+
+  public static int methodPublicStaticLightGreylist() { return 321; }
+  static int methodPackageStaticLightGreylist() { return 322; }
+  protected static int methodProtectedStaticLightGreylist() { return 323; }
+  private static int methodPrivateStaticLightGreylist() { return 324; }
+
+  public static int methodPublicStaticDarkGreylist() { return 331; }
+  static int methodPackageStaticDarkGreylist() { return 332; }
+  protected static int methodProtectedStaticDarkGreylist() { return 333; }
+  private static int methodPrivateStaticDarkGreylist() { return 334; }
+
+  public static int methodPublicStaticBlacklist() { return 341; }
+  static int methodPackageStaticBlacklist() { return 342; }
+  protected static int methodProtectedStaticBlacklist() { return 343; }
+  private static int methodPrivateStaticBlacklist() { return 344; }
+
+  public static int methodPublicStaticBlacklistAndCorePlatformApi() { return 351; }
+  static int methodPackageStaticBlacklistAndCorePlatformApi() { return 352; }
+  protected static int methodProtectedStaticBlacklistAndCorePlatformApi() { return 353; }
+  private static int methodPrivateStaticBlacklistAndCorePlatformApi() { return 354; }
+
+  // CONSTRUCTOR
+
+  // Whitelist
+  public ParentClass(int x, short y) {}
+  ParentClass(float x, short y) {}
+  protected ParentClass(long x, short y) {}
+  private ParentClass(double x, short y) {}
+
+  // Light greylist
+  public ParentClass(int x, boolean y) {}
+  ParentClass(float x, boolean y) {}
+  protected ParentClass(long x, boolean y) {}
+  private ParentClass(double x, boolean y) {}
+
+  // Dark greylist
+  public ParentClass(int x, byte y) {}
+  ParentClass(float x, byte y) {}
+  protected ParentClass(long x, byte y) {}
+  private ParentClass(double x, byte y) {}
+
+  // Blacklist
+  public ParentClass(int x, char y) {}
+  ParentClass(float x, char y) {}
+  protected ParentClass(long x, char y) {}
+  private ParentClass(double x, char y) {}
+
+  // Blacklist and CorePlatformApi
+  public ParentClass(int x, int y) {}
+  ParentClass(float x, int y) {}
+  protected ParentClass(long x, int y) {}
+  private ParentClass(double x, int y) {}
+
+  // HELPERS
+
+  public int callMethodPublicWhitelist() { return methodPublicWhitelist(); }
+  public int callMethodPackageWhitelist() { return methodPackageWhitelist(); }
+  public int callMethodProtectedWhitelist() { return methodProtectedWhitelist(); }
+
+  public int callMethodPublicLightGreylist() { return methodPublicLightGreylist(); }
+  public int callMethodPackageLightGreylist() { return methodPackageLightGreylist(); }
+  public int callMethodProtectedLightGreylist() { return methodProtectedLightGreylist(); }
+
+  public int callMethodPublicDarkGreylist() { return methodPublicDarkGreylist(); }
+  public int callMethodPackageDarkGreylist() { return methodPackageDarkGreylist(); }
+  public int callMethodProtectedDarkGreylist() { return methodProtectedDarkGreylist(); }
+
+  public int callMethodPublicBlacklist() { return methodPublicBlacklist(); }
+  public int callMethodPackageBlacklist() { return methodPackageBlacklist(); }
+  public int callMethodProtectedBlacklist() { return methodProtectedBlacklist(); }
+
+  public int callMethodPublicBlacklistAndCorePlatformApi() {
+    return methodPublicBlacklistAndCorePlatformApi();
+  }
+
+  public int callMethodPackageBlacklistAndCorePlatformApi() {
+    return methodPackageBlacklistAndCorePlatformApi();
+  }
+
+  public int callMethodProtectedBlacklistAndCorePlatformApi() {
+    return methodProtectedBlacklistAndCorePlatformApi();
+  }
+}
diff --git a/test/2030-core-platform-api-jni/src/ParentInterface.java b/test/2030-core-platform-api-jni/src/ParentInterface.java
new file mode 100644
index 0000000..1c5b58f
--- /dev/null
+++ b/test/2030-core-platform-api-jni/src/ParentInterface.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+public interface ParentInterface {
+  // STATIC FIELD
+  static int fieldPublicStaticWhitelist = 11;
+  static int fieldPublicStaticLightGreylist = 12;
+  static int fieldPublicStaticDarkGreylist = 13;
+  static int fieldPublicStaticBlacklist = 14;
+  static int fieldPublicStaticBlacklistAndCorePlatformApi = 15;
+
+  // INSTANCE METHOD
+  int methodPublicWhitelist();
+  int methodPublicLightGreylist();
+  int methodPublicDarkGreylist();
+  int methodPublicBlacklist();
+  int methodPublicBlacklistAndCorePlatformApi();
+
+  // STATIC METHOD
+  static int methodPublicStaticWhitelist() { return 21; }
+  static int methodPublicStaticLightGreylist() { return 22; }
+  static int methodPublicStaticDarkGreylist() { return 23; }
+  static int methodPublicStaticBlacklist() { return 24; }
+  static int methodPublicStaticBlacklistAndCorePlatformApi() { return 25; }
+
+  // DEFAULT METHOD
+  default int methodPublicDefaultWhitelist() { return 31; }
+  default int methodPublicDefaultLightGreylist() { return 32; }
+  default int methodPublicDefaultDarkGreylist() { return 33; }
+  default int methodPublicDefaultBlacklist() { return 34; }
+  default int methodPublicDefaultBlacklistAndCorePlatformApi() { return 35; }
+}
diff --git a/test/Android.bp b/test/Android.bp
index 7182513..53f76c6 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -601,6 +601,7 @@
         "1972-jni-id-swap-indices/jni_id.cc",
         "1985-structural-redefine-stack-scope/stack_scope.cc",
         "2011-stack-walk-concurrent-instrument/stack_walk_concurrent.cc",
+        "2030-core-platform-api-jni/core-platform-api-jni.cc",
         "common/runtime_state.cc",
         "common/stack_inspect.cc",
     ],
diff --git a/test/knownfailures.json b/test/knownfailures.json
index 80904db..bb80531 100644
--- a/test/knownfailures.json
+++ b/test/knownfailures.json
@@ -490,7 +490,8 @@
             "607-daemon-stress",
             "674-hiddenapi",
             "687-deopt",
-            "904-object-allocation"
+            "904-object-allocation",
+            "2030-core-platform-api-jni"
         ],
         "description": ["Tests that sometimes fail when run with jvmti-stress for unknown reasons."],
         "bug": "b/120995005",
@@ -544,7 +545,8 @@
             "690-hiddenapi-same-name-methods",
             "804-class-extends-itself",
             "921-hello-failure",
-            "999-redefine-hiddenapi"
+            "999-redefine-hiddenapi",
+            "2030-core-platform-api-jni"
         ],
         "description": [
             "Tests that use illegal dex files or otherwise break dexter assumptions"
@@ -566,7 +568,8 @@
             "692-vdex-inmem-loader",
             "693-vdex-inmem-loader-evict",
             "944-transform-classloaders",
-            "999-redefine-hiddenapi"
+            "999-redefine-hiddenapi",
+            "2030-core-platform-api-jni"
         ],
         "description": [
             "Tests that use custom class loaders or other features not supported ",
@@ -1204,7 +1207,8 @@
                   "2004-double-virtual-structural-abstract",
                   "2005-pause-all-redefine-multithreaded",
                   "2006-virtual-structural-finalizing",
-                  "2007-virtual-structural-finalizable"
+                  "2007-virtual-structural-finalizable",
+                  "2030-core-platform-api-jni"
                 ],
         "variant": "jvm",
         "description": ["Doesn't run on RI."]