summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/libos_port.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-06 19:28:39 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-06 19:28:39 -0400
commit6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf (patch)
tree3b3680902cd2223cbe60aaec0a0799cfc7b26be6 /debug/risc-v-gsp-rm-vm/sim/libos_port.py
parenta5473c91b8869b017385582ea969783b33553ded (diff)
downloadopen-iov-6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf.tar.gz
open-iov-6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf.zip
risc-v-gsp-rm-debugger: land docs 45-53 + harness/sim tool set with tests
rm_capture (chunked decode), launch_audit, state_seed/state_decode, valprobe, rehost, contracts+run_contracts, host_traffic, gmmu_walk, msgq_attach, libos_port, log_decode, server_snapshot; doc index updates in the tool overview, operations pages and the repo README.
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.py386
1 files changed, 386 insertions, 0 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
new file mode 100644
index 0000000..eaa3f95
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/sim/libos_port.py
@@ -0,0 +1,386 @@
+"""libos port receive-path model (the B5 message-loop blocker, first half).
+
+Status: **offline development tool (new, additive)**. This module models the
+part of the GSP firmware's libos *port receive* machinery that the B3
+message-loop task needs before it can make progress: the argument contract of
+``TASK_PORT_SEND_RECV_WAIT`` (syscall 1), the asynchronous receive post, the
+synchronous send/reply, the cross-payload handoff performed when a receive
+completes, and the external-interrupt port retirement that a host doorbell wake
+performs.
+
+Evidence kind: **firmware use + RTOS port implementation** (the same evidence
+class as :mod:`sim.libos`). Every structure below was observed on the real
+570.144 image, not inferred from a stub:
+
+* the message-loop task posts four asynchronous receives through a wrapper at
+ firmware vaddr ``0x4018000`` that moves its arguments ``a0-a3`` into
+ ``a4-a7``, zeroes ``a0-a3`` and ``t1``/``t2``, and executes ``ecall`` with
+ ``t0 = 1`` (an async receive post):
+ ``(shuttle 10, port 6, payload 0, size 0)`` immediately followed by
+ ``INTERRUPT_ARM_EXTERNAL(0xFF)`` -- the external-interrupt receive;
+ ``(shuttle 11, port 7, payload 0x5ba8e10, size 4)``;
+ ``(shuttle 9, port 8, payload 0x4015070, size 0x808)``;
+ ``(shuttle 14, port 13, payload 0x5ba8e20, size 0x24)``;
+* it then performs one synchronous send on the default shuttle id (1) to port
+ 12 with a zero-length payload, ``waitId = 1``, ``timeout = -1`` (infinite);
+* it blocks in ``PortWait(LIBOS_SHUTTLE_ANY, timeout = -1)`` (ecall at
+ ``0x4019df4``);
+* on an external interrupt the port that was armed retires with
+ ``error_code = LIBOS_OK`` and ``payload_size = 0`` (the report path), and the
+ wait returns with ``a0 = status``, ``a4 = completedSize``,
+ ``a5 = completedRemoteTaskId``, ``a6 = completedShuttle``.
+
+The argument contract modelled (mirrors the RTOS ``libosPortSendRecvAndWait``):
+
+=============== =========================================================
+register meaning at ``ecall``
+=============== =========================================================
+``t0`` syscall number (1)
+``a0`` send shuttle id (0 = none)
+``a1`` send port id
+``a2``/``a3`` send payload address / size
+``a4`` receive shuttle id (0 = none)
+``a5`` receive port id
+``a6``/``a7`` receive payload address / capacity
+``t1`` wait id (0 = do not block, ``0xFFFF`` = any)
+``t2`` timeout in ns (``0xFFFFFFFFFFFFFFFF`` = infinite)
+=============== =========================================================
+
+On return: ``a0`` = status, ``a4`` = completed size, ``a5`` = originating task
+id, ``a6`` = completed shuttle (local handle).
+
+Honesty rules (mandatory; docs/51 section 6, docs/06-ACCURACY.md):
+
+* the model is **data-driven by observation**: port roles come from the posted
+ receive sequence of the real image; nothing infers a port number that was
+ not observed;
+* every modelled event carries ``"modelled": true`` and a label -- the host
+ doorbell wake is ``shim:host-doorbell-wake``, the boot handshake send is
+ ``modelled:peer-consumes-on-send``. These are not firmware behaviour;
+* **receives are never completed with invented payloads**: a wake only retires
+ the external-interrupt receive with the RTOS report contract (rare, zero
+ length). No command, status or event payload is fabricated;
+* wait semantics are strictly "return the first completed shuttle or stop";
+ the model never returns from a blocking wait that nothing completed;
+* L0/L1 only: no timing (L2) and no device-environment (L3) claim; with a
+ single modelled task there is no scheduler and no other task is run.
+
+Development-only, offline: no GPU, kernel module, mdev, guest, network, QEMU,
+``/dev`` access or reboot. Nothing here is a product dependency and nothing
+is injected into a guest.
+"""
+from __future__ import annotations
+
+from sim.libos import (
+ INTERRUPT_ARM_EXTERNAL,
+ NAMES,
+ TASK_PORT_SEND_RECV_WAIT,
+)
+
+#: Report schema tag for :meth:`LibosPortModel.state`.
+REPORT_FORMAT = "greeneye-msgq-port/1"
+
+#: ``LIBOS_SHUTTLE_ANY`` (RTOS port API).
+SHUTTLE_ANY = 0xFFFF
+#: The task's default shuttle id (RTOS port API).
+SHUTTLE_DEFAULT = 1
+#: ``LIBOS_TIMEOUT_INFINITE``.
+TIMEOUT_INFINITE = 0xFFFFFFFFFFFFFFFF
+#: LIBOS_OK.
+LIBOS_OK = 0
+
+#: Observed port roles for the real 570.144 message-loop task. The values are
+#: the port ids carried by the task's posted receives (resource handles); the
+#: labels describe behaviour only.
+PORT_ROLES = {
+ 6: "extintr", # external-interrupt receive (armed with 0xFF)
+ 7: "notification", # 4-byte notification buffer
+ 8: "request", # 0x808-byte request/reply buffer
+ 12: "handshake", # zero-length boot handshake send target
+ 13: "event", # 36-byte event buffer
+}
+
+#: Port label used when a port was not part of the observed post sequence.
+ROLE_DATA = "data"
+
+#: The wake contract observed for the external-interrupt receive: the RTOS
+#: report retires the waiting shuttle with error LIBOS_OK and a zero payload.
+EXTINTR_WAKE_CONTRACT = "shim:host-doorbell-wake"
+HANDSHAKE_CONTRACT = "modelled:peer-consumes-on-send"
+
+
+def role_for(port: int) -> str:
+ """Role label for a port id (observed table; ``"data"`` otherwise)."""
+ return PORT_ROLES.get(int(port), ROLE_DATA)
+
+
+class LibosPortModel:
+ """Deterministic model of the libos receive path for one GSP task.
+
+ The model is a ``Machine.syscall_handler``: it owns syscall 1
+ (``TASK_PORT_SEND_RECV_WAIT``) and the interrupt-arm call, and delegates
+ every other syscall to ``base`` (:class:`sim.libos.LibosSyscalls`, which
+ keeps recording). ``machine.x`` is the only machine state touched.
+ """
+
+ name = "libos-port-model"
+
+ def __init__(self, base=None, *, task: str = "gsp-rm-task"):
+ self.base = base
+ self.task = task
+ #: Async receive posts in observation order.
+ self.posts: list[dict] = []
+ #: Pending receives by shuttle id.
+ self.pending: dict[int, dict] = {}
+ #: Completed handoffs awaiting a wait (FIFO).
+ self.completed: list[dict] = []
+ #: Synchronous sends observed (send payloads are not copied; the peer
+ #: side is not modelled).
+ self.sends: list[dict] = []
+ #: Blocking waits observed.
+ self.waits: list[dict] = []
+ #: Labelled model events (every one is explicitly ``modelled``).
+ self.events: list[dict] = []
+ #: External interrupt mask last armed (RTOS ``IRQMSET`` semantics).
+ #: An installed base model may already have observed an arm before this
+ #: model was attached (the recorded boot arm), so seed from it.
+ self.armed_mask = int(getattr(base, "armed_mask", 0))
+ #: Recorded host doorbell wakes.
+ self.host_kicks = 0
+ #: Host wakes not yet handed to a receive (the firmware may post and
+ #: arm after the host has rung the doorbell; the pending interrupt is
+ #: delivered at the first armed receive).
+ self.kicks_pending = 0
+ #: The request that stopped the run (unmodelled or blocking), if any.
+ self.stopped: dict | None = None
+
+ # -- observation -------------------------------------------------------
+
+ def _event(self, name: str, **fields) -> dict:
+ event = {"event": name, "modelled": True, "task": self.task}
+ event.update(fields)
+ self.events.append(event)
+ return event
+
+ def host_kick(self, *, label=EXTINTR_WAKE_CONTRACT, queue=None, value=None,
+ bit=None) -> dict:
+ """Record the host doorbell write; deliver it when a receive is armed.
+
+ The host submit path writes ``NV_PGSP_QUEUE_HEAD(0)``; the mapping from
+ that write to a falcon interrupt bit is **not modelled** (unobserved).
+ The wake is delivered to the external-interrupt receive once it has
+ been posted and interrupts are armed (the firmware may do both after
+ the host has already rung). Returns the event; ``"delivered"`` is the
+ retired shuttle id, or ``None`` when the wake remains pending.
+ """
+ self.host_kicks += 1
+ self.kicks_pending += 1
+ delivered = self._try_kick()
+ return self._event(
+ "host_kick",
+ contract=label,
+ queue=queue,
+ value=value,
+ falcon_irq_bit=bit,
+ delivered=delivered,
+ pending=bool(self.kicks_pending),
+ note=("queue-head write -> falcon interrupt bit is unobserved; "
+ "the wake is the documented shim"),
+ )
+
+ def _try_kick(self):
+ """Deliver one pending host wake to an armed extintr receive, if any."""
+ if not self.kicks_pending or not self.armed_mask:
+ return None
+ for shuttle in sorted(self.pending):
+ post = self.pending[shuttle]
+ if post["role"] != "extintr":
+ continue
+ self.kicks_pending -= 1
+ self._complete(
+ shuttle,
+ payload_size=0,
+ origin_task=0,
+ contract=EXTINTR_WAKE_CONTRACT,
+ reason="external-interrupt report (armed mask "
+ f"{self.armed_mask:#x}, pending host doorbell wake)",
+ )
+ return shuttle
+ return None
+
+ def _complete(self, shuttle: int, *, payload_size: int, origin_task: int,
+ contract: str, reason: str) -> dict:
+ post = self.pending.pop(int(shuttle))
+ 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"]),
+ }
+ self.completed.append(entry)
+ self._event("receive_complete", **entry)
+ return entry
+
+ # -- syscall surface ---------------------------------------------------
+
+ def handle(self, machine, pc: int = 0):
+ """``Machine`` syscall hook: ``(handled, result)``."""
+ num = int(machine.x[5])
+ if num == TASK_PORT_SEND_RECV_WAIT:
+ return self._handle_port(machine, pc)
+ if num == INTERRUPT_ARM_EXTERNAL:
+ self.armed_mask |= int(machine.x[10])
+ self._event("interrupt_arm_external", mask=int(machine.x[10]), pc=pc)
+ self._try_kick()
+ if self.base is not None:
+ handled, result = self.base.handle(machine, pc)
+ else:
+ handled, result = (True, LIBOS_OK) if num == INTERRUPT_ARM_EXTERNAL \
+ else (False, None)
+ if not handled and self.stopped is None:
+ entry = getattr(self.base, "stopped", None)
+ number = int(entry["num"]) if entry else num
+ self.stopped = {
+ "num": number,
+ "name": (entry or {}).get("name", NAMES.get(number, f"unknown_{number}")),
+ "pc": int(pc),
+ "reason": "unmodelled-syscall",
+ }
+ return handled, result
+
+ def _handle_port(self, machine, pc: int):
+ 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
+
+ if recv:
+ self._post(shuttle=a4, port=a5, payload=a6, capacity=a7, pc=pc,
+ wait_id=t1, timeout=t2)
+ 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:
+ # Async receive post: the kernel returns LIBOS_OK without touching
+ # the wait outputs (a4-a6 still hold the receive arguments).
+ return True, LIBOS_OK
+
+ # Pure blocking wait: return the first completed shuttle, or stop.
+ 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("wait_complete", **record)
+ return True, LIBOS_OK
+
+ record = {
+ "pc": int(pc),
+ "wait_id": t1,
+ "timeout": t2,
+ "reason": "port-wait-blocks",
+ "pending": sorted(self.pending),
+ }
+ 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": "port-wait-blocks",
+ "pending": sorted(self.pending),
+ }
+ return False, 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),
+ }
+ self.posts.append(post)
+ self.pending[int(shuttle)] = post
+ self._event("receive_post", **post)
+ self._try_kick()
+ return post
+
+ def _send(self, *, shuttle: int, port: int, payload: int, size: int,
+ wait_id: int, timeout: int, pc: int, machine) -> dict:
+ """Model a synchronous send/reply as completed by its peer.
+
+ Only the boot handshake shape was observed (zero-length payload to port
+ 12 on the default shuttle). The peer is not modelled: consumption is
+ an explicit, labelled assumption. The wait outputs receive the RTOS
+ transcribe contract for a completed send (``a4`` = copied size,
+ ``a5`` = 0, ``a6`` = send shuttle).
+ """
+ record = {
+ "shuttle": int(shuttle),
+ "port": int(port),
+ "role": role_for(port) if port else "reply",
+ "payload": int(payload),
+ "size": int(size),
+ "pc": int(pc),
+ "wait_id": int(wait_id),
+ "timeout": int(timeout),
+ "contract": HANDSHAKE_CONTRACT,
+ }
+ self.sends.append(record)
+ machine.x[14] = int(size) & 0xFFFFFFFFFFFFFFFF
+ machine.x[15] = 0
+ machine.x[16] = int(shuttle) & 0xFFFFFFFFFFFFFFFF
+ machine.x[17] = 0
+ self._event("send_complete", **record)
+ return record
+
+ # -- report surface ----------------------------------------------------
+
+ def state(self) -> dict:
+ """Deterministic serialisable state for the ``greeneye-msgq-attach/1``
+ report (no host timestamps)."""
+ return {
+ "format": REPORT_FORMAT,
+ "task": self.task,
+ "armed_mask": int(self.armed_mask),
+ "host_kicks": int(self.host_kicks),
+ "kicks_pending": int(self.kicks_pending),
+ "posts": [dict(post) for post in self.posts],
+ "pending": sorted(self.pending),
+ "completed": [dict(entry) for entry in self.completed],
+ "sends": [dict(record) for record in self.sends],
+ "waits": [dict(record) for record in self.waits],
+ "events": [dict(event) for event in self.events],
+ "stopped": dict(self.stopped) if self.stopped else None,
+ }