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