Skip to main content

mediatek_optical/receiver/
programming.rs

1//! Operand-checked evidence for application update range selection.
2//!
3//! Selection is not a write proof: the caller must bind the total-length source
4//! and establish that the programming call consumes the selected cells. In
5//! particular, matching a header does not imply that boot is preserved.
6
7mod acceptance;
8pub use acceptance::{inspect_acceptance_tail, AcceptanceTail};
9
10mod reserved;
11pub use reserved::{inspect_reserved_skips, ReservedSkips};
12
13/// Both branches of the legacy application's update-bit-zero range selection.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Selection {
16    /// First instruction of the checked range-selection block.
17    pub address: u32,
18    /// First instruction after the complete selection block.
19    pub end: u32,
20    /// RAM cell receiving the selected upload offset.
21    pub offset_cell: u32,
22    /// RAM cell receiving the selected programming start.
23    pub start_cell: u32,
24    /// RAM cell receiving total length minus selected start.
25    pub length_cell: u32,
26    /// Register holding a pointer to the upload's total-length cell.
27    pub total_pointer_register: u8,
28}
29
30/// Calls and branches selecting optional boot CMAC and mandatory application CMAC.
31/// The referenced checker routines still require independent validation.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct IntegritySelection {
34    /// Address of the update-state load.
35    pub address: u32,
36    /// Called only when update flag bit zero is set.
37    pub boot_check: u32,
38    /// Called with mask one unless the boot check has already failed.
39    pub application_check: u32,
40    /// Continuation reached only when the selected checks returned nonzero.
41    pub success: u32,
42    /// Error path reached when a selected checker returns zero.
43    pub failure: u32,
44}
45
46/// A direct staged-byte signature comparison following range selection.
47/// Every instruction between selection and comparison is checked: there is no
48/// intervening decoder call on this path. Earlier stream effects remain a
49/// separate obligation.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct PlainSignature {
52    /// RAM cell containing the upload staging pointer.
53    pub staging_cell: u32,
54    /// Image offset compared with the installed signature.
55    pub offset: u32,
56    /// Number of bytes compared.
57    pub length: u32,
58    /// Continuation after all bytes match.
59    pub success: u32,
60    /// Continuation when a byte differs.
61    pub mismatch: u32,
62    /// Continuation when the selected range does not cover the guard region.
63    pub outside_range: u32,
64}
65
66/// Bind the legacy direct signature loop to an independently matched selection.
67/// This establishes a plaintext constraint, not acceptance of the whole image.
68pub fn inspect_plain_signature(
69    bytes: &[u8],
70    base: u32,
71    selection: &Selection,
72) -> Option<PlainSignature> {
73    if inspect_selection(bytes, base, selection.address)? != *selection {
74        return None;
75    }
76    let start = selection.end;
77    let at = |index: u32| start.checked_add(index.checked_mul(2)?);
78    let op = |index| word(bytes, base, at(index)?);
79    for (index, expected) in [
80        (0, 0x2201),
81        (1, 0x0692),
82        (2, 0x6810),
83        (3, 0x2110),
84        (4, 0x4388),
85        (5, 0x6010),
86        (7, 0x6820),
87        (8, 0x2283),
88        (9, 0x0252),
89        (10, 0x4290),
90        (12, 0x6820),
91        (14, 0x1840),
92        (16, 0x4288),
93        (19, 0x2100),
94        (20, 0x6800),
95        (21, 0x0213),
96        (23, 0x0a1b),
97        (24, 0x181b),
98        (25, 0x781b),
99        (29, 0x1c49),
100        (30, 0x0609),
101        (31, 0x0e09),
102        (32, 0x1c52),
103        (33, 0x2908),
104        (34, 0xd3f1),
105    ] {
106        if op(index)? != expected {
107            return None;
108        }
109    }
110    if literal(bytes, base, at(6)?, 4)? != selection.offset_cell
111        || literal(bytes, base, at(15)?, 1)? != 0x10700
112    {
113        return None;
114    }
115    // The length pointer survives selection in the destination register of its
116    // last literal load; bind the use here to that exact register.
117    let last_load = word(bytes, base, selection.end.checked_sub(4)?)?;
118    let length_register = (last_load >> 8) & 7;
119    if op(13)? != 0x6801 | length_register << 3 {
120        return None;
121    }
122    let signature_register = (op(22)? >> 8) & 7;
123    if signature_register < 4
124        || literal(bytes, base, at(22)?, signature_register)? != 0x10600
125        || op(26)? != 0x5c40 | signature_register << 3 | signature_register
126        || op(27)? != 0x4283 | signature_register << 3
127    {
128        return None;
129    }
130    let branch = |index, condition| {
131        let instruction = op(index)?;
132        if instruction & 0xff00 != condition {
133            return None;
134        }
135        let target =
136            u32::try_from(i64::from(at(index)?) + 4 + i64::from(instruction as u8 as i8) * 2)
137                .ok()?;
138        word(bytes, base, target)?;
139        Some(target)
140    };
141    let outside_range = branch(11, 0xd800)?;
142    if branch(17, 0xd900)? != outside_range {
143        return None;
144    }
145    let mismatch = branch(28, 0xd100)?;
146    let success = at(35)?;
147    if outside_range < success || mismatch < success {
148        return None;
149    }
150    let staging_cell = literal(bytes, base, at(18)?, 0)?;
151    if !ram(staging_cell)
152        || [
153            selection.offset_cell,
154            selection.start_cell,
155            selection.length_cell,
156        ]
157        .contains(&staging_cell)
158    {
159        return None;
160    }
161    Some(PlainSignature {
162        staging_cell,
163        offset: 0x10600,
164        length: 8,
165        success,
166        mismatch,
167        outside_range,
168    })
169}
170
171fn word(bytes: &[u8], base: u32, address: u32) -> Option<u16> {
172    let offset = usize::try_from(address.checked_sub(base)?).ok()?;
173    Some(u16::from_le_bytes(
174        bytes.get(offset..offset.checked_add(2)?)?.try_into().ok()?,
175    ))
176}
177
178fn literal(bytes: &[u8], base: u32, address: u32, register: u16) -> Option<u32> {
179    let op = word(bytes, base, address)?;
180    if op & 0xff00 != 0x4800 | register << 8 {
181        return None;
182    }
183    let at = (address.checked_add(4)? & !3).checked_add(u32::from(op & 255) * 4)?;
184    Some(
185        u32::from(word(bytes, base, at)?) | u32::from(word(bytes, base, at.checked_add(2)?)?) << 16,
186    )
187}
188
189fn ram(address: u32) -> bool {
190    address & 3 == 0 && (0x02000000..0x02010000).contains(&address)
191}
192
193/// Inspect either established compiler form, rejecting ambiguous matches.
194pub fn inspect_selection(bytes: &[u8], base: u32, start: u32) -> Option<Selection> {
195    match (
196        inspect_legacy_selection(bytes, base, start),
197        inspect_shared_selection(bytes, base, start),
198    ) {
199        (Some(proof), None) | (None, Some(proof)) => Some(proof),
200        _ => None,
201    }
202}
203
204fn inspect_shared_selection(bytes: &[u8], base: u32, start: u32) -> Option<Selection> {
205    if start & 1 != 0 || base & 1 != 0 {
206        return None;
207    }
208    // The compiler may insert a literal pool between the two arms. Follow
209    // their explicit branch targets instead of assuming adjacent blocks.
210    let branch_target = |address: u32, conditional: bool| {
211        let instruction = word(bytes, base, address)?;
212        let delta = if conditional {
213            if instruction & 0xff00 != 0xd000 {
214                return None;
215            }
216            i64::from(instruction as u8 as i8) * 2
217        } else {
218            if instruction & 0xf800 != 0xe000 {
219                return None;
220            }
221            i64::from((i32::from(instruction & 0x7ff) << 21) >> 20)
222        };
223        u32::try_from(i64::from(address) + 4 + delta).ok()
224    };
225    let alternate = branch_target(start.checked_add(8)?, true)?;
226    let join = branch_target(start.checked_add(20)?, false)?;
227    if alternate < start.checked_add(22)? || join != alternate.checked_add(14)? {
228        return None;
229    }
230    let at = |index: u32| {
231        if index < 11 {
232            start.checked_add(index.checked_mul(2)?)
233        } else {
234            alternate.checked_add(index.checked_sub(11)?.checked_mul(2)?)
235        }
236    };
237    let op = |index| word(bytes, base, at(index)?);
238    if literal(bytes, base, start, 0)? != 0x04002040 {
239        return None;
240    }
241    for (index, expected) in [
242        (1, 0x6980),
243        (2, 0x07c1),
244        (6, 0x2100),
245        (7, 0x6011),
246        (8, 0x6001),
247        (12, 0x2101),
248        (13, 0x0409),
249        (14, 0x6011),
250        (15, 0x6001),
251        (17, 0x1a51),
252    ] {
253        if op(index)? != expected {
254            return None;
255        }
256    }
257    let start_cell = literal(bytes, base, at(3)?, 0)?;
258    let offset_cell = literal(bytes, base, at(5)?, 2)?;
259    if literal(bytes, base, at(11)?, 2)? != offset_cell {
260        return None;
261    }
262    let total = op(9)?;
263    if total & 0xffc7 != 0x6801 || op(16)? != (total & !7) | 2 {
264        return None;
265    }
266    let pointer = ((total >> 3) & 7) as u8;
267    if pointer < 3 {
268        return None;
269    }
270    let length_register = (op(18)? >> 8) & 7;
271    let length_cell = literal(bytes, base, at(18)?, length_register)?;
272    if length_register == 1 || op(19)? != 0x6001 | length_register << 3 {
273        return None;
274    }
275    if ![offset_cell, start_cell, length_cell].into_iter().all(ram)
276        || offset_cell == start_cell
277        || offset_cell == length_cell
278        || start_cell == length_cell
279    {
280        return None;
281    }
282    Some(Selection {
283        address: start,
284        end: at(20)?,
285        offset_cell,
286        start_cell,
287        length_cell,
288        total_pointer_register: pointer,
289    })
290}
291
292/// Establish the legacy application's complete boot/application CMAC choice.
293/// Clearing update bit zero skips the boot checker, never the application one.
294pub fn inspect_legacy_integrity(bytes: &[u8], base: u32, start: u32) -> Option<IntegritySelection> {
295    use super::decoding::branch_link;
296    if base & 1 != 0 || start & 1 != 0 || literal(bytes, base, start, 0)? != 0x04002040 {
297        return None;
298    }
299    let at = |delta: u32| start.checked_add(delta);
300    for (offset, expected) in [
301        (2, 0x6980),
302        (4, 0x07c0),
303        (6, 0xd003),
304        (12, 0x2801),
305        (14, 0xd102),
306        (16, 0x2001),
307        (22, 0x2800),
308    ] {
309        if word(bytes, base, at(offset)?)? != expected {
310            return None;
311        }
312    }
313    let branch = word(bytes, base, at(24)?)?;
314    if branch & 0xff00 != 0xd100 {
315        return None;
316    }
317    let success = u32::try_from(i64::from(at(24)?) + 4 + i64::from(branch as u8 as i8) * 2).ok()?;
318    let failure = at(26)?;
319    word(bytes, base, success)?;
320    word(bytes, base, failure)?;
321    Some(IntegritySelection {
322        address: start,
323        boot_check: branch_link(bytes, base, at(8)?)?,
324        application_check: branch_link(bytes, base, at(18)?)?,
325        success,
326        failure,
327    })
328}
329
330/// Match all instructions, both branches, literals and the branch join.
331/// This compiler form selects start zero when MMIO bit zero is set, otherwise
332/// `0x10000`; it always stores length `total - start`. Other forms return None.
333pub fn inspect_legacy_selection(bytes: &[u8], base: u32, start: u32) -> Option<Selection> {
334    if start & 1 != 0 || base & 1 != 0 {
335        return None;
336    }
337    let at = |index: u32| start.checked_add(index.checked_mul(2)?);
338    let op = |index| word(bytes, base, at(index)?);
339    if literal(bytes, base, start, 0)? != 0x04002040 {
340        return None;
341    }
342    // Exact branch displacements bind both arms and their join, not just opcodes.
343    for (index, expected) in [
344        (1, 0x6980),
345        (2, 0x07c0),
346        (3, 0xd006),
347        (5, 0x2000),
348        (6, 0x6008),
349        (8, 0x6008),
350        (10, 0xe007),
351        (12, 0x2101),
352        (13, 0x0409),
353        (14, 0x6001),
354        (16, 0x6001),
355        (18, 0x1a40),
356    ] {
357        if op(index)? != expected {
358            return None;
359        }
360    }
361    let offset_cell = literal(bytes, base, at(4)?, 1)?;
362    let start_cell = literal(bytes, base, at(7)?, 1)?;
363    if literal(bytes, base, at(11)?, 0)? != offset_cell
364        || literal(bytes, base, at(15)?, 0)? != start_cell
365    {
366        return None;
367    }
368    let total = op(9)?;
369    if total & 0xffc7 != 0x6800 || op(17)? != total {
370        return None;
371    }
372    let pointer = ((total >> 3) & 7) as u8;
373    // Both r0 and r1 have been overwritten in these arms.
374    if pointer < 2 {
375        return None;
376    }
377    let load = op(19)?;
378    let length_register = (load >> 8) & 7;
379    let length_cell = literal(bytes, base, at(19)?, length_register)?;
380    if length_register == 0 || op(20)? != 0x6000 | length_register << 3 {
381        return None;
382    }
383    if ![offset_cell, start_cell, length_cell].into_iter().all(ram)
384        || offset_cell == start_cell
385        || offset_cell == length_cell
386        || start_cell == length_cell
387    {
388        return None;
389    }
390    Some(Selection {
391        address: start,
392        end: at(21)?,
393        offset_cell,
394        start_cell,
395        length_cell,
396        total_pointer_register: pointer,
397    })
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    fn fixture() -> Vec<u8> {
405        let words = [
406            0x48b5u16, 0x6980, 0x07c0, 0xd006, 0x49a3, 0x2000, 0x6008, 0x49b6, 0x6008, 0x6820,
407            0xe007, 0x489f, 0x2101, 0x0409, 0x6001, 0x48b2, 0x6001, 0x6820, 0x1a40, 0x4ea1, 0x6030,
408        ];
409        let mut bytes = vec![0; 0x300];
410        for (i, op) in words.into_iter().enumerate() {
411            bytes[2 * i..2 * i + 2].copy_from_slice(&op.to_le_bytes());
412        }
413        for (address, value) in [
414            (0x9d7f8u32, 0x04002040u32),
415            (0x9d7b8, 0x02000c60),
416            (0x9d80c, 0x02000c68),
417            (0x9d7d0, 0x02000c6c),
418        ] {
419            let at = (address - 0x9d522) as usize;
420            bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
421        }
422        bytes
423    }
424
425    // Relocated operand-equivalent shared-load form, with an optional literal
426    // island between the branches (as in UH12NS40 1.03).
427    fn shared_fixture(island: usize) -> Vec<u8> {
428        let mut bytes = vec![0; 0x300];
429        let alternate = 22 + island;
430        let join = alternate + 14;
431        let mut put = |at: usize, value: u16| {
432            bytes[at..at + 2].copy_from_slice(&value.to_le_bytes());
433        };
434        for (at, op) in [
435            (2, 0x6980),
436            (4, 0x07c1),
437            (12, 0x2100),
438            (14, 0x6011),
439            (16, 0x6001),
440            (18, 0x6821),
441        ] {
442            put(at, op);
443        }
444        put(8, 0xd000 | ((alternate - 12) / 2) as u16);
445        put(20, 0xe000 | ((join - 24) / 2) as u16);
446        for (offset, op) in [
447            (2, 0x2101),
448            (4, 0x0409),
449            (6, 0x6011),
450            (8, 0x6001),
451            (10, 0x6822),
452            (12, 0x1a51),
453            (16, 0x6031),
454        ] {
455            put(alternate + offset, op);
456        }
457        for (at, reg, literal_at, value) in [
458            (0, 0, 0x200, 0x04002040u32),
459            (6, 0, 0x204, 0x02000c68),
460            (10, 2, 0x208, 0x02000c60),
461            (alternate, 2, 0x208, 0x02000c60),
462            (join, 6, 0x20c, 0x02000c6c),
463        ] {
464            let op = 0x4800u16 | reg << 8 | ((literal_at - ((at + 4) & !3)) / 4) as u16;
465            bytes[at..at + 2].copy_from_slice(&op.to_le_bytes());
466            bytes[literal_at..literal_at + 4].copy_from_slice(&value.to_le_bytes());
467        }
468        bytes
469    }
470
471    #[test]
472    fn shared_selection_follows_branches_over_literal_islands() {
473        for island in [0, 0x76] {
474            let bytes = shared_fixture(island);
475            let proof = inspect_selection(&bytes, 0, 0).unwrap();
476            assert_eq!(proof.end, (40 + island) as u32);
477            assert_eq!(proof.start_cell, 0x02000c68);
478            assert_eq!(proof.total_pointer_register, 4);
479            // Both branch destinations and both arms must remain bound.
480            for at in [8, 20, 22 + island + 4, 22 + island + 12, 22 + island + 16] {
481                let mut changed = bytes.clone();
482                changed[at] ^= 1;
483                assert!(inspect_selection(&changed, 0, 0).is_none(), "at {at:#x}");
484            }
485            // Literal data is not executable fallthrough.
486            if island != 0 {
487                let mut changed = bytes.clone();
488                changed[22..22 + island].fill(0xff);
489                assert_eq!(inspect_selection(&changed, 0, 0), Some(proof));
490            }
491        }
492    }
493
494    #[test]
495    fn intm_selection_binds_both_arms_and_distinct_cells() {
496        let evidence = inspect_legacy_selection(&fixture(), 0x9d522, 0x9d522).unwrap();
497        assert_eq!(evidence.offset_cell, 0x02000c60);
498        assert_eq!(evidence.start_cell, 0x02000c68);
499        assert_eq!(evidence.length_cell, 0x02000c6c);
500        assert_eq!(evidence.total_pointer_register, 4);
501        assert_eq!(evidence.end, 0x9d54c);
502    }
503
504    #[test]
505    fn changed_guards_stores_subtraction_and_joins_are_rejected() {
506        let bytes = fixture();
507        for index in [1, 2, 3, 5, 6, 8, 10, 12, 13, 14, 16, 17, 18, 20] {
508            let mut changed = bytes.clone();
509            changed[index * 2] ^= 1;
510            assert!(
511                inspect_legacy_selection(&changed, 0x9d522, 0x9d522).is_none(),
512                "instruction {index}"
513            );
514        }
515        assert!(inspect_legacy_selection(&bytes[..42], 0x9d522, 0x9d522).is_none());
516        assert!(inspect_legacy_selection(&bytes, u32::MAX - 1, u32::MAX - 1).is_none());
517    }
518}