diff --git a/core/java/android/os/CancellationSignalBeamer.java b/core/java/android/os/CancellationSignalBeamer.java new file mode 100644 index 0000000000000..afb5ff7bf6265 --- /dev/null +++ b/core/java/android/os/CancellationSignalBeamer.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.system.SystemCleaner; + +import java.lang.ref.Cleaner; +import java.lang.ref.Reference; +import java.util.HashMap; + +/** + * A transport for {@link CancellationSignal}, but unlike + * {@link CancellationSignal#createTransport()} doesn't require pre-creating the transport in the + * target process. Instead, cancellation is forwarded over the same IPC surface as the cancellable + * request. + * + *

Important: For this to work, the following invariants must be held up: + *

+ *

Caveats: + *

+ * + * + *

Usage: + *

+ *  // Sender:
+ *
+ *  class FooManager {
+ *    var mCancellationSignalSender = new CancellationSignalBeamer.Sender() {
+ *      @Override
+ *      public void onCancel(IBinder token) { remoteIFooService.onCancelToken(token); }
+ *
+ *      @Override
+ *      public void onForget(IBinder token) { remoteIFooService.onForgetToken(token); }
+ *    };
+ *
+ *    public void doCancellableOperation(..., CancellationSignal cs) {
+ *      try (var csToken = mCancellationSignalSender.beam(cs)) {
+ *          remoteIFooService.doCancellableOperation(..., csToken);
+ *      }
+ *    }
+ *  }
+ *
+ *  // Receiver:
+ *
+ *  class FooManagerService extends IFooService.Stub {
+ *    var mCancellationSignalReceiver = new CancellationSignalBeamer.Receiver();
+ *
+ *    @Override
+ *    public void doCancellableOperation(..., IBinder csToken) {
+ *      CancellationSignal cs = mCancellationSignalReceiver.unbeam(csToken))
+ *      // ...
+ *    }
+ *
+ *    @Override
+ *    public void onCancelToken(..., IBinder csToken) {
+ *      mCancellationSignalReceiver.cancelToken(csToken))
+ *    }
+ *
+ *    @Override
+ *    public void onForgetToken(..., IBinder csToken) {
+ *      mCancellationSignalReceiver.forgetToken(csToken))
+ *    }
+ *  }
+ *
+ * 
+ * + * @hide + */ +public class CancellationSignalBeamer { + + static final Cleaner sCleaner = SystemCleaner.cleaner(); + + /** The sending side of an {@link CancellationSignalBeamer} */ + public abstract static class Sender { + + /** + * Beams a {@link CancellationSignal} through an existing Binder interface. + * + * @param cs the {@code CancellationSignal} to beam, or {@code null}. + * @return an {@link IBinder} token. MUST be {@link CloseableToken#close}d after + * the binder call transporting it to the remote process, best with + * try-with-resources. {@code null} if {@code cs} was {@code null}. + */ + // TODO(b/254888024): @MustBeClosed + @Nullable + public CloseableToken beam(@Nullable CancellationSignal cs) { + if (cs == null) { + return null; + } + return new Token(this, cs); + } + + /** + * A {@link #beam}ed {@link CancellationSignal} was closed. + * + * MUST be forwarded to {@link Receiver#cancel} with proper ordering. See + * {@link CancellationSignalBeamer} for details. + */ + public abstract void onCancel(IBinder token); + + /** + * A {@link #beam}ed {@link CancellationSignal} was GC'd. + * + * MUST be forwarded to {@link Receiver#forget} with proper ordering. See + * {@link CancellationSignalBeamer} for details. + */ + public abstract void onForget(IBinder token); + + private static class Token extends Binder implements CloseableToken, Runnable { + + private final Sender mSender; + private Preparer mPreparer; + + private Token(Sender sender, CancellationSignal signal) { + mSender = sender; + mPreparer = new Preparer(sender, signal, this); + } + + @Override + public void close() { + Preparer preparer = mPreparer; + mPreparer = null; + if (preparer != null) { + preparer.setup(); + } + } + + @Override + public void run() { + mSender.onForget(this); + } + + private static class Preparer implements CancellationSignal.OnCancelListener { + private final Sender mSender; + private final CancellationSignal mSignal; + private final Token mToken; + + private Preparer(Sender sender, CancellationSignal signal, Token token) { + mSender = sender; + mSignal = signal; + mToken = token; + } + + void setup() { + sCleaner.register(this, mToken); + mSignal.setOnCancelListener(this); + } + + @Override + public void onCancel() { + try { + mSender.onCancel(mToken); + } finally { + // Make sure we dispatch onCancel before the cleaner can run. + Reference.reachabilityFence(this); + } + } + } + } + + /** + * A {@link #beam}ed {@link CancellationSignal} ready for sending over Binder. + * + * MUST be closed after it is sent over binder, ideally through try-with-resources. + */ + public interface CloseableToken extends IBinder, AutoCloseable { + @Override + void close(); // No throws + } + } + + /** The receiving side of a {@link CancellationSignalBeamer}. */ + public static class Receiver implements IBinder.DeathRecipient { + private final HashMap mTokenMap = new HashMap<>(); + private final boolean mCancelOnSenderDeath; + + /** + * Constructs a new {@code Receiver}. + * + * @param cancelOnSenderDeath if true, {@link CancellationSignal}s obtained from + * {@link #unbeam} are automatically {@link #cancel}led if the sender token + * {@link Binder#linkToDeath dies}; otherwise they are simnply dropped. Note: if the + * sending process drops all references to the {@link CancellationSignal} before + * process death, the cancellation is not guaranteed. + */ + public Receiver(boolean cancelOnSenderDeath) { + mCancelOnSenderDeath = cancelOnSenderDeath; + } + + /** + * Unbeams a token that was obtained via {@link Sender#beam} and turns it back into a + * {@link CancellationSignal}. + * + * A subsequent call to {@link #cancel} with the same token will cancel the returned + * {@code CancellationSignal}. + * + * @param token a token that was obtained from {@link Sender}, possibly in a remote process. + * @return a {@link CancellationSignal} linked to the given token. + */ + @Nullable + public CancellationSignal unbeam(@Nullable IBinder token) { + if (token == null) { + return null; + } + synchronized (this) { + CancellationSignal cs = mTokenMap.get(token); + if (cs != null) { + return cs; + } + + cs = new CancellationSignal(); + mTokenMap.put(token, cs); + try { + token.linkToDeath(this, 0); + } catch (RemoteException e) { + dead(token); + } + return cs; + } + } + + /** + * Forgets state associated with the given token (if any). + * + * Subsequent calls to {@link #cancel} or binder death notifications on the token will not + * have any effect. + * + * This MUST be invoked when forwarding {@link Sender#onForget}, otherwise the token and + * {@link CancellationSignal} will leak if the token was ever {@link #unbeam}ed. + * + * Optionally, the receiving service logic may also invoke this if it can guarantee that + * the unbeamed CancellationSignal isn't needed anymore (i.e. the cancellable operation + * using the CancellationSignal has been fully completed). + * + * @param token the token to forget. No-op if {@code null}. + */ + public void forget(@Nullable IBinder token) { + synchronized (this) { + if (mTokenMap.remove(token) != null) { + token.unlinkToDeath(this, 0); + } + } + } + + /** + * Cancels the {@link CancellationSignal} associated with the given token (if any). + * + * This MUST be invoked when forwarding {@link Sender#onCancel}, otherwise the token and + * {@link CancellationSignal} will leak if the token was ever {@link #unbeam}ed. + * + * Optionally, the receiving service logic may also invoke this if it can guarantee that + * the unbeamed CancellationSignal isn't needed anymore (i.e. the cancellable operation + * using the CancellationSignal has been fully completed). + * + * @param token the token to forget. No-op if {@code null}. + */ + public void cancel(@Nullable IBinder token) { + CancellationSignal cs; + synchronized (this) { + cs = mTokenMap.get(token); + if (cs != null) { + forget(token); + } else { + return; + } + } + cs.cancel(); + } + + private void dead(@NonNull IBinder token) { + if (mCancelOnSenderDeath) { + cancel(token); + } else { + forget(token); + } + } + + @Override + public void binderDied(@NonNull IBinder who) { + dead(who); + } + + @Override + public void binderDied() { + throw new RuntimeException("unreachable"); + } + } +} diff --git a/core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java b/core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java new file mode 100644 index 0000000000000..42c97f3876a6a --- /dev/null +++ b/core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static android.os.CancellationSignalBeamer.Sender; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.CancellationSignalBeamer.Receiver; +import android.util.PollingCheck; +import android.util.PollingCheck.PollingCheckCondition; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.Cleaner; +import java.lang.ref.Reference; +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CancellationSignalBeamerTest { + + private CancellationSignal mSenderSignal = new CancellationSignal(); + private CancellationSignal mReceivedSignal; + private Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + @Test + public void testBeam_null() { + try (var token = mSender.beam(null)) { + assertThat(token).isNull(); + invokeGenericService(token); + } + assertThat(mReceivedSignal).isNull(); + } + + @Test + public void testBeam_nonNull() { + try (var token = mSender.beam(mSenderSignal)) { + assertThat(token).isNotNull(); + invokeGenericService(token); + } + assertThat(mReceivedSignal).isNotNull(); + } + + @Test + public void testBeam_async() { + IBinder outerToken; + try (var token = mSender.beam(mSenderSignal)) { + assertThat(token).isNotNull(); + outerToken = token; + } + invokeGenericService(outerToken); + assertThat(mReceivedSignal).isNotNull(); + } + + @Test + public void testCancelOnSentSignal_cancelsReceivedSignal() { + try (var token = mSender.beam(mSenderSignal)) { + invokeGenericService(token); + } + mSenderSignal.cancel(); + assertThat(mReceivedSignal.isCanceled()).isTrue(); + } + + @Test + public void testSendingCancelledSignal_cancelsReceivedSignal() { + mSenderSignal.cancel(); + try (var token = mSender.beam(mSenderSignal)) { + invokeGenericService(token); + } + assertThat(mReceivedSignal.isCanceled()).isTrue(); + } + + @Test + public void testUnbeam_null() { + assertThat(mReceiver.unbeam(null)).isNull(); + } + + @Test + public void testForget_null() { + mReceiver.forget(null); + } + + @Test + public void testCancel_null() { + mReceiver.cancel(null); + } + + @Test + public void testForget_withUnknownToken() { + mReceiver.forget(new Binder()); + } + + @Test + public void testCancel_withUnknownToken() { + mReceiver.cancel(new Binder()); + } + + @Test + public void testBinderDied_withUnknownToken() { + mReceiver.binderDied(new Binder()); + } + + @Test + public void testReceiverWithCancelOnSenderDead_cancelsOnSenderDeath() { + var receiver = new Receiver(true /* cancelOnSenderDeath */); + var token = new Binder(); + var signal = receiver.unbeam(token); + receiver.binderDied(token); + assertThat(signal.isCanceled()).isTrue(); + } + + @Test + public void testReceiverWithoutCancelOnSenderDead_doesntCancelOnSenderDeath() { + var receiver = new Receiver(false /* cancelOnSenderDeath */); + var token = new Binder(); + var signal = receiver.unbeam(token); + receiver.binderDied(token); + assertThat(signal.isCanceled()).isFalse(); + } + + @Test + public void testDroppingSentSignal_dropsReceivedSignal() throws Exception { + // In a multiprocess scenario, sending token over Binder might leak the token + // on both ends if we create a reference cycle. Simulate that worst-case scenario + // here by leaking it directly, then test that cleanup of the signals still works. + var receivedSignalCleaned = new CountDownLatch(1); + var tokenRef = new Object[1]; + // Reference the cancellation signals in a separate method scope, so we don't + // accidentally leak them on the stack / in a register. + Runnable r = () -> { + try (var token = mSender.beam(mSenderSignal)) { + tokenRef[0] = token; + invokeGenericService(token); + } + mSenderSignal = null; + + Cleaner.create().register(mReceivedSignal, receivedSignalCleaned::countDown); + mReceivedSignal = null; + }; + r.run(); + + waitForWithGc(() -> receivedSignalCleaned.getCount() == 0); + + Reference.reachabilityFence(tokenRef[0]); + } + + @Test + public void testRepeatedBeaming_doesntLeak() throws Exception { + var receivedSignalCleaned = new CountDownLatch(1); + var tokenRef = new Object[1]; + // Reference the cancellation signals in a separate method scope, so we don't + // accidentally leak them on the stack / in a register. + Runnable r = () -> { + try (var token = mSender.beam(mSenderSignal)) { + tokenRef[0] = token; + invokeGenericService(token); + } + // Beaming again leaves mReceivedSignal dangling, so it should be collected. + mSender.beam(mSenderSignal).close(); + + Cleaner.create().register(mReceivedSignal, receivedSignalCleaned::countDown); + mReceivedSignal = null; + }; + r.run(); + + waitForWithGc(() -> receivedSignalCleaned.getCount() == 0); + + Reference.reachabilityFence(tokenRef[0]); + } + + private void waitForWithGc(PollingCheckCondition condition) throws IOException { + try { + PollingCheck.waitFor(() -> { + Runtime.getRuntime().gc(); + return condition.canProceed(); + }); + } catch (AssertionError e) { + File heap = new File(mContext.getExternalFilesDir(null), "dump.hprof"); + Debug.dumpHprofData(heap.getAbsolutePath()); + throw e; + } + } + + private void invokeGenericService(IBinder cancellationSignalToken) { + mReceivedSignal = mReceiver.unbeam(cancellationSignalToken); + } + + private final Sender mSender = new Sender() { + @Override + public void onCancel(IBinder token) { + mReceiver.cancel(token); + } + + @Override + public void onForget(IBinder token) { + mReceiver.forget(token); + } + }; + + private final Receiver mReceiver = new Receiver(false); +}