diff --git a/tests/GosCompatTests/GosCompatCheckApp/Android.bp b/tests/GosCompatTests/GosCompatCheckApp/Android.bp index dc9c17f023342..c60b60e5e9806 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/Android.bp +++ b/tests/GosCompatTests/GosCompatCheckApp/Android.bp @@ -12,6 +12,25 @@ cc_library_shared { stl: "none", } +cc_library_shared { + name: "libgoscompat_dmabuf_release_jni", + srcs: ["jni/dmabuf_release_jni.cpp"], + // The helper app is SDK-built, so the JNI dependency needs a matching SDK variant. + sdk_version: "current", + header_libs: ["jni_headers"], + shared_libs: [ + "libEGL", + "libGLESv2", + "liblog", + ], + cflags: [ + "-Wall", + "-Wextra", + "-Werror", + ], + stl: "c++_static", +} + android_test_helper_app { name: "GosCompatCheckApp", srcs: [ @@ -19,7 +38,10 @@ android_test_helper_app { "src/**/*.kt", ], manifest: "AndroidManifest.xml", - jni_libs: ["libgoscompat_maps_scan_jni"], + jni_libs: [ + "libgoscompat_maps_scan_jni", + "libgoscompat_dmabuf_release_jni", + ], // Keep the JNI library inside the standalone helper APK loaded by Tradefed and manual installs. use_embedded_native_libs: true, // Build both ABIs so the helper can run under either app process ABI in multi-ABI test runs. diff --git a/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml b/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml index 70b02b471e839..2e6e818ccc050 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml +++ b/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml @@ -23,6 +23,28 @@ android:process=":maps_scan" android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + + + + + + + + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_TAG "GosCompatDmaBufRelease" +#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +#ifndef EGL_PROTECTED_CONTENT_EXT +#define EGL_PROTECTED_CONTENT_EXT 0x32C0 +#endif + +#ifndef GL_TEXTURE_PROTECTED_EXT +#define GL_TEXTURE_PROTECTED_EXT 0x8BFA +#endif + +namespace { + +constexpr const char* DIRECT_HEAP_DEVICE_PREFIX = "/dev/dma_heap/"; +constexpr const char* VFRAME_SECURE_HEAP_NAME = "vframe-secure"; +constexpr const char* VSTREAM_SECURE_HEAP_NAME = "vstream-secure"; +constexpr uint64_t DIRECT_HEAP_BYTES_PER_PIXEL = 4; +constexpr uint64_t DIRECT_HEAP_EXPECTED_CHUNK_SIZE = 65536; +constexpr const char* EGL_PROTECTED_CONTENT_EXTENSION = "EGL_EXT_protected_content"; +constexpr const char* GL_PROTECTED_TEXTURES_EXTENSION = "GL_EXT_protected_textures"; + +std::mutex g_workload_mutex; +/* + * Retained DMA-BUF fds intentionally stay outside ScopedFd. The tests need to + * choose between explicit close through release_workload() and process teardown. + */ +std::vector g_retained_fds; + +struct ProtectedEglWorkload { + EGLDisplay display = EGL_NO_DISPLAY; + EGLContext context = EGL_NO_CONTEXT; + EGLSurface surface = EGL_NO_SURFACE; + GLuint texture = 0; +}; + +ProtectedEglWorkload g_protected_egl_workload; + +struct NativeResult { + bool ready = false; + bool unsupported = false; + bool protected_content = true; + int allocated_buffers = 0; + int pid = 0; + int tid = 0; + std::string heap_path; + std::string heap_name; + std::string allocator; + std::string allocation; + std::string error; +}; + +int get_thread_id() { + return static_cast(syscall(SYS_gettid)); +} + +void close_fd(int fd) { + if (fd >= 0) { + close(fd); + } +} + +bool has_extension(const char* extensions, const char* extension) { + if (extensions == nullptr || extension == nullptr) { + return false; + } + + const size_t extension_length = std::strlen(extension); + const char* current = extensions; + while ((current = std::strstr(current, extension)) != nullptr) { + const bool starts_token = current == extensions || current[-1] == ' '; + const char next = current[extension_length]; + const bool ends_token = next == '\0' || next == ' '; + if (starts_token && ends_token) { + return true; + } + current += extension_length; + } + return false; +} + +std::string hex_error(const char* prefix, unsigned int error) { + char buffer[64]; + std::snprintf(buffer, sizeof(buffer), "%s 0x%x", prefix, error); + return buffer; +} + +void destroy_protected_egl_workload(ProtectedEglWorkload* workload) { + if (workload->display == EGL_NO_DISPLAY) { + return; + } + + if (workload->context != EGL_NO_CONTEXT && workload->surface != EGL_NO_SURFACE) { + eglMakeCurrent(workload->display, workload->surface, workload->surface, + workload->context); + } + if (workload->texture != 0) { + glDeleteTextures(1, &workload->texture); + } + eglMakeCurrent(workload->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (workload->surface != EGL_NO_SURFACE) { + eglDestroySurface(workload->display, workload->surface); + } + if (workload->context != EGL_NO_CONTEXT) { + eglDestroyContext(workload->display, workload->context); + } + eglTerminate(workload->display); + *workload = ProtectedEglWorkload(); +} + +class ScopedFd { +public: + explicit ScopedFd(int fd = -1) : mFd(fd) {} + + ~ScopedFd() { + reset(); + } + + ScopedFd(const ScopedFd&) = delete; + ScopedFd& operator=(const ScopedFd&) = delete; + + ScopedFd(ScopedFd&& other) noexcept : mFd(other.release()) {} + + ScopedFd& operator=(ScopedFd&& other) noexcept { + if (this != &other) { + reset(other.release()); + } + return *this; + } + + int get() const { + return mFd; + } + + int release() { + int fd = mFd; + mFd = -1; + return fd; + } + + void reset(int fd = -1) { + close_fd(mFd); + mFd = fd; + } + +private: + int mFd; +}; + +void release_workload() { + std::lock_guard lock(g_workload_mutex); + for (int fd : g_retained_fds) { + close_fd(fd); + } + g_retained_fds.clear(); + + destroy_protected_egl_workload(&g_protected_egl_workload); +} + +bool is_allowed_heap_name(const std::string& heap_name) { + return heap_name == VFRAME_SECURE_HEAP_NAME || heap_name == VSTREAM_SECURE_HEAP_NAME; +} + +void set_errno_error(NativeResult* result, const std::string& operation, int error_number) { + result->error = operation + ": " + std::strerror(error_number); +} + +uint64_t direct_heap_allocation_size(int width, int height, NativeResult* result) { + const uint64_t safe_width = width > 0 ? static_cast(width) : 1; + const uint64_t safe_height = height > 0 ? static_cast(height) : 1; + const uint64_t max_size = std::numeric_limits::max(); + if (safe_width > max_size / safe_height || + safe_width * safe_height > max_size / DIRECT_HEAP_BYTES_PER_PIXEL) { + result->error = "Direct secure DMA-BUF allocation size overflowed"; + return 0; + } + return safe_width * safe_height * DIRECT_HEAP_BYTES_PER_PIXEL; +} + +NativeResult start_workload(const std::string& requested_heap_name, int width, int height, + int buffer_count) { + release_workload(); + + NativeResult result; + result.pid = getpid(); + result.tid = get_thread_id(); + + const std::string heap_name = requested_heap_name.empty() + ? VFRAME_SECURE_HEAP_NAME : requested_heap_name; + result.heap_name = heap_name; + result.allocator = "dma_heap"; + + if (!is_allowed_heap_name(heap_name)) { + result.unsupported = true; + result.error = "Unsupported direct secure DMA-BUF heap name"; + return result; + } + + const uint64_t allocation_size = direct_heap_allocation_size(width, height, &result); + if (allocation_size == 0) { + return result; + } + result.allocation = "bytes=" + std::to_string(allocation_size) + + ", expected_chunks=" + + std::to_string(allocation_size / DIRECT_HEAP_EXPECTED_CHUNK_SIZE) + + ", expected_chunk_size=" + std::to_string(DIRECT_HEAP_EXPECTED_CHUNK_SIZE); + + const std::string heap_path = std::string(DIRECT_HEAP_DEVICE_PREFIX) + heap_name; + result.heap_path = heap_path; + int raw_heap_fd; + do { + raw_heap_fd = open(heap_path.c_str(), O_RDONLY | O_CLOEXEC); + } while (raw_heap_fd < 0 && errno == EINTR); + if (raw_heap_fd < 0) { + const int error_number = errno; + if (error_number == ENOENT || error_number == EACCES) { + result.unsupported = true; + } + set_errno_error(&result, "open " + heap_path, error_number); + return result; + } + ScopedFd heap_fd(raw_heap_fd); + + /* + * This follows the same DMA-BUF allocation and fd release shape as + * DmaBufHeapTest.Allocate and DmaBufHeapTest.RepeatedAllocate in + * system/memory/libdmabufheap/tests/dmabuf_heap_test.cpp. This helper uses + * the raw DMA heap UAPI because it must target the Pixel secure heap device + * names directly and run inside the SDK-built helper APK process. + */ + std::vector allocated_fds; + const int requested_buffers = buffer_count > 0 ? buffer_count : 1; + for (int i = 0; i < requested_buffers; i++) { + struct dma_heap_allocation_data data = {}; + data.len = allocation_size; + data.fd_flags = O_RDWR | O_CLOEXEC; + data.heap_flags = 0; + + int ret; + do { + ret = ioctl(heap_fd.get(), DMA_HEAP_IOCTL_ALLOC, &data); + } while (ret < 0 && errno == EINTR); + if (ret < 0) { + set_errno_error(&result, "DMA_HEAP_IOCTL_ALLOC " + heap_name, errno); + break; + } + ScopedFd allocation(static_cast(data.fd)); + allocated_fds.push_back(std::move(allocation)); + } + + result.allocated_buffers = static_cast(allocated_fds.size()); + if (!result.error.empty()) { + return result; + } + if (allocated_fds.empty()) { + if (result.error.empty()) { + result.error = "No DMA-BUF file descriptors were allocated"; + } + return result; + } + + result.ready = true; + result.unsupported = false; + result.error.clear(); + { + std::vector fds; + fds.reserve(allocated_fds.size()); + for (ScopedFd& fd : allocated_fds) { + fds.push_back(fd.release()); + } + + std::lock_guard lock(g_workload_mutex); + g_retained_fds = std::move(fds); + } + return result; +} + +/* + * This follows the public CTS protected EGL shape rather than private GPU APIs: + * - cts/tests/tests/media/common/src/android/media/cts/OutputSurface.java creates a + * protected GLES context and protected pbuffer with EGL_PROTECTED_CONTENT_EXT. + * - cts/tests/vr/src/android/vr/cts/RendererProtectedTexturesTest.java marks a + * texture with GL_TEXTURE_PROTECTED_EXT, and VrExtensionBehaviorTest.java validates + * those protected attributes. + * + * Unlike CTS, this helper keeps the protected EGL resources live until release_workload() + * or process teardown, so the test covers the DMA-BUF final release path. + */ +NativeResult start_protected_egl_workload(int width, int height, int iterations) { + release_workload(); + + NativeResult result; + ProtectedEglWorkload workload; + result.pid = getpid(); + result.tid = get_thread_id(); + result.heap_name = "vendor-selected"; + result.allocator = "EGL_EXT_protected_content"; + result.protected_content = true; + + auto fail = [&result, &workload](const std::string& error) -> NativeResult { + result.error = error; + destroy_protected_egl_workload(&workload); + return result; + }; + auto unsupported = [&result, &workload](const std::string& error) -> NativeResult { + result.unsupported = true; + result.error = error; + destroy_protected_egl_workload(&workload); + return result; + }; + + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) { + result.error = hex_error("eglGetDisplay failed with EGL error", eglGetError()); + return result; + } + + if (!eglInitialize(display, nullptr, nullptr)) { + result.error = hex_error("eglInitialize failed with EGL error", eglGetError()); + return result; + } + workload.display = display; + + const char* egl_extensions = eglQueryString(display, EGL_EXTENSIONS); + if (!has_extension(egl_extensions, EGL_PROTECTED_CONTENT_EXTENSION)) { + return unsupported("EGL_EXT_protected_content is not advertised"); + } + + if (!eglBindAPI(EGL_OPENGL_ES_API)) { + return fail(hex_error("eglBindAPI failed with EGL error", eglGetError())); + } + + const EGLint config_attributes[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE, + }; + EGLConfig config = nullptr; + EGLint config_count = 0; + if (!eglChooseConfig(display, config_attributes, &config, 1, &config_count) || + config_count == 0) { + return fail(hex_error("eglChooseConfig failed with EGL error", eglGetError())); + } + + EGLint max_pbuffer_width = 1; + EGLint max_pbuffer_height = 1; + eglGetConfigAttrib(display, config, EGL_MAX_PBUFFER_WIDTH, &max_pbuffer_width); + eglGetConfigAttrib(display, config, EGL_MAX_PBUFFER_HEIGHT, &max_pbuffer_height); + if (max_pbuffer_width < 1 || max_pbuffer_height < 1) { + return fail("EGL config reported an invalid pbuffer size limit"); + } + const EGLint requested_width = width > 0 ? width : 1; + const EGLint requested_height = height > 0 ? height : 1; + const EGLint actual_width = requested_width <= max_pbuffer_width + ? requested_width : max_pbuffer_width; + const EGLint actual_height = requested_height <= max_pbuffer_height + ? requested_height : max_pbuffer_height; + + const EGLint context_attributes[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, + EGL_NONE, + }; + EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attributes); + if (context == EGL_NO_CONTEXT) { + return fail(hex_error("eglCreateContext failed with EGL error", eglGetError())); + } + workload.context = context; + + const EGLint surface_attributes[] = { + EGL_WIDTH, actual_width, + EGL_HEIGHT, actual_height, + EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, + EGL_NONE, + }; + EGLSurface surface = eglCreatePbufferSurface(display, config, surface_attributes); + if (surface == EGL_NO_SURFACE) { + return fail(hex_error("eglCreatePbufferSurface failed with EGL error", eglGetError())); + } + workload.surface = surface; + + if (!eglMakeCurrent(display, surface, surface, context)) { + return fail(hex_error("eglMakeCurrent failed with EGL error", eglGetError())); + } + + const char* gl_extensions = reinterpret_cast(glGetString(GL_EXTENSIONS)); + if (!has_extension(gl_extensions, GL_PROTECTED_TEXTURES_EXTENSION)) { + return unsupported("GL_EXT_protected_textures is not advertised"); + } + + GLuint texture = 0; + glGenTextures(1, &texture); + workload.texture = texture; + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_PROTECTED_EXT, GL_TRUE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, actual_width, actual_height, 0, GL_RGBA, + GL_UNSIGNED_BYTE, nullptr); + GLenum gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) { + return fail(hex_error("protected glTexImage2D failed with GL error", gl_error)); + } + + const int draw_iterations = iterations > 0 ? iterations : 1; + glViewport(0, 0, actual_width, actual_height); + for (int i = 0; i < draw_iterations; i++) { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + } + glFinish(); + gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) { + return fail(hex_error("protected GL workload failed with GL error", gl_error)); + } + + result.ready = true; + result.unsupported = false; + result.allocated_buffers = 2; + result.allocation = "protected_pbuffer=" + std::to_string(actual_width) + + "x" + std::to_string(actual_height) + + ", protected_texture=" + std::to_string(actual_width) + + "x" + std::to_string(actual_height) + + ", requested=" + std::to_string(requested_width) + + "x" + std::to_string(requested_height) + + ", iterations=" + std::to_string(draw_iterations); + + { + std::lock_guard lock(g_workload_mutex); + g_protected_egl_workload = workload; + } + return result; +} + +jobjectArray result_to_array(JNIEnv* env, const NativeResult& result) { + jclass string_class = env->FindClass("java/lang/String"); + jobjectArray array = env->NewObjectArray(11, string_class, nullptr); + const std::string values[] = { + result.ready ? "true" : "false", + result.unsupported ? "true" : "false", + result.protected_content ? "true" : "false", + std::to_string(result.allocated_buffers), + std::to_string(result.pid), + std::to_string(result.tid), + result.heap_path, + result.heap_name, + result.allocator, + result.allocation, + result.error, + }; + for (jsize i = 0; i < 11; i++) { + jstring value = env->NewStringUTF(values[i].c_str()); + env->SetObjectArrayElement(array, i, value); + env->DeleteLocalRef(value); + } + return array; +} + +} // namespace + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_app_grapheneos_goscompat_checks_dmabuf_DmaBufReleaseRunner_nativeStartWorkload( + JNIEnv* env, jobject /* thiz */, jstring heap_name, jint width, jint height, + jint buffer_count) { + const char* heap_name_chars = heap_name != nullptr + ? env->GetStringUTFChars(heap_name, nullptr) : nullptr; + std::string heap_name_string = heap_name_chars != nullptr ? heap_name_chars : ""; + if (heap_name_chars != nullptr) { + env->ReleaseStringUTFChars(heap_name, heap_name_chars); + } + + NativeResult result = start_workload(heap_name_string, width, height, buffer_count); + if (!result.ready && !result.error.empty()) { + ALOGE("%s", result.error.c_str()); + } + return result_to_array(env, result); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_grapheneos_goscompat_checks_dmabuf_DmaBufReleaseRunner_nativeReleaseWorkload( + JNIEnv* /* env */, jobject /* thiz */) { + release_workload(); +} + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_app_grapheneos_goscompat_checks_dmabuf_DmaBufReleaseRunner_nativeStartProtectedEglWorkload( + JNIEnv* env, jobject /* thiz */, jint width, jint height, jint iterations) { + NativeResult result = start_protected_egl_workload(width, height, iterations); + if (!result.ready && !result.error.empty()) { + ALOGE("%s", result.error.c_str()); + } + return result_to_array(env, result); +} diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/GosCompatCheckActivity.kt b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/GosCompatCheckActivity.kt index c7c80ca754e51..d46091c75e193 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/GosCompatCheckActivity.kt +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/GosCompatCheckActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -37,19 +38,24 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import app.grapheneos.goscompat.checks.dmabuf.DmaBufReleasePanel class GosCompatCheckActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE) enableEdgeToEdge( - statusBarStyle = SystemBarStyle.auto(AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT), - navigationBarStyle = SystemBarStyle.auto(AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT), + statusBarStyle = SystemBarStyle.auto( + AndroidColor.TRANSPARENT, + AndroidColor.TRANSPARENT, + ), + navigationBarStyle = SystemBarStyle.auto( + AndroidColor.TRANSPARENT, + AndroidColor.TRANSPARENT, + ), ) setContent { @@ -71,18 +77,21 @@ class GosCompatCheckActivity : ComponentActivity() { reflectiveMapsScanRunning = reflectiveMapsScanRunning.value, onRunDirectMapsScan = { startMapsScan( - GosCompatContract.METHOD_RUN_DIRECT_MAPS_SCAN_CHECK, + GosCompatContract.MapsScan.Method.RUN_DIRECT_CHECK, directMapsScanResult, directMapsScanRunning, ) }, onRunReflectiveMapsScan = { startMapsScan( - GosCompatContract.METHOD_RUN_REFLECTIVE_MAPS_SCAN_CHECK, + GosCompatContract.MapsScan.Method.RUN_REFLECTIVE_CHECK, reflectiveMapsScanResult, reflectiveMapsScanRunning, ) }, + dmaBufReleaseContent = { + DmaBufReleasePanel(this@GosCompatCheckActivity) + }, ) } } @@ -111,7 +120,7 @@ class GosCompatCheckActivity : ComponentActivity() { val startTimeMillis = System.currentTimeMillis() return try { val client = context.contentResolver.acquireUnstableContentProviderClient( - GosCompatContract.MAPS_SCAN_CONTENT_URI, + GosCompatContract.MapsScan.CONTENT_URI, ) ?: return MapsScanResult.failed("Maps scan provider was unavailable") val bundle = try { // Keep the UI process from becoming a stable provider dependent. The scanner is @@ -143,6 +152,7 @@ private fun GosCompatCheckScreen( reflectiveMapsScanRunning: Boolean, onRunDirectMapsScan: () -> Unit, onRunReflectiveMapsScan: () -> Unit, + dmaBufReleaseContent: @Composable () -> Unit, ) { Column( modifier = Modifier @@ -158,23 +168,42 @@ private fun GosCompatCheckScreen( fontWeight = FontWeight.SemiBold, ) - MapsScanControl( - title = "Direct JNI maps scan", - mapsScanResult = directMapsScanResult, - mapsScanRunning = directMapsScanRunning, - onRunMapsScan = onRunDirectMapsScan, - ) - ResultSection("Direct JNI details", directMapsScanResult?.details.orEmpty()) - ResultSection("Direct JNI errors", directMapsScanResult?.errors.orEmpty()) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text( + text = "Memory maps scan", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + MapsScanControl( + title = "Direct JNI maps scan", + mapsScanResult = directMapsScanResult, + mapsScanRunning = directMapsScanRunning, + onRunMapsScan = onRunDirectMapsScan, + ) + ResultSection("Direct JNI details", directMapsScanResult?.details.orEmpty()) + ResultSection("Direct JNI errors", directMapsScanResult?.errors.orEmpty()) - MapsScanControl( - title = "Reflective JNI maps scan", - mapsScanResult = reflectiveMapsScanResult, - mapsScanRunning = reflectiveMapsScanRunning, - onRunMapsScan = onRunReflectiveMapsScan, - ) - ResultSection("Reflective JNI details", reflectiveMapsScanResult?.details.orEmpty()) - ResultSection("Reflective JNI errors", reflectiveMapsScanResult?.errors.orEmpty()) + MapsScanControl( + title = "Reflective JNI maps scan", + mapsScanResult = reflectiveMapsScanResult, + mapsScanRunning = reflectiveMapsScanRunning, + onRunMapsScan = onRunReflectiveMapsScan, + ) + ResultSection("Reflective JNI details", reflectiveMapsScanResult?.details.orEmpty()) + ResultSection("Reflective JNI errors", reflectiveMapsScanResult?.errors.orEmpty()) + } + } + + dmaBufReleaseContent() } } @@ -209,10 +238,7 @@ private fun MapsScanControl( text = status, modifier = Modifier .background(statusColor.copy(alpha = 0.12f), RoundedCornerShape(6.dp)) - .padding(horizontal = 12.dp, vertical = 8.dp) - .semantics { - contentDescription = "Memory maps scan status: $status" - }, + .padding(horizontal = 12.dp, vertical = 8.dp), color = statusColor, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, @@ -231,7 +257,7 @@ private fun MapsScanControl( } @Composable -private fun ResultSection(title: String, lines: List) { +internal fun ResultSection(title: String, lines: List) { Column(modifier = Modifier.fillMaxWidth()) { Text( text = title, diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanCrashActivity.java b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanCrashActivity.java index f61196238bedc..c5fbf4be93af2 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanCrashActivity.java +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanCrashActivity.java @@ -8,14 +8,14 @@ public final class MapsScanCrashActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - String mode = getIntent().getStringExtra(GosCompatContract.EXTRA_MAPS_SCAN_MODE); - String token = getIntent().getStringExtra(GosCompatContract.EXTRA_MAPS_SCAN_TOKEN); + String mode = getIntent().getStringExtra(GosCompatContract.MapsScan.Extra.MODE); + String token = getIntent().getStringExtra(GosCompatContract.MapsScan.Extra.TOKEN); MapsScanResultStore.clear(this); // Keep the native scan outside the launch transaction so a segfault is attributed // to this isolated app process rather than the instrumentation shell command. Thread launcher = new Thread(() -> { - MapsScanResult result = GosCompatContract.MODE_REFLECTIVE.equals(mode) + MapsScanResult result = GosCompatContract.MapsScan.Mode.REFLECTIVE.equals(mode) ? MapsScanRunner.runReflective() : MapsScanRunner.runDirect(); MapsScanResultStore.save(getApplicationContext(), token, result); diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanProvider.java b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanProvider.java index 8c4111061b660..797af963c49f0 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanProvider.java +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanProvider.java @@ -14,17 +14,17 @@ public boolean onCreate() { @Override public Bundle call(String method, String arg, Bundle extras) { - if (GosCompatContract.METHOD_RUN_MAPS_SCAN_CHECK.equals(method) - || GosCompatContract.METHOD_RUN_DIRECT_MAPS_SCAN_CHECK.equals(method)) { + if (GosCompatContract.MapsScan.Method.RUN_CHECK.equals(method) + || GosCompatContract.MapsScan.Method.RUN_DIRECT_CHECK.equals(method)) { return MapsScanRunner.runDirect().toBundle(); } - if (GosCompatContract.METHOD_RUN_REFLECTIVE_MAPS_SCAN_CHECK.equals(method)) { + if (GosCompatContract.MapsScan.Method.RUN_REFLECTIVE_CHECK.equals(method)) { return MapsScanRunner.runReflective().toBundle(); } - if (GosCompatContract.METHOD_GET_MAPS_SCAN_RESULT.equals(method)) { + if (GosCompatContract.MapsScan.Method.GET_RESULT.equals(method)) { return MapsScanResultStore.load(getContext(), arg); } - if (GosCompatContract.METHOD_CLEAR_MAPS_SCAN_RESULT.equals(method)) { + if (GosCompatContract.MapsScan.Method.CLEAR_RESULT.equals(method)) { return MapsScanResultStore.clear(getContext()); } return super.call(method, arg, extras); diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResult.java b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResult.java index 5402c70180d9c..0cbd8cc6fefce 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResult.java +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResult.java @@ -76,31 +76,31 @@ public List getErrors() { public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putBoolean(GosCompatContract.KEY_MAPS_SCAN_COMPLETED, mCompleted); - bundle.putBoolean(GosCompatContract.KEY_MAPS_SCAN_WORKER_THREAD, mWorkerThread); - bundle.putInt(GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES, mSelectedRanges); - bundle.putLong(GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES, mScannedBytes); - bundle.putInt(GosCompatContract.KEY_MAPS_SCAN_CALLER_TID, mCallerTid); - bundle.putInt(GosCompatContract.KEY_MAPS_SCAN_WORKER_TID, mWorkerTid); - bundle.putString(GosCompatContract.KEY_STATUS_TEXT, getStatusText()); - bundle.putString(GosCompatContract.KEY_SUMMARY, getSummary()); - bundle.putStringArrayList(GosCompatContract.KEY_MAPS_SCAN_DETAILS, + bundle.putBoolean(GosCompatContract.MapsScan.Key.COMPLETED, mCompleted); + bundle.putBoolean(GosCompatContract.MapsScan.Key.WORKER_THREAD, mWorkerThread); + bundle.putInt(GosCompatContract.MapsScan.Key.SELECTED_RANGES, mSelectedRanges); + bundle.putLong(GosCompatContract.MapsScan.Key.SCANNED_BYTES, mScannedBytes); + bundle.putInt(GosCompatContract.MapsScan.Key.CALLER_TID, mCallerTid); + bundle.putInt(GosCompatContract.MapsScan.Key.WORKER_TID, mWorkerTid); + bundle.putString(GosCompatContract.MapsScan.Key.STATUS_TEXT, getStatusText()); + bundle.putString(GosCompatContract.MapsScan.Key.SUMMARY, getSummary()); + bundle.putStringArrayList(GosCompatContract.MapsScan.Key.DETAILS, new ArrayList<>(mDetails)); - bundle.putStringArrayList(GosCompatContract.KEY_MAPS_SCAN_ERRORS, + bundle.putStringArrayList(GosCompatContract.MapsScan.Key.ERRORS, new ArrayList<>(mErrors)); return bundle; } public static MapsScanResult fromBundle(Bundle bundle) { return new MapsScanResult( - bundle.getBoolean(GosCompatContract.KEY_MAPS_SCAN_COMPLETED), - bundle.getBoolean(GosCompatContract.KEY_MAPS_SCAN_WORKER_THREAD), - bundle.getInt(GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES), - bundle.getLong(GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES), - bundle.getInt(GosCompatContract.KEY_MAPS_SCAN_CALLER_TID), - bundle.getInt(GosCompatContract.KEY_MAPS_SCAN_WORKER_TID), - getStringArrayList(bundle, GosCompatContract.KEY_MAPS_SCAN_DETAILS), - getStringArrayList(bundle, GosCompatContract.KEY_MAPS_SCAN_ERRORS)); + bundle.getBoolean(GosCompatContract.MapsScan.Key.COMPLETED), + bundle.getBoolean(GosCompatContract.MapsScan.Key.WORKER_THREAD), + bundle.getInt(GosCompatContract.MapsScan.Key.SELECTED_RANGES), + bundle.getLong(GosCompatContract.MapsScan.Key.SCANNED_BYTES), + bundle.getInt(GosCompatContract.MapsScan.Key.CALLER_TID), + bundle.getInt(GosCompatContract.MapsScan.Key.WORKER_TID), + getStringArrayList(bundle, GosCompatContract.MapsScan.Key.DETAILS), + getStringArrayList(bundle, GosCompatContract.MapsScan.Key.ERRORS)); } public static MapsScanResult failed(String error) { diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResultStore.java b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResultStore.java index c72431bf9021b..6e585c01f9ec7 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResultStore.java +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanResultStore.java @@ -41,18 +41,18 @@ static void save(Context context, String token, MapsScanResult result) { .commit(); Properties properties = new Properties(); - properties.setProperty(GosCompatContract.EXTRA_MAPS_SCAN_TOKEN, token); - properties.setProperty(GosCompatContract.KEY_MAPS_SCAN_COMPLETED, + properties.setProperty(GosCompatContract.MapsScan.Extra.TOKEN, token); + properties.setProperty(GosCompatContract.MapsScan.Key.COMPLETED, Boolean.toString(result.isCompleted())); - properties.setProperty(GosCompatContract.KEY_MAPS_SCAN_WORKER_THREAD, + properties.setProperty(GosCompatContract.MapsScan.Key.WORKER_THREAD, Boolean.toString(result.usedWorkerThread())); - properties.setProperty(GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES, + properties.setProperty(GosCompatContract.MapsScan.Key.SELECTED_RANGES, Integer.toString(result.getSelectedRanges())); - properties.setProperty(GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES, + properties.setProperty(GosCompatContract.MapsScan.Key.SCANNED_BYTES, Long.toString(result.getScannedBytes())); - properties.setProperty(GosCompatContract.KEY_MAPS_SCAN_CALLER_TID, + properties.setProperty(GosCompatContract.MapsScan.Key.CALLER_TID, Integer.toString(result.getCallerTid())); - properties.setProperty(GosCompatContract.KEY_MAPS_SCAN_WORKER_TID, + properties.setProperty(GosCompatContract.MapsScan.Key.WORKER_TID, Integer.toString(result.getWorkerTid())); try (FileOutputStream output = new FileOutputStream(resultFile(context))) { properties.store(output, null); @@ -64,31 +64,31 @@ static Bundle load(Context context, String token) { Bundle bundle = new Bundle(); SharedPreferences prefs = prefs(context); if (token == null || !token.equals(prefs.getString(KEY_TOKEN, null))) { - bundle.putBoolean(GosCompatContract.KEY_MAPS_SCAN_RESULT_AVAILABLE, false); + bundle.putBoolean(GosCompatContract.MapsScan.Key.RESULT_AVAILABLE, false); return bundle; } boolean completed = prefs.getBoolean(KEY_COMPLETED, false); boolean workerThread = prefs.getBoolean(KEY_WORKER_THREAD, false); - bundle.putBoolean(GosCompatContract.KEY_MAPS_SCAN_RESULT_AVAILABLE, true); - bundle.putBoolean(GosCompatContract.KEY_MAPS_SCAN_COMPLETED, completed); - bundle.putBoolean(GosCompatContract.KEY_MAPS_SCAN_WORKER_THREAD, workerThread); - bundle.putInt(GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES, + bundle.putBoolean(GosCompatContract.MapsScan.Key.RESULT_AVAILABLE, true); + bundle.putBoolean(GosCompatContract.MapsScan.Key.COMPLETED, completed); + bundle.putBoolean(GosCompatContract.MapsScan.Key.WORKER_THREAD, workerThread); + bundle.putInt(GosCompatContract.MapsScan.Key.SELECTED_RANGES, prefs.getInt(KEY_SELECTED_RANGES, 0)); - bundle.putLong(GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES, + bundle.putLong(GosCompatContract.MapsScan.Key.SCANNED_BYTES, prefs.getLong(KEY_SCANNED_BYTES, 0)); - bundle.putInt(GosCompatContract.KEY_MAPS_SCAN_CALLER_TID, + bundle.putInt(GosCompatContract.MapsScan.Key.CALLER_TID, prefs.getInt(KEY_CALLER_TID, 0)); - bundle.putInt(GosCompatContract.KEY_MAPS_SCAN_WORKER_TID, + bundle.putInt(GosCompatContract.MapsScan.Key.WORKER_TID, prefs.getInt(KEY_WORKER_TID, 0)); - bundle.putString(GosCompatContract.KEY_STATUS_TEXT, + bundle.putString(GosCompatContract.MapsScan.Key.STATUS_TEXT, completed && workerThread ? "Completed" : "Failed"); - bundle.putString(GosCompatContract.KEY_SUMMARY, + bundle.putString(GosCompatContract.MapsScan.Key.SUMMARY, completed && workerThread ? "Native maps scan completed from a worker thread." : "Native maps scan did not complete from a worker thread."); - bundle.putStringArrayList(GosCompatContract.KEY_MAPS_SCAN_DETAILS, new ArrayList<>()); - bundle.putStringArrayList(GosCompatContract.KEY_MAPS_SCAN_ERRORS, new ArrayList<>()); + bundle.putStringArrayList(GosCompatContract.MapsScan.Key.DETAILS, new ArrayList<>()); + bundle.putStringArrayList(GosCompatContract.MapsScan.Key.ERRORS, new ArrayList<>()); return bundle; } @@ -97,7 +97,7 @@ static Bundle clear(Context context) { resultFile(context).delete(); Bundle bundle = new Bundle(); - bundle.putBoolean(GosCompatContract.KEY_MAPS_SCAN_RESULT_AVAILABLE, false); + bundle.putBoolean(GosCompatContract.MapsScan.Key.RESULT_AVAILABLE, false); return bundle; } @@ -106,6 +106,6 @@ private static SharedPreferences prefs(Context context) { } private static File resultFile(Context context) { - return new File(context.getFilesDir(), GosCompatContract.MAPS_SCAN_RESULT_FILE); + return new File(context.getFilesDir(), GosCompatContract.MapsScan.RESULT_FILE); } } diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanTombstoneReporter.java b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanTombstoneReporter.java index 390ca4d44e6d8..9c43ec2d9cf2c 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanTombstoneReporter.java +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/MapsScanTombstoneReporter.java @@ -76,7 +76,7 @@ static MapsScanResult getNativeCrashResult(Context context, long startTimeMillis private static boolean isMatchingNativeCrash(ApplicationExitInfo exit, long startTimeMillis) { return exit.getTimestamp() >= startTimeMillis && exit.getReason() == ApplicationExitInfo.REASON_CRASH_NATIVE - && GosCompatContract.MAPS_SCAN_PROCESS.equals(exit.getProcessName()); + && GosCompatContract.MapsScan.PROCESS.equals(exit.getProcessName()); } private static String formatTombstone(ApplicationExitInfo exit) throws Exception { diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseActivity.kt b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseActivity.kt new file mode 100644 index 0000000000000..f0801d2c292c9 --- /dev/null +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseActivity.kt @@ -0,0 +1,74 @@ +package app.grapheneos.goscompat.checks.dmabuf + +import android.app.Activity +import android.os.Bundle +import android.os.SystemClock +import app.grapheneos.goscompat.checks.GosCompatContract + +open class DmaBufReleaseActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val token = intent.getStringExtra(GosCompatContract.DmaBufRelease.Extra.TOKEN) + val mode = intent.getStringExtra(GosCompatContract.DmaBufRelease.Extra.MODE) + ?: GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT + val heapName = intent.getStringExtra(GosCompatContract.DmaBufRelease.Extra.HEAP_NAME) + val width = intent.getIntExtra( + GosCompatContract.DmaBufRelease.Extra.WIDTH, + GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_WIDTH, + ) + val height = intent.getIntExtra( + GosCompatContract.DmaBufRelease.Extra.HEIGHT, + GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_HEIGHT, + ) + val bufferCount = intent.getIntExtra( + GosCompatContract.DmaBufRelease.Extra.BUFFER_COUNT, + GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_COUNT, + ) + val iterations = intent.getIntExtra( + GosCompatContract.DmaBufRelease.Extra.ITERATIONS, + GosCompatContract.DmaBufRelease.DEFAULT_ITERATIONS, + ) + val releaseAfterReady = intent.getBooleanExtra( + GosCompatContract.DmaBufRelease.Extra.RELEASE_AFTER_READY, + false, + ) + + DmaBufReleaseProgressStore.clear(this) + + Thread({ + var result = DmaBufReleaseRunner.start( + mode = mode, + width = width, + height = height, + bufferCount = bufferCount, + iterations = iterations, + heapName = heapName, + ) + if (result.ready && releaseAfterReady) { + result = try { + DmaBufReleaseRunner.release() + result.copy(released = true) + } catch (e: RuntimeException) { + result.copy( + ready = false, + error = "Manual DMA-BUF release failed: " + + "${e.javaClass.simpleName}: ${e.message}", + ) + } + } + DmaBufReleaseProgressStore.save(applicationContext, token, result) + runOnUiThread { finish() } + if (!result.ready || releaseAfterReady) { + return@Thread + } + + // Keep this process and its native buffer references live until shell stops it. + while (true) { + SystemClock.sleep(1_000) + } + }, "dmabuf-release-workload").start() + } +} + +class DmaBufReleaseManualActivity : DmaBufReleaseActivity() diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleasePanel.kt b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleasePanel.kt new file mode 100644 index 0000000000000..881ef35007853 --- /dev/null +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleasePanel.kt @@ -0,0 +1,365 @@ +package app.grapheneos.goscompat.checks.dmabuf + +import android.app.Activity +import android.content.Intent +import android.os.Looper +import android.os.SystemClock +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import app.grapheneos.goscompat.checks.GosCompatContract +import app.grapheneos.goscompat.checks.ResultSection +import java.util.UUID +import java.util.concurrent.CountDownLatch + +@Composable +internal fun DmaBufReleasePanel(activity: Activity) { + val runs = remember { + DMABUF_UI_OPTIONS.map { option -> + DmaBufUiRun( + option = option, + result = mutableStateOf(null), + running = mutableStateOf(false), + ) + } + } + val allRunning = remember { mutableStateOf(false) } + + DmaBufReleaseCard( + runs = runs, + allRunning = allRunning.value, + onRun = { run -> startDmaBufRelease(activity, run) }, + onRunAll = { + startAllDmaBufRelease( + activity = activity, + runs = runs, + allRunning = allRunning, + ) + }, + ) +} + +private data class DmaBufUiOption( + val title: String, + val detailsTitle: String, + val mode: String, + val heapName: String?, + val width: Int, + val height: Int, + val bufferCount: Int, +) + +private data class DmaBufUiRun( + val option: DmaBufUiOption, + val result: MutableState, + val running: MutableState, +) + +private val DMABUF_UI_OPTIONS = listOf( + DmaBufUiOption( + title = "Direct vframe-secure multi chunk", + detailsTitle = "Direct vframe-secure multi chunk details", + mode = GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + heapName = GosCompatContract.DmaBufRelease.Heap.VFRAME_SECURE, + width = GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_WIDTH, + height = GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_HEIGHT, + bufferCount = GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_COUNT, + ), + DmaBufUiOption( + title = "Direct vstream-secure multi chunk", + detailsTitle = "Direct vstream-secure multi chunk details", + mode = GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + heapName = GosCompatContract.DmaBufRelease.Heap.VSTREAM_SECURE, + width = GosCompatContract.DmaBufRelease.VSTREAM_SECURE_DIRECT_WIDTH, + height = GosCompatContract.DmaBufRelease.VSTREAM_SECURE_DIRECT_HEIGHT, + bufferCount = GosCompatContract.DmaBufRelease.VSTREAM_SECURE_DIRECT_COUNT, + ), + DmaBufUiOption( + title = "Direct vframe-secure one chunk", + detailsTitle = "Direct vframe-secure one chunk details", + mode = GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + heapName = GosCompatContract.DmaBufRelease.Heap.VFRAME_SECURE, + width = GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_WIDTH, + height = GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_HEIGHT, + bufferCount = GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_COUNT, + ), + DmaBufUiOption( + title = "Direct vstream-secure one chunk", + detailsTitle = "Direct vstream-secure one chunk details", + mode = GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + heapName = GosCompatContract.DmaBufRelease.Heap.VSTREAM_SECURE, + width = GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_WIDTH, + height = GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_HEIGHT, + bufferCount = GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_COUNT, + ), + DmaBufUiOption( + title = "Protected EGL resources", + detailsTitle = "Protected EGL details", + mode = GosCompatContract.DmaBufRelease.Mode.PROTECTED_EGL, + heapName = null, + width = GosCompatContract.DmaBufRelease.PROTECTED_EGL_WIDTH, + height = GosCompatContract.DmaBufRelease.PROTECTED_EGL_HEIGHT, + bufferCount = GosCompatContract.DmaBufRelease.PROTECTED_EGL_RESOURCE_COUNT, + ), +) + +private fun startDmaBufRelease( + activity: Activity, + run: DmaBufUiRun, +) { + val result = run.result + val running = run.running + if (running.value) { + return + } + running.value = true + result.value = null + + Thread { + val checkResult = runDmaBufReleaseWorkload(activity, run.option) + activity.runOnUiThread { + result.value = checkResult + running.value = false + } + }.start() +} + +private fun startAllDmaBufRelease( + activity: Activity, + runs: List, + allRunning: MutableState, +) { + if (allRunning.value || runs.any { it.running.value }) { + return + } + allRunning.value = true + + Thread { + try { + runs.forEach { run -> + activity.runOnUiThread { + run.result.value = null + run.running.value = true + } + val checkResult = runDmaBufReleaseWorkload( + activity = activity, + option = run.option, + ) + activity.runOnUiThread { + run.result.value = checkResult + run.running.value = false + } + } + } finally { + activity.runOnUiThread { + runs.forEach { it.running.value = false } + allRunning.value = false + } + } + }.start() +} + +private fun runDmaBufReleaseWorkload( + activity: Activity, + option: DmaBufUiOption, +): DmaBufReleaseResult { + val width = option.width + val height = option.height + val bufferCount = option.bufferCount + val iterations = GosCompatContract.DmaBufRelease.DEFAULT_ITERATIONS + val token = UUID.randomUUID().toString() + + return try { + activity.runOnUiThreadBlocking { + DmaBufReleaseProgressStore.clear(activity) + val intent = Intent(activity, DmaBufReleaseManualActivity::class.java) + .putExtra(GosCompatContract.DmaBufRelease.Extra.TOKEN, token) + .putExtra(GosCompatContract.DmaBufRelease.Extra.MODE, option.mode) + .putExtra(GosCompatContract.DmaBufRelease.Extra.WIDTH, width) + .putExtra(GosCompatContract.DmaBufRelease.Extra.HEIGHT, height) + .putExtra(GosCompatContract.DmaBufRelease.Extra.BUFFER_COUNT, bufferCount) + .putExtra(GosCompatContract.DmaBufRelease.Extra.ITERATIONS, iterations) + .putExtra( + GosCompatContract.DmaBufRelease.Extra.RELEASE_AFTER_READY, + true, + ) + if (option.heapName != null) { + intent.putExtra( + GosCompatContract.DmaBufRelease.Extra.HEAP_NAME, + option.heapName, + ) + } + activity.startActivity(intent) + } + + val deadline = SystemClock.elapsedRealtime() + DMABUF_UI_RESULT_TIMEOUT_MILLIS + var latest: DmaBufReleaseResult? = null + while (SystemClock.elapsedRealtime() < deadline) { + latest = DmaBufReleaseProgressStore.load(activity, token) + if (latest != null) { + break + } + SystemClock.sleep(DMABUF_UI_POLL_INTERVAL_MILLIS) + } + + latest ?: DmaBufReleaseResult.failed( + mode = option.mode, + width = width, + height = height, + requestedBuffers = bufferCount, + iterations = iterations, + error = "Timed out waiting for DMA-BUF workload readiness", + ) + } catch (e: Exception) { + DmaBufReleaseResult.failedFromThrowable( + mode = option.mode, + width = width, + height = height, + requestedBuffers = bufferCount, + iterations = iterations, + throwable = e, + errorPrefix = "Failed to start DMA-BUF workload", + ) + } +} + +private fun Activity.runOnUiThreadBlocking(action: () -> Unit) { + if (Looper.myLooper() == Looper.getMainLooper()) { + action() + return + } + + val latch = CountDownLatch(1) + var failure: Throwable? = null + runOnUiThread { + try { + action() + } catch (e: Throwable) { + failure = e + } finally { + latch.countDown() + } + } + latch.await() + failure?.let { throw it } +} + +@Composable +private fun DmaBufReleaseCard( + runs: List, + allRunning: Boolean, + onRun: (DmaBufUiRun) -> Unit, + onRunAll: () -> Unit, +) { + val anyRunning = allRunning || runs.any { it.running.value } + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text( + text = "DMA-BUF release", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Button(onClick = onRunAll, enabled = !anyRunning) { + Text("Run all") + } + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = if (allRunning) "Running all options" else "Ready", + style = MaterialTheme.typography.bodyLarge, + ) + } + runs.forEach { run -> + val result = run.result.value + val running = run.running.value + DmaBufReleaseControl( + title = run.option.title, + result = result, + running = running, + enabled = !allRunning && !running, + onRun = { onRun(run) }, + ) + ResultSection(run.option.detailsTitle, result?.details.orEmpty()) + } + } + } +} + +@Composable +private fun DmaBufReleaseControl( + title: String, + result: DmaBufReleaseResult?, + running: Boolean, + enabled: Boolean, + onRun: () -> Unit, +) { + val status = when { + running -> "Running" + result != null -> result.statusText + else -> "Not run" + } + val statusColor = when { + running -> MaterialTheme.colorScheme.tertiary + result?.ready == true -> MaterialTheme.colorScheme.primary + result != null -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = status, + modifier = Modifier + .background(statusColor.copy(alpha = 0.12f), RoundedCornerShape(6.dp)) + .padding(horizontal = 12.dp, vertical = 8.dp), + color = statusColor, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.width(12.dp)) + Button(onClick = onRun, enabled = enabled) { + Text("Run") + } + } + + Text( + text = result?.summary ?: "DMA-BUF workload has not been run.", + style = MaterialTheme.typography.bodyLarge, + ) + } +} + +private const val DMABUF_UI_RESULT_TIMEOUT_MILLIS = 15_000L +private const val DMABUF_UI_POLL_INTERVAL_MILLIS = 200L diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseProgressStore.kt b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseProgressStore.kt new file mode 100644 index 0000000000000..f33ceadaecb7d --- /dev/null +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseProgressStore.kt @@ -0,0 +1,60 @@ +package app.grapheneos.goscompat.checks.dmabuf + +import android.content.Context +import app.grapheneos.goscompat.checks.GosCompatContract +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.util.Properties + +object DmaBufReleaseProgressStore { + fun save(context: Context, token: String?, result: DmaBufReleaseResult) { + if (token.isNullOrEmpty()) { + return + } + + try { + FileOutputStream(resultFile(context)).use { output -> + result.toProperties(token).store(output, null) + } + } catch (_: IOException) { + } + } + + fun load(context: Context, token: String): DmaBufReleaseResult? { + val properties = loadProperties(context) ?: return null + if (properties.getProperty(GosCompatContract.DmaBufRelease.Extra.TOKEN) != token) { + return null + } + if (!properties.getProperty( + GosCompatContract.DmaBufRelease.Key.RESULT_AVAILABLE, + ).toBoolean() + ) { + return null + } + return DmaBufReleaseResult.fromProperties(properties) + } + + fun clear(context: Context) { + resultFile(context).delete() + } + + private fun loadProperties(context: Context): Properties? { + val file = resultFile(context) + if (!file.exists()) { + return null + } + + return try { + Properties().also { properties -> + FileInputStream(file).use { input -> properties.load(input) } + } + } catch (_: IOException) { + null + } + } + + private fun resultFile(context: Context): File = + File(context.filesDir, GosCompatContract.DmaBufRelease.RESULT_FILE) +} diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseResult.kt b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseResult.kt new file mode 100644 index 0000000000000..51a47e961b47c --- /dev/null +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseResult.kt @@ -0,0 +1,266 @@ +package app.grapheneos.goscompat.checks.dmabuf + +import app.grapheneos.goscompat.checks.GosCompatContract +import java.util.Properties + +data class DmaBufReleaseResult( + val mode: String, + val ready: Boolean, + val unsupported: Boolean, + val protectedContent: Boolean, + val width: Int, + val height: Int, + val requestedBuffers: Int, + val allocatedBuffers: Int, + val iterations: Int, + val pid: Int, + val tid: Int, + val heapPath: String, + val heapName: String, + val allocator: String, + val allocation: String, + val error: String, + val released: Boolean = false, +) { + val statusText: String + get() = when { + ready && released -> "Released" + ready -> "Ready" + unsupported -> "Unsupported" + else -> "Failed" + } + + val summary: String + get() = when { + mode == GosCompatContract.DmaBufRelease.Mode.PROTECTED_EGL && ready && released -> + "Created and released protected EGL resources." + mode == GosCompatContract.DmaBufRelease.Mode.PROTECTED_EGL && ready -> + "Created protected EGL resources." + ready && released -> + "Allocated and released $allocatedBuffers direct secure chunk heap DMA-BUF " + + "file descriptor." + ready -> + "Allocated $allocatedBuffers direct secure chunk heap DMA-BUF file descriptor." + unsupported -> "Unsupported: $error" + error.isNotEmpty() -> "Failed: $error" + else -> "DMA-BUF workload failed." + } + + val details: List + get() = buildList { + add("mode=$mode") + add("pid=$pid, tid=$tid") + add("size=${width}x$height") + if (mode == GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT) { + add("fds=$allocatedBuffers/$requestedBuffers") + } else { + add("resources=$allocatedBuffers") + } + add("iterations=$iterations") + add("protectedContent=$protectedContent") + add("released=$released") + add("heap=$heapPath") + add("heapName=$heapName") + add("allocator=$allocator") + add("allocation=$allocation") + if (error.isNotEmpty()) { + add("error=$error") + } + } + + fun toProperties(token: String): Properties = Properties().apply { + setProperty(GosCompatContract.DmaBufRelease.Extra.TOKEN, token) + setProperty(GosCompatContract.DmaBufRelease.Key.RESULT_AVAILABLE, "true") + setProperty(GosCompatContract.DmaBufRelease.Key.READY, ready.toString()) + setProperty(GosCompatContract.DmaBufRelease.Key.UNSUPPORTED, unsupported.toString()) + setProperty(GosCompatContract.DmaBufRelease.Key.MODE, mode) + setProperty(GosCompatContract.DmaBufRelease.Key.WIDTH, width.toString()) + setProperty(GosCompatContract.DmaBufRelease.Key.HEIGHT, height.toString()) + setProperty( + GosCompatContract.DmaBufRelease.Key.REQUESTED_BUFFERS, + requestedBuffers.toString(), + ) + setProperty( + GosCompatContract.DmaBufRelease.Key.ALLOCATED_BUFFERS, + allocatedBuffers.toString(), + ) + setProperty(GosCompatContract.DmaBufRelease.Key.ITERATIONS, iterations.toString()) + setProperty(GosCompatContract.DmaBufRelease.Key.PID, pid.toString()) + setProperty(GosCompatContract.DmaBufRelease.Key.TID, tid.toString()) + setProperty( + GosCompatContract.DmaBufRelease.Key.PROTECTED_CONTENT, + protectedContent.toString(), + ) + setProperty(GosCompatContract.DmaBufRelease.Key.RELEASED, released.toString()) + setProperty(GosCompatContract.DmaBufRelease.Key.HEAP_PATH, heapPath) + setProperty(GosCompatContract.DmaBufRelease.Key.HEAP_NAME, heapName) + setProperty(GosCompatContract.DmaBufRelease.Key.ALLOCATOR, allocator) + setProperty(GosCompatContract.DmaBufRelease.Key.ALLOCATION, allocation) + setProperty(GosCompatContract.DmaBufRelease.Key.ERROR, error) + } + + companion object { + private const val FIELD_READY = 0 + private const val FIELD_UNSUPPORTED = 1 + private const val FIELD_PROTECTED_CONTENT = 2 + private const val FIELD_ALLOCATED_BUFFERS = 3 + private const val FIELD_PID = 4 + private const val FIELD_TID = 5 + private const val FIELD_HEAP_PATH = 6 + private const val FIELD_HEAP_NAME = 7 + private const val FIELD_ALLOCATOR = 8 + private const val FIELD_ALLOCATION = 9 + private const val FIELD_ERROR = 10 + private const val FIELD_COUNT = 11 + + fun fromNativeFields( + mode: String, + width: Int, + height: Int, + requestedBuffers: Int, + iterations: Int, + fields: Array?, + ): DmaBufReleaseResult { + if (fields == null || fields.size != FIELD_COUNT) { + return failed( + mode = mode, + width = width, + height = height, + requestedBuffers = requestedBuffers, + iterations = iterations, + error = "Native DMA-BUF workload returned an invalid result", + ) + } + + return DmaBufReleaseResult( + mode = mode, + ready = fields[FIELD_READY].toBoolean(), + unsupported = fields[FIELD_UNSUPPORTED].toBoolean(), + protectedContent = fields[FIELD_PROTECTED_CONTENT].toBoolean(), + width = width, + height = height, + requestedBuffers = requestedBuffers, + allocatedBuffers = fields[FIELD_ALLOCATED_BUFFERS].toIntOrZero(), + iterations = iterations, + pid = fields[FIELD_PID].toIntOrZero(), + tid = fields[FIELD_TID].toIntOrZero(), + heapPath = fields[FIELD_HEAP_PATH], + heapName = fields[FIELD_HEAP_NAME], + allocator = fields[FIELD_ALLOCATOR], + allocation = fields[FIELD_ALLOCATION], + error = fields[FIELD_ERROR], + ) + } + + fun fromProperties(properties: Properties): DmaBufReleaseResult = DmaBufReleaseResult( + mode = properties.getProperty(GosCompatContract.DmaBufRelease.Key.MODE, ""), + ready = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.READY, + "", + ).toBoolean(), + unsupported = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.UNSUPPORTED, + "", + ).toBoolean(), + protectedContent = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.PROTECTED_CONTENT, + "", + ).toBoolean(), + width = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.WIDTH, + ).toIntOrZero(), + height = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.HEIGHT, + ).toIntOrZero(), + requestedBuffers = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.REQUESTED_BUFFERS, + ).toIntOrZero(), + allocatedBuffers = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.ALLOCATED_BUFFERS, + ).toIntOrZero(), + iterations = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.ITERATIONS, + ).toIntOrZero(), + pid = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.PID, + ).toIntOrZero(), + tid = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.TID, + ).toIntOrZero(), + heapPath = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.HEAP_PATH, + "", + ), + heapName = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.HEAP_NAME, + "", + ), + allocator = properties.getProperty(GosCompatContract.DmaBufRelease.Key.ALLOCATOR, ""), + allocation = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.ALLOCATION, + "", + ), + error = properties.getProperty(GosCompatContract.DmaBufRelease.Key.ERROR, ""), + released = properties.getProperty( + GosCompatContract.DmaBufRelease.Key.RELEASED, + "", + ).toBoolean(), + ) + + fun failed( + mode: String, + width: Int, + height: Int, + requestedBuffers: Int, + iterations: Int, + error: String, + ): DmaBufReleaseResult = DmaBufReleaseResult( + mode = mode, + ready = false, + unsupported = false, + protectedContent = false, + width = width, + height = height, + requestedBuffers = requestedBuffers, + allocatedBuffers = 0, + iterations = iterations, + pid = 0, + tid = 0, + heapPath = "", + heapName = "", + allocator = "", + allocation = "", + error = error, + ) + + fun failedFromThrowable( + mode: String, + width: Int, + height: Int, + requestedBuffers: Int, + iterations: Int, + throwable: Throwable, + errorPrefix: String = "", + ): DmaBufReleaseResult = failed( + mode = mode, + width = width, + height = height, + requestedBuffers = requestedBuffers, + iterations = iterations, + error = throwableError(errorPrefix, throwable), + ) + + private fun throwableError(errorPrefix: String, throwable: Throwable): String = + buildString { + if (errorPrefix.isNotEmpty()) { + append(errorPrefix) + append(": ") + } + append(throwable.javaClass.simpleName) + append(": ") + append(throwable.message) + } + + private fun String?.toIntOrZero(): Int = this?.toIntOrNull() ?: 0 + } +} diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseRunner.kt b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseRunner.kt new file mode 100644 index 0000000000000..7ad353a93b44a --- /dev/null +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/dmabuf/DmaBufReleaseRunner.kt @@ -0,0 +1,87 @@ +package app.grapheneos.goscompat.checks.dmabuf + +import app.grapheneos.goscompat.checks.GosCompatContract + +object DmaBufReleaseRunner { + private var nativeLoaded = false + + @Synchronized + fun start( + mode: String, + width: Int, + height: Int, + bufferCount: Int, + iterations: Int, + heapName: String?, + ): DmaBufReleaseResult { + release() + + return try { + ensureNativeLoaded() + val fields = when (mode) { + GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT -> + nativeStartWorkload( + heapName, + width, + height, + bufferCount, + ) + GosCompatContract.DmaBufRelease.Mode.PROTECTED_EGL -> + nativeStartProtectedEglWorkload( + width, + height, + iterations, + ) + else -> return DmaBufReleaseResult.failed( + mode = mode, + width = width, + height = height, + requestedBuffers = bufferCount, + iterations = iterations, + error = "Unsupported DMA-BUF workload mode: $mode", + ) + } + DmaBufReleaseResult.fromNativeFields( + mode = mode, + width = width, + height = height, + requestedBuffers = bufferCount, + iterations = iterations, + fields = fields, + ) + } catch (e: RuntimeException) { + DmaBufReleaseResult.failedFromThrowable(mode, width, height, bufferCount, iterations, e) + } catch (e: UnsatisfiedLinkError) { + DmaBufReleaseResult.failedFromThrowable(mode, width, height, bufferCount, iterations, e) + } + } + + @Synchronized + fun release() { + if (nativeLoaded) { + nativeReleaseWorkload() + } + } + + private fun ensureNativeLoaded() { + if (!nativeLoaded) { + System.loadLibrary("goscompat_dmabuf_release_jni") + nativeLoaded = true + } + } + + private external fun nativeStartWorkload( + heapName: String?, + width: Int, + height: Int, + bufferCount: Int, + ): Array + + private external fun nativeStartProtectedEglWorkload( + width: Int, + height: Int, + iterations: Int, + ): Array + + private external fun nativeReleaseWorkload() +} diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/webviewua/WebViewUaStartupActivity.java b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/webviewua/WebViewUaStartupActivity.java new file mode 100644 index 0000000000000..9af1037d0b715 --- /dev/null +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/webviewua/WebViewUaStartupActivity.java @@ -0,0 +1,121 @@ +package app.grapheneos.goscompat.checks.webviewua; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Looper; +import android.os.Process; +import android.os.Handler; +import android.os.SystemClock; +import android.webkit.WebSettings; + +import app.grapheneos.goscompat.checks.GosCompatContract; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +public final class WebViewUaStartupActivity extends Activity { + private static final Object STARTUP_LOCK = new Object(); + private static final long UI_RUNNABLE_START_TIMEOUT_MILLIS = 5_000; + private static final long EXIT_PROCESS_DELAY_MILLIS = 250; + + private final AtomicBoolean mResultSaved = new AtomicBoolean(); + private boolean mExitProcessAfterResult; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String token = getIntent().getStringExtra( + GosCompatContract.WebViewUaStartup.Extra.TOKEN); + mExitProcessAfterResult = getIntent().getBooleanExtra( + GosCompatContract.WebViewUaStartup.Extra.EXIT_PROCESS, false); + WebViewUaStartupResultStore.clear(this); + + Thread worker = new Thread(() -> runLockInversionScenario(token), + "webview-ua-startup-worker"); + worker.start(); + } + + private void runLockInversionScenario(String token) { + final int workerTid = Process.myTid(); + final AtomicInteger uiTid = new AtomicInteger(); + final AtomicLong durationMillis = new AtomicLong(-1L); + final AtomicInteger userAgentLength = new AtomicInteger(); + final AtomicBoolean workerCompleted = new AtomicBoolean(); + final AtomicReference error = new AtomicReference<>(); + final CountDownLatch uiRunnableStarted = new CountDownLatch(1); + + try { + synchronized (STARTUP_LOCK) { + runOnUiThread(() -> { + int currentUiTid = Process.myTid(); + uiTid.set(currentUiTid); + uiRunnableStarted.countDown(); + + synchronized (STARTUP_LOCK) { + boolean completed = workerCompleted.get(); + String failure = completed ? null : error.get(); + if (failure == null && !completed) { + failure = "Worker released lock before getDefaultUserAgent completed"; + } + saveResult(token, completed, workerTid != currentUiTid, true, + Looper.myLooper() == Looper.getMainLooper(), workerTid, + currentUiTid, durationMillis.get(), userAgentLength.get(), + failure); + finish(); + } + }); + + if (!uiRunnableStarted.await(UI_RUNNABLE_START_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS)) { + saveResult(token, false, false, false, false, workerTid, 0, + durationMillis.get(), userAgentLength.get(), + "UI runnable did not start while worker held lock"); + runOnUiThread(this::finish); + return; + } + + long startMillis = SystemClock.elapsedRealtime(); + String userAgent = WebSettings.getDefaultUserAgent(getApplicationContext()); + durationMillis.set(SystemClock.elapsedRealtime() - startMillis); + userAgentLength.set(userAgent != null ? userAgent.length() : 0); + if (userAgent == null || userAgent.isEmpty()) { + error.set("Default user agent was empty"); + } else { + workerCompleted.set(true); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + saveResult(token, false, true, uiTid.get() > 0, false, workerTid, uiTid.get(), + durationMillis.get(), userAgentLength.get(), + "Interrupted while waiting for UI runnable"); + runOnUiThread(this::finish); + } catch (RuntimeException e) { + saveResult(token, false, true, uiTid.get() > 0, false, workerTid, uiTid.get(), + durationMillis.get(), userAgentLength.get(), + e.getClass().getSimpleName() + ": " + e.getMessage()); + runOnUiThread(this::finish); + } + } + + private void saveResult(String token, boolean completed, boolean workerThread, + boolean uiThread, boolean mainLooper, int workerTid, int uiTid, long durationMillis, + int userAgentLength, String error) { + if (!mResultSaved.compareAndSet(false, true)) { + return; + } + WebViewUaStartupResultStore.save(getApplicationContext(), token, completed, workerThread, + uiThread, mainLooper, workerTid, uiTid, durationMillis, userAgentLength, error); + if (mExitProcessAfterResult) { + new Handler(Looper.getMainLooper()).postDelayed(() -> { + finish(); + Process.killProcess(Process.myPid()); + }, EXIT_PROCESS_DELAY_MILLIS); + } + } +} diff --git a/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/webviewua/WebViewUaStartupResultStore.java b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/webviewua/WebViewUaStartupResultStore.java new file mode 100644 index 0000000000000..d29c11f16db0d --- /dev/null +++ b/tests/GosCompatTests/GosCompatCheckApp/src/app/grapheneos/goscompat/checks/webviewua/WebViewUaStartupResultStore.java @@ -0,0 +1,60 @@ +package app.grapheneos.goscompat.checks.webviewua; + +import android.content.Context; + +import app.grapheneos.goscompat.checks.GosCompatContract; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +final class WebViewUaStartupResultStore { + private WebViewUaStartupResultStore() { + } + + static void save(Context context, String token, boolean completed, boolean workerThread, + boolean uiThread, boolean mainLooper, int workerTid, int uiTid, long durationMillis, + int userAgentLength, String error) { + if (token == null || token.isEmpty()) { + return; + } + + Properties properties = new Properties(); + properties.setProperty(GosCompatContract.WebViewUaStartup.Extra.TOKEN, token); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.RESULT_AVAILABLE, + Boolean.toString(true)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.COMPLETED, + Boolean.toString(completed)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.WORKER_THREAD, + Boolean.toString(workerThread)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.UI_THREAD, + Boolean.toString(uiThread)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.MAIN_LOOPER, + Boolean.toString(mainLooper)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.WORKER_TID, + Integer.toString(workerTid)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.UI_TID, + Integer.toString(uiTid)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.DURATION_MS, + Long.toString(durationMillis)); + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.USER_AGENT_LENGTH, + Integer.toString(userAgentLength)); + if (error != null && !error.isEmpty()) { + properties.setProperty(GosCompatContract.WebViewUaStartup.Key.ERROR, error); + } + + try (FileOutputStream output = new FileOutputStream(resultFile(context))) { + properties.store(output, null); + } catch (IOException ignored) { + } + } + + static void clear(Context context) { + resultFile(context).delete(); + } + + private static File resultFile(Context context) { + return new File(context.getFilesDir(), GosCompatContract.WebViewUaStartup.RESULT_FILE); + } +} diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/Android.bp b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/Android.bp new file mode 100644 index 0000000000000..fc65da028084c --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/Android.bp @@ -0,0 +1,22 @@ +android_test { + name: "GosCompatDmaBufReleaseTests", + srcs: [ + "src/**/*.java", + ], + manifest: "AndroidManifest.xml", + test_config: "AndroidTest.xml", + static_libs: [ + "GosCompatCheckCommon", + "androidx.test.ext.junit", + "androidx.test.runner", + "androidx.test.uiautomator_uiautomator", + "truth", + ], + libs: ["android.test.runner.stubs"], + data: [ + ":GosCompatCheckApp", + ], + platform_apis: true, + certificate: "platform", + test_suites: ["device-tests"], +} diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/AndroidManifest.xml b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/AndroidManifest.xml new file mode 100644 index 0000000000000..5d64b1c3c1816 --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/AndroidTest.xml b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/AndroidTest.xml new file mode 100644 index 0000000000000..775dbe158fab6 --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/AndroidTest.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/README.md b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/README.md new file mode 100644 index 0000000000000..13dcaf24e7edb --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/README.md @@ -0,0 +1,95 @@ +# GosCompatDmaBufReleaseTests + +This module is a targeted regression test for a Pixel kernel memory error (KASAN incompatability +from rebuilding pointers without the original tags) in the Samsung DMA-BUF secure chunk heap release +path. + +The tests will panic on a KASAN kernel without the patch for the memory error. Note that these tests +are designed to be non-priv apps. + +## Access rules + +The helper app intentionally opens the heap devices directly. This is allowed by the device policy +and file modes: + +- `vendor/etc/ueventd.rc` sets both heap devices to mode `0444`. +- Vendor SEPolicy labels both devices as `dmabuf_system_secure_heap_device`. +- `system/sepolicy/private/app.te` allows non-isolated app domains to read and open + `dmabuf_system_secure_heap_device` character devices. + +The native helper only accepts the allowlisted heap names above. It does not use the intent extra as +an arbitrary file path. + +## Relationship to existing AOSP tests + +The native helper follows the same basic allocation and release pattern as the existing DMA-BUF heap +unit tests: + +- `system/memory/libdmabufheap/tests/dmabuf_heap_test.cpp`: `DmaBufHeapTest.Allocate` allocates + DMA-BUF fds and closes them to release the buffers. +- `system/memory/libdmabufheap/tests/dmabuf_heap_test.cpp`: `DmaBufHeapTest.RepeatedAllocate` + repeats the same allocate and close path. +- `system/memory/libdmabufheap/tests/dmabuf_heap_test.c`: `libdmabufheaptest()` exercises the C + wrapper around the same libdmabufheap allocation flow. + +Note that these tests did not panic. In general, CTS tests in AOSP don't exercise these +Pixel-specific paths or they don't test process teardown. + +This helper intentionally does not link `libdmabufheap`, because it is built inside the SDK-built +helper APK and needs to target Pixel secure heap device names directly. Instead, it opens +`/dev/dma_heap/vframe-secure` or `/dev/dma_heap/vstream-secure` and issues `DMA_HEAP_IOCTL_ALLOC`, +then either closes the returned fd during manual release or leaves it open until process teardown +closes it. + +The protected EGL tests use the public EGL/GLES shape that is closest to the GPU driver usage: +`EGL_EXT_protected_content` on a protected context and pbuffer surface, plus +`GL_EXT_protected_textures` for protected texture storage. The tests cover explicit release, normal +app stop, and force stop because different devices can release the underlying DMA-BUF from different +threads. For example, shiba reproduced the panic from the Mali memory purge thread during explicit +protected EGL resource release. + +`DmaBufReleaseSequenceTests` repeats the same manual release sequence as the standalone UI's Run all +button in one helper process. This catches timing-sensitive cleanup paths better than the isolated +per-workload tests because shiba needed several Run all attempts before reproducing the panic. + +## Useful atest commands + +Run the whole module: + +```sh +atest GosCompatDmaBufReleaseTests +``` + +Run the protected EGL cases individually: + +```sh +ATEST_MODULE=GosCompatDmaBufReleaseTests +TEST_PKG=app.grapheneos.goscompat.dmabufrelease.tests + +atest "${ATEST_MODULE}:${TEST_PKG}.ProtectedEglReleaseTests#\ +protectedEglResourcesCanBeManuallyReleasedAfterReady" +atest "${ATEST_MODULE}:${TEST_PKG}.ProtectedEglReleaseTests#\ +protectedEglResourcesCanBeStoppedAfterReady" +atest "${ATEST_MODULE}:${TEST_PKG}.ProtectedEglReleaseTests#\ +protectedEglResourcesCanBeForceStoppedAfterReady" +``` + +Run the direct secure chunk heap parameterized cases: + +```sh +ATEST_MODULE=GosCompatDmaBufReleaseTests +TEST_PKG=app.grapheneos.goscompat.dmabufrelease.tests + +atest "${ATEST_MODULE}:${TEST_PKG}.SecureChunkHeapReleaseTests#\ +secureChunkHeapBufferCanBeReleasedAfterReady" +``` + +Run the repeated UI-shaped manual release sequence: + +```sh +ATEST_MODULE=GosCompatDmaBufReleaseTests +TEST_PKG=app.grapheneos.goscompat.dmabufrelease.tests + +atest "${ATEST_MODULE}:${TEST_PKG}.DmaBufReleaseSequenceTests#\ +manualReleaseRunAllSequenceCanBeRepeatedInOneProcess" +``` diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/DmaBufReleaseSequenceTests.java b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/DmaBufReleaseSequenceTests.java new file mode 100644 index 0000000000000..b8cddfed5ca35 --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/DmaBufReleaseSequenceTests.java @@ -0,0 +1,64 @@ +package app.grapheneos.goscompat.dmabufrelease.tests; + +import android.app.Instrumentation; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Certain devices like shiba can't reproduce the panic immediately due to different GPU memory + * management models. Multiple runs are required for the panic to trigger from shiba's + * mali-mem-purge task + */ +public final class DmaBufReleaseSequenceTests { + private static final int REPEAT_COUNT = 8; + private static final ReleaseTestSupport.Workload[] RUN_ALL_SEQUENCE = { + ReleaseTestSupport.Workload.directVframeSecureMultiChunk(), + ReleaseTestSupport.Workload.directVstreamSecureMultiChunk(), + ReleaseTestSupport.Workload.directVframeSecureOneChunk(), + ReleaseTestSupport.Workload.directVstreamSecureOneChunk(), + ReleaseTestSupport.Workload.protectedEgl(), + }; + + private ReleaseTestSupport mSupport; + + @Before + public void setUp() throws Exception { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + UiDevice device = UiDevice.getInstance(instrumentation); + mSupport = new ReleaseTestSupport(device); + mSupport.forceStopHelperApp(); + } + + @After + public void tearDown() throws Exception { + if (mSupport != null) { + mSupport.forceStopHelperApp(); + } + } + + @Test + public void manualReleaseRunAllSequenceCanBeRepeatedInOneProcess() throws Exception { + for (int pass = 0; pass < REPEAT_COUNT; pass++) { + for (ReleaseTestSupport.Workload workload : RUN_ALL_SEQUENCE) { + try { + ReleaseTestSupport.DmaBufResult result = mSupport.runReleaseAttempt( + workload, + ReleaseTestSupport.ReleaseAction.MANUAL_RELEASE, + ReleaseTestSupport.HelperProcessMode.REUSE_PROCESS); + mSupport.assertReleaseResult( + result, + workload, + ReleaseTestSupport.ReleaseAction.MANUAL_RELEASE); + } catch (AssertionError e) { + throw new AssertionError("Run all sequence pass " + (pass + 1) + "/" + + REPEAT_COUNT + ", workload=" + workload, e); + } + } + } + } +} diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/ProtectedEglReleaseTests.java b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/ProtectedEglReleaseTests.java new file mode 100644 index 0000000000000..fcc469932eca4 --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/ProtectedEglReleaseTests.java @@ -0,0 +1,51 @@ +package app.grapheneos.goscompat.dmabufrelease.tests; + +import android.app.Instrumentation; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public final class ProtectedEglReleaseTests { + private final ReleaseTestSupport.Workload mWorkload = + ReleaseTestSupport.Workload.protectedEgl(); + private ReleaseTestSupport mSupport; + + @Before + public void setUp() { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + UiDevice device = UiDevice.getInstance(instrumentation); + mSupport = new ReleaseTestSupport(device); + } + + @After + public void tearDown() throws Exception { + if (mSupport != null) { + mSupport.forceStopHelperApp(); + } + } + + @Test + public void protectedEglResourcesCanBeForceStoppedAfterReady() throws Exception { + run(ReleaseTestSupport.ReleaseAction.FORCE_STOP); + } + + @Test + public void protectedEglResourcesCanBeStoppedAfterReady() throws Exception { + run(ReleaseTestSupport.ReleaseAction.STOP_APP); + } + + @Test + public void protectedEglResourcesCanBeManuallyReleasedAfterReady() throws Exception { + run(ReleaseTestSupport.ReleaseAction.MANUAL_RELEASE); + } + + private void run(ReleaseTestSupport.ReleaseAction releaseAction) throws Exception { + ReleaseTestSupport.DmaBufResult result = + mSupport.runReleaseAttempt(mWorkload, releaseAction); + mSupport.assertProtectedEglResult(result, mWorkload, releaseAction); + } +} diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/ReleaseTestSupport.java b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/ReleaseTestSupport.java new file mode 100644 index 0000000000000..ea9aa4fc8fefc --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/ReleaseTestSupport.java @@ -0,0 +1,336 @@ +package app.grapheneos.goscompat.dmabufrelease.tests; + +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.test.uiautomator.UiDevice; + +import app.grapheneos.goscompat.checks.GosCompatContract; + +import java.io.StringReader; +import java.util.Properties; +import java.util.UUID; + +final class ReleaseTestSupport { + private static final long READY_TIMEOUT_MILLIS = 20_000; + private static final long POST_PROCESS_STOP_WAIT_MILLIS = 5_000; + private static final long POST_MANUAL_RELEASE_WAIT_MILLIS = 1_000; + private static final long POLL_INTERVAL_MILLIS = 200; + + private final UiDevice mDevice; + + ReleaseTestSupport(UiDevice device) { + mDevice = device; + } + + void forceStopHelperApp() throws Exception { + mDevice.executeShellCommand("am force-stop " + GosCompatContract.App.PACKAGE_NAME); + } + + DmaBufResult runReleaseAttempt(Workload workload, ReleaseAction releaseAction) + throws Exception { + return runReleaseAttempt(workload, releaseAction, HelperProcessMode.CLEAN_START); + } + + DmaBufResult runReleaseAttempt(Workload workload, ReleaseAction releaseAction, + HelperProcessMode helperProcessMode) throws Exception { + int iterations = GosCompatContract.DmaBufRelease.DEFAULT_ITERATIONS; + String token = UUID.randomUUID().toString(); + String attemptLabel = workload.label() + " " + releaseAction.label; + + if (helperProcessMode == HelperProcessMode.CLEAN_START) { + forceStopHelperApp(); + } + + StringBuilder command = new StringBuilder("am start -n ") + .append(GosCompatContract.DmaBufRelease.ACTIVITY) + .append(" --es ").append(GosCompatContract.DmaBufRelease.Extra.TOKEN) + .append(" ").append(token) + .append(" --es ").append(GosCompatContract.DmaBufRelease.Extra.MODE) + .append(" ").append(workload.mode()) + .append(" --ei ").append(GosCompatContract.DmaBufRelease.Extra.WIDTH) + .append(" ").append(workload.width()) + .append(" --ei ").append(GosCompatContract.DmaBufRelease.Extra.HEIGHT) + .append(" ").append(workload.height()) + .append(" --ei ").append(GosCompatContract.DmaBufRelease.Extra.BUFFER_COUNT) + .append(" ").append(workload.bufferCount()) + .append(" --ei ").append(GosCompatContract.DmaBufRelease.Extra.ITERATIONS) + .append(" ").append(iterations) + .append(" --ez ").append(GosCompatContract.DmaBufRelease.Extra.RELEASE_AFTER_READY) + .append(" ").append(releaseAction == ReleaseAction.MANUAL_RELEASE); + if (workload.heapName() != null) { + command.append(" --es ").append(GosCompatContract.DmaBufRelease.Extra.HEAP_NAME) + .append(" ").append(workload.heapName()); + } + + String output = mDevice.executeShellCommand(command.toString()); + assertWithMessage(attemptLabel + ": " + output).that(output).doesNotContain("Error"); + + DmaBufResult result = waitForResult(token, workload.mode()); + + if (releaseAction == ReleaseAction.MANUAL_RELEASE) { + java.lang.Thread.sleep(POST_MANUAL_RELEASE_WAIT_MILLIS); + } else { + mDevice.executeShellCommand(releaseAction.shellCommand); + waitForDmaBufProcessExit(result.pid(), attemptLabel); + java.lang.Thread.sleep(POST_PROCESS_STOP_WAIT_MILLIS); + } + mDevice.executeShellCommand("true"); + return result; + } + + void assertDirectSecureChunkHeapResult(DmaBufResult result, Workload workload, + ReleaseAction releaseAction) { + String message = workload.label() + " " + releaseAction.label + ": " + result; + assertReady(result, workload, releaseAction); + assertWithMessage(message).that(result.allocatedBuffers()) + .isEqualTo(result.requestedBuffers()); + assertWithMessage(message).that(result.heapPath()) + .contains("/dev/dma_heap/" + workload.heapName()); + assertWithMessage(message).that(result.heapName()).isEqualTo(workload.heapName()); + assertWithMessage(message).that(result.allocator()).isEqualTo("dma_heap"); + } + + void assertProtectedEglResult(DmaBufResult result, Workload workload, + ReleaseAction releaseAction) { + String message = workload.label() + " " + releaseAction.label + ": " + result; + assertReady(result, workload, releaseAction); + assertWithMessage(message).that(result.allocatedBuffers()) + .isEqualTo(GosCompatContract.DmaBufRelease.PROTECTED_EGL_RESOURCE_COUNT); + assertWithMessage(message).that(result.heapName()).isEqualTo("vendor-selected"); + assertWithMessage(message).that(result.allocator()) + .isEqualTo("EGL_EXT_protected_content"); + } + + void assertReleaseResult(DmaBufResult result, Workload workload, + ReleaseAction releaseAction) { + if (GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT + .equals(workload.mode())) { + assertDirectSecureChunkHeapResult(result, workload, releaseAction); + return; + } + if (GosCompatContract.DmaBufRelease.Mode.PROTECTED_EGL.equals(workload.mode())) { + assertProtectedEglResult(result, workload, releaseAction); + return; + } + throw new AssertionError("Unsupported DMA-BUF workload mode: " + workload.mode()); + } + + private void assertReady(DmaBufResult result, Workload workload, + ReleaseAction releaseAction) { + String message = workload.label() + " " + releaseAction.label + ": " + result; + assertWithMessage(message).that(result.mode()).isEqualTo(workload.mode()); + assertWithMessage(message).that(result.unsupported()).isFalse(); + assertWithMessage(message).that(result.ready()).isTrue(); + assertWithMessage(message).that(result.protectedContent()).isTrue(); + assertWithMessage(message).that(result.released()) + .isEqualTo(releaseAction == ReleaseAction.MANUAL_RELEASE); + assertWithMessage(message).that(result.width()).isAtLeast(1); + assertWithMessage(message).that(result.height()).isAtLeast(1); + assertWithMessage(message).that(result.requestedBuffers()).isAtLeast(1); + assertWithMessage(message).that(result.allocatedBuffers()).isAtLeast(1); + assertWithMessage(message).that(result.pid()).isGreaterThan(0); + assertWithMessage(message).that(result.tid()).isGreaterThan(0); + } + + private DmaBufResult waitForResult(String token, String mode) throws Exception { + long deadline = System.currentTimeMillis() + READY_TIMEOUT_MILLIS; + while (System.currentTimeMillis() < deadline) { + DmaBufResult result = getStoredResult(token); + if (result != null) { + return result; + } + java.lang.Thread.sleep(POLL_INTERVAL_MILLIS); + } + + throw new AssertionError("Timed out waiting for DMA-BUF release workload result in " + + mode + " mode"); + } + + private DmaBufResult getStoredResult(String token) throws Exception { + String output = mDevice.executeShellCommand("run-as " + GosCompatContract.App.PACKAGE_NAME + + " cat files/" + GosCompatContract.DmaBufRelease.RESULT_FILE + + " 2>/dev/null"); + if (output == null || output.trim().isEmpty()) { + return null; + } + + Properties properties = new Properties(); + try { + properties.load(new StringReader(output)); + if (!token.equals(properties.getProperty( + GosCompatContract.DmaBufRelease.Extra.TOKEN))) { + return null; + } + + return new DmaBufResult( + properties.getProperty(GosCompatContract.DmaBufRelease.Key.MODE, ""), + getBoolean(properties, GosCompatContract.DmaBufRelease.Key.READY), + getBoolean(properties, GosCompatContract.DmaBufRelease.Key.UNSUPPORTED), + getBoolean(properties, + GosCompatContract.DmaBufRelease.Key.PROTECTED_CONTENT), + getInt(properties, GosCompatContract.DmaBufRelease.Key.WIDTH), + getInt(properties, GosCompatContract.DmaBufRelease.Key.HEIGHT), + getInt(properties, + GosCompatContract.DmaBufRelease.Key.REQUESTED_BUFFERS), + getInt(properties, + GosCompatContract.DmaBufRelease.Key.ALLOCATED_BUFFERS), + getInt(properties, GosCompatContract.DmaBufRelease.Key.ITERATIONS), + getInt(properties, GosCompatContract.DmaBufRelease.Key.PID), + getInt(properties, GosCompatContract.DmaBufRelease.Key.TID), + getBoolean(properties, GosCompatContract.DmaBufRelease.Key.RELEASED), + properties.getProperty(GosCompatContract.DmaBufRelease.Key.HEAP_PATH, ""), + properties.getProperty(GosCompatContract.DmaBufRelease.Key.HEAP_NAME, ""), + properties.getProperty(GosCompatContract.DmaBufRelease.Key.ALLOCATOR, ""), + properties.getProperty(GosCompatContract.DmaBufRelease.Key.ALLOCATION, ""), + properties.getProperty(GosCompatContract.DmaBufRelease.Key.ERROR, "")); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + private void waitForDmaBufProcessExit(int pid, String attemptLabel) throws Exception { + long deadline = System.currentTimeMillis() + POST_PROCESS_STOP_WAIT_MILLIS; + while (System.currentTimeMillis() < deadline) { + String pidofOutput = mDevice.executeShellCommand("pidof " + + GosCompatContract.DmaBufRelease.PROCESS + " || true"); + if (!containsPid(pidofOutput, pid)) { + return; + } + java.lang.Thread.sleep(POLL_INTERVAL_MILLIS); + } + + throw new AssertionError(attemptLabel + ": DMA-BUF release process stayed alive"); + } + + private static boolean containsPid(String pidofOutput, int pid) { + if (pidofOutput == null) { + return false; + } + String expected = Integer.toString(pid); + for (String candidate : pidofOutput.trim().split("\\s+")) { + if (expected.equals(candidate)) { + return true; + } + } + return false; + } + + private static boolean getBoolean(Properties properties, String key) { + return Boolean.parseBoolean(properties.getProperty(key)); + } + + private static int getInt(Properties properties, String key) { + return Integer.parseInt(properties.getProperty(key, "0")); + } + + record Workload(String label, String mode, String heapName, int width, int height, + int bufferCount) { + static Workload directVframeSecureMultiChunk() { + return new Workload( + "vframe-secure multi chunk", + GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + GosCompatContract.DmaBufRelease.Heap.VFRAME_SECURE, + GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_WIDTH, + GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_HEIGHT, + GosCompatContract.DmaBufRelease.VFRAME_SECURE_DIRECT_COUNT); + } + + static Workload directVstreamSecureMultiChunk() { + return new Workload( + "vstream-secure multi chunk", + GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + GosCompatContract.DmaBufRelease.Heap.VSTREAM_SECURE, + GosCompatContract.DmaBufRelease.VSTREAM_SECURE_DIRECT_WIDTH, + GosCompatContract.DmaBufRelease.VSTREAM_SECURE_DIRECT_HEIGHT, + GosCompatContract.DmaBufRelease.VSTREAM_SECURE_DIRECT_COUNT); + } + + static Workload directVframeSecureOneChunk() { + return new Workload( + "vframe-secure one chunk", + GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + GosCompatContract.DmaBufRelease.Heap.VFRAME_SECURE, + GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_WIDTH, + GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_HEIGHT, + GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_COUNT); + } + + static Workload directVstreamSecureOneChunk() { + return new Workload( + "vstream-secure one chunk", + GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT, + GosCompatContract.DmaBufRelease.Heap.VSTREAM_SECURE, + GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_WIDTH, + GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_HEIGHT, + GosCompatContract.DmaBufRelease.SECURE_CHUNK_HEAP_ONE_CHUNK_COUNT); + } + + static Workload protectedEgl() { + return new Workload( + "protected EGL", + GosCompatContract.DmaBufRelease.Mode.PROTECTED_EGL, + null, + GosCompatContract.DmaBufRelease.PROTECTED_EGL_WIDTH, + GosCompatContract.DmaBufRelease.PROTECTED_EGL_HEIGHT, + GosCompatContract.DmaBufRelease.PROTECTED_EGL_RESOURCE_COUNT); + } + + @Override + public String toString() { + return label; + } + } + + record DmaBufResult(String mode, boolean ready, boolean unsupported, + boolean protectedContent, int width, int height, int requestedBuffers, + int allocatedBuffers, int iterations, int pid, int tid, boolean released, + String heapPath, String heapName, String allocator, String allocation, + String error) { + @Override + public String toString() { + String resourceText = GosCompatContract.DmaBufRelease.Mode.SECURE_CHUNK_HEAP_DIRECT + .equals(mode()) + ? ", fds=" + allocatedBuffers() + "/" + requestedBuffers() + : ", resources=" + allocatedBuffers(); + return "mode=" + mode() + + ", ready=" + ready() + + ", unsupported=" + unsupported() + + ", protectedContent=" + protectedContent() + + ", size=" + width() + "x" + height() + + resourceText + + ", iterations=" + iterations() + + ", pid=" + pid() + + ", tid=" + tid() + + ", released=" + released() + + ", heap=" + heapPath() + + ", heapName=" + heapName() + + ", allocator=" + allocator() + + ", allocation=" + allocation() + + ", error=" + error(); + } + } + + enum ReleaseAction { + FORCE_STOP("force stop", "am force-stop " + GosCompatContract.App.PACKAGE_NAME), + STOP_APP("stop app", "am stop-app " + GosCompatContract.App.PACKAGE_NAME), + MANUAL_RELEASE("manual release", ""); + + final String label; + final String shellCommand; + + ReleaseAction(String label, String shellCommand) { + this.label = label; + this.shellCommand = shellCommand; + } + + @Override + public String toString() { + return label; + } + } + + enum HelperProcessMode { + CLEAN_START, + REUSE_PROCESS, + } +} diff --git a/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/SecureChunkHeapReleaseTests.java b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/SecureChunkHeapReleaseTests.java new file mode 100644 index 0000000000000..1e5f47b5a7645 --- /dev/null +++ b/tests/GosCompatTests/GosCompatDmaBufReleaseTests/src/app/grapheneos/goscompat/dmabufrelease/tests/SecureChunkHeapReleaseTests.java @@ -0,0 +1,87 @@ +package app.grapheneos.goscompat.dmabufrelease.tests; + +import android.app.Instrumentation; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(Parameterized.class) +public final class SecureChunkHeapReleaseTests { + private final ReleaseTestSupport.Workload mWorkload; + private final ReleaseTestSupport.ReleaseAction mReleaseAction; + private ReleaseTestSupport mSupport; + + @Parameterized.Parameters(name = "{0}, {1}") + public static List parameters() { + return Arrays.asList(new Object[][] { + { + ReleaseTestSupport.Workload.directVframeSecureMultiChunk(), + ReleaseTestSupport.ReleaseAction.FORCE_STOP, + }, + { + ReleaseTestSupport.Workload.directVframeSecureMultiChunk(), + ReleaseTestSupport.ReleaseAction.MANUAL_RELEASE, + }, + { + ReleaseTestSupport.Workload.directVstreamSecureMultiChunk(), + ReleaseTestSupport.ReleaseAction.FORCE_STOP, + }, + { + ReleaseTestSupport.Workload.directVstreamSecureMultiChunk(), + ReleaseTestSupport.ReleaseAction.MANUAL_RELEASE, + }, + { + ReleaseTestSupport.Workload.directVframeSecureOneChunk(), + ReleaseTestSupport.ReleaseAction.FORCE_STOP, + }, + { + ReleaseTestSupport.Workload.directVframeSecureOneChunk(), + ReleaseTestSupport.ReleaseAction.MANUAL_RELEASE, + }, + { + ReleaseTestSupport.Workload.directVstreamSecureOneChunk(), + ReleaseTestSupport.ReleaseAction.FORCE_STOP, + }, + { + ReleaseTestSupport.Workload.directVstreamSecureOneChunk(), + ReleaseTestSupport.ReleaseAction.MANUAL_RELEASE, + }, + }); + } + + public SecureChunkHeapReleaseTests(ReleaseTestSupport.Workload workload, + ReleaseTestSupport.ReleaseAction releaseAction) { + mWorkload = workload; + mReleaseAction = releaseAction; + } + + @Before + public void setUp() { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + UiDevice device = UiDevice.getInstance(instrumentation); + mSupport = new ReleaseTestSupport(device); + } + + @After + public void tearDown() throws Exception { + if (mSupport != null) { + mSupport.forceStopHelperApp(); + } + } + + @Test + public void secureChunkHeapBufferCanBeReleasedAfterReady() throws Exception { + ReleaseTestSupport.DmaBufResult result = + mSupport.runReleaseAttempt(mWorkload, mReleaseAction); + mSupport.assertDirectSecureChunkHeapResult(result, mWorkload, mReleaseAction); + } +} diff --git a/tests/GosCompatTests/GosCompatMapsScanTests/README.md b/tests/GosCompatTests/GosCompatMapsScanTests/README.md new file mode 100644 index 0000000000000..7832c8e68722b --- /dev/null +++ b/tests/GosCompatTests/GosCompatMapsScanTests/README.md @@ -0,0 +1,2 @@ +`GosCompatMapsScanTests` covers compatibility with apps using native libraries that scan +`/proc/self/maps` and read selected mapped memory ranges. diff --git a/tests/GosCompatTests/GosCompatMapsScanTests/src/app/grapheneos/goscompat/tests/MapsScanTests.java b/tests/GosCompatTests/GosCompatMapsScanTests/src/app/grapheneos/goscompat/tests/MapsScanTests.java index c16b61c0f7db5..8fff66897e73c 100644 --- a/tests/GosCompatTests/GosCompatMapsScanTests/src/app/grapheneos/goscompat/tests/MapsScanTests.java +++ b/tests/GosCompatTests/GosCompatMapsScanTests/src/app/grapheneos/goscompat/tests/MapsScanTests.java @@ -61,17 +61,17 @@ public void setUp() { @After public void tearDown() throws Exception { - mDevice.executeShellCommand("am force-stop " + GosCompatContract.APP_PACKAGE); + mDevice.executeShellCommand("am force-stop " + GosCompatContract.App.PACKAGE_NAME); } @Test public void directNativeMapsScanDoesNotCrashFromJavaWorkerThread() throws Exception { - assertMapsScanModeDoesNotCrash(GosCompatContract.MODE_DIRECT); + assertMapsScanModeDoesNotCrash(GosCompatContract.MapsScan.Mode.DIRECT); } @Test public void reflectiveNativeMapsScanDoesNotCrashFromJavaWorkerThread() throws Exception { - assertMapsScanModeDoesNotCrash(GosCompatContract.MODE_REFLECTIVE); + assertMapsScanModeDoesNotCrash(GosCompatContract.MapsScan.Mode.REFLECTIVE); } private void assertMapsScanModeDoesNotCrash(String mode) throws Exception { @@ -85,21 +85,21 @@ private void assertMapsScanAttemptDoesNotCrash(String mode, int attempt) throws // The translucent trigger activity starts the scan asynchronously. Force-stop first so each // attempt gets a newly created helper process with fresh bionic thread mappings. - mDevice.executeShellCommand("am force-stop " + GosCompatContract.APP_PACKAGE); + mDevice.executeShellCommand("am force-stop " + GosCompatContract.App.PACKAGE_NAME); long startTimeMillis = System.currentTimeMillis(); // Use an activity launch because shell-started background services are rejected by // ActivityManager background-start restrictions. String output = mDevice.executeShellCommand("am start -n " - + GosCompatContract.MAPS_SCAN_CRASH_ACTIVITY - + " --es " + GosCompatContract.EXTRA_MAPS_SCAN_MODE + " " + mode - + " --es " + GosCompatContract.EXTRA_MAPS_SCAN_TOKEN + " " + token); + + GosCompatContract.MapsScan.CRASH_ACTIVITY + + " --es " + GosCompatContract.MapsScan.Extra.MODE + " " + mode + + " --es " + GosCompatContract.MapsScan.Extra.TOKEN + " " + token); assertWithMessage(output).that(output).doesNotContain("Error"); ScanOutcome outcome = waitForOutcome(token, startTimeMillis, mode, attempt); if (outcome.result != null) { assertCompletedResult(outcome.result, mode, attempt); - mDevice.executeShellCommand("am force-stop " + GosCompatContract.APP_PACKAGE); + mDevice.executeShellCommand("am force-stop " + GosCompatContract.App.PACKAGE_NAME); return; } @@ -121,7 +121,7 @@ private ScanOutcome waitForOutcome(String token, long startTimeMillis, String mo Bundle result = getStoredResult(token); if (result != null - && result.getBoolean(GosCompatContract.KEY_MAPS_SCAN_RESULT_AVAILABLE)) { + && result.getBoolean(GosCompatContract.MapsScan.Key.RESULT_AVAILABLE)) { return ScanOutcome.forResult(result); } @@ -137,36 +137,38 @@ private Bundle getStoredResult(String token) { try { // Poll a file instead of the provider so the instrumentation process never depends // on a binder transaction into a helper process that may be crashing. - String output = mDevice.executeShellCommand("run-as " + GosCompatContract.APP_PACKAGE - + " cat files/" + GosCompatContract.MAPS_SCAN_RESULT_FILE + " 2>/dev/null"); + String output = mDevice.executeShellCommand( + "run-as " + GosCompatContract.App.PACKAGE_NAME + + " cat files/" + GosCompatContract.MapsScan.RESULT_FILE + + " 2>/dev/null"); if (output == null || output.trim().isEmpty()) { return null; } Properties properties = new Properties(); properties.load(new StringReader(output)); - if (!token.equals(properties.getProperty(GosCompatContract.EXTRA_MAPS_SCAN_TOKEN))) { + if (!token.equals(properties.getProperty(GosCompatContract.MapsScan.Extra.TOKEN))) { return null; } boolean completed = getBoolean(properties, - GosCompatContract.KEY_MAPS_SCAN_COMPLETED); + GosCompatContract.MapsScan.Key.COMPLETED); boolean workerThread = getBoolean(properties, - GosCompatContract.KEY_MAPS_SCAN_WORKER_THREAD); + GosCompatContract.MapsScan.Key.WORKER_THREAD); Bundle result = new Bundle(); - result.putBoolean(GosCompatContract.KEY_MAPS_SCAN_RESULT_AVAILABLE, true); - result.putBoolean(GosCompatContract.KEY_MAPS_SCAN_COMPLETED, completed); - result.putBoolean(GosCompatContract.KEY_MAPS_SCAN_WORKER_THREAD, workerThread); - result.putInt(GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES, - getInt(properties, GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES)); - result.putLong(GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES, - getLong(properties, GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES)); - result.putInt(GosCompatContract.KEY_MAPS_SCAN_CALLER_TID, - getInt(properties, GosCompatContract.KEY_MAPS_SCAN_CALLER_TID)); - result.putInt(GosCompatContract.KEY_MAPS_SCAN_WORKER_TID, - getInt(properties, GosCompatContract.KEY_MAPS_SCAN_WORKER_TID)); - result.putString(GosCompatContract.KEY_STATUS_TEXT, + result.putBoolean(GosCompatContract.MapsScan.Key.RESULT_AVAILABLE, true); + result.putBoolean(GosCompatContract.MapsScan.Key.COMPLETED, completed); + result.putBoolean(GosCompatContract.MapsScan.Key.WORKER_THREAD, workerThread); + result.putInt(GosCompatContract.MapsScan.Key.SELECTED_RANGES, + getInt(properties, GosCompatContract.MapsScan.Key.SELECTED_RANGES)); + result.putLong(GosCompatContract.MapsScan.Key.SCANNED_BYTES, + getLong(properties, GosCompatContract.MapsScan.Key.SCANNED_BYTES)); + result.putInt(GosCompatContract.MapsScan.Key.CALLER_TID, + getInt(properties, GosCompatContract.MapsScan.Key.CALLER_TID)); + result.putInt(GosCompatContract.MapsScan.Key.WORKER_TID, + getInt(properties, GosCompatContract.MapsScan.Key.WORKER_TID)); + result.putString(GosCompatContract.MapsScan.Key.STATUS_TEXT, completed && workerThread ? "Completed" : "Failed"); return result; } catch (Throwable ignored) { @@ -188,7 +190,7 @@ private static long getLong(Properties properties, String key) { private ApplicationExitInfo findNativeCrash(long startTimeMillis) throws Exception { List exits = ShellIdentityUtils.invokeMethodWithShellPermissions( - GosCompatContract.APP_PACKAGE, + GosCompatContract.App.PACKAGE_NAME, 0, 16, mActivityManager::getHistoricalProcessExitReasons, @@ -200,7 +202,7 @@ private ApplicationExitInfo findNativeCrash(long startTimeMillis) throws Excepti if (exit.getReason() != ApplicationExitInfo.REASON_CRASH_NATIVE) { continue; } - if (!GosCompatContract.MAPS_SCAN_PROCESS.equals(exit.getProcessName())) { + if (!GosCompatContract.MapsScan.PROCESS.equals(exit.getProcessName())) { continue; } return exit; @@ -232,22 +234,22 @@ private byte[] fetchTombstone(ApplicationExitInfo exitInfo) throws Exception { private void assertCompletedResult(Bundle result, String mode, int attempt) { String message = "mode=" + mode + ", attempt=" + attempt + "/" + SCAN_ATTEMPTS - + ", status=" + result.getString(GosCompatContract.KEY_STATUS_TEXT) + + ", status=" + result.getString(GosCompatContract.MapsScan.Key.STATUS_TEXT) + ", selectedRanges=" - + result.getInt(GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES) + + result.getInt(GosCompatContract.MapsScan.Key.SELECTED_RANGES) + ", scannedBytes=" - + result.getLong(GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES) - + ", callerTid=" + result.getInt(GosCompatContract.KEY_MAPS_SCAN_CALLER_TID) - + ", workerTid=" + result.getInt(GosCompatContract.KEY_MAPS_SCAN_WORKER_TID); + + result.getLong(GosCompatContract.MapsScan.Key.SCANNED_BYTES) + + ", callerTid=" + result.getInt(GosCompatContract.MapsScan.Key.CALLER_TID) + + ", workerTid=" + result.getInt(GosCompatContract.MapsScan.Key.WORKER_TID); assertWithMessage(message).that(result.getBoolean( - GosCompatContract.KEY_MAPS_SCAN_COMPLETED)).isTrue(); + GosCompatContract.MapsScan.Key.COMPLETED)).isTrue(); assertWithMessage(message).that(result.getBoolean( - GosCompatContract.KEY_MAPS_SCAN_WORKER_THREAD)).isTrue(); + GosCompatContract.MapsScan.Key.WORKER_THREAD)).isTrue(); assertWithMessage(message).that(result.getInt( - GosCompatContract.KEY_MAPS_SCAN_SELECTED_RANGES)).isAtLeast(1); + GosCompatContract.MapsScan.Key.SELECTED_RANGES)).isAtLeast(1); assertWithMessage(message).that(result.getLong( - GosCompatContract.KEY_MAPS_SCAN_SCANNED_BYTES)).isGreaterThan(0L); + GosCompatContract.MapsScan.Key.SCANNED_BYTES)).isGreaterThan(0L); } private void reportTombstoneArtifacts(String mode, int attempt, String formattedTombstone, diff --git a/tests/GosCompatTests/GosCompatWebViewTests/Android.bp b/tests/GosCompatTests/GosCompatWebViewTests/Android.bp new file mode 100644 index 0000000000000..9b6010edc622b --- /dev/null +++ b/tests/GosCompatTests/GosCompatWebViewTests/Android.bp @@ -0,0 +1,23 @@ +android_test { + name: "GosCompatWebViewTests", + srcs: [ + "src/**/*.java", + ], + manifest: "AndroidManifest.xml", + test_config: "AndroidTest.xml", + static_libs: [ + "GosCompatCheckCommon", + "androidx.test.ext.junit", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.uiautomator_uiautomator", + "truth", + ], + libs: ["android.test.runner.stubs"], + data: [ + ":GosCompatCheckApp", + ], + platform_apis: true, + certificate: "platform", + test_suites: ["device-tests"], +} diff --git a/tests/GosCompatTests/GosCompatWebViewTests/AndroidManifest.xml b/tests/GosCompatTests/GosCompatWebViewTests/AndroidManifest.xml new file mode 100644 index 0000000000000..ed775d899c135 --- /dev/null +++ b/tests/GosCompatTests/GosCompatWebViewTests/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/tests/GosCompatTests/GosCompatWebViewTests/AndroidTest.xml b/tests/GosCompatTests/GosCompatWebViewTests/AndroidTest.xml new file mode 100644 index 0000000000000..0776945b24fb5 --- /dev/null +++ b/tests/GosCompatTests/GosCompatWebViewTests/AndroidTest.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/tests/GosCompatTests/GosCompatWebViewTests/README.md b/tests/GosCompatTests/GosCompatWebViewTests/README.md new file mode 100644 index 0000000000000..fc327d0f6d60a --- /dev/null +++ b/tests/GosCompatTests/GosCompatWebViewTests/README.md @@ -0,0 +1,43 @@ +`GosCompatWebViewTests` covers WebView default user agent behavior. It relaunches +the helper app process for each attempt so `WebSettings.getDefaultUserAgent()` +is exercised from a fresh app start. + +One test covers WebView default user agent startup when an app worker thread +holds an app lock and the app UI thread is blocked trying to acquire the same +lock on the Android main looper. Another test verifies the default WebView user +agent is reduced without depending on the exact Vanadium major version. + +This is especially evident with apps not following recommended practices for Google Mobile Ads SDK. +For example, an app can call this on the main thread: + +```java +new AdLoader.Builder(context, adUnitId) + .forNativeAd(nativeAdLoadedListener) + .withNativeAdOptions(nativeAdOptions) + .withAdListener(adListener) + .build() // This causes the deadlock since this is on main thread + .loadAd(adRequest); +``` +Google recommends to only call AdLoader.Builder from a worker thread [^1]. However, not all apps +follow these recommendations. The nuance is that `loadAd` can only be called on the main thread, but +the creation of the AdLoader should be done on a worker thread. + +During app startup, a worker thread in the Mobile Ads SDK holds a singleton lock and calls +`WebSettings.getDefaultUserAgent()`. When the `WebViewFasterGetDefaultUserAgent` feature for WebView +is disabled, this blocks the worker thread until its task posted to the main thread to initialize +WebView finishes. + +Meanwhile, the main thread of the app enters Mobile Ads SDK by the function calls above. Inside of +`AdLoader.Builder#build`, if the Dynamite ads module is not available, the library has a local +fallback path which attempts to acquires the singleton lock that the worker thread is holding. The +result is that the main thread gets blocked on a lock held by the worker thread which is waiting for +main thread to process its task. + +Dynamite ads module can be unavailable if Play services + Play Store are not installed (or have some +other issue). This would block users from using any app with SDK used in this way. + +By enabling `WebViewFasterGetDefaultUserAgent`, the worker thread no longer waits on the +initialization task posted to main thread to finish and gets a user agent immediately. This works +around this deadlock. + +[^1]: https://developers.google.com/admob/android/native#build diff --git a/tests/GosCompatTests/GosCompatWebViewTests/src/app/grapheneos/goscompat/webview/tests/WebViewTests.java b/tests/GosCompatTests/GosCompatWebViewTests/src/app/grapheneos/goscompat/webview/tests/WebViewTests.java new file mode 100644 index 0000000000000..391512257717d --- /dev/null +++ b/tests/GosCompatTests/GosCompatWebViewTests/src/app/grapheneos/goscompat/webview/tests/WebViewTests.java @@ -0,0 +1,219 @@ +package app.grapheneos.goscompat.webview.tests; + +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; + +import android.app.Instrumentation; +import android.content.Context; +import android.os.Bundle; +import android.webkit.WebSettings; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import app.grapheneos.goscompat.checks.GosCompatContract; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.StringReader; +import java.util.Properties; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class WebViewTests { + private static final long OUTCOME_TIMEOUT_MILLIS = 15_000; + private static final long POLL_INTERVAL_MILLIS = 200; + private static final int START_ATTEMPTS = 5; + // Source: Chromium android_webview/browser/aw_content_browser_client.cc + // GetUserAgent() WebView hardcodes this platform when WebViewReduceUAAndroidVersionDeviceModel + // is enabled. + private static final String REDUCED_WEBVIEW_PLATFORM = "Linux; Android 10; K; wv"; + private static final Pattern CHROME_PRODUCT_PATTERN = + Pattern.compile("\\bChrome/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\b"); + + private UiDevice mDevice; + + @Before + public void setUp() { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + mDevice = UiDevice.getInstance(instrumentation); + } + + @After + public void tearDown() throws Exception { + mDevice.executeShellCommand("am force-stop " + GosCompatContract.App.PACKAGE_NAME); + } + + @Test + public void defaultUserAgentDoesNotDeadlockWhenUiThreadWaitsOnAppLock() throws Exception { + for (int attempt = 1; attempt <= START_ATTEMPTS; attempt++) { + assertFreshProcessStartupAttemptCompletes(attempt, START_ATTEMPTS); + } + } + + @Test + public void defaultUserAgentIsReduced() throws Exception { + Context context = InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getApplicationContext(); + + assertDefaultUserAgentIsReduced(WebSettings.getDefaultUserAgent(context)); + } + + private void assertFreshProcessStartupAttemptCompletes(int attempt, int totalAttempts) + throws Exception { + String token = UUID.randomUUID().toString(); + + // Force-stop before every attempt so WebView startup is exercised from a fresh app process. + mDevice.executeShellCommand("am force-stop " + GosCompatContract.App.PACKAGE_NAME); + + String output = mDevice.executeShellCommand("am start -n " + + GosCompatContract.WebViewUaStartup.ACTIVITY + + " --es " + GosCompatContract.WebViewUaStartup.Extra.TOKEN + " " + token); + assertWithMessage(output).that(output).doesNotContain("Error"); + + Bundle result = waitForResult(token, attempt, totalAttempts); + assertCompletedResult(result, attempt, totalAttempts); + + mDevice.executeShellCommand("am force-stop " + GosCompatContract.App.PACKAGE_NAME); + } + + private Bundle waitForResult(String token, int attempt, int totalAttempts) throws Exception { + long deadline = System.currentTimeMillis() + OUTCOME_TIMEOUT_MILLIS; + while (System.currentTimeMillis() < deadline) { + Bundle result = getStoredResult(token); + if (result != null + && result.getBoolean( + GosCompatContract.WebViewUaStartup.Key.RESULT_AVAILABLE)) { + return result; + } + + java.lang.Thread.sleep(POLL_INTERVAL_MILLIS); + } + + fail("Timed out waiting for WebView default user agent startup result on attempt " + + attempt + " of " + totalAttempts + ". A worker thread holds an app lock while " + + "WebSettings.getDefaultUserAgent() waits for UI-thread WebView startup, and the " + + "UI thread is blocked trying to acquire the same app lock."); + return null; + } + + private Bundle getStoredResult(String token) throws Exception { + String output = mDevice.executeShellCommand( + "run-as " + GosCompatContract.App.PACKAGE_NAME + + " cat files/" + GosCompatContract.WebViewUaStartup.RESULT_FILE + + " 2>/dev/null"); + if (output == null || output.trim().isEmpty()) { + return null; + } + + Properties properties = new Properties(); + try { + properties.load(new StringReader(output)); + } catch (IllegalArgumentException e) { + return null; + } + if (!token.equals(properties.getProperty( + GosCompatContract.WebViewUaStartup.Extra.TOKEN))) { + return null; + } + + Bundle result = new Bundle(); + result.putBoolean(GosCompatContract.WebViewUaStartup.Key.RESULT_AVAILABLE, + getBoolean(properties, + GosCompatContract.WebViewUaStartup.Key.RESULT_AVAILABLE)); + result.putBoolean(GosCompatContract.WebViewUaStartup.Key.COMPLETED, + getBoolean(properties, GosCompatContract.WebViewUaStartup.Key.COMPLETED)); + result.putBoolean(GosCompatContract.WebViewUaStartup.Key.WORKER_THREAD, + getBoolean(properties, + GosCompatContract.WebViewUaStartup.Key.WORKER_THREAD)); + result.putBoolean(GosCompatContract.WebViewUaStartup.Key.UI_THREAD, + getBoolean(properties, GosCompatContract.WebViewUaStartup.Key.UI_THREAD)); + result.putBoolean(GosCompatContract.WebViewUaStartup.Key.MAIN_LOOPER, + getBoolean(properties, + GosCompatContract.WebViewUaStartup.Key.MAIN_LOOPER)); + result.putInt(GosCompatContract.WebViewUaStartup.Key.WORKER_TID, + getInt(properties, GosCompatContract.WebViewUaStartup.Key.WORKER_TID)); + result.putInt(GosCompatContract.WebViewUaStartup.Key.UI_TID, + getInt(properties, GosCompatContract.WebViewUaStartup.Key.UI_TID)); + result.putLong(GosCompatContract.WebViewUaStartup.Key.DURATION_MS, + getLong(properties, GosCompatContract.WebViewUaStartup.Key.DURATION_MS)); + result.putInt(GosCompatContract.WebViewUaStartup.Key.USER_AGENT_LENGTH, + getInt(properties, + GosCompatContract.WebViewUaStartup.Key.USER_AGENT_LENGTH)); + result.putString(GosCompatContract.WebViewUaStartup.Key.ERROR, + properties.getProperty(GosCompatContract.WebViewUaStartup.Key.ERROR, "")); + return result; + } + + private void assertCompletedResult(Bundle result, int attempt, int totalAttempts) { + String message = "attempt=" + attempt + "/" + totalAttempts + + ", completed=" + + result.getBoolean(GosCompatContract.WebViewUaStartup.Key.COMPLETED) + + ", workerThread=" + + result.getBoolean(GosCompatContract.WebViewUaStartup.Key.WORKER_THREAD) + + ", uiThread=" + + result.getBoolean(GosCompatContract.WebViewUaStartup.Key.UI_THREAD) + + ", mainLooper=" + + result.getBoolean(GosCompatContract.WebViewUaStartup.Key.MAIN_LOOPER) + + ", workerTid=" + + result.getInt(GosCompatContract.WebViewUaStartup.Key.WORKER_TID) + + ", uiTid=" + result.getInt(GosCompatContract.WebViewUaStartup.Key.UI_TID) + + ", durationMs=" + + result.getLong(GosCompatContract.WebViewUaStartup.Key.DURATION_MS) + + ", userAgentLength=" + + result.getInt(GosCompatContract.WebViewUaStartup.Key.USER_AGENT_LENGTH) + + ", error=" + + result.getString(GosCompatContract.WebViewUaStartup.Key.ERROR); + + assertWithMessage(message).that(result.getBoolean( + GosCompatContract.WebViewUaStartup.Key.COMPLETED)).isTrue(); + assertWithMessage(message).that(result.getBoolean( + GosCompatContract.WebViewUaStartup.Key.WORKER_THREAD)).isTrue(); + assertWithMessage(message).that(result.getBoolean( + GosCompatContract.WebViewUaStartup.Key.UI_THREAD)).isTrue(); + assertWithMessage(message).that(result.getBoolean( + GosCompatContract.WebViewUaStartup.Key.MAIN_LOOPER)).isTrue(); + assertWithMessage(message).that(result.getInt( + GosCompatContract.WebViewUaStartup.Key.USER_AGENT_LENGTH)).isGreaterThan(0); + } + + private void assertDefaultUserAgentIsReduced(String userAgent) { + String message = "userAgent=" + userAgent; + + assertWithMessage(message).that(userAgent).isNotEmpty(); + assertWithMessage(message).that(extractPlatform(userAgent)) + .isEqualTo(REDUCED_WEBVIEW_PLATFORM); + assertWithMessage(message).that(userAgent).doesNotContain("Build/"); + + Matcher chromeProductMatcher = CHROME_PRODUCT_PATTERN.matcher(userAgent); + assertWithMessage(message).that(chromeProductMatcher.find()).isTrue(); + assertWithMessage(message).that(chromeProductMatcher.group(2)).isEqualTo("0"); + assertWithMessage(message).that(chromeProductMatcher.group(3)).isEqualTo("0"); + assertWithMessage(message).that(chromeProductMatcher.group(4)).isEqualTo("0"); + } + + private static String extractPlatform(String userAgent) { + int start = userAgent.indexOf('('); + int end = userAgent.indexOf(')', start + 1); + if (start < 0 || end <= start) { + return ""; + } + return userAgent.substring(start + 1, end); + } + + private static boolean getBoolean(Properties properties, String key) { + return Boolean.parseBoolean(properties.getProperty(key)); + } + + private static int getInt(Properties properties, String key) { + return Integer.parseInt(properties.getProperty(key, "0")); + } + + private static long getLong(Properties properties, String key) { + return Long.parseLong(properties.getProperty(key, "0")); + } +} diff --git a/tests/GosCompatTests/README.md b/tests/GosCompatTests/README.md index 7f785c1ec058e..188dae45d722b 100644 --- a/tests/GosCompatTests/README.md +++ b/tests/GosCompatTests/README.md @@ -1,9 +1,10 @@ # GosCompatTests -`GosCompatCheckApp` is a standalone helper app with a manual UI. +These tests are generally shapped as non-privileged apps in order to regression test compatability +with patterns or code used in actual apps. -`GosCompatMapsScanTests` covers compatibility with apps using native libraries that scan -`/proc/self/maps` and read selected mapped memory ranges. +`GosCompatCheckApp` is a standalone helper app with a manual UI. See subdirectories for descriptions +of each test. You can run tests from the checkout root via this directory's `TEST_MAPPING`: @@ -11,5 +12,8 @@ You can run tests from the checkout root via this directory's `TEST_MAPPING`: atest --test-mapping frameworks/base/tests/GosCompatTests:gos_postsubmit ``` -Alternatively, view the `TEST_MAPPING` file or Android.bp files and run test modules directly with +Generally, the device should be unlocked and on user 0 while the tests are running. The tests can +be run on user builds both via `atest` and using the standalone UI in `GosCompatCheckApp`. + +Alternatively, view the `TEST_MAPPING` file or Android.bp files and run test modules directly with `atest`. diff --git a/tests/GosCompatTests/TEST_MAPPING b/tests/GosCompatTests/TEST_MAPPING index d08ceacd6d715..34c0d222ad81e 100644 --- a/tests/GosCompatTests/TEST_MAPPING +++ b/tests/GosCompatTests/TEST_MAPPING @@ -2,6 +2,12 @@ "gos_postsubmit": [ { "name": "GosCompatMapsScanTests" + }, + { + "name": "GosCompatWebViewTests" + }, + { + "name": "GosCompatDmaBufReleaseTests" } ] } diff --git a/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java b/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java index a9bd2263bfa11..44931627f4b4c 100644 --- a/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java +++ b/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java @@ -3,38 +3,183 @@ import android.net.Uri; public final class GosCompatContract { - public static final String APP_PACKAGE = "app.grapheneos.goscompat.checks"; - public static final String MAPS_SCAN_PROCESS = APP_PACKAGE + ":maps_scan"; - public static final String ACTIVITY = APP_PACKAGE + "/.GosCompatCheckActivity"; - public static final String MAPS_SCAN_CRASH_ACTIVITY = - APP_PACKAGE + "/.MapsScanCrashActivity"; - public static final String MAPS_SCAN_AUTHORITY = APP_PACKAGE + ".maps_scan_provider"; - public static final Uri MAPS_SCAN_CONTENT_URI = Uri.parse("content://" + MAPS_SCAN_AUTHORITY); - public static final String MAPS_SCAN_RESULT_FILE = "maps_scan_result.properties"; - - public static final String EXTRA_MAPS_SCAN_MODE = "maps_scan_mode"; - public static final String EXTRA_MAPS_SCAN_TOKEN = "maps_scan_token"; - public static final String MODE_DIRECT = "direct"; - public static final String MODE_REFLECTIVE = "reflective"; - - public static final String METHOD_RUN_MAPS_SCAN_CHECK = "run_maps_scan_check"; - public static final String METHOD_RUN_DIRECT_MAPS_SCAN_CHECK = "run_direct_maps_scan_check"; - public static final String METHOD_RUN_REFLECTIVE_MAPS_SCAN_CHECK = - "run_reflective_maps_scan_check"; - public static final String METHOD_GET_MAPS_SCAN_RESULT = "get_maps_scan_result"; - public static final String METHOD_CLEAR_MAPS_SCAN_RESULT = "clear_maps_scan_result"; - - public static final String KEY_STATUS_TEXT = "status_text"; - public static final String KEY_SUMMARY = "summary"; - public static final String KEY_MAPS_SCAN_RESULT_AVAILABLE = "maps_scan_result_available"; - public static final String KEY_MAPS_SCAN_COMPLETED = "maps_scan_completed"; - public static final String KEY_MAPS_SCAN_WORKER_THREAD = "maps_scan_worker_thread"; - public static final String KEY_MAPS_SCAN_SELECTED_RANGES = "maps_scan_selected_ranges"; - public static final String KEY_MAPS_SCAN_SCANNED_BYTES = "maps_scan_scanned_bytes"; - public static final String KEY_MAPS_SCAN_CALLER_TID = "maps_scan_caller_tid"; - public static final String KEY_MAPS_SCAN_WORKER_TID = "maps_scan_worker_tid"; - public static final String KEY_MAPS_SCAN_DETAILS = "maps_scan_details"; - public static final String KEY_MAPS_SCAN_ERRORS = "maps_scan_errors"; + public static final class App { + public static final String PACKAGE_NAME = "app.grapheneos.goscompat.checks"; + + private App() { + } + } + + public static final class MapsScan { + public static final String PROCESS = App.PACKAGE_NAME + ":maps_scan"; + public static final String CRASH_ACTIVITY = App.PACKAGE_NAME + "/.MapsScanCrashActivity"; + public static final String AUTHORITY = App.PACKAGE_NAME + ".maps_scan_provider"; + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); + public static final String RESULT_FILE = "maps_scan_result.properties"; + + public static final class Extra { + public static final String MODE = "maps_scan_mode"; + public static final String TOKEN = "maps_scan_token"; + + private Extra() { + } + } + + public static final class Mode { + public static final String DIRECT = "direct"; + public static final String REFLECTIVE = "reflective"; + + private Mode() { + } + } + + public static final class Method { + public static final String RUN_CHECK = "run_maps_scan_check"; + public static final String RUN_DIRECT_CHECK = "run_direct_maps_scan_check"; + public static final String RUN_REFLECTIVE_CHECK = "run_reflective_maps_scan_check"; + public static final String GET_RESULT = "get_maps_scan_result"; + public static final String CLEAR_RESULT = "clear_maps_scan_result"; + + private Method() { + } + } + + public static final class Key { + public static final String RESULT_AVAILABLE = "maps_scan_result_available"; + public static final String COMPLETED = "maps_scan_completed"; + public static final String WORKER_THREAD = "maps_scan_worker_thread"; + public static final String SELECTED_RANGES = "maps_scan_selected_ranges"; + public static final String SCANNED_BYTES = "maps_scan_scanned_bytes"; + public static final String CALLER_TID = "maps_scan_caller_tid"; + public static final String WORKER_TID = "maps_scan_worker_tid"; + public static final String STATUS_TEXT = "status_text"; + public static final String SUMMARY = "summary"; + public static final String DETAILS = "maps_scan_details"; + public static final String ERRORS = "maps_scan_errors"; + + private Key() { + } + } + + private MapsScan() { + } + } + + public static final class WebViewUaStartup { + public static final String ACTIVITY = + App.PACKAGE_NAME + "/.webviewua.WebViewUaStartupActivity"; + public static final String RESULT_FILE = "webview_ua_startup_result.properties"; + + public static final class Extra { + public static final String TOKEN = "webview_ua_startup_token"; + public static final String EXIT_PROCESS = "webview_ua_startup_exit_process"; + + private Extra() { + } + } + + public static final class Key { + public static final String RESULT_AVAILABLE = + "webview_ua_startup_result_available"; + public static final String COMPLETED = "webview_ua_startup_completed"; + public static final String WORKER_THREAD = "webview_ua_startup_worker_thread"; + public static final String UI_THREAD = "webview_ua_startup_ui_thread"; + public static final String MAIN_LOOPER = "webview_ua_startup_main_looper"; + public static final String WORKER_TID = "webview_ua_startup_worker_tid"; + public static final String UI_TID = "webview_ua_startup_ui_tid"; + public static final String DURATION_MS = "webview_ua_startup_duration_ms"; + public static final String USER_AGENT_LENGTH = + "webview_ua_startup_user_agent_length"; + public static final String ERROR = "webview_ua_startup_error"; + + private Key() { + } + } + + private WebViewUaStartup() { + } + } + + public static final class DmaBufRelease { + public static final String PROCESS = App.PACKAGE_NAME + ":dmabuf_release"; + public static final String ACTIVITY = App.PACKAGE_NAME + "/.dmabuf.DmaBufReleaseActivity"; + public static final String RESULT_FILE = "dmabuf_release_result.properties"; + public static final int DEFAULT_ITERATIONS = 4; + public static final int VFRAME_SECURE_DIRECT_WIDTH = 8192; + public static final int VFRAME_SECURE_DIRECT_HEIGHT = 8192; + public static final int VFRAME_SECURE_DIRECT_COUNT = 1; + public static final int VSTREAM_SECURE_DIRECT_WIDTH = 1024; + public static final int VSTREAM_SECURE_DIRECT_HEIGHT = 1024; + public static final int VSTREAM_SECURE_DIRECT_COUNT = 1; + public static final int SECURE_CHUNK_HEAP_ONE_CHUNK_WIDTH = 128; + public static final int SECURE_CHUNK_HEAP_ONE_CHUNK_HEIGHT = 128; + public static final int SECURE_CHUNK_HEAP_ONE_CHUNK_COUNT = 1; + public static final int PROTECTED_EGL_WIDTH = 8192; + public static final int PROTECTED_EGL_HEIGHT = 8192; + public static final int PROTECTED_EGL_RESOURCE_COUNT = 2; + + public static final class Extra { + public static final String TOKEN = "dmabuf_release_token"; + public static final String MODE = "dmabuf_release_mode"; + public static final String HEAP_NAME = "dmabuf_release_heap_name"; + public static final String WIDTH = "dmabuf_release_width"; + public static final String HEIGHT = "dmabuf_release_height"; + public static final String BUFFER_COUNT = "dmabuf_release_buffer_count"; + public static final String ITERATIONS = "dmabuf_release_iterations"; + public static final String RELEASE_AFTER_READY = + "dmabuf_release_release_after_ready"; + + private Extra() { + } + } + + public static final class Mode { + public static final String SECURE_CHUNK_HEAP_DIRECT = + "secure_chunk_heap_direct"; + public static final String PROTECTED_EGL = "protected_egl"; + + private Mode() { + } + } + + public static final class Heap { + public static final String VFRAME_SECURE = "vframe-secure"; + public static final String VSTREAM_SECURE = "vstream-secure"; + + private Heap() { + } + } + + public static final class Key { + public static final String RESULT_AVAILABLE = "dmabuf_release_result_available"; + public static final String READY = "dmabuf_release_ready"; + public static final String UNSUPPORTED = "dmabuf_release_unsupported"; + public static final String MODE = "dmabuf_release_mode"; + public static final String WIDTH = "dmabuf_release_width"; + public static final String HEIGHT = "dmabuf_release_height"; + public static final String REQUESTED_BUFFERS = + "dmabuf_release_requested_buffers"; + public static final String ALLOCATED_BUFFERS = + "dmabuf_release_allocated_buffers"; + public static final String ITERATIONS = "dmabuf_release_iterations"; + public static final String PID = "dmabuf_release_pid"; + public static final String TID = "dmabuf_release_tid"; + public static final String PROTECTED_CONTENT = + "dmabuf_release_protected_content"; + public static final String RELEASED = "dmabuf_release_released"; + public static final String HEAP_PATH = "dmabuf_release_heap_path"; + public static final String HEAP_NAME = "dmabuf_release_heap_name"; + public static final String ALLOCATOR = "dmabuf_release_allocator"; + public static final String ALLOCATION = "dmabuf_release_allocation"; + public static final String ERROR = "dmabuf_release_error"; + + private Key() { + } + } + + private DmaBufRelease() { + } + } private GosCompatContract() { }