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