diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba0c818..9e6a039a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add Binder IPC call instrumentation ([#1159](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1159)) + ## 6.4.0-alpha.4 ### Internal Changes 🔧 diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt index efc48c90..daa7dfa8 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt @@ -203,6 +203,7 @@ fun ApplicationAndroidComponentsExtension.configure( extension.includeSourceContext, extension.dexguardEnabled, extension.tracingInstrumentation.appStart.enabled, + extension.tracingInstrumentation.binderIpc.enabled, ) /** * We have to register SentryModulesService as a build event listener, so it will not be @@ -235,6 +236,9 @@ fun ApplicationAndroidComponentsExtension.configure( params.appStartEnabled.setDisallowChanges( extension.tracingInstrumentation.appStart.enabled ) + params.binderIpcEnabled.setDisallowChanges( + extension.tracingInstrumentation.binderIpc.enabled + ) params.tmpDir.set(tmpDir) } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/BinderIpcExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/BinderIpcExtension.kt new file mode 100644 index 00000000..3247f94d --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/BinderIpcExtension.kt @@ -0,0 +1,10 @@ +package io.sentry.android.gradle.extensions + +import javax.inject.Inject +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +open class BinderIpcExtension @Inject constructor(objects: ObjectFactory) { + /** Enables or disables Binder IPC call instrumentation. Defaults to true. */ + val enabled: Property = objects.property(Boolean::class.java).convention(true) +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt index d51ca26f..10c9de7a 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt @@ -71,6 +71,12 @@ open class TracingInstrumentationExtension @Inject constructor(objects: ObjectFa fun appStart(appStartExtensionAction: Action) { appStartExtensionAction.execute(appStart) } + + val binderIpc: BinderIpcExtension = objects.newInstance(BinderIpcExtension::class.java) + + fun binderIpc(binderIpcAction: Action) { + binderIpcAction.execute(binderIpc) + } } enum class InstrumentationFeature(val integrationName: String) { diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt index 18a8a1d0..f17b0496 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt @@ -13,6 +13,7 @@ import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.Android import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement import io.sentry.android.gradle.instrumentation.appstart.Application import io.sentry.android.gradle.instrumentation.appstart.ContentProvider +import io.sentry.android.gradle.instrumentation.binder.BinderIpc import io.sentry.android.gradle.instrumentation.logcat.Logcat import io.sentry.android.gradle.instrumentation.logcat.LogcatLevel import io.sentry.android.gradle.instrumentation.okhttp.OkHttp @@ -63,6 +64,8 @@ abstract class SpanAddingClassVisitorFactory : @get:Input val logcatEnabled: Property @get:Input val appStartEnabled: Property + + @get:Input val binderIpcEnabled: Property } private val instrumentable: ClassInstrumentable @@ -106,6 +109,7 @@ abstract class SpanAddingClassVisitorFactory : RemappingInstrumentable().takeIf { sentryModulesService.isFileIOInstrEnabled() }, ComposeNavigation().takeIf { sentryModulesService.isComposeInstrEnabled() }, Logcat().takeIf { sentryModulesService.isLogcatInstrEnabled() }, + BinderIpc().takeIf { sentryModulesService.isBinderIpcInstrEnabled() }, Application().takeIf { sentryModulesService.isAppStartInstrEnabled() }, ContentProvider().takeIf { sentryModulesService.isAppStartInstrEnabled() }, ) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpc.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpc.kt new file mode 100644 index 00000000..a95bbc19 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpc.kt @@ -0,0 +1,32 @@ +package io.sentry.android.gradle.instrumentation.binder + +import com.android.build.api.instrumentation.ClassContext +import io.sentry.android.gradle.instrumentation.ClassInstrumentable +import io.sentry.android.gradle.instrumentation.CommonClassVisitor +import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory +import io.sentry.android.gradle.instrumentation.util.isSentryClass +import org.objectweb.asm.ClassVisitor + +class BinderIpc : ClassInstrumentable { + + companion object { + private const val CLASSNAME = "BinderIpc" + } + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters, + ): ClassVisitor { + return CommonClassVisitor( + apiVersion, + originalVisitor, + CLASSNAME, + listOf(BinderIpcMethodInstrumentable()), + parameters, + ) + } + + override fun isInstrumentable(data: ClassContext) = !data.isSentryClass() +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpcMethodVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpcMethodVisitor.kt new file mode 100644 index 00000000..024d1ec4 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpcMethodVisitor.kt @@ -0,0 +1,119 @@ +package io.sentry.android.gradle.instrumentation.binder + +import io.sentry.android.gradle.instrumentation.MethodContext +import io.sentry.android.gradle.instrumentation.MethodInstrumentable +import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.commons.GeneratorAdapter +import org.objectweb.asm.commons.Method + +class BinderIpcMethodInstrumentable : MethodInstrumentable { + + override fun getVisitor( + instrumentableContext: MethodContext, + apiVersion: Int, + originalVisitor: MethodVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters, + ): MethodVisitor = BinderIpcMethodVisitor(apiVersion, originalVisitor, instrumentableContext) + + override fun isInstrumentable(data: MethodContext): Boolean = true +} + +private const val SENTRY_IPC_TRACER = "io/sentry/android/core/SentryIpcTracer" + +class BinderIpcMethodVisitor( + apiVersion: Int, + originalVisitor: MethodVisitor, + instrumentableContext: MethodContext, +) : + GeneratorAdapter( + apiVersion, + originalVisitor, + instrumentableContext.access, + instrumentableContext.name, + instrumentableContext.descriptor, + ) { + + override fun visitMethodInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + isInterface: Boolean, + ) { + val spec = BinderMethodRegistry.lookup(owner, name) + if (spec == null) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + return + } + + val isInstanceCall = opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKEINTERFACE + val isStaticCall = opcode == Opcodes.INVOKESTATIC + if (spec.isStatic && !isStaticCall || !spec.isStatic && !isInstanceCall) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + return + } + + val argTypes = Method(name, descriptor).argumentTypes + + // Save arguments from the stack into temp locals (reverse order for LIFO) + val argLocals = IntArray(argTypes.size) + for (i in argLocals.size - 1 downTo 0) { + argLocals[i] = newLocal(argTypes[i]) + storeLocal(argLocals[i]) + } + + val receiverLocal = + if (!spec.isStatic) { + val local = newLocal(Type.getObjectType(owner)) + storeLocal(local) + local + } else { + -1 + } + + mv.visitLdcInsn(spec.component) + mv.visitLdcInsn(name) + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + SENTRY_IPC_TRACER, + "onCallStart", + "(Ljava/lang/String;Ljava/lang/String;)I", + false, + ) + val cookieLocal = newLocal(Type.INT_TYPE) + storeLocal(cookieLocal) + + val tryStart = Label() + val tryEnd = Label() + val catchHandler = Label() + val afterFinally = Label() + + mv.visitTryCatchBlock(tryStart, tryEnd, catchHandler, null) + mv.visitLabel(tryStart) + + if (!spec.isStatic) { + loadLocal(receiverLocal) + } + for (local in argLocals) { + loadLocal(local) + } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + + mv.visitLabel(tryEnd) + loadLocal(cookieLocal) + mv.visitMethodInsn(Opcodes.INVOKESTATIC, SENTRY_IPC_TRACER, "onCallEnd", "(I)V", false) + mv.visitJumpInsn(Opcodes.GOTO, afterFinally) + + // catch-all handler: call onCallEnd then re-throw + mv.visitLabel(catchHandler) + loadLocal(cookieLocal) + mv.visitMethodInsn(Opcodes.INVOKESTATIC, SENTRY_IPC_TRACER, "onCallEnd", "(I)V", false) + mv.visitInsn(Opcodes.ATHROW) + + mv.visitLabel(afterFinally) + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderMethodRegistry.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderMethodRegistry.kt new file mode 100644 index 00000000..ed1e5bde --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderMethodRegistry.kt @@ -0,0 +1,423 @@ +package io.sentry.android.gradle.instrumentation.binder + +data class BinderMethodSpec( + val owner: String, + val name: String, + val component: String, + val isStatic: Boolean = false, +) + +object BinderMethodRegistry { + + private val registry: Map> = buildRegistry() + + fun lookup(owner: String, name: String): BinderMethodSpec? { + val specs = registry[owner] ?: return null + return specs.find { it.name == name } + } + + @Suppress("LongMethod") + private fun buildRegistry(): Map> { + val specs = mutableListOf() + + specs.addAll( + "android/content/ContentResolver", + "ContentResolver", + "query", + "insert", + "update", + "delete", + "call", + "bulkInsert", + "openInputStream", + "openOutputStream", + "openAssetFileDescriptor", + "openFileDescriptor", + "acquireContentProviderClient", + "registerContentObserver", + "getType", + ) + + specs.addAll( + "android/content/pm/PackageManager", + "PackageManager", + "getInstalledPackages", + "getPackageInfo", + "resolveActivity", + "queryIntentActivities", + "getInstalledApplications", + "resolveService", + "queryIntentServices", + "getApplicationInfo", + "getActivityInfo", + "getServiceInfo", + "getReceiverInfo", + "getProviderInfo", + "checkPermission", + "hasSystemFeature", + "getLaunchIntentForPackage", + "getComponentEnabledSetting", + "setComponentEnabledSetting", + "getPackagesForUid", + "getInstallerPackageName", + "getInstallSourceInfo", + ) + + for (settingsClass in + listOf( + "android/provider/Settings\$Secure", + "android/provider/Settings\$Global", + "android/provider/Settings\$System", + )) { + val component = "Settings." + settingsClass.substringAfterLast("\$") + for (method in listOf("getString", "getInt", "getLong", "getFloat", "putString", "putInt")) { + specs.add(BinderMethodSpec(settingsClass, method, component, isStatic = true)) + } + } + + for (ctx in listOf("android/content/Context", "android/content/ContextWrapper")) { + specs.addAll( + ctx, + "Context", + "startService", + "stopService", + "bindService", + "unbindService", + "sendBroadcast", + "sendOrderedBroadcast", + "startActivity", + "startActivities", + "startForegroundService", + "registerReceiver", + "unregisterReceiver", + "checkSelfPermission", + "checkPermission", + ) + } + + specs.addAll( + "android/net/ConnectivityManager", + "ConnectivityManager", + "getActiveNetworkInfo", + "getActiveNetwork", + "getNetworkCapabilities", + "getAllNetworks", + "isActiveNetworkMetered", + "registerDefaultNetworkCallback", + "registerNetworkCallback", + ) + + specs.addAll( + "android/app/ActivityManager", + "ActivityManager", + "getRunningAppProcesses", + "getMemoryInfo", + "getRunningServices", + "getProcessMemoryInfo", + ) + + specs.addAll( + "android/view/inputmethod/InputMethodManager", + "InputMethodManager", + "showSoftInput", + "hideSoftInputFromWindow", + "restartInput", + "isActive", + ) + + specs.addAll( + "android/hardware/camera2/CameraManager", + "CameraManager", + "getCameraIdList", + "getCameraCharacteristics", + "openCamera", + ) + + specs.addAll( + "android/os/PowerManager", + "PowerManager", + "isInteractive", + "isDeviceIdleMode", + "isPowerSaveMode", + ) + specs.addAll("android/os/PowerManager\$WakeLock", "PowerManager.WakeLock", "acquire", "release") + + specs.addAll( + "android/location/LocationManager", + "LocationManager", + "getLastKnownLocation", + "requestLocationUpdates", + "getProviders", + ) + + specs.addAll( + "android/telephony/TelephonyManager", + "TelephonyManager", + "getDeviceId", + "getNetworkOperator", + "getSimOperator", + "getNetworkOperatorName", + "getSimOperatorName", + "getLine1Number", + "getSubscriberId", + "getNetworkType", + "getDataNetworkType", + "getAllCellInfo", + ) + + specs.addAll( + "android/net/wifi/WifiManager", + "WifiManager", + "getConnectionInfo", + "isWifiEnabled", + "setWifiEnabled", + "getScanResults", + "startScan", + "getConfiguredNetworks", + "addNetwork", + "removeNetwork", + "disconnect", + "reconnect", + "reassociate", + "getDhcpInfo", + "getWifiState", + ) + + specs.addAll( + "android/bluetooth/BluetoothAdapter", + "BluetoothAdapter", + "isEnabled", + "getState", + "getName", + "getAddress", + "getBondedDevices", + "startDiscovery", + "cancelDiscovery", + "isDiscovering", + "enable", + "disable", + "getScanMode", + "setScanMode", + ) + + specs.addAll( + "android/bluetooth/BluetoothDevice", + "BluetoothDevice", + "getName", + "getBondState", + "getType", + "createBond", + "removeBond", + "connectGatt", + "getBatteryLevel", + "getUuids", + ) + + specs.addAll( + "android/bluetooth/BluetoothGatt", + "BluetoothGatt", + "connect", + "disconnect", + "discoverServices", + "readCharacteristic", + "writeCharacteristic", + "readDescriptor", + "writeDescriptor", + "readRemoteRssi", + "requestMtu", + ) + + specs.addAll( + "android/bluetooth/BluetoothManager", + "BluetoothManager", + "getConnectedDevices", + "getConnectionState", + "getDevicesMatchingConnectionStates", + "openGattServer", + ) + + specs.addAll( + "android/media/AudioManager", + "AudioManager", + "getStreamVolume", + "getStreamMaxVolume", + "setStreamVolume", + "getRingerMode", + "setRingerMode", + "requestAudioFocus", + "abandonAudioFocus", + "getMode", + "setMode", + "isMusicActive", + "isBluetoothA2dpOn", + "isBluetoothScoOn", + ) + + specs.addAll( + "android/content/ClipboardManager", + "ClipboardManager", + "getPrimaryClip", + "setPrimaryClip", + "hasPrimaryClip", + ) + + specs.addAll( + "android/app/NotificationManager", + "NotificationManager", + "notify", + "cancel", + "cancelAll", + "getActiveNotifications", + ) + + specs.addAll( + "android/app/AlarmManager", + "AlarmManager", + "set", + "setExact", + "setRepeating", + "setWindow", + "cancel", + ) + + specs.addAll( + "android/app/KeyguardManager", + "KeyguardManager", + "isKeyguardLocked", + "isDeviceLocked", + "isKeyguardSecure", + ) + + specs.addAll( + "android/accounts/AccountManager", + "AccountManager", + "getAccounts", + "getAccountsByType", + "getAuthToken", + ) + + specs.addAll("android/os/UserManager", "UserManager", "getUserProfiles", "isUserUnlocked") + + specs.addAll( + "android/hardware/display/DisplayManager", + "DisplayManager", + "getDisplays", + "getDisplay", + ) + + specs.addAll( + "android/os/Vibrator", + "Vibrator", + "vibrate", + "cancel", + "hasVibrator", + "hasAmplitudeControl", + ) + + specs.addAll( + "android/os/VibratorManager", + "VibratorManager", + "getVibratorIds", + "getDefaultVibrator", + "vibrate", + "cancel", + ) + + specs.addAll( + "android/app/job/JobScheduler", + "JobScheduler", + "schedule", + "enqueue", + "cancel", + "cancelAll", + "getAllPendingJobs", + "getPendingJob", + ) + + specs.addAll( + "android/content/pm/ShortcutManager", + "ShortcutManager", + "setDynamicShortcuts", + "addDynamicShortcuts", + "removeDynamicShortcuts", + "removeAllDynamicShortcuts", + "getDynamicShortcuts", + "getPinnedShortcuts", + "updateShortcuts", + "requestPinShortcut", + "isRequestPinShortcutSupported", + "pushDynamicShortcut", + ) + + specs.addAll( + "android/app/AppOpsManager", + "AppOpsManager", + "checkOp", + "checkOpNoThrow", + "noteOp", + "noteOpNoThrow", + "noteProxyOp", + "noteProxyOpNoThrow", + "startOp", + "startOpNoThrow", + "finishOp", + "checkPackage", + ) + + specs.addAll( + "android/os/storage/StorageManager", + "StorageManager", + "getStorageVolumes", + "getPrimaryStorageVolume", + "getAllocatableBytes", + "getCacheSizeBytes", + ) + + specs.addAll( + "android/telephony/SubscriptionManager", + "SubscriptionManager", + "getActiveSubscriptionInfoList", + "getActiveSubscriptionInfo", + "getActiveSubscriptionInfoCount", + "getActiveSubscriptionInfoForSimSlotIndex", + ) + + specs.addAll( + "android/app/WallpaperManager", + "WallpaperManager", + "getDrawable", + "peekDrawable", + "getWallpaperColors", + "setResource", + "setBitmap", + "getDesiredMinimumWidth", + "getDesiredMinimumHeight", + "isWallpaperSupported", + "isSetWallpaperAllowed", + ) + + val strictMode = "android/os/StrictMode" + for (method in + listOf( + "setThreadPolicy", + "setThreadPolicyMask", + "allowThreadDiskWrites", + "allowThreadDiskReads", + "allowThreadViolations", + )) { + specs.add(BinderMethodSpec(strictMode, method, "StrictMode", isStatic = true)) + } + + return specs.groupBy { it.owner } + } + + private fun MutableList.addAll( + owner: String, + component: String, + vararg methods: String, + ) { + for (method in methods) { + add(BinderMethodSpec(owner, method, component)) + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt index 29ba71bb..2ce5d360 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt @@ -44,6 +44,10 @@ abstract class SentryModulesService : features.add("AppStartInstrumentation") } + if (isBinderIpcInstrEnabled()) { + features.add("BinderIpcInstrumentation") + } + if (parameters.sourceContextEnabled.getOrElse(false)) { features.add("SourceContext") } @@ -116,6 +120,10 @@ abstract class SentryModulesService : sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_CORE, SentryVersions.VERSION_APP_START) && parameters.appStartEnabled.get() + fun isBinderIpcInstrEnabled(): Boolean = + sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_CORE, SentryVersions.VERSION_BINDER_IPC) && + parameters.binderIpcEnabled.get() + private fun Map.isAtLeast( module: ModuleIdentifier, minVersion: SemVer, @@ -129,6 +137,7 @@ abstract class SentryModulesService : sourceContextEnabled: Provider, dexguardEnabled: Provider, appStartEnabled: Provider, + binderIpcEnabled: Provider, ): Provider { return project.gradle.sharedServices.registerIfAbsent( getBuildServiceName(SentryModulesService::class.java), @@ -139,6 +148,7 @@ abstract class SentryModulesService : it.parameters.sourceContextEnabled.setDisallowChanges(sourceContextEnabled) it.parameters.dexguardEnabled.setDisallowChanges(dexguardEnabled) it.parameters.appStartEnabled.setDisallowChanges(appStartEnabled) + it.parameters.binderIpcEnabled.setDisallowChanges(binderIpcEnabled) } } } @@ -156,5 +166,7 @@ abstract class SentryModulesService : @get:Input val dexguardEnabled: Property @get:Input val appStartEnabled: Property + + @get:Input val binderIpcEnabled: Property } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt index 22275617..09ec8d63 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt @@ -33,6 +33,7 @@ internal object SentryVersions { internal val VERSION_SQLITE = SemVer(6, 21, 0) internal val VERSION_ANDROID_OKHTTP_LISTENER = SemVer(6, 20, 0) internal val VERSION_OKHTTP = SemVer(7, 0, 0) + internal val VERSION_BINDER_IPC = SemVer(8, 40, 0) } internal object SentryModules { diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpcMethodVisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpcMethodVisitorTest.kt new file mode 100644 index 00000000..72a30e38 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderIpcMethodVisitorTest.kt @@ -0,0 +1,144 @@ +package io.sentry.android.gradle.instrumentation.binder + +import io.sentry.android.gradle.instrumentation.CommonClassVisitor +import io.sentry.android.gradle.instrumentation.fakes.TestSpanAddingParameters +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.util.Textifier +import org.objectweb.asm.util.TraceClassVisitor + +class BinderIpcMethodVisitorTest { + + @get:Rule val tmpDir = TemporaryFolder() + + private fun instrument(classBytes: ByteArray): ByteArray { + val reader = ClassReader(classBytes) + val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS) + val visitor = + CommonClassVisitor( + Opcodes.ASM9, + writer, + "TestClass", + listOf(BinderIpcMethodInstrumentable()), + TestSpanAddingParameters(debugOutput = false, inMemoryDir = tmpDir.root), + ) + reader.accept(visitor, ClassReader.SKIP_FRAMES) + return writer.toByteArray() + } + + private fun disassemble(bytes: ByteArray): String { + val sw = StringWriter() + ClassReader(bytes).accept(TraceClassVisitor(null, Textifier(), PrintWriter(sw)), 0) + return sw.toString() + } + + @Test + fun `wraps known instance binder call with tracer start and end`() { + // Method body: ContentResolver.query(uri, null, null, null, null) + val bytes = + SyntheticClass.build("callQuery", "(Landroid/content/ContentResolver;Landroid/net/Uri;)V") { + visitVarInsn(Opcodes.ALOAD, 0) // resolver + visitVarInsn(Opcodes.ALOAD, 1) // uri + visitInsn(Opcodes.ACONST_NULL) + visitInsn(Opcodes.ACONST_NULL) + visitInsn(Opcodes.ACONST_NULL) + visitInsn(Opcodes.ACONST_NULL) + visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "android/content/ContentResolver", + "query", + "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;", + false, + ) + visitInsn(Opcodes.POP) + visitInsn(Opcodes.RETURN) + } + + val instrumented = instrument(bytes) + val text = disassemble(instrumented) + + assertTrue( + text.contains("io/sentry/android/core/SentryIpcTracer.onCallStart"), + "onCallStart should be emitted:\n$text", + ) + assertTrue( + text.contains("io/sentry/android/core/SentryIpcTracer.onCallEnd"), + "onCallEnd should be emitted:\n$text", + ) + assertTrue( + text.contains("LDC \"ContentResolver\""), + "component constant should be pushed:\n$text", + ) + assertTrue(text.contains("LDC \"query\""), "method name constant should be pushed:\n$text") + assertTrue(text.contains("android/content/ContentResolver.query")) + assertTrue(text.contains("TRYCATCHBLOCK"), "try/catch handler should be emitted:\n$text") + } + + @Test + fun `wraps known static binder call`() { + val bytes = + SyntheticClass.build("callSettings", "()Ljava/lang/String;") { + visitInsn(Opcodes.ACONST_NULL) // resolver + visitLdcInsn("some_key") + visitMethodInsn( + Opcodes.INVOKESTATIC, + "android/provider/Settings\$Secure", + "getString", + "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;", + false, + ) + visitInsn(Opcodes.ARETURN) + } + + val instrumented = instrument(bytes) + val text = disassemble(instrumented) + + assertTrue(text.contains("io/sentry/android/core/SentryIpcTracer.onCallStart")) + assertTrue(text.contains("LDC \"Settings.Secure\"")) + } + + @Test + fun `does not wrap unknown method calls`() { + val bytes = + SyntheticClass.build("callUnknown", "(Ljava/lang/String;)I") { + visitVarInsn(Opcodes.ALOAD, 1) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false) + visitInsn(Opcodes.IRETURN) + } + + val instrumented = instrument(bytes) + val text = disassemble(instrumented) + + assertFalse(text.contains("SentryIpcTracer"), "unknown calls must not be wrapped:\n$text") + } + + @Test + fun `does not wrap when opcode does not match registry kind`() { + // Registry marks ContentResolver.query as instance-only; emit as INVOKESTATIC and expect no + // wrap + val bytes = + SyntheticClass.build("callStaticQuery", "()V") { + visitMethodInsn( + Opcodes.INVOKESTATIC, + "android/content/ContentResolver", + "query", + "()V", + false, + ) + visitInsn(Opcodes.RETURN) + } + + val instrumented = instrument(bytes) + val text = disassemble(instrumented) + + assertFalse(text.contains("SentryIpcTracer")) + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderMethodRegistryTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderMethodRegistryTest.kt new file mode 100644 index 00000000..17184204 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/BinderMethodRegistryTest.kt @@ -0,0 +1,43 @@ +package io.sentry.android.gradle.instrumentation.binder + +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.junit.Test + +class BinderMethodRegistryTest { + + @Test + fun `lookup returns null for unknown owner`() { + assertNull(BinderMethodRegistry.lookup("com/example/Foo", "bar")) + } + + @Test + fun `lookup returns null for known owner but unknown method`() { + assertNull(BinderMethodRegistry.lookup("android/content/ContentResolver", "unknownMethod")) + } + + @Test + fun `lookup returns spec for known instance binder method`() { + val spec = BinderMethodRegistry.lookup("android/content/ContentResolver", "query") + assertNotNull(spec) + assertEquals("ContentResolver", spec.component) + assertFalse(spec.isStatic) + } + + @Test + fun `lookup returns spec for known static binder method`() { + val spec = BinderMethodRegistry.lookup("android/provider/Settings\$Secure", "getString") + assertNotNull(spec) + assertEquals("Settings.Secure", spec.component) + assertTrue(spec.isStatic) + } + + @Test + fun `lookup covers Context subtypes separately`() { + assertNotNull(BinderMethodRegistry.lookup("android/content/Context", "startService")) + assertNotNull(BinderMethodRegistry.lookup("android/content/ContextWrapper", "startService")) + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/SyntheticClass.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/SyntheticClass.kt new file mode 100644 index 00000000..7a934c4b --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/binder/SyntheticClass.kt @@ -0,0 +1,20 @@ +package io.sentry.android.gradle.instrumentation.binder + +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +internal object SyntheticClass { + + fun build(methodName: String, descriptor: String, body: MethodVisitor.() -> Unit): ByteArray { + val cw = ClassWriter(ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES) + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "TestClass", null, "java/lang/Object", null) + val mv = cw.visitMethod(Opcodes.ACC_PUBLIC, methodName, descriptor, null, null) + mv.visitCode() + mv.body() + mv.visitMaxs(0, 0) + mv.visitEnd() + cw.visitEnd() + return cw.toByteArray() + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt index cfe4c0d2..dbc56b44 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt @@ -43,4 +43,7 @@ class TestSpanAddingParameters( override val appStartEnabled: Property get() = TODO() + + override val binderIpcEnabled: Property + get() = TODO() } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt index c2f728d8..74ee2cef 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt @@ -57,6 +57,7 @@ class SentryModulesCollectorTest { val sourceContextEnabled = fakeProject.provider { true } val dexguardEnabled = fakeProject.provider { true } val appStartEnabled = fakeProject.provider { true } + val binderIpcEnabled = fakeProject.provider { true } val project = spy(fakeProject) whenever(project.logger).thenReturn(logger) @@ -69,6 +70,7 @@ class SentryModulesCollectorTest { sourceContextEnabled, dexguardEnabled, appStartEnabled, + binderIpcEnabled, ) return project