summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/semfix_watch.py
blob: ffadb05ffce2a5210570e94d91801c6b0ca0d73c (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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#!/usr/bin/env python3
# semfix_watch.py - keep UVM VA->PA translations fresh from guest truth
# (host-side dev tool, no guest changes).
#
# Two jobs:
#  1. Semaphore pool: the emulation's content-scanned map can hold a *stale*
#     translation for a tracking-semaphore pool page (same VA, previous
#     client's page), so GPU-tracking-semaphore waits never clear. The
#     tracking semaphore object (kprobe `uvmt` gives its kernel pointer) has
#     `semaphore.page->memory->vas[]` with the pool page's CPU and GPU VAs;
#     QEMU turns the CPU VA into a GPA (`gva2gpa`) which we pin.
#  2. CE pushbuffer: the pushes live in a UVM rm_mem with GPU VA
#     `0x120000000` whose CPU VA is vmalloc-mapped guest RAM. Its pages map
#     linearly *descending* in GPA (page i -> base_gpa - i*0x1000). Pinning
#     the range lets the host pump read the pushes directly and execute the
#     PTE memsets/copies instead of only acknowledging their releases.
#
# Sources: guest kprobe on uvm_gpu_tracking_semaphore_update_completed_value
# (`sem=0x...`), QEMU QMP `x/..gx` reads and `gva2gpa`.
#
# Usage: tools/guest-diag/semfix_watch.py [interval-seconds]
# Runs until killed; prints one line per newly resolved/pinned page.

import json
import os
import re
import socket
import subprocess
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
VMEXEC = os.path.normpath(os.path.join(HERE, "..", "..", "scripts", "vgpu_vm.py"))
PARAM = "/sys/module/open_iov_vfio/parameters"
QMP_SOCK = os.environ.get("QMP_SOCK", "/tmp/qemu-qmp.sock")

GPU_VA_LO = 0x100000000
GPU_VA_HI = 0x2000000000
PUSHBUF_VA_LO = 0x120000000
PUSHBUF_VA_HI = 0x120400000
PUSHBUF_MAX_PAGES = 1024


class Qmp:
    def __init__(self, path):
        self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.s.connect(path)
        self.s.settimeout(15)
        self._rd()
        self.cmd("qmp_capabilities")

    def _rd(self):
        time.sleep(0.03)
        try:
            return self.s.recv(1 << 20).decode()
        except Exception:
            return ""

    def cmd(self, name, **args):
        try:
            self.s.sendall(json.dumps({"execute": name, "arguments": args}).encode() + b"\n")
            return json.loads(self._rd() or "{}")
        except Exception:
            return {}

    def hmp(self, line):
        return self.cmd("human-monitor-command", **{"command-line": line}).get("return", "")

    def read64(self, addr, n=1):
        out = self.hmp("x/%dgx 0x%x" % (n, addr))
        m = re.search(r":(.*)$", out, re.S)
        if not m:
            return []
        return [int(x, 16) for x in re.findall(r"0x([0-9a-fA-F]+)", m.group(1))][:n]

    def gva2gpa(self, va):
        out = self.hmp("gva2gpa 0x%x" % va)
        m = re.search(r"gpa: 0x([0-9a-fA-F]+)", out)
        return int(m.group(1), 16) if m else None


def is_kern(v):
    return v and (v >> 48) == 0xFFFF


def is_gpu(v):
    return v and GPU_VA_LO <= v < GPU_VA_HI


def guest_trace():
    """(sem kptrs, pushbuffer kptrs) from the trace ring, then clear it.

    Read *before* clearing: uvm_pushbuffer_create fires once per client and
    would be wiped by a clear that races it.
    """
    cmd = ("sudo sh -c 'grep -E \"uvmt|uvpb\" /sys/kernel/tracing/trace | tail -200; "
           "echo > /sys/kernel/tracing/trace'")
    try:
        out = subprocess.run([VMEXEC, "exec", cmd], capture_output=True,
                             text=True, timeout=90).stdout
    except Exception:
        return set(), set()
    sems = {int(m, 16) for m in re.findall(r"sem=0x([0-9a-fA-F]+)", out)}
    pbs = {int(m, 16) for m in re.findall(r"pb=0x([0-9a-fA-F]+)", out)}
    return sems, pbs


def resolve_pb_ptr(q, pb):
    """pushbuffer pointer -> rm_mem -> vas[] -> (gpu_vas, cpu, gpa)."""
    mem = q.read64(pb + 8)
    if not mem or not is_kern(mem[0]):
        return [], None, None
    vas = q.read64(mem[0] + 0x10, 16)
    gpu_vas = [v for v in vas if PUSHBUF_VA_LO <= v < PUSHBUF_VA_HI]
    cpu = next((v for v in vas if is_kern(v)), None)
    if not gpu_vas or not cpu:
        return [], None, None
    gpa = q.gva2gpa(cpu)
    if not gpa:
        return [], None, None
    return gpu_vas, cpu, gpa


def pin(va, pa):
    cmd = "printf '0x%x,0x%x' > %s/sema_map_add" % (va, pa, PARAM)
    for _ in range(40):
        r = subprocess.run(["sudo", "-n", "sh", "-c", cmd],
                           capture_output=True, text=True)
        if r.returncode == 0 and "timed out" not in (r.stderr or ""):
            return True
        time.sleep(0.05)  # async queue full: drain and retry
    return False


def resolve_sem_pool(q, sem):
    page = q.read64(sem)
    if not page or not page[0]:
        return []
    mem = q.read64(page[0])
    if not mem or not mem[0]:
        return []
    vas = q.read64(mem[0] + 0x10, 16)
    cpu = next((v for v in vas if is_kern(v)), None)
    gpu_vas = [v for v in vas if is_gpu(v)]
    if not cpu or not gpu_vas:
        return []
    gpa = q.gva2gpa(cpu)
    if not gpa:
        return []
    return [(gv, gpa) for gv in gpu_vas]


def resolve_pushbuffer(q, sem):
    """Find the pushbuffer rm_mem near the channel and return (gpu_vas, cpu, gpa)."""
    base = sem & ~0xFFF
    words = []
    for i in range(64):
        chunk = q.read64(base - 0x40000 + i * 0x1000, 512)
        if not chunk:
            continue
        words += chunk
    # The manager/pushbuffer live in the same slab region as the channel.
    prefix = sem >> 32
    cands = list(dict.fromkeys(w for w in words
                               if is_kern(w) and (w >> 32) == prefix))
    # Chain 1: value is the channel manager; +8 pushbuffer, +8 rm_mem.
    deadline = time.time() + 45
    for M in cands[:1200]:
        if time.time() > deadline:
            break
        pb = q.read64(M + 8)
        if not pb or not is_kern(pb[0]):
            continue
        mem = q.read64(pb[0] + 8)
        if not mem or not is_kern(mem[0]):
            continue
        vas = q.read64(mem[0] + 0x10, 16)
        gpu_vas = [v for v in vas if PUSHBUF_VA_LO <= v < PUSHBUF_VA_HI]
        if not gpu_vas:
            continue
        cpu = next((v for v in vas if is_kern(v)), None)
        if not cpu:
            continue
        gpa = q.gva2gpa(cpu)
        if gpa:
            return gpu_vas, cpu, gpa
    # Chain 2: value is the pushbuffer itself; +8 = rm_mem.
    # Two QMP reads per candidate, so cap the scan (bounded in time).
    deadline = time.time() + 30
    for V in cands[:400]:
        if time.time() > deadline:
            break
        mem = q.read64(V + 8)
        if not mem or not is_kern(mem[0]):
            continue
        g5 = q.read64(mem[0] + 0x38)
        if not g5 or not (PUSHBUF_VA_LO <= g5[0] < PUSHBUF_VA_HI):
            continue
        g4 = q.read64(mem[0] + 0x30)
        cpu = g4[0] if g4 and is_kern(g4[0]) else None
        if not cpu:
            continue
        gpa = q.gva2gpa(cpu)
        if gpa:
            return [g5[0]], cpu, gpa
    return [], None, None


def pin_pushbuffer(q, gpu_vas, cpu, gpa):
    """Pin the whole pushbuffer with *per-page* translations (the layout is
    not linear across the allocation: page 32 jumped on a real client)."""
    pinned = 0
    for gv in gpu_vas:
        miss = 0
        for i in range(PUSHBUF_MAX_PAGES):
            g = q.gva2gpa(cpu + i * 0x1000)
            if not g:
                miss += 1
                if miss >= 32:
                    print("semfix: pushbuf va=0x%x: 32 unmapped pages at %d, stop (%d pinned)"
                          % (gv, i, pinned), flush=True)
                    return pinned
                continue
            miss = 0
            if not pin(gv + i * 0x1000, g):
                print("semfix: pushbuf va=0x%x: pin failed at page %d" % (gv, i), flush=True)
                return pinned
            pinned += 1
        print("semfix: pushbuf va=0x%x cpu=0x%x gpa=0x%x: pinned %d page(s)"
              % (gv, cpu, gpa, pinned), flush=True)
    return pinned


def main():
    interval = float(sys.argv[1]) if len(sys.argv) > 1 else 3.0
    q = Qmp(QMP_SOCK)
    seen = set()
    seen_pb = set()
    pinned_ranges = set()

    def pin_range_once(gpu_vas, cpu, gpa):
        key = tuple(gpu_vas), cpu
        if key in pinned_ranges:
            return
        pinned_ranges.add(key)
        pin_pushbuffer(q, gpu_vas, cpu, gpa)

    print("semfix: watching (interval %.1fs)" % interval, flush=True)
    while True:
        sems, pbs = guest_trace()
        for pb in pbs - seen_pb:
            seen_pb.add(pb)
            try:
                gpu_vas, cpu, gpa = resolve_pb_ptr(q, pb)
            except Exception as e:
                print("semfix: pb=0x%x resolve failed: %s" % (pb, e), flush=True)
                continue
            if gpu_vas:
                pin_range_once(gpu_vas, cpu, gpa)
            else:
                print("semfix: pb=0x%x no pushbuffer rm_mem (retry)" % pb, flush=True)
        for sem in sems - seen:
            try:
                pairs = resolve_sem_pool(q, sem)
            except Exception as e:
                print("semfix: sem=0x%x resolve failed: %s" % (sem, e), flush=True)
                continue
            if not pairs:
                print("semfix: sem=0x%x unresolved (retry)" % sem, flush=True)
                continue
            seen.add(sem)
            for gv, gpa in pairs:
                ok = pin(gv, gpa)
                print("semfix: sem=0x%x va=0x%x -> gpa=0x%x pin=%s"
                      % (sem, gv, gpa, "ok" if ok else "FAILED"), flush=True)
            try:
                gpu_vas, cpu, gpa = resolve_pushbuffer(q, sem)
            except Exception as e:
                print("semfix: pushbuf resolve failed: %s" % e, flush=True)
                gpu_vas = []
            if gpu_vas:
                pin_range_once(gpu_vas, cpu, gpa)
            else:
                print("semfix: pushbuf not found near sem=0x%x (retry)" % sem, flush=True)
        time.sleep(interval)


if __name__ == "__main__":
    main()