Skip to main content

mediatek_optical/receiver/
analysis.rs

1//! Immutable application receiver evidence collected from one code snapshot.
2//!
3//! Discovery is deliberately separate from an executable transfer contract.
4//! A scratch decoder and direct plaintext comparison do not, by themselves,
5//! prove the complete integrity/programming path or authorize a write.
6use super::{
7    decoding::{self, Call, Value},
8    programming::{self, IntegritySelection, PlainSignature, Selection},
9};
10
11/// Range selection and the immediately following direct signature comparison.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SelectedRange {
14    /// Operand-bound range selection.
15    pub selection: Selection,
16    /// Present only if the uninterrupted direct comparison grammar matches.
17    pub signature: Option<PlainSignature>,
18    /// Unique checked acceptance tail using these selection cells. Reachability
19    /// from the signature loop and helper semantics are separate obligations.
20    pub acceptance: Option<programming::AcceptanceTail>,
21}
22
23/// A scratch header call and in-place body call sharing the same pointer cell
24/// and AES routine. Both selected-start policies are traced independently.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct BulkDecode {
27    /// Scratch header input/output evidence.
28    pub header: Call,
29    /// Body call when the selected start is zero.
30    pub from_zero: Call,
31    /// Body call when boot is preserved. Its footprint may exceed image size;
32    /// consumers must not silently clip it to uploaded bytes.
33    pub preserving_boot: Call,
34}
35
36/// Read-only evidence retained by the loaded application receiver.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ApplicationAnalysis {
39    ranges: Vec<SelectedRange>,
40    integrity: Vec<IntegritySelection>,
41    scratch: Vec<Call>,
42    bulk: Vec<BulkDecode>,
43    conflicting_arguments: bool,
44}
45
46impl ApplicationAnalysis {
47    /// Analyze a bounded application code snapshot once. No firmware target,
48    /// filename, model or version string contributes to the result.
49    pub fn inspect(bytes: &[u8], base: u32) -> Option<Self> {
50        if base & 1 != 0 {
51            return None;
52        }
53        let end = base.checked_add(u32::try_from(bytes.len()).ok()?)?;
54        let mut ranges = Vec::new();
55        let mut integrity = Vec::new();
56        for address in (base..end).step_by(2) {
57            if let Some(selection) = programming::inspect_selection(bytes, base, address) {
58                let signature = programming::inspect_plain_signature(bytes, base, &selection);
59                ranges.push(SelectedRange {
60                    selection,
61                    signature,
62                    acceptance: None,
63                });
64            }
65            if let Some(proof) = programming::inspect_legacy_integrity(bytes, base, address) {
66                integrity.push(proof);
67            }
68        }
69        let mut scratch: Vec<Call> = Vec::new();
70        let mut conflicting_arguments = false;
71        for range in &mut ranges {
72            let Some(signature) = range.signature else {
73                continue;
74            };
75            let mut candidates = integrity.iter().filter_map(|checks| {
76                if checks.address < signature.success
77                    || checks.address.checked_sub(signature.success)? > 1024
78                {
79                    return None;
80                }
81                programming::inspect_acceptance_tail(
82                    bytes,
83                    base,
84                    &range.selection,
85                    &signature,
86                    checks,
87                )
88            });
89            let first = candidates.next();
90            if candidates.next().is_some() {
91                conflicting_arguments = true;
92            } else {
93                range.acceptance = first;
94            }
95        }
96        for range in &ranges {
97            let Some(signature) = range.signature else {
98                continue;
99            };
100            // The signature grammar independently establishes which RAM cell
101            // holds staged bytes. Trace candidate setups without treating every
102            // RAM load as a staging pointer. Reachability remains unproven here.
103            let first = range.selection.address.saturating_sub(512).max(base);
104            for start in (first..range.selection.address).step_by(2) {
105                let Some(call) =
106                    decoding::trace_call(bytes, base, start, [Value::Unknown; 8], |a| {
107                        (a == Value::Constant(signature.staging_cell)).then_some(Value::Staging(0))
108                    })
109                else {
110                    continue;
111                };
112                if call.address >= range.selection.address
113                    || call.length != 16
114                    || call.source != Value::Staging(0x3000)
115                    || !matches!(call.destination, Value::Stack(_))
116                {
117                    continue;
118                }
119                let Some(offset) = call
120                    .target
121                    .checked_sub(base)
122                    .and_then(|x| usize::try_from(x).ok())
123                else {
124                    continue;
125                };
126                let Some(routine) = bytes.get(offset..) else {
127                    continue;
128                };
129                if !super::matches(routine, super::AES) {
130                    continue;
131                }
132                if let Some(previous) = scratch.iter().find(|c| c.address == call.address) {
133                    // r3 is not an AES argument. Different tracing start points
134                    // can establish different irrelevant r3 facts.
135                    conflicting_arguments |= previous.target != call.target
136                        || previous.source != call.source
137                        || previous.destination != call.destination
138                        || previous.length != call.length;
139                } else {
140                    scratch.push(call);
141                }
142            }
143        }
144        scratch.sort_by_key(|c| c.address);
145        let mut bulk = Vec::new();
146        for range in &ranges {
147            let trace = |start| {
148                decoding::trace_call(bytes, base, range.selection.end, [Value::Unknown; 8], |a| {
149                    match a {
150                        Value::Constant(cell) if cell == range.selection.start_cell => {
151                            Some(Value::Constant(start))
152                        }
153                        Value::Constant(cell)
154                            if cell & 3 == 0 && (0x02000000..0x02010000).contains(&cell) =>
155                        {
156                            Some(Value::Loaded { cell, offset: 0 })
157                        }
158                        _ => None,
159                    }
160                })
161            };
162            let (Some(from_zero), Some(preserving_boot)) = (trace(0), trace(0x10000)) else {
163                continue;
164            };
165            let Value::Loaded { cell, offset: 0 } = from_zero.source else {
166                continue;
167            };
168            if from_zero.destination != from_zero.source
169                || from_zero.length != 0x200000
170                || preserving_boot.address != from_zero.address
171                || preserving_boot.target != from_zero.target
172                || preserving_boot.length != from_zero.length
173                || preserving_boot.source
174                    != (Value::Loaded {
175                        cell,
176                        offset: 0x10000,
177                    })
178                || preserving_boot.destination != preserving_boot.source
179            {
180                continue;
181            }
182            let Some(offset) = from_zero
183                .target
184                .checked_sub(base)
185                .and_then(|x| usize::try_from(x).ok())
186            else {
187                continue;
188            };
189            if !bytes
190                .get(offset..)
191                .is_some_and(|routine| super::matches(routine, super::AES))
192            {
193                continue;
194            }
195            let first = range.selection.address.saturating_sub(512).max(base);
196            let mut header: Option<Call> = None;
197            let mut ambiguous = false;
198            for start in (first..range.selection.address).step_by(2) {
199                let Some(call) =
200                    decoding::trace_call(bytes, base, start, [Value::Unknown; 8], |a| {
201                        (a == Value::Constant(cell)).then_some(Value::Loaded { cell, offset: 0 })
202                    })
203                else {
204                    continue;
205                };
206                if call.address >= range.selection.address
207                    || call.target != from_zero.target
208                    || call.source
209                        != (Value::Loaded {
210                            cell,
211                            offset: 0x3000,
212                        })
213                    || call.length != 16
214                    || !matches!(call.destination, Value::Stack(_))
215                {
216                    continue;
217                }
218                if let Some(previous) = header {
219                    ambiguous |= previous.address != call.address
220                        || previous.destination != call.destination;
221                } else {
222                    header = Some(call);
223                }
224            }
225            if ambiguous {
226                conflicting_arguments = true;
227            } else if let Some(header) = header {
228                bulk.push(BulkDecode {
229                    header,
230                    from_zero,
231                    preserving_boot,
232                });
233            }
234        }
235        Some(Self {
236            ranges,
237            integrity,
238            scratch,
239            bulk,
240            conflicting_arguments,
241        })
242    }
243
244    /// All matched range selections, retaining missing signature evidence.
245    pub fn ranges(&self) -> &[SelectedRange] {
246        &self.ranges
247    }
248    /// Matched integrity-selection blocks; checker semantics remain separate.
249    pub fn integrity_selections(&self) -> &[IntegritySelection] {
250        &self.integrity
251    }
252    /// Argument-traced scratch header decoder calls, not whole-image decoders.
253    pub fn scratch_decoders(&self) -> &[Call] {
254        &self.scratch
255    }
256    /// Whether candidate traces disagree about a decoder's actual arguments.
257    pub fn conflicting_arguments(&self) -> bool {
258        self.conflicting_arguments
259    }
260    /// Positive header/body call evidence. An empty result never means that
261    /// the receiver accepts plaintext; incomplete grammars remain unknown.
262    pub fn bulk_decoders(&self) -> &[BulkDecode] {
263        &self.bulk
264    }
265}