aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/ifwi_parse_model.py
blob: 715e086f2f96f3730b72b3fcc5d3aa2ee8933459 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""Host-side model of the PSP's pre-auth IFWI parser.

    WARNING -- THIS MODEL DIVERGES FROM THE FIRMWARE. DO NOT CRAFT WITH IT.

    Differentially tested against the real parser under Unicorn
    (`psp_emu/harness/tsu_parser_diff.py`, `fuzz_ifwi_parser.py`): over 400
    mutations it disagrees with the firmware on **84**, in both directions:

        firmware FAULTS, model rejects     42   model is safer, hides the bug
        firmware ACCEPTS, model rejects    34   model is stricter
        firmware REJECTS, model accepts     8   model is too lenient -- the
                                                dangerous direction for crafting

    The stricter class comes from `fw_tail_scan`: this model raises on the first
    slot's failure, while the firmware marks that slot invalid and carries on.
    An attempted fix (make per-slot failure non-fatal) was MEASURED AND MADE
    THINGS WORSE -- agreement fell 316 -> 305 and "model accepts, firmware
    rejects" jumped 8 -> 53 -- so it was reverted rather than shipped.

    The real semantics, read from `fw_tail_scan` (0xE0F950): an error register
    accumulates across both slots and is never cleared, so a slot-0 failure
    leaves it set even when slot 1 parses cleanly -- and `rom_parse_directory`
    may still succeed on the valid slot regardless. Modelling that faithfully
    needs the whole interaction, not a one-line change.

    Until someone does that, use the firmware itself via the harnesses above.


Mirrors, step for step, what rom_parse_directory and its callees do in
sys_drv2.elf. Purpose: validate the model against a KNOWN-GOOD image before
using it to craft anything. If this cannot parse the real BIOS, the model is
wrong and nothing built on it can be trusted.

Firmware references:
  rom_parse_directory   0xE16964
  fw_tail_scan          0xE0F950
  param_list_scan       0xE0F8A8
  ring_entry_validate   0xE0FAEC
  fw_tail_record_check  0xE0FC6C
"""
import struct, sys

M32 = 0xFFFFFFFF
IFWI_MAGIC = 0x55AA55AA
PSP_MAGIC  = 0x50535024   # '$PSP'
SGN_MAGIC  = 0x4E475324   # '$SGN'

class Fail(Exception): pass

def u32(b, off):
    if off + 4 > len(b) or off < 0: raise Fail("read out of image at %#x" % off)
    return struct.unpack_from('<I', b, off)[0]

def fw_tail_scan(img, ctx, trace):
    """v4 = *(img+20) then *(img+44); bound (v4+16)&M32 <= 0x200000; expect '$PSP'."""
    for slot, hdr_off in ((0, 20), (1, 44)):
        v4 = u32(img, hdr_off)
        trace.append("  fw_tail_scan[%d]: v4 = *(img+%d) = %#x" % (slot, hdr_off, v4))
        if ((v4 + 16) & M32) > 0x200000:
            raise Fail("slot %d: bound check failed (v4+16 = %#x)" % (slot, (v4+16)&M32))
        magic = u32(img, v4)
        if magic != PSP_MAGIC:
            raise Fail("slot %d: expected $PSP at %#x, got %#010x" % (slot, v4, magic))
        count = u32(img, v4 + 8)
        if count < 2:
            raise Fail("slot %d: count %d < 2  (-> 0xFFFF000B)" % (slot, count))
        ctx[slot] = {'dir_off': v4, 'len': 16*count + 16, 'valid': 1, 'count': count}
        trace.append("    -> $PSP ok, count=%d, len=%#x" % (count, 16*count+16))
    return ctx

def param_list_scan(img, ptr, count, ctx, trace):
    """entries of 4 dwords; type 316/317; 64-bit bound; low dword of v9 must == 32."""
    seen = 0
    for i in range(count):
        base = ptr + i*16
        t = u32(img, base)
        if t not in (316, 317):
            continue
        v9 = u32(img, base+4) | (u32(img, base+12) << 32)
        size = u32(img, base+8)
        if not (size + v9 <= 0x200000 and (v9 & M32) == 32):
            raise Fail("param entry %d (type %d): bound/offset check failed (size=%#x v9=%#x)" % (i, t, size, v9))
        idx = 0 if t == 316 else 1
        # a1[2] is the OFFSET stored at ctx+40 and used by ring_entry_validate;
        # a1[1] is a fixed marker that must equal 32.
        ctx[idx].update({'p_offset': size, 'p_marker': u32(img, base+4), 'p_valid': 1})
        trace.append("  param_list_scan: type=%d -> offset=%#x marker=%d" % (t, size, u32(img, base+4)))
        seen += 1
        if seen == 2: break
    if seen != 2:
        raise Fail("param_list_scan: found %d of 2 required entries (types 316/317)" % seen)

def ring_entry_validate(img, ctx, idx, trace):
    e = ctx[idx]
    if not e.get('p_valid'): raise Fail("ring_entry_validate[%d]: slot not marked valid" % idx)
    v4 = e['p_offset']          # ctx+40 == a1[2]
    v5 = u32(img, v4 + 16)
    ln = u32(img, v4 + 24)
    if ((v5 + ln) & M32) > 0x200000:
        raise Fail("ring_entry_validate[%d]: (v5+len) bound failed" % idx)
    e.update({'r_off': v5, 'r_len': ln, 'r_valid': 1})
    trace.append("  ring_entry_validate[%d]: off=%#x len=%#x" % (idx, v5, ln))

def fw_tail_record_check(img, ctx, trace):
    """v3 = *(img+4096); bound; expect '$SGN'; walk count records of 16 bytes."""
    v3 = u32(img, 4096)
    trace.append("  fw_tail_record_check: v3 = *(img+0x1000) = %#x" % v3)
    if ((v3 + 16) & M32) > 0x200000: raise Fail("$SGN bound check failed")
    if (m := u32(img, v3)) != SGN_MAGIC:
        raise Fail("expected $SGN at %#x, got %#010x" % (v3, m))
    count = u32(img, v3 + 8)
    trace.append("    -> $SGN ok, %d records" % count)
    recs = {}
    for i in range(1, 8):
        for j in range(count):
            base = v3 + 16 + j*16
            if u32(img, base) == i:
                recs[i] = tuple(u32(img, base + k*4) for k in range(4))
                break
    ctx['records'] = recs
    trace.append("    record types present: %s" % sorted(recs))

def parse(img):
    trace = []
    ctx = {0: {}, 1: {}}
    if (m := u32(img, 0)) != IFWI_MAGIC:
        raise Fail("IFWI magic: got %#010x want %#010x" % (m, IFWI_MAGIC))
    trace.append("rom_parse_directory: IFWI magic ok")
    fw_tail_scan(img, ctx, trace)
    slot = next((s for s in (0,1) if ctx[s].get('valid')), None)
    if slot is None: raise Fail("no valid directory slot")
    d = ctx[slot]['dir_off']
    param_list_scan(img, d + 16, u32(img, d + 8), ctx, trace)
    ring_entry_validate(img, ctx, 0, trace)
    ring_entry_validate(img, ctx, 1, trace)
    fw_tail_record_check(img, ctx, trace)
    return ctx, trace

if __name__ == '__main__':
    for path in sys.argv[1:]:
        img = open(path, 'rb').read()
        print("=== %s (%d bytes) ===" % (path, len(img)))
        try:
            ctx, trace = parse(img)
            for t in trace: print(t)
            print("  RESULT: PARSE SUCCEEDED -> would reach tsu_write_active_slot")
        except Fail as e:
            print("  RESULT: PARSE FAILED -> %s" % e)
            print("          (returns before the writer; flash untouched)")
        print()