summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/libos_port.py
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim/libos_port.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/libos_port.py511
1 files changed, 509 insertions, 2 deletions
diff --git a/debug/risc-v-gsp-rm-vm/sim/libos_port.py b/debug/risc-v-gsp-rm-vm/sim/libos_port.py
index eaa3f95..b1f596d 100644
--- a/debug/risc-v-gsp-rm-vm/sim/libos_port.py
+++ b/debug/risc-v-gsp-rm-vm/sim/libos_port.py
@@ -73,13 +73,33 @@ from __future__ import annotations
from sim.libos import (
INTERRUPT_ARM_EXTERNAL,
+ MEMORY_QUERY,
NAMES,
+ SHUTTLE_RESET,
TASK_PORT_SEND_RECV_WAIT,
+ TASK_STATE_QUERY,
)
#: Report schema tag for :meth:`LibosPortModel.state`.
REPORT_FORMAT = "greeneye-msgq-port/1"
+#: Report schema tag for :meth:`LibosPortNetwork.state`.
+NETWORK_REPORT_FORMAT = "greeneye-msgq-port-network/1"
+
+#: The 8-byte "SPURIOUS" sentinel the firmware compares a queried task's
+#: ``xepc`` against (the little-endian bytes of the literal, observed in the
+#: image rodata and compared as a u64). A modelled external-interrupt report
+#: carries it when no task trapped.
+SPURIOUS_XEPC = 0x53554F4952555053
+
+#: Modelled external-interrupt cause (interrupt bit | machine external
+#: interrupt code) for the doorbell wake's report state.
+EXTERNAL_INTERRUPT_XCAUSE = (1 << 63) | 11
+
+#: ``LibosTaskState`` size (32 registers + xepc + xcause + xbadaddr +
+#: nvriscv), the payload of ``TASK_STATE_QUERY``.
+TASK_STATE_BYTES = 288
+
#: ``LIBOS_SHUTTLE_ANY`` (RTOS port API).
SHUTTLE_ANY = 0xFFFF
#: The task's default shuttle id (RTOS port API).
@@ -263,6 +283,10 @@ class LibosPortModel:
send = a0 != 0 or a1 != 0
recv = a4 != 0 or a5 != 0
+ # A receive with a non-zero wait id is a *synchronous* receive (or the
+ # receive half of a sendrecv): the kernel blocks until a message is
+ # delivered. Only a wait id of 0 is an asynchronous post.
+ sync_recv = recv and t1 != 0
if recv:
self._post(shuttle=a4, port=a5, payload=a6, capacity=a7, pc=pc,
@@ -270,11 +294,54 @@ class LibosPortModel:
if send:
self._send(shuttle=a0, port=a1, payload=a2, size=a3, wait_id=t1,
timeout=t2, pc=pc, machine=machine)
- return True, LIBOS_OK
- if recv:
+ if not recv:
+ return True, LIBOS_OK
+ if recv and not sync_recv:
# Async receive post: the kernel returns LIBOS_OK without touching
# the wait outputs (a4-a6 still hold the receive arguments).
return True, LIBOS_OK
+ if sync_recv:
+ if not self.completed:
+ self._try_kick()
+ if self.completed:
+ entry = self.completed.pop(0)
+ machine.x[14] = entry["payload_size"] & 0xFFFFFFFFFFFFFFFF
+ machine.x[15] = entry["origin_task"] & 0xFFFFFFFFFFFFFFFF
+ machine.x[16] = entry["shuttle"] & 0xFFFFFFFFFFFFFFFF
+ machine.x[17] = 0
+ record = {
+ "pc": int(pc),
+ "wait_id": t1,
+ "timeout": t2,
+ "completed": entry["shuttle"],
+ "payload_size": entry["payload_size"],
+ "origin_task": entry["origin_task"],
+ "role": entry["role"],
+ "contract": entry["contract"],
+ }
+ self.waits.append(record)
+ self._event("sync_recv_complete", **record)
+ return True, LIBOS_OK
+ record = {
+ "pc": int(pc),
+ "wait_id": t1,
+ "timeout": t2,
+ "reason": "sync-recv-blocks",
+ "port": int(a5),
+ "shuttle": int(a4),
+ "pending": sorted(self.pending),
+ }
+ self.waits.append(record)
+ self._event("sync_recv_block", **record)
+ if self.stopped is None:
+ self.stopped = {
+ "num": TASK_PORT_SEND_RECV_WAIT,
+ "name": "task_port_send_recv_wait",
+ "pc": int(pc),
+ "reason": "sync-recv-blocks",
+ "pending": sorted(self.pending),
+ }
+ return False, None
# Pure blocking wait: return the first completed shuttle, or stop.
if not self.completed:
@@ -384,3 +451,443 @@ class LibosPortModel:
"events": [dict(event) for event in self.events],
"stopped": dict(self.stopped) if self.stopped else None,
}
+
+
+def task_state_bytes(*, xcause: int, xepc: int, xbadaddr: int = 0,
+ registers=None, priv_err_info: int = 0) -> bytes:
+ """Serialise a ``LibosTaskState`` payload (pure; no machine).
+
+ Layout: 32 u64 registers, then ``xepc``, ``xcause``, ``xbadaddr`` and the
+ NVRISCV error word (``priv_err_info`` u32 + reserved u32).
+ """
+ buf = bytearray(TASK_STATE_BYTES)
+ for index, value in enumerate(registers or ()):
+ if 0 <= index < 32:
+ buf[index * 8:index * 8 + 8] = (int(value) & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "little")
+ buf[256:264] = (int(xepc) & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "little")
+ buf[264:272] = (int(xcause) & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "little")
+ buf[272:280] = (int(xbadaddr) & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "little")
+ buf[280:284] = (int(priv_err_info) & 0xFFFFFFFF).to_bytes(4, "little")
+ return bytes(buf)
+
+
+class LibosPortNetwork(LibosPortModel):
+ """Two-task libos port network (additive, opt-in).
+
+ The real 570.144 GSP image links **two** tasks (an init daemon and the RM
+ task); the single-hart linear model only ever runs one of them. This
+ subclass models the libos port machinery between the modelled tasks:
+
+ * receives/posts/completions are kept per task (shuttle ids are
+ task-local handles and collide across tasks);
+ * a send is routed to a receive waiting on the same port id in another
+ task, copying the firmware's own payload bytes (never an invented
+ payload); the receiver's completion records the sender's task index and
+ pairs a reply back to the sender's receive half (sendrecv);
+ * ``TASK_STATE_QUERY`` serves a modelled trap state for the external
+ interrupt report (the queried state's ``xcause`` is the interrupt cause
+ and ``xepc`` the "SPURIOUS" sentinel the firmware checks; both are
+ labelled model values, not firmware observations);
+ * ``MEMORY_QUERY`` and ``SHUTTLE_RESET`` are acknowledged from the flat
+ RAM model (labelled);
+ * ``INTERRUPT_ARM_EXTERNAL`` records the second argument (the port the
+ report is bound to) in addition to the mask.
+
+ The single-task :class:`LibosPortModel` behaviour and report schema are
+ unchanged; this class is used only when the runner schedules a second
+ task.
+ """
+
+ name = "libos-port-network"
+
+ def __init__(self, base=None, *, tasks=("init", "rm"), current=None):
+ task = current or tasks[0]
+ super().__init__(base=base, task=task)
+ self.tasks = tuple(tasks)
+ self.current = task
+ self.task_completions = {name: [] for name in self.tasks}
+ self.task_recvs = {name: {} for name in self.tasks}
+ self.send_queue = {port: [] for port in ()}
+ self.reply_map = {}
+ self.cross_task_payloads = []
+ self.services = []
+ self.queries = []
+ self.arm_ports = []
+ self.report_trap = None
+ self.switches = []
+ #: Blocked syscall per task (so a resumed ecall is not re-executed as
+ #: a fresh send/post: the kernel would switch back *after* the wait).
+ self.blocked = {}
+
+ # -- task identity -----------------------------------------------------
+
+ def switch(self, task: str, pc: int = 0) -> dict:
+ """Set the executing task (runner-side cooperative switch)."""
+ self.current = task
+ self.task = task
+ event = self._event("task_switch", pc=int(pc), to=task)
+ self.switches.append({"pc": int(pc), "to": task})
+ return event
+
+ def _task_index(self, name: str) -> int:
+ return self.tasks.index(name) if name in self.tasks else 0
+
+ # -- routing helpers ---------------------------------------------------
+
+ def _find_receiver(self, port: int):
+ for name in self.tasks:
+ for shuttle in sorted(self.task_recvs[name]):
+ post = self.task_recvs[name][shuttle]
+ if post["port"] == int(port):
+ return post
+ return None
+
+ def _post(self, *, shuttle: int, port: int, payload: int, capacity: int,
+ pc: int, wait_id: int, timeout: int) -> dict:
+ post = {
+ "shuttle": int(shuttle),
+ "port": int(port),
+ "role": role_for(port),
+ "payload": int(payload),
+ "capacity": int(capacity),
+ "pc": int(pc),
+ "wait_id": int(wait_id),
+ "timeout": int(timeout),
+ "task": self.current,
+ }
+ self.posts.append(post)
+ self.task_recvs[self.current][int(shuttle)] = post
+ self._event("receive_post", **post)
+ self._try_kick()
+ return post
+
+ def _complete(self, shuttle: int, *, payload_size: int, origin_task: int,
+ contract: str, reason: str) -> dict:
+ owner = self.current
+ post = None
+ for name in self.tasks:
+ if int(shuttle) in self.task_recvs[name]:
+ owner = name
+ post = self.task_recvs[name].pop(int(shuttle))
+ break
+ if post is None: # pragma: no cover - caller only completes posts
+ raise KeyError(f"shuttle {shuttle} is not posted")
+ entry = {
+ "shuttle": int(shuttle),
+ "port": int(post["port"]),
+ "role": post["role"],
+ "payload": int(post["payload"]),
+ "capacity": int(post["capacity"]),
+ "payload_size": int(payload_size),
+ "origin_task": int(origin_task),
+ "contract": contract,
+ "reason": reason,
+ "post_pc": int(post["pc"]),
+ "task": owner,
+ }
+ self.completed.append(entry)
+ self.task_completions[owner].append(entry)
+ self._event("receive_complete", **entry)
+ return entry
+
+ def _copy(self, machine, dst: int, src: int, size: int) -> bool:
+ if size <= 0:
+ return True
+ try:
+ data = machine.read_mem(int(src), int(size))
+ machine.write_mem(int(dst), data)
+ return True
+ except Exception:
+ return False
+
+ def _take_completion(self, machine, pc: int):
+ queue = self.task_completions[self.current]
+ if not queue:
+ return None
+ entry = queue.pop(0)
+ machine.x[14] = entry["payload_size"] & 0xFFFFFFFFFFFFFFFF
+ machine.x[15] = entry["origin_task"] & 0xFFFFFFFFFFFFFFFF
+ machine.x[16] = entry["shuttle"] & 0xFFFFFFFFFFFFFFFF
+ machine.x[17] = 0
+ record = {
+ "pc": int(pc),
+ "completed": entry["shuttle"],
+ "payload_size": entry["payload_size"],
+ "origin_task": entry["origin_task"],
+ "role": entry["role"],
+ "contract": entry["contract"],
+ "task": self.current,
+ }
+ self.waits.append(record)
+ self._event("wait_complete", **record)
+ return record
+
+ def _block(self, machine, pc: int, *, reason: str, port: int,
+ shuttle: int) -> None:
+ kind = {"port-wait-blocks": "wait", "sync-recv-blocks": "recv",
+ "sync-send-blocks": "send"}.get(reason, "wait")
+ self.blocked[self.current] = {"kind": kind, "reason": reason,
+ "pc": int(pc), "port": int(port),
+ "shuttle": int(shuttle)}
+ record = {
+ "pc": int(pc),
+ "reason": reason,
+ "port": int(port),
+ "shuttle": int(shuttle),
+ "pending": sorted(s for name in self.tasks
+ for s in self.task_recvs[name]),
+ "task": self.current,
+ }
+ self.waits.append(record)
+ self._event("wait_block", **record)
+ if self.stopped is None:
+ self.stopped = {
+ "num": TASK_PORT_SEND_RECV_WAIT,
+ "name": "task_port_send_recv_wait",
+ "pc": int(pc),
+ "reason": reason,
+ "pending": record["pending"],
+ "task": self.current,
+ }
+
+ # -- syscall surface ---------------------------------------------------
+
+ def handle(self, machine, pc: int = 0):
+ num = int(machine.x[5])
+ if num == TASK_PORT_SEND_RECV_WAIT:
+ return self._handle_network(machine, pc)
+ if num == INTERRUPT_ARM_EXTERNAL:
+ mask, port = int(machine.x[10]), int(machine.x[11])
+ self.armed_mask |= mask
+ self.arm_ports.append({"mask": mask, "port": port, "pc": int(pc),
+ "task": self.current})
+ self._event("interrupt_arm_external", mask=mask, port=port,
+ pc=pc, task=self.current)
+ self._try_kick()
+ return True, LIBOS_OK
+ if num == TASK_STATE_QUERY:
+ self.queries.append({"pc": int(pc), "task_index": int(machine.x[10]),
+ "buffer": int(machine.x[11]),
+ "task": self.current})
+ trap = self.report_trap
+ if trap is None:
+ # No modelled trap state: the kernel would return an access
+ # error for an invalid target; return LIBOS_ERROR_INVALID_ACCESS
+ # (labelled model value) and let the firmware error path run.
+ return True, 0x80000000
+ state = task_state_bytes(
+ xcause=trap["xcause"], xepc=trap["xepc"],
+ xbadaddr=trap["xbadaddr"], registers=trap.get("registers"),
+ )
+ try:
+ machine.write_mem(int(machine.x[11]), state)
+ except Exception:
+ return True, 0x80000000
+ self._event("task_state_query", **self.queries[-1],
+ xcause=hex(trap["xcause"]), xepc=hex(trap["xepc"]))
+ return True, LIBOS_OK
+ if num == MEMORY_QUERY:
+ remote, pointer = int(machine.x[10]), int(machine.x[11])
+ attrs = 0x1 | 0x2 | 0x8 # read | write | cached (flat model)
+ resolved = pointer
+ translator = getattr(machine, "translator", None)
+ if translator is not None:
+ sim = translator.translate(pointer)
+ if sim is not None:
+ resolved = sim
+ machine.x[11] = attrs
+ machine.x[12] = pointer & ~0xFFF
+ machine.x[13] = 0x1000
+ machine.x[14] = 1 # LIBOS_MEMORY_APERTURE_SYSCOH
+ machine.x[15] = resolved
+ record = {"pc": int(pc), "remote_task": remote, "pointer": pointer,
+ "resolved": resolved, "task": self.current}
+ self.services.append(dict(record, service="memory_query"))
+ self._event("memory_query", **record)
+ return True, LIBOS_OK
+ if num == SHUTTLE_RESET:
+ record = {"pc": int(pc), "shuttle": int(machine.x[10]),
+ "task": self.current}
+ self.services.append(dict(record, service="shuttle_reset"))
+ self._event("shuttle_reset", **record)
+ return True, LIBOS_OK
+ return super().handle(machine, pc)
+
+ def _handle_network(self, machine, pc: int):
+ resuming = self.blocked.pop(self.current, None)
+ if resuming is not None and resuming["kind"] in ("recv", "wait"):
+ record = self._take_completion(machine, pc)
+ if record is not None:
+ return True, LIBOS_OK
+ self._block(machine, pc, reason=resuming["reason"],
+ port=resuming["port"], shuttle=resuming["shuttle"])
+ return False, None
+ if resuming is not None and resuming["kind"] == "send":
+ self._block(machine, pc, reason=resuming["reason"],
+ port=resuming["port"], shuttle=resuming["shuttle"])
+ return False, None
+ a0, a1, a2, a3 = (int(machine.x[10]), int(machine.x[11]),
+ int(machine.x[12]), int(machine.x[13]))
+ a4, a5, a6, a7 = (int(machine.x[14]), int(machine.x[15]),
+ int(machine.x[16]), int(machine.x[17]))
+ t1, t2 = int(machine.x[6]), int(machine.x[7])
+ send = a0 != 0 or a1 != 0
+ recv = a4 != 0 or a5 != 0
+ sync_recv = recv and t1 != 0
+
+ if send and a1 == 0:
+ # Reply: route through the received shuttle's partner.
+ target = self.reply_map.pop((self.current, a0), None)
+ if target is not None:
+ name, shuttle = target
+ post = self.task_recvs[name].get(shuttle)
+ if post is not None:
+ size = min(a3, int(post["capacity"]))
+ ok = self._copy(machine, post["payload"], a2, size)
+ entry = self._complete(
+ shuttle, payload_size=size if ok else 0,
+ origin_task=self._task_index(self.current),
+ contract="modelled:cross-task-reply",
+ reason=f"reply from {self.current} ({size} bytes)")
+ self.sends.append({"shuttle": a0, "port": 0, "size": a3,
+ "pc": int(pc), "task": self.current,
+ "queued": False, "delivered_to": entry["shuttle"]})
+ self.cross_task_payloads.append({
+ "port": 0, "size": size, "from": self.current,
+ "to": entry.get("task", name), "kind": "reply"})
+ machine.x[14] = size
+ machine.x[15] = 0
+ machine.x[16] = a0
+ machine.x[17] = 0
+ if t1 == a0 or not recv:
+ return True, LIBOS_OK
+
+ if send and a1 != 0:
+ partner = self._find_receiver(a1)
+ if partner is None:
+ size = a3
+ self.sends.append({"shuttle": a0, "port": a1, "size": size,
+ "pc": int(pc), "task": self.current,
+ "queued": True, "payload": a2})
+ return self._unrouted_send(machine, pc, a0, a1, a3, t1,
+ recv, sync_recv)
+ size = min(a3, int(partner["capacity"]))
+ ok = self._copy(machine, partner["payload"], a2, size)
+ entry = self._complete(
+ int(partner["shuttle"]), payload_size=size if ok else 0,
+ origin_task=self._task_index(self.current),
+ contract="modelled:cross-task-send",
+ reason=f"send from {self.current} port {a1} copied {size}")
+ self.sends.append({"shuttle": a0, "port": a1, "size": a3,
+ "pc": int(pc), "task": self.current,
+ "queued": False, "delivered_to": entry["shuttle"]})
+ self.cross_task_payloads.append({
+ "port": int(a1), "size": size, "from": self.current,
+ "to": entry.get("task"), "kind": "send"})
+ if a4 or a5:
+ self.reply_map[(entry.get("task"), entry["shuttle"])] = (self.current, a4)
+ if t1 == a0:
+ machine.x[14] = size
+ machine.x[15] = 0
+ machine.x[16] = a0
+ machine.x[17] = 0
+ return True, LIBOS_OK
+ if not recv:
+ return True, LIBOS_OK
+
+ if recv:
+ self._post(shuttle=a4, port=a5, payload=a6, capacity=a7, pc=pc,
+ wait_id=t1, timeout=t2)
+ if sync_recv:
+ record = self._take_completion(machine, pc)
+ if record is not None:
+ return True, LIBOS_OK
+ self._block(machine, pc, reason="sync-recv-blocks", port=a5,
+ shuttle=a4)
+ return False, None
+ return True, LIBOS_OK
+
+ record = self._take_completion(machine, pc)
+ if record is not None:
+ return True, LIBOS_OK
+ self._block(machine, pc, reason="port-wait-blocks", port=0, shuttle=0)
+ return False, None
+
+ def _unrouted_send(self, machine, pc, shuttle, port, size, wait_id,
+ recv, sync_recv):
+ """A send with no modelled receiver: keep the boot-shape shim.
+
+ The zero-length boot handshake send had no modelled peer and is an
+ explicit, labelled assumption (``modelled:peer-consumes-on-send``);
+ every other unrouted send stays queued and blocks its sync sender.
+ """
+ if size != 0:
+ if wait_id == shuttle:
+ self._block(machine, pc, reason="sync-send-blocks", port=port,
+ shuttle=shuttle)
+ return False, None
+ return True, LIBOS_OK
+ machine.x[14] = 0
+ machine.x[15] = 0
+ machine.x[16] = shuttle
+ machine.x[17] = 0
+ self._event("send_complete", shuttle=shuttle, port=port, size=0,
+ pc=int(pc), task=self.current,
+ contract="modelled:peer-consumes-on-send")
+ if wait_id == shuttle or not recv:
+ return True, LIBOS_OK
+ if sync_recv:
+ record = self._take_completion(machine, pc)
+ if record is not None:
+ return True, LIBOS_OK
+ self._block(machine, pc, reason="sync-recv-blocks", port=0,
+ shuttle=0)
+ return False, None
+ return True, LIBOS_OK
+
+ def _try_kick(self):
+ """Deliver a pending host wake to the extintr receive (documented shim).
+
+ The bound interrupt port is recorded from the arm call (the real kernel
+ delivers to the armed port); the modelled report retires the waiting
+ external-interrupt receive and records the modelled trap state its
+ ``TASK_STATE_QUERY`` consumer observes.
+ """
+ if not self.kicks_pending:
+ return None
+ for name in self.tasks:
+ for shuttle in sorted(self.task_recvs[name]):
+ post = self.task_recvs[name][shuttle]
+ if post["role"] != "extintr":
+ continue
+ self.kicks_pending -= 1
+ self.report_trap = {
+ "xcause": EXTERNAL_INTERRUPT_XCAUSE,
+ "xepc": SPURIOUS_XEPC,
+ "xbadaddr": 0,
+ "origin_task": 0,
+ }
+ self._complete(shuttle, payload_size=0, origin_task=0,
+ contract=EXTINTR_WAKE_CONTRACT,
+ reason="external-interrupt report")
+ return shuttle
+ return None
+
+ def state(self) -> dict:
+ base = super().state()
+ base.update({
+ "format": NETWORK_REPORT_FORMAT,
+ "tasks": list(self.tasks),
+ "current": self.current,
+ "task_completions": {name: [dict(e) for e in entries]
+ for name, entries in sorted(self.task_completions.items())},
+ "recvs": {name: sorted(self.task_recvs[name])
+ for name in self.tasks},
+ "cross_task_payloads": [dict(p) for p in self.cross_task_payloads],
+ "services": [dict(s) for s in self.services],
+ "queries": [dict(q) for q in self.queries],
+ "arm_ports": [dict(a) for a in self.arm_ports],
+ "report_trap": dict(self.report_trap) if self.report_trap else None,
+ "switches": [dict(s) for s in self.switches],
+ })
+ return base