Skip to main content

asc_engine/
compose.rs

1//! Conditions answered over the whole APK before a search uses them:
2//! `method:{RULE}` ([`Query::Nested`]) becomes the set of methods whose code
3//! matches RULE, a [`Query::Virtual`] set gains the calls that may dispatch
4//! to its methods, and callgraph endpoints become method signatures.
5//!
6//! Every condition of one call is answered by one rule-set pass
7//! ([`Engine::find_refs_rules`]), which also keeps each of them in L3.
8
9use crate::hierarchy::{ClassGraph, Defining};
10use crate::{CacheConfig, DexView, Engine, EngineError, FindOptions, Lineage, Result, names};
11use asc_dex::Dex;
12use asc_findrefs::{ClassQuery, CompiledQuery, Defined, MemberQuery, Query};
13use std::collections::{BTreeMap, BTreeSet, HashMap};
14use std::sync::atomic::AtomicBool;
15
16/// Most classes of the program that may define a supertype's method for a
17/// dispatch set to follow the calls naming it ([`Query::Virtual`]'s
18/// `fan_out`). An AIDL interface has its `Stub`, `Stub.Proxy`, `Default`
19/// and a service or two; an abstract base a handful of subclasses. A
20/// functional interface (`Runnable`, Kotlin's `FunctionN`) or a lifecycle
21/// callback has hundreds to thousands, and its calls say nothing about
22/// which of them runs.
23pub const DEFAULT_FAN_OUT: usize = 16;
24/// Most subclasses one method of a dispatch set is followed into.
25const MAX_INHERITORS: usize = 10_000;
26/// Most supertypes of one class a dispatch alias is made on.
27const MAX_SUPERS: usize = 1_000;
28/// Most classes of a supertype's subtree walked to count the classes
29/// defining its method. A broader supertype is not followed whatever the
30/// count: its calls may land on any of thousands of receivers (a Kotlin
31/// lambda base, `Serializable`), and walking it per method is most of the
32/// cost of a dispatch set.
33const MAX_WALK: usize = 1_024;
34/// Classes a `(dispatch ...)` note names before it counts the rest.
35const MAX_LABEL_CLASSES: usize = 3;
36
37/// The methods whose code matches each query of a
38/// [`Engine::matching_methods`] call, and its notes.
39pub struct MatchingMethods {
40    /// `Lcls;->name(params)ret` per query.
41    pub sets: Vec<BTreeSet<String>>,
42    /// The supertype methods whose calls a dispatch set did not follow.
43    pub notes: Vec<String>,
44}
45
46/// Queries resolved over the whole program, and the notes of what the
47/// resolution left out.
48pub(crate) struct Resolved {
49    /// `None` when nothing had to be resolved.
50    pub queries: Option<Vec<Query>>,
51    /// Per query, sorted and each once.
52    pub notes: Vec<Vec<String>>,
53}
54
55impl Resolved {
56    fn nothing(queries: usize) -> Resolved {
57        Resolved { queries: None, notes: vec![Vec::new(); queries] }
58    }
59}
60
61impl Engine {
62    /// The signatures (`Lcls;->name(params)ret`) of the methods whose code
63    /// matches each query, all queries in one pass.
64    pub fn matching_methods(&self, queries: &[Query], cancel: &AtomicBool) -> Result<MatchingMethods> {
65        if queries.iter().any(|q| q.relation().is_some()) {
66            return Err(EngineError::Query(
67                "extends: / impl: name classes, not code: a method condition needs string, literal, type, method or field conditions"
68                    .into(),
69            ));
70        }
71        let mut sets = vec![BTreeSet::new(); queries.len()];
72        let mut errors = Vec::new();
73        let summary = self.find_refs_rules(queries, FindOptions::default(), cancel, &mut |i, batch| {
74            if let Some(e) = batch.error {
75                errors.push(format!("{}: {e}", batch.entry_name));
76            }
77            sets[i].extend(batch.results.into_iter().map(|r| r.signature));
78        })?;
79        // A method missing from the set would silently drop rows downstream.
80        if !errors.is_empty() {
81            return Err(EngineError::Query(format!("cannot answer a method condition: {}", errors.join("; "))));
82        }
83        Ok(MatchingMethods { sets, notes: summary.notes })
84    }
85
86    /// `queries` with every condition that needs the whole program resolved:
87    /// `override:` / `in:{...}` over the class hierarchy, `method:{RULE}`
88    /// over the code, then the dispatch aliases of each [`Query::Virtual`]
89    /// set.
90    pub(crate) fn resolve(&self, queries: &[Query], cancel: &AtomicBool) -> Result<Resolved> {
91        let defs = self.resolve_defined(queries, cancel)?;
92        let nested = self.resolve_nested(defs.as_deref().unwrap_or(queries), cancel)?;
93        let base = nested.queries.as_deref().or(defs.as_deref()).unwrap_or(queries);
94        let dispatched = self.resolve_dispatch(base, cancel)?;
95        let notes = nested.notes.into_iter().zip(dispatched.notes).map(|(a, b)| merge_notes(a.into_iter().chain(b).collect())).collect();
96        Ok(Resolved { queries: dispatched.queries.or(nested.queries).or(defs), notes })
97    }
98
99    /// For a call that reads every DEX more than once (conditions resolved
100    /// over the program, then the search) on an engine without a cache: an
101    /// engine like this one whose memory cache keeps the DEX files the first
102    /// pass inflated for the next ones. `None` when this engine has a cache
103    /// or `needed` is false.
104    pub(crate) fn transient(&self, needed: bool) -> Result<Option<Engine>> {
105        if !needed || self.cache.is_some() {
106            return Ok(None);
107        }
108        // Room for every DEX and its tables, so no pass evicts what the next reads.
109        let dex_bytes: u64 = self.open()?.dex_entries()?.iter().map(|e| e.uncomp_size).sum();
110        let memory_bytes = usize::try_from(dex_bytes.saturating_mul(2)).unwrap_or(usize::MAX).max(64 << 20);
111        let mut engine = Engine::new(&self.apk_path).with_threads(self.threads).with_limits(self.limits).with_debug(self.debug);
112        // Logged and counted as this engine.
113        engine.logger = self.logger.clone();
114        engine.stats = self.stats.clone();
115        Ok(Some(engine.with_cache(CacheConfig { memory_bytes, ..Default::default() })))
116    }
117
118    /// `queries` with each [`Query::Virtual`] set extended by the calls that
119    /// may dispatch to its methods ([`Hierarchy::dispatch_aliases`]).
120    pub(crate) fn resolve_dispatch(&self, queries: &[Query], cancel: &AtomicBool) -> Result<Resolved> {
121        // (query index, set, fan-out) of every unresolved dispatch set
122        let mut sets = Vec::new();
123        for (i, q) in queries.iter().enumerate() {
124            let mut own = Vec::new();
125            virtual_sets(q, &mut own);
126            sets.extend(own.into_iter().map(|(set, fan_out)| (i, set, fan_out)));
127        }
128        if sets.is_empty() {
129            return Ok(Resolved::nothing(queries.len()));
130        }
131        // Constructors, initializers and members without one class and an
132        // exact name dispatch nowhere: they need no hierarchy.
133        let graph = if sets.iter().flat_map(|(_, s, _)| s.iter()).any(|m| dispatch_target(m).is_some()) {
134            let (views, mut errors) = self.load_views(None, cancel)?;
135            let graph = self.class_graph(&views, &mut errors, cancel)?;
136            Some((views, graph))
137        } else {
138            None
139        };
140        let mut hierarchy = graph.as_ref().map(|(views, graph)| Hierarchy::new(graph, views)).transpose()?;
141        let mut notes = vec![Vec::new(); queries.len()];
142        let mut expanded = Vec::with_capacity(sets.len());
143        for (i, set, fan_out) in sets {
144            expanded.push(match hierarchy.as_mut() {
145                Some(h) => {
146                    let e = h.dispatch_aliases(set, fan_out)?;
147                    notes[i].extend(e.skipped.iter().map(|(alias, n)| skipped_note(alias, *n)));
148                    (e.set, e.via)
149                }
150                None => (set.to_vec(), BTreeMap::new()),
151            });
152        }
153        let mut expanded = expanded.into_iter();
154        let queries = queries.iter().map(|q| with_aliases(q, &mut expanded)).collect();
155        Ok(Resolved { queries: Some(queries), notes: notes.into_iter().map(merge_notes).collect() })
156    }
157
158    /// `queries` with every `method:{RULE}` replaced by the methods RULE
159    /// matches. The notes of the inner rules go to each query holding one.
160    pub(crate) fn resolve_nested(&self, queries: &[Query], cancel: &AtomicBool) -> Result<Resolved> {
161        let mut inner = Vec::new();
162        let mut holds = Vec::with_capacity(queries.len());
163        for q in queries {
164            let before = inner.len();
165            nested_rules(q, &mut inner);
166            holds.push(inner.len() > before);
167        }
168        if inner.is_empty() {
169            return Ok(Resolved::nothing(queries.len()));
170        }
171        let inner: Vec<Query> = inner.into_iter().cloned().collect();
172        let matching = self.matching_methods(&inner, cancel)?;
173        let mut sets = matching.sets.into_iter();
174        let resolved = queries.iter().map(|q| substitute(q, &mut sets)).collect::<Result<_>>()?;
175        let notes = holds.into_iter().map(|h| if h { matching.notes.clone() } else { Vec::new() }).collect();
176        Ok(Resolved { queries: Some(resolved), notes })
177    }
178}
179
180/// The note of a supertype method a dispatch set did not follow.
181fn skipped_note(alias: &str, defining: Defining) -> String {
182    let why = match defining {
183        Defining { count, complete: true } => format!("{count} implementers"),
184        Defining { count, complete: false } => format!("over {MAX_WALK} subclasses, {count}+ implementers"),
185    };
186    format!("dispatch not followed via {alias} ({why}): name it explicitly, or write method:*{{...}}, to include")
187}
188
189/// `notes` sorted, each once.
190pub(crate) fn merge_notes(mut notes: Vec<String>) -> Vec<String> {
191    notes.sort();
192    notes.dedup();
193    notes
194}
195
196/// Whether answering `q` reads the whole program before its search:
197/// `method:{RULE}`, a dispatch set, `override:` or `in:{...}`.
198pub(crate) fn needs_program(q: &Query) -> bool {
199    match q {
200        Query::Nested(_) | Query::Virtual { .. } | Query::Defined(Defined::Override(_) | Defined::In(_)) => true,
201        Query::All { all, none } => all.iter().chain(none).any(needs_program),
202        Query::Call { target, .. } => needs_program(target),
203        Query::String(_)
204        | Query::StringLiteral(_)
205        | Query::Type(_)
206        | Query::Method(_)
207        | Query::MethodSet(_)
208        | Query::Defined(_)
209        | Query::Field(_)
210        | Query::FieldAccess { .. }
211        | Query::Extends(_)
212        | Query::Implements(_) => false,
213    }
214}
215
216/// Compile `q` and its inner rules, each `method:{RULE}` standing for no
217/// method: a bad pattern anywhere fails before any search runs.
218pub(crate) fn check_patterns(q: &Query) -> Result<()> {
219    let mut inner = Vec::new();
220    nested_rules(q, &mut inner);
221    for rule in inner {
222        check_patterns(rule)?;
223    }
224    CompiledQuery::new(&substitute(q, &mut std::iter::repeat_with(BTreeSet::new))?)?;
225    Ok(())
226}
227
228/// The inner rules of the `method:{RULE}` conditions of `q`, in the order
229/// [`substitute`] replaces them.
230fn nested_rules<'q>(q: &'q Query, out: &mut Vec<&'q Query>) {
231    match q {
232        Query::Nested(inner) => out.push(inner),
233        Query::All { all, none } => all.iter().chain(none).for_each(|p| nested_rules(p, out)),
234        Query::Call { target, .. } | Query::Virtual { target, .. } => nested_rules(target, out),
235        Query::String(_)
236        | Query::StringLiteral(_)
237        | Query::Type(_)
238        | Query::Method(_)
239        | Query::MethodSet(_)
240        | Query::Defined(_)
241        | Query::Field(_)
242        | Query::FieldAccess { .. }
243        | Query::Extends(_)
244        | Query::Implements(_) => {}
245    }
246}
247
248/// `q` with each `method:{RULE}` replaced by the next method set of `sets`.
249fn substitute(q: &Query, sets: &mut impl Iterator<Item = BTreeSet<String>>) -> Result<Query> {
250    Ok(match q {
251        Query::Nested(_) => {
252            let methods = sets.next().expect("one method set per nested rule");
253            Query::MethodSet(methods.iter().map(|m| names::exact_method(m)).collect::<Result<_>>()?)
254        }
255        Query::All { all, none } => Query::All {
256            all: all.iter().map(|p| substitute(p, sets)).collect::<Result<_>>()?,
257            none: none.iter().map(|p| substitute(p, sets)).collect::<Result<_>>()?,
258        },
259        Query::Call { target, constraints } => {
260            Query::Call { target: Box::new(substitute(target, sets)?), constraints: constraints.clone() }
261        }
262        Query::Virtual { target, via, fan_out } => {
263            Query::Virtual { target: Box::new(substitute(target, sets)?), via: via.clone(), fan_out: *fan_out }
264        }
265        // `check_patterns` compiles before resolution: stand an unresolved
266        // predicate in for its resolved form so compiling does not reject it.
267        Query::Defined(Defined::Override(_) | Defined::In(_)) => {
268            Query::Defined(Defined::Members { classes: Vec::new(), name: None, proto: None })
269        }
270        q => q.clone(),
271    })
272}
273
274/// The method sets (and fan-out limits) of the [`Query::Virtual`]
275/// conditions of `q` not yet resolved, in the order [`with_aliases`]
276/// replaces them. Their targets are method sets by now:
277/// [`Engine::resolve_nested`] runs first.
278fn virtual_sets<'q>(q: &'q Query, out: &mut Vec<(&'q [MemberQuery], Option<usize>)>) {
279    match q {
280        Query::Virtual { target, via, fan_out } if via.is_empty() => {
281            if let Query::MethodSet(set) = &**target {
282                out.push((set, *fan_out));
283            }
284        }
285        Query::All { all, none } => all.iter().chain(none).for_each(|p| virtual_sets(p, out)),
286        Query::Call { target, .. } => virtual_sets(target, out),
287        _ => {}
288    }
289}
290
291/// `q` with each set [`virtual_sets`] found replaced by the next of `sets`.
292fn with_aliases(q: &Query, sets: &mut impl Iterator<Item = (Vec<MemberQuery>, BTreeMap<String, String>)>) -> Query {
293    match q {
294        Query::Virtual { target, via, fan_out } if via.is_empty() && matches!(**target, Query::MethodSet(_)) => {
295            let (set, via) = sets.next().expect("one expansion per dispatch set");
296            Query::Virtual { target: Box::new(Query::MethodSet(set)), via, fan_out: *fan_out }
297        }
298        Query::All { all, none } => Query::All {
299            all: all.iter().map(|p| with_aliases(p, sets)).collect(),
300            none: none.iter().map(|p| with_aliases(p, sets)).collect(),
301        },
302        Query::Call { target, constraints } => {
303            Query::Call { target: Box::new(with_aliases(target, sets)), constraints: constraints.clone() }
304        }
305        q => q.clone(),
306    }
307}
308
309/// What a set member dispatches on: its class, its exact name and, when
310/// concrete, its proto (`(I)V`). `None` for a member naming no single class
311/// and name, and for constructors and initializers.
312fn dispatch_target(m: &MemberQuery) -> Option<(&str, &str, Option<String>)> {
313    let class = m.class.as_ref().filter(|c| c.precise)?;
314    let name = m.name.as_deref().filter(|n| m.exact_name && !n.starts_with('<'))?;
315    Some((&class.pattern, name, m.signature.as_ref().and_then(crate::defs::descriptor_proto)))
316}
317
318/// A dispatch set resolved by [`Hierarchy::dispatch_aliases`].
319struct Expansion {
320    /// The set's methods, then its aliases.
321    set: Vec<MemberQuery>,
322    /// Alias key -> the `(dispatch ...)` label of its calls.
323    via: BTreeMap<String, String>,
324    /// Supertype aliases left out by the fan-out limit, with how many
325    /// classes define them.
326    skipped: BTreeMap<String, Defining>,
327}
328
329/// The class hierarchy of the program, with what each class declares read
330/// at most once: the dispatch sets of a call walk the same subtrees (a
331/// Kotlin lambda base, say) for many methods.
332struct Hierarchy<'g> {
333    graph: &'g ClassGraph,
334    /// One per view, parsed once.
335    dexes: Vec<Dex<'g>>,
336    /// Class -> its view and the `method_idx` of every method it defines.
337    declared: HashMap<String, (usize, Vec<u32>)>,
338    /// Alias key -> the classes defining it in its class's subtree.
339    defining: HashMap<String, Defining>,
340}
341
342impl<'g> Hierarchy<'g> {
343    fn new(graph: &'g ClassGraph, views: &'g [DexView]) -> Result<Hierarchy<'g>> {
344        let dexes = views.iter().map(|v| Dex::with_name(&v.data, &v.name)).collect::<std::result::Result<_, _>>()?;
345        Ok(Hierarchy { graph, dexes, declared: HashMap::new(), defining: HashMap::new() })
346    }
347
348    /// Whether `class` defines `name` (with `proto`, or any overload). Names
349    /// are compared as bytes; a proto is only formatted for a matching name.
350    fn defines(&mut self, class: &str, name: &str, proto: Option<&str>) -> Result<bool> {
351        if !self.declared.contains_key(class) {
352            let methods = match self.graph.definition(class) {
353                Some((view, def_idx)) => {
354                    let dex = &self.dexes[view];
355                    let def = dex.class_def(def_idx)?;
356                    let ids = if def.class_data_off == 0 {
357                        Vec::new()
358                    } else {
359                        let data = dex.class_data(def.class_data_off)?;
360                        data.direct_methods.iter().chain(&data.virtual_methods).map(|m| m.method_idx).collect()
361                    };
362                    (view, ids)
363                }
364                None => (0, Vec::new()),
365            };
366            self.declared.insert(class.to_string(), methods);
367        }
368        let (view, ids) = &self.declared[class];
369        let Some(dex) = self.dexes.get(*view) else { return Ok(false) };
370        for &idx in ids {
371            let mid = dex.method_id(idx)?;
372            if dex.string_bytes(mid.name_idx)? == name.as_bytes()
373                && proto.is_none_or(|want| asc_smali::proto_descriptor(dex, u32::from(mid.proto_idx)).is_ok_and(|p| p == want))
374            {
375                return Ok(true);
376            }
377        }
378        Ok(false)
379    }
380
381    /// Whether `name` (`proto`) is a member of `class`: it or a supertype
382    /// declares it. A class outside the program may declare anything, so
383    /// one among them (`class` included) answers yes.
384    fn has_member(&mut self, class: &str, name: &str, proto: Option<&str>) -> Result<bool> {
385        let graph = self.graph;
386        for c in std::iter::once(class.to_string()).chain(graph.reachable(class, Lineage::Supers, MAX_SUPERS)) {
387            if c == "Ljava/lang/Object;" {
388                continue;
389            }
390            if graph.definition(&c).is_none() || self.defines(&c, name, proto)? {
391                return Ok(true);
392            }
393        }
394        Ok(false)
395    }
396
397    /// `set` plus the calls that may dispatch to one of its methods, and the
398    /// label of each one added. A call may land on `C->m` when it names `m`
399    /// on a subclass of `C` that inherits it, or on a supertype of `C` or of
400    /// such a subclass (an interface it implements, say): each of those
401    /// becomes an alias of `C`, keyed `Lsup;->m(params)ret` (`Lsup;->m` when
402    /// the set names no proto). `java.lang.Object` is left out: every class
403    /// has its methods, so its calls would match whatever the set holds.
404    ///
405    /// A supertype's method that more than `fan_out` classes of the program
406    /// define is skipped instead: its calls may run any of them. Calls
407    /// naming an inheriting subclass are always kept, since they can only
408    /// reach `C`'s method.
409    fn dispatch_aliases(&mut self, set: &[MemberQuery], fan_out: Option<usize>) -> Result<Expansion> {
410        let key = |class: &str, name: &str, proto: Option<&str>| format!("{class}->{name}{}", proto.unwrap_or(""));
411        let originals: BTreeSet<String> = set.iter().filter_map(dispatch_target).map(|(c, n, p)| key(c, n, p.as_deref())).collect();
412        // alias key -> its query and the set's classes it stands for
413        let mut aliases: BTreeMap<String, (MemberQuery, BTreeSet<&str>)> = BTreeMap::new();
414        let mut skipped = BTreeMap::new();
415        let graph = self.graph;
416        for m in set {
417            let Some((class, name, proto)) = dispatch_target(m) else { continue };
418            let proto = proto.as_deref();
419            let inheritors = graph.inheritors(class, MAX_INHERITORS, |c| self.defines(c, name, proto))?;
420            let subclasses = inheritors.iter().skip(1).map(|c| (c.clone(), false));
421            let supers = inheritors.iter().flat_map(|c| graph.reachable(c, Lineage::Supers, MAX_SUPERS)).map(|s| (s, true));
422            for (on, is_super) in subclasses.chain(supers) {
423                let k = key(&on, name, proto);
424                if on == "Ljava/lang/Object;" || originals.contains(&k) || skipped.contains_key(&k) {
425                    continue;
426                }
427                // No call names a method its class neither declares nor inherits.
428                if is_super && !self.has_member(&on, name, proto)? {
429                    continue;
430                }
431                if let (true, Some(limit)) = (is_super, fan_out) {
432                    let n = match self.defining.get(&k) {
433                        Some(&n) => n,
434                        None => {
435                            let n = graph.defining(&on, MAX_WALK, |c| self.defines(c, name, proto))?;
436                            self.defining.insert(k.clone(), n);
437                            n
438                        }
439                    };
440                    if !n.complete || n.count > limit {
441                        aliases.remove(&k);
442                        skipped.insert(k, n);
443                        continue;
444                    }
445                }
446                let (_, stands_for) = aliases.entry(k).or_insert_with(|| {
447                    (MemberQuery { class: Some(ClassQuery { pattern: on, precise: true }), ..m.clone() }, BTreeSet::new())
448                });
449                stands_for.insert(class);
450            }
451        }
452        let mut out = set.to_vec();
453        let mut via = BTreeMap::new();
454        for (k, (q, stands_for)) in aliases {
455            out.push(q);
456            via.insert(k, label(&stands_for));
457        }
458        Ok(Expansion { set: out, via, skipped })
459    }
460}
461
462/// `La;|Lb;`, or the first [`MAX_LABEL_CLASSES`] then `+N`.
463fn label(classes: &BTreeSet<&str>) -> String {
464    let mut out = classes.iter().take(MAX_LABEL_CLASSES).copied().collect::<Vec<_>>().join("|");
465    if classes.len() > MAX_LABEL_CLASSES {
466        out.push_str(&format!(" +{}", classes.len() - MAX_LABEL_CLASSES));
467    }
468    out
469}