Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Watch destroyed services #2014

Merged
merged 2 commits into from
Dec 20, 2020
Merged

Watch destroyed services #2014

merged 2 commits into from
Dec 20, 2020

Conversation

ChaosLeung
Copy link
Contributor

@ChaosLeung ChaosLeung commented Dec 10, 2020

This may help detect leaks of services.

Before API 19, ActivityThread.mServices is a HashMap. The ActivityThread.handleStopService method will remove the service instance stored in mServices, and then Service.onDestroy method will be called. So, we can set a new HashMap and override the remove method to listen Service.onDestroy. (Yes, inspired by #2007)

Start from API 19, ActivityThread.mServices is an ArrayMap, and ArrayMap is a final class. We only can use ActivityThread.H.STOP_SERVICE to listen Service.onDestroy.

Test on API 16:
image

Test on API 29:
image

@CLAassistant
Copy link

CLAassistant commented Dec 10, 2020

CLA assistant check
All committers have signed the CLA.

@ChaosLeung
Copy link
Contributor Author

On a second thought, this approach is imperfect, both of them happen before onDestroy.

@pyricau
Copy link
Member

pyricau commented Dec 10, 2020

This is really cool! Is this hitting any API grey list / black list?

@ChaosLeung
Copy link
Contributor Author

ChaosLeung commented Dec 11, 2020

Yes, all of them are in greylist.

From the android-11-lists:

Landroid/app/ActivityManager;->IActivityManagerSingleton:Landroid/util/Singleton;
Landroid/app/ActivityThread;->currentActivityThread()Landroid/app/ActivityThread;
Landroid/app/ActivityThread;->mServices:Landroid/util/ArrayMap;
Landroid/app/ActivityThread;->mH:Landroid/app/ActivityThread$H;
Landroid/os/Handler;->mCallback:Landroid/os/Handler$Callback;
Landroid/util/Singleton;->mInstance:Ljava/lang/Object;

But WindowManagerGlobal is also described in greylist.

Landroid/view/WindowManagerGlobal;->getInstance()Landroid/view/WindowManagerGlobal;
Landroid/view/WindowManagerGlobal;->mViews:Ljava/util/ArrayList;

Should I provide a config to disable the watcher like leak_canary_watcher_install_root_view_detach?

@pyricau
Copy link
Member

pyricau commented Dec 11, 2020

Should I provide a config to disable the watcher like leak_canary_watcher_install_root_view_detach?

Depends on whether this has a chance of breaking apps. The RootView hack installs a different array list instance so quite risky.

@ChaosLeung
Copy link
Contributor Author

Okay, the destroyed service hack has same risk.

@ChaosLeung
Copy link
Contributor Author

ChaosLeung commented Dec 12, 2020

The new approach:

  • PreDestroyed: Set a different Handler.Callback object to ActivityThread.mH.mCallback for tracing ActivityThread.H.STOP_SERVICE message. Every time the Handler.Callback receive the message, we can get the pending destroy Service's token (msg.obj) and instance (from ActivityThread.mServices).

  • Destroyed: After the Service instance was called onDestroy, ActivityThread will call IActivityManager#serviceDoneExecuting (call flow of ActivityThread#handleStopService). So we can set a dynamic proxy to ActivityManager.IActivityManagerSingleton (start from API 26) or ActivityManagerNative.gDefault (before API 26) for tracing IActivityManager#serviceDoneExecuting. When receive a call of serviceDoneExecuting, we can get the Service's token (fist parameter of serviceDoneExecuting) to find out matching Service instance and watch it.

Due to the risk in this approach, now the ServiceDestroyWatcher can be disabled via:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <bool name="leak_canary_watcher_install_service_destroy">false</bool>
</resources>

@pyricau
Copy link
Member

pyricau commented Dec 20, 2020

Thanks! Sorry for the slow review, this is really cool but I need time to dig into it

@pyricau
Copy link
Member

pyricau commented Dec 20, 2020

Going through the code on cs.android.com

Another thing that's called is ActivityThread.removeContextRegistrations() which removes an entry from ActivityThread.receivers and ActivityThread.unregisteredReceivers which are ArrayMap instances (interestingly only receivers has @UnsupportedAppUsage. Not sure if we can leverage that, just wanted to write it down. ArrayMap is a final class so it looks like we can't hook in listeners there.

private val configProvider: () -> AppWatcher.Config
) {

private val mTmpServices = mutableMapOf<IBinder, Service>()
Copy link
Member

@pyricau pyricau Dec 20, 2020

Choose a reason for hiding this comment

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

We likely want a mutableMapOf<IBinder, WeakReference> to not end up being the cause of a service leak, and maybe even a WeakHashMap<IBinder, WeakReference> (ie weak key as well because idk if we should risk retaining the IBinder either)

Copy link
Member

Choose a reason for hiding this comment

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

Followed up here: #2029

serviceDestroyWatcher.onServicePreDestroy(key, it)
}
}
mCallback?.handleMessage(msg) ?: false
Copy link
Member

Choose a reason for hiding this comment

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

yeah ok this is cool. Replace the handler callback and delegate to it, which ensures that this stays compatible with any other code running the same hack.

@pyricau
Copy link
Member

pyricau commented Dec 20, 2020

Note: merged main to fix conflicts.

@pyricau pyricau merged commit e296e83 into square:main Dec 20, 2020
@ChaosLeung
Copy link
Contributor Author

Another thing that's called is ActivityThread.removeContextRegistrations() which removes an entry from ActivityThread.receivers and ActivityThread.unregisteredReceivers which are ArrayMap instances (interestingly only receivers has @UnsupportedAppUsage. Not sure if we can leverage that, just wanted to write it down. ArrayMap is a final class so it looks like we can't hook in listeners there.

We can handle the ActivityThread.H.CLEAN_UP_CONTEXT message. When receiving the message, we can get all leaking receivers from LoadedApk.mReceivers. But this approach only work for dynamic BroadcastReceiver but not static (Maybe we don’t need to care about static BroadcastReceiver).

@pyricau
Copy link
Member

pyricau commented Dec 21, 2020

Does this mean we could use this to detect leaking receivers? I'm not sure what defines a leak for a receiver exactly. After deregistration?

@ChaosLeung
Copy link
Contributor Author

ChaosLeung commented Dec 21, 2020

I'm not sure what defines a leak for a receiver exactly. After deregistration?

In my opinion, a leaked receiver means that its associated context has been destroyed (such as destroyed Activity/Service) and it has been unregistered.

Does this mean we could use this to detect leaking receivers?

In source of ActivityThread, the ActivityThread.H.CLEAN_UP_CONTEXT message only sent in handleDestroyActivity, handleStopService. Therefore, the approach mentioned above can only detect those receivers registered via Activity or Service are leaked or not.

On second thought, it is difficult to cover all scenarios. Because there is no way to detect those receivers registered with application context are leaked or not.

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.

3 participants