summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/guest_mode.sh
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-11-12 20:01:04 -0500
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-11-12 20:01:04 -0500
commit725898e44666b4def396f7f3c5e50712fd37aa33 (patch)
tree261bba910db49036624cc2cf89baac6554a6f58d /tools/guest-diag/guest_mode.sh
parent800c108f9d2463066a221a0e4ff56867304551a6 (diff)
downloadopen-iov-725898e44666b4def396f7f3c5e50712fd37aa33.tar.gz
open-iov-725898e44666b4def396f7f3c5e50712fd37aa33.zip
vgsp: stop fabricating 0xFFFF into the nvPush error notifier; add r1780 GL ops tooling
The uncommitted keep-alive change wrote 0xFFFF into slot 0's status field (u16 at notifier+0x0e) on the X DDX / GLX kick-idle channel families. Ground truth (570.144 reference sources + stock nvidia_drv.so disassembly): slot 0 of an nvPush notifier block is the ERROR notifier; nvPushCheckChannelError reads that status and 0xFFFF aborts every nvPushWaitForNotifier/idle wait as a channel error. There is no pb_kickIfIdle in 570.144 - the DDX DMA-idle test reads the progress-tracker semaphore word, not this slot. Leave slot 0 untouched; keep the 0x8000->0 completion ack for the remaining slots and the r864 0xFFFF/0x8000 completion on the RC-watchdog/beef families. Correct the CONT.408/409 comments accordingly. New host/guest tooling: gpa.py (QMP gva2gpa), tick_gen.py (guest-RM timer BO tick), guest_mode.sh (shared nvClip modeNum reader), gl_verify.sh (X/GLX bring-up with ScreenInit-lottery retries), push_gl_tools.sh, the gzdoom_pbuf_run.sh guest recipe and the test_tri SDL2 harness validation program; ignore tools/guest-diag/build/.
Diffstat (limited to 'tools/guest-diag/guest_mode.sh')
-rwxr-xr-xtools/guest-diag/guest_mode.sh285
1 files changed, 285 insertions, 0 deletions
diff --git a/tools/guest-diag/guest_mode.sh b/tools/guest-diag/guest_mode.sh
new file mode 100755
index 0000000..2460131
--- /dev/null
+++ b/tools/guest-diag/guest_mode.sh
@@ -0,0 +1,285 @@
+#!/bin/bash
+# guest_mode.sh - read the NVIDIA X driver's nvClip screens[0].modeNum.
+#
+# The X driver (nvidia_drv.so) creates the nvClip shared area through its
+# anonymous-file helper: memfd_create first (name derived from
+# "/.nvidia_drv.XXXXXX"), then O_TMPFILE, then mkstemp+unlink. 570.144 size
+# is 0x245000 bytes. The fd is handed to GLX clients over X SEND_FDS.
+# Layout (570.144, verified against nvidia_drv.so 570.144):
+# +0x00 u32 grabbed
+# +0x04 u32 token (unique X-server token, sanity check)
+# +0x08 u32 screens[0].modeNum <-- this script
+# screens[] stride 0x84 (132) bytes
+#
+# modeNum parity is the GL client's __GL_DEV_MODESWITCH_TEST_COMPLETE bit:
+# even = device enabled / mode switch complete
+# odd = mode switch in progress / device disabled
+# GZDoom's GL init must observe an EVEN value; odd makes __glNVStateInit
+# clear initBits, buffer objects get no GPU memory, and GZDoom crashes at
+# 0x20 in FFlatVertexBuffer::Copy.
+#
+# STDOUT contract (parse this, nothing else):
+# MODENUM <n>
+# NO_XORG
+#
+# Exit codes:
+# 0 MODENUM printed
+# 3 NO_XORG (no Xorg process with an nvClip fd)
+# 4 permission / unexpected error (NO_XORG still printed)
+#
+# Usage:
+# sudo ./guest_mode.sh # one shot, display from $DISPLAY (default :0)
+# sudo ./guest_mode.sh --wait 15 # poll up to 15 s for Xorg's nvClip fd
+# sudo ./guest_mode.sh --pid 1515 # force one Xorg pid (testing/forensics)
+# sudo ./guest_mode.sh --interval 50 # poll interval ms (with --wait)
+# sudo ./guest_mode.sh --debug # candidate details on stderr
+#
+# Requires: bash, python3 (stdlib only), root (or a user that can read
+# /proc/<Xorg>/fd; Xorg normally runs as root in the guest).
+
+set -u
+
+if ! command -v python3 >/dev/null 2>&1; then
+ echo "guest_mode.sh: python3 not found" >&2
+ echo "NO_XORG"
+ exit 4
+fi
+
+exec python3 - "$@" <<'PY'
+import argparse
+import os
+import re
+import struct
+import sys
+import time
+
+NVCLIP_TARGET_RE = re.compile(r'(?:^|/)\.nvidia_drv\.[^/]*$|memfd:.*nvidia_drv')
+# 570.144 initial size is 0x245000. ExtendSharedArea() ftruncates upward
+# when a second channel block is needed, so accept a range, prefer exact.
+NVCLIP_SIZE_EXACT = 0x245000
+NVCLIP_SIZE_MIN = 0x80000
+NVCLIP_SIZE_MAX = 0x800000
+MODE_OFF = 0x8
+TOKEN_OFF = 0x4
+
+
+def eprint(msg):
+ print(msg, file=sys.stderr)
+
+
+def parse_args():
+ p = argparse.ArgumentParser(add_help=True)
+ p.add_argument('--pid', type=int, default=0,
+ help='only inspect this pid (bypasses the Xorg comm check)')
+ p.add_argument('--display', default=os.environ.get('DISPLAY', ':0'),
+ help='X display to prefer (default $DISPLAY or :0)')
+ p.add_argument('--wait', type=float, default=0.0,
+ help='poll up to SECONDS for the fd (default 0 = one shot)')
+ p.add_argument('--interval', type=float, default=200.0,
+ help='poll interval in milliseconds (default 200)')
+ p.add_argument('--debug', action='store_true',
+ help='print candidate details to stderr')
+ return p.parse_args()
+
+
+def read_text(path):
+ with open(path, 'rb') as f:
+ return f.read()
+
+
+def proc_args(pid):
+ try:
+ raw = read_text('/proc/%d/cmdline' % pid).split(b'\0')
+ except OSError:
+ return []
+ return [x.decode('utf-8', 'replace') for x in raw if x]
+
+
+def proc_comm(pid):
+ try:
+ return read_text('/proc/%d/comm' % pid).decode('utf-8', 'replace').strip()
+ except OSError:
+ return ''
+
+
+def proc_starttime(pid):
+ # /proc/pid/stat field 22 (1-based). Fields after the comm field:
+ # rest[0] is field 3 (state), so starttime is rest[19].
+ try:
+ data = read_text('/proc/%d/stat' % pid).decode('utf-8', 'replace')
+ rest = data.rsplit(')', 1)[1].split()
+ return int(rest[19])
+ except (OSError, IndexError, ValueError):
+ return 0
+
+
+def display_number(display):
+ # ":0", ":0.0", "host:0.0" -> 0
+ try:
+ tail = display.rsplit(':', 1)[1]
+ return int(tail.split('.', 1)[0])
+ except (IndexError, ValueError):
+ return None
+
+
+def is_xorg(pid):
+ comm = proc_comm(pid)
+ if comm in ('Xorg', 'X'):
+ return True
+ args = proc_args(pid)
+ if not args:
+ return False
+ base = os.path.basename(args[0])
+ return base in ('Xorg', 'X')
+
+
+def display_matches(pid, disp):
+ if disp is None:
+ return False
+ needle = ':%d' % disp
+ for arg in proc_args(pid):
+ if arg == needle or arg.startswith(needle + '.'):
+ return True
+ return False
+
+
+def candidate_fds(pid):
+ """Yield (fd_name, target, size) for nvClip-like fds of pid."""
+ fd_dir = '/proc/%d/fd' % pid
+ try:
+ names = os.listdir(fd_dir)
+ except PermissionError:
+ raise
+ except OSError:
+ return
+ for name in names:
+ if not name.isdigit():
+ continue
+ path = os.path.join(fd_dir, name)
+ try:
+ target = os.readlink(path)
+ st = os.stat(path)
+ except PermissionError:
+ raise
+ except OSError:
+ continue
+ if not NVCLIP_TARGET_RE.search(target):
+ continue
+ if not (NVCLIP_SIZE_MIN <= st.st_size <= NVCLIP_SIZE_MAX):
+ continue
+ yield name, target, st.st_size
+
+
+def read_candidate(pid, fd_name, target, size):
+ path = '/proc/%d/fd/%s' % (pid, fd_name)
+ try:
+ fd = os.open(path, os.O_RDONLY)
+ except OSError as exc:
+ return None, str(exc)
+ try:
+ blob = os.pread(fd, 12, 0)
+ except OSError as exc:
+ return None, str(exc)
+ finally:
+ os.close(fd)
+ if len(blob) < 12:
+ return None, 'short read (%d bytes)' % len(blob)
+ grabbed, token, mode = struct.unpack_from('<III', blob, 0)
+ if mode > 0xFFFF:
+ return None, 'implausible modeNum 0x%x' % mode
+ return {
+ 'pid': pid,
+ 'fd': int(fd_name),
+ 'target': target,
+ 'size': size,
+ 'grabbed': grabbed,
+ 'token': token,
+ 'mode': mode,
+ 'start': proc_starttime(pid),
+ 'disp': display_matches(pid, display_number(args.display)),
+ }, None
+
+
+def scan(debug=False):
+ """Return (best_candidate_or_None, saw_permission_error)."""
+ best = None
+ perm_error = False
+
+ try:
+ pids = [int(x) for x in os.listdir('/proc') if x.isdigit()]
+ except OSError as exc:
+ eprint('guest_mode.sh: cannot list /proc: %s' % exc)
+ return None, False
+
+ for pid in pids:
+ if args.pid and pid != args.pid:
+ continue
+ if not args.pid and not is_xorg(pid):
+ continue
+ try:
+ fds = list(candidate_fds(pid))
+ except PermissionError:
+ perm_error = True
+ if debug:
+ eprint('pid %d: permission denied listing fd/' % pid)
+ continue
+ for fd_name, target, size in fds:
+ cand, err = read_candidate(pid, fd_name, target, size)
+ if cand is None:
+ if debug:
+ eprint('pid %d fd %s: %s' % (pid, fd_name, err))
+ continue
+ if debug:
+ eprint('candidate pid=%d fd=%s size=0x%x token=0x%x mode=%d '
+ 'start=%d display_match=%s target=%s' %
+ (cand['pid'], cand['fd'], cand['size'], cand['token'],
+ cand['mode'], cand['start'], cand['disp'], cand['target']))
+ # Score: display match, exact size, nonzero token, newest Xorg.
+ key = (cand['disp'],
+ 1 if cand['size'] == NVCLIP_SIZE_EXACT else 0,
+ 1 if cand['token'] != 0 else 0,
+ cand['start'],
+ -cand['pid'])
+ if best is None or key > best[0]:
+ best = (key, cand)
+
+ return (best[1] if best else None), perm_error
+
+
+def main():
+ global args
+ args = parse_args()
+ deadline = time.monotonic() + max(0.0, args.wait)
+ interval = max(0.01, args.interval / 1000.0)
+ perm_error = False
+
+ while True:
+ cand, pe = scan(args.debug)
+ perm_error = perm_error or pe
+ if cand is not None:
+ print('MODENUM %d' % cand['mode'])
+ return 0
+ if time.monotonic() >= deadline:
+ break
+ time.sleep(interval)
+
+ if perm_error:
+ eprint('guest_mode.sh: Xorg exists but /proc/<pid>/fd is not readable; '
+ 'run as root (sudo).')
+ print('NO_XORG')
+ return 4
+ print('NO_XORG')
+ return 3
+
+
+if __name__ == '__main__':
+ try:
+ sys.exit(main())
+ except KeyboardInterrupt:
+ print('NO_XORG')
+ sys.exit(4)
+ except Exception as exc: # keep the stdout contract even on bugs
+ eprint('guest_mode.sh: unexpected error: %s' % exc)
+ print('NO_XORG')
+ sys.exit(4)
+PY \ No newline at end of file