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#[cfg(test)]
666mod tests {
667    use super::*;
668
669    fn u32put(b: &mut [u8], off: usize, v: u32) {
670        b[off..off + 4].copy_from_slice(&v.to_le_bytes());
671    }
672
673    // Build a minimal DEX header + string/type/proto/method id tables for a
674    // single class with a single method whose proto has the given param type
675    // descriptors. Only the sections method_param_types() reads are populated.
676    fn build_dex(class_desc: &[u8], method_name: &[u8], params: &[&[u8]]) -> Vec<u8> {
677        let mut out = vec![0u8; 112];
678        out[0..4].copy_from_slice(DEX_MAGIC);
679        out[4..8].copy_from_slice(b"035\0");
680
681        let mut strings: Vec<Vec<u8>> = vec![class_desc.to_vec(), method_name.to_vec()];
682        for p in params {
683            if !strings.iter().any(|s| s == p) {
684                strings.push(p.to_vec());
685            }
686        }
687        let n_string = strings.len();
688        let mut types: Vec<Vec<u8>> = vec![class_desc.to_vec()];
689        for p in params {
690            if !types.iter().any(|t| t == p) {
691                types.push(p.to_vec());
692            }
693        }
694        let n_type = types.len();
695
696        let off_string_ids = 112;
697        let off_type_ids = off_string_ids + n_string * 4;
698        let off_proto_ids = off_type_ids + n_type * 4;
699        let off_method_ids = off_proto_ids + 12; // one proto
700
701        // Reserve id table space.
702        out.resize(off_method_ids + 8, 0); // one method
703
704        // string_data items (in file order, referenced by string_ids)
705        let mut string_offs = Vec::with_capacity(n_string);
706        for s in &strings {
707            string_offs.push(out.len());
708            let n = s.len() as u8;
709            out.push(n); // short ULEB128 length (ASCII only)
710            out.extend_from_slice(s);
711            out.push(0);
712        }
713
714        // type_list for the single proto's parameters.
715        let params_off = out.len();
716        out.extend_from_slice(&(params.len() as u32).to_le_bytes());
717        for p in params {
718            let ti = types.iter().position(|t| t == p).unwrap();
719            out.extend_from_slice(&(ti as u16).to_le_bytes());
720        }
721
722        // Header: counts + offsets (offsets 56..=100).
723        u32put(&mut out, 56, n_string as u32);
724        u32put(&mut out, 60, off_string_ids as u32);
725        u32put(&mut out, 64, n_type as u32);
726        u32put(&mut out, 68, off_type_ids as u32);
727        u32put(&mut out, 72, 1); // proto count
728        u32put(&mut out, 76, off_proto_ids as u32);
729        u32put(&mut out, 80, 0); // field_ids count
730        u32put(&mut out, 84, 0);
731        u32put(&mut out, 88, 1); // method count
732        u32put(&mut out, 92, off_method_ids as u32);
733        u32put(&mut out, 96, 0); // class_defs count
734        u32put(&mut out, 100, 0);
735
736        // string_ids
737        for (i, &so) in string_offs.iter().enumerate() {
738            u32put(&mut out, off_string_ids + i * 4, so as u32);
739        }
740        // type_ids → descriptor string index
741        for (i, t) in types.iter().enumerate() {
742            let si = strings.iter().position(|s| s == t).unwrap();
743            u32put(&mut out, off_type_ids + i * 4, si as u32);
744        }
745        // proto_ids: shorty, return, params_off
746        u32put(&mut out, off_proto_ids, 0);
747        u32put(&mut out, off_proto_ids + 4, 0);
748        u32put(&mut out, off_proto_ids + 8, params_off as u32);
749        // method_ids: class_idx, proto_idx, name_idx
750        let m = off_method_ids;
751        out[m..m + 2].copy_from_slice(&0u16.to_le_bytes());
752        out[m + 2..m + 4].copy_from_slice(&0u16.to_le_bytes());
753        u32put(&mut out, m + 4, 1);
754
755        out
756    }
757
758    #[test]
759    fn parses_int_first_proto() {
760        let dex = build_dex(
761            b"Landroid/app/IProcessObserver;",
762            b"onForegroundActivitiesChanged",
763            &[b"I", b"I", b"Z"],
764        );
765        let params = method_param_types(
766            &dex,
767            b"Landroid/app/IProcessObserver;",
768            b"onForegroundActivitiesChanged",
769        )
770        .expect("method present");
771        assert_eq!(params, [&b"I"[..], &b"I"[..], &b"Z"[..]]);
772    }
773
774    #[test]
775    fn rejects_string_first_proto() {
776        // Stock signature onForegroundActivitiesChanged(String, int, boolean)
777        let dex = build_dex(
778            b"Landroid/app/IProcessObserver;",
779            b"onForegroundActivitiesChanged",
780            &[b"Ljava/lang/String;", b"I", b"Z"],
781        );
782        let params = method_param_types(
783            &dex,
784            b"Landroid/app/IProcessObserver;",
785            b"onForegroundActivitiesChanged",
786        )
787        .expect("method present");
788        assert_eq!(params.first().map(|t| *t == b"I"), Some(false));
789    }
790
791    #[test]
792    fn absent_method_returns_none() {
793        let dex = build_dex(
794            b"Landroid/app/IProcessObserver;",
795            b"onForegroundActivitiesChanged",
796            &[b"I"],
797        );
798        assert!(
799            method_param_types(&dex, b"Landroid/app/IProcessObserver;", b"onProcessDied").is_none()
800        );
801    }
802
803    #[test]
804    fn capped_param_count_does_not_allocate_huge() {
805        // A corrupt DEX declaring a gigantic type_list size must not drive a
806        // multi-gigabyte `with_capacity` allocation; it is capped to the room
807        // actually present after the count field.
808        let mut dex = vec![0u8; 136];
809        dex[0..4].copy_from_slice(DEX_MAGIC);
810        // string_ids: size 1 @ 112, type_ids: size 1 @ 116.
811        dex[56..60].copy_from_slice(&1u32.to_le_bytes());
812        dex[60..64].copy_from_slice(&112u32.to_le_bytes());
813        dex[64..68].copy_from_slice(&1u32.to_le_bytes());
814        dex[68..72].copy_from_slice(&116u32.to_le_bytes());
815        // proto_ids: size 1 @ 120; params_off points to a huge type_list count.
816        dex[72..76].copy_from_slice(&1u32.to_le_bytes());
817        dex[76..80].copy_from_slice(&120u32.to_le_bytes());
818        // method_ids: size 1 @ 124.
819        dex[88..92].copy_from_slice(&1u32.to_le_bytes());
820        dex[92..96].copy_from_slice(&124u32.to_le_bytes());
821        // proto_item: shorty/return/params_off at 120; params_off = 128 (the count).
822        dex[128..132].copy_from_slice(&u32::MAX.to_le_bytes());
823        // method_item at 124: class 0, proto 0, name 1.
824        // String 0 = class, string 1 = method name.
825        let params = method_param_types(
826            &dex,
827            b"Landroid/app/IProcessObserver;",
828            b"onForegroundActivitiesChanged",
829        );
830        assert!(params.is_none());
831    }
832
833    #[test]
834    fn deep_encoded_value_recursion_is_bounded() {
835        // Nest VALUE_ARRAY inside VALUE_ARRAY far beyond the depth bound; the
836        // walker must return None instead of overflowing the stack.
837        let mut bytes = vec![0u8; 2 + 2 * (MAX_ENCODED_VALUE_DEPTH + 2)];
838        bytes[0] = 0x1c; // VALUE_ARRAY
839        bytes[1] = 1; // one element
840        let mut off = 2;
841        for _ in 0..=MAX_ENCODED_VALUE_DEPTH + 1 {
842            bytes[off] = 0x1c; // VALUE_ARRAY
843            bytes[off + 1] = 1;
844            off += 2;
845        }
846        let mut pos = 0;
847        let r = skip_encoded_value(&bytes, &mut pos);
848        assert!(r.is_none());
849    }
850}