Skip to main content

asc_findrefs/
scanner.rs

1//! Raw code-region scan for opcodes carrying a constant-pool index.
2
3use crate::insn_locator::InsnLocator;
4
5/// Which index table a query targets.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum RefKind {
8    String,
9    Type,
10    Method,
11    Field,
12}
13
14/// How a Dalvik field instruction uses its referenced field.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub enum FieldAccess {
17    Read,
18    Write,
19}
20
21impl FieldAccess {
22    /// Classify a field opcode. Reads are `iget*` / `sget*`; writes are
23    /// `iput*` / `sput*`.
24    pub const fn from_opcode(opcode: u8) -> Option<Self> {
25        match opcode {
26            0x52..=0x58 | 0x60..=0x66 => Some(FieldAccess::Read),
27            0x59..=0x5f | 0x67..=0x6d => Some(FieldAccess::Write),
28            _ => None,
29        }
30    }
31
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            FieldAccess::Read => "read",
35            FieldAccess::Write => "write",
36        }
37    }
38}
39
40impl RefKind {
41    pub fn as_str(self) -> &'static str {
42        match self {
43            RefKind::String => "string",
44            RefKind::Type => "type",
45            RefKind::Method => "method",
46            RefKind::Field => "field",
47        }
48    }
49}
50
51/// Dense bitset over constant-pool indexes.
52#[derive(Debug, Clone, Default)]
53pub struct IdxSet {
54    bits: Vec<u64>,
55}
56
57impl IdxSet {
58    pub fn insert(&mut self, idx: u32) {
59        let word = (idx >> 6) as usize;
60        if word >= self.bits.len() {
61            self.bits.resize(word + 1, 0);
62        }
63        self.bits[word] |= 1u64 << (idx & 63);
64    }
65
66    #[inline]
67    pub fn contains(&self, idx: u32) -> bool {
68        match self.bits.get((idx >> 6) as usize) {
69            Some(w) => w & (1u64 << (idx & 63)) != 0,
70            None => false,
71        }
72    }
73
74    pub fn is_empty(&self) -> bool {
75        self.bits.iter().all(|&w| w == 0)
76    }
77
78    /// Ascending members.
79    pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
80        self.bits.iter().enumerate().flat_map(|(w, &bits)| {
81            let mut bits = bits;
82            std::iter::from_fn(move || {
83                if bits == 0 {
84                    return None;
85                }
86                let t = bits.trailing_zeros();
87                bits &= bits - 1;
88                Some((w as u32) * 64 + t)
89            })
90        })
91    }
92}
93
94impl FromIterator<u32> for IdxSet {
95    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self {
96        let mut s = IdxSet::default();
97        for i in iter {
98            s.insert(i);
99        }
100        s
101    }
102}
103
104/// Opcode byte → operand width (0 = not an opcode of this kind).
105#[inline]
106fn opcode_width(kind: RefKind, b: u8) -> u8 {
107    match kind {
108        RefKind::String => match b {
109            0x1a => 2,
110            0x1b => 4,
111            _ => 0,
112        },
113        RefKind::Type => matches!(b, 0x1c | 0x1f | 0x20 | 0x22 | 0x23 | 0x24 | 0x25) as u8 * 2,
114        RefKind::Field => (b.wrapping_sub(0x52) <= 0x6d - 0x52) as u8 * 2,
115        RefKind::Method => (b.wrapping_sub(0x6e) <= 4 || b.wrapping_sub(0x74) <= 4 || b == 0xfa || b == 0xfb) as u8 * 2,
116    }
117}
118
119/// Above this many targets the per-target `memmem` pass costs more than one
120/// linear sweep over the region.
121const MEMMEM_MAX_TARGETS: usize = 8;
122
123/// Sweeps the code-item region of a DEX.
124pub struct CodeScanner<'l> {
125    insn: &'l InsnLocator,
126}
127
128impl<'l> CodeScanner<'l> {
129    pub fn new(insn: &'l InsnLocator) -> Self {
130        CodeScanner { insn }
131    }
132
133    /// Return `(absolute offset, matched index)` for every position in the code
134    /// region of `buf` whose opcode byte belongs to `kind` and whose operand is
135    /// in `idxs`, in ascending offset order. Positions are unverified: the
136    /// instruction locator decides whether they sit on an instruction boundary.
137    ///
138    /// `buf` must be the DEX buffer the instruction locator was built from.
139    pub fn scan(&self, buf: &[u8], kind: RefKind, idxs: &IdxSet) -> Vec<(u32, u32)> {
140        self.scan_inner(buf, kind, idxs, false)
141    }
142
143    /// Like [`CodeScanner::scan`], but omits byte-pattern matches at odd DEX
144    /// offsets. All real DEX instructions start at even offsets; the public
145    /// raw scan keeps them for low-level callers, while FindRefs uses this
146    /// path before method verification.
147    pub(crate) fn scan_instruction_candidates(&self, buf: &[u8], kind: RefKind, idxs: &IdxSet) -> Vec<(u32, u32)> {
148        self.scan_inner(buf, kind, idxs, true)
149    }
150
151    fn scan_inner(&self, buf: &[u8], kind: RefKind, idxs: &IdxSet, aligned: bool) -> Vec<(u32, u32)> {
152        let end = self.insn.code_item_end().min(buf.len());
153        let start = self.insn.code_item_start();
154        if start >= end || idxs.is_empty() {
155            return Vec::new();
156        }
157        let region = &buf[start..end];
158        let targets = idxs.iter().collect::<Vec<u32>>();
159        if targets.len() <= MEMMEM_MAX_TARGETS {
160            return scan_by_operand(region, start, kind, &targets, aligned);
161        }
162        scan_by_opcode(region, start, kind, idxs, aligned)
163    }
164}
165
166/// Few targets: search the operand bytes with SIMD `memmem` and check the
167/// opcode two bytes before each candidate. Operand bytes are far more
168/// selective than opcode bytes, so almost every position is skipped.
169fn scan_by_operand(region: &[u8], base: usize, kind: RefKind, targets: &[u32], aligned: bool) -> Vec<(u32, u32)> {
170    let mut out = Vec::new();
171    for &idx in targets {
172        let narrow = (idx as u16).to_le_bytes();
173        let wide = idx.to_le_bytes();
174        let needles: [(&[u8], u8); 2] = [(&narrow, 2), (&wide, 4)];
175        for (needle, width) in needles {
176            if width == 2 && idx > u16::MAX as u32 {
177                continue;
178            }
179            if width == 4 && kind != RefKind::String {
180                continue; // only const-string/jumbo carries a 32-bit operand
181            }
182            let finder = memchr::memmem::Finder::new(needle);
183            // `find_iter` skips overlapping matches: with a self-overlapping
184            // needle such as `05 05` and a register byte equal to the operand
185            // (`1a 05 05 05`) the first hit lands one byte early and the real
186            // candidate would be jumped over. Resume one byte after each hit.
187            let mut from = 0;
188            while let Some(rel) = finder.find(&region[from..]) {
189                let pos = from + rel;
190                from = pos + 1;
191                if pos >= 2 && opcode_width(kind, region[pos - 2]) == width && (!aligned || (base + pos - 2) & 1 == 0) {
192                    out.push(((base + pos - 2) as u32, idx));
193                }
194            }
195        }
196    }
197    out.sort_unstable();
198    out.dedup();
199    out
200}
201
202/// Check the candidate whose opcode byte sits at `i` and push it if it
203/// references one of `idxs`.
204#[inline]
205fn check_candidate(region: &[u8], base: usize, kind: RefKind, idxs: &IdxSet, i: usize, out: &mut Vec<(u32, u32)>) {
206    let width = opcode_width(kind, region[i]);
207    if width == 0 {
208        return;
209    }
210    let idx = if width == 2 {
211        let Some(b) = region.get(i + 2..i + 4) else { return };
212        u16::from_le_bytes([b[0], b[1]]) as u32
213    } else {
214        let Some(b) = region.get(i + 2..i + 6) else { return };
215        u32::from_le_bytes([b[0], b[1], b[2], b[3]])
216    };
217    if idxs.contains(idx) {
218        out.push(((base + i) as u32, idx));
219    }
220}
221
222/// Linear sweep over every byte with a 256-entry opcode-width table.
223fn linear_sweep(region: &[u8], base: usize, kind: RefKind, idxs: &IdxSet, out: &mut Vec<(u32, u32)>) {
224    let mut table = [0u8; 256];
225    for (b, w) in table.iter_mut().enumerate() {
226        *w = opcode_width(kind, b as u8);
227    }
228    let len = region.len();
229    for (i, &b) in region.iter().enumerate() {
230        let width = table[b as usize];
231        if width == 0 {
232            continue;
233        }
234        let idx = if width == 2 {
235            if i + 4 > len {
236                continue;
237            }
238            u16::from_le_bytes([region[i + 2], region[i + 3]]) as u32
239        } else {
240            if i + 6 > len {
241                continue;
242            }
243            u32::from_le_bytes([region[i + 2], region[i + 3], region[i + 4], region[i + 5]])
244        };
245        if idxs.contains(idx) {
246            out.push(((base + i) as u32, idx));
247        }
248    }
249}
250
251/// Whether a `memchr` prefilter on these high operand bytes is worthwhile.
252/// `0x00` (indexes below 256) and `0xFF` are everywhere in bytecode (zero
253/// registers and padding, `-1` literals), so a prefilter on them fires on
254/// most positions and only adds overhead over the linear sweep.
255fn prefilter_is_selective(highs: &[u8]) -> bool {
256    highs.len() <= 3 && !highs.iter().any(|&b| b == 0x00 || b == 0xFF)
257}
258
259/// Many targets. Strings have only two opcodes, so `memchr2` finds the
260/// candidates. For the other kinds the high operand byte is usually shared by
261/// most targets (R8 packs related ids together): when the targets span at most
262/// three distinct, selective high bytes, `memchr` on those bytes prefilters
263/// the region; otherwise fall back to one linear sweep over the opcode bytes.
264fn scan_by_opcode(region: &[u8], base: usize, kind: RefKind, idxs: &IdxSet, aligned: bool) -> Vec<(u32, u32)> {
265    let mut out = Vec::new();
266    if kind == RefKind::String {
267        for i in memchr::memchr2_iter(0x1a, 0x1b, region) {
268            if !aligned || (base + i) & 1 == 0 {
269                check_candidate(region, base, kind, idxs, i, &mut out);
270            }
271        }
272        return out;
273    }
274    let mut highs: Vec<u8> = idxs.iter().map(|i| (i >> 8) as u8).collect();
275    highs.sort_unstable();
276    highs.dedup();
277    if !prefilter_is_selective(&highs) {
278        linear_sweep(region, base, kind, idxs, &mut out);
279        return out;
280    }
281    let positions: Box<dyn Iterator<Item = usize>> = match highs.as_slice() {
282        [a] => Box::new(memchr::memchr_iter(*a, region)),
283        [a, b] => Box::new(memchr::memchr2_iter(*a, *b, region)),
284        [a, b, c] => Box::new(memchr::memchr3_iter(*a, *b, *c, region)),
285        _ => unreachable!("prefilter_is_selective bounds the byte set"),
286    };
287    // The high operand byte sits at opcode + 3.
288    for p in positions {
289        if p >= 3 && (!aligned || (base + p - 3) & 1 == 0) {
290            check_candidate(region, base, kind, idxs, p - 3, &mut out);
291        }
292    }
293    out
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn prefilter_falls_back_on_common_bytes() {
302        assert!(prefilter_is_selective(&[0x12]));
303        assert!(prefilter_is_selective(&[0x12, 0x13, 0x14]));
304        assert!(!prefilter_is_selective(&[0x00]));
305        assert!(!prefilter_is_selective(&[0x12, 0xff]));
306        assert!(!prefilter_is_selective(&[1, 2, 3, 4]));
307    }
308
309    #[test]
310    fn every_field_opcode_has_the_right_access() {
311        for opcode in 0u8..=u8::MAX {
312            let expected = match opcode {
313                0x52..=0x58 | 0x60..=0x66 => Some(FieldAccess::Read),
314                0x59..=0x5f | 0x67..=0x6d => Some(FieldAccess::Write),
315                _ => None,
316            };
317            assert_eq!(FieldAccess::from_opcode(opcode), expected, "opcode 0x{opcode:02x}");
318        }
319    }
320
321    /// Every scan strategy on `region` for `targets`, as (label, candidates).
322    fn all_strategies(region: &[u8], kind: RefKind, targets: &[u32]) -> Vec<(&'static str, Vec<(u32, u32)>)> {
323        let idxs: IdxSet = targets.iter().copied().collect();
324        let mut linear = Vec::new();
325        linear_sweep(region, 0, kind, &idxs, &mut linear);
326        vec![
327            ("operand", scan_by_operand(region, 0, kind, targets, false)),
328            ("opcode", scan_by_opcode(region, 0, kind, &idxs, false)),
329            ("linear", linear),
330        ]
331    }
332
333    fn assert_all_agree(region: &[u8], kind: RefKind, targets: &[u32], expected: &[(u32, u32)]) {
334        for (label, got) in all_strategies(region, kind, targets) {
335            assert_eq!(got, expected, "{label} strategy for {kind:?} {targets:x?}");
336        }
337    }
338
339    #[test]
340    fn self_overlapping_operand_is_not_skipped() {
341        // const-string v5, string@0x0505: the needle `05 05` also matches at
342        // the register byte one position earlier.
343        let region = [0x1a, 0x05, 0x05, 0x05, 0x0e, 0x00];
344        assert_all_agree(&region, RefKind::String, &[0x0505], &[(0, 0x0505)]);
345        // iget v5, v0, field@0x0505 (AG byte 0x05 too).
346        let region = [0x52, 0x05, 0x05, 0x05, 0x0e, 0x00];
347        assert_all_agree(&region, RefKind::Field, &[0x0505], &[(0, 0x0505)]);
348        // Longer self-overlap: `05 05 05 05` needle preceded by more 05 bytes.
349        let region = [0x00, 0x05, 0x05, 0x1b, 0x05, 0x05, 0x05, 0x05, 0x05, 0x0e, 0x00];
350        assert_all_agree(&region, RefKind::String, &[0x0505_0505], &[(3, 0x0505_0505)]);
351        // Two adjacent candidates that share operand bytes.
352        let region = [0x1a, 0x05, 0x05, 0x05, 0x1a, 0x05, 0x05, 0x05];
353        assert_all_agree(&region, RefKind::String, &[0x0505], &[(0, 0x0505), (4, 0x0505)]);
354    }
355
356    #[test]
357    fn jumbo_string_index_uses_wide_operand() {
358        // const-string/jumbo v1, string@0x00012345 followed by a narrow
359        // const-string whose 16-bit operand is the low half of the same index.
360        let mut region = vec![0x1b, 0x01];
361        region.extend_from_slice(&0x0001_2345u32.to_le_bytes());
362        region.extend_from_slice(&[0x1a, 0x02, 0x45, 0x23, 0x0e, 0x00]);
363        // The jumbo index only matches the wide form.
364        assert_all_agree(&region, RefKind::String, &[0x0001_2345], &[(0, 0x0001_2345)]);
365        // The narrow index only matches the narrow form (const-string).
366        assert_all_agree(&region, RefKind::String, &[0x2345], &[(6, 0x2345)]);
367        // Field/method/type kinds never carry a 32-bit operand.
368        assert_all_agree(&region, RefKind::Field, &[0x0001_2345], &[]);
369    }
370
371    #[test]
372    fn strategies_agree() {
373        // A synthetic region: field ops with various operands, some noise.
374        let mut region = Vec::new();
375        for i in 0u32..600 {
376            let idx = (i * 7) % 700;
377            region.extend_from_slice(&[0x52 + (i % 4) as u8, 0x10]);
378            region.extend_from_slice(&(idx as u16).to_le_bytes());
379            region.extend_from_slice(&[0x0e, 0x00, 0x00, 0x00]);
380        }
381        // Targets below 256 (high byte 0x00) force the linear sweep; the
382        // operand search must find exactly the same candidates.
383        let targets: Vec<u32> = (0..20).map(|i| i * 7).collect();
384        let idxs: IdxSet = targets.iter().copied().collect();
385        let a = scan_by_operand(&region, 0, RefKind::Field, &targets, false);
386        let b = scan_by_opcode(&region, 0, RefKind::Field, &idxs, false);
387        assert!(!a.is_empty());
388        assert_eq!(a, b);
389        // Targets sharing one selective high byte (0x02) use the memchr prefilter.
390        let targets: Vec<u32> = (74..94).map(|k| k * 7).collect();
391        assert!(targets.iter().all(|&t| t >> 8 == 2));
392        let idxs: IdxSet = targets.iter().copied().collect();
393        let a = scan_by_operand(&region, 0, RefKind::Field, &targets, false);
394        let b = scan_by_opcode(&region, 0, RefKind::Field, &idxs, false);
395        assert!(!a.is_empty());
396        assert_eq!(a, b);
397    }
398}