aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/toctou_replicate.py
blob: fe424737b1ddeeb8ecb3455da6f8a4689347653e (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
"""Replicate the off=0x118 / delay=65535 hit with a PAIRED CONTROL.

The sweep flagged: LOAD_TA succeeded (verification passed) but the TA then failed
to answer. Offset 0x118 is image_header+0x18 -- the requested memory footprint,
parsed by bl_load_ta_image AFTER the integrity check -- so this is exactly where
"verify hashed A, loader consumed B" would show up.

BUT the sweep was running at ~92% exception rate, so a lone TIMEOUT may simply be
background noise. This script settles it by interleaving two arms under identical
conditions and comparing TIMEOUT rates:

    arm FLIP     : one-shot swap armed at (off, delay)   <- byte can change
    arm CONTROL  : identical sequence, but A and B are BOTH the genuine byte
                   (one-shot still fires, same timing, same code path -- only the
                    written value is harmless)

If the effect is real, FLIP shows loaded-but-dead TAs and CONTROL does not.
If both arms time out at similar rates, it is background instability, not TOCTOU.

Paired/interleaved so drift in device health hits both arms equally.
"""
import sys, time, json
from psp_out.toctou_sweep import Sweep

OFF   = int(sys.argv[1], 0) if len(sys.argv) > 1 else 0x118
DELAY = int(sys.argv[2], 0) if len(sys.argv) > 2 else 65535
N     = int(sys.argv[3]) if len(sys.argv) > 3 else 30


def one(s, off, delay, corrupt):
  """One attempt. corrupt=False -> both A and B are genuine (control)."""
  g = s.hdcp[off]
  b = (g ^ 0xFF) if corrupt else g
  s.unload_all(); s.stage()
  s.set_ab(off, g, b)
  s.oneshot(off, delay)
  st, sid, shm, exc = s.load()
  if st == 0 and sid:
    r = s.invoke(sid, shm)
    s.unload_all()
    if r[0] == 'TIMEOUT':          return 'LOADED_DEAD'
    return 'LOADED_OK' if (r[0], r[1][:16] if r[1] else None) == s.baseline else 'LOADED_DIFF'
  s.unload_all()
  return 'REJECT' if not exc else 'ERR'


if __name__ == '__main__':
  s = Sweep()
  s.baseline = s.get_baseline(verbose=True)
  if s.baseline is None:
    s.recover(); s.baseline = s.get_baseline(verbose=True)
  assert s.baseline, "no baseline"
  print("replicating off=%#x delay=%d, %d paired trials\n" % (OFF, DELAY, N), flush=True)

  tally = {'FLIP': {}, 'CTRL': {}}
  for i in range(N):
    for arm, corrupt in (('FLIP', True), ('CTRL', False)):
      try:
        r = one(s, OFF, DELAY, corrupt)
      except Exception as e:
        r = 'EXC:' + type(e).__name__
        s.recover(); s.baseline = s.baseline or s.get_baseline()
      tally[arm][r] = tally[arm].get(r, 0) + 1
    if (i + 1) % 5 == 0:
      print("  %2d/%d  FLIP=%s  CTRL=%s" % (i + 1, N, tally['FLIP'], tally['CTRL']), flush=True)

  print("\nFINAL")
  for arm in ('FLIP', 'CTRL'):
    t = tally[arm]; tot = sum(t.values()) or 1
    dead = t.get('LOADED_DEAD', 0) + t.get('LOADED_DIFF', 0)
    print("  %-5s %s   loaded-but-wrong: %d/%d (%.0f%%)" % (arm, t, dead, tot, 100.0 * dead / tot))
  f = tally['FLIP'].get('LOADED_DEAD', 0) + tally['FLIP'].get('LOADED_DIFF', 0)
  c = tally['CTRL'].get('LOADED_DEAD', 0) + tally['CTRL'].get('LOADED_DIFF', 0)
  print("\nverdict:", "SIGNAL (flip >> control) -- investigate as real TOCTOU" if f > c + 2 else
        "NO SIGNAL -- consistent with background instability, not a verify/load split")
  json.dump(tally, open('/tmp/toctou_replicate.json', 'w'), indent=1)