From 148fe71469330894f490c2c10dac2538f0e34edf Mon Sep 17 00:00:00 2001 From: alice Date: Sun, 13 Sep 2026 19:49:25 +0000 Subject: [PATCH 1/2] vms-387: anchor test_kmod_devtab_terminal via devtab-terminal-withdrawal-not-honored test_kmod_devtab_terminal was an in-scope negctl suite with NO can-fail anchor (coverage-gate FAIL). It exercises the dynamic RTAn: terminal unit (vms-f881): mint an executive-real, $GETDVI-visible, $ASSIGN-able terminal, PTY round-trip, survive owner death, and WITHDRAW it when the session ends. New defect neuters the teardown gate: vms_devtab_remove_terminal() unlinks only a genuine dynamic terminal (`if (dynamic_term) exec_list_del(...)` then `if (!dynamic_term) return -ENODEV`). Guarding the gate to `if (0 && dynamic_term)` makes withdrawal a permanent no-op -- the RTAn: row is never unlinked and the call returns -ENODEV, so a minted terminal leaks and $GETDVI keeps resolving RTA0: after the session ended. Isolated by construction: the MINT side is untouched (row still created with dynamic_term=1, DC$_TERM, OPA0-shape devchar), so every creation/characteristics/ $ASSIGN-owner/PTY-round-trip assertion stays green; the OPA0: (console) removal-refused check stays green (console is not dynamic_term, correctly still refused). Only the two withdrawal assertions redden: require_fail: vms_devtab_remove_terminal("RTA0:") withdraws the unit it minted knock_on: RTA0: no longer exists after withdrawal - targets kernel-core/vms_devtab.c; anchor `if (dynamic_term)` is unique. - /* negctl */ source marker added above the withdrawal CHECK (pairing floor). - floor 150 -> 151. selftest: injects + no-op-reapply has teeth; every require_fail/knock_on text exists literally in source; test_kmod_devtab_terminal cleared from both coverage FAIL lists. Runtime exact-red set proven separately via negctl-adhoc before flag. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HMDFjHCqxDuXgbyxNU572h --- tests/qemu/facility_defects.sh | 28 ++++++++++++++++++++++++++ tests/qemu/facility_defects_floor.txt | 5 ++++- tests/qemu/test_kmod_devtab_terminal.c | 4 ++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/qemu/facility_defects.sh b/tests/qemu/facility_defects.sh index 31a93b74a..c6f5817ea 100755 --- a/tests/qemu/facility_defects.sh +++ b/tests/qemu/facility_defects.sh @@ -551,6 +551,7 @@ getsyi-csid-reported-without-valid fork-work-dispatch-uncounted fork-worker-start-reports-success-unstarted devtab-io-error-not-charged +devtab-terminal-withdrawal-not-honored setexit-status-not-recorded spawn-notify-flag-not-set register-subprocess-identity-self-declared @@ -837,6 +838,26 @@ EOF knock_on_why) echo "the same dropped increment means the count never leaves zero across BOTH injected failures, so the second-failure assertion (which checks it moved to +2) reddens identically, and \$DEVICE_SCAN's projection of that same field (cmd_show_error's own reader) reports the same wrong (zero) value.";; esac;; + devtab-terminal-withdrawal-not-honored) + case "$_f" in + facility) echo "dynamic RTAn: terminal-unit teardown -- vms_devtab_remove_terminal() withdraws the executive-real terminal device SET TERMINAL/network login minted (vms-f881), so the RTAn: unit and its \$GETDVI visibility go away when the session ends";; + targets) echo "kernel-core/vms_devtab.c";; + suites_red) echo "test_kmod_devtab_terminal";; + blind_suites) echo "";; + blind_why) echo "";; + isolation) echo "isolated";; + why) echo "vms_devtab_remove_terminal() gates the unlink on the row being a genuine dynamic terminal (\`if (dynamic_term) exec_list_del(&dev->list);\` then \`if (!dynamic_term) return -ENODEV;\`). Guarding that gate to \`if (0 && dynamic_term)\` makes the withdrawal a permanent no-op: the RTAn: row is never unlinked and the call returns -ENODEV, so a minted terminal can never be torn down -- the executive leaks the unit and \$GETDVI keeps resolving RTA0: after the session ended. The MINT side is untouched (the row is still created with dynamic_term=1, DC\$_TERM class, OPA0-shape devchar), so every creation/characteristics/\$ASSIGN-owner/PTY-round-trip assertion stays green; and the OPA0: (console) removal-refused check stays green because the console is not dynamic_term and is correctly still refused. Only the two withdrawal assertions redden. The original \`if (dynamic_term)\` text is gone after substitution (no-op re-apply, selftest).";; + require_fail) cat <<'EOF' +vms_devtab_remove_terminal("RTA0:") withdraws the unit it minted +EOF + ;; + knock_on_fail) cat <<'EOF' +RTA0: no longer exists after withdrawal +EOF + ;; + knock_on_why) echo "one gate, two dependent observations: the neutered \`if (0 && dynamic_term)\` makes vms_devtab_remove_terminal return -ENODEV without unlinking, so the withdrawal call itself fails (require_fail) AND the follow-up \$GETDVI on RTA0: still resolves the leaked row instead of SS\$_NOSUCHDEV (knock_on). Every other assertion in the suite reads a path this gate does not touch -- the mint, the DC\$_TERM/devchar/width/page characteristics, the unowned->owned \$ASSIGN transition, the PTY byte round-trip (its backing is reported off the still-set dynamic_term flag at the GETPTY handler), the survives-owner-death release, and the OPA0: removal-refused check (the console is correctly refused either way) -- and all stay green.";; + esac;; + setexit-status-not-recorded) case "$_f" in facility) echo "\$EXIT/\$STATUS cross-process readback (VMS_IOCTL_GETEXIT by VMS PID / by Linux pid, vms-707/vms-e9a)";; @@ -6639,6 +6660,13 @@ apply_edit() { # Unique increment (vms_devtab_note_io_error's own errcnt bump); dropping # it leaves the counter unmoved. Gone after apply (no-op re-apply). sed -i 's|dev->errcnt++;|/* NEGCTL devtab-io-error-not-charged: increment dropped */|' "$_file";; + devtab-terminal-withdrawal-not-honored) + # UNIQUE TEXT: `if (dynamic_term)` occurs once, in + # vms_devtab_remove_terminal()'s unlink gate. Guarding it to + # `if (0 && dynamic_term)` makes the unlink a no-op, so the call returns + # -ENODEV and the RTAn: row is never withdrawn. The original text is gone + # after substitution (no-op re-apply, selftest). + sed -i 's| if (dynamic_term)| if (0 \&\& dynamic_term) /* NEGCTL devtab-terminal-withdrawal-not-honored */|' "$_file";; setexit-status-not-recorded) # Unique line (vms_ioctl_getexit's shared post-switch read); narrowed to # a ternary that passes SEL_SELF through and masks every cross-process diff --git a/tests/qemu/facility_defects_floor.txt b/tests/qemu/facility_defects_floor.txt index a7a0958a0..77bfe3980 100644 --- a/tests/qemu/facility_defects_floor.txt +++ b/tests/qemu/facility_defects_floor.txt @@ -466,4 +466,7 @@ # Raised to 150 by rms-dirfind-exact-version-ignored (vms-b8a2 unblocked the # ';N'-bearing anchor text for test_syssvc_rms_workload's A4/A6 assertions; # see its entry in facility_defects.sh for what it covers and why). -150 +# Raised to 151 by devtab-terminal-withdrawal-not-honored (vms-387: re-anchor +# test_kmod_devtab_terminal, previously an in-scope suite with no negctl anchor; +# neuters vms_devtab_remove_terminal's dynamic_term unlink gate). +151 diff --git a/tests/qemu/test_kmod_devtab_terminal.c b/tests/qemu/test_kmod_devtab_terminal.c index a9b0aae70..9be20329c 100644 --- a/tests/qemu/test_kmod_devtab_terminal.c +++ b/tests/qemu/test_kmod_devtab_terminal.c @@ -360,6 +360,10 @@ int main(int argc, char **argv) status = vms_kif_getdvi_devnam(CONSOLE_DEV, &info); CHECK(status == SS_NORMAL, "OPA0: is untouched by the refused removal"); + /* The withdrawal rides vms_devtab_remove_terminal()'s dynamic_term unlink + * gate; neuter that gate and the minted RTAn: row can never be torn down, + * so this call and the follow-up SS_NOSUCHDEV check both redden. */ + /* negctl: devtab-terminal-withdrawal-not-honored */ CHECK(write_param(REMOVE_PARAM, RTA_DEV) == 0, "vms_devtab_remove_terminal(\"RTA0:\") withdraws the unit it minted"); memset(&info, 0, sizeof(info)); From d7ceb6452e6fea91535bf824e733ba99e8c052e5 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 14 Sep 2026 13:43:10 +0000 Subject: [PATCH 2/2] vms-387: fix devtab-terminal injection -- non-fatal (dynamic_term=0), was rc=139 use-after-free The runtime proof caught that the first injection was FATAL, not a clean redden: guarding `if (dynamic_term)` to `if (0 && dynamic_term)` skipped the unlink but still fell through to exec_free() on a still-linked node (the real dynamic_term was still 1 for RTA0:, so the `if (!dynamic_term) return -ENODEV` guard did not fire) -- a use-after-free that SIGSEGV'd the guest (SUITE ... rc=139) and left 111 subsequent suites NEVER RAN. Corrected: force the LOCAL classification instead -- `dynamic_term = 0;`. Now remove treats every row as non-dynamic: the unlink is skipped AND `if (!dynamic_term) return -ENODEV` fires BEFORE exec_free(), so the row is neither unlinked nor freed. Clean no-op withdrawal, guest survives, only the two withdrawal assertions redden. Same isolation (OPA0: is genuinely non-dynamic and still correctly refused; mint/characteristics/$ASSIGN/PTY all untouched). selftest: injects + no-op-reapply has teeth; every require_fail/knock_on text exists literally in source. Runtime re-proven non-fatal before flagging. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/qemu/facility_defects.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/qemu/facility_defects.sh b/tests/qemu/facility_defects.sh index c6f5817ea..2892fe73b 100755 --- a/tests/qemu/facility_defects.sh +++ b/tests/qemu/facility_defects.sh @@ -846,7 +846,7 @@ EOF blind_suites) echo "";; blind_why) echo "";; isolation) echo "isolated";; - why) echo "vms_devtab_remove_terminal() gates the unlink on the row being a genuine dynamic terminal (\`if (dynamic_term) exec_list_del(&dev->list);\` then \`if (!dynamic_term) return -ENODEV;\`). Guarding that gate to \`if (0 && dynamic_term)\` makes the withdrawal a permanent no-op: the RTAn: row is never unlinked and the call returns -ENODEV, so a minted terminal can never be torn down -- the executive leaks the unit and \$GETDVI keeps resolving RTA0: after the session ended. The MINT side is untouched (the row is still created with dynamic_term=1, DC\$_TERM class, OPA0-shape devchar), so every creation/characteristics/\$ASSIGN-owner/PTY-round-trip assertion stays green; and the OPA0: (console) removal-refused check stays green because the console is not dynamic_term and is correctly still refused. Only the two withdrawal assertions redden. The original \`if (dynamic_term)\` text is gone after substitution (no-op re-apply, selftest).";; + why) echo "vms_devtab_remove_terminal() classifies the row it looked up (\`dynamic_term = (dev->dynamic_term != 0);\`) and then, only for a genuine dynamic terminal, unlinks it (\`if (dynamic_term) exec_list_del(&dev->list);\`) and frees it, else returns -ENODEV. Forcing that LOCAL classification to 0 (\`dynamic_term = 0;\`) makes remove treat EVERY row as non-dynamic: the unlink is skipped and the call returns -ENODEV before exec_free(), so a minted RTAn: terminal can never be torn down -- the executive leaks the unit and \$GETDVI keeps resolving RTA0: after the session ended. This is a CLEAN no-op withdrawal, not a use-after-free: the row is neither unlinked nor freed, so the guest survives and only the withdrawal assertions redden. (Guarding the \`if (dynamic_term)\` gate instead would skip the unlink but still reach exec_free() on a still-linked node -> rc=139 SIGSEGV, a fatal defect -- rejected for that reason.) The MINT side is untouched (the row is still created with dynamic_term=1, DC\$_TERM class, OPA0-shape devchar), so every creation/characteristics/\$ASSIGN-owner/PTY-round-trip assertion stays green; and the OPA0: (console) removal-refused check stays green because the console is genuinely non-dynamic and is correctly still refused. Only the two withdrawal assertions redden. The original \`dynamic_term = (dev->dynamic_term != 0);\` text is gone after substitution (no-op re-apply, selftest).";; require_fail) cat <<'EOF' vms_devtab_remove_terminal("RTA0:") withdraws the unit it minted EOF @@ -6661,12 +6661,17 @@ apply_edit() { # it leaves the counter unmoved. Gone after apply (no-op re-apply). sed -i 's|dev->errcnt++;|/* NEGCTL devtab-io-error-not-charged: increment dropped */|' "$_file";; devtab-terminal-withdrawal-not-honored) - # UNIQUE TEXT: `if (dynamic_term)` occurs once, in - # vms_devtab_remove_terminal()'s unlink gate. Guarding it to - # `if (0 && dynamic_term)` makes the unlink a no-op, so the call returns - # -ENODEV and the RTAn: row is never withdrawn. The original text is gone - # after substitution (no-op re-apply, selftest). - sed -i 's| if (dynamic_term)| if (0 \&\& dynamic_term) /* NEGCTL devtab-terminal-withdrawal-not-honored */|' "$_file";; + # UNIQUE TEXT: `dynamic_term = (dev->dynamic_term != 0);` occurs once, in + # vms_devtab_remove_terminal(). Forcing the LOCAL classification to 0 + # makes remove treat EVERY row as non-dynamic: `if (dynamic_term)` skips + # the exec_list_del (no unlink) and `if (!dynamic_term)` returns -ENODEV + # BEFORE exec_free() -- a clean no-op withdrawal, NOT a use-after-free. + # (Guarding the `if (dynamic_term)` gate instead would skip the unlink but + # still fall through to exec_free() on a still-linked node -> rc=139 SIGSEGV, + # a FATAL defect that kills the guest; this local-classification form keeps + # it non-fatal so only the two withdrawal assertions redden.) The original + # text is gone after substitution (no-op re-apply, selftest). + sed -i 's| dynamic_term = (dev->dynamic_term != 0);| dynamic_term = 0; /* NEGCTL devtab-terminal-withdrawal-not-honored: remove classifies every row non-dynamic -> -ENODEV, no unlink/free */|' "$_file";; setexit-status-not-recorded) # Unique line (vms_ioctl_getexit's shared post-switch read); narrowed to # a ternary that passes SEL_SELF through and masks every cross-process