Skip to content

feat(instrumentation): Add binder IPC call tracing#1159

Open
markushi wants to merge 11 commits into
mainfrom
markushi/feat/binder-tracing
Open

feat(instrumentation): Add binder IPC call tracing#1159
markushi wants to merge 11 commits into
mainfrom
markushi/feat/binder-tracing

Conversation

@markushi

@markushi markushi commented Apr 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds simple registry-based bytecode instrumentation that wraps known APIs causing Binder IPC calls (ContentResolver, PackageManager, Settings.*, ConnectivityManager, various system service managers, etc.) with SentryIpcTracer.onCallStart / onCallEnd calls so the SDK can emit spans/metrics for cross-process hops.
  • The matching adapter class io.sentry.android.core.internal.binder.SentryBinderAdapter is added in feat(android): binder IPC tracing adapter for Gradle plugin instrumentation sentry-java#5326 and will ship in sentry-java TBD. Instrumentation is planned to no-op when sentry-android-core is below that version.
  • New tracingInstrumentation.binder.enabled extension property, defaults to true.

Test plan

  • ./gradlew :plugin-build:test --tests "io.sentry.android.gradle.instrumentation.binder.*" (9 tests, all pass)
    • BinderMethodRegistryTest — registry lookup (unknown owner, unknown method, instance, static, Context subtype coverage)
    • BinderIpcMethodVisitorTest — ASM-level visitor test: wraps known instance + static calls with tracer, emits correct component/method LDC constants, adds try/catch handler, leaves unknown calls and opcode-mismatched calls untouched
  • Full unit test suite passes
  • spotlessApply clean

Requires

🤖 Generated with Claude Code

markushi and others added 4 commits April 3, 2026 08:39
…acer adapter presence

The SentryIpcTracer adapter class is introduced in sentry-java 8.40.0
(getsentry/sentry-java#5326). Bump VERSION_BINDER_IPC so the instrumentation
no-ops when the adapter is not present on the classpath. Adds unit test
coverage for the registry lookup and the ASM visitor (tracer wrapping,
static vs instance handling, non-binder pass-through).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against c88ce04

markushi and others added 7 commits June 3, 2026 14:33
SentryBinderAdapter.onCallStart now returns a nullable Object token
instead of an int cookie, and onCallEnd accepts that token. Update the
emitted bytecode descriptors accordingly and pin the full method
descriptors in the visitor tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
to avoid any confusion, let's remove IPC and stick with "Binder"
everywhere
…flag

Rewrite the static/instance opcode guard in BinderMethodVisitor as an
explicit check and document why it is required, extract the duplicated
onCallEnd emission, and add comments explaining the try/finally rewrite.
Rename binderIpcEnabled to binderEnabled and enable the isSentryClass
guard in Binder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@markushi markushi marked this pull request as ready for review June 15, 2026 07:36

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c99a18d. Configure here.

sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_CORE, SentryVersions.VERSION_APP_START) &&
parameters.appStartEnabled.get()

fun isBinderInstrEnabled(): Boolean = parameters.binderEnabled.get()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing SDK version gate

High Severity

isBinderInstrEnabled() only checks the Gradle binder.enabled flag (default true) and never verifies sentry-android-core is at least the version that ships SentryBinderAdapter. With the flag on and an older SDK, the transform still injects SentryBinderAdapter calls and the app can fail at runtime when those paths run.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c99a18d. Configure here.

sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_CORE, SentryVersions.VERSION_APP_START) &&
parameters.appStartEnabled.get()

fun isBinderInstrEnabled(): Boolean = parameters.binderEnabled.get()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The isBinderInstrEnabled() function is missing a version check, causing runtime crashes (NoClassDefFoundError) for users with older sentry-android-core versions when binder instrumentation is enabled by default.
Severity: CRITICAL

Suggested Fix

Update the isBinderInstrEnabled() function to include a version check for sentry-android-core. The check should verify that the SDK version is at least VERSION_BINDER (8.40.0) before enabling instrumentation, similar to other instrumentation checks in the file. The implementation should be sentryModules.isAtLeast(SENTRY_ANDROID_CORE, VERSION_BINDER) && parameters.binderEnabled.get().

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt#L123

Potential issue: The function `isBinderInstrEnabled()` only checks if the feature is
enabled by the user, but it omits a crucial version check for the `sentry-android-core`
dependency. All other similar instrumentation features in the codebase include a version
guard. Because this check is missing, if a project uses an older version of
`sentry-android-core` (specifically, a version below 8.40.0 which does not include the
`SentryBinderAdapter` class), the build plugin will still inject bytecode that calls
this missing class. This will result in a `NoClassDefFoundError` at runtime, causing the
application to crash whenever an instrumented Binder call is executed.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@0xadam-brown 0xadam-brown left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @markushi – looks excellent! 🥇

Just the missing SDK version gate + TODOs that are essential. A few additional comments on my end, but no other blockers.

open class BinderExtension @Inject constructor(objects: ObjectFactory) {
/**
* Enables or disables Binder IPC call instrumentation. Defaults to true. This requires
* sentry-android-core version TODO or above, and needs to be enabled. See

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h: TODO (same below)

(though I'm sure they're on your radar)

open class BinderExtension @Inject constructor(objects: ObjectFactory) {
/**
* Enables or disables Binder IPC call instrumentation. Defaults to true. This requires
* sentry-android-core version TODO or above, and needs to be enabled. See

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Should we remove the "and needs to be enabled" since we enable it by default, or is that phrase referring to something else?

}

override fun isInstrumentable(data: ClassContext) = !data.isSentryClass()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Any concerns about impact on build time?

Our hash map lookups are fast, and I see we also perform the same scan for logging, etc., so the effect will be incremental for anyone who hasn't disabled the defaults. Not a blocker, just making sure we considered it + curious to hear how we think about these sorts of scans (eg, okay to add indefinitely b/c cost is marginal vs something we weigh carefully each time out).


data class BinderMethodSpec(
val owner: String,
val name: String,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: name -> method[Name]

(I found myself making this translation as I read through the diff. Up to you, and ignore if ASM always uses name, of course.)


// region content & package management

specs.addAll(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work gathering all of these!

Do we have a maintenance strategy going forward, by chance? Could be worth considering a Warden job, etc. at some point focused on hard-to-cover maintenance gaps.

// is LIFO, so arguments are popped in reverse order.
val argTypes = Method(name, descriptor).argumentTypes
val argLocals = IntArray(argTypes.size)
for (i in argLocals.size - 1 downTo 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit if you want it: argsLocal.size - 1 -> argsLocals.lastIndex

sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_CORE, SentryVersions.VERSION_APP_START) &&
parameters.appStartEnabled.get()

fun isBinderInstrEnabled(): Boolean = parameters.binderEnabled.get()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

fun `lookup returns spec for known instance binder method`() {
val spec = BinderMethodRegistry.lookup("android/content/ContentResolver", "query")
assertNotNull(spec)
assertEquals("ContentResolver", spec.component)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Worth asserting on entire spec content? ie, also name and owner

(same below, esp covering subtypes separately, which would benefit most)

val text = disassemble(instrumented)

assertFalse(text.contains("SentryBinderAdapter"))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l/m: Could be worth adding a test that actually loads the instrumented class to trigger bytecode verification (eg, no corrupted stack) and to verify that we can recover the binder method return value.

eg, create minimal stubs (a recording SentryBinderAdapter + a fake BluetoothAdapter.isEnabled() or whatever) -> instrument a synthetic class that calls BluetoothAdapter.isEnabled() and returns true -> load and invoke via reflection -> verify true is actually returned.

Comment thread CHANGELOG.md

### Features

- Add Binder IPC call instrumentation ([#1159](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1159))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Do we normally also tell folks how to enable/disable (+ inform them that it's auto-enabled)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants