diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-06 19:57:20 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-06 19:57:20 -0400 |
| commit | 23d8683f3012ac579db424735d1418814bb7ff01 (patch) | |
| tree | 384cca6be54d7a5936544ff40e7715cf7751a4a9 /debug/risc-v-gsp-rm-vm/sim/libos_port.py | |
| parent | 9de2a2a9899236b62b413ceb78946778fc6ada39 (diff) | |
| download | open-iov-23d8683f3012ac579db424735d1418814bb7ff01.tar.gz open-iov-23d8683f3012ac579db424735d1418814bb7ff01.zip | |
risc-v-gsp-rm-debugger: B5 two-task attach + aggregate-state decode + docs 51/53 status
- sim/libos_port.py: port-role model + two-task network (extintr handoff,
cross-task send/reply, TASK_STATE_QUERY trap state)
- sim/msgq_attach.py: image task discovery, bounded two-task schedule,
second-task-frontier divergence fields (first ring poll still blocked on
post-init resume + GSP_ARGUMENTS_CACHED handoff; no fabricated polls)
- harness/state_decode.py: aggregate 'group: key=value' state parsing
(20-gsp-phys-state-* dumps) + kv diff classification
- tests: 127 green across the five observability modules
- docs/51/53: status and changelog updates
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.py | 511 |
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 |
