diff --git a/infer/src/absint/ConcurrencyModels.ml b/infer/src/absint/ConcurrencyModels.ml index 5a4aaf0a185..e6caa6fda6a 100644 --- a/infer/src/absint/ConcurrencyModels.ml +++ b/infer/src/absint/ConcurrencyModels.ml @@ -12,6 +12,7 @@ type lock_effect = | Lock of HilExp.t list | Unlock of HilExp.t list | LockedIfTrue of HilExp.t list + | LockedIfZero of HilExp.t list | GuardConstruct of {guard: HilExp.t; lock: HilExp.t; acquire_now: bool} | GuardLock of HilExp.t | GuardLockedIfTrue of HilExp.t @@ -32,6 +33,8 @@ let make_unlock = make_lock_action "release" (fun a -> Unlock a) let make_trylock = make_lock_action "conditionally acquire" (fun a -> LockedIfTrue a) +let make_zero_trylock = make_lock_action "conditionally acquire" (fun a -> LockedIfZero a) + let make_guard_construct procname = function | [_guard] -> (* constructor is called without a mutex *) @@ -107,6 +110,7 @@ end = struct { classname: string [@default ""] ; lock: string list [@default []] ; trylock: string list [@default []] + ; trylock_zero: string list [@default []] (** trylocks that return zero on success *) ; unlock: string list [@default []] ; recursive: bool [@default true] } [@@deriving of_yojson] @@ -115,9 +119,14 @@ end = struct let lock_models = let def = - {classname= ""; lock= ["lock"]; trylock= ["try_lock"]; unlock= ["unlock"]; recursive= false} + { classname= "" + ; lock= ["lock"] + ; trylock= ["try_lock"] + ; trylock_zero= [] + ; unlock= ["unlock"] + ; recursive= false } in - let c_rec = {classname= ""; lock= []; trylock= []; unlock= []; recursive= true} in + let c_rec = {def with lock= []; trylock= []; unlock= []; recursive= true} in let shd = { def with lock= "lock_shared" :: def.lock @@ -132,6 +141,7 @@ end = struct in let config_locks = lock_model_cfg_of_yojson Config.lock_model in [ {c_rec with lock= ["pthread_mutex_lock"]; unlock= ["pthread_mutex_unlock"]} + ; {def with classname= "android::Mutex"; trylock= []; trylock_zero= ["timedLock"; "tryLock"]} ; { def with classname= "apache::thrift::concurrency::Monitor" ; trylock= "timedlock" :: def.trylock } @@ -170,7 +180,7 @@ end = struct fun pname -> QualifiedCppName.Match.match_qualifiers matcher (Procname.get_qualifiers pname) - let is_lock, is_unlock, is_trylock, is_std_lock = + let is_lock, is_unlock, is_trylock, is_zero_trylock, is_std_lock = (* TODO std::try_lock *) let mk_model_matcher ~f = let lock_methods = @@ -182,6 +192,7 @@ end = struct ( mk_model_matcher ~f:(fun mdl -> mdl.lock) , mk_model_matcher ~f:(fun mdl -> mdl.unlock) , mk_model_matcher ~f:(fun mdl -> mdl.trylock) + , mk_model_matcher ~f:(fun mdl -> mdl.trylock_zero) , mk_matcher ["std::lock"] ) @@ -191,6 +202,8 @@ end = struct let guards = (* TODO std::scoped_lock *) [ (* no lock/unlock *) + "android::Mutex::Autolock" + ; (* no lock/unlock *) "apache::thrift::concurrency::Guard" ; (* no lock/unlock *) "apache::thrift::concurrency::RWGuard" @@ -270,6 +283,7 @@ end = struct else if is_lock pname then make_lock pname fst_arg else if is_unlock pname then make_unlock pname fst_arg else if is_trylock pname then make_trylock pname fst_arg + else if is_zero_trylock pname then make_zero_trylock pname fst_arg else if is_guard_constructor pname then make_guard_construct pname actuals else if is_guard_lock pname then make_guard_lock pname actuals else if is_guard_unlock pname then make_guard_unlock pname actuals diff --git a/infer/src/absint/ConcurrencyModels.mli b/infer/src/absint/ConcurrencyModels.mli index faedf9b57f8..3ac139862c3 100644 --- a/infer/src/absint/ConcurrencyModels.mli +++ b/infer/src/absint/ConcurrencyModels.mli @@ -12,6 +12,8 @@ type lock_effect = | Lock of HilExp.t list (** simultaneously acquire a list of locks *) | Unlock of HilExp.t list (** simultaneously release a list of locks *) | LockedIfTrue of HilExp.t list (** simultaneously attempt to acquire a list of locks *) + | LockedIfZero of HilExp.t list + (** simultaneously attempt to acquire a list of locks, returning zero on success *) | GuardConstruct of {guard: HilExp.t; lock: HilExp.t; acquire_now: bool} (** mutex guard construction - clang only *) | GuardLock of HilExp.t (** lock underlying mutex via guard - clang only *) diff --git a/infer/src/absint/HilExp.ml b/infer/src/absint/HilExp.ml index b6e0b81e3e3..a531ea5be6a 100644 --- a/infer/src/absint/HilExp.ml +++ b/infer/src/absint/HilExp.ml @@ -575,6 +575,8 @@ and eval_boolean_exp var = function eval_boolean_binop Bool.equal var e1 e2 | BinaryOperator (Binop.Ne, e1, e2) -> eval_boolean_binop Bool.( <> ) var e1 e2 + | Cast (_, e) -> + eval_boolean_exp var e | _ -> (* non-boolean expression; can't evaluate it *) None diff --git a/infer/src/concurrency/AbstractAddress.ml b/infer/src/concurrency/AbstractAddress.ml index 9954db1d05b..0b645b9f49b 100644 --- a/infer/src/concurrency/AbstractAddress.ml +++ b/infer/src/concurrency/AbstractAddress.ml @@ -165,12 +165,15 @@ type t = | Parameter of {index: int; path: unrooted_path} (** method parameter represented by its 0-indexed position, root var is not used in comparison *) + | Opaque of {path: unrooted_path} + (** reached from a value the caller cannot name, eg returned by a call, root var is not used + in comparison *) [@@deriving compare, equal] let get_typ tenv = function | Class _ -> Some StdTyp.Java.pointer_to_java_lang_class - | Global {path} | Parameter {path} -> + | Global {path} | Parameter {path} | Opaque {path} -> get_typ tenv path @@ -204,7 +207,8 @@ let rec inner_class_normalise tenv ((typ, (accesses : access_list)) as path) = let equal_across_threads tenv t1 t2 = match (t1, t2) with - | Parameter {path= (_, typ1), accesses1}, Parameter {path= (_, typ2), accesses2} -> + | ( (Parameter {path= (_, typ1), accesses1} | Opaque {path= (_, typ1), accesses1}) + , (Parameter {path= (_, typ2), accesses2} | Opaque {path= (_, typ2), accesses2}) ) -> (* parameter position/names can be ignored across threads, if types and accesses are equal *) let path1 = inner_class_normalise tenv (typ1, accesses1) in let path2 = inner_class_normalise tenv (typ2, accesses2) in @@ -263,12 +267,15 @@ let pp fmt t = F.fprintf fmt "C{%s}" (Typ.Name.name typename) | Parameter {index; path} -> F.fprintf fmt "P<%i>{%a}" index pp_path path + | Opaque {path} -> + F.fprintf fmt "O{%a}" pp_path path let root_class = function | Class {typename} -> Some typename - | Global {path= (_, {desc}), _} | Parameter {path= (_, {desc}), _} -> ( + | Global {path= (_, {desc}), _} | Parameter {path= (_, {desc}), _} | Opaque {path= (_, {desc}), _} + -> ( match desc with | Tstruct typename | Tptr ({desc= Tstruct typename}, _) -> Some typename @@ -288,7 +295,7 @@ let describe fmt t = match t with | Class {typename} -> MF.wrap_monospaced describe_class_object fmt typename - | Global {path} | Parameter {path} -> + | Global {path} | Parameter {path} | Opaque {path} -> F.fprintf fmt "%a%a" (MF.wrap_monospaced describe_path) path describe_root t @@ -298,28 +305,44 @@ let pp_subst fmt subst = PrettyPrintable.pp_collection fmt ~pp_item:(Pp.option pp) (Array.to_list subst) +(* the address of a local is not opaque: objects on the stack of the caller are not shared *) +let rec make_opaque (hilexp : HilExp.t) = + match hilexp with + | AccessExpression access_exp -> ( + match HilExp.AccessExpression.to_accesses access_exp with + | HilExp.AccessExpression.Base base, accesses + when not (List.mem accesses MemoryAccess.TakeAddress ~equal:equal_access) -> + Some (Opaque {path= (base, accesses)}) + | _ -> + None ) + | Cast (_, hilexp) -> + make_opaque hilexp + | _ -> + None + + let make_subst formal_map actuals = - let actuals = Array.of_list actuals in - let len = - (* deal with var args functions *) - Int.max (FormalMap.cardinal formal_map) (Array.length actuals) - in - let subst = Array.create ~len None in - FormalMap.iter - (fun _base idx -> - if idx < Array.length actuals then subst.(idx) <- make formal_map actuals.(idx) ) - formal_map ; - subst + Array.of_list_map actuals ~f:(fun actual -> + match make formal_map actual with None -> make_opaque actual | address -> address ) + + +let without_opaque subst = + Array.map subst ~f:(function Some (Opaque _) -> None | address -> address) let apply_subst (subst : subst) t = match t with - | Global _ | Class _ -> + | Global _ | Class _ | Opaque _ -> Some t - | Parameter {index; path= _, []} -> ( + | Parameter {index; path= (var, _), []} -> ( try (* Special case for when the parameter is used without additional accesses, eg [x] as opposed to [x.f[].g]. *) - subst.(index) + match subst.(index) with + | Some (Opaque {path= (_, typ), accesses}) -> + (* print the parameter rather than the caller's temporary *) + Some (Opaque {path= ((var, typ), accesses)}) + | address -> + address with Invalid_argument _ -> None ) | Parameter {index; path} -> ( try @@ -338,4 +361,11 @@ let apply_subst (subst : subst) t = None ) | Some (Global global) -> ( match append ~on_to:global.path path with Some path -> Some (Global {path}) | None -> None ) + | Some (Opaque {path= (_, typ), accesses}) -> ( + let (var, _), _ = path in + match append ~on_to:((var, typ), accesses) path with + | Some path -> + Some (Opaque {path}) + | None -> + None ) with Invalid_argument _ -> None ) diff --git a/infer/src/concurrency/AbstractAddress.mli b/infer/src/concurrency/AbstractAddress.mli index adb270cb61d..e92017a24cc 100644 --- a/infer/src/concurrency/AbstractAddress.mli +++ b/infer/src/concurrency/AbstractAddress.mli @@ -14,19 +14,23 @@ module F = Format - rooted at formal parameters (these are identified by the parameter index and the path without the root variable, though that variable is kept for pretty printing); - rooted at global variables; - - non access-path expressions representing class objects (java only). + - non access-path expressions representing class objects (java only); + - rooted at values passed to a callee that the caller cannot name, eg returned by a call + (opaque). - Notably, there are no addresses rooted at locals (because proving aliasing between those is - difficult). + Notably, there are no addresses rooted at the address of a local (because proving aliasing + between those is difficult). There are two notions of equality: - Equality for comparing two addresses within the same thread/process/trace. Under this, identical globals and identical class objects compare equal. Parameter-rooted paths compare - equal if their parameter indices, types and lists of accesses are equal. + equal if their parameter indices, types and lists of accesses are equal. Opaque paths compare + equal if their types and lists of accesses are equal. - Equality for comparing two addresses in two distinct threads/traces. Globals and class objects - are compared in the same way, but parameter-rooted paths need only have equal access lists (ie - [x.f.g == y.f.g]). This allows demonically aliasing parameters in *distinct* threads. *) + are compared in the same way, but parameter-rooted and opaque paths need only have equal + access lists (ie [x.f.g == y.f.g]). This allows demonically aliasing parameters in *distinct* + threads. *) include PrettyPrintable.PrintableOrderedType @@ -50,11 +54,16 @@ val is_class_object : t -> bool [static synchronized void foo()] *) (** A substitution from formal position indices to address options. [None] is used to for actuals - that cannot be resolved to an address (eg local-rooted paths or arithmetic expressions). *) + that cannot be resolved to an address (eg addresses of locals or arithmetic expressions). *) type subst val pp_subst : F.formatter -> subst -> unit [@@warning "-unused-value-declaration"] val make_subst : FormalMap.t -> HilExp.t list -> subst +(** [make_subst formals actuals] maps the position of each actual to its address in terms of the + caller's [formals] *) + +val without_opaque : subst -> subst +(** maps the actuals that only have an opaque address to [None] *) val apply_subst : subst -> t -> t option diff --git a/infer/src/concurrency/RacerDDomain.ml b/infer/src/concurrency/RacerDDomain.ml index 000ec24805a..f50140c0e3a 100644 --- a/infer/src/concurrency/RacerDDomain.ml +++ b/infer/src/concurrency/RacerDDomain.ml @@ -449,7 +449,8 @@ module OwnershipDomain = struct end module Attribute = struct - type t = Nothing | Functional | OnMainThread | LockHeld | Synchronized [@@deriving equal] + type t = Nothing | Functional | OnMainThread | LockHeld | LockHeldIfZero | Synchronized + [@@deriving equal] let pp fmt t = ( match t with @@ -461,6 +462,8 @@ module Attribute = struct "OnMainThread" | LockHeld -> "LockHeld" + | LockHeldIfZero -> + "LockHeldIfZero" | Synchronized -> "Synchronized" ) |> F.pp_print_string fmt @@ -733,7 +736,12 @@ let release_lock (astate : t) = ; threads= ThreadsDomain.update_for_lock_use astate.threads } -let lock_if_true ret_access_exp (astate : t) = +let add_lock_attribute attribute ret_access_exp (astate : t) = { astate with - attribute_map= AttributeMapDomain.add ret_access_exp Attribute.LockHeld astate.attribute_map + attribute_map= AttributeMapDomain.add ret_access_exp attribute astate.attribute_map ; threads= ThreadsDomain.update_for_lock_use astate.threads } + + +let lock_if_true = add_lock_attribute Attribute.LockHeld + +let lock_if_zero = add_lock_attribute Attribute.LockHeldIfZero diff --git a/infer/src/concurrency/RacerDDomain.mli b/infer/src/concurrency/RacerDDomain.mli index 9a888a46517..d10b6a07ee0 100644 --- a/infer/src/concurrency/RacerDDomain.mli +++ b/infer/src/concurrency/RacerDDomain.mli @@ -115,6 +115,7 @@ module Attribute : sig | Functional (** holds a value returned from a callee marked [@Functional] *) | OnMainThread (** boolean is true if the current procedure is running on the main thread *) | LockHeld (** boolean is true if a lock is currently held *) + | LockHeldIfZero (** value is zero if a lock is currently held *) | Synchronized (** the object is a synchronized data structure *) end @@ -201,4 +202,6 @@ val release_lock : t -> t val lock_if_true : HilExp.access_expression -> t -> t +val lock_if_zero : HilExp.access_expression -> t -> t + val branch_never_returns : unit -> t diff --git a/infer/src/concurrency/RacerDProcAnalysis.ml b/infer/src/concurrency/RacerDProcAnalysis.ml index 6ac5d2076f5..0636abc5590 100644 --- a/infer/src/concurrency/RacerDProcAnalysis.ml +++ b/infer/src/concurrency/RacerDProcAnalysis.ml @@ -91,6 +91,8 @@ module TransferFunctions (CFG : ProcCfg.S) = struct Domain.release_lock astate | LockedIfTrue _ | GuardLockedIfTrue _ -> Domain.lock_if_true ret_access_exp astate + | LockedIfZero _ -> + Domain.lock_if_zero ret_access_exp astate | GuardConstruct {acquire_now= false} -> astate | NoEffect when RacerDModels.proc_is_ignored_by_racerd callee_pname -> @@ -166,13 +168,15 @@ module TransferFunctions (CFG : ProcCfg.S) = struct let do_assume formals assume_exp loc tenv (astate : Domain.t) = let open Domain in - let apply_choice bool_value (acc : Domain.t) = function + let rec apply_choice bool_value (acc : Domain.t) = function | Attribute.LockHeld -> let locks = if bool_value then LockDomain.acquire_lock acc.locks else LockDomain.release_lock acc.locks in {acc with locks} + | Attribute.LockHeldIfZero -> + apply_choice (not bool_value) acc Attribute.LockHeld | Attribute.OnMainThread -> let threads = if bool_value then ThreadsDomain.AnyThreadButSelf else ThreadsDomain.AnyThread @@ -181,15 +185,40 @@ module TransferFunctions (CFG : ProcCfg.S) = struct | Attribute.(Functional | Nothing | Synchronized) -> acc in + (* trylocks returning zero on success return negative error codes, so [r < 0] means [r != 0] *) + let rec zero_status_as_equality (exp : HilExp.t) : HilExp.t = + match exp with + | BinaryOperator (Lt, e1, e2) when HilExp.is_int_zero e2 -> + BinaryOperator (Ne, e1, e2) + | BinaryOperator (Ge, e1, e2) when HilExp.is_int_zero e2 -> + BinaryOperator (Eq, e1, e2) + | BinaryOperator (Gt, e1, e2) when HilExp.is_int_zero e1 -> + BinaryOperator (Ne, e1, e2) + | BinaryOperator (Le, e1, e2) when HilExp.is_int_zero e1 -> + BinaryOperator (Eq, e1, e2) + | UnaryOperator (LNot, e, typ) -> + UnaryOperator (LNot, zero_status_as_equality e, typ) + | Cast (typ, e) -> + Cast (typ, zero_status_as_equality e) + | _ -> + exp + in let astate = add_access tenv formals loc ~is_write:false astate assume_exp in match HilExp.get_access_exprs assume_exp with | [access_expr] -> + let attribute = AttributeMapDomain.get access_expr astate.attribute_map in + let assume_exp = + match attribute with + | Attribute.LockHeldIfZero -> + zero_status_as_equality assume_exp + | _ -> + assume_exp + in HilExp.eval_boolean_exp access_expr assume_exp |> Option.value_map ~default:astate ~f:(fun bool_value -> (* prune (prune_exp) can only evaluate to true if the choice is [bool_value]. add the constraint that the choice must be [bool_value] to the state *) - AttributeMapDomain.get access_expr astate.attribute_map - |> apply_choice bool_value astate ) + apply_choice bool_value astate attribute ) | _ -> astate diff --git a/infer/src/concurrency/starvation.ml b/infer/src/concurrency/starvation.ml index c0b479f11b3..60eb93d9e81 100644 --- a/infer/src/concurrency/starvation.ml +++ b/infer/src/concurrency/starvation.ml @@ -172,7 +172,8 @@ module TransferFunctions (CFG : ProcCfg.S) = struct else Domain.set_non_null formals lhs_access_exp astate - let do_call {interproc= {proc_desc; tenv; analyze_dependency}; formals} lhs callee actuals loc + let do_call ?(ignore_lock_state = false) ?release_held_locks + {interproc= {proc_desc; tenv; analyze_dependency}; formals} lhs callee actuals loc (astate : Domain.t) = let open Domain in let procname = Procdesc.get_proc_name proc_desc in @@ -205,7 +206,11 @@ module TransferFunctions (CFG : ProcCfg.S) = struct Some (make_ret_attr (Looper ForUIThread)) else None in - let get_callee_summary () = analyze_dependency callee |> AnalysisResult.to_option in + let get_callee_summary () = + analyze_dependency callee |> AnalysisResult.to_option + |> Option.map ~f:(fun (summary : summary) -> + if ignore_lock_state then {summary with lock_state= LockState.top} else summary ) + in let treat_handler_constructor () = if StarvationModels.is_handler_constructor tenv callee actuals then match actuals_acc_exps with @@ -269,7 +274,8 @@ module TransferFunctions (CFG : ProcCfg.S) = struct |> Option.map ~f:(fun summary -> let subst = Lock.make_subst formals actuals in let callsite = CallSite.make callee loc in - Domain.integrate_summary ~tenv ~procname ~lhs ~subst formals callsite astate summary ) + Domain.integrate_summary ?release_held_locks ~tenv ~procname ~lhs ~subst formals callsite + astate summary ) in IList.eval_until_first_some [ treat_handler_constructor @@ -280,6 +286,99 @@ module TransferFunctions (CFG : ProcCfg.S) = struct |> Option.value ~default:astate + let is_cpp_constructor = function + | Procname.ObjC_Cpp {kind= CPPConstructor _} -> + true + | _ -> + false + + + let is_rooted_at_formal_or_global formals acc_exp = + let ((var, _) as base) = HilExp.AccessExpression.get_base acc_exp in + Var.is_global var || FormalMap.is_formal base formals + + + (** Like [do_call], but infers scoped guards from summaries. A constructor called on a local + object, or a call returning one by value, that leaves exactly one lock held which the caller + can express makes the object a guard of that lock if its destructor releases exactly one lock. + Other locks left held by constructing an object not rooted at a formal or a global are + released at once, as their release through the object cannot be expressed. A method called on + a guard that releases (or acquires) exactly one lock the caller cannot express, ie one reached + through the guard, unlocks (or locks) the guard. *) + let do_call_with_inferred_guards + ({interproc= {proc_desc; tenv; analyze_dependency}; formals} as analysis_data) + ~assign_last_arg lhs callee actuals loc (astate : Domain.t) = + let procname = Procdesc.get_proc_name proc_desc in + let get_lock_state pname = + analyze_dependency pname |> AnalysisResult.to_option + |> Option.map ~f:(fun (summary : Domain.summary) -> summary.lock_state) + in + let lock_in_caller lock = Domain.Lock.(apply_subst (make_subst formals actuals) lock) in + let destructor_releases_one_lock (obj : HilExp.t) = + match obj with + | AccessExpression + (AddressOf (Base (_, {Typ.desc= Tstruct name | Tptr ({desc= Tstruct name}, _)}))) -> + Tenv.lookup tenv name + |> Option.bind ~f:(fun ({methods} : Struct.t) -> + List.find_map methods ~f:(fun meth -> + let pname = Struct.name_of_tenv_method meth in + Option.some_if (Procname.is_destructor pname) pname ) ) + |> Option.bind ~f:get_lock_state + |> Option.exists ~f:(fun lock_state -> + Option.is_some (Domain.LockState.get_single_unlocked_lock lock_state) ) + | _ -> + false + in + let is_constructor = is_cpp_constructor callee in + let initialised_object = + if is_constructor then List.hd actuals + else if assign_last_arg then List.last actuals + else None + in + match (callee, actuals) with + | Procname.ObjC_Cpp {kind= CPPDestructor _}, guard :: _ when Domain.is_guard astate guard -> + let astate = do_call ~ignore_lock_state:true analysis_data lhs callee actuals loc astate in + Domain.remove_guard astate guard + | Procname.ObjC_Cpp {kind= CPPMethod _}, guard :: _ when Domain.is_guard astate guard -> + let astate = do_call analysis_data lhs callee actuals loc astate in + let lock_state = get_lock_state callee in + let has_single_guard_lock get_single_lock = + Option.bind lock_state ~f:get_single_lock + |> Option.exists ~f:(fun lock -> Option.is_none (lock_in_caller lock)) + in + if has_single_guard_lock Domain.LockState.get_single_unlocked_lock then + Domain.unlock_guard astate guard + else if has_single_guard_lock Domain.LockState.get_single_held_lock then + Domain.lock_guard ~procname ~loc tenv astate guard + else astate + | _ -> ( + match initialised_object with + | Some obj + when not (get_access_expr obj |> Option.exists ~f:(is_rooted_at_formal_or_global formals)) + -> ( + let guard_lock = + match (obj : HilExp.t) with + | AccessExpression (AddressOf (Base (ProgramVar _, _))) -> + get_lock_state callee + |> Option.bind ~f:Domain.LockState.get_single_held_lock + |> Option.bind ~f:lock_in_caller + |> Option.filter ~f:(fun _ -> destructor_releases_one_lock obj) + | _ -> + None + in + match guard_lock with + | Some lock -> + let astate = + do_call ~ignore_lock_state:true analysis_data lhs callee actuals loc astate + in + Domain.add_guard ~acquire_now:true ~procname ~loc tenv astate obj lock + | None -> + do_call ~release_held_locks:is_constructor analysis_data lhs callee actuals loc astate + ) + | _ -> + do_call analysis_data lhs callee actuals loc astate ) + + let do_metadata (metadata : Sil.instr_metadata) astate = match metadata with ExitScope (vars, _) -> Domain.remove_dead_vars astate vars | _ -> astate @@ -287,7 +386,14 @@ module TransferFunctions (CFG : ProcCfg.S) = struct let do_load tenv formals ~lhs rhs_exp rhs_typ (astate : Domain.t) = let lhs_var = fst lhs in let add_deref = match (lhs_var : Var.t) with LogicalVar _ -> true | ProgramVar _ -> false in - let rhs_hil_exp = hilexp_of_sil ~add_deref astate rhs_exp rhs_typ in + let rhs_hil_exp = + match hilexp_of_sil ~add_deref astate rhs_exp rhs_typ with + | AccessExpression acc_exp as hil_exp -> + Domain.FieldAliases.get acc_exp astate.field_aliases + |> Option.value_map ~default:hil_exp ~f:(fun alias -> HilExp.AccessExpression alias) + | hil_exp -> + hil_exp + in let astate = get_access_expr_or_const rhs_hil_exp |> Option.value_map ~default:astate ~f:(fun acc_exp -> @@ -297,6 +403,46 @@ module TransferFunctions (CFG : ProcCfg.S) = struct do_assignment tenv formals lhs_hil_acc_exp rhs_hil_exp astate + let do_field_store formals typ lhs_acc_exp rhs_exp (astate : Domain.t) = + let stored_pointer = + match (lhs_acc_exp : HilExp.AccessExpression.t) with + | FieldOffset _ when Typ.is_pointer typ && is_rooted_at_formal_or_global formals lhs_acc_exp + -> + get_access_expr rhs_exp |> Option.filter ~f:(is_rooted_at_formal_or_global formals) + | _ -> + None + in + { astate with + field_aliases= Domain.FieldAliases.assign lhs_acc_exp stored_pointer astate.field_aliases } + + + (** the callee may store into the memory reachable from its actuals, eg [this->mutex_] through + [this] or through another pointer to the same object *) + let forget_field_aliases_reachable_from tenv actuals (astate : Domain.t) = + let forget_fields_of_pointee field_aliases actual = + match + get_access_expr actual + |> Option.bind ~f:(fun exp -> HilExp.AccessExpression.get_typ exp tenv) + with + | Some {Typ.desc= Tptr ({desc= Tstruct name}, _)} -> + Domain.FieldAliases.forget_fields field_aliases ~f:(fun field -> + Tenv.mem_supers tenv name ~f:(fun super _ -> + Typ.Name.equal super (Fieldname.get_class_name field) ) ) + | _ -> + field_aliases + in + let field_aliases = + List.fold actuals ~init:astate.field_aliases ~f:(fun field_aliases actual -> + let field_aliases = forget_fields_of_pointee field_aliases actual in + get_access_expr actual + |> Option.bind ~f:(fun acc_exp -> + HilExp.AccessExpression.add_access acc_exp MemoryAccess.Dereference ) + |> Option.value_map ~default:field_aliases ~f:(fun pointee -> + Domain.FieldAliases.assign pointee None field_aliases ) ) + in + {astate with field_aliases} + + let do_cast tenv formals id base_typ actuals astate = match actuals with | [(e, typ); _sizeof] -> @@ -369,6 +515,11 @@ module TransferFunctions (CFG : ProcCfg.S) = struct hilexp_of_sil ~add_deref:true astate e1 (Typ.mk_ptr typ) |> get_access_expr |> Option.value_map ~default:astate ~f:(fun lhs_hil_acc_exp -> + let astate = + if is_cpp_constructor procname then + do_field_store formals typ lhs_hil_acc_exp rhs_hil_exp astate + else astate + in do_assignment tenv formals lhs_hil_acc_exp rhs_hil_exp astate ) | Call (_, Const (Cfun callee), actuals, _, _) when should_skip_analysis tenv callee (hilexp_of_sils ~add_deref:false astate actuals) -> @@ -379,9 +530,10 @@ module TransferFunctions (CFG : ProcCfg.S) = struct | Call ((id, typ), Const (Cfun callee), fn_ptr :: fn_args, loc, _) when Procname.equal callee BuiltinDecl.__call_c_function_ptr -> do_function_pointer_call analysis_data loc id typ fn_ptr fn_args astate - | Call ((id, typ), Const (Cfun callee), sil_actuals, loc, _) -> ( + | Call ((id, typ), Const (Cfun callee), sil_actuals, loc, {CallFlags.cf_assign_last_arg}) -> ( let ret_base = (Var.of_id id, typ) in let actuals = hilexp_of_sils ~add_deref:false astate sil_actuals in + let astate = forget_field_aliases_reachable_from tenv actuals astate in match get_lock_effect callee actuals with | Lock locks -> do_lock locks loc astate @@ -400,7 +552,7 @@ module TransferFunctions (CFG : ProcCfg.S) = struct Domain.unlock_guard astate guard | GuardDestroy guard -> Domain.remove_guard astate guard - | LockedIfTrue _ | GuardLockedIfTrue _ -> + | LockedIfTrue _ | LockedIfZero _ | GuardLockedIfTrue _ -> astate | NoEffect when is_synchronized_library_call tenv callee -> (* model a synchronized call without visible internal behaviour *) @@ -422,7 +574,8 @@ module TransferFunctions (CFG : ProcCfg.S) = struct | NoEffect -> (* in C++/Obj C we only care about deadlocks, not starvation errors *) let ret_exp = HilExp.AccessExpression.base ret_base in - do_call analysis_data ret_exp callee actuals loc astate ) + do_call_with_inferred_guards analysis_data ~assign_last_arg:cf_assign_last_arg ret_exp + callee actuals loc astate ) | Call ((id, _), _, _, _, _) -> (* call havocs LHS *) Domain.remove_dead_vars astate [Var.of_id id] diff --git a/infer/src/concurrency/starvationDomain.ml b/infer/src/concurrency/starvationDomain.ml index 9f42f100b84..c4748a97fb7 100644 --- a/infer/src/concurrency/starvationDomain.ml +++ b/infer/src/concurrency/starvationDomain.ml @@ -208,6 +208,53 @@ module VarDomain = struct let set var acc_exp astate = add var (NonTop acc_exp) astate end +module FieldAliases = struct + module Value = AbstractDomain.Flat (HilExp.AccessExpression) + include AbstractDomain.SafeInvertedMap (HilExp.AccessExpression) (Value) + + let rec has_prefix ~prefix exp = + HilExp.AccessExpression.equal prefix exp + || HilExp.AccessExpression.truncate exp + |> Option.exists ~f:(fun (exp, _) -> has_prefix ~prefix exp) + + + let rec mentions_field ~f (exp : HilExp.AccessExpression.t) = + match exp with + | Base _ -> + false + | FieldOffset (prefix, field) -> + f field || mentions_field ~f prefix + | ArrayOffset (prefix, _, _) | AddressOf prefix | Dereference prefix -> + mentions_field ~f prefix + + + let get exp astate = find_opt exp astate |> Option.bind ~f:Value.get + + let forget ~f astate = + filter (fun exp value -> not (f exp || Value.get value |> Option.exists ~f)) astate + + + let forget_fields ~f astate = forget ~f:(mentions_field ~f) astate + + let assign lhs rhs_opt astate = + let is_overwritten exp = + has_prefix ~prefix:lhs exp + || + (* the store may be through another pointer to the same object, eg a copy of [this] *) + match (lhs : HilExp.AccessExpression.t) with + | FieldOffset (_, field) -> + mentions_field ~f:(Fieldname.equal field) exp + | _ -> + false + in + let astate = forget ~f:is_overwritten astate in + match rhs_opt with + | Some rhs when not (is_overwritten rhs) -> + add lhs (Value.v rhs) astate + | _ -> + astate +end + module Event = struct type t = | Ipc of {callee: Procname.t; thread: ThreadDomain.t} @@ -336,17 +383,14 @@ module Event = struct Some event | Some lock -> Some (MonitorWait {lock; thread}) ) - | LockAcquire {locks; thread; callsite= None; call_context= []} -> ( + | LockAcquire ({locks} as lock_acquire) -> ( match Lock.apply_subst_to_list subst locks with | [] -> None | locks' when phys_equal locks locks' -> Some event | locks -> - Some (LockAcquire {locks; thread; callsite= None; call_context= []}) ) - (* Don't do substitution if inter procedural lock acquire *) - | LockAcquire {callsite= _; call_context= _} -> - Some event + Some (LockAcquire {lock_acquire with locks}) ) let has_recursive_lock tenv event = @@ -449,6 +493,14 @@ module LockState : sig val release : Lock.t -> t -> t val get_acquisitions : t -> Acquisitions.t + + val forget_held : t -> t + + val get_single_held_lock : t -> Lock.t option + + val get_single_unlocked_lock : t -> Lock.t option + + val get_unlocked : t -> Lock.t list end = struct (* abstraction limit for lock counts *) let max_lock_depth_allowed = 5 @@ -472,6 +524,26 @@ end = struct let get_acquisitions {acquisitions} = acquisitions + let forget_held lock_state = {lock_state with held= HeldMap.top; acquisitions= Acquisitions.empty} + + let get_single_held_lock {held; unlocked} = + match HeldMap.bindings held with + | [(lock, count)] when UnlockedMap.is_empty unlocked && Int.equal (count :> int) 1 -> + Some lock + | _ -> + None + + + let get_single_unlocked_lock {held; unlocked} = + match UnlockedMap.bindings unlocked with + | [(lock, count)] when HeldMap.is_empty held && Int.equal (count :> int) 1 -> + Some lock + | _ -> + None + + + let get_unlocked {unlocked} = UnlockedMap.bindings unlocked |> List.map ~f:fst + let pp fmt {held; unlocked; acquisitions} = F.fprintf fmt "{@[map= %a;@;unlocked= %a;@;acquisitions= %a@]}" HeldMap.pp held UnlockedMap.pp unlocked Acquisitions.pp acquisitions @@ -570,42 +642,46 @@ end = struct let integrate_summary ~procname ~callsite ~subst ~other lock_state = (* Release all locks that were unlocked by summary *) - let subst_lock lock = - match Lock.apply_subst subst lock with Some lock' -> lock' | None -> lock - in let rec unlocked_folder lock other_count lock_state = if UnlockCount.is_bottom other_count then lock_state - else - unlocked_folder lock - (UnlockCount.decrement other_count) - (release (subst_lock lock) lock_state) + else unlocked_folder lock (UnlockCount.decrement other_count) (release lock lock_state) + in + (* callee locks that cannot be expressed in the caller, eg locks of local objects, are ignored + here and below: their paths are relative to the callee's formals *) + let lock_state = + UnlockedMap.fold + (fun lock other_count lock_state -> + Lock.apply_subst subst lock + |> Option.value_map ~default:lock_state ~f:(fun lock -> + unlocked_folder lock other_count lock_state ) ) + other.unlocked lock_state in - let lock_state = UnlockedMap.fold unlocked_folder other.unlocked lock_state in (* acquire all locks that are held in the summary, adding the callsite to the acquisitions *) - let rec held_folder lock other_count lock_state = + let rec held_folder (acquisition : Acquisition.t) other_count lock_state = if LockCount.is_top other_count then lock_state else - let acquisition = Acquisitions.find (Acquisition.make_dummy lock) other.acquisitions in - let acquisition = Acquisition.with_callsite_at_proc ~procname acquisition callsite in - let acquisition = - match Acquisition.apply_subst subst acquisition with - | None -> - acquisition - | Some acq -> - acq - in - let lock_state = acquire' acquisition (subst_lock lock) lock_state in - held_folder lock (LockCount.decrement other_count) lock_state + let lock_state = acquire' acquisition acquisition.elem.lock lock_state in + held_folder acquisition (LockCount.decrement other_count) lock_state in - let lock_state = HeldMap.fold held_folder other.held lock_state in - lock_state + HeldMap.fold + (fun lock other_count lock_state -> + Acquisitions.find (Acquisition.make_dummy lock) other.acquisitions + |> Acquisition.apply_subst subst + |> Option.value_map ~default:lock_state ~f:(fun acquisition -> + let acquisition = Acquisition.with_callsite_at_proc ~procname acquisition callsite in + held_folder acquisition other_count lock_state ) ) + other.held lock_state end module CriticalPairElement = struct - type t = {acquisitions: Acquisitions.t; event: Event.t} [@@deriving compare] + type t = {acquisitions: Acquisitions.t; event: Event.t; released: Lock.t list} + [@@deriving compare] - let pp fmt {acquisitions; event} = - F.fprintf fmt "{@[acquisitions= %a;@;event= %a@]}" Acquisitions.pp acquisitions Event.pp event + let pp fmt {acquisitions; event; released} = + F.fprintf fmt "{@[acquisitions= %a;@;event= %a" Acquisitions.pp acquisitions Event.pp event ; + if not (List.is_empty released) then + F.fprintf fmt ";@;released= %a" (Pp.semicolon_seq Lock.pp) released ; + F.fprintf fmt "@]}" let describe = pp @@ -613,12 +689,19 @@ module CriticalPairElement = struct let get_thread {event} = Event.get_thread event let apply_subst subst elem = - match Event.apply_subst subst elem.event with + (* locks of objects the caller cannot name, often fresh or thread-local ones, are only kept + when a callee leaves them held *) + let without_opaque = Lock.without_opaque subst in + let event_subst = + match elem.event with LockAcquire {callsite= Some _} -> subst | _ -> without_opaque + in + match Event.apply_subst event_subst elem.event with | None -> None | Some event -> - let acquisitions = Acquisitions.apply_subst subst elem.acquisitions in - Some {acquisitions; event} + let acquisitions = Acquisitions.apply_subst without_opaque elem.acquisitions in + let released = Lock.apply_subst_to_list without_opaque elem.released in + Some {acquisitions; event; released} let is_blocking_call elt = Event.is_blocking_call elt.event @@ -630,7 +713,7 @@ module CriticalPair = struct (CriticalPairElement) (ExplicitTrace.DefaultCallPrinter) - let make ~loc acquisitions event = make {acquisitions; event} loc + let make ~loc ~released acquisitions event = make {acquisitions; event; released} loc let get_thread {elem} = CriticalPairElement.get_thread elem @@ -699,18 +782,26 @@ module CriticalPair = struct let is_blocking_call pair = CriticalPairElement.is_blocking_call pair.elem - let integrate_summary_opt ~subst ~tenv ~ignore_blocking_calls existing_acquisitions call_site + let integrate_summary_opt ~subst ~tenv ~ignore_blocking_calls lock_state call_site (caller_thread : ThreadDomain.t) (callee_pair : t) = if ignore_blocking_calls && is_blocking_call callee_pair then None else apply_subst subst callee_pair - |> Option.bind ~f:(filter_out_reentrant_relocks (Some tenv) existing_acquisitions) - |> Option.bind ~f:(apply_caller_thread caller_thread) - |> Option.map ~f:(fun callee_pair -> - let f (elem : CriticalPairElement.t) = - {elem with acquisitions= Acquisitions.union existing_acquisitions elem.acquisitions} + |> Option.bind ~f:(fun callee_pair -> + let lock_state = + List.fold callee_pair.elem.released ~init:lock_state ~f:(fun acc lock -> + LockState.release lock acc ) in - map ~f callee_pair ) + let existing_acquisitions = LockState.get_acquisitions lock_state in + filter_out_reentrant_relocks (Some tenv) existing_acquisitions callee_pair + |> Option.bind ~f:(apply_caller_thread caller_thread) + |> Option.map ~f:(fun callee_pair -> + let f (elem : CriticalPairElement.t) = + { elem with + acquisitions= Acquisitions.union existing_acquisitions elem.acquisitions + ; released= LockState.get_unlocked lock_state } + in + map ~f callee_pair ) ) |> Option.map ~f:(fun callee_pair -> with_callsite callee_pair call_site) @@ -801,13 +892,10 @@ module NullLocsCriticalPairs = struct let with_callsite astate ~tenv ~subst ~ignore_blocking_calls lock_state null_locs call_site thread = - let existing_acquisitions = LockState.get_acquisitions lock_state in CriticalPairs.fold (fun pair acc -> - CriticalPair.integrate_summary_opt ~subst ~tenv ~ignore_blocking_calls existing_acquisitions - call_site thread pair - |> Option.bind - ~f:(CriticalPair.filter_out_reentrant_relocks (Some tenv) existing_acquisitions) + CriticalPair.integrate_summary_opt ~subst ~tenv ~ignore_blocking_calls lock_state call_site + thread pair |> Option.value_map ~default:acc ~f:(fun pair -> add {null_locs; pair} acc) ) astate empty @@ -925,6 +1013,7 @@ type t = ; thread: ThreadDomain.t ; scheduled_work: ScheduledWorkDomain.t ; var_state: VarDomain.t + ; field_aliases: FieldAliases.t ; null_locs: NullLocs.t ; lazily_initalized: LazilyInitialized.t } [@@deriving abstract_domain] @@ -938,6 +1027,7 @@ let initial = ; thread= ThreadDomain.bottom ; scheduled_work= ScheduledWorkDomain.bottom ; var_state= VarDomain.top + ; field_aliases= FieldAliases.top ; null_locs= NullLocs.empty ; lazily_initalized= LazilyInitialized.empty } @@ -951,39 +1041,44 @@ let pp fmt astate = thread= %a;@;\ scheduled_work= %a;@;\ var_state= %a;@;\ + field_aliases= %a;@;\ null_locs= %a\n\ lazily_initialized= %a\n\ \ @]}" GuardToLockMap.pp astate.guard_map LockState.pp astate.lock_state NullLocsCriticalPairs.pp astate.critical_pairs AttributeDomain.pp astate.attributes ThreadDomain.pp astate.thread - ScheduledWorkDomain.pp astate.scheduled_work VarDomain.pp astate.var_state NullLocs.pp - astate.null_locs LazilyInitialized.pp astate.lazily_initalized + ScheduledWorkDomain.pp astate.scheduled_work VarDomain.pp astate.var_state FieldAliases.pp + astate.field_aliases NullLocs.pp astate.null_locs LazilyInitialized.pp astate.lazily_initalized let add_critical_pair ~tenv_opt lock_state null_locs event ~loc acc = let acquisitions = LockState.get_acquisitions lock_state in - let critical_pair = CriticalPair.make ~loc acquisitions event in + let released = LockState.get_unlocked lock_state in + let critical_pair = CriticalPair.make ~loc ~released acquisitions event in CriticalPair.filter_out_reentrant_relocks tenv_opt acquisitions critical_pair |> Option.value_map ~default:acc ~f:(fun pair -> NullLocsCriticalPairs.add {null_locs; pair} acc) -let interproc_acquire ~tenv ~procname ~callsite ~subst summary_lock_state astate = +let interproc_acquire ~tenv ~procname ~callsite ~subst ~release_held_locks summary_lock_state astate + = let new_acquisitions = LockState.get_acquisitions summary_lock_state in let critical_pairs = Acquisitions.fold (fun acquisition acc -> - let loc = CallSite.loc callsite in - let event = - let lock = - Lock.apply_subst subst acquisition.elem.lock - |> Option.value ~default:acquisition.elem.lock - in - Event.make_interprocedural_acquire callsite [lock] astate.thread - (Acquisition.make_loc_trace acquisition) - in - add_critical_pair ~tenv_opt:(Some tenv) astate.lock_state astate.null_locs event ~loc acc ) + Lock.apply_subst subst acquisition.elem.lock + |> Option.value_map ~default:acc ~f:(fun lock -> + let loc = CallSite.loc callsite in + let event = + Event.make_interprocedural_acquire callsite [lock] astate.thread + (Acquisition.make_loc_trace acquisition) + in + add_critical_pair ~tenv_opt:(Some tenv) astate.lock_state astate.null_locs event ~loc + acc ) ) new_acquisitions astate.critical_pairs in + let summary_lock_state = + if release_held_locks then LockState.forget_held summary_lock_state else summary_lock_state + in let lock_state = LockState.integrate_summary ~procname ~callsite ~subst ~other:summary_lock_state astate.lock_state @@ -1077,6 +1172,8 @@ let remove_guard astate guard = {astate with guard_map= GuardToLockMap.remove_guard astate.guard_map guard} ) +let is_guard astate guard = GuardToLockMap.mem guard astate.guard_map + let unlock_guard astate guard = GuardToLockMap.find_opt guard astate.guard_map |> Option.value_map ~default:astate ~f:(fun lock_opt -> @@ -1148,14 +1245,16 @@ let set_non_null formals acc_exp astate = else astate -let integrate_summary ~tenv ~procname ~lhs ~subst formals callsite (astate : t) (summary : summary) - = +let integrate_summary ?(release_held_locks = false) ~tenv ~procname ~lhs ~subst formals callsite + (astate : t) (summary : summary) = let critical_pairs' = NullLocsCriticalPairs.with_callsite summary.critical_pairs ~tenv ~subst astate.lock_state astate.null_locs callsite astate.thread ~ignore_blocking_calls:astate.ignore_blocking_calls in (* apply summary held locks *) - let astate = interproc_acquire ~procname ~callsite ~subst ~tenv summary.lock_state astate in + let astate = + interproc_acquire ~procname ~callsite ~subst ~tenv ~release_held_locks summary.lock_state astate + in let astate = { astate with critical_pairs= NullLocsCriticalPairs.join astate.critical_pairs critical_pairs' diff --git a/infer/src/concurrency/starvationDomain.mli b/infer/src/concurrency/starvationDomain.mli index add654de70c..366f2d8362f 100644 --- a/infer/src/concurrency/starvationDomain.mli +++ b/infer/src/concurrency/starvationDomain.mli @@ -51,6 +51,22 @@ module AccessExpressionOrConst : sig type t = AE of HilExp.AccessExpression.t | Const of Const.t [@@deriving equal] end +(** Pointers stored into fields by a C++ constructor, eg [this->mutex_ = mutex] in a scoped guard, + so that a lock taken through the field can be expressed in terms of the stored value. *) +module FieldAliases : sig + include AbstractDomain.WithTop + + val get : HilExp.AccessExpression.t -> t -> HilExp.AccessExpression.t option + (** the value stored in the given field, if known *) + + val forget_fields : f:(Fieldname.t -> bool) -> t -> t + (** forget the aliases that go through a field satisfying [f] *) + + val assign : HilExp.AccessExpression.t -> HilExp.AccessExpression.t option -> t -> t + (** [assign lhs rhs_opt] forgets the aliases invalidated by a store to [lhs], then records that + [lhs] holds [rhs], if given and not itself invalidated by the store *) +end + module VarDomain : sig include AbstractDomain.WithTop @@ -91,7 +107,15 @@ module Acquisition : sig type t = private {elem: AcquisitionElem.t; loc: Location.t; trace: CallSite.t list} end -module LockState : AbstractDomain.WithTop +module LockState : sig + include AbstractDomain.WithTop + + val get_single_held_lock : t -> Lock.t option + (** the lock held, if exactly one lock is held once and no lock is released *) + + val get_single_unlocked_lock : t -> Lock.t option + (** the lock released, if exactly one lock is released once and no lock is held *) +end (** A set of lock acquisitions with source locations and procnames. *) module Acquisitions : sig @@ -104,9 +128,10 @@ module Acquisitions : sig (** is the given lock held, modulo memory abstraction across threads *) end -(** An event and the currently-held locks at the time it occurred. *) +(** An event and the currently-held locks at the time it occurred. [released] are the locks that + were released before the event without being held, so not held by callers at that time. *) module CriticalPairElement : sig - type t = private {acquisitions: Acquisitions.t; event: Event.t} + type t = private {acquisitions: Acquisitions.t; event: Event.t; released: Lock.t list} end (** A [CriticalPairElement] equipped with a call stack. The intuition is that if we have a critical @@ -200,6 +225,7 @@ type t = ; thread: ThreadDomain.t ; scheduled_work: ScheduledWorkDomain.t ; var_state: VarDomain.t + ; field_aliases: FieldAliases.t ; null_locs: NullLocs.t ; lazily_initalized: LazilyInitialized.t } @@ -242,6 +268,9 @@ val add_guard : val lock_guard : procname:Procname.t -> loc:Location.t -> Tenv.t -> t -> HilExp.t -> t (** Acquire the lock the guard was constructed with. *) +val is_guard : t -> HilExp.t -> bool +(** Whether a guard was constructed on the expression and not destroyed yet. *) + val remove_guard : t -> HilExp.t -> t (** Destroy the guard and release its lock. *) @@ -265,7 +294,8 @@ val empty_summary : summary val pp_summary : F.formatter -> summary -> unit val integrate_summary : - tenv:Tenv.t + ?release_held_locks:bool + -> tenv:Tenv.t -> procname:Procname.t -> lhs:HilExp.AccessExpression.t -> subst:Lock.subst @@ -275,7 +305,8 @@ val integrate_summary : -> summary -> t (** apply a callee summary to the current abstract state; [lhs] is the expression assigned the - returned value, if any *) + returned value, if any; with [release_held_locks], the locks that the callee leaves held are + released right after being acquired *) val summary_of_astate : Procdesc.t -> t -> summary diff --git a/infer/tests/codetoanalyze/c/starvation/issues.exp b/infer/tests/codetoanalyze/c/starvation/issues.exp index 97139626d20..ce73e0007e8 100644 --- a/infer/tests/codetoanalyze/c/starvation/issues.exp +++ b/infer/tests/codetoanalyze/c/starvation/issues.exp @@ -8,6 +8,18 @@ codetoanalyze/c/starvation/function_pointer.c, lock_m2_fptr_m1_fptr_function_poi codetoanalyze/c/starvation/function_pointer.c, lock_m2_fptr_m1_fptr_no_function_pointer, 30, DEADLOCK, no_bucket, ERROR, [[Trace 1] `lock_m2_fptr_m1_fptr_no_function_pointer`, locks `&(m2_fptr)` in `struct FakeMut`, locks `&(m1_fptr)` in `struct FakeMut`,[Trace 2] `lock_m2_fptr_m1_fptr_function_pointer`, locks `&(m1_fptr)` in `struct FakeMut`, locks `&(m2_fptr)` in `struct FakeMut` locked at lock_m2_fptr_indirectly at line 23, column 3, locks `&(m2_fptr)` in `struct FakeMut`] codetoanalyze/c/starvation/interproc_dedup.c, direct_one_way_bad, 26, DEADLOCK, no_bucket, ERROR, [[Trace 1] `direct_one_way_bad`, locks `&(dedup_m1)` in `struct FakeMut`, locks `&(dedup_m2)` in `struct FakeMut`,[Trace 2] `main`, locks `&(dedup_m2)` in `struct FakeMut`,Method call: `indirect_one_way_bad`,Method call: `direct_one_way_bad`, locks `&(dedup_m1)` in `struct FakeMut`] codetoanalyze/c/starvation/interproc_dedup.c, main, 38, DEADLOCK, no_bucket, ERROR, [[Trace 1] `main`, locks `&(dedup_m2)` in `struct FakeMut`,Method call: `indirect_one_way_bad`,Method call: `direct_one_way_bad`, locks `&(dedup_m1)` in `struct FakeMut`,[Trace 2] `direct_one_way_bad`, locks `&(dedup_m1)` in `struct FakeMut`, locks `&(dedup_m2)` in `struct FakeMut`] +codetoanalyze/c/starvation/lock_wrappers.c, wrapped_wm1_wm2_bad, 24, DEADLOCK, no_bucket, ERROR, [[Trace 1] `wrapped_wm1_wm2_bad`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex` locked at lock_wrapper at line 25, column 3, locks `m` in `struct WMutex`,[Trace 2] `conn_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, wrapped_wm1_wm2_bad, 24, DEADLOCK, no_bucket, ERROR, [[Trace 1] `wrapped_wm1_wm2_bad`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex` locked at lock_wrapper at line 25, column 3, locks `m` in `struct WMutex`,[Trace 2] `direct_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, direct_wm2_wm1_bad, 32, DEADLOCK, no_bucket, ERROR, [[Trace 1] `direct_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`,[Trace 2] `FP_guard_wm1_wm2_ok`,Method call: `guard_init`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, direct_wm2_wm1_bad, 32, DEADLOCK, no_bucket, ERROR, [[Trace 1] `direct_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`,[Trace 2] `wrapped_wm1_wm2_bad`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex` locked at lock_wrapper at line 25, column 3, locks `m` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, conn_wm2_wm1_bad, 54, DEADLOCK, no_bucket, ERROR, [[Trace 1] `conn_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`,[Trace 2] `FP_guard_wm1_wm2_ok`,Method call: `guard_init`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, conn_wm2_wm1_bad, 54, DEADLOCK, no_bucket, ERROR, [[Trace 1] `conn_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`,[Trace 2] `wrapped_wm1_wm2_bad`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex` locked at lock_wrapper at line 25, column 3, locks `m` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, FP_guard_wm1_wm2_ok, 67, DEADLOCK, no_bucket, ERROR, [[Trace 1] `FP_guard_wm1_wm2_ok`,Method call: `guard_init`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex`,[Trace 2] `conn_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, FP_guard_wm1_wm2_ok, 67, DEADLOCK, no_bucket, ERROR, [[Trace 1] `FP_guard_wm1_wm2_ok`,Method call: `guard_init`, locks `&(wm1)` in `struct WMutex`, locks `&(wm2)` in `struct WMutex`,[Trace 2] `direct_wm2_wm1_bad`, locks `&(wm2)` in `struct WMutex`, locks `&(wm1)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, token_wm4_wm5_bad, 96, DEADLOCK, no_bucket, ERROR, [[Trace 1] `token_wm4_wm5_bad`,Method call: `wrap_lock_wm4`,Method call: `lock_wm4`, locks `&(wm4)` in `struct WMutex`, locks `&(wm5)` in `struct WMutex`,[Trace 2] `direct_wm5_wm4_bad`, locks `&(wm5)` in `struct WMutex`, locks `&(wm4)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, two_lock_token_wm6_wm5_bad, 113, DEADLOCK, no_bucket, ERROR, [[Trace 1] `two_lock_token_wm6_wm5_bad`,Method call: `lock_wm6_wm7`, locks `&(wm6)` in `struct WMutex`,Method call: `lock_wm6_wm7`, locks `&(wm7)` in `struct WMutex`, locks `&(wm5)` in `struct WMutex`,[Trace 2] `direct_wm5_wm6_bad`, locks `&(wm5)` in `struct WMutex`, locks `&(wm6)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, direct_wm5_wm4_bad, 128, DEADLOCK, no_bucket, ERROR, [[Trace 1] `direct_wm5_wm4_bad`, locks `&(wm5)` in `struct WMutex`, locks `&(wm4)` in `struct WMutex`,[Trace 2] `token_wm4_wm5_bad`,Method call: `wrap_lock_wm4`,Method call: `lock_wm4`, locks `&(wm4)` in `struct WMutex`, locks `&(wm5)` in `struct WMutex`] +codetoanalyze/c/starvation/lock_wrappers.c, direct_wm5_wm6_bad, 136, DEADLOCK, no_bucket, ERROR, [[Trace 1] `direct_wm5_wm6_bad`, locks `&(wm5)` in `struct WMutex`, locks `&(wm6)` in `struct WMutex`,[Trace 2] `two_lock_token_wm6_wm5_bad`,Method call: `lock_wm6_wm7`, locks `&(wm6)` in `struct WMutex`,Method call: `lock_wm6_wm7`, locks `&(wm7)` in `struct WMutex`, locks `&(wm5)` in `struct WMutex`] codetoanalyze/c/starvation/nested_unbalanced_locks.c, nested_unbalanced_lock, 18, DEADLOCK, no_bucket, ERROR, [[Trace 1] `nested_unbalanced_lock`,Method call: `wrap_lock_m1`,Method call: `lock_m1`, locks `&(m1)` in `struct FakeMut`, locks `&(m2)` in `struct FakeMut` locked at wrap_lock_m2 at line 50, column 3,Method call: `lock_m2`, locks `&(m2)` in `struct FakeMut`,[Trace 2] `direct_m2_m1_lock`, locks `&(m2)` in `struct FakeMut`, locks `&(m1)` in `struct FakeMut`] codetoanalyze/c/starvation/nested_unbalanced_locks.c, direct_m2_m1_lock, 62, DEADLOCK, no_bucket, ERROR, [[Trace 1] `direct_m2_m1_lock`, locks `&(m2)` in `struct FakeMut`, locks `&(m1)` in `struct FakeMut`,[Trace 2] `nested_unbalanced_lock`,Method call: `wrap_lock_m1`,Method call: `lock_m1`, locks `&(m1)` in `struct FakeMut`, locks `&(m2)` in `struct FakeMut` locked at wrap_lock_m2 at line 50, column 3,Method call: `lock_m2`, locks `&(m2)` in `struct FakeMut`] codetoanalyze/c/starvation/unbalanced_locks.c, simple_unbalanced_lock, 18, DEADLOCK, no_bucket, ERROR, [[Trace 1] `simple_unbalanced_lock`,Method call: `lock_m1_wrap`, locks `&(m1)` in `struct AMutex`, locks `&(m2)` in `struct AMutex`,[Trace 2] `balanced_m2_m1_lock`, locks `&(m2)` in `struct AMutex`, locks `&(m1)` in `struct AMutex`] diff --git a/infer/tests/codetoanalyze/c/starvation/lock_wrappers.c b/infer/tests/codetoanalyze/c/starvation/lock_wrappers.c new file mode 100644 index 00000000000..b2b41df6c26 --- /dev/null +++ b/infer/tests/codetoanalyze/c/starvation/lock_wrappers.c @@ -0,0 +1,141 @@ +/* + * 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. + */ + +struct WMutex { + int blah; +}; +void pthread_mutex_lock(struct WMutex*); +void pthread_mutex_unlock(struct WMutex*); + +struct WMutex wm1; +struct WMutex wm2; +struct WMutex wm3; + +void lock_wrapper(struct WMutex* m) { pthread_mutex_lock(m); } + +void unlock_wrapper(struct WMutex* m) { pthread_mutex_unlock(m); } + +// the caller has fewer parameters than the wrappers +int wrapped_wm1_wm2_bad() { + pthread_mutex_lock(&wm1); + lock_wrapper(&wm2); + unlock_wrapper(&wm2); + pthread_mutex_unlock(&wm1); + return 0; +} + +int direct_wm2_wm1_bad() { + pthread_mutex_lock(&wm2); + pthread_mutex_lock(&wm1); + pthread_mutex_unlock(&wm1); + pthread_mutex_unlock(&wm2); + return 1; +} + +struct WConn { + struct WMutex* m; +}; + +void conn_acquire(struct WConn* c, struct WMutex* m) { + c->m = m; + pthread_mutex_lock(c->m); +} + +void conn_release(struct WConn* c) { pthread_mutex_unlock(c->m); } + +// the lock taken and released through the field of the connection is balanced +int conn_wm2_wm1_bad(struct WConn* c) { + conn_acquire(c, &wm3); + conn_release(c); + pthread_mutex_lock(&wm2); + pthread_mutex_lock(&wm1); + pthread_mutex_unlock(&wm1); + pthread_mutex_unlock(&wm2); + return 0; +} + +struct WGuard { + struct WMutex* m; +}; + +void guard_init(struct WGuard* g, struct WMutex* m) { + g->m = m; + pthread_mutex_lock(m); +} + +void guard_release(struct WGuard* g) { pthread_mutex_unlock(g->m); } + +// the release through the field of the local guard is not matched with the +// lock taken through the parameter, so wm1 is considered held when wm2 is taken +int FP_guard_wm1_wm2_ok(int b) { + if (b) { + struct WGuard g; + guard_init(&g, &wm1); + guard_release(&g); + pthread_mutex_lock(&wm2); + pthread_mutex_unlock(&wm2); + } + return 0; +} + +struct WMutex wm4; +struct WMutex wm5; +struct WMutex wm6; +struct WMutex wm7; + +struct WToken { + int id; +}; + +struct WToken lock_wm4(void) { + struct WToken t = {4}; + pthread_mutex_lock(&wm4); + return t; +} + +void wrap_lock_wm4(void) { struct WToken t = lock_wm4(); } + +// the lock taken by a function returning a struct stays held in its callers +int token_wm4_wm5_bad() { + wrap_lock_wm4(); + pthread_mutex_lock(&wm5); + pthread_mutex_unlock(&wm5); + pthread_mutex_unlock(&wm4); + return 0; +} + +struct WToken lock_wm6_wm7(void) { + struct WToken t = {6}; + pthread_mutex_lock(&wm6); + pthread_mutex_lock(&wm7); + return t; +} + +int two_lock_token_wm6_wm5_bad() { + struct WToken t = lock_wm6_wm7(); + pthread_mutex_lock(&wm5); + pthread_mutex_unlock(&wm5); + pthread_mutex_unlock(&wm7); + pthread_mutex_unlock(&wm6); + return 0; +} + +int direct_wm5_wm4_bad() { + pthread_mutex_lock(&wm5); + pthread_mutex_lock(&wm4); + pthread_mutex_unlock(&wm4); + pthread_mutex_unlock(&wm5); + return 0; +} + +int direct_wm5_wm6_bad() { + pthread_mutex_lock(&wm5); + pthread_mutex_lock(&wm6); + pthread_mutex_unlock(&wm6); + pthread_mutex_unlock(&wm5); + return 0; +} diff --git a/infer/tests/codetoanalyze/cpp/racerd/android_mutex.cpp b/infer/tests/codetoanalyze/cpp/racerd/android_mutex.cpp new file mode 100644 index 00000000000..098d5b14cbc --- /dev/null +++ b/infer/tests/codetoanalyze/cpp/racerd/android_mutex.cpp @@ -0,0 +1,136 @@ +/* + * 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. + */ + +// mock of android::Mutex and its scoped guard +namespace android { +class Mutex { + public: + int lock(); + void unlock(); + // return 0 on success + int tryLock(); + int timedLock(long long timeoutNs); + + class Autolock { + public: + explicit Autolock(Mutex& mutex) : mLock(mutex) { mLock.lock(); } + ~Autolock() { mLock.unlock(); } + + private: + Mutex& mLock; + }; +}; +} // namespace android + +namespace android_mutex { + +class Counter { + public: + void increment() { + android::Mutex::Autolock lock(mLock); + count_++; + } + + int get_count_bad() { return count_; } + + int get_count_ok() { + android::Mutex::Autolock lock(mLock); + return count_; + } + + void try_increment_ok() { + if (mLock.tryLock() == 0) { + count_++; + mLock.unlock(); + } + } + + void timed_increment_ok() { + if (mLock.timedLock(1000) != 0) { + return; + } + count_++; + mLock.unlock(); + } + + void failed_try_increment_bad() { + if (mLock.tryLock() != 0) { + count_++; + } else { + mLock.unlock(); + } + } + + // errors are negative + void negative_try_increment_ok() { + if (mLock.tryLock() < 0) { + return; + } + count_++; + mLock.unlock(); + } + + void non_negative_try_increment_ok() { + if (mLock.tryLock() >= 0) { + count_++; + mLock.unlock(); + } + } + + void negative_try_increment_bad() { + if (mLock.tryLock() < 0) { + count_++; + } + } + + void stored_try_increment_ok() { + const bool locked = mLock.tryLock() == 0; + if (locked) { + count_++; + mLock.unlock(); + } + } + + private: + android::Mutex mLock; + int count_; +}; +} // namespace android_mutex + +namespace android { +enum { OK = 0, NO_ERROR = OK }; +} // namespace android + +namespace android_mutex { + +class StatusCounter { + public: + void increment() { + android::Mutex::Autolock lock(mLock); + count_++; + } + + void no_error_try_increment_ok() { + if (mLock.tryLock() == android::NO_ERROR) { + count_++; + mLock.unlock(); + } + } + + void ok_timed_increment_ok() { + if (mLock.timedLock(1000) != android::OK) { + return; + } + count_++; + mLock.unlock(); + } + + private: + android::Mutex mLock; + int count_; +}; +} // namespace android_mutex diff --git a/infer/tests/codetoanalyze/cpp/racerd/issues.exp b/infer/tests/codetoanalyze/cpp/racerd/issues.exp index 6e4f9cd398e..45cd52f6f07 100644 --- a/infer/tests/codetoanalyze/cpp/racerd/issues.exp +++ b/infer/tests/codetoanalyze/cpp/racerd/issues.exp @@ -1,3 +1,6 @@ +codetoanalyze/cpp/racerd/android_mutex.cpp, android_mutex::Counter::get_count_bad, 38, LOCK_CONSISTENCY_VIOLATION, no_bucket, WARNING, [,access to `this->count_`,,access to `this->count_`] +codetoanalyze/cpp/racerd/android_mutex.cpp, android_mutex::Counter::failed_try_increment_bad, 62, LOCK_CONSISTENCY_VIOLATION, no_bucket, WARNING, [,access to `this->count_`,,access to `this->count_`] +codetoanalyze/cpp/racerd/android_mutex.cpp, android_mutex::Counter::negative_try_increment_bad, 86, LOCK_CONSISTENCY_VIOLATION, no_bucket, WARNING, [,access to `this->count_`,,access to `this->count_`] codetoanalyze/cpp/racerd/atomic_libcxx.cpp, atomic_libcxx::Counter::get_plain_bad, 72, LOCK_CONSISTENCY_VIOLATION, no_bucket, WARNING, [,access to `this->plain_`,,access to `this->plain_`] codetoanalyze/cpp/racerd/atomic_libstdcxx.cpp, atomic_libstdcxx::Counter::get_plain_bad, 50, LOCK_CONSISTENCY_VIOLATION, no_bucket, WARNING, [,access to `this->plain_`,,access to `this->plain_`] codetoanalyze/cpp/racerd/basics.cpp, basics::Basic::get_suspiciously_read_bad, 40, LOCK_CONSISTENCY_VIOLATION, no_bucket, WARNING, [,access to `this->suspiciously_read`,,access to `this->suspiciously_read`] diff --git a/infer/tests/codetoanalyze/cpp/starvation/android_mutex.cpp b/infer/tests/codetoanalyze/cpp/starvation/android_mutex.cpp new file mode 100644 index 00000000000..b09085f51a0 --- /dev/null +++ b/infer/tests/codetoanalyze/cpp/starvation/android_mutex.cpp @@ -0,0 +1,154 @@ +/* + * 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. + */ + +#include + +// mock of android::Mutex and its scoped guard +namespace android { +class Mutex { + public: + int lock(); + void unlock(); + int tryLock(); + + class Autolock { + public: + explicit Autolock(Mutex& mutex) : mLock(mutex) { mLock.lock(); } + explicit Autolock(Mutex* mutex) : mLock(*mutex) { mLock.lock(); } + ~Autolock() { mLock.unlock(); } + + private: + Mutex& mLock; + }; +}; + +typedef Mutex::Autolock AutoMutex; +} // namespace android + +namespace android_mutex { + +class WithAutolock { + public: + void thread1_bad() { + android::Mutex::Autolock lock1(mutex_1); + android::Mutex::Autolock lock2(mutex_2); + } + + void thread2_bad() { + android::Mutex::Autolock lock2(mutex_2); + android::Mutex::Autolock lock1(mutex_1); + } + + private: + android::Mutex mutex_1; + android::Mutex mutex_2; +}; + +class WithAutoMutexPointer { + public: + void thread1_bad() { + android::AutoMutex lock1(&mutex_1); + android::AutoMutex lock2(&mutex_2); + } + + void thread2_bad() { + android::AutoMutex lock2(&mutex_2); + android::AutoMutex lock1(&mutex_1); + } + + private: + android::Mutex mutex_1; + android::Mutex mutex_2; +}; + +class Direct { + public: + void thread1_bad() { + mutex_1.lock(); + mutex_2.lock(); + mutex_2.unlock(); + mutex_1.unlock(); + } + + void thread2_bad() { + mutex_2.lock(); + mutex_1.lock(); + mutex_1.unlock(); + mutex_2.unlock(); + } + + private: + android::Mutex mutex_1; + android::Mutex mutex_2; +}; + +class SameOrder { + public: + void thread1_ok() { + android::Mutex::Autolock lock1(mutex_1); + android::Mutex::Autolock lock2(mutex_2); + } + + void thread2_ok() { + android::Mutex::Autolock lock1(mutex_1); + android::Mutex::Autolock lock2(mutex_2); + } + + private: + android::Mutex mutex_1; + android::Mutex mutex_2; +}; + +class SequentialScopes { + public: + void thread1_ok() { + { + android::Mutex::Autolock lock1(mutex_1); + } + { + android::Mutex::Autolock lock2(mutex_2); + } + } + + void thread2_ok() { + { + android::Mutex::Autolock lock2(mutex_2); + } + { + android::Mutex::Autolock lock1(mutex_1); + } + } + + private: + android::Mutex mutex_1; + android::Mutex mutex_2; +}; + +// android::Mutex is not recursive +class SelfDeadlock { + public: + void relock_bad() { + android::Mutex::Autolock lock1(mutex_); + android::Mutex::Autolock lock2(mutex_); + } + + void lock_mutex() { android::Mutex::Autolock lock(mutex_); } + + void interproc_bad() { + android::Mutex::Autolock lock(mutex_); + lock_mutex(); + } + + void lock_guard_bad() { + std::lock_guard lock1(mutex_); + std::lock_guard lock2(mutex_); + } + + private: + android::Mutex mutex_; +}; +} // namespace android_mutex diff --git a/infer/tests/codetoanalyze/cpp/starvation/custom_guards.cpp b/infer/tests/codetoanalyze/cpp/starvation/custom_guards.cpp new file mode 100644 index 00000000000..9bc7ecaec59 --- /dev/null +++ b/infer/tests/codetoanalyze/cpp/starvation/custom_guards.cpp @@ -0,0 +1,731 @@ +/* + * 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. + */ + +#include +#include + +// scoped guards that are not modelled are recognised from the summaries of +// their constructors and destructors +namespace custom_guards { + +class RefGuard { + public: + explicit RefGuard(std::mutex& m) : m_(m) { m_.lock(); } + + ~RefGuard() { m_.unlock(); } + + private: + std::mutex& m_; +}; + +class PtrGuard { + public: + explicit PtrGuard(std::mutex* m) : m_(m) { m_->lock(); } + + ~PtrGuard() { m_->unlock(); } + + private: + std::mutex* m_; +}; + +// pthread mutexes are treated as recursive +class PthreadGuard { + public: + explicit PthreadGuard(pthread_mutex_t* m) : m_(m) { pthread_mutex_lock(m_); } + + ~PthreadGuard() { pthread_mutex_unlock(m_); } + + private: + pthread_mutex_t* m_; +}; + +// a lock class that is not modelled, with a nested guard +class Mutex { + public: + void lock() { m_.lock(); } + + void unlock() { m_.unlock(); } + + class Guard { + public: + explicit Guard(Mutex& m) : m_(m) { m_.lock(); } + + ~Guard() { m_.unlock(); } + + private: + Mutex& m_; + }; + + private: + std::mutex m_; +}; + +// a guard that can release and reacquire its lock +class RelockGuard { + public: + explicit RelockGuard(std::mutex& m) : m_(m) { m_.lock(); } + + ~RelockGuard() { m_.unlock(); } + + void lock() { m_.lock(); } + + void unlock() { m_.unlock(); } + + private: + std::mutex& m_; +}; + +// a guard whose destructor releases its lock only if it holds it +class UniqueGuard { + public: + explicit UniqueGuard(std::mutex& m) : m_(m), owns_(true) { m_.lock(); } + + ~UniqueGuard() { + if (owns_) { + m_.unlock(); + } + } + + void lock() { + m_.lock(); + owns_ = true; + } + + void unlock() { + m_.unlock(); + owns_ = false; + } + + private: + std::mutex& m_; + bool owns_; +}; + +// releases a lock for its lifetime +class Unlocker { + public: + explicit Unlocker(std::mutex& m) : m_(m) { m_.unlock(); } + + ~Unlocker() { m_.lock(); } + + private: + std::mutex& m_; +}; + +class TwoLockGuard { + public: + TwoLockGuard(std::mutex& m1, std::mutex& m2) : m1_(m1), m2_(m2) { + m1_.lock(); + m2_.lock(); + } + + ~TwoLockGuard() { + m2_.unlock(); + m1_.unlock(); + } + + private: + std::mutex& m1_; + std::mutex& m2_; +}; + +class HelperGuard { + public: + explicit HelperGuard(std::mutex& m) : m_(m) { acquire(); } + + ~HelperGuard() { m_.unlock(); } + + private: + void acquire() { m_.lock(); } + + std::mutex& m_; +}; + +// releases a lock taken by the function that returns it +class Releaser { + public: + explicit Releaser(std::mutex* m) : m_(m) {} + + ~Releaser() { m_->unlock(); } + + private: + std::mutex* m_; +}; + +// values that do not release a lock when destroyed +struct Name { + ~Name() {} + + int id; +}; + +struct Point { + int x; + int y; +}; + +void trace_mutex(std::mutex* m); + +// passes its mutex to a call before locking it +class TracingGuard { + public: + explicit TracingGuard(std::mutex* m) : m_(m) { + trace_mutex(m_); + m_->lock(); + } + + ~TracingGuard() { m_->unlock(); } + + private: + std::mutex* m_; +}; + +// locks the mutex stored by a callee of its constructor +class RebindingGuard { + public: + RebindingGuard(std::mutex* initial, std::mutex* m) : m_(initial) { + rebind(m); + m_->lock(); + } + + ~RebindingGuard() { m_->unlock(); } + + private: + void rebind(std::mutex* m) { m_ = m; } + + std::mutex* m_; +}; + +class ReassigningGuard { + public: + ReassigningGuard(std::mutex* initial, std::mutex* m) : m_(initial) { + m_ = m; + m_->lock(); + } + + ~ReassigningGuard() { m_->unlock(); } + + private: + std::mutex* m_; +}; + +// leaves the release of the lock taken by its constructor to its user +class Acquirer { + public: + explicit Acquirer(std::mutex& m) : m_(m) { m_.lock(); } + + ~Acquirer() {} + + std::mutex& m_; +}; + +void release(Acquirer& acquirer) { acquirer.m_.unlock(); } + +class WithRefGuard { + public: + void thread1_bad() { + RefGuard lock1(mutex_1); + RefGuard lock2(mutex_2); + } + + void thread2_bad() { + RefGuard lock2(mutex_2); + RefGuard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class WithPtrGuard { + public: + void thread1_bad() { + PtrGuard lock1(&mutex_1); + PtrGuard lock2(&mutex_2); + } + + void thread2_bad() { + PtrGuard lock2(&mutex_2); + PtrGuard lock1(&mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class WithPthreadGuard { + public: + void thread1_bad() { + PthreadGuard lock1(&mutex_1); + PthreadGuard lock2(&mutex_2); + } + + void thread2_bad() { + PthreadGuard lock2(&mutex_2); + PthreadGuard lock1(&mutex_1); + } + + private: + pthread_mutex_t mutex_1; + pthread_mutex_t mutex_2; +}; + +class WithNestedGuard { + public: + void thread1_bad() { + Mutex::Guard lock1(mutex_1); + Mutex::Guard lock2(mutex_2); + } + + void thread2_bad() { + Mutex::Guard lock2(mutex_2); + Mutex::Guard lock1(mutex_1); + } + + private: + Mutex mutex_1; + Mutex mutex_2; +}; + +class Interprocedural { + public: + void lock_2() { RefGuard lock2(mutex_2); } + + void thread1_bad() { + RefGuard lock1(mutex_1); + lock_2(); + } + + void thread2_bad() { + RefGuard lock2(mutex_2); + RefGuard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class Relock { + public: + void thread1_bad() { + RelockGuard lock2(mutex_2); + lock2.unlock(); + RefGuard lock1(mutex_1); + lock2.lock(); + } + + void thread2_bad() { + RefGuard lock2(mutex_2); + RefGuard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class ConditionalRelease { + public: + // a guard whose destructor releases its lock only on some paths is not + // recognised + void FN_thread1_bad() { + UniqueGuard lock2(mutex_2); + lock2.unlock(); + RefGuard lock1(mutex_1); + lock2.lock(); + } + + void FN_thread2_bad() { + RefGuard lock2(mutex_2); + RefGuard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class NoInversion { + public: + void sequential_ok() { + { + RefGuard lock2(mutex_2); + } + { + RefGuard lock1(mutex_1); + } + } + + void same_order_ok() { + RefGuard lock1(mutex_1); + RefGuard lock2(mutex_2); + } + + void early_unlock_ok() { + RelockGuard lock2(mutex_2); + lock2.unlock(); + { + RefGuard lock1(mutex_1); + } + lock2.lock(); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class WithUnlocker { + public: + // the lock released by an Unlocker is not reacquired by its destructor + void FN_thread1_bad() { + RefGuard lock2(mutex_2); + { + Unlocker unlock2(mutex_2); + } + RefGuard lock1(mutex_1); + } + + void FN_thread2_bad() { + RefGuard lock1(mutex_1); + RefGuard lock2(mutex_2); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class SelfDeadlock { + public: + void relock_bad() { + RefGuard lock1(mutex_); + RefGuard lock2(mutex_); + } + + private: + std::mutex mutex_; +}; + +class ReturnedGuard { + public: + Releaser lock_1() { + mutex_1.lock(); + return Releaser(&mutex_1); + } + + void thread1_bad() { + const Releaser& lock1 = lock_1(); + RefGuard lock2(mutex_2); + } + + void thread2_bad() { + RefGuard lock2(mutex_2); + RefGuard lock1(mutex_1); + } + + void sequential_ok(bool b) { + if (b) { + { + const Releaser& lock1 = lock_1(); + } + RefGuard lock2(mutex_2); + } + } + + // in C++11 the temporary that a copy-initialisation is made from releases the + // lock at the end of the full-expression, so no guard is recognised + void FN_copy_init_bad() { + Releaser lock1 = lock_1(); + RefGuard lock2(mutex_2); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +// a value returned with a lock held is a guard only if its destructor releases +// a lock +class ReturnedValue { + public: + Name lock_1_name() { + mutex_1.lock(); + return Name(); + } + + Point lock_1_point() { + mutex_1.lock(); + return Point{1, 2}; + } + + Point lock_1_3_point() { + mutex_1.lock(); + mutex_3.lock(); + return Point{1, 3}; + } + + void destroyed_name_bad() { + { + Name name = lock_1_name(); + } + mutex_2.lock(); + mutex_2.unlock(); + mutex_1.unlock(); + } + + void temporary_name_bad() { + lock_1_name(); + mutex_2.lock(); + mutex_2.unlock(); + mutex_1.unlock(); + } + + void wrap_lock_1() { Point point = lock_1_point(); } + + void wrapped_point_bad() { + wrap_lock_1(); + mutex_2.lock(); + mutex_2.unlock(); + mutex_1.unlock(); + } + + void two_lock_point_bad() { + Point point = lock_1_3_point(); + mutex_2.lock(); + mutex_2.unlock(); + mutex_3.unlock(); + mutex_1.unlock(); + } + + void reverse_bad() { + mutex_2.lock(); + mutex_1.lock(); + mutex_1.unlock(); + mutex_3.lock(); + mutex_3.unlock(); + mutex_2.unlock(); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; + std::mutex mutex_3; +}; + +// the locks of guards that hold several locks, or that are on the heap, are +// not tracked +class Untracked { + public: + void two_lock_guard_ok(bool b) { + if (b) { + { + TwoLockGuard lock12(mutex_1, mutex_2); + } + RefGuard lock3(mutex_3); + } + } + + void heap_guard_ok(bool b) { + if (b) { + RefGuard* lock1 = new RefGuard(mutex_1); + delete lock1; + RefGuard lock3(mutex_3); + } + } + + void reverse_ok() { + RefGuard lock3(mutex_3); + RefGuard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; + std::mutex mutex_3; +}; + +class WithHelperGuard { + public: + // the lock taken by a helper of the constructor is not recognised as held + // by the guard + void FN_thread1_bad() { + HelperGuard lock1(mutex_1); + HelperGuard lock2(mutex_2); + } + + void FN_thread2_bad() { + HelperGuard lock2(mutex_2); + HelperGuard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +// an object whose destructor does not release the lock taken by its +// constructor is not a guard +class WithAcquirer { + public: + void guard_then_lock_bad() { + RefGuard lock1(mutex_1); + RefGuard lock2(mutex_2); + } + + void acquirer_then_lock_ok() { + Acquirer acquirer(mutex_1); + release(acquirer); + RefGuard lock2(mutex_2); + } + + // the lock taken by the constructor of an object that is not a guard is + // treated as released at once + void FN_acquirer_held_bad() { + Acquirer acquirer(mutex_1); + RefGuard lock2(mutex_2); + release(acquirer); + } + + void reverse_bad() { + RefGuard lock2(mutex_2); + RefGuard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +class WithTracingGuard { + public: + void thread1_bad() { + TracingGuard lock1(&mutex_1); + TracingGuard lock2(&mutex_2); + } + + void thread2_bad() { + TracingGuard lock2(&mutex_2); + TracingGuard lock1(&mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +// stores through a copy of `this` +class SelfAliasGuard { + public: + SelfAliasGuard(std::mutex* initial, std::mutex* m) { + m_ = initial; + SelfAliasGuard* self = this; + self->m_ = m; + m_->lock(); + } + + ~SelfAliasGuard() { m_->unlock(); } + + private: + std::mutex* m_; +}; + +class WithRebindingGuard { + public: + // stores made by callees of the constructor are not tracked, so a call that + // may write the guard's fields (any call taking `this`) before the lock makes + // the lock unrecognised + void FN_rebound_bad() { + RebindingGuard lock2(&mutex_1, &mutex_2); + std::lock_guard lock3(mutex_3); + } + + void reassigned_bad() { + ReassigningGuard lock2(&mutex_1, &mutex_2); + std::lock_guard lock3(mutex_3); + } + + // the store through `self` makes the lock unrecognised + void FN_self_alias_bad() { + SelfAliasGuard lock2(&mutex_1, &mutex_2); + std::lock_guard lock3(mutex_3); + } + + void reverse_3_2_bad() { + std::lock_guard lock3(mutex_3); + std::lock_guard lock2(mutex_2); + } + + void reverse_3_1_ok() { + std::lock_guard lock3(mutex_3); + std::lock_guard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; + std::mutex mutex_3; +}; + +// a lock reached through a field has the same name in every method, even after +// a pointer is stored into the field +class StoreThenLock { + public: + void set_and_lock_bad(std::mutex* m) { + other_ = m; + std::lock_guard lock1(mutex_1); + std::lock_guard lock2(*other_); + } + + void lock_bad() { + std::lock_guard lock2(*other_); + std::lock_guard lock1(mutex_1); + } + + private: + std::mutex mutex_1; + std::mutex* other_; +}; + +// takes and releases a lock through a field in different methods +class Session { + public: + void begin(std::mutex* m) { + mutex_ = m; + mutex_->lock(); + } + + void end() { mutex_->unlock(); } + + private: + std::mutex* mutex_; +}; + +class WithSession { + public: + void thread1_bad() { + session_.begin(&mutex_1); + session_.end(); + std::lock_guard lock2(mutex_2); + std::lock_guard lock3(mutex_3); + } + + void thread2_bad() { + std::lock_guard lock3(mutex_3); + std::lock_guard lock2(mutex_2); + } + + private: + Session session_; + std::mutex mutex_1; + std::mutex mutex_2; + std::mutex mutex_3; +}; +} // namespace custom_guards diff --git a/infer/tests/codetoanalyze/cpp/starvation/issues.exp b/infer/tests/codetoanalyze/cpp/starvation/issues.exp index f4ff294019f..a7a8d002630 100644 --- a/infer/tests/codetoanalyze/cpp/starvation/issues.exp +++ b/infer/tests/codetoanalyze/cpp/starvation/issues.exp @@ -1,3 +1,12 @@ +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::WithAutolock::thread1_bad, 37, DEADLOCK, no_bucket, ERROR, [[Trace 1] `android_mutex::WithAutolock::thread1_bad`, locks `&(this->mutex_1)` in `class android_mutex::WithAutolock`, locks `&(this->mutex_2)` in `class android_mutex::WithAutolock`,[Trace 2] `android_mutex::WithAutolock::thread2_bad`, locks `&(this->mutex_2)` in `class android_mutex::WithAutolock`, locks `&(this->mutex_1)` in `class android_mutex::WithAutolock`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::WithAutolock::thread2_bad, 42, DEADLOCK, no_bucket, ERROR, [[Trace 1] `android_mutex::WithAutolock::thread2_bad`, locks `&(this->mutex_2)` in `class android_mutex::WithAutolock`, locks `&(this->mutex_1)` in `class android_mutex::WithAutolock`,[Trace 2] `android_mutex::WithAutolock::thread1_bad`, locks `&(this->mutex_1)` in `class android_mutex::WithAutolock`, locks `&(this->mutex_2)` in `class android_mutex::WithAutolock`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::WithAutoMutexPointer::thread1_bad, 54, DEADLOCK, no_bucket, ERROR, [[Trace 1] `android_mutex::WithAutoMutexPointer::thread1_bad`, locks `&(this->mutex_1)` in `class android_mutex::WithAutoMutexPointer`, locks `&(this->mutex_2)` in `class android_mutex::WithAutoMutexPointer`,[Trace 2] `android_mutex::WithAutoMutexPointer::thread2_bad`, locks `&(this->mutex_2)` in `class android_mutex::WithAutoMutexPointer`, locks `&(this->mutex_1)` in `class android_mutex::WithAutoMutexPointer`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::WithAutoMutexPointer::thread2_bad, 59, DEADLOCK, no_bucket, ERROR, [[Trace 1] `android_mutex::WithAutoMutexPointer::thread2_bad`, locks `&(this->mutex_2)` in `class android_mutex::WithAutoMutexPointer`, locks `&(this->mutex_1)` in `class android_mutex::WithAutoMutexPointer`,[Trace 2] `android_mutex::WithAutoMutexPointer::thread1_bad`, locks `&(this->mutex_1)` in `class android_mutex::WithAutoMutexPointer`, locks `&(this->mutex_2)` in `class android_mutex::WithAutoMutexPointer`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::Direct::thread1_bad, 71, DEADLOCK, no_bucket, ERROR, [[Trace 1] `android_mutex::Direct::thread1_bad`, locks `&(this->mutex_1)` in `class android_mutex::Direct`, locks `&(this->mutex_2)` in `class android_mutex::Direct`,[Trace 2] `android_mutex::Direct::thread2_bad`, locks `&(this->mutex_2)` in `class android_mutex::Direct`, locks `&(this->mutex_1)` in `class android_mutex::Direct`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::Direct::thread2_bad, 78, DEADLOCK, no_bucket, ERROR, [[Trace 1] `android_mutex::Direct::thread2_bad`, locks `&(this->mutex_2)` in `class android_mutex::Direct`, locks `&(this->mutex_1)` in `class android_mutex::Direct`,[Trace 2] `android_mutex::Direct::thread1_bad`, locks `&(this->mutex_1)` in `class android_mutex::Direct`, locks `&(this->mutex_2)` in `class android_mutex::Direct`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::SelfDeadlock::relock_bad, 135, DEADLOCK, no_bucket, ERROR, [In method `android_mutex::SelfDeadlock::relock_bad`, locks `&(this->mutex_)` in `class android_mutex::SelfDeadlock`, locks `&(this->mutex_)` in `class android_mutex::SelfDeadlock`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::SelfDeadlock::interproc_bad, 142, DEADLOCK, no_bucket, ERROR, [In method `android_mutex::SelfDeadlock::interproc_bad`, locks `&(this->mutex_)` in `class android_mutex::SelfDeadlock`,Method call: `android_mutex::SelfDeadlock::lock_mutex`, locks `&(this->mutex_)` in `class android_mutex::SelfDeadlock`] +codetoanalyze/cpp/starvation/android_mutex.cpp, android_mutex::SelfDeadlock::lock_guard_bad, 147, DEADLOCK, no_bucket, ERROR, [In method `android_mutex::SelfDeadlock::lock_guard_bad`, locks `&(this->mutex_)` in `class android_mutex::SelfDeadlock`, locks `&(this->mutex_)` in `class android_mutex::SelfDeadlock`] codetoanalyze/cpp/starvation/basics.cpp, basics::Basic::thread1_bad, 18, DEADLOCK, no_bucket, ERROR, [[Trace 1] `basics::Basic::thread1_bad`, locks `&(this->mutex_1)` in `class basics::Basic`, locks `&(this->mutex_2)` in `class basics::Basic`,[Trace 2] `basics::Basic::thread2_bad`, locks `&(this->mutex_2)` in `class basics::Basic`, locks `&(this->mutex_1)` in `class basics::Basic`] codetoanalyze/cpp/starvation/basics.cpp, basics::Basic::thread2_bad, 26, DEADLOCK, no_bucket, ERROR, [[Trace 1] `basics::Basic::thread2_bad`, locks `&(this->mutex_2)` in `class basics::Basic`, locks `&(this->mutex_1)` in `class basics::Basic`,[Trace 2] `basics::Basic::thread1_bad`, locks `&(this->mutex_1)` in `class basics::Basic`, locks `&(this->mutex_2)` in `class basics::Basic`] codetoanalyze/cpp/starvation/basics.cpp, basics::WithGuard::thread1_bad, 44, DEADLOCK, no_bucket, ERROR, [[Trace 1] `basics::WithGuard::thread1_bad`, locks `&(this->mutex_1)` in `class basics::WithGuard`, locks `&(this->mutex_2)` in `class basics::WithGuard`,[Trace 2] `basics::WithGuard::thread2_bad`, locks `&(this->mutex_2)` in `class basics::WithGuard`, locks `&(this->mutex_1)` in `class basics::WithGuard`] @@ -8,6 +17,49 @@ codetoanalyze/cpp/starvation/basics.cpp, basics::SelfDeadlock::complicated_bad, codetoanalyze/cpp/starvation/basics.cpp, basics::PathSensitive::FP_ok, 142, DEADLOCK, no_bucket, ERROR, [In method `basics::PathSensitive::FP_ok`, locks `&(this->mutex_)` in `class basics::PathSensitive`, locks `&(this->mutex_)` in `class basics::PathSensitive`] codetoanalyze/cpp/starvation/crossfile-1.cpp, CrossFileOne::lock_my_mutex_first_then_the_other, 12, DEADLOCK, no_bucket, ERROR, [[Trace 1] `CrossFileOne::lock_my_mutex_first_then_the_other`, locks `&(this->_mutex)` in `class CrossFileOne`,Method call: `CrossFileTwo::just_lock_my_mutex`, locks `&(other->_mutex)` in `class CrossFileTwo`,[Trace 2] `CrossFileTwo::lock_my_mutex_first_then_the_other`, locks `&(this->_mutex)` in `class CrossFileTwo`,Method call: `CrossFileOne::just_lock_my_mutex`, locks `&(other->_mutex)` in `class CrossFileOne`] codetoanalyze/cpp/starvation/crossfile-2.cpp, CrossFileTwo::lock_my_mutex_first_then_the_other, 12, DEADLOCK, no_bucket, ERROR, [[Trace 1] `CrossFileTwo::lock_my_mutex_first_then_the_other`, locks `&(this->_mutex)` in `class CrossFileTwo`,Method call: `CrossFileOne::just_lock_my_mutex`, locks `&(other->_mutex)` in `class CrossFileOne`,[Trace 2] `CrossFileOne::lock_my_mutex_first_then_the_other`, locks `&(this->_mutex)` in `class CrossFileOne`,Method call: `CrossFileTwo::just_lock_my_mutex`, locks `&(other->_mutex)` in `class CrossFileTwo`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithRefGuard::thread1_bad, 231, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithRefGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithRefGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithRefGuard`,[Trace 2] `custom_guards::WithRefGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithRefGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithRefGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithRefGuard::thread2_bad, 236, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithRefGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithRefGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithRefGuard`,[Trace 2] `custom_guards::WithRefGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithRefGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithRefGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithPtrGuard::thread1_bad, 248, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithPtrGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithPtrGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithPtrGuard`,[Trace 2] `custom_guards::WithPtrGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithPtrGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithPtrGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithPtrGuard::thread2_bad, 253, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithPtrGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithPtrGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithPtrGuard`,[Trace 2] `custom_guards::WithPtrGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithPtrGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithPtrGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithPthreadGuard::thread1_bad, 265, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithPthreadGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithPthreadGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithPthreadGuard`,[Trace 2] `custom_guards::WithPthreadGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithPthreadGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithPthreadGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithPthreadGuard::thread2_bad, 270, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithPthreadGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithPthreadGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithPthreadGuard`,[Trace 2] `custom_guards::WithPthreadGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithPthreadGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithPthreadGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithNestedGuard::thread1_bad, 282, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithNestedGuard::thread1_bad`, locks `&(this->mutex_1.m_)` in `class custom_guards::WithNestedGuard`, locks `&(this->mutex_2.m_)` in `class custom_guards::WithNestedGuard`,[Trace 2] `custom_guards::WithNestedGuard::thread2_bad`, locks `&(this->mutex_2.m_)` in `class custom_guards::WithNestedGuard`, locks `&(this->mutex_1.m_)` in `class custom_guards::WithNestedGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithNestedGuard::thread2_bad, 287, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithNestedGuard::thread2_bad`, locks `&(this->mutex_2.m_)` in `class custom_guards::WithNestedGuard`, locks `&(this->mutex_1.m_)` in `class custom_guards::WithNestedGuard`,[Trace 2] `custom_guards::WithNestedGuard::thread1_bad`, locks `&(this->mutex_1.m_)` in `class custom_guards::WithNestedGuard`, locks `&(this->mutex_2.m_)` in `class custom_guards::WithNestedGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::Interprocedural::thread1_bad, 301, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::Interprocedural::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::Interprocedural`,Method call: `custom_guards::Interprocedural::lock_2`, locks `&(this->mutex_2)` in `class custom_guards::Interprocedural`,[Trace 2] `custom_guards::Interprocedural::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::Interprocedural`, locks `&(this->mutex_1)` in `class custom_guards::Interprocedural`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::Interprocedural::thread2_bad, 306, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::Interprocedural::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::Interprocedural`, locks `&(this->mutex_1)` in `class custom_guards::Interprocedural`,[Trace 2] `custom_guards::Interprocedural::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::Interprocedural`,Method call: `custom_guards::Interprocedural::lock_2`, locks `&(this->mutex_2)` in `class custom_guards::Interprocedural`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::Relock::thread1_bad, 320, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::Relock::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::Relock`, locks `&(this->mutex_2)` in `class custom_guards::Relock`,[Trace 2] `custom_guards::Relock::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::Relock`, locks `&(this->mutex_1)` in `class custom_guards::Relock`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::Relock::thread2_bad, 325, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::Relock::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::Relock`, locks `&(this->mutex_1)` in `class custom_guards::Relock`,[Trace 2] `custom_guards::Relock::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::Relock`, locks `&(this->mutex_2)` in `class custom_guards::Relock`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::SelfDeadlock::relock_bad, 409, DEADLOCK, no_bucket, ERROR, [In method `custom_guards::SelfDeadlock::relock_bad`, locks `&(this->mutex_)` in `class custom_guards::SelfDeadlock`, locks `&(this->mutex_)` in `class custom_guards::SelfDeadlock`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedGuard::thread1_bad, 425, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedGuard`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedGuard`,[Trace 2] `custom_guards::ReturnedGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedGuard`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedGuard::thread2_bad, 430, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedGuard`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedGuard`,[Trace 2] `custom_guards::ReturnedGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedGuard`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::destroyed_name_bad, 460, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::destroyed_name_bad`,Method call: `custom_guards::ReturnedValue::lock_1_name`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::temporary_name_bad, 460, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::temporary_name_bad`,Method call: `custom_guards::ReturnedValue::lock_1_name`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::wrapped_point_bad, 465, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::wrapped_point_bad`,Method call: `custom_guards::ReturnedValue::wrap_lock_1`,Method call: `custom_guards::ReturnedValue::lock_1_point`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::two_lock_point_bad, 470, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::two_lock_point_bad`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_3)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::two_lock_point_bad, 470, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::two_lock_point_bad`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_3)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_3)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::reverse_bad, 509, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::destroyed_name_bad`,Method call: `custom_guards::ReturnedValue::lock_1_name`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::reverse_bad, 509, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::temporary_name_bad`,Method call: `custom_guards::ReturnedValue::lock_1_name`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::reverse_bad, 509, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::two_lock_point_bad`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_3)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::reverse_bad, 509, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_3)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::two_lock_point_bad`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,Method call: `custom_guards::ReturnedValue::lock_1_3_point`, locks `&(this->mutex_3)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::ReturnedValue::reverse_bad, 509, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::ReturnedValue::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`,[Trace 2] `custom_guards::ReturnedValue::wrapped_point_bad`,Method call: `custom_guards::ReturnedValue::wrap_lock_1`,Method call: `custom_guards::ReturnedValue::lock_1_point`, locks `&(this->mutex_1)` in `class custom_guards::ReturnedValue`, locks `&(this->mutex_2)` in `class custom_guards::ReturnedValue`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithAcquirer::guard_then_lock_bad, 579, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithAcquirer::guard_then_lock_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithAcquirer`, locks `&(this->mutex_2)` in `class custom_guards::WithAcquirer`,[Trace 2] `custom_guards::WithAcquirer::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithAcquirer`, locks `&(this->mutex_1)` in `class custom_guards::WithAcquirer`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithAcquirer::reverse_bad, 598, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithAcquirer::reverse_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithAcquirer`, locks `&(this->mutex_1)` in `class custom_guards::WithAcquirer`,[Trace 2] `custom_guards::WithAcquirer::guard_then_lock_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithAcquirer`, locks `&(this->mutex_2)` in `class custom_guards::WithAcquirer`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithTracingGuard::thread1_bad, 610, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithTracingGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithTracingGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithTracingGuard`,[Trace 2] `custom_guards::WithTracingGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithTracingGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithTracingGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithTracingGuard::thread2_bad, 615, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithTracingGuard::thread2_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithTracingGuard`, locks `&(this->mutex_1)` in `class custom_guards::WithTracingGuard`,[Trace 2] `custom_guards::WithTracingGuard::thread1_bad`, locks `&(this->mutex_1)` in `class custom_guards::WithTracingGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithTracingGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithRebindingGuard::reassigned_bad, 651, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithRebindingGuard::reassigned_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithRebindingGuard`, locks `&(this->mutex_3)` in `class custom_guards::WithRebindingGuard`,[Trace 2] `custom_guards::WithRebindingGuard::reverse_3_2_bad`, locks `&(this->mutex_3)` in `class custom_guards::WithRebindingGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithRebindingGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithRebindingGuard::reverse_3_2_bad, 662, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithRebindingGuard::reverse_3_2_bad`, locks `&(this->mutex_3)` in `class custom_guards::WithRebindingGuard`, locks `&(this->mutex_2)` in `class custom_guards::WithRebindingGuard`,[Trace 2] `custom_guards::WithRebindingGuard::reassigned_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithRebindingGuard`, locks `&(this->mutex_3)` in `class custom_guards::WithRebindingGuard`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::StoreThenLock::set_and_lock_bad, 683, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::StoreThenLock::set_and_lock_bad`, locks `&(this->mutex_1)` in `class custom_guards::StoreThenLock`, locks `this->other_` in `class custom_guards::StoreThenLock`,[Trace 2] `custom_guards::StoreThenLock::lock_bad`, locks `this->other_` in `class custom_guards::StoreThenLock`, locks `&(this->mutex_1)` in `class custom_guards::StoreThenLock`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::StoreThenLock::lock_bad, 688, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::StoreThenLock::lock_bad`, locks `this->other_` in `class custom_guards::StoreThenLock`, locks `&(this->mutex_1)` in `class custom_guards::StoreThenLock`,[Trace 2] `custom_guards::StoreThenLock::set_and_lock_bad`, locks `&(this->mutex_1)` in `class custom_guards::StoreThenLock`, locks `this->other_` in `class custom_guards::StoreThenLock`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithSession::thread1_bad, 716, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithSession::thread1_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithSession`, locks `&(this->mutex_3)` in `class custom_guards::WithSession`,[Trace 2] `custom_guards::WithSession::thread2_bad`, locks `&(this->mutex_3)` in `class custom_guards::WithSession`, locks `&(this->mutex_2)` in `class custom_guards::WithSession`] +codetoanalyze/cpp/starvation/custom_guards.cpp, custom_guards::WithSession::thread2_bad, 721, DEADLOCK, no_bucket, ERROR, [[Trace 1] `custom_guards::WithSession::thread2_bad`, locks `&(this->mutex_3)` in `class custom_guards::WithSession`, locks `&(this->mutex_2)` in `class custom_guards::WithSession`,[Trace 2] `custom_guards::WithSession::thread1_bad`, locks `&(this->mutex_2)` in `class custom_guards::WithSession`, locks `&(this->mutex_3)` in `class custom_guards::WithSession`] +codetoanalyze/cpp/starvation/release_in_callee.cpp, release_in_callee::RelockUnderOtherLock::relock_bad, 101, DEADLOCK, no_bucket, ERROR, [[Trace 1] `release_in_callee::RelockUnderOtherLock::relock_bad`, locks `&(this->mutex_1)` in `class release_in_callee::RelockUnderOtherLock`, locks `&(this->mutex_2)` in `class release_in_callee::RelockUnderOtherLock`,[Trace 2] `release_in_callee::RelockUnderOtherLock::relock_bad`, locks `&(this->mutex_2)` in `class release_in_callee::RelockUnderOtherLock`,Method call: `release_in_callee::RelockUnderOtherLock::relock_1`, locks `&(this->mutex_1)` in `class release_in_callee::RelockUnderOtherLock`] +codetoanalyze/cpp/starvation/release_in_callee.cpp, release_in_callee::RelockUnderOtherLock::relock_bad, 102, DEADLOCK, no_bucket, ERROR, [[Trace 1] `release_in_callee::RelockUnderOtherLock::relock_bad`, locks `&(this->mutex_2)` in `class release_in_callee::RelockUnderOtherLock`,Method call: `release_in_callee::RelockUnderOtherLock::relock_1`, locks `&(this->mutex_1)` in `class release_in_callee::RelockUnderOtherLock`,[Trace 2] `release_in_callee::RelockUnderOtherLock::relock_bad`, locks `&(this->mutex_1)` in `class release_in_callee::RelockUnderOtherLock`, locks `&(this->mutex_2)` in `class release_in_callee::RelockUnderOtherLock`] codetoanalyze/cpp/starvation/skip.cpp, skipped::Skip::not_skipped_bad, 19, DEADLOCK, no_bucket, ERROR, [In method `skipped::Skip::not_skipped_bad`,Method call: `skipped::Skip::private_deadlock`, locks `&(this->mutex_)` in `class skipped::Skip`, locks `&(this->mutex_)` in `class skipped::Skip`] codetoanalyze/cpp/starvation/skip.cpp, skipped::SkipTemplate::not_skipped_bad, 44, DEADLOCK, no_bucket, ERROR, [In method `skipped::SkipTemplate::not_skipped_bad`,Method call: `skipped::SkipTemplate::private_deadlock`, locks `&(this->mutex_)` in `class skipped::SkipTemplate`, locks `&(this->mutex_)` in `class skipped::SkipTemplate`] codetoanalyze/cpp/starvation/skip.cpp, skipped::UseTemplate::foo, 51, DEADLOCK, no_bucket, ERROR, [In method `skipped::UseTemplate::foo`,Method call: `skipped::SkipTemplate::not_skipped_bad`,Method call: `skipped::SkipTemplate::private_deadlock`, locks `&(this->x.mutex_)` in `class skipped::UseTemplate`, locks `&(this->x.mutex_)` in `class skipped::UseTemplate`] +codetoanalyze/cpp/starvation/substitution.cpp, substitution::ParamLocks::thread1_bad, 48, DEADLOCK, no_bucket, ERROR, [[Trace 1] `substitution::ParamLocks::thread1_bad`, locks `&(this->mutex_1)` in `class substitution::ParamLocks`, locks `&(this->mutex_2)` in `class substitution::ParamLocks` locked at substitution::ParamLocks::lock_param at line 49, column 5, locks `m` in `class std::mutex`,[Trace 2] `substitution::ParamLocks::thread2_bad`, locks `&(this->mutex_2)` in `class substitution::ParamLocks`, locks `&(this->mutex_1)` in `class substitution::ParamLocks`] +codetoanalyze/cpp/starvation/substitution.cpp, substitution::ParamLocks::thread2_bad, 55, DEADLOCK, no_bucket, ERROR, [[Trace 1] `substitution::ParamLocks::thread2_bad`, locks `&(this->mutex_2)` in `class substitution::ParamLocks`, locks `&(this->mutex_1)` in `class substitution::ParamLocks`,[Trace 2] `substitution::ParamLocks::thread1_bad`, locks `&(this->mutex_1)` in `class substitution::ParamLocks`, locks `&(this->mutex_2)` in `class substitution::ParamLocks` locked at substitution::ParamLocks::lock_param at line 49, column 5, locks `m` in `class std::mutex`] +codetoanalyze/cpp/starvation/substitution.cpp, substitution::TwoLevels::thread1_bad, 77, DEADLOCK, no_bucket, ERROR, [[Trace 1] `substitution::TwoLevels::thread1_bad`, locks `&(this->mutex_1)` in `class substitution::TwoLevels`,Method call: `substitution::TwoLevels::lock_and_unlock_param`, locks `&(this->mutex_2)` in `class substitution::TwoLevels` locked at substitution::TwoLevels::lock_param at line 72, column 5, locks `m` in `class std::mutex`,[Trace 2] `substitution::TwoLevels::thread2_bad`, locks `&(this->mutex_2)` in `class substitution::TwoLevels`, locks `&(this->mutex_1)` in `class substitution::TwoLevels`] +codetoanalyze/cpp/starvation/substitution.cpp, substitution::TwoLevels::thread2_bad, 83, DEADLOCK, no_bucket, ERROR, [[Trace 1] `substitution::TwoLevels::thread2_bad`, locks `&(this->mutex_2)` in `class substitution::TwoLevels`, locks `&(this->mutex_1)` in `class substitution::TwoLevels`,[Trace 2] `substitution::TwoLevels::thread1_bad`, locks `&(this->mutex_1)` in `class substitution::TwoLevels`,Method call: `substitution::TwoLevels::lock_and_unlock_param`, locks `&(this->mutex_2)` in `class substitution::TwoLevels` locked at substitution::TwoLevels::lock_param at line 72, column 5, locks `m` in `class std::mutex`] +codetoanalyze/cpp/starvation/substitution.cpp, substitution::Returned::thread1_bad, 99, DEADLOCK, no_bucket, ERROR, [[Trace 1] `substitution::Returned::thread1_bad`,Method call: `substitution::Returned::lock_it`, locks `&(this->mutex_)` in `class substitution::Returned`, locks `&(this->mutex_1)` in `class substitution::Returned`,[Trace 2] `substitution::Returned::thread2_bad`, locks `&(this->mutex_1)` in `class substitution::Returned`, locks `&(this->mutex_)` in `class substitution::Returned` locked at substitution::Returned::lock_it at line 112, column 5, locks `&(this->mutex_)` in `class substitution::Returned`] +codetoanalyze/cpp/starvation/substitution.cpp, substitution::Returned::thread2_bad, 111, DEADLOCK, no_bucket, ERROR, [[Trace 1] `substitution::Returned::thread2_bad`, locks `&(this->mutex_1)` in `class substitution::Returned`, locks `&(this->mutex_)` in `class substitution::Returned` locked at substitution::Returned::lock_it at line 112, column 5, locks `&(this->mutex_)` in `class substitution::Returned`,[Trace 2] `substitution::Returned::thread1_bad`,Method call: `substitution::Returned::lock_it`, locks `&(this->mutex_)` in `class substitution::Returned`, locks `&(this->mutex_1)` in `class substitution::Returned`] diff --git a/infer/tests/codetoanalyze/cpp/starvation/release_in_callee.cpp b/infer/tests/codetoanalyze/cpp/starvation/release_in_callee.cpp new file mode 100644 index 00000000000..803bc9277c1 --- /dev/null +++ b/infer/tests/codetoanalyze/cpp/starvation/release_in_callee.cpp @@ -0,0 +1,119 @@ +/* + * 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. + */ + +#include + +// a callee may release a lock held by its caller and take it again later +namespace release_in_callee { + +void sleep_a_bit(); + +class Mutex { + public: + void Lock() { mu_.lock(); } + + void Unlock() { mu_.unlock(); } + + private: + std::mutex mu_; +}; + +class MutexLock { + public: + explicit MutexLock(Mutex* mu) : mu_(mu) { mu_->Lock(); } + + ~MutexLock() { mu_->Unlock(); } + + private: + Mutex* const mu_; +}; + +class Relock { + public: + void guard_ok() { + MutexLock l(&mutex_); + make_room(); + } + + void lock_guard_ok() { + std::lock_guard l(mutex_1); + relock_1(); + } + + private: + void make_room() { + if (busy_) { + mutex_.Unlock(); + sleep_a_bit(); + mutex_.Lock(); + } + } + + void relock_1() { + if (busy_) { + mutex_1.unlock(); + sleep_a_bit(); + mutex_1.lock(); + } + } + + Mutex mutex_; + std::mutex mutex_1; + bool busy_; +}; + +class TakeWhileReleased { + public: + void thread1_ok() { + std::lock_guard l(mutex_1); + take_2(); + } + + void thread2_ok() { + std::lock_guard lock2(mutex_2); + std::lock_guard lock1(mutex_1); + } + + private: + void take_2() { + if (busy_) { + mutex_1.unlock(); + { + std::lock_guard lock2(mutex_2); + } + mutex_1.lock(); + } + } + + std::mutex mutex_1; + std::mutex mutex_2; + bool busy_; +}; + +class RelockUnderOtherLock { + public: + // retakes mutex_1 while holding mutex_2 + void relock_bad() { + std::lock_guard lock1(mutex_1); + std::lock_guard lock2(mutex_2); + relock_1(); + } + + private: + void relock_1() { + if (busy_) { + mutex_1.unlock(); + sleep_a_bit(); + mutex_1.lock(); + } + } + + std::mutex mutex_1; + std::mutex mutex_2; + bool busy_; +}; +} // namespace release_in_callee diff --git a/infer/tests/codetoanalyze/cpp/starvation/substitution.cpp b/infer/tests/codetoanalyze/cpp/starvation/substitution.cpp new file mode 100644 index 00000000000..7c12021543b --- /dev/null +++ b/infer/tests/codetoanalyze/cpp/starvation/substitution.cpp @@ -0,0 +1,150 @@ +/* + * 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. + */ + +#include + +namespace substitution { + +// locks taken in a callee on a local object are not tracked, and must not be +// confused with locks of the caller's parameters +struct Holder { + void lock_it() { mutex_.lock(); } + + void unlock_it() { mutex_.unlock(); } + + void lock_local_and_this_ok() { + Holder tmp; + tmp.lock_it(); + lock_it(); + unlock_it(); + tmp.unlock_it(); + } + + std::mutex mutex_; +}; + +void two_locals_ok() { + Holder h1; + Holder h2; + h1.lock_it(); + h2.lock_it(); + h2.unlock_it(); + h1.unlock_it(); +} + +// locks rooted at any parameter of the callee are substituted, even if the +// caller has fewer parameters than the callee +class ParamLocks { + public: + void lock_param(std::mutex* m) { m->lock(); } + + void unlock_param(std::mutex* m) { m->unlock(); } + + void thread1_bad() { + mutex_1.lock(); + lock_param(&mutex_2); + unlock_param(&mutex_2); + mutex_1.unlock(); + } + + void thread2_bad() { + mutex_2.lock(); + mutex_1.lock(); + mutex_1.unlock(); + mutex_2.unlock(); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +// locks taken by the callee of a callee are substituted at each call +class TwoLevels { + public: + void lock_param(std::mutex* m) { m->lock(); } + + void lock_and_unlock_param(std::mutex* m) { + lock_param(m); + m->unlock(); + } + + void thread1_bad() { + mutex_1.lock(); + lock_and_unlock_param(&mutex_2); + mutex_1.unlock(); + } + + void thread2_bad() { + mutex_2.lock(); + mutex_1.lock(); + mutex_1.unlock(); + mutex_2.unlock(); + } + + private: + std::mutex mutex_1; + std::mutex mutex_2; +}; + +// locks left held on an object returned by a call are kept +class Returned { + public: + static Returned* get(); + + void lock_it() { mutex_.lock(); } + + void unlock_it() { mutex_.unlock(); } + + void thread1_bad() { + get()->lock_it(); + mutex_1.lock(); + mutex_1.unlock(); + get()->unlock_it(); + } + + void thread2_bad() { + mutex_1.lock(); + lock_it(); + unlock_it(); + mutex_1.unlock(); + } + + private: + std::mutex mutex_; + std::mutex mutex_1; +}; + +// critical sections of a callee on an object the caller cannot name, eg a fresh +// one, are not kept +class Fresh; + +struct Locked { + void lock_and_unlock() { std::lock_guard l(mutex_); } + + void lock_then_ok(Fresh* f); + + std::mutex mutex_; +}; + +class Fresh { + public: + void fresh_ok() { + std::lock_guard l(mutex_); + Locked* p = new Locked(); + p->lock_and_unlock(); + delete p; + } + + std::mutex mutex_; +}; + +void Locked::lock_then_ok(Fresh* f) { + std::lock_guard l(mutex_); + std::lock_guard l2(f->mutex_); +} +} // namespace substitution diff --git a/infer/tests/codetoanalyze/java/starvation/Parameters.java b/infer/tests/codetoanalyze/java/starvation/Parameters.java index e0deec79b2f..e0179fac9c8 100644 --- a/infer/tests/codetoanalyze/java/starvation/Parameters.java +++ b/infer/tests/codetoanalyze/java/starvation/Parameters.java @@ -41,4 +41,86 @@ public void anotherWayEmulateSyncBad() { } } } + + static Parameters sObject; + static final Object sLock = new Object(); + + // Next two methods will deadlock; the first one has no parameters + static void staticEmulateSyncBad() { + synchronized (sLock) { + emulateSynchronized(sObject); + } + } + + void staticObjectThenLockBad() { + synchronized (sObject) { + synchronized (sLock) { + } + } + } + + final java.util.concurrent.locks.Lock mLock = new java.util.concurrent.locks.ReentrantLock(); + + static Parameters getInstance() { + return sObject; + } + + void acquire() { + mLock.lock(); + } + + void release() { + mLock.unlock(); + } + + // the next three methods deadlock if the instance is sObject + static void instanceThenStaticLockBad() { + getInstance().acquire(); + synchronized (sLock) { + } + getInstance().release(); + } + + static void localInstanceThenStaticLockBad() { + Parameters p = getInstance(); + p.acquire(); + synchronized (sLock) { + } + p.release(); + } + + void staticLockThenInstanceBad() { + synchronized (sLock) { + mLock.lock(); + mLock.unlock(); + } + } + + static void acquireAndRelease(Parameters p) { + p.acquire(); + p.release(); + } + + static void staticLockThenHelperBad() { + synchronized (sLock) { + acquireAndRelease(getInstance()); + } + } + + synchronized void syncMethod() {} + + void thisThenStaticLockOk() { + synchronized (this) { + synchronized (sLock) { + } + } + } + + // a fresh object cannot be locked by another thread + static void staticLockThenFreshOk() { + Parameters p = new Parameters(); + synchronized (sLock) { + p.syncMethod(); + } + } } diff --git a/infer/tests/codetoanalyze/java/starvation/issues.exp b/infer/tests/codetoanalyze/java/starvation/issues.exp index e45c09a6916..60cc6778854 100644 --- a/infer/tests/codetoanalyze/java/starvation/issues.exp +++ b/infer/tests/codetoanalyze/java/starvation/issues.exp @@ -65,6 +65,16 @@ codetoanalyze/java/starvation/ObjWait.java, ObjWait.indirectWaitSameLockOnMainOk codetoanalyze/java/starvation/Parameters.java, Parameters.otherWaySyncOnParamBad(java.lang.Object):void, 20, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.otherWaySyncOnParamBad(Object)`, locks `x` in `class java.lang.Object`, locks `this` in `class Parameters`,[Trace 2] `void Parameters.oneWaySyncOnParamBad(Object)`, locks `this` in `class Parameters`,Method call: `void Parameters.syncOnParam(Object)`, locks `x` in `class java.lang.Object`] codetoanalyze/java/starvation/Parameters.java, Parameters.oneWayEmulateSyncBad():void, 34, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.oneWayEmulateSyncBad()`, locks `this` in `class Parameters`,Method call: `void Parameters.emulateSynchronized(Parameters)`, locks `this.someObject` in `class Parameters`,[Trace 2] `void Parameters.anotherWayEmulateSyncBad()`, locks `this.someObject` in `class Parameters`, locks `this` in `class Parameters`] codetoanalyze/java/starvation/Parameters.java, Parameters.anotherWayEmulateSyncBad():void, 39, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.anotherWayEmulateSyncBad()`, locks `this.someObject` in `class Parameters`, locks `this` in `class Parameters`,[Trace 2] `void Parameters.oneWayEmulateSyncBad()`, locks `this` in `class Parameters`,Method call: `void Parameters.emulateSynchronized(Parameters)`, locks `this.someObject` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.staticEmulateSyncBad():void, 50, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.staticEmulateSyncBad()`, locks `Parameters.sLock` in `class Parameters`,Method call: `void Parameters.emulateSynchronized(Parameters)`, locks `Parameters.sObject` in `class Parameters`,[Trace 2] `void Parameters.staticObjectThenLockBad()`, locks `Parameters.sObject` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.staticObjectThenLockBad():void, 56, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.staticObjectThenLockBad()`, locks `Parameters.sObject` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`,[Trace 2] `void Parameters.staticEmulateSyncBad()`, locks `Parameters.sLock` in `class Parameters`,Method call: `void Parameters.emulateSynchronized(Parameters)`, locks `Parameters.sObject` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.instanceThenStaticLockBad():void, 69, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.instanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`,[Trace 2] `void Parameters.staticLockThenHelperBad()`, locks `Parameters.sLock` in `class Parameters`,Method call: `void Parameters.acquireAndRelease(Parameters)`, locks `p.mLock` in `class Parameters` locked at void Parameters.acquire() at line 100, locks `this.mLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.instanceThenStaticLockBad():void, 69, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.instanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`,[Trace 2] `void Parameters.staticLockThenInstanceBad()`, locks `Parameters.sLock` in `class Parameters`, locks `this.mLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.localInstanceThenStaticLockBad():void, 69, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.localInstanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`,[Trace 2] `void Parameters.staticLockThenHelperBad()`, locks `Parameters.sLock` in `class Parameters`,Method call: `void Parameters.acquireAndRelease(Parameters)`, locks `p.mLock` in `class Parameters` locked at void Parameters.acquire() at line 100, locks `this.mLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.localInstanceThenStaticLockBad():void, 69, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.localInstanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`,[Trace 2] `void Parameters.staticLockThenInstanceBad()`, locks `Parameters.sLock` in `class Parameters`, locks `this.mLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.staticLockThenInstanceBad():void, 93, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.staticLockThenInstanceBad()`, locks `Parameters.sLock` in `class Parameters`, locks `this.mLock` in `class Parameters`,[Trace 2] `void Parameters.instanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.staticLockThenInstanceBad():void, 93, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.staticLockThenInstanceBad()`, locks `Parameters.sLock` in `class Parameters`, locks `this.mLock` in `class Parameters`,[Trace 2] `void Parameters.localInstanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.staticLockThenHelperBad():void, 105, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.staticLockThenHelperBad()`, locks `Parameters.sLock` in `class Parameters`,Method call: `void Parameters.acquireAndRelease(Parameters)`, locks `p.mLock` in `class Parameters` locked at void Parameters.acquire() at line 100, locks `this.mLock` in `class Parameters`,[Trace 2] `void Parameters.instanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`] +codetoanalyze/java/starvation/Parameters.java, Parameters.staticLockThenHelperBad():void, 105, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void Parameters.staticLockThenHelperBad()`, locks `Parameters.sLock` in `class Parameters`,Method call: `void Parameters.acquireAndRelease(Parameters)`, locks `p.mLock` in `class Parameters` locked at void Parameters.acquire() at line 100, locks `this.mLock` in `class Parameters`,[Trace 2] `void Parameters.localInstanceThenStaticLockBad()`,Method call: `void Parameters.acquire()`, locks `this.mLock` in `class Parameters`, locks `Parameters.sLock` in `class Parameters`] codetoanalyze/java/starvation/PubPriv.java, PubPriv.transactBad():void, 21, IPC_ON_UI_THREAD, no_bucket, WARNING, [`void PubPriv.transactBad()`,Method call: `void PubPriv.doTransactOk()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] codetoanalyze/java/starvation/PubPriv.java, PubPriv.alsoBad():void, 25, IPC_ON_UI_THREAD, no_bucket, WARNING, [`void PubPriv.alsoBad()`,Method call: `void PubPriv.transactBad()`,Method call: `void PubPriv.doTransactOk()`,calls `boolean Binder.transact(int,Parcel,Parcel,int)`] codetoanalyze/java/starvation/PubPriv.java, PubPriv.callOneWayBad():void, 49, DEADLOCK, no_bucket, ERROR, [[Trace 1] `void PubPriv.callOneWayBad()`,Method call: `void PubPriv.oneWayOk()`, locks `this.lockA` in `class PubPriv`, locks `this.lockB` in `class PubPriv`,[Trace 2] `void PubPriv.callAnotherWayBad()`,Method call: `void PubPriv.anotherWayOk()`, locks `this.lockB` in `class PubPriv`, locks `this.lockA` in `class PubPriv`]