blob: 641523492d5c71a97ac24a1a35d293993fde37a8 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Final
from shared.protocol.aspace import (
COMMAND_NAME,
AddressRegionEntry,
AspaceRequest,
AspaceResponse,
)
from shared.protocol.base import Response
from zxdb_dap import ZxdbConsoleCommandArguments
__all__ = ["COMMAND_NAME", "handle", "parse_aspace_output"]
if TYPE_CHECKING:
from daemon.daemon import Daemon
# `zxdb` indents nested VMAR/mapping names by 2 spaces per hierarchy level.
_INDENT_SPACES_PER_DEPTH: Final[int] = 2
_ASPACE_PREFIX_RE = re.compile(
r"""
^\s*
(?P<start>0x[0-9a-fA-F]+) # Start virtual address
\s+
(?P<end>0x[0-9a-fA-F]+) # End virtual address
\s+
(?P<prot>[r-][w-][x-][sp]?) # Protection flags (e.g. rwx, rw-, r-xs)
\s+
(?P<size>\d+[BKMGT]) # Human-readable region size (e.g. 4K, 2M)
(?P<rest>.*)$ # Remaining columns (VMO/Linux columns + name)
""",
re.VERBOSE,
)
_VMO_COLS_RE = re.compile(
r"""
^\s*
(?P<koid>\d+) # Backing VMO KOID (Fuchsia aspace output)
\s+
(?P<offset>0x[0-9a-fA-F]+) # Byte offset into VMO
\s+
(?P<cmt_pgs>\d+) # Committed page count
\s(?P<name_part>.*)$ # Indented VMAR/VMO region name
""",
re.VERBOSE,
)
_LINUX_COLS_RE = re.compile(
r"""
^\s*
(?P<offset>0x[0-9a-fA-F]+) # File mapping offset (Linux /proc/maps format)
\s(?P<name_part>.*)$ # Mapped file path or region name
""",
re.VERBOSE,
)
_PAGE_SIZE_RE = re.compile(r"^\s*Page size:\s*(?P<page_size>\d+)\s*$")
_TOTAL_MAPPED_RE = re.compile(r"^\s*Total mapped bytes:\s*(?P<mapped>\d+)\s*$")
_TOTAL_COMMITTED_RE = re.compile(
r"""
^\s*Total[ ]committed[ ]pages:\s*
(?P<pages>\d+) # Committed page count
\s*=\s*
(?P<bytes>\d+) # Committed byte total
\s+bytes\s*$
""",
re.VERBOSE,
)
def _parse_aspace_region_line(
line: str, name_col_idx: int | None
) -> AddressRegionEntry | None:
"""Parses a single `aspace` table row into an `AddressRegionEntry`."""
m = _ASPACE_PREFIX_RE.match(line)
if not m:
return None
rest = m.group("rest")
vmo_koid: int | None = None
vmo_offset: str | None = None
committed_pages: int | None = None
if m_vmo := _VMO_COLS_RE.match(rest):
vmo_koid = int(m_vmo.group("koid"))
vmo_offset = m_vmo.group("offset").lower()
committed_pages = int(m_vmo.group("cmt_pgs"))
raw_name = m_vmo.group("name_part")
elif m_linux := _LINUX_COLS_RE.match(rest):
vmo_offset = m_linux.group("offset").lower()
raw_name = m_linux.group("name_part")
else:
raw_name = rest
name = raw_name.strip()
depth = 0
if name_col_idx is not None and len(line) > name_col_idx and name:
name_pos = line.rfind(name)
if name_pos >= name_col_idx:
depth = max(
0, (name_pos - name_col_idx) // _INDENT_SPACES_PER_DEPTH
)
return AddressRegionEntry(
start=m.group("start").lower(),
end=m.group("end").lower(),
prot=m.group("prot"),
size=m.group("size"),
vmo_koid=vmo_koid,
vmo_offset=vmo_offset,
committed_pages=committed_pages,
name=name,
depth=depth,
)
def parse_aspace_output(text: str, pid: int | None = None) -> AspaceResponse:
"""Parses `zxdb` `aspace` output into a structured `AspaceResponse`."""
regions: list[AddressRegionEntry] = []
page_size: int | None = None
total_mapped_bytes: int | None = None
total_committed_pages: int | None = None
total_committed_bytes: int | None = None
name_col_idx: int | None = None
for line in text.splitlines():
if name_col_idx is None and "Start" in line and "Name" in line:
name_col_idx = line.find("Name")
continue
if m_ps := _PAGE_SIZE_RE.match(line):
page_size = int(m_ps.group("page_size"))
continue
if m_tm := _TOTAL_MAPPED_RE.match(line):
total_mapped_bytes = int(m_tm.group("mapped"))
continue
if m_tc := _TOTAL_COMMITTED_RE.match(line):
total_committed_pages = int(m_tc.group("pages"))
total_committed_bytes = int(m_tc.group("bytes"))
continue
if region := _parse_aspace_region_line(line, name_col_idx):
regions.append(region)
return AspaceResponse(
pid=pid,
regions=regions,
page_size=page_size,
total_mapped_bytes=total_mapped_bytes,
total_committed_pages=total_committed_pages,
total_committed_bytes=total_committed_bytes,
)
async def handle(
daemon: Daemon, req: AspaceRequest
) -> Response[AspaceResponse]:
"""Handles an `aspace` request by running `aspace` via `zxdb.ConsoleCommand`."""
if not daemon.zxdb_writer:
return Response(
success=False, message="Not connected to zxdb DAP server"
)
pid, err = daemon.resolve_process_id(req.pid)
if err is not None:
return Response(success=False, message=err)
aspace_cmd = daemon.scope_console_command(
f"aspace -- {req.address}" if req.address else "aspace", pid
)
try:
cmd_resp = await daemon.dap_client.zxdb_console_command(
ZxdbConsoleCommandArguments(
command=aspace_cmd,
)
)
if not cmd_resp.success or cmd_resp.body is None:
return Response(
success=False,
message=cmd_resp.message or "Failed to query address space",
)
raw_text = cmd_resp.body.output or ""
if "Region not mapped." in raw_text:
return Response(
success=True,
body=AspaceResponse(pid=pid, regions=[]),
message="Region not mapped.",
)
parsed = parse_aspace_output(raw_text, pid=pid)
if (
not parsed.regions
and parsed.page_size is None
and "Start" not in raw_text
and raw_text.strip()
):
return Response(success=False, message=raw_text.strip())
if req.filter:
needle = req.filter.lower()
parsed.regions = [
r
for r in parsed.regions
if needle in r.name.lower()
or needle in r.prot.lower()
or (r.vmo_koid is not None and needle in str(r.vmo_koid))
]
return Response(success=True, body=parsed)
except Exception as e:
return Response(
success=False, message=f"Failed to query address space: {e}"
)