From 0c4d2d7a92a7917a4a723e91f86aa30248e4842e Mon Sep 17 00:00:00 2001 From: Nikos Gorogiannis <nikosg@fb.com> Date: Mon, 18 Nov 2019 01:57:27 -0800 Subject: [PATCH] [starvation-whole-program] recognize Android callback methods as scheduled work Summary: Android may spontaneously call these methods on the UI thread, so recognize the fact. Reviewed By: ezgicicek Differential Revision: D18530477 fbshipit-source-id: a8a798779 --- infer/src/concurrency/ConcurrencyModels.mli | 3 + infer/src/concurrency/starvation.ml | 12 +- .../starvation-whole-program/MyActivity.java | 111 ++++++++++++++++++ .../java/starvation-whole-program/issues.exp | 8 ++ 4 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 infer/tests/codetoanalyze/java/starvation-whole-program/MyActivity.java diff --git a/infer/src/concurrency/ConcurrencyModels.mli b/infer/src/concurrency/ConcurrencyModels.mli index d912a2937..baa965e2a 100644 --- a/infer/src/concurrency/ConcurrencyModels.mli +++ b/infer/src/concurrency/ConcurrencyModels.mli @@ -61,3 +61,6 @@ val annotated_as_worker_thread : val runs_on_ui_thread : attrs_of_pname:(Typ.Procname.t -> ProcAttributes.t option) -> Tenv.t -> Typ.Procname.t -> bool (** is method not transitively annotated @WorkerThread and is modeled or annotated @UIThread or equivalent? *) + +val is_modeled_ui_method : Tenv.t -> Typ.Procname.t -> bool +(** is method a known Android UI thread callback (eg [Activity.onCreate]) *) diff --git a/infer/src/concurrency/starvation.ml b/infer/src/concurrency/starvation.ml index 79a919885..0bb125d75 100644 --- a/infer/src/concurrency/starvation.ml +++ b/infer/src/concurrency/starvation.ml @@ -578,13 +578,15 @@ let get_summary_of_scheduled_work (work_item : Domain.ScheduledWorkItem.t) = (* given a summary, do [f work critical_pairs] for each [work] item scheduled in the summary, where [critical_pairs] are those of the method scheduled, adapted to the thread it's scheduled for *) -let iter_summary ~f (summary : Summary.t) = - let caller = Summary.get_proc_name summary in +let iter_summary ~f exe_env (summary : Summary.t) = let open Domain in Payloads.starvation summary.payloads - |> Option.iter ~f:(fun ({scheduled_work} : summary) -> + |> Option.iter ~f:(fun ({scheduled_work; critical_pairs} : summary) -> + let pname = Summary.get_proc_name summary in + let tenv = Exe_env.get_tenv exe_env pname in + if ConcurrencyModels.is_modeled_ui_method tenv pname then f pname critical_pairs ; ScheduledWorkDomain.iter - (fun work -> get_summary_of_scheduled_work work |> Option.iter ~f:(f caller)) + (fun work -> get_summary_of_scheduled_work work |> Option.iter ~f:(f pname)) scheduled_work ) @@ -636,7 +638,7 @@ let whole_program_analysis () = let work_set = WorkHashSet.create 1 in let exe_env = Exe_env.mk () in L.progress "Processing on-disk summaries...@." ; - SpecsFiles.iter ~f:(iter_summary ~f:(WorkHashSet.add_pairs work_set)) ; + SpecsFiles.iter ~f:(iter_summary exe_env ~f:(WorkHashSet.add_pairs work_set)) ; L.progress "Loaded %d pairs@." (WorkHashSet.length work_set) ; L.progress "Reporting on processed summaries...@." ; report exe_env work_set diff --git a/infer/tests/codetoanalyze/java/starvation-whole-program/MyActivity.java b/infer/tests/codetoanalyze/java/starvation-whole-program/MyActivity.java new file mode 100644 index 000000000..acba89adf --- /dev/null +++ b/infer/tests/codetoanalyze/java/starvation-whole-program/MyActivity.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import android.app.Activity; +import android.os.Binder; +import android.os.Bundle; +import android.os.RemoteException; +import java.util.concurrent.Executor; + +class MyActivity extends Activity { + Binder b; + + private void bad() { + try { + b.transact(0, null, null, 0); + } catch (RemoteException r) { + } + } + + // overrides so no Bad suffixes + + @Override + protected void onCreate(Bundle savedInstanceState) { + bad(); + } + + @Override + public void onStart() { + bad(); + } + + @Override + public void onRestart() { + bad(); + } + + Object monitorA; + @ForNonUiThread private final Executor mNonUiThreadExecutor = null; + + // method is a UI thread callback, and schedules a transaction in the background + // but it synchronises on the lock protecting the transaction, thus stalling the main thread + @Override + public void onStop() { + synchronized (monitorA) { + } + + mNonUiThreadExecutor.execute( + new Runnable() { + @Override + public void run() { + synchronized (monitorA) { + bad(); + } + } + }); + } + + Object monitorB, monitorC; + + // method is a UI thread callback and deadlocks with work scheduled in + // another callback (onPause) but which schedules work to a background thread + @Override + public void onDestroy() { + synchronized (monitorC) { + synchronized (monitorB) { + } + } + } + + @Override + public void onPause() { + mNonUiThreadExecutor.execute( + new Runnable() { + @Override + public void run() { + synchronized (monitorB) { + synchronized (monitorC) { + } + } + } + }); + } + + Object FP_monitorD, FP_monitorE; + + // False positive: by the time the work is scheduled, no lock is held, so no deadlock + // Locks are named FP_* so that the report is clearly an FP (we can't change the name of the + // override). + @Override + public void onResume() { + synchronized (FP_monitorD) { + synchronized (FP_monitorE) { + } + } + + mNonUiThreadExecutor.execute( + new Runnable() { + @Override + public void run() { + synchronized (FP_monitorE) { + synchronized (FP_monitorD) { + } + } + } + }); + } +} diff --git a/infer/tests/codetoanalyze/java/starvation-whole-program/issues.exp b/infer/tests/codetoanalyze/java/starvation-whole-program/issues.exp index 38ef75db1..d385fef9e 100644 --- a/infer/tests/codetoanalyze/java/starvation-whole-program/issues.exp +++ b/infer/tests/codetoanalyze/java/starvation-whole-program/issues.exp @@ -5,3 +5,11 @@ codetoanalyze/java/starvation-whole-program/Deadlock.java, Deadlock.postOnBGThre codetoanalyze/java/starvation-whole-program/DirectStarvation.java, DirectStarvation.postBlockingCallToUIThreadBad():void, 29, STARVATION, no_bucket, ERROR, [`void DirectStarvation.postBlockingCallToUIThreadBad()`,Method call: `void DirectStarvation$1.run()`,Method call: `void DirectStarvation.doTransact()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] codetoanalyze/java/starvation-whole-program/IndirectStarvation.java, IndirectStarvation.postBlockingCallToBackgroundThreadAndLockBad():void, 32, STARVATION, no_bucket, ERROR, [[Trace 1] `void IndirectStarvation.postBlockingCallToBackgroundThreadAndLockBad()`,Method call: `void IndirectStarvation$1.run()`, locks `this.monitorA` in `class IndirectStarvation`,[Trace 2] `void IndirectStarvation.postBlockingCallToBackgroundThreadAndLockBad()`,Method call: `void IndirectStarvation$2.run()`, locks `this.monitorA` in `class IndirectStarvation`,Method call: `void IndirectStarvation.doTransact()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] codetoanalyze/java/starvation-whole-program/ModeledExecutors.java, ModeledExecutors.postBlockingCallToUIThreadBad():void, 25, STARVATION, no_bucket, ERROR, [`void ModeledExecutors.postBlockingCallToUIThreadBad()`,Method call: `void ModeledExecutors$1.run()`,Method call: `void ModeledExecutors.doTransact()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onCreate(android.os.Bundle):void, 28, STARVATION, no_bucket, ERROR, [`void MyActivity.onCreate(Bundle)`,Method call: `void MyActivity.bad()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onDestroy():void, 68, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void MyActivity.onDestroy()`, locks `this.monitorC` in `class MyActivity`, locks `this.monitorB` in `class MyActivity`,[Trace 2] `void MyActivity.onPause()`,Method call: `void MyActivity$2.run()`, locks `this.monitorB` in `class MyActivity`, locks `this.monitorC` in `class MyActivity`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onPause():void, 76, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void MyActivity.onPause()`,Method call: `void MyActivity$2.run()`, locks `this.monitorB` in `class MyActivity`, locks `this.monitorC` in `class MyActivity`,[Trace 2] `void MyActivity.onDestroy()`, locks `this.monitorC` in `class MyActivity`, locks `this.monitorB` in `class MyActivity`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onRestart():void, 38, STARVATION, no_bucket, ERROR, [`void MyActivity.onRestart()`,Method call: `void MyActivity.bad()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onResume():void, 95, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void MyActivity.onResume()`, locks `this.FP_monitorD` in `class MyActivity`, locks `this.FP_monitorE` in `class MyActivity`,[Trace 2] `void MyActivity.onResume()`,Method call: `void MyActivity$3.run()`, locks `this.FP_monitorE` in `class MyActivity`, locks `this.FP_monitorD` in `class MyActivity`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onResume():void, 100, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void MyActivity.onResume()`,Method call: `void MyActivity$3.run()`, locks `this.FP_monitorE` in `class MyActivity`, locks `this.FP_monitorD` in `class MyActivity`,[Trace 2] `void MyActivity.onResume()`, locks `this.FP_monitorD` in `class MyActivity`, locks `this.FP_monitorE` in `class MyActivity`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onStart():void, 33, STARVATION, no_bucket, ERROR, [`void MyActivity.onStart()`,Method call: `void MyActivity.bad()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] +codetoanalyze/java/starvation-whole-program/MyActivity.java, MyActivity.onStop():void, 48, STARVATION, no_bucket, ERROR, [[Trace 1] `void MyActivity.onStop()`, locks `this.monitorA` in `class MyActivity`,[Trace 2] `void MyActivity.onStop()`,Method call: `void MyActivity$1.run()`, locks `this.monitorA` in `class MyActivity`,Method call: `void MyActivity.bad()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`]