Skip to main content

mediatek_optical/
receiver.rs

1//! Read-only detection of the active MediaTek update receiver.
2//!
3//! BOOT and application receivers can require different transfer encodings,
4//! even in the same image. Positive Thumb code signatures identify the direct
5//! banner comparison or the decrypt-before-banner-check path. Neither a model,
6//! revision date, target image nor absence of AES selects plaintext.
7//!
8//! The bounded search covers the receiver locations in the audited MT1939 /
9//! MT1959 corpus. Unreadable or unrecognized code returns an error. This is not
10//! permission to flash and does not replace image/hardware compatibility gates.
11use crate::{
12    cdb,
13    drive::{self, Transport},
14    INQUIRY_LEN,
15};
16
17pub mod entry;
18
19#[cfg(all(feature = "image", feature = "enc"))]
20pub mod analysis;
21#[cfg(all(feature = "image", feature = "enc"))]
22pub mod decoding;
23#[cfg(all(feature = "image", feature = "enc"))]
24pub mod layout;
25#[cfg(all(feature = "image", feature = "enc"))]
26pub mod programming;
27
28#[cfg(all(feature = "image", feature = "enc"))]
29mod mixed;
30#[cfg(all(feature = "image", feature = "enc"))]
31mod prepared;
32#[cfg(all(feature = "image", feature = "enc"))]
33pub use prepared::{ExecuteError, PreparedUpdate, TransferEncoding, TransferPlan};
34#[cfg(all(feature = "image", feature = "enc"))]
35mod profile;
36#[cfg(all(feature = "image", feature = "enc"))]
37pub mod reference;
38#[cfg(all(feature = "image", feature = "enc"))]
39pub use profile::detect;
40
41/// Code currently handling commands.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Mode {
44    /// Recovery bootloader (INQUIRY revision `BOOT`).
45    Boot,
46    /// Running firmware application.
47    Firmware,
48}
49
50/// Encoding of a region in a receiver transfer plan.
51/// Legacy receiver detection reports the header path; executable plans expose
52/// header and body separately through `TransferEncoding`.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum Encoding {
55    /// Send the canonical image unchanged.
56    Plaintext,
57    /// Send the AES-128-ECB transport envelope.
58    Encrypted,
59}
60
61/// A live INQUIRY binding, obtained by [`detect_mode`].
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct DetectedMode {
64    mode: Mode,
65    inquiry: [u8; INQUIRY_LEN],
66}
67impl DetectedMode {
68    /// Whether BOOT or the application is running.
69    pub fn mode(&self) -> Mode {
70        self.mode
71    }
72    /// Standard INQUIRY bytes used to bind this detection.
73    pub fn inquiry(&self) -> &[u8; INQUIRY_LEN] {
74        &self.inquiry
75    }
76}
77
78/// Detection failed; no implicit encoding is selected.
79///
80/// Hosts must keep a wildcard arm; the set of variants may grow.
81///
82/// ```compile_fail,E0004
83/// fn f(e: mediatek_optical::receiver::Error<()>) -> u8 { match e { mediatek_optical::receiver::Error::Read { .. } => 0, mediatek_optical::receiver::Error::InvalidIdentity { .. } => 1, mediatek_optical::receiver::Error::Unknown { .. } => 2, mediatek_optical::receiver::Error::Conflict { .. } => 3, mediatek_optical::receiver::Error::Changed { .. } => 4, mediatek_optical::receiver::Error::UnknownEntry { .. } => 5, } }
84/// ```
85#[derive(Debug)]
86#[non_exhaustive]
87pub enum Error<E> {
88    /// A required read failed or was short.
89    Read(drive::Error<E>),
90    /// INQUIRY is not a complete optical-drive identity with a revision.
91    InvalidIdentity,
92    /// No positive receiver signature was readable in the supported range.
93    Unknown,
94    /// More than one receiver location or encoding matched.
95    Conflict,
96    /// Identity or receiver bytes changed after detection.
97    Changed,
98    /// The running receiver's entry grammar is unreadable or unrecognized.
99    UnknownEntry,
100}
101impl<E: core::fmt::Debug> core::fmt::Display for Error<E> {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        match self {
104            Self::Read(e) => write!(f, "receiver read failed: {e}"),
105            Self::InvalidIdentity => f.write_str("invalid MediaTek receiver INQUIRY identity"),
106            Self::Unknown => f.write_str(
107                "active MediaTek receiver signature unreadable or unknown; encoding not selected",
108            ),
109            Self::Conflict => {
110                f.write_str("conflicting MediaTek receiver signatures; encoding not selected")
111            }
112            Self::Changed => {
113                f.write_str("MediaTek receiver changed since detection; re-plan the flash")
114            }
115            Self::UnknownEntry => f.write_str(
116                "active MediaTek entry contract unreadable or unknown; no PREPARE selected",
117            ),
118        }
119    }
120}
121#[cfg(feature = "std")]
122impl<E: core::fmt::Debug + 'static> std::error::Error for Error<E> {
123    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124        match self {
125            Self::Read(source) => Some(source),
126            Self::InvalidIdentity
127            | Self::Unknown
128            | Self::Conflict
129            | Self::Changed
130            | Self::UnknownEntry => None,
131        }
132    }
133}
134
135/// Identify BOOT versus running firmware using standard INQUIRY only.
136/// No feature 0x010C, descriptor, update command or data-out is required.
137pub fn detect_mode<T: Transport>(t: &mut T) -> Result<DetectedMode, Error<T::Error>> {
138    let mut inquiry = [0; INQUIRY_LEN];
139    drive::read(
140        t,
141        &cdb::inquiry(INQUIRY_LEN as u16),
142        &mut inquiry,
143        INQUIRY_LEN,
144    )
145    .map_err(Error::Read)?;
146    let revision = &inquiry[32..36];
147    if inquiry[0] & 0x1f != 5
148        || inquiry[4] < 31
149        || !revision.iter().all(|b| b.is_ascii_graphic() || *b == b' ')
150        || revision.iter().all(|b| *b == b' ')
151    {
152        return Err(Error::InvalidIdentity);
153    }
154    let mode = if revision == b"BOOT" {
155        Mode::Boot
156    } else {
157        Mode::Firmware
158    };
159    Ok(DetectedMode { mode, inquiry })
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163struct Evidence {
164    address: u32,
165    len: usize,
166    bytes: [u8; 64],
167}
168impl Evidence {
169    fn new(address: u32, bytes: &[u8]) -> Self {
170        let mut e = Self {
171            address,
172            len: bytes.len(),
173            bytes: [0; 64],
174        };
175        e.bytes[..bytes.len()].copy_from_slice(bytes);
176        e
177    }
178}
179
180/// Positive receiver evidence, bound to the live mode and code bytes.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct Receiver {
183    identity: DetectedMode,
184    encoding: Encoding,
185    site: Evidence,
186    transform: Option<Evidence>,
187    #[cfg(all(feature = "image", feature = "enc"))]
188    profile: Option<profile::Profile>,
189    #[cfg(all(feature = "image", feature = "enc"))]
190    reference: Option<reference::Binding>,
191}
192impl Receiver {
193    /// Cached application evidence from the same snapshot used by live entry
194    /// detection. This is analysis, not a completed transfer authorization.
195    #[cfg(all(feature = "image", feature = "enc"))]
196    pub fn application_analysis(&self) -> Option<&analysis::ApplicationAnalysis> {
197        self.profile.as_ref()?.application.as_ref()
198    }
199    /// Active receiver mode.
200    pub fn mode(&self) -> Mode {
201        self.identity.mode
202    }
203    /// Header-path classification retained for compatibility. Consult the
204    /// prepared transfer plan for separate header/body wire encodings.
205    pub fn encoding(&self) -> Encoding {
206        self.encoding
207    }
208    /// PREPARE dialect from full live entry detection; encoding-only detection
209    /// deliberately supplies no command.
210    #[cfg(all(feature = "image", feature = "enc"))]
211    pub fn prepare_command(&self) -> Option<cdb::Prepare> {
212        self.profile.as_ref().map(|profile| profile.prepare)
213    }
214    /// Address of the matched receiver instructions.
215    pub fn signature_address(&self) -> u32 {
216        self.site.address
217    }
218    /// Re-read identity and matched instructions immediately before writing.
219    pub fn revalidate<T: Transport>(&self, t: &mut T) -> Result<(), Error<T::Error>> {
220        #[cfg(all(feature = "image", feature = "enc"))]
221        if let Some(reference) = &self.reference {
222            return reference.revalidate(t);
223        }
224        self.revalidate_signature(t)?;
225        #[cfg(all(feature = "image", feature = "enc"))]
226        if let Some(profile) = &self.profile {
227            profile.revalidate(t)?;
228        }
229        Ok(())
230    }
231
232    // Detection finishes with a small coherence check. The complete snapshot
233    // is checked separately, immediately before any mutating execution.
234    fn revalidate_signature<T: Transport>(&self, t: &mut T) -> Result<(), Error<T::Error>> {
235        if detect_mode(t)? != self.identity {
236            return Err(Error::Changed);
237        }
238        for e in core::iter::once(&self.site).chain(self.transform.iter()) {
239            let mut bytes = [0; 64];
240            drive::read_memory(t, e.address, &mut bytes[..e.len]).map_err(Error::Read)?;
241            if bytes[..e.len] != e.bytes[..e.len] {
242                return Err(Error::Changed);
243            }
244        }
245        Ok(())
246    }
247}
248
249// 0x100 masks a relocation/stack-slot byte. These are positive instruction
250// sequences, not whole-build hashes. Matches are halfword aligned.
251const ANY: u16 = 0x100;
252const PLAIN_DIRECT: &[u16] = &[
253    0x03, 0x21, 0x09, 0x03, 0x61, 0x18, ANY, 0xa8, 0x00, 0x5d, 0x09, 0x78, 0x88, 0x42,
254];
255const PLAIN_APP: &[u16] = &[
256    0x03, 0x20, ANY, 0x49, 0x00, 0x03, 0x20, 0x18, 0x09, 0x68, 0x00, 0x02, 0x00, 0x0a, 0x40, 0x18,
257    ANY, 0xa9, 0x09, 0x5d, 0x00, 0x78, 0x88, 0x42,
258];
259const PLAIN_BOOT: &[u16] = &[
260    0x03, 0x20, 0x00, 0x03, 0x20, 0x18, 0xf9, 0x69, 0x00, 0x02, 0x00, 0x0a, 0x40, 0x18, ANY, 0xa9,
261    0x09, 0x5d, 0x00, 0x78, 0x88, 0x42,
262];
263const PLAIN_LOGGED: &[u16] = &[
264    0x03, 0x20, 0x00, 0x03, 0x20, 0x18, 0x05, 0x02, 0xf8, 0x69, 0x2d, 0x0a, 0x28, 0x18, 0x02, 0x78,
265    ANY, 0x48, 0x21, 0x00, ANY, ANY, ANY, ANY, 0xf8, 0x69, 0x28, 0x18, 0x00, 0x78, 0x31, 0x5d,
266    0x88, 0x42,
267];
268const AES: &[u16] = &[
269    0xf7, 0xb5, 0x06, 0x00, 0x14, 0x00, ANY, 0x48, 0x11, 0x00, 0x84, 0xb0, ANY, ANY, ANY, ANY,
270    0x00, 0x2c, 0x01, 0xd0, 0x20, 0x07, 0x02, 0xd0, 0x00, 0x20, 0x07, 0xb0, 0xf0, 0xbd, 0xa0, 0x08,
271    0x01, 0x22, 0x92, 0x06, 0x02, 0x90, 0x10, 0x68, 0x10, 0x21, 0x88, 0x43, 0x10, 0x60,
272];
273fn matches(bytes: &[u8], pattern: &[u16]) -> bool {
274    bytes.len() >= pattern.len()
275        && bytes
276            .iter()
277            .zip(pattern)
278            .all(|(&b, &p)| p == ANY || p == b as u16)
279}
280fn contains(bytes: &[u8], needle: &[u8]) -> bool {
281    bytes.windows(needle.len()).any(|b| b == needle)
282}
283fn branch_target(bytes: &[u8], address: u32) -> Option<u32> {
284    let a = u16::from_le_bytes([bytes[0], bytes[1]]);
285    let b = u16::from_le_bytes([bytes[2], bytes[3]]);
286    if a & 0xf800 != 0xf000 || b & 0xf800 != 0xf800 {
287        return None;
288    }
289    let delta = (((a & 2047) as i32) << 12) | (((b & 2047) as i32) << 1);
290    let delta = (delta << 9) >> 9;
291    u32::try_from(address as i64 + 4 + delta as i64).ok()
292}
293
294/// Detect the transfer encoding by reading the active receiver's instructions.
295/// Reads memory only; never stages data or tries alternative write formats.
296/// `mode` must come from [`detect_mode`]; identity is checked again on return.
297pub fn detect_encoding<T: Transport>(
298    t: &mut T,
299    mode: &DetectedMode,
300) -> Result<Receiver, Error<T::Error>> {
301    let (start, end) = match mode.mode {
302        Mode::Boot => (0x3400, 0x3800),
303        Mode::Firmware => (0x98000, 0xb4000),
304    };
305    let mut found: Option<Receiver> = None;
306    // 64-byte overlap preserves instructions spanning a read boundary.
307    let mut block = [0; 1088];
308    let mut base = start;
309    while base < end {
310        let len = core::cmp::min(block.len(), (end - base) as usize);
311        if drive::read_memory(t, base, &mut block[..len]).is_ok() {
312            for off in (0..len.saturating_sub(4)).step_by(2) {
313                let bytes = &block[off..len];
314                let address = base + off as u32;
315                let mut candidate = None;
316                for pattern in [PLAIN_DIRECT, PLAIN_APP, PLAIN_BOOT, PLAIN_LOGGED] {
317                    if matches(bytes, pattern) {
318                        candidate = Some(Receiver {
319                            identity: mode.clone(),
320                            encoding: Encoding::Plaintext,
321                            site: Evidence::new(address, &bytes[..pattern.len()]),
322                            transform: None,
323                            #[cfg(all(feature = "image", feature = "enc"))]
324                            profile: None,
325                            #[cfg(all(feature = "image", feature = "enc"))]
326                            reference: None,
327                        });
328                    }
329                }
330                // Validate a header-decrypt call AND its hardware AES routine.
331                // The caller must pass staged header +0x3000 and length 16.
332                let before = &block[off.saturating_sub(32)..off];
333                if off >= 32
334                    && contains(before, &[0x10, 0x22])
335                    && (contains(before, &[3, 0x20, 0, 3])
336                        || contains(before, &[3, 0x23]) && contains(before, &[0x1b, 3]))
337                {
338                    if let Some(target) = branch_target(bytes, address) {
339                        let valid_range = match mode.mode {
340                            Mode::Boot => target < 0x10000,
341                            Mode::Firmware => (0x10000..0x1c0000).contains(&target),
342                        };
343                        let mut routine = [0; 46];
344                        if valid_range
345                            && drive::read_memory(t, target, &mut routine).is_ok()
346                            && matches(&routine, AES)
347                        {
348                            let begin = off.saturating_sub(32);
349                            candidate = Some(Receiver {
350                                identity: mode.clone(),
351                                encoding: Encoding::Encrypted,
352                                site: Evidence::new(base + begin as u32, &block[begin..off + 4]),
353                                transform: Some(Evidence::new(target, &routine)),
354                                #[cfg(all(feature = "image", feature = "enc"))]
355                                profile: None,
356                                #[cfg(all(feature = "image", feature = "enc"))]
357                                reference: None,
358                            });
359                        }
360                    }
361                }
362                if let Some(candidate) = candidate {
363                    if let Some(previous) = &found {
364                        if previous != &candidate {
365                            return Err(Error::Conflict);
366                        }
367                    } else {
368                        found = Some(candidate);
369                    }
370                }
371            }
372        }
373        base += 1024;
374    }
375    let receiver = found.ok_or(Error::Unknown)?;
376    receiver.revalidate(t)?;
377    Ok(receiver)
378}
379
380#[cfg(test)]
381mod tests;