summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/phys_claim_cycle.sh
blob: 6f13d758a162f3c46f3af5aed72d97c71ae13162 (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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
#!/bin/bash
# phys_claim_cycle.sh - supervised physical-GPU claim/release cycle harness.
#
# Drives one or more full "D0 pre-lock" cycles of the physical-ownership
# backend (open-iov-vfio + gsp_phys_gpu=1 + gsp_phys_hazard_ack=1):
#
#   start persistenced -> wait GPU+root port active -> power/control=on on
#   GPU+port -> dmesg -C -> (stop persistenced && insmod) as ONE command ->
#   verify lock/claim patterns -> hold -> verify while claimed ->
#   rmmod -> verify release patterns -> restore auto + persistenced ->
#   health check.
#
# !!! HOST-FREEZE HAZARD !!!
# Claiming the GPU unbinds the distribution NVIDIA driver. On this host the
# dGPU board then power-gate and fail to come back, hard-freezing the system
# with no crash record (observed twice on 2026-09-30). Run only in a
# supervised session, with other host work stopped and the PART 87 recovery
# plan ready. The harness itself never retries an aborted cycle.
#
# RULE: never stop after FWSEC alone on this host. --boot (the full boot
# lifecycle) is the only write mode; --fwsec is retired and refused.
#
# Usage: phys_claim_cycle.sh --yes [--hold SECONDS] [--cycles N] [--layout]
#                             [--dryrun] [--boot-dryrun] [--boot] [--keep]
#                             [--ko PATH] [--fw NAME] [--no-restore]
#
#   --yes        required: acknowledge the host-freeze hazard
#   --hold S     seconds to keep the GPU claimed (default 30)
#   --cycles N   cycles to run, each only if the previous was green (default 1)
#   --layout     also pass gsp_phys_layout=1 (rung 3b); "stage 3b complete"
#                then becomes an expected (but non-fatal) pattern
#   --dryrun     also pass gsp_phys_fwsec_dryrun=1 (implies --layout): run the
#                READ-ONLY stage-3c validation (VBIOS/BIT/FWSEC parse and the
#                staging/placement plan, no device writes); expects the
#                "gsp-phys-fwsec-dryrun:" and "stage 3c dry-run complete"
#                lines. It is also implied by --boot-dryrun (alias)
#   --boot-dryrun implies --dryrun and also passes gsp_phys_boot_dryrun=1
#                (implies --layout): host-memory-only stage-3d staging
#                rehearsal (radix3 build, queue/PTE/args fill, all placement
#                math; ZERO BAR0 writes); additionally expects the
#                "stage 3d prime complete" line
#   --boot       pass gsp_phys_boot=1 (implies --layout): the ONLY write mode.
#                Runs the full boot lifecycle (FWSEC, Booter Load, RISC-V
#                start, FALCON_OS, RM-init poll). Requires "stage 3d
#                complete"; "stage 3e complete" = full green, "stage 3e
#                incomplete" = non-fatal warning (RM-init not observed, boot
#                still valid); on release requires "teardown complete: WPR2
#                down" (missing = red; do not retry)
#   --keep       after a GREEN cycle do NOT rmmod: leave the module loaded and
#                the GPU claimed by open-iov-gsp-phys (power/control=on on
#                GPU+port, nvidia-persistenced stopped) and skip the release
#                verification (no rebind/released lines); the hold checks
#                (lspci id, driver, runtime active) and abort signatures still
#                run. Prints a notice with the exact recovery steps. Compatible
#                with --boot/--boot-dryrun/--dryrun/--layout (it only changes
#                the ending); single cycle only (--cycles 1). Exit 0 = the
#                claimed session is intentionally left running.
#   --fwsec      RETIRED (2026-10-01). A FWSEC-only carve is a one-way door on
#                this fused part: it left WPR2 up and was not host-recoverable.
#                It is refused immediately and never reaches insmod. Use
#                --boot; never stop after FWSEC alone on this host.
#   --ko PATH    module to insmod (default kernel/open-iov-vfio.ko)
#   --fw NAME    GSP firmware to stage (request_firmware name under
#                /lib/firmware; default: the module's own default, currently
#                nvidia/570.211.01/gsp_tu10x.bin)
#   --no-restore do not restore power/control=auto or restart persistenced
#                at the end of each cycle (leaves the D0 hold in place)
#
# --boot is mutually exclusive with --dryrun/--boot-dryrun.
#
# Exit codes: 0 = all cycles green (with --keep: the claimed session was left
#             running intentionally); 1 = --yes missing / usage error;
#             2 = abort (hard abort signature, failed verification, red
#             in-window rail event); 3 = precondition failure.
#
# Per-cycle full dmesg is saved to
#   /tmp/opencode/phys_claim_cycle_<timestamp>_<n>.log
#
# Env overrides: GPU_BDF (default 0000:01:00.0), PORT_BDF (default: the GPU's
# upstream bridge, normally 0000:00:01.0), PHYS_CLAIM_LOGDIR.

set -u

HERE="$(dirname "$(readlink -f "$0")")"
REPO_ROOT="$(readlink -f "$HERE/../..")"

USE_YES=0
HOLD=30
CYCLES=1
LAYOUT=0
DRYRUN=0
FWSEC=0
BOOT=0
BOOT_DRYRUN=0
KEEP=0
NO_RESTORE=0
KO="$REPO_ROOT/kernel/open-iov-vfio.ko"
FW=""

GPU_BDF="${GPU_BDF:-0000:01:00.0}"
if [ -z "${PORT_BDF:-}" ] && [ -e "/sys/bus/pci/devices/$GPU_BDF" ]; then
    PORT_BDF="$(basename "$(dirname "$(readlink -f "/sys/bus/pci/devices/$GPU_BDF")")")"
fi
PORT_BDF="${PORT_BDF:-0000:00:01.0}"

LOGDIR="${PHYS_CLAIM_LOGDIR:-/tmp/opencode}"
TS="$(date +%Y%m%d_%H%M%S)"
WATCHERS='pdwatch|watch_ctx|uvmt_poll|invd\.py|bpwait|sempages|track_waits|guest_pin|guest_pmap|vmrestart|push_guest_tools|nvidia-smi -l|dmesg -w'

usage() {
    awk '/^[^#]/{exit} {print}' "$0" | grep -E '^#( |$)' | sed 's/^# \{0,1\}//'
}

while [ $# -gt 0 ]; do
    case "$1" in
        --yes)        USE_YES=1 ;;
        --hold)       HOLD="${2:-}"; shift ;;
        --cycles)     CYCLES="${2:-}"; shift ;;
        --layout)     LAYOUT=1 ;;
        --dryrun)     DRYRUN=1; LAYOUT=1 ;;
        --boot-dryrun) BOOT_DRYRUN=1; LAYOUT=1; DRYRUN=1 ;;
        --boot)       BOOT=1; LAYOUT=1 ;;
        --keep)       KEEP=1 ;;
        --fwsec)
            cat >&2 <<'EOF'
--fwsec is RETIRED: FWSEC-only is a one-way door on this fused part.
A FWSEC-only carve left WPR2 up and was not host-recoverable (2026-10-01).
Never stop after FWSEC alone on this host. Use --boot (full boot lifecycle).
EOF
            exit 1 ;;
        --ko)         KO="${2:-}"; shift ;;
        --fw)         FW="${2:-}"; shift ;;
        --no-restore) NO_RESTORE=1 ;;
        -h|--help)    usage; exit 0 ;;
        *) echo "unknown argument: $1" >&2; usage >&2; exit 1 ;;
    esac
    shift
done

case "$HOLD" in ''|*[!0-9]*) echo "--hold must be a non-negative integer" >&2; exit 1 ;; esac
case "$CYCLES" in ''|*[!0-9]*|0) echo "--cycles must be a positive integer" >&2; exit 1 ;; esac
if [ "$KEEP" -eq 1 ] && [ "$CYCLES" -ne 1 ]; then
    echo "--keep leaves the claimed session running: single cycle only (--cycles 1)" >&2
    exit 1
fi
if [ "$DRYRUN" -eq 1 ] && [ "$FWSEC" -eq 1 ]; then
    echo "--dryrun and --fwsec are mutually exclusive" >&2
    exit 1
fi
if [ "$BOOT" -eq 1 ] && { [ "$DRYRUN" -eq 1 ] || [ "$BOOT_DRYRUN" -eq 1 ]; }; then
    echo "--boot is mutually exclusive with --dryrun/--boot-dryrun" >&2
    exit 1
fi
if [ "$HOLD" -lt 30 ]; then
    echo "WARN: --hold $HOLD is below the verified 30 s minimum" >&2
fi

MODE="none"
[ "$LAYOUT" -eq 1 ] && MODE="layout"
[ "$DRYRUN" -eq 1 ] && MODE="dryrun"
[ "$BOOT_DRYRUN" -eq 1 ] && MODE="boot-dryrun"
[ "$BOOT" -eq 1 ] && MODE="boot"

if [ "$USE_YES" -ne 1 ]; then
    cat >&2 <<'EOF'
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
HOST-FREEZE HAZARD - REFUSING TO RUN WITHOUT --yes

The physical claim unbinds the distribution NVIDIA driver from the dGPU.
On this host the board can then power-gate and fail to come back, hard-
freezing the system with no crash record (observed twice on 2026-09-30).
Run only when supervised, with other host work stopped and the PART 87
recovery plan ready. Re-run with --yes to acknowledge.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
EOF
    exit 1
fi

log()  { echo "[$(date +%H:%M:%S)] $*"; }
warn() { echo "[$(date +%H:%M:%S)] WARN: $*" >&2; }
pre_fail() { echo "PRECONDITION FAIL: $*" >&2; exit 3; }

INTERRUPTED=0
on_signal() {
    INTERRUPTED=1
    echo
    echo "ABORT: interrupted; leaving module/lock state as-is (no cleanup)." >&2
    exit 2
}
trap on_signal INT TERM

# ---------------------------------------------------------------- preflight

[ -c /dev/null ] || pre_fail "no /dev/null?"
if ! sudo -n true 2>/dev/null; then
    sudo -v 2>/dev/null || pre_fail "current user cannot sudo"
fi

[ -f "$KO" ] || pre_fail "module not found: $KO (build it or pass --ko)"
KO_HASH="$(sha256sum "$KO" | awk '{print $1}')"
KO_SRCVER="$(modinfo -F srcversion "$KO" 2>/dev/null || true)"
if [ -d "$REPO_ROOT/.git" ]; then
    HEAD_COMMIT="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)"
    log "preflight: repo HEAD ${HEAD_COMMIT:-unknown}"
    if git -C "$REPO_ROOT" status --porcelain -- kernel 2>/dev/null | grep -q .; then
        warn "kernel/ has uncommitted changes; HEAD does not describe $KO"
    fi
else
    HEAD_COMMIT=""
    warn "not a git checkout; cannot record HEAD"
fi
log "preflight: ko $KO sha256 $KO_HASH srcversion ${KO_SRCVER:-unknown}"
if [ "$KO" -ot "$REPO_ROOT/kernel/gsp_phys_gpu.c" ]; then
    warn "$KO is older than kernel/gsp_phys_gpu.c; rebuild before cycling"
fi

[ -e "/sys/bus/pci/devices/$GPU_BDF" ] || pre_fail "GPU $GPU_BDF not present"
[ -e "/sys/bus/pci/devices/$PORT_BDF" ] || pre_fail "root port $PORT_BDF not present"

if pgrep -f qemu-system >/dev/null 2>&1; then
    pgrep -af qemu-system >&2
    pre_fail "QEMU is running; stop the VM first"
fi

if compgen -G "/sys/bus/mdev/devices/*" >/dev/null 2>&1; then
    ls -d /sys/bus/mdev/devices/* >&2
    pre_fail "mdev devices exist; remove them first"
fi

WATCH_PIDS="$(pgrep -af "$WATCHERS" 2>/dev/null || true)"
if [ -n "$WATCH_PIDS" ]; then
    echo "$WATCH_PIDS" >&2
    pre_fail "stale watcher process(es) running; stop them first"
fi

if lsmod 2>/dev/null | awk '{print $1}' | grep -qx open_iov_vfio; then
    pre_fail "open_iov_vfio is already loaded; rmmod it first"
fi

command -v lspci >/dev/null 2>&1 || pre_fail "lspci not found"
command -v nvidia-smi >/dev/null 2>&1 || pre_fail "nvidia-smi not found"
command -v timeout >/dev/null 2>&1 || pre_fail "timeout not found"

sudo systemctl start nvidia-persistenced 2>/dev/null || true
for _ in $(seq 1 10); do
    systemctl is-active --quiet nvidia-persistenced 2>/dev/null && break
    sleep 1
done
systemctl is-active --quiet nvidia-persistenced 2>/dev/null \
    || pre_fail "nvidia-persistenced did not start"
if ! nvidia-smi -L >/dev/null 2>&1; then
    pre_fail "nvidia-smi -L failed with persistenced running"
fi
log "preflight: persistenced active, nvidia-smi OK, GPU=$GPU_BDF port=$PORT_BDF"

mkdir -p "$LOGDIR" || pre_fail "cannot create $LOGDIR"

# ---------------------------------------------------------------- helpers

save_dmesg() {
    sudo dmesg > "$1" 2>/dev/null || true
}

# Count rail-cycle lines strictly inside the locked window: after the first
# "power domain locked" line and before the first "power domain released"
# line (or the end of the log if the locks never released). Outside the
# window is normal RTD3.
in_window_rail_count() {
    local f="$1" first rel
    first="$(grep -n "power domain locked" "$f" 2>/dev/null | head -1 | cut -d: -f1)"
    [ -n "$first" ] || { echo 0; return; }
    rel="$(grep -n "power domain released" "$f" 2>/dev/null | head -1 | cut -d: -f1)"
    if [ -n "$rel" ]; then
        awk -v a="$first" -v b="$rel" \
            '/xHC error in resume|root hub lost power/ && NR > a && NR < b { n++ } END { print n+0 }' "$f"
    else
        awk -v a="$first" \
            '/xHC error in resume|root hub lost power/ && NR > a { n++ } END { print n+0 }' "$f"
    fi
}

rail_count() {
    grep -c -E "xHC error in resume|root hub lost power" "$1" 2>/dev/null || true
}

abort_signature() {
    grep -q -E "not ready .*after resume" "$1" 2>/dev/null && return 0
    grep -q -E "board unresponsive|gsp-phys.*WARN|WARNING:.*gsp-phys" "$1" 2>/dev/null && return 0
    return 1
}

wait_active() {
    local i
    for i in $(seq 1 10); do
        if [ "$(cat "/sys/bus/pci/devices/$GPU_BDF/power/runtime_status" 2>/dev/null)" = active ] &&
           [ "$(cat "/sys/bus/pci/devices/$PORT_BDF/power/runtime_status" 2>/dev/null)" = active ]; then
            return 0
        fi
        nvidia-smi -L >/dev/null 2>&1 || true
        sleep 1
    done
    return 1
}

set_power_control() {
    local bdf="$1" val="$2"
    sudo sh -c "echo '$val' > '/sys/bus/pci/devices/$bdf/power/control'" 2>/dev/null || return 1
    [ "$(cat "/sys/bus/pci/devices/$bdf/power/control" 2>/dev/null)" = "$val" ] || return 1
    return 0
}

pci_field() {
    lspci -k -s "$1" 2>/dev/null | sed -n "s/.*Kernel driver in use: //p" | head -1
}

abort_now() {
    local n="$1" dlog="$2" msg="$3"
    save_dmesg "$dlog"
    echo "ABORT: cycle $n: $msg" >&2
    echo "ABORT: state left as-is (no rmmod, no restore). Log: $dlog" >&2
    exit 2
}

# ---------------------------------------------------------------- cycles

ALL_GREEN=1
for n in $(seq 1 "$CYCLES"); do
    [ "$INTERRUPTED" -eq 0 ] || exit 2
    DLOG="$LOGDIR/phys_claim_cycle_${TS}_${n}.log"
    log "=== cycle $n/$CYCLES (mode=$MODE, hold ${HOLD}s, layout=$LAYOUT, ko=$(basename "$KO")) ==="

    sudo systemctl start nvidia-persistenced 2>/dev/null || true
    if ! wait_active; then
        pre_fail "cycle $n: GPU/port did not reach runtime_status=active"
    fi

    set_power_control "$GPU_BDF" on || pre_fail "cycle $n: cannot set GPU power/control=on"
    set_power_control "$PORT_BDF" on || pre_fail "cycle $n: cannot set port power/control=on"

    sudo dmesg -C 2>/dev/null || pre_fail "cycle $n: dmesg -C failed"

    LAYOUT_ARG=""
    [ "$LAYOUT" -eq 1 ] && LAYOUT_ARG="gsp_phys_layout=1"
    DRYRUN_ARG=""
    [ "$DRYRUN" -eq 1 ] && DRYRUN_ARG="gsp_phys_fwsec_dryrun=1"
    BOOTDRY_ARG=""
    [ "$BOOT_DRYRUN" -eq 1 ] && BOOTDRY_ARG="gsp_phys_boot_dryrun=1"
    BOOT_ARG=""
    [ "$BOOT" -eq 1 ] && BOOT_ARG="gsp_phys_boot=1"
    FW_ARG=""
    [ -n "$FW" ] && FW_ARG="gsp_phys_fw=$FW"
    # One command: stop persistenced and insmod with the D0 pre-lock in place.
    # gsp_phys_fwsec=1 is never passed: --fwsec refuses before this point.
    if ! { sudo systemctl stop nvidia-persistenced && sudo insmod "$KO" \
            gsp_phys_gpu=1 gsp_phys_hazard_ack=1 $LAYOUT_ARG $DRYRUN_ARG $BOOTDRY_ARG $BOOT_ARG $FW_ARG; }; then
        save_dmesg "$DLOG"
        echo "ABORT: cycle $n: stop-persistenced && insmod failed" >&2
        exit 2
    fi
    sleep 1
    save_dmesg "$DLOG"

    if grep -q "gsp-phys: refusing claim" "$DLOG" 2>/dev/null; then
        echo "PRECONDITION FAIL: cycle $n: module refused the claim (GPU/port were already runtime-suspended)" >&2
        warn "no claim was taken; unloading the module is safe"
        timeout 60 sudo rmmod open_iov_vfio 2>/dev/null || true
        save_dmesg "$DLOG"
        exit 3
    fi

    if abort_signature "$DLOG"; then
        abort_now "$n" "$DLOG" "hard abort signature after insmod"
    fi

    NR_LOCKS="$(grep -c "power domain locked" "$DLOG" 2>/dev/null || true)"
    OK_GPU_LOCK=0; grep -q "power domain locked: $GPU_BDF" "$DLOG" && OK_GPU_LOCK=1
    OK_PORT_LOCK=0; grep -q "power domain locked: $PORT_BDF" "$DLOG" && OK_PORT_LOCK=1
    OK_UNBIND=0;  grep -q "unbinding nvidia from" "$DLOG" && OK_UNBIND=1
    OK_CLAIM=0;   grep -q "physical GPU claimed" "$DLOG" && OK_CLAIM=1
    OK_BOARD=0;   grep -q "board check after claim.*id=10de:1ed3" "$DLOG" && OK_BOARD=1
    OK_STAGE3B=1
    if [ "$LAYOUT" -eq 1 ]; then
        OK_STAGE3B=0; grep -q "stage 3b complete" "$DLOG" && OK_STAGE3B=1
    fi
    OK_DRYRUN=1
    if [ "$DRYRUN" -eq 1 ]; then
        OK_DRYRUN=0
        if grep -q "gsp-phys-fwsec-dryrun:" "$DLOG" && \
           grep -q "stage 3c dry-run complete" "$DLOG"; then
            OK_DRYRUN=1
        fi
    fi
    OK_BOOTPRIME=1
    if [ "$BOOT_DRYRUN" -eq 1 ]; then
        OK_BOOTPRIME=0
        grep -q "stage 3d prime complete" "$DLOG" && OK_BOOTPRIME=1
    fi
    OK_BOOT=1
    OK_BOOT3E=0
    if [ "$BOOT" -eq 1 ]; then
        OK_BOOT=0
        if grep -q "stage 3d complete" "$DLOG"; then
            OK_BOOT=1
            if grep -q "stage 3e complete" "$DLOG"; then
                OK_BOOT3E=2
            elif grep -q "stage 3e incomplete" "$DLOG"; then
                OK_BOOT3E=1
            fi
        fi
    fi
    OK_FWSEC=1
    if [ "$FWSEC" -eq 1 ]; then
        OK_FWSEC=0
        grep -q "stage 3c complete" "$DLOG" && OK_FWSEC=1
    fi

    if [ "$NR_LOCKS" -eq 0 ] && [ "$OK_CLAIM" -eq 0 ]; then
        echo "ABORT: cycle $n: no locks and no claim took effect" >&2
        warn "no D0 locks were taken; unloading the module is safe"
        timeout 60 sudo rmmod open_iov_vfio 2>/dev/null || true
        save_dmesg "$DLOG"
        exit 2
    fi
    if [ "$OK_GPU_LOCK" -ne 1 ] || [ "$OK_PORT_LOCK" -ne 1 ] || \
       [ "$OK_UNBIND" -ne 1 ] || [ "$OK_CLAIM" -ne 1 ] || [ "$OK_BOARD" -ne 1 ]; then
        abort_now "$n" "$DLOG" \
            "claim patterns incomplete (locks=$NR_LOCKS gpu=$OK_GPU_LOCK port=$OK_PORT_LOCK unbind=$OK_UNBIND claim=$OK_CLAIM board=$OK_BOARD)"
    fi
    if [ "$NR_LOCKS" -ne 5 ]; then
        warn "cycle $n: expected 5 power domain locked lines, saw $NR_LOCKS"
        ALL_GREEN=0
    fi
    if [ "$LAYOUT" -eq 1 ] && [ "$OK_STAGE3B" -ne 1 ]; then
        warn "cycle $n: gsp_phys_layout=1 but no 'stage 3b complete' line"
    fi
    if [ "$DRYRUN" -eq 1 ] && [ "$OK_DRYRUN" -ne 1 ]; then
        warn "cycle $n: gsp_phys_fwsec_dryrun=1 but the read-only dry-run did not complete (missing 'gsp-phys-fwsec-dryrun:'/'stage 3c dry-run complete')"
    fi
    if [ "$BOOT_DRYRUN" -eq 1 ] && [ "$OK_BOOTPRIME" -ne 1 ]; then
        warn "cycle $n: gsp_phys_boot_dryrun=1 but 'stage 3d prime complete' is missing (staging rehearsal did not complete)"
    fi
    if [ "$BOOT" -eq 1 ]; then
        if [ "$OK_BOOT" -ne 1 ]; then
            warn "cycle $n: gsp_phys_boot=1 but 'stage 3d complete' is missing (boot lifecycle did not report success)"
        elif [ "$OK_BOOT3E" -eq 0 ]; then
            warn "cycle $n: gsp_phys_boot=1: neither 'stage 3e complete' nor 'stage 3e incomplete' present (RM-init status not reported)"
        elif [ "$OK_BOOT3E" -eq 1 ]; then
            warn "cycle $n: gsp_phys_boot=1: 'stage 3e incomplete' (RM-init not observed; stage 3d boot remains valid)"
        fi
    fi
    if [ "$FWSEC" -eq 1 ] && [ "$OK_FWSEC" -ne 1 ]; then
        warn "cycle $n: gsp_phys_fwsec=1 but 'stage 3c complete' is missing (FWSEC did not report success)"
    fi

    IN_RAIL_BEFORE="$(in_window_rail_count "$DLOG")"
    OUT_RAIL_BEFORE="$(( $(rail_count "$DLOG") - IN_RAIL_BEFORE ))"
    log "cycle $n: claimed and verified (locks=$NR_LOCKS, rail outside=$OUT_RAIL_BEFORE). Holding ${HOLD}s"
    sleep "$HOLD"

    save_dmesg "$DLOG"
    if abort_signature "$DLOG"; then
        abort_now "$n" "$DLOG" "hard abort signature during hold"
    fi

    LSPCI_NN="$(lspci -nn -s "$GPU_BDF" 2>/dev/null || true)"
    DRV="$(pci_field "$GPU_BDF")"
    RSTAT_GPU="$(cat "/sys/bus/pci/devices/$GPU_BDF/power/runtime_status" 2>/dev/null || echo '?')"
    RSTAT_PORT="$(cat "/sys/bus/pci/devices/$PORT_BDF/power/runtime_status" 2>/dev/null || echo '?')"
    HOLD_RED=0
    echo "$LSPCI_NN" | grep -q "10de:1ed3" || HOLD_RED=1
    [ "$DRV" = "open-iov-gsp-phys" ] || HOLD_RED=1
    [ "$RSTAT_GPU" = active ] && [ "$RSTAT_PORT" = active ] || HOLD_RED=1
    if [ "$HOLD_RED" -ne 0 ]; then
        abort_now "$n" "$DLOG" \
            "hold verification failed (lspci='$LSPCI_NN' driver='$DRV' gpu=$RSTAT_GPU port=$RSTAT_PORT)"
    fi

    if [ "$KEEP" -eq 1 ]; then
        save_dmesg "$DLOG"
        log "cycle $n: --keep: no rmmod, no release verification; the claimed session stays up"
        cat <<EOF
===============================================================================
--keep: the claimed session is INTENTIONALLY LEFT RUNNING.
  lspci: $LSPCI_NN
  driver=$DRV  gpu=$RSTAT_GPU  port=$RSTAT_PORT
  power/control=on (GPU $GPU_BDF and port $PORT_BDF)
  nvidia-persistenced stopped; open_iov_vfio loaded and holding the claim.
To end the claimed session:
  sudo rmmod open_iov_vfio
  echo auto | sudo tee /sys/bus/pci/devices/$GPU_BDF/power/control
  echo auto | sudo tee /sys/bus/pci/devices/$PORT_BDF/power/control
  sudo systemctl start nvidia-persistenced
Cycle log: $DLOG
===============================================================================
EOF
        log "VERDICT: cycle $n claimed, verified and left running (--keep, mode=$MODE, exit 0)"
        exit 0
    fi

    timeout 60 sudo rmmod open_iov_vfio >/dev/null 2>&1
    RMMOD_RC=$?
    sleep 1
    save_dmesg "$DLOG"
    if [ "$RMMOD_RC" -eq 124 ]; then
        abort_now "$n" "$DLOG" "rmmod timed out after 60s"
    fi
    if [ "$RMMOD_RC" -ne 0 ]; then
        abort_now "$n" "$DLOG" "rmmod failed (rc=$RMMOD_RC)"
    fi
    if abort_signature "$DLOG"; then
        abort_now "$n" "$DLOG" "hard abort signature after rmmod"
    fi

    OK_REBIND=0;  grep -q "rebound to nvidia (rc=1)" "$DLOG" && OK_REBIND=1
    OK_BEFORE=0;  grep -q "board check before rebind.*id=10de:1ed3" "$DLOG" && OK_BEFORE=1
    OK_TEARDOWN=1
    if [ "$FWSEC" -eq 1 ] || [ "$BOOT" -eq 1 ]; then
        OK_TEARDOWN=0
        grep -q "teardown complete: WPR2 down" "$DLOG" && OK_TEARDOWN=1
    fi
    NR_RELEASED="$(grep -c "power domain released" "$DLOG" 2>/dev/null || true)"
    if [ "$OK_REBIND" -ne 1 ] || [ "$NR_RELEASED" -ne 5 ]; then
        ALL_GREEN=0
        warn "cycle $n: release verification failed (rebind=$OK_REBIND released=$NR_RELEASED); locks may still be held - do not retry"
        save_dmesg "$DLOG"
        break
    fi
    if [ "$OK_BEFORE" -ne 1 ]; then
        warn "cycle $n: missing 'board check before rebind id=10de:1ed3' line"
        ALL_GREEN=0
    fi
    if { [ "$FWSEC" -eq 1 ] || [ "$BOOT" -eq 1 ]; } && [ "$OK_TEARDOWN" -ne 1 ]; then
        if [ "$BOOT" -eq 1 ]; then
            warn "cycle $n: gsp_phys_boot=1 but no 'teardown complete: WPR2 down' on release (WPR2 may still be up; do not retry)"
        else
            warn "cycle $n: gsp_phys_fwsec=1 but no 'teardown complete: WPR2 down' on release (WPR2 may still be up; do not retry)"
        fi
        ALL_GREEN=0
    fi

    if [ "$NO_RESTORE" -eq 0 ]; then
        set_power_control "$GPU_BDF" auto || warn "cycle $n: failed to restore GPU power/control=auto"
        set_power_control "$PORT_BDF" auto || warn "cycle $n: failed to restore port power/control=auto"
        sudo systemctl start nvidia-persistenced 2>/dev/null || true
        wait_active || warn "cycle $n: GPU/port not active after persistenced restart"
    else
        log "cycle $n: --no-restore: leaving power/control=on and persistenced stopped"
    fi

    HEALTH_RED=0
    nvidia-smi -L >/dev/null 2>&1 || HEALTH_RED=1
    [ "$(pci_field "$GPU_BDF")" = "nvidia" ] || HEALTH_RED=1
    if [ "$NO_RESTORE" -eq 0 ]; then
        [ "$(cat "/sys/bus/pci/devices/$GPU_BDF/power/control" 2>/dev/null)" = auto ] || HEALTH_RED=1
        [ "$(cat "/sys/bus/pci/devices/$PORT_BDF/power/control" 2>/dev/null)" = auto ] || HEALTH_RED=1
    fi
    [ "$(cat "/sys/bus/pci/devices/$GPU_BDF/d3cold_allowed" 2>/dev/null)" = 1 ] || HEALTH_RED=1

    IN_RAIL="$(in_window_rail_count "$DLOG")"
    OUT_RAIL="$(( $(rail_count "$DLOG") - IN_RAIL ))"
    if [ "$IN_RAIL" -gt 0 ]; then
        warn "cycle $n: RED - $IN_RAIL rail-cycle line(s) INSIDE the locked window (lock failed)"
        ALL_GREEN=0
    fi
    if [ "$OUT_RAIL" -gt 0 ]; then
        warn "cycle $n: normal RTD3 rail cycle outside the locked window ($OUT_RAIL line(s))"
    fi

    if [ "$HEALTH_RED" -ne 0 ]; then
        ALL_GREEN=0
        warn "cycle $n: health check failed (nvidia-smi/driver/control/d3cold)"
    fi
    if [ "$OK_STAGE3B" -ne 1 ]; then
        ALL_GREEN=0
    fi
    if [ "$OK_DRYRUN" -ne 1 ]; then
        ALL_GREEN=0
    fi
    if [ "$OK_BOOTPRIME" -ne 1 ]; then
        ALL_GREEN=0
    fi
    if [ "$OK_BOOT" -ne 1 ]; then
        ALL_GREEN=0
    fi
    if [ "$BOOT" -eq 1 ] && [ "$OK_BOOT3E" -eq 0 ]; then
        ALL_GREEN=0
    fi
    if [ "$OK_FWSEC" -ne 1 ]; then
        ALL_GREEN=0
    fi

    if [ "$ALL_GREEN" -eq 1 ] && [ "$IN_RAIL" -eq 0 ] && [ "$HEALTH_RED" -eq 0 ]; then
        log "cycle $n: GREEN (locks=$NR_LOCKS, released=$NR_RELEASED, in-window rail=0, outside rail=$OUT_RAIL, hold=${HOLD}s)"
    else
        log "cycle $n: RED (locks=$NR_LOCKS, released=$NR_RELEASED, in-window rail=$IN_RAIL, outside rail=$OUT_RAIL, health_ok=$((1-HEALTH_RED)))"
        if [ "$IN_RAIL" -gt 0 ]; then
            echo "ABORT: cycle $n: rail cycle inside the locked window; not retrying" >&2
            save_dmesg "$DLOG"
            exit 2
        fi
        break
    fi
done

if [ "$ALL_GREEN" -eq 1 ]; then
    log "VERDICT: all $CYCLES cycle(s) green (mode=$MODE, exit 0)"
    exit 0
fi
echo "VERDICT: not green (mode=$MODE); do not retry in this boot (exit 2)" >&2
exit 2