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
|
#!/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_sems():
"""Kernel pointers of tracking semaphores the client is spinning on."""
cmd = ("sudo sh -c 'echo > /sys/kernel/tracing/trace; sleep 2; "
"grep uvmt /sys/kernel/tracing/trace | tail -4'")
try:
out = subprocess.run([VMEXEC, "exec", cmd], capture_output=True,
text=True, timeout=90).stdout
except Exception:
return set()
return {int(m, 16) for m in re.findall(r"sem=0x([0-9a-fA-F]+)", out)}
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(9):
chunk = q.read64(base - 0x8000 + i * 0x1000, 512)
if not chunk:
break
words += chunk
cands = list(dict.fromkeys(w for w in words if is_kern(w)))
# Chain 1: value is the channel manager; +8 pushbuffer, +8 rm_mem.
for M in cands:
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):
pinned = 0
for gv in gpu_vas:
for i in range(PUSHBUF_MAX_PAGES):
if i in (0, 1, 2, 3, 8, 32, 128, 512):
g = q.gva2gpa(cpu + i * 0x1000)
if g != gpa - i * 0x1000:
print("semfix: pushbuf va=0x%x: page %d not linear (gpa=%s), stop at %d"
% (gv, i, hex(g) if g else None, pinned), flush=True)
return pinned
if not pin(gv + i * 0x1000, gpa - i * 0x1000):
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()
pb_done = False
print("semfix: watching (interval %.1fs)" % interval, flush=True)
while True:
for sem in guest_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)
if not pb_done:
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_pushbuffer(q, gpu_vas, cpu, gpa)
pb_done = True
else:
print("semfix: pushbuf not found near sem=0x%x (retry)" % sem, flush=True)
time.sleep(interval)
if __name__ == "__main__":
main()
|