summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/tests/test_injection.py
blob: 33cc092539be85881acdf2dc1b9bf97278e29920 (plain)
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""Integration tests for Frida-like remote calls, run-until and service injection.

Skipped cleanly when QEMU, the demo firmware, clang/lld or the SDK are absent.
The native simulator is a first-class injection target too: ``NativeInjectionTests``
proves the demo service blob loads and that direct and firmware-mailbox
invocations agree without QEMU.
"""
from __future__ import annotations

import io
import contextlib
import os
import shutil
import subprocess
import unittest

import greeneye as ge
from greeneye.errors import RspTimeout

HERE = os.path.dirname(os.path.abspath(__file__))
TOOL_ROOT = os.path.join(HERE, "..")
DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf")
SERVICE_ELF = os.path.join(TOOL_ROOT, "sdk", "build", "injected_demo.elf")


def qemu_available() -> bool:
    return bool(os.environ.get("GREENEYE_QEMU") or shutil.which("qemu-system-riscv64"))


def native_available() -> bool:
    try:
        import greeneye.sim_backend  # noqa: F401

        return True
    except Exception:
        return False


def toolchain_available() -> bool:
    return bool(shutil.which("clang")) and (
        shutil.which("ld.lld") or os.path.exists("/usr/lib/llvm-18/bin/ld.lld")
    )


def build_sdk() -> bool:
    if os.path.exists(SERVICE_ELF):
        return True
    result = subprocess.run(["make", "-C", os.path.join(TOOL_ROOT, "sdk")], capture_output=True, text=True)
    return result.returncode == 0 and os.path.exists(SERVICE_ELF)


@unittest.skipUnless(qemu_available(), "qemu-system-riscv64 not installed")
@unittest.skipUnless(os.path.exists(DEMO_ELF), "demo firmware not built (make demo)")
class CallTests(unittest.TestCase):
    def test_remote_call(self):
        session = ge.launch(kernel=DEMO_ELF, name="unittest-call")
        try:
            result = session.call("rpc_ping", (0x1111,))
            self.assertEqual(result, 0x1111 ^ 0xBEEF)
            # registers must be restored around the call
            self.assertEqual(session.pc, 0x1000)
        finally:
            session.close()

    def test_run_until(self):
        session = ge.launch(kernel=DEMO_ELF, name="unittest-rununtil")
        try:
            hit = session.run_until("rpc_sum", timeout=5.0)
            self.assertEqual(hit.addr, session.symbols.lookup("rpc_sum"))
            self.assertEqual(hit.kind, "breakpoint")
        finally:
            session.close()

    def test_run_until_timeout(self):
        session = ge.launch(kernel=DEMO_ELF, name="unittest-rununtil-timeout")
        try:
            with self.assertRaises(RspTimeout):
                session.run_until("rpc_fault", timeout=0.2)  # self-test never calls it
        finally:
            session.close()


@unittest.skipUnless(qemu_available(), "qemu-system-riscv64 not installed")
@unittest.skipUnless(os.path.exists(DEMO_ELF), "demo firmware not built (make demo)")
class ReplTests(unittest.TestCase):
    def test_repl_commands(self):
        from greeneye.cli import GreenEyeRepl
        from greeneye.plugins import PluginManager

        session = ge.launch(kernel=DEMO_ELF, record=True, name="unittest-repl")
        output = io.StringIO()
        try:
            ctrl = session.symbols.lookup("gsp_ctrl")
            repl = GreenEyeRepl(session, PluginManager(session))
            with contextlib.redirect_stdout(output):
                repl.onecmd("bp rpc_sum")
                repl.onecmd("watch gsp_ctrl+0x10 8 w")
                repl.onecmd(f"x {ctrl:#x} 16")
                repl.onecmd("sym rpc_ping")
                repl.onecmd("c 0.6")
                repl.onecmd("events 5")
            text = output.getvalue()
            self.assertIn("rpc_sum", text)
            self.assertIn(f"{ctrl:08x}", text)
            self.assertIn(f"watch w@{ctrl + 0x10:#x}", text)
        finally:
            session.close()


@unittest.skipUnless(qemu_available(), "qemu-system-riscv64 not installed")
@unittest.skipUnless(os.path.exists(DEMO_ELF), "demo firmware not built")
@unittest.skipUnless(toolchain_available(), "clang/lld not available")
class InjectionTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        result = subprocess.run(
            ["make", "-C", os.path.join(TOOL_ROOT, "sdk")],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0 or not os.path.exists(SERVICE_ELF):
            raise unittest.SkipTest(f"SDK build failed: {result.stderr[-200:]}")

    def test_direct_and_firmware_invocation(self):
        from greeneye.inject import invoke_remote, invoke_via_firmware, load_service

        session = ge.launch(kernel=DEMO_ELF, record=True, name="unittest-inject")
        try:
            service = load_service(session, SERVICE_ELF, slot=6)
            self.assertEqual(invoke_remote(session, service, 1, 0x41), 0x41 ^ 0xBEEF)
            self.assertEqual(invoke_remote(session, service, 2, 0x10, 0x20), 0x30)
            session.run(duration=0.3)
            self.assertEqual(invoke_via_firmware(session, service, 1, 0x41), 0x41 ^ 0xBEEF)
            self.assertEqual(invoke_via_firmware(session, service, 2, 0x10, 0x20), 0x30)
        finally:
            session.close()


@unittest.skipUnless(native_available(), "native backend unavailable")
@unittest.skipUnless(os.path.exists(DEMO_ELF), "demo firmware not built (make demo)")
@unittest.skipUnless(toolchain_available(), "clang/lld not available")
class NativeInjectionTests(unittest.TestCase):
    """Injection SDK against the native simulator (no QEMU required)."""

    @classmethod
    def setUpClass(cls):
        if not build_sdk():
            raise unittest.SkipTest("SDK build failed")

    def test_native_direct_and_firmware_invocation(self):
        from greeneye.inject import invoke_remote, invoke_via_firmware, load_service, services

        session = ge.launch(kernel=DEMO_ELF, backend="native", record=True, name="unittest-native-inject")
        try:
            service = load_service(session, SERVICE_ELF, slot=6)
            self.assertEqual(services(session), [service])
            self.assertEqual(invoke_remote(session, service, 1, 0x41), 0x41 ^ 0xBEEF)
            self.assertEqual(invoke_remote(session, service, 2, 0x10, 0x20), 0x30)
            session.run(duration=0.3)
            self.assertEqual(invoke_via_firmware(session, service, 1, 0x41), 0x41 ^ 0xBEEF)
            self.assertEqual(invoke_via_firmware(session, service, 2, 0x10, 0x20), 0x30)
        finally:
            session.close()

    def test_native_injection_replay_is_deterministic(self):
        from greeneye.inject import invoke_remote, load_service

        results = []
        for _ in range(2):
            session = ge.launch(kernel=DEMO_ELF, backend="native", record=False, name="unittest-native-inject-replay")
            try:
                service = load_service(session, SERVICE_ELF, slot=6)
                results.append(invoke_remote(session, service, 2, 0x10, 0x20))
                results.append(session.pc)
            finally:
                session.close()
        self.assertEqual(results[0], 0x30)
        self.assertEqual(results, results[:2] + results[:2])


if __name__ == "__main__":
    unittest.main()