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