Skip to content

Commit 1283245

Browse files
authored
binder: Fix a ServiceConnection leak (#8861)
Closes #8726
1 parent b29c3ec commit 1283245

File tree

2 files changed

+22
-1
lines changed

2 files changed

+22
-1
lines changed

binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public synchronized void bind() {
119119
state = State.BINDING;
120120
Status bindResult = bindInternal(sourceContext, bindIntent, this, bindFlags);
121121
if (!bindResult.isOk()) {
122+
handleBindServiceFailure(sourceContext, this);
122123
state = State.UNBOUND;
123124
mainThreadExecutor.execute(() -> notifyUnbound(bindResult));
124125
}
@@ -142,6 +143,19 @@ private static Status bindInternal(
142143
}
143144
}
144145

146+
// Over the years, the API contract for Context#bindService() has been inconsistent on the subject
147+
// of error handling. But inspecting recent AOSP implementations shows that, internally,
148+
// bindService() retains a reference to the ServiceConnection when it throws certain Exceptions
149+
// and even when it returns false. To avoid leaks, we *always* call unbindService() in case of
150+
// error and simply ignore any "Service not registered" IAE and other RuntimeExceptions.
151+
private static void handleBindServiceFailure(Context context, ServiceConnection conn) {
152+
try {
153+
context.unbindService(conn);
154+
} catch (RuntimeException e) {
155+
logger.log(Level.FINE, "Could not clean up after bindService() failure.", e);
156+
}
157+
}
158+
145159
@Override
146160
@AnyThread
147161
public void unbind() {

binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ public void setUp() {
6868
shadowApplication = shadowOf(appContext);
6969
shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder);
7070

71+
// Don't call onServiceDisconnected() upon unbindService(), just like the real Android doesn't.
72+
shadowApplication.setUnbindServiceCallsOnServiceDisconnected(false);
73+
7174
binding = newBuilder().build();
7275
shadowOf(getMainLooper()).idle();
7376
}
@@ -137,6 +140,7 @@ public void testBindUnbind() throws Exception {
137140
assertThat(observer.gotUnboundEvent).isTrue();
138141
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.CANCELLED);
139142
assertThat(binding.isSourceContextCleared()).isTrue();
143+
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
140144
}
141145

142146
@Test
@@ -174,6 +178,7 @@ public void testBindFailure() throws Exception {
174178
assertThat(observer.gotUnboundEvent).isTrue();
175179
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.UNIMPLEMENTED);
176180
assertThat(binding.isSourceContextCleared()).isTrue();
181+
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
177182
}
178183

179184
@Test
@@ -187,6 +192,7 @@ public void testBindSecurityException() throws Exception {
187192
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.PERMISSION_DENIED);
188193
assertThat(observer.unboundReason.getCause()).isEqualTo(securityException);
189194
assertThat(binding.isSourceContextCleared()).isTrue();
195+
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
190196
}
191197

192198
@Test
@@ -257,7 +263,8 @@ private void assertNoLockHeld() {
257263
} catch (IllegalMonitorStateException ime) {
258264
// Expected.
259265
} catch (InterruptedException inte) {
260-
throw new AssertionError("Interrupted exception when we shouldn't have been able to wait.", inte);
266+
throw new AssertionError(
267+
"Interrupted exception when we shouldn't have been able to wait.", inte);
261268
}
262269
}
263270

0 commit comments

Comments
 (0)