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()
|