Skip to main content

mediatek_optical/receiver/
decoding.rs

1//! Bounded Thumb argument tracing for receiver AES calls.
2//!
3//! A nearby MOV #16 is not argument evidence. Trace each supported instruction
4//! from supplied incoming register facts to the actual call. Conditional branches,
5//! stores, unknown instructions and overflow stop analysis. Handler ownership
6//! and incoming facts remain separate obligations of the control-flow analyzer.
7
8/// Values sufficient to distinguish staging addresses from scratch outputs.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Value {
11    /// No established value; never usable as an AES argument.
12    Unknown,
13    /// An exact immediate or literal value.
14    Constant(u32),
15    /// Byte offset relative to the upload staging buffer.
16    Staging(u32),
17    /// Byte offset relative to the current stack pointer.
18    Stack(u32),
19    /// Pointer loaded from a specific RAM cell, before assigning it a staging
20    /// role. Keeping provenance avoids guessing that every RAM load is staging.
21    Loaded {
22        /// Address of the pointer cell.
23        cell: u32,
24        /// Constant offset added after loading the pointer.
25        offset: u32,
26    },
27}
28
29impl Value {
30    fn add(self, other: Self) -> Option<Self> {
31        match (self, other) {
32            (Self::Constant(a), Self::Constant(b)) => Some(Self::Constant(a.checked_add(b)?)),
33            (Self::Staging(a), Self::Constant(b)) | (Self::Constant(b), Self::Staging(a)) => {
34                Some(Self::Staging(a.checked_add(b)?))
35            }
36            (Self::Stack(a), Self::Constant(b)) | (Self::Constant(b), Self::Stack(a)) => {
37                Some(Self::Stack(a.checked_add(b)?))
38            }
39            (Self::Loaded { cell, offset }, Self::Constant(b))
40            | (Self::Constant(b), Self::Loaded { cell, offset }) => Some(Self::Loaded {
41                cell,
42                offset: offset.checked_add(b)?,
43            }),
44            _ => None,
45        }
46    }
47}
48
49/// Proven arguments at a direct Thumb BL call.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Call {
52    /// Instruction address of the call, for retained evidence and revalidation.
53    pub address: u32,
54    /// Exact target, which still requires independent AES-routine verification.
55    pub target: u32,
56    /// Source of the input blocks.
57    pub source: Value,
58    /// Distinguishes scratch decoding from in-place staging decoding.
59    pub destination: Value,
60    /// Exact length, established through data flow into r2.
61    pub length: u32,
62    /// Preserved r3 evidence; programming wrappers use it to select their mode.
63    pub fourth_argument: Value,
64}
65
66fn word(bytes: &[u8], base: u32, address: u32) -> Option<u16> {
67    let offset = usize::try_from(address.checked_sub(base)?).ok()?;
68    Some(u16::from_le_bytes(
69        bytes.get(offset..offset.checked_add(2)?)?.try_into().ok()?,
70    ))
71}
72
73/// Decode the legacy Thumb BL used by the audited MT19xx receivers.
74/// BLX and malformed/truncated instructions deliberately do not match.
75pub fn branch_link(bytes: &[u8], base: u32, address: u32) -> Option<u32> {
76    let first = word(bytes, base, address)?;
77    let second = word(bytes, base, address.checked_add(2)?)?;
78    if first & 0xf800 != 0xf000 || second & 0xf800 != 0xf800 {
79        return None;
80    }
81    let encoded = (u32::from(first & 2047) << 12) | (u32::from(second & 2047) << 1);
82    let delta = ((encoded as i32) << 9) >> 9;
83    u32::try_from(i64::from(address) + 4 + i64::from(delta)).ok()
84}
85
86/// Trace an argument block and direct trampolines, at most 32 instructions.
87/// `load` supplies established values for nonliteral memory reads. It must not
88/// infer staging pointers from model strings or the target firmware identity.
89pub fn trace_call(
90    bytes: &[u8],
91    base: u32,
92    start: u32,
93    mut registers: [Value; 8],
94    mut load: impl FnMut(Value) -> Option<Value>,
95) -> Option<Call> {
96    if base & 1 != 0 || start & 1 != 0 {
97        return None;
98    }
99    let mut address = start;
100    let mut visited = [u32::MAX; 32];
101    for step in 0..32 {
102        if visited[..step].contains(&address) {
103            return None;
104        }
105        visited[step] = address;
106        let op = word(bytes, base, address)?;
107        if let Some(target) = branch_link(bytes, base, address) {
108            let Value::Constant(length) = registers[2] else {
109                return None;
110            };
111            if length == 0 || length % 16 != 0 || registers[..2].contains(&Value::Unknown) {
112                return None;
113            }
114            return Some(Call {
115                address,
116                target,
117                source: registers[0],
118                destination: registers[1],
119                length,
120                fourth_argument: registers[3],
121            });
122        }
123        let d = usize::from(op & 7);
124        let s = usize::from((op >> 3) & 7);
125        if op & 0xf800 == 0xe000 {
126            let delta = ((i32::from(op & 0x7ff) << 21) >> 20) as i64;
127            address = u32::try_from(i64::from(address) + 4 + delta).ok()?;
128            continue;
129        } else if op & 0xf800 == 0x4800 {
130            let literal = (address.checked_add(4)? & !3).checked_add(u32::from(op & 255) * 4)?;
131            let low = u32::from(word(bytes, base, literal)?);
132            let high = u32::from(word(bytes, base, literal.checked_add(2)?)?);
133            registers[usize::from((op >> 8) & 7)] = Value::Constant(low | high << 16);
134        } else if op & 0xf800 == 0x2000 {
135            registers[usize::from((op >> 8) & 7)] = Value::Constant(u32::from(op & 255));
136        } else if op & 0xf800 == 0x0000 {
137            let shift = u32::from((op >> 6) & 31);
138            registers[d] = if shift == 0 {
139                registers[s]
140            } else if let Value::Constant(value) = registers[s] {
141                Value::Constant(u32::try_from(u64::from(value) << shift).ok()?)
142            } else {
143                return None;
144            };
145        } else if op & 0xfe00 == 0x1800 {
146            registers[d] = registers[s].add(registers[usize::from((op >> 6) & 7)])?;
147        } else if op & 0xfe00 == 0x1c00 {
148            registers[d] = registers[s].add(Value::Constant(u32::from((op >> 6) & 7)))?;
149        } else if op & 0xf800 == 0x3000 {
150            let d = usize::from((op >> 8) & 7);
151            registers[d] = registers[d].add(Value::Constant(u32::from(op & 255)))?;
152        } else if op & 0xf800 == 0x6800 {
153            let pointer = registers[s].add(Value::Constant(u32::from((op >> 6) & 31) * 4))?;
154            registers[d] = load(pointer)?;
155        } else if op & 0xf800 == 0xa800 {
156            registers[usize::from((op >> 8) & 7)] = Value::Stack(u32::from(op & 255) * 4);
157        } else {
158            return None;
159        }
160        address = address.checked_add(2)?;
161    }
162    None
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    // INTM application's actual header setup, including its PC-relative literal.
170    fn header() -> Vec<u8> {
171        let mut bytes = vec![0; 0x370];
172        bytes[..18].copy_from_slice(&[
173            0xda, 0x48, 0x03, 0x23, 0x00, 0x68, 0x1b, 0x03, 0x10, 0x22, 0xc0, 0x18, 0x05, 0xa9,
174            0xfd, 0xf7, 0xc9, 0xfd,
175        ]);
176        bytes[0x36c..0x370].copy_from_slice(&0x0200_0c34u32.to_le_bytes());
177        bytes
178    }
179
180    fn inspect(bytes: &[u8]) -> Option<Call> {
181        trace_call(bytes, 0x9d480, 0x9d480, [Value::Unknown; 8], |address| {
182            (address == Value::Constant(0x0200_0c34)).then_some(Value::Staging(0))
183        })
184    }
185
186    #[test]
187    fn actual_header_arguments_are_scratch_not_in_place() {
188        let call = inspect(&header()).unwrap();
189        assert_eq!(
190            call,
191            Call {
192                address: 0x9d48e,
193                target: 0x9b024,
194                source: Value::Staging(0x3000),
195                destination: Value::Stack(0x14),
196                length: 16,
197                fourth_argument: Value::Constant(0x3000),
198            }
199        );
200    }
201
202    #[test]
203    fn operand_changes_cannot_retain_the_same_proof() {
204        let bytes = header();
205        let original = inspect(&bytes).unwrap();
206        for at in [0, 2, 4, 6, 8, 10, 12, 14, 16, 0x36c] {
207            let mut changed = bytes.clone();
208            changed[at] ^= 1;
209            assert_ne!(inspect(&changed), Some(original), "mutation at {at:#x}");
210        }
211    }
212
213    #[test]
214    fn truncated_literals_unknown_memory_and_control_flow_fail_closed() {
215        let bytes = header();
216        assert!(inspect(&bytes[..0x36f]).is_none());
217        assert!(trace_call(&bytes, 0x9d480, 0x9d480, [Value::Unknown; 8], |_| None).is_none());
218        let mut branch = bytes;
219        branch[2..4].copy_from_slice(&0xe000u16.to_le_bytes());
220        assert!(inspect(&branch).is_none());
221        branch[..2].copy_from_slice(&0xe7feu16.to_le_bytes());
222        assert!(inspect(&branch).is_none());
223        assert!(trace_call(
224            &branch,
225            u32::MAX - 1,
226            u32::MAX - 1,
227            [Value::Unknown; 8],
228            |_| None
229        )
230        .is_none());
231    }
232}