Skip to main content

coreshift_core/android/
dex.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Minimal ZIP + DEX parser for reading TRANSACTION_* static int field values
6//! from `framework.jar` without any subprocess or external tool.
7//!
8//! Only handles STORED (uncompressed) DEX entries. Android framework JARs
9//! store DEX uncompressed so ART can mmap directly from the ZIP.
10
11// ── Byte readers ──────────────────────────────────────────────────────────────
12
13fn u16le(b: &[u8], off: usize) -> Option<u16> {
14    let s = b.get(off..off + 2)?;
15    Some(u16::from_le_bytes([s[0], s[1]]))
16}
17
18fn u32le(b: &[u8], off: usize) -> Option<u32> {
19    let s = b.get(off..off + 4)?;
20    Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
21}
22
23fn uleb128(b: &[u8], off: &mut usize) -> Option<u32> {
24    let mut result = 0u32;
25    let mut shift = 0u32;
26    loop {
27        let byte = *b.get(*off)?;
28        *off += 1;
29        result |= ((byte & 0x7f) as u32) << shift;
30        if byte & 0x80 == 0 {
31            return Some(result);
32        }
33        shift += 7;
34        if shift >= 35 {
35            return None;
36        }
37    }
38}
39
40// ── ZIP ───────────────────────────────────────────────────────────────────────
41
42const ZIP_EOCD_SIG: [u8; 4] = [0x50, 0x4b, 0x05, 0x06];
43const ZIP_CD_SIG: [u8; 4] = [0x50, 0x4b, 0x01, 0x02];
44const ZIP_LOCAL_SIG: [u8; 4] = [0x50, 0x4b, 0x03, 0x04];
45const ZIP_STORED: u16 = 0;
46
47struct ZipEntry {
48    local_off: usize,
49    comp_size: usize,
50}
51
52fn zip_find_dex_entries(data: &[u8]) -> Vec<ZipEntry> {
53    // Scan backwards for EOCD (ignore ZIP comment — Android JARs have none).
54    let scan_start = data.len().saturating_sub(65558);
55    let eocd_pos = match data[scan_start..]
56        .windows(4)
57        .rposition(|w| w == ZIP_EOCD_SIG)
58    {
59        Some(p) => scan_start + p,
60        None => return Vec::new(),
61    };
62
63    let cd_size = match u32le(data, eocd_pos + 12) {
64        Some(v) => v as usize,
65        None => return Vec::new(),
66    };
67    let cd_off = match u32le(data, eocd_pos + 16) {
68        Some(v) => v as usize,
69        None => return Vec::new(),
70    };
71
72    let mut pos = cd_off;
73    let cd_end = cd_off.saturating_add(cd_size);
74    let mut entries = Vec::new();
75
76    while pos + 46 <= cd_end && pos + 46 <= data.len() {
77        if data.get(pos..pos + 4) != Some(&ZIP_CD_SIG) {
78            break;
79        }
80
81        let compression = match u16le(data, pos + 10) {
82            Some(v) => v,
83            None => break,
84        };
85        let comp_size = match u32le(data, pos + 20) {
86            Some(v) => v as usize,
87            None => break,
88        };
89        let local_off = match u32le(data, pos + 42) {
90            Some(v) => v as usize,
91            None => break,
92        };
93        let fname_len = match u16le(data, pos + 28) {
94            Some(v) => v as usize,
95            None => break,
96        };
97        let extra_len = match u16le(data, pos + 30) {
98            Some(v) => v as usize,
99            None => break,
100        };
101        let comment_len = match u16le(data, pos + 32) {
102            Some(v) => v as usize,
103            None => break,
104        };
105
106        let fname_end = pos + 46 + fname_len;
107        if fname_end > data.len() {
108            break;
109        }
110        let fname = &data[pos + 46..fname_end];
111
112        // Collect classes*.dex entries that are STORED.
113        let is_dex = fname.starts_with(b"classes") && fname.ends_with(b".dex");
114        if is_dex && compression == ZIP_STORED {
115            entries.push(ZipEntry {
116                local_off,
117                comp_size,
118            });
119        }
120
121        pos = match pos.checked_add(46 + fname_len + extra_len + comment_len) {
122            Some(v) => v,
123            None => break,
124        };
125    }
126
127    // Sort by comp_size descending: the largest DEX is most likely to
128    // contain IActivityManager.
129    entries.sort_by_key(|e| std::cmp::Reverse(e.comp_size));
130    entries
131}
132
133fn zip_entry_data<'a>(data: &'a [u8], entry: &ZipEntry) -> Option<&'a [u8]> {
134    let lh = entry.local_off;
135    if data.get(lh..lh + 4) != Some(&ZIP_LOCAL_SIG) {
136        return None;
137    }
138    let fname_len = u16le(data, lh + 26)? as usize;
139    let extra_len = u16le(data, lh + 28)? as usize;
140    let data_start = lh + 30 + fname_len + extra_len;
141    data.get(data_start..data_start + entry.comp_size)
142}
143
144// ── DEX ───────────────────────────────────────────────────────────────────────
145
146const DEX_MAGIC: &[u8] = b"dex\n";
147
148/// Max length of one scanned DEX string (ULEB128 length prefix + NUL terminator
149/// included in the budget). A legitimate DEX string (class/field/method
150/// descriptor, type descriptor) is short — the largest realistic entries are
151/// handfuls of hundreds of bytes. Capping the NUL scan keeps a crafted or
152/// corrupt DEX that points many `string_data_off`s at a long non-NUL region
153/// from degrading to an O(n²) CPU scan that hangs the root daemon (finding 10).
154const MAX_DEX_STRING_LEN: usize = 8192;
155
156fn dex_string(dex: &[u8], string_ids_off: usize, idx: usize) -> Option<&[u8]> {
157    // Bounds-check the string id itself so a corrupt index cannot index past
158    // the string_ids table (the caller is expected to have capped `idx`, but a
159    // wrong offset still resolves garbage otherwise).
160    if string_ids_off
161        .checked_add(idx.saturating_mul(4))
162        .map_or(true, |end| end + 4 > dex.len())
163    {
164        return None;
165    }
166    let str_data_off = u32le(dex, string_ids_off + idx * 4)? as usize;
167    // Skip ULEB128 UTF-16 length, bounded so a malformed high continuation
168    // byte cannot scan to end of buffer.
169    let mut off = str_data_off;
170    let uleb_budget = off.checked_add(5)?;
171    loop {
172        let b = *dex.get(off)?;
173        off += 1;
174        if b & 0x80 == 0 {
175            break;
176        }
177        if off >= uleb_budget {
178            return None;
179        }
180    }
181    // Null-terminated MUTF-8 bytes, bounded by MAX_DEX_STRING_LEN.
182    let start = off;
183    loop {
184        let b = *dex.get(off)?;
185        if b == 0 {
186            break;
187        }
188        off += 1;
189        if off.saturating_sub(start) >= MAX_DEX_STRING_LEN {
190            return None;
191        }
192    }
193    dex.get(start..off)
194}
195
196/// Max nesting depth for `encoded_value` arrays/annotations. Bounds the
197/// recursion in [`skip_encoded_value`] against a crafted DEX that nests
198/// `VALUE_ARRAY` / `VALUE_ANNOTATION` arbitrarily deep.
199const MAX_ENCODED_VALUE_DEPTH: usize = 64;
200
201fn skip_encoded_value(dex: &[u8], off: &mut usize) -> Option<()> {
202    skip_encoded_value_depth(dex, off, 0)
203}
204
205fn skip_encoded_value_depth(dex: &[u8], off: &mut usize, depth: usize) -> Option<()> {
206    if depth > MAX_ENCODED_VALUE_DEPTH {
207        return None;
208    }
209    let vbyte = *dex.get(*off)?;
210    *off += 1;
211    let vtype = vbyte & 0x1f;
212    let varg = (vbyte >> 5) as usize;
213    match vtype {
214        // value_arg+1 bytes follow
215        0x00 | 0x02 | 0x03 | 0x04 | 0x06 | 0x10 | 0x11 | 0x15 | 0x16 | 0x17 | 0x18 | 0x19
216        | 0x1a | 0x1b => {
217            *off = off.checked_add(varg + 1)?;
218        }
219        0x1c => {
220            // VALUE_ARRAY
221            let size = uleb128(dex, off)?;
222            for _ in 0..size {
223                skip_encoded_value_depth(dex, off, depth + 1)?;
224            }
225        }
226        0x1d => {
227            // VALUE_ANNOTATION
228            uleb128(dex, off)?; // type_idx
229            let size = uleb128(dex, off)?;
230            for _ in 0..size {
231                uleb128(dex, off)?; // name_idx
232                skip_encoded_value_depth(dex, off, depth + 1)?;
233            }
234        }
235        0x1e | 0x1f => {} // VALUE_NULL / VALUE_BOOLEAN — no extra bytes
236        _ => return None,
237    }
238    Some(())
239}
240
241fn read_int_encoded_value(dex: &[u8], off: &mut usize) -> Option<u32> {
242    let vbyte = *dex.get(*off)?;
243    *off += 1;
244    let vtype = vbyte & 0x1f;
245    let varg = (vbyte >> 5) as usize;
246    // VALUE_INT = 0x04, value_arg+1 bytes, little-endian
247    if vtype != 0x04 {
248        return None;
249    }
250    let size = varg + 1;
251    if size > 4 {
252        return None;
253    }
254    let mut val = 0u32;
255    for i in 0..size {
256        val |= (*dex.get(*off + i)? as u32) << (i * 8);
257    }
258    *off += size;
259    Some(val)
260}
261
262/// Cap a DEX section's declared element count to what can physically fit in
263/// the buffer after its offset. Element sizes: string_ids 4, type_ids 4,
264/// proto_ids 12, field_ids 8, method_ids 8, class_defs 32. A corrupt header
265/// count must not drive a multi-billion-element scan or allocation.
266fn cap_section_size(dex: &[u8], off: usize, elem_size: usize, declared: usize) -> usize {
267    let room = dex.len().saturating_sub(off);
268    declared.min(room / elem_size)
269}
270
271fn find_in_dex(dex: &[u8], class_desc: &[u8], field_name: &[u8]) -> Option<u32> {
272    if !dex.starts_with(DEX_MAGIC) || dex.len() < 112 {
273        return None;
274    }
275
276    let string_ids_size =
277        cap_section_size(dex, u32le(dex, 60)? as usize, 4, u32le(dex, 56)? as usize);
278    let string_ids_off = u32le(dex, 60)? as usize;
279    let type_ids_size =
280        cap_section_size(dex, u32le(dex, 68)? as usize, 4, u32le(dex, 64)? as usize);
281    let type_ids_off = u32le(dex, 68)? as usize;
282    let field_ids_size =
283        cap_section_size(dex, u32le(dex, 84)? as usize, 8, u32le(dex, 80)? as usize);
284    let field_ids_off = u32le(dex, 84)? as usize;
285    let class_defs_size =
286        cap_section_size(dex, u32le(dex, 100)? as usize, 32, u32le(dex, 96)? as usize);
287    let class_defs_off = u32le(dex, 100)? as usize;
288
289    // Find string index for class descriptor
290    let mut class_str_idx: Option<usize> = None;
291    for i in 0..string_ids_size {
292        if dex_string(dex, string_ids_off, i) == Some(class_desc) {
293            class_str_idx = Some(i);
294            break;
295        }
296    }
297    let class_str_idx = class_str_idx?;
298
299    // Find type index
300    let mut class_type_idx: Option<usize> = None;
301    for i in 0..type_ids_size {
302        if u32le(dex, type_ids_off + i * 4)? as usize == class_str_idx {
303            class_type_idx = Some(i);
304            break;
305        }
306    }
307    let class_type_idx = class_type_idx?;
308
309    // Find string index for field name
310    let mut field_str_idx: Option<u32> = None;
311    for i in 0..string_ids_size {
312        if dex_string(dex, string_ids_off, i) == Some(field_name) {
313            field_str_idx = Some(i as u32);
314            break;
315        }
316    }
317    let field_str_idx = field_str_idx?;
318
319    // Find global field_idx in field_ids
320    let mut target_field_idx: Option<u32> = None;
321    for i in 0..field_ids_size {
322        let foff = field_ids_off + i * 8;
323        let fclass = u16le(dex, foff)? as usize;
324        let fname = u32le(dex, foff + 4)?;
325        if fclass == class_type_idx && fname == field_str_idx {
326            target_field_idx = Some(i as u32);
327            break;
328        }
329    }
330    let target_field_idx = target_field_idx?;
331
332    // Find class def
333    let mut class_data_off = None;
334    let mut static_vals_off = None;
335    for i in 0..class_defs_size {
336        let coff = class_defs_off + i * 32;
337        if u32le(dex, coff)? as usize == class_type_idx {
338            class_data_off = Some(u32le(dex, coff + 24)? as usize);
339            static_vals_off = Some(u32le(dex, coff + 28)? as usize);
340            break;
341        }
342    }
343    let class_data_off = class_data_off?;
344    let static_vals_off = static_vals_off?;
345    if class_data_off == 0 || static_vals_off == 0 {
346        return None;
347    }
348
349    // Walk class_data_item static fields to find position of target_field_idx
350    let mut off = class_data_off;
351    let static_fields_size = uleb128(dex, &mut off)?;
352    let _instance_fields = uleb128(dex, &mut off)?;
353    let _direct_methods = uleb128(dex, &mut off)?;
354    let _virtual_methods = uleb128(dex, &mut off)?;
355
356    let mut field_pos: Option<usize> = None;
357    let mut cur_field_idx = 0u32;
358    for i in 0..static_fields_size as usize {
359        let diff = uleb128(dex, &mut off)?;
360        let _access_flags = uleb128(dex, &mut off)?;
361        cur_field_idx += diff;
362        if cur_field_idx == target_field_idx {
363            field_pos = Some(i);
364            break;
365        }
366    }
367    let field_pos = field_pos?;
368
369    // Read encoded_array at static_vals_off, skip to field_pos, read int
370    let mut sv = static_vals_off;
371    let sv_size = uleb128(dex, &mut sv)? as usize;
372    if field_pos >= sv_size {
373        return None;
374    }
375
376    for i in 0..=field_pos {
377        if i == field_pos {
378            return read_int_encoded_value(dex, &mut sv);
379        }
380        skip_encoded_value(dex, &mut sv)?;
381    }
382    None
383}
384
385/// Return the declared parameter types of a DEX method's proto.
386///
387/// Looks up `class_desc.method_name` in the method_ids table and returns the
388/// type descriptor list from the referenced proto's type_list. Returns `None`
389/// when any section is unreadable, the class is absent, or the method does
390/// not exist (so callers can distinguish "method present" from "not found").
391///
392/// Needed DEX sections: string_ids (56/60), type_ids (64/68), proto_ids
393/// (72/76), method_ids (88/92).
394fn method_param_types<'a>(
395    dex: &'a [u8],
396    class_desc: &[u8],
397    method_name: &[u8],
398) -> Option<Vec<&'a [u8]>> {
399    if !dex.starts_with(DEX_MAGIC) || dex.len() < 112 {
400        return None;
401    }
402
403    let string_ids_size =
404        cap_section_size(dex, u32le(dex, 60)? as usize, 4, u32le(dex, 56)? as usize);
405    let string_ids_off = u32le(dex, 60)? as usize;
406    let type_ids_size =
407        cap_section_size(dex, u32le(dex, 68)? as usize, 4, u32le(dex, 64)? as usize);
408    let type_ids_off = u32le(dex, 68)? as usize;
409    let proto_ids_size =
410        cap_section_size(dex, u32le(dex, 76)? as usize, 12, u32le(dex, 72)? as usize);
411    let proto_ids_off = u32le(dex, 76)? as usize;
412    let method_ids_size =
413        cap_section_size(dex, u32le(dex, 92)? as usize, 8, u32le(dex, 88)? as usize);
414    let method_ids_off = u32le(dex, 92)? as usize;
415
416    // String idx for the class descriptor
417    let mut class_str_idx: Option<usize> = None;
418    for i in 0..string_ids_size {
419        if dex_string(dex, string_ids_off, i) == Some(class_desc) {
420            class_str_idx = Some(i);
421            break;
422        }
423    }
424    let class_str_idx = class_str_idx?;
425
426    // Type idx for the class descriptor
427    let mut class_type_idx: Option<usize> = None;
428    for i in 0..type_ids_size {
429        if u32le(dex, type_ids_off + i * 4)? as usize == class_str_idx {
430            class_type_idx = Some(i);
431            break;
432        }
433    }
434    let class_type_idx = class_type_idx?;
435
436    // String idx for the method name
437    let mut method_str_idx: Option<u32> = None;
438    for i in 0..string_ids_size {
439        if dex_string(dex, string_ids_off, i) == Some(method_name) {
440            method_str_idx = Some(i as u32);
441            break;
442        }
443    }
444    let method_str_idx = method_str_idx?;
445
446    // Method idx → proto_idx, matching class and name
447    let mut proto_idx: Option<usize> = None;
448    for i in 0..method_ids_size {
449        let moff = method_ids_off + i * 8;
450        let fclass = u16le(dex, moff)? as usize;
451        let fproto = u16le(dex, moff + 2)? as usize;
452        let fname = u32le(dex, moff + 4)?;
453        if fclass == class_type_idx && fname == method_str_idx {
454            proto_idx = Some(fproto);
455            break;
456        }
457    }
458    let proto_idx = proto_idx?;
459    if proto_idx >= proto_ids_size {
460        return None;
461    }
462
463    // proto_id_item: shorty_idx, return_type_idx, parameters_off
464    let poff = proto_ids_off + proto_idx * 12;
465    let params_off = u32le(dex, poff + 8)? as usize;
466    if params_off == 0 {
467        return Some(Vec::new());
468    }
469
470    // type_list: size u32, then u16 type indices. Cap the count to what can
471    // actually fit in the remaining buffer so a corrupt DEX cannot request a
472    // multi-gigabyte allocation (`Vec::with_capacity` aborts on overflow/OOM).
473    let param_count = u32le(dex, params_off)? as usize;
474    let room = dex.len().saturating_sub(params_off + 4);
475    let param_count = param_count.min(room / 2);
476    let mut params = Vec::with_capacity(param_count);
477    for i in 0..param_count {
478        let tidx = u16le(dex, params_off + 4 + i * 2)? as usize;
479        // Bounds-check the type index against the type_ids table before
480        // dereferencing it: a raw out-of-range `u16` would read garbage as a
481        // string index and could silently flip the fgproc observer-descriptor
482        // decision (finding 11).
483        if tidx >= type_ids_size {
484            return None;
485        }
486        let desc_str_idx = u32le(dex, type_ids_off + tidx * 4)? as usize;
487        if desc_str_idx >= string_ids_size {
488            return None;
489        }
490        let desc = dex_string(dex, string_ids_off, desc_str_idx)?;
491        params.push(desc);
492    }
493    Some(params)
494}
495
496// ── Public API ────────────────────────────────────────────────────────────────
497
498/// Search `framework.jar` for the value of a static int field.
499///
500/// `class_desc` uses DEX descriptor syntax, e.g.
501/// `"Landroid/app/IActivityManager$Stub;"`.
502///
503/// Returns `None` if the JAR is unreadable, the class/field is absent, or
504/// the entry is compressed (DEFLATE — not expected for framework DEX).
505pub fn find_transaction_code(jar_path: &str, class_desc: &str, field_name: &str) -> Option<u32> {
506    let data = std::fs::read(jar_path).ok()?;
507    let entries = zip_find_dex_entries(&data);
508    for entry in &entries {
509        if let Some(dex) = zip_entry_data(&data, entry) {
510            if let Some(code) = find_in_dex(dex, class_desc.as_bytes(), field_name.as_bytes()) {
511                return Some(code);
512            }
513        }
514    }
515    None
516}
517
518/// Resolve all four tx codes needed for binder observer mode.
519///
520/// Returns `(observer_code, query_code, api_mode, fg_code)` where:
521/// - `observer_code` = `TRANSACTION_registerProcessObserver`
522/// - `query_code`    = `TRANSACTION_getFocusedRootTaskInfo` (or StackInfo on API 29)
523/// - `api_mode`      = 1 (RootTaskInfo) or 2 (StackInfo)
524/// - `fg_code`       = `TRANSACTION_onForegroundActivitiesChanged`
525pub fn resolve_tx_codes_from_dex() -> Option<(u32, u32, u8, u32)> {
526    const JAR: &str = "/system/framework/framework.jar";
527    const AM_STUB: &str = "Landroid/app/IActivityManager$Stub;";
528    const OBS_STUB: &str = "Landroid/app/IProcessObserver$Stub;";
529
530    let observer_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_registerProcessObserver")?;
531    let fg_code =
532        find_transaction_code(JAR, OBS_STUB, "TRANSACTION_onForegroundActivitiesChanged")?;
533
534    if let Some(query_code) =
535        find_transaction_code(JAR, AM_STUB, "TRANSACTION_getFocusedRootTaskInfo")
536    {
537        return Some((observer_code, query_code, 1, fg_code));
538    }
539    // API 29 fallback
540    let query_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_getFocusedStackInfo")?;
541    Some((observer_code, query_code, 2, fg_code))
542}
543
544/// Resolve `IPowerManager.isInteractive()` transaction code from DEX.
545/// Resolve the `IDisplayManager.getDisplayInfo` transaction code from DEX.
546///
547/// Returns `TRANSACTION_getDisplayInfo` on `android.hardware.display
548/// .IDisplayManager$Stub`, used to query the active display refresh rate and
549/// render frame rate for FPS normalization. Resolved at call time so it tracks
550/// the installed ROM's framework.
551pub fn resolve_display_info_tx() -> Option<u32> {
552    const JAR: &str = "/system/framework/framework.jar";
553    find_transaction_code(
554        JAR,
555        "Landroid/hardware/display/IDisplayManager$Stub;",
556        "TRANSACTION_getDisplayInfo",
557    )
558}
559
560pub fn resolve_is_interactive_tx() -> Option<u32> {
561    const JAR: &str = "/system/framework/framework.jar";
562    find_transaction_code(
563        JAR,
564        "Landroid/os/IPowerManager$Stub;",
565        "TRANSACTION_isInteractive",
566    )
567}
568
569/// Resolve the `IForegroundProcessObserver` registration tx codes from DEX.
570///
571/// Returns `(register_code, on_change_code)` where:
572/// - `register_code` = `TRANSACTION_registerForegroundProcessObserver`
573/// - `on_change_code` = `TRANSACTION_onForegroundProcessChanged`
574pub fn resolve_fgproc_codes() -> Option<(u32, u32)> {
575    const JAR: &str = "/system/framework/framework.jar";
576    const AM_STUB: &str = "Landroid/app/IActivityManager$Stub;";
577    const FGPROC_STUB: &str = "Landroid/app/IForegroundProcessObserver$Stub;";
578
579    let register_code = find_transaction_code(
580        JAR,
581        AM_STUB,
582        "TRANSACTION_registerForegroundProcessObserver",
583    )?;
584    let on_change_code =
585        find_transaction_code(JAR, FGPROC_STUB, "TRANSACTION_onForegroundProcessChanged")?;
586    Some((register_code, on_change_code))
587}
588
589/// Resolve the fallback `IProcessObserver` foreground observer tx codes used
590/// by ROMs that removed `IForegroundProcessObserver`.
591///
592/// Custom ROMs drop the stock `IForegroundProcessObserver` (no
593/// `registerForegroundProcessObserver` / `onForegroundProcessChanged`) but
594/// repurpose `IProcessObserver.onForegroundActivitiesChanged` to deliver the
595/// foreground **pid** (`(I,I,Z)V` — pid, uid, fg) instead of the stock
596/// package string (`(String,I,Z)V`). For those ROMs the daemon can register
597/// via the classic `registerProcessObserver` and read the pid straight out of
598/// the callback parcel.
599///
600/// Returns `None` when the stock [`resolve_fgproc_codes`] path succeeds
601/// (callers must prefer it), when `onForegroundActivitiesChanged` does not
602/// lead with an `int` parameter, or when either tx code cannot be resolved.
603pub fn resolve_fgproc_codes_fallback() -> Option<(u32, u32)> {
604    const JAR: &str = "/system/framework/framework.jar";
605    const AM_STUB: &str = "Landroid/app/IActivityManager$Stub;";
606    const OBS_STUB: &str = "Landroid/app/IProcessObserver$Stub;";
607    const OBS_IFACE: &str = "Landroid/app/IProcessObserver;";
608
609    // Stock IForegroundProcessObserver present → stock path is fine.
610    if resolve_fgproc_codes().is_some() {
611        return None;
612    }
613
614    let data = std::fs::read(JAR).ok()?;
615    let entries = zip_find_dex_entries(&data);
616
617    // onForegroundActivitiesChanged must declare an int first param (pid);
618    // stock String-first (pkg) form cannot serve this daemon.
619    let mut pid_first = false;
620    for entry in &entries {
621        if let Some(dex) = zip_entry_data(&data, entry) {
622            if let Some(params) =
623                method_param_types(dex, OBS_IFACE.as_bytes(), b"onForegroundActivitiesChanged")
624            {
625                pid_first = params.first().map(|t| *t == b"I").unwrap_or(false);
626                break;
627            }
628        }
629    }
630    if !pid_first {
631        return None;
632    }
633
634    let register_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_registerProcessObserver")?;
635    let on_change_code =
636        find_transaction_code(JAR, OBS_STUB, "TRANSACTION_onForegroundActivitiesChanged")?;
637    Some((register_code, on_change_code))
638}
639
640/// Resolve the `IWindowManager` task-FPS callback tx codes from DEX.
641///
642/// Returns `(register_code, unregister_code, on_fps_code)` where:
643/// - `register_code`   = `TRANSACTION_registerTaskFpsCallback`
644/// - `unregister_code` = `TRANSACTION_unregisterTaskFpsCallback`
645/// - `on_fps_code`     = `TRANSACTION_onFpsReported` on `ITaskFpsCallback`
646pub fn resolve_fps_codes() -> Option<(u32, u32, u32)> {
647    const JAR: &str = "/system/framework/framework.jar";
648    const IWM_STUB: &str = "Landroid/view/IWindowManager$Stub;";
649    const FPS_STUB: &str = "Landroid/window/ITaskFpsCallback$Stub;";
650
651    let register_code =
652        find_transaction_code(JAR, IWM_STUB, "TRANSACTION_registerTaskFpsCallback")?;
653    let unregister_code =
654        find_transaction_code(JAR, IWM_STUB, "TRANSACTION_unregisterTaskFpsCallback")?;
655    let on_fps_code = find_transaction_code(JAR, FPS_STUB, "TRANSACTION_onFpsReported")?;
656    Some((register_code, unregister_code, on_fps_code))
657}
658
659/// Resolve the content-provider handoff tx codes from DEX.
660///
661/// Returns `(get_code, remove_code)` where:
662/// - `get_code`    = `TRANSACTION_getContentProviderExternal`
663/// - `remove_code` = `TRANSACTION_removeContentProviderExternalAsUser` —
664///   the live Android 14 release method (`removeContentProviderExternal`
665///   is deprecated on that API level and takes an `IBinder` token, not a
666///   user id).
667///
668/// Both are resolved from `Landroid/app/IActivityManager$Stub;` in the
669/// installed framework.jar.
670pub fn resolve_handoff_codes() -> Option<(u32, u32)> {
671    const JAR: &str = "/system/framework/framework.jar";
672    const AM_STUB: &str = "Landroid/app/IActivityManager$Stub;";
673
674    let get_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_getContentProviderExternal")?;
675    let remove_code = find_transaction_code(
676        JAR,
677        AM_STUB,
678        "TRANSACTION_removeContentProviderExternalAsUser",
679    )?;
680    Some((get_code, remove_code))
681}
682
683/// Resolve the `IActivityTaskManager` task-stack listener tx codes from DEX.
684///
685/// Returns `(register_code, unregister_code)`:
686/// - `register_code`   = `TRANSACTION_registerTaskStackListener`
687/// - `unregister_code` = `TRANSACTION_unregisterTaskStackListener`
688///
689/// This target's ROM exposes the legacy `ITaskStackListener` /
690/// `registerTaskStackListener` pair on `IActivityTaskManager`; the newer
691/// `ITaskChangeListener` / `registerTaskChangeListener` interface is absent
692/// and must not be relied on.
693pub fn resolve_task_stack_codes() -> Option<(u32, u32)> {
694    const JAR: &str = "/system/framework/framework.jar";
695    const ATM_STUB: &str = "Landroid/app/IActivityTaskManager$Stub;";
696
697    let register_code =
698        find_transaction_code(JAR, ATM_STUB, "TRANSACTION_registerTaskStackListener")?;
699    let unregister_code =
700        find_transaction_code(JAR, ATM_STUB, "TRANSACTION_unregisterTaskStackListener")?;
701    Some((register_code, unregister_code))
702}
703
704/// Uid lifecycle observer tx codes (uid-scoped, event-driven both ways).
705///
706/// `registerUidObserver(observer, which, cutpoint, callingPackage)` reports
707/// per-uid lifecycle events: `onUidGone` (no more processes for the uid),
708/// `onUidActive` (uid is no longer idle), `onUidIdle`, `onUidCachedChanged`
709/// (cached state changed), plus `onUidStateChanged`/`onUidProcAdjChanged`
710/// when the corresponding `UID_OBSERVER_*` flags are requested. The daemon
711/// pins the app's uid at handoff, so it can filter these events on that
712/// single uid — no pid resolution, no `/proc`, no retry loop.
713///
714/// `registerUidObserverForUids` (added in S) is the filtered variant that
715/// only delivers events for a caller-supplied uid list; it requires the
716/// caller to hold `PACKAGE_USAGE_STATS` (or `PERMISSION_GRANTED` via
717/// root/system `canAccessUnexportedComponents`).
718pub struct UidObserverCodes {
719    /// `TRANSACTION_registerUidObserver` on `IActivityManager$Stub`
720    pub register_code: u32,
721    /// `TRANSACTION_unregisterUidObserver` on `IActivityManager$Stub`
722    pub unregister_code: u32,
723    /// `TRANSACTION_registerUidObserverForUids` on `IActivityManager$Stub`
724    /// (filtered variant, API 31+)
725    pub register_for_uids_code: Option<u32>,
726    /// `TRANSACTION_onUidGone` on `IUidObserver$Stub`
727    pub on_gone_code: u32,
728    /// `TRANSACTION_onUidActive` on `IUidObserver$Stub`
729    pub on_active_code: u32,
730    /// `TRANSACTION_onUidIdle` on `IUidObserver$Stub`
731    pub on_idle_code: u32,
732    /// `TRANSACTION_onUidCachedChanged` on `IUidObserver$Stub`
733    pub on_cached_code: u32,
734    /// `TRANSACTION_onUidStateChanged` on `IUidObserver$Stub`
735    pub on_state_code: u32,
736    /// `TRANSACTION_onUidProcAdjChanged` on `IUidObserver$Stub`
737    pub on_adj_code: u32,
738}
739
740/// Resolve the `IUidObserver` registration + callback tx codes from DEX.
741pub fn resolve_uid_observer_codes() -> Option<UidObserverCodes> {
742    const JAR: &str = "/system/framework/framework.jar";
743    const AM_STUB: &str = "Landroid/app/IActivityManager$Stub;";
744    const UID_OBS_STUB: &str = "Landroid/app/IUidObserver$Stub;";
745
746    let register_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_registerUidObserver")?;
747    let unregister_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_unregisterUidObserver")?;
748    let register_for_uids_code =
749        find_transaction_code(JAR, AM_STUB, "TRANSACTION_registerUidObserverForUids");
750    let on_gone_code = find_transaction_code(JAR, UID_OBS_STUB, "TRANSACTION_onUidGone")?;
751    let on_active_code = find_transaction_code(JAR, UID_OBS_STUB, "TRANSACTION_onUidActive")?;
752    let on_idle_code = find_transaction_code(JAR, UID_OBS_STUB, "TRANSACTION_onUidIdle")?;
753    let on_cached_code =
754        find_transaction_code(JAR, UID_OBS_STUB, "TRANSACTION_onUidCachedChanged")?;
755    let on_state_code = find_transaction_code(JAR, UID_OBS_STUB, "TRANSACTION_onUidStateChanged")?;
756    let on_adj_code = find_transaction_code(JAR, UID_OBS_STUB, "TRANSACTION_onUidProcAdjChanged")?;
757
758    Some(UidObserverCodes {
759        register_code,
760        unregister_code,
761        register_for_uids_code,
762        on_gone_code,
763        on_active_code,
764        on_idle_code,
765        on_cached_code,
766        on_state_code,
767        on_adj_code,
768    })
769}
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774
775    fn u32put(b: &mut [u8], off: usize, v: u32) {
776        b[off..off + 4].copy_from_slice(&v.to_le_bytes());
777    }
778
779    // Build a minimal DEX header + string/type/proto/method id tables for a
780    // single class with a single method whose proto has the given param type
781    // descriptors. Only the sections method_param_types() reads are populated.
782    fn build_dex(class_desc: &[u8], method_name: &[u8], params: &[&[u8]]) -> Vec<u8> {
783        let mut out = vec![0u8; 112];
784        out[0..4].copy_from_slice(DEX_MAGIC);
785        out[4..8].copy_from_slice(b"035\0");
786
787        let mut strings: Vec<Vec<u8>> = vec![class_desc.to_vec(), method_name.to_vec()];
788        for p in params {
789            if !strings.iter().any(|s| s == p) {
790                strings.push(p.to_vec());
791            }
792        }
793        let n_string = strings.len();
794        let mut types: Vec<Vec<u8>> = vec![class_desc.to_vec()];
795        for p in params {
796            if !types.iter().any(|t| t == p) {
797                types.push(p.to_vec());
798            }
799        }
800        let n_type = types.len();
801
802        let off_string_ids = 112;
803        let off_type_ids = off_string_ids + n_string * 4;
804        let off_proto_ids = off_type_ids + n_type * 4;
805        let off_method_ids = off_proto_ids + 12; // one proto
806
807        // Reserve id table space.
808        out.resize(off_method_ids + 8, 0); // one method
809
810        // string_data items (in file order, referenced by string_ids)
811        let mut string_offs = Vec::with_capacity(n_string);
812        for s in &strings {
813            string_offs.push(out.len());
814            let n = s.len() as u8;
815            out.push(n); // short ULEB128 length (ASCII only)
816            out.extend_from_slice(s);
817            out.push(0);
818        }
819
820        // type_list for the single proto's parameters.
821        let params_off = out.len();
822        out.extend_from_slice(&(params.len() as u32).to_le_bytes());
823        for p in params {
824            let ti = types.iter().position(|t| t == p).unwrap();
825            out.extend_from_slice(&(ti as u16).to_le_bytes());
826        }
827
828        // Header: counts + offsets (offsets 56..=100).
829        u32put(&mut out, 56, n_string as u32);
830        u32put(&mut out, 60, off_string_ids as u32);
831        u32put(&mut out, 64, n_type as u32);
832        u32put(&mut out, 68, off_type_ids as u32);
833        u32put(&mut out, 72, 1); // proto count
834        u32put(&mut out, 76, off_proto_ids as u32);
835        u32put(&mut out, 80, 0); // field_ids count
836        u32put(&mut out, 84, 0);
837        u32put(&mut out, 88, 1); // method count
838        u32put(&mut out, 92, off_method_ids as u32);
839        u32put(&mut out, 96, 0); // class_defs count
840        u32put(&mut out, 100, 0);
841
842        // string_ids
843        for (i, &so) in string_offs.iter().enumerate() {
844            u32put(&mut out, off_string_ids + i * 4, so as u32);
845        }
846        // type_ids → descriptor string index
847        for (i, t) in types.iter().enumerate() {
848            let si = strings.iter().position(|s| s == t).unwrap();
849            u32put(&mut out, off_type_ids + i * 4, si as u32);
850        }
851        // proto_ids: shorty, return, params_off
852        u32put(&mut out, off_proto_ids, 0);
853        u32put(&mut out, off_proto_ids + 4, 0);
854        u32put(&mut out, off_proto_ids + 8, params_off as u32);
855        // method_ids: class_idx, proto_idx, name_idx
856        let m = off_method_ids;
857        out[m..m + 2].copy_from_slice(&0u16.to_le_bytes());
858        out[m + 2..m + 4].copy_from_slice(&0u16.to_le_bytes());
859        u32put(&mut out, m + 4, 1);
860
861        out
862    }
863
864    #[test]
865    fn parses_int_first_proto() {
866        let dex = build_dex(
867            b"Landroid/app/IProcessObserver;",
868            b"onForegroundActivitiesChanged",
869            &[b"I", b"I", b"Z"],
870        );
871        let params = method_param_types(
872            &dex,
873            b"Landroid/app/IProcessObserver;",
874            b"onForegroundActivitiesChanged",
875        )
876        .expect("method present");
877        assert_eq!(params, [&b"I"[..], &b"I"[..], &b"Z"[..]]);
878    }
879
880    #[test]
881    fn rejects_string_first_proto() {
882        // Stock signature onForegroundActivitiesChanged(String, int, boolean)
883        let dex = build_dex(
884            b"Landroid/app/IProcessObserver;",
885            b"onForegroundActivitiesChanged",
886            &[b"Ljava/lang/String;", b"I", b"Z"],
887        );
888        let params = method_param_types(
889            &dex,
890            b"Landroid/app/IProcessObserver;",
891            b"onForegroundActivitiesChanged",
892        )
893        .expect("method present");
894        assert_eq!(params.first().map(|t| *t == b"I"), Some(false));
895    }
896
897    #[test]
898    fn absent_method_returns_none() {
899        let dex = build_dex(
900            b"Landroid/app/IProcessObserver;",
901            b"onForegroundActivitiesChanged",
902            &[b"I"],
903        );
904        assert!(
905            method_param_types(&dex, b"Landroid/app/IProcessObserver;", b"onProcessDied").is_none()
906        );
907    }
908
909    #[test]
910    fn capped_param_count_does_not_allocate_huge() {
911        // A corrupt DEX declaring a gigantic type_list size must not drive a
912        // multi-gigabyte `with_capacity` allocation; it is capped to the room
913        // actually present after the count field.
914        let mut dex = vec![0u8; 136];
915        dex[0..4].copy_from_slice(DEX_MAGIC);
916        // string_ids: size 1 @ 112, type_ids: size 1 @ 116.
917        dex[56..60].copy_from_slice(&1u32.to_le_bytes());
918        dex[60..64].copy_from_slice(&112u32.to_le_bytes());
919        dex[64..68].copy_from_slice(&1u32.to_le_bytes());
920        dex[68..72].copy_from_slice(&116u32.to_le_bytes());
921        // proto_ids: size 1 @ 120; params_off points to a huge type_list count.
922        dex[72..76].copy_from_slice(&1u32.to_le_bytes());
923        dex[76..80].copy_from_slice(&120u32.to_le_bytes());
924        // method_ids: size 1 @ 124.
925        dex[88..92].copy_from_slice(&1u32.to_le_bytes());
926        dex[92..96].copy_from_slice(&124u32.to_le_bytes());
927        // proto_item: shorty/return/params_off at 120; params_off = 128 (the count).
928        dex[128..132].copy_from_slice(&u32::MAX.to_le_bytes());
929        // method_item at 124: class 0, proto 0, name 1.
930        // String 0 = class, string 1 = method name.
931        let params = method_param_types(
932            &dex,
933            b"Landroid/app/IProcessObserver;",
934            b"onForegroundActivitiesChanged",
935        );
936        assert!(params.is_none());
937    }
938
939    #[test]
940    fn deep_encoded_value_recursion_is_bounded() {
941        // Nest VALUE_ARRAY inside VALUE_ARRAY far beyond the depth bound; the
942        // walker must return None instead of overflowing the stack.
943        let mut bytes = vec![0u8; 2 + 2 * (MAX_ENCODED_VALUE_DEPTH + 2)];
944        bytes[0] = 0x1c; // VALUE_ARRAY
945        bytes[1] = 1; // one element
946        let mut off = 2;
947        for _ in 0..=MAX_ENCODED_VALUE_DEPTH + 1 {
948            bytes[off] = 0x1c; // VALUE_ARRAY
949            bytes[off + 1] = 1;
950            off += 2;
951        }
952        let mut pos = 0;
953        let r = skip_encoded_value(&bytes, &mut pos);
954        assert!(r.is_none());
955    }
956}