asc-findrefs 0.2.1

Cross-reference search primitives for asc-rs: string/type/method/field locators, O(1) instruction-to-method resolution and raw code section scanning.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! Raw code-region scan for opcodes carrying a constant-pool index.

use crate::insn_locator::InsnLocator;

/// Which index table a query targets.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RefKind {
    /// `string_ids` (`const-string[/jumbo]`).
    String,
    /// `type_ids` (`const-class`, `check-cast`, `new-instance`, ...).
    Type,
    /// `method_ids` (`invoke-*`).
    Method,
    /// `field_ids` (`iget*` / `iput*` / `sget*` / `sput*`).
    Field,
}

/// How a Dalvik field instruction uses its referenced field.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum FieldAccess {
    /// `iget*` / `sget*`.
    Read,
    /// `iput*` / `sput*`.
    Write,
}

impl FieldAccess {
    /// Classify a field opcode. Reads are `iget*` / `sget*`; writes are
    /// `iput*` / `sput*`.
    pub const fn from_opcode(opcode: u8) -> Option<Self> {
        match opcode {
            0x52..=0x58 | 0x60..=0x66 => Some(FieldAccess::Read),
            0x59..=0x5f | 0x67..=0x6d => Some(FieldAccess::Write),
            _ => None,
        }
    }

    /// Lower-case name used in query syntax and output (`read` / `write`).
    pub const fn as_str(self) -> &'static str {
        match self {
            FieldAccess::Read => "read",
            FieldAccess::Write => "write",
        }
    }
}

impl RefKind {
    /// Lower-case name used in output (`string`, `type`, `method`, `field`).
    pub fn as_str(self) -> &'static str {
        match self {
            RefKind::String => "string",
            RefKind::Type => "type",
            RefKind::Method => "method",
            RefKind::Field => "field",
        }
    }
}

/// Dense bitset over constant-pool indexes.
#[derive(Debug, Clone, Default)]
pub struct IdxSet {
    bits: Vec<u64>,
}

impl IdxSet {
    /// Add `idx`, growing the bitset to cover it.
    pub fn insert(&mut self, idx: u32) {
        let word = (idx >> 6) as usize;
        if word >= self.bits.len() {
            self.bits.resize(word + 1, 0);
        }
        self.bits[word] |= 1u64 << (idx & 63);
    }

    /// Whether `idx` is in the set.
    #[inline]
    pub fn contains(&self, idx: u32) -> bool {
        match self.bits.get((idx >> 6) as usize) {
            Some(w) => w & (1u64 << (idx & 63)) != 0,
            None => false,
        }
    }

    /// Whether no index is set.
    pub fn is_empty(&self) -> bool {
        self.bits.iter().all(|&w| w == 0)
    }

    /// Ascending members.
    pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
        self.bits.iter().enumerate().flat_map(|(w, &bits)| {
            let mut bits = bits;
            std::iter::from_fn(move || {
                if bits == 0 {
                    return None;
                }
                let t = bits.trailing_zeros();
                bits &= bits - 1;
                Some((w as u32) * 64 + t)
            })
        })
    }
}

impl FromIterator<u32> for IdxSet {
    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self {
        let mut s = IdxSet::default();
        for i in iter {
            s.insert(i);
        }
        s
    }
}

/// Opcode byte → operand width (0 = not an opcode of this kind).
#[inline]
fn opcode_width(kind: RefKind, b: u8) -> u8 {
    match kind {
        RefKind::String => match b {
            0x1a => 2,
            0x1b => 4,
            _ => 0,
        },
        RefKind::Type => matches!(b, 0x1c | 0x1f | 0x20 | 0x22 | 0x23 | 0x24 | 0x25) as u8 * 2,
        RefKind::Field => (b.wrapping_sub(0x52) <= 0x6d - 0x52) as u8 * 2,
        RefKind::Method => (b.wrapping_sub(0x6e) <= 4 || b.wrapping_sub(0x74) <= 4 || b == 0xfa || b == 0xfb) as u8 * 2,
    }
}

/// Above this many targets the per-target `memmem` pass costs more than one
/// linear sweep over the region.
const MEMMEM_MAX_TARGETS: usize = 8;

/// Sweeps the code-item region of a DEX.
pub struct CodeScanner<'l> {
    insn: &'l InsnLocator,
}

impl<'l> CodeScanner<'l> {
    /// A scanner over the code region `insn` was built for.
    pub fn new(insn: &'l InsnLocator) -> Self {
        CodeScanner { insn }
    }

    /// Return `(absolute offset, matched index)` for every position in the code
    /// region of `buf` whose opcode byte belongs to `kind` and whose operand is
    /// in `idxs`, in ascending offset order. Positions are unverified: the
    /// instruction locator decides whether they sit on an instruction boundary.
    ///
    /// `buf` must be the DEX buffer the instruction locator was built from.
    pub fn scan(&self, buf: &[u8], kind: RefKind, idxs: &IdxSet) -> Vec<(u32, u32)> {
        self.scan_inner(buf, kind, idxs, false)
    }

    /// Like [`CodeScanner::scan`], but omits byte-pattern matches at odd DEX
    /// offsets. All real DEX instructions start at even offsets; the public
    /// raw scan keeps them for low-level callers, while FindRefs uses this
    /// path before method verification.
    pub(crate) fn scan_instruction_candidates(&self, buf: &[u8], kind: RefKind, idxs: &IdxSet) -> Vec<(u32, u32)> {
        self.scan_inner(buf, kind, idxs, true)
    }

    fn scan_inner(&self, buf: &[u8], kind: RefKind, idxs: &IdxSet, aligned: bool) -> Vec<(u32, u32)> {
        let end = self.insn.code_item_end().min(buf.len());
        let start = self.insn.code_item_start();
        if start >= end || idxs.is_empty() {
            return Vec::new();
        }
        let region = &buf[start..end];
        let targets = idxs.iter().collect::<Vec<u32>>();
        if targets.len() <= MEMMEM_MAX_TARGETS {
            return scan_by_operand(region, start, kind, &targets, aligned);
        }
        scan_by_opcode(region, start, kind, idxs, aligned)
    }
}

/// Few targets: search the operand bytes with SIMD `memmem` and check the
/// opcode two bytes before each candidate. Operand bytes are far more
/// selective than opcode bytes, so almost every position is skipped.
fn scan_by_operand(region: &[u8], base: usize, kind: RefKind, targets: &[u32], aligned: bool) -> Vec<(u32, u32)> {
    let mut out = Vec::new();
    for &idx in targets {
        let narrow = (idx as u16).to_le_bytes();
        let wide = idx.to_le_bytes();
        let needles: [(&[u8], u8); 2] = [(&narrow, 2), (&wide, 4)];
        for (needle, width) in needles {
            if width == 2 && idx > u16::MAX as u32 {
                continue;
            }
            if width == 4 && kind != RefKind::String {
                continue; // only const-string/jumbo carries a 32-bit operand
            }
            let finder = memchr::memmem::Finder::new(needle);
            // `find_iter` skips overlapping matches: with a self-overlapping
            // needle such as `05 05` and a register byte equal to the operand
            // (`1a 05 05 05`) the first hit lands one byte early and the real
            // candidate would be jumped over. Resume one byte after each hit.
            let mut from = 0;
            while let Some(rel) = finder.find(&region[from..]) {
                let pos = from + rel;
                from = pos + 1;
                if pos >= 2 && opcode_width(kind, region[pos - 2]) == width && (!aligned || (base + pos - 2) & 1 == 0) {
                    out.push(((base + pos - 2) as u32, idx));
                }
            }
        }
    }
    out.sort_unstable();
    out.dedup();
    out
}

/// Check the candidate whose opcode byte sits at `i` and push it if it
/// references one of `idxs`.
#[inline]
fn check_candidate(region: &[u8], base: usize, kind: RefKind, idxs: &IdxSet, i: usize, out: &mut Vec<(u32, u32)>) {
    let width = opcode_width(kind, region[i]);
    if width == 0 {
        return;
    }
    let idx = if width == 2 {
        let Some(b) = region.get(i + 2..i + 4) else { return };
        u16::from_le_bytes([b[0], b[1]]) as u32
    } else {
        let Some(b) = region.get(i + 2..i + 6) else { return };
        u32::from_le_bytes([b[0], b[1], b[2], b[3]])
    };
    if idxs.contains(idx) {
        out.push(((base + i) as u32, idx));
    }
}

/// Linear sweep over every byte with a 256-entry opcode-width table.
fn linear_sweep(region: &[u8], base: usize, kind: RefKind, idxs: &IdxSet, out: &mut Vec<(u32, u32)>) {
    let mut table = [0u8; 256];
    for (b, w) in table.iter_mut().enumerate() {
        *w = opcode_width(kind, b as u8);
    }
    let len = region.len();
    for (i, &b) in region.iter().enumerate() {
        let width = table[b as usize];
        if width == 0 {
            continue;
        }
        let idx = if width == 2 {
            if i + 4 > len {
                continue;
            }
            u16::from_le_bytes([region[i + 2], region[i + 3]]) as u32
        } else {
            if i + 6 > len {
                continue;
            }
            u32::from_le_bytes([region[i + 2], region[i + 3], region[i + 4], region[i + 5]])
        };
        if idxs.contains(idx) {
            out.push(((base + i) as u32, idx));
        }
    }
}

/// Whether a `memchr` prefilter on these high operand bytes is worthwhile.
/// `0x00` (indexes below 256) and `0xFF` are everywhere in bytecode (zero
/// registers and padding, `-1` literals), so a prefilter on them fires on
/// most positions and only adds overhead over the linear sweep.
fn prefilter_is_selective(highs: &[u8]) -> bool {
    highs.len() <= 3 && !highs.iter().any(|&b| b == 0x00 || b == 0xFF)
}

/// Many targets. Strings have only two opcodes, so `memchr2` finds the
/// candidates. For the other kinds the high operand byte is usually shared by
/// most targets (R8 packs related ids together): when the targets span at most
/// three distinct, selective high bytes, `memchr` on those bytes prefilters
/// the region; otherwise fall back to one linear sweep over the opcode bytes.
fn scan_by_opcode(region: &[u8], base: usize, kind: RefKind, idxs: &IdxSet, aligned: bool) -> Vec<(u32, u32)> {
    let mut out = Vec::new();
    if kind == RefKind::String {
        for i in memchr::memchr2_iter(0x1a, 0x1b, region) {
            if !aligned || (base + i) & 1 == 0 {
                check_candidate(region, base, kind, idxs, i, &mut out);
            }
        }
        return out;
    }
    let mut highs: Vec<u8> = idxs.iter().map(|i| (i >> 8) as u8).collect();
    highs.sort_unstable();
    highs.dedup();
    if !prefilter_is_selective(&highs) {
        linear_sweep(region, base, kind, idxs, &mut out);
        return out;
    }
    let positions: Box<dyn Iterator<Item = usize>> = match highs.as_slice() {
        [a] => Box::new(memchr::memchr_iter(*a, region)),
        [a, b] => Box::new(memchr::memchr2_iter(*a, *b, region)),
        [a, b, c] => Box::new(memchr::memchr3_iter(*a, *b, *c, region)),
        _ => unreachable!("prefilter_is_selective bounds the byte set"),
    };
    // The high operand byte sits at opcode + 3.
    for p in positions {
        if p >= 3 && (!aligned || (base + p - 3) & 1 == 0) {
            check_candidate(region, base, kind, idxs, p - 3, &mut out);
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prefilter_falls_back_on_common_bytes() {
        assert!(prefilter_is_selective(&[0x12]));
        assert!(prefilter_is_selective(&[0x12, 0x13, 0x14]));
        assert!(!prefilter_is_selective(&[0x00]));
        assert!(!prefilter_is_selective(&[0x12, 0xff]));
        assert!(!prefilter_is_selective(&[1, 2, 3, 4]));
    }

    #[test]
    fn every_field_opcode_has_the_right_access() {
        for opcode in 0u8..=u8::MAX {
            let expected = match opcode {
                0x52..=0x58 | 0x60..=0x66 => Some(FieldAccess::Read),
                0x59..=0x5f | 0x67..=0x6d => Some(FieldAccess::Write),
                _ => None,
            };
            assert_eq!(FieldAccess::from_opcode(opcode), expected, "opcode 0x{opcode:02x}");
        }
    }

    /// Every scan strategy on `region` for `targets`, as (label, candidates).
    fn all_strategies(region: &[u8], kind: RefKind, targets: &[u32]) -> Vec<(&'static str, Vec<(u32, u32)>)> {
        let idxs: IdxSet = targets.iter().copied().collect();
        let mut linear = Vec::new();
        linear_sweep(region, 0, kind, &idxs, &mut linear);
        vec![
            ("operand", scan_by_operand(region, 0, kind, targets, false)),
            ("opcode", scan_by_opcode(region, 0, kind, &idxs, false)),
            ("linear", linear),
        ]
    }

    fn assert_all_agree(region: &[u8], kind: RefKind, targets: &[u32], expected: &[(u32, u32)]) {
        for (label, got) in all_strategies(region, kind, targets) {
            assert_eq!(got, expected, "{label} strategy for {kind:?} {targets:x?}");
        }
    }

    #[test]
    fn self_overlapping_operand_is_not_skipped() {
        // const-string v5, string@0x0505: the needle `05 05` also matches at
        // the register byte one position earlier.
        let region = [0x1a, 0x05, 0x05, 0x05, 0x0e, 0x00];
        assert_all_agree(&region, RefKind::String, &[0x0505], &[(0, 0x0505)]);
        // iget v5, v0, field@0x0505 (AG byte 0x05 too).
        let region = [0x52, 0x05, 0x05, 0x05, 0x0e, 0x00];
        assert_all_agree(&region, RefKind::Field, &[0x0505], &[(0, 0x0505)]);
        // Longer self-overlap: `05 05 05 05` needle preceded by more 05 bytes.
        let region = [0x00, 0x05, 0x05, 0x1b, 0x05, 0x05, 0x05, 0x05, 0x05, 0x0e, 0x00];
        assert_all_agree(&region, RefKind::String, &[0x0505_0505], &[(3, 0x0505_0505)]);
        // Two adjacent candidates that share operand bytes.
        let region = [0x1a, 0x05, 0x05, 0x05, 0x1a, 0x05, 0x05, 0x05];
        assert_all_agree(&region, RefKind::String, &[0x0505], &[(0, 0x0505), (4, 0x0505)]);
    }

    #[test]
    fn jumbo_string_index_uses_wide_operand() {
        // const-string/jumbo v1, string@0x00012345 followed by a narrow
        // const-string whose 16-bit operand is the low half of the same index.
        let mut region = vec![0x1b, 0x01];
        region.extend_from_slice(&0x0001_2345u32.to_le_bytes());
        region.extend_from_slice(&[0x1a, 0x02, 0x45, 0x23, 0x0e, 0x00]);
        // The jumbo index only matches the wide form.
        assert_all_agree(&region, RefKind::String, &[0x0001_2345], &[(0, 0x0001_2345)]);
        // The narrow index only matches the narrow form (const-string).
        assert_all_agree(&region, RefKind::String, &[0x2345], &[(6, 0x2345)]);
        // Field/method/type kinds never carry a 32-bit operand.
        assert_all_agree(&region, RefKind::Field, &[0x0001_2345], &[]);
    }

    #[test]
    fn strategies_agree() {
        // A synthetic region: field ops with various operands, some noise.
        let mut region = Vec::new();
        for i in 0u32..600 {
            let idx = (i * 7) % 700;
            region.extend_from_slice(&[0x52 + (i % 4) as u8, 0x10]);
            region.extend_from_slice(&(idx as u16).to_le_bytes());
            region.extend_from_slice(&[0x0e, 0x00, 0x00, 0x00]);
        }
        // Targets below 256 (high byte 0x00) force the linear sweep; the
        // operand search must find exactly the same candidates.
        let targets: Vec<u32> = (0..20).map(|i| i * 7).collect();
        let idxs: IdxSet = targets.iter().copied().collect();
        let a = scan_by_operand(&region, 0, RefKind::Field, &targets, false);
        let b = scan_by_opcode(&region, 0, RefKind::Field, &idxs, false);
        assert!(!a.is_empty());
        assert_eq!(a, b);
        // Targets sharing one selective high byte (0x02) use the memchr prefilter.
        let targets: Vec<u32> = (74..94).map(|k| k * 7).collect();
        assert!(targets.iter().all(|&t| t >> 8 == 2));
        let idxs: IdxSet = targets.iter().copied().collect();
        let a = scan_by_operand(&region, 0, RefKind::Field, &targets, false);
        let b = scan_by_opcode(&region, 0, RefKind::Field, &idxs, false);
        assert!(!a.is_empty());
        assert_eq!(a, b);
    }
}