root/src/dps8/dps8_mgp.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. mgp_show_nunits
  2. mgp_set_nunits
  3. mgp_show_device_name
  4. mgp_set_device_name
  5. mgp_show_socket_path
  6. mgp_set_socket_path
  7. mgp_reset
  8. mgpAttach
  9. mgpDetach
  10. mgp_init
  11. mgp_init_dev_state
  12. ncp_close_cb
  13. ncp_alloc_cb
  14. ncp_read_cb
  15. ncp_connect_cb
  16. ncp_connect
  17. ncp_write_cb
  18. ncp_send_packet
  19. ncp_recv_packet
  20. pkt8_to_word36
  21. word36_to_pkt8
  22. get_ddcw
  23. cmd_name
  24. dumppkt
  25. mgp_validate_dcw_state
  26. mgp_cmd
  27. mgp_iom_cmd
  28. mgp_check_dma_ptw
  29. mgp_process_event

   1 /*
   2  * vim: filetype=c:tabstop=4:ai:expandtab
   3  * SPDX-License-Identifier: ICU
   4  * scspell-id: 35c9cf5c-ebbf-11ed-8d34-80ee73e9b8e7
   5  *
   6  * ---------------------------------------------------------------------------
   7  *
   8  * Copyright (c) 2007-2013 Michael Mondy
   9  * Copyright (c) 2015-2018 Charles Anthony
  10  * Copyright (c) 2023 Björn Victor
  11  * Copyright (c) 2026 Eric Swenson
  12  * Copyright (c) 2026 Jeffrey H. Johnson
  13  * Copyright (c) 2021-2026 The DPS8M Development Team
  14  *
  15  * This software is made available under the terms of the ICU License.
  16  * See the LICENSE.md file at the top-level directory of this distribution.
  17  *
  18  * ---------------------------------------------------------------------------
  19  */
  20 
  21 // This is a thin shim that passes MGP packets between the IOM (Multics)
  22 // and an external NCP process (multics_ncp) via a Unix domain socket.
  23 //
  24 // The NCP process handles all Chaosnet protocol logic: connection management,
  25 // flow control, retransmission, and endpoint ID mapping.
  26 //
  27 // Communication protocol with the NCP:
  28 //   - Single bidirectional Unix domain socket (default: /tmp/mgp_ncp)
  29 //   - Length-prefixed framing: [2-byte BE length][MGP packet in 8-bit format]
  30 //   - The shim converts between Multics 36-bit/9-bit IOM words and 8-bit bytes
  31 //
  32 // Write (Multics -> NCP): Read 36-bit words from IOM, convert to 8-bit,
  33 //   send length-prefixed to NCP, return IOM_CMD_DISCONNECT (terminate).
  34 //
  35 // Read (NCP -> Multics): Set want_to_read flag, return IOM_CMD_PENDING.
  36 //   In mgp_process_event(), poll NCP socket non-blocking; if data available,
  37 //   read length-prefixed packet, convert 8-bit to 36-bit, write to IOM,
  38 //   send marker interrupt.
  39 
  40 #include <stdio.h>
  41 #include <stdarg.h>
  42 #include <ctype.h>
  43 #include <unistd.h>
  44 #include <stdint.h>
  45 #include <errno.h>
  46 #include <fcntl.h>
  47 
  48 #include <sys/types.h>
  49 #include <time.h>
  50 #include <sys/time.h>
  51 
  52 #include "dps8.h"
  53 #include "dps8_sir.h"
  54 #include "dps8_iom.h"
  55 #include "dps8_mgp.h"
  56 #include "dps8_sys.h"
  57 #include "dps8_cable.h"
  58 #include "dps8_cpu.h"
  59 #include "dps8_faults.h"
  60 #include "dps8_scu.h"
  61 #include "dps8_utils.h"
  62 
  63 #include <uv.h>
  64 
  65 #if defined(THREADZ) || defined(LOCKLESS)
  66 # include "threadz.h"
  67 #endif
  68 
  69 #if defined(NO_LOCALE)
  70 # define xstrerror_l strerror
  71 #endif
  72 
  73 #if defined(FREE)
  74 # undef FREE
  75 #endif /* if defined(FREE) */
  76 #define FREE(p) do  \
  77   {                 \
  78     free((p));      \
  79     (p) = NULL;     \
  80   } while(0)
  81 
  82 #if defined(WITH_MGP_DEV)
  83 
  84 # define DBG_CTR  1
  85 
  86 // Path to NCP process Unix socket (configurable via SET MGP SOCKET=<path>)
  87 # define NCP_SOCKET_PATH_DEFAULT  "/tmp/mgp_ncp"
  88 # define NCP_SOCKET_PATH_MAX      108
  89 
  90 static char ncp_socket_path[NCP_SOCKET_PATH_MAX] = NCP_SOCKET_PATH_DEFAULT;
  91 
  92 // Number of words in an MGP packet header (4 words x 36 bits = 16 x 9-bit bytes)
  93 # define MGP_PACKET_HEADER_SIZE  4
  94 
  95 // Maximum data bytes in an MGP packet
  96 # define MGP_MAX_DATA  488
  97 
  98 // Frame header size for NCP communication (2-byte big-endian length prefix)
  99 # define NCP_FRAME_HEADER_SIZE  2
 100 
 101 // Maximum 8-bit packet size (16 header + 488 data)
 102 # define MAX_PKT_BYTES  (16 + MGP_MAX_DATA)
 103 
 104 /* Maximum plausible DDCW_TALLY for any MGP channel buffer, in 36-bit words.
 105  * The read channel uses data_size=129 and the write channel uses data_size=128
 106  * (set by initialize_workspace in mgp_read_dcm_.pl1 / mgp_write_dcm_.pl1).
 107  * MAX_PKT_BYTES/4 = 126 words for the payload plus ~4 header words ~= 130 max.
 108  * We use 256 as a generous upper bound so this check remains valid even if the
 109  * Multics buffer size is changed, as long as it stays within one IOM page
 110  * (the ioi_ workspace is 2000 octal = 1024 words = one page).
 111  * TALLY=0 (IOM convention for 4096) and TALLY > MGP_MAX_TALLY both indicate
 112  * DCW corruption; see the validation checks in mgp_cmd cases 001 and 011.  */
 113 # define MGP_MAX_TALLY  256
 114 
 115 /* Backoff after a PTW failure: do not reconnect for this many seconds.
 116  * When Multics's memory manager pages out the MGP daemon's IOM DMA buffer
 117  * pages after overnight idle, the PTW check fails.  Without a backoff the
 118  * terminate-interrupt -> mgp_cmd(001) -> ncp_connect cycle repeats every
 119  * ~60 ms (too fast for the memory manager to page the data back in).
 120  * The backoff check lives in ncp_connect() so it catches ALL reconnect
 121  * paths, including ncp_send_packet() (Multics WRITE cmd) which previously
 122  * bypassed the per-event check and caused ~91 ms reconnect cycles.     */
 123 # define PTW_BACKOFF_SECS  15
 124 
 125 /* Timeout before declaring the channel "masked+stuck" and forcing recovery.
 126  * See masked_since in mgp_dev_state and the masked-channel check in
 127  * mgp_process_event for details.                                            */
 128 # define MASKED_STUCK_TIMEOUT_SECS  30
 129 
 130 /* Timeout before releasing an IOM channel that has been waiting for the NCP
 131  * to connect.  Keeps the channel from staying in IOM_CMD_PENDING indefinitely
 132  * when multics_ncp is not running.                                           */
 133 # define NCP_ABSENT_TIMEOUT_SECS    30
 134 
 135 /* Timeout before declaring "no forward progress" and forcing a hard socket
 136  * reset, independent of the masked/want_to_read state.
 137  *
 138  * MASKED_STUCK_TIMEOUT_SECS (above) only accumulates while the channel is
 139  * continuously seen masked across successive mgp_process_event() calls.
 140  * ioi_masked$timer's mask_channel (Multics's own channel-timeout handler,
 141  * ioi_masked.pl1) both masks the channel AND immediately issues a dummy
 142  * "unmask connect" (reset-status IDCW) in the same call when the device is
 143  * multiplexed.  That reconnect can flip want_to_read back to 1 for one
 144  * mgp_process_event() tick, which resets masked_since to 0 (see the
 145  * "channel is not masked" branch below) before MASKED_STUCK_TIMEOUT_SECS is
 146  * ever reached -> so a channel that is genuinely wedged in a repeating
 147  * mask/unmask timeout loop (observed at ~4s intervals against Multics's
 148  * short mgpr device read timeout) can defeat that safety net indefinitely.
 149  *
 150  * This second, independent watchdog tracks wall-clock time since the last
 151  * *successfully delivered* packet (last_progress_time), updated only when a
 152  * packet is actually written into Multics's workspace.  It is immune to the
 153  * masked/unmask flicker because it does not depend on observing !masked in
 154  * between: it fires purely on elapsed time without any real delivery.      */
 155 # define NO_PROGRESS_TIMEOUT_SECS   60
 156 
 157 /* Minimum valid DDCW_ADDR for the MGP read channel workspace.
 158  *
 159  * The read channel workspace layout (mgp_read_dcm_.pl1, buffer_size=6,
 160  * data_size=129):
 161  *   offsets  0-11: DCW list (6 x IDCW + 6 x DDCW)
 162  *   offset     12: TDCW (circular wrap; DATA_ADDRESS=0)
 163  *   offset     13: reset_idcw
 164  *   offsets 14-21: status_queue (4 x istat = 8 words)
 165  *   offset  22+ : buffer(0..5) data areas (129 words each)
 166  *
 167  * Any DDCW_ADDR < 22 is invalid (it points into the DCW list or control
 168  * structures, not into a data buffer).  DDCW_ADDR=0 in particular is set
 169  * by iom_list_service when it processes the TDCW at offset 12 via
 170  * unpack_DCW, which stores the TDCW's DATA_ADDRESS field (=0, all zero
 171  * bits) into p->DDCW_ADDR.
 172  */
 173 # define MGP_FIRST_BUFFER_OFFSET    22
 174 
 175 static void mgp_init_dev_state(void);
 176 
 177 # if defined(TESTING)
 178 static void dumppkt(char *hdr, word36 *buf, uint words);
 179 # endif
 180 
 181 struct mgp_dev_state
 182 {
 183   uv_pipe_t *pipe;             /* connected pipe to NCP process              */
 184   u_char in_buffer[MAX_PKT_BYTES * 2]; /* libuv read buffer                  */
 185   int in_buffer_len;           /* bytes buffered                             */
 186   u_char want_to_read;         /* flag: Multics has a pending read           */
 187   uint read_unit_idx;          /* saved IOM unit index for pending read      */
 188   uint read_unit_chan;         /* saved IOM channel for pending read         */
 189   u_char delivery_succeeded;   /* set by mgp_cmd(READ) when IOM accepts pkt  */
 190   time_t want_to_read_since;   /* wall-clock time when want_to_read was set  */
 191   time_t ptw_failed_at;        /* wall-clock time of last PTW check failure  */
 192   time_t masked_since;         /* wall-clock time channel first seen masked while connected */
 193   time_t last_progress_time;   /* wall-clock time of last successful packet delivery to Multics;
 194                                    0 means "not yet established" (lazily initialized)         */
 195 } mgp_dev_state;
 196 
 197 static struct mgp_state
 198   {
 199     char device_name[MAX_DEV_NAME_LEN];
 200   } mgp_state[N_MGP_UNITS_MAX];
 201 
 202 # define N_MGP_UNITS  2 // default
 203 
 204 # define UNIT_FLAGS \
 205         ( UNIT_FIX | UNIT_ATTABLE | UNIT_ROABLE | UNIT_DISABLE | UNIT_IDLE )
 206 
 207 UNIT mgp_unit[N_MGP_UNITS_MAX] = {
 208   {
 209     UDATA(NULL, UNIT_FLAGS, 0),
 210     0,   0,   0,   0,   0,
 211     NULL,  NULL,  NULL,  NULL
 212   }
 213 };
 214 
 215 # define MGP_UNIT_IDX(uptr)  (( uptr ) - mgp_unit )
 216 
 217 static DEBTAB mgp_dt[] = {
 218      { "NOTIFY", DBG_NOTIFY, NULL },
 219      { "INFO",   DBG_INFO,   NULL },
 220      { "ERR",    DBG_ERR,    NULL },
 221      { "WARN",   DBG_WARN,   NULL },
 222      { "DEBUG",  DBG_DEBUG,  NULL },
 223      { "ALL",    DBG_ALL,    NULL }, // Don't move as it messes up DBG message
 224      { NULL,     0,          NULL }
 225 };
 226 
 227 static t_stat
 228 mgp_show_nunits(UNUSED FILE *st, UNUSED UNIT *uptr, UNUSED int val,
     /* [previous][next][first][last][top][bottom][index][help] */
 229                 UNUSED const void *desc)
 230 {
 231   sim_printf("Number of MGP units in system is %d\r\n", mgp_dev.numunits);
 232 
 233   return SCPE_OK;
 234 }
 235 
 236 static t_stat
 237 mgp_set_nunits(UNUSED UNIT *uptr, UNUSED int32 value, const char *cptr,
     /* [previous][next][first][last][top][bottom][index][help] */
 238                UNUSED void *desc)
 239 {
 240   if (!cptr)
 241     {
 242       return SCPE_ARG;
 243     }
 244 
 245   int n = atoi(cptr);
 246   if (n < 1 || n > N_MGP_UNITS_MAX)
 247     {
 248       return SCPE_ARG;
 249     }
 250 
 251   mgp_dev.numunits = (uint32)n;
 252 
 253   return SCPE_OK;
 254 }
 255 
 256 static t_stat
 257 mgp_show_device_name(UNUSED FILE *st, UNIT *uptr, UNUSED int val,
     /* [previous][next][first][last][top][bottom][index][help] */
 258                      UNUSED const void *desc)
 259 {
 260   int n = (int)MGP_UNIT_IDX(uptr);
 261 
 262   if (n < 0 || n >= N_MGP_UNITS_MAX)
 263     {
 264       return SCPE_ARG;
 265     }
 266 
 267   if (mgp_state[n].device_name[1] != 0)
 268     {
 269       sim_printf("name     : %s", mgp_state[n].device_name);
 270     }
 271   else
 272     {
 273       sim_printf("name     : MGP%d", n);
 274     }
 275 
 276   return SCPE_OK;
 277 }
 278 
 279 static t_stat
 280 mgp_set_device_name(UNIT *uptr, UNUSED int32 value, const char *cptr,
     /* [previous][next][first][last][top][bottom][index][help] */
 281                     UNUSED void *desc)
 282 {
 283   int n = (int)MGP_UNIT_IDX(uptr);
 284 
 285   if (n < 0 || n >= N_MGP_UNITS_MAX)
 286     {
 287       return SCPE_ARG;
 288     }
 289 
 290   if (cptr)
 291     {
 292       strncpy(mgp_state[n].device_name, cptr, MAX_DEV_NAME_LEN - 1);
 293       mgp_state[n].device_name[MAX_DEV_NAME_LEN - 1] = 0;
 294     }
 295   else
 296     {
 297       mgp_state[n].device_name[0] = 0;
 298     }
 299 
 300   return SCPE_OK;
 301 }
 302 
 303 static t_stat
 304 mgp_show_socket_path(UNUSED FILE *st, UNUSED UNIT *uptr, UNUSED int val,
     /* [previous][next][first][last][top][bottom][index][help] */
 305                      UNUSED const void *desc)
 306 {
 307   sim_printf("NCP socket path: %s\r\n", ncp_socket_path);
 308   return SCPE_OK;
 309 }
 310 
 311 static t_stat
 312 mgp_set_socket_path(UNUSED UNIT *uptr, UNUSED int32 value, const char *cptr,
     /* [previous][next][first][last][top][bottom][index][help] */
 313                     UNUSED void *desc)
 314 {
 315   if (!cptr || strlen(cptr) == 0)
 316     return SCPE_ARG;
 317   if (strlen(cptr) >= NCP_SOCKET_PATH_MAX - 1)
 318     {
 319       sim_printf("NCP socket path too long (max %d chars)\r\n",
 320                  NCP_SOCKET_PATH_MAX - 1);
 321       return SCPE_ARG;
 322     }
 323   strncpy(ncp_socket_path, cptr, NCP_SOCKET_PATH_MAX - 1);
 324   ncp_socket_path[NCP_SOCKET_PATH_MAX - 1] = '\0';
 325   sim_printf("MGP NCP socket path set to: %s\r\n", ncp_socket_path);
 326   return SCPE_OK;
 327 }
 328 
 329 # define UNIT_WATCH  UNIT_V_UF
 330 
 331 static MTAB mgp_mod[] = {
 332 # if !defined(SPEED)
 333   { UNIT_WATCH, 1, "WATCH",   "WATCH",   0, 0, NULL, NULL },
 334   { UNIT_WATCH, 0, "NOWATCH", "NOWATCH", 0, 0, NULL, NULL },
 335 # endif /* if !defined(SPEED) */
 336   {
 337     MTAB_XTD | MTAB_VDV | MTAB_NMO | MTAB_VALR, /* Mask               */
 338     0,                                          /* Match              */
 339     "NUNITS",                                   /* Print string       */
 340     "NUNITS",                                   /* Match string       */
 341     mgp_set_nunits,                             /* Validation routine */
 342     mgp_show_nunits,                            /* Display routine    */
 343     "Number of MGP units in the system",        /* Value descriptor   */
 344     NULL                                        /* Help               */
 345   },
 346   {
 347     MTAB_XTD | MTAB_VUN | MTAB_VALR | MTAB_NC,  /* Mask               */
 348     0,                                          /* Match              */
 349     "NAME",                                     /* Print string       */
 350     "NAME",                                     /* Match string       */
 351     mgp_set_device_name,                        /* Validation routine */
 352     mgp_show_device_name,                       /* Display routine    */
 353     "Set the device name",                      /* Value descriptor   */
 354     NULL                                        /* Help               */
 355   },
 356   {
 357     MTAB_XTD | MTAB_VDV | MTAB_VALR | MTAB_NC,  /* Mask               */
 358     0,                                          /* Match              */
 359     "SOCKET",                                   /* Print string       */
 360     "SOCKET",                                   /* Match string       */
 361     mgp_set_socket_path,                        /* Validation routine */
 362     mgp_show_socket_path,                       /* Display routine    */
 363     "Unix socket path for NCP connection",      /* Value descriptor   */
 364     NULL                                        /* Help               */
 365   },
 366   MTAB_eol
 367 };
 368 
 369 static t_stat
 370 mgp_reset(UNUSED DEVICE *dptr)
     /* [previous][next][first][last][top][bottom][index][help] */
 371 {
 372   return SCPE_OK;
 373 }
 374 
 375 static t_stat
 376 mgpAttach(UNIT *uptr, const char *cptr)
     /* [previous][next][first][last][top][bottom][index][help] */
 377 {
 378   if (!cptr)
 379     {
 380       return SCPE_ARG;
 381     }
 382 
 383   // If we're already attached, then detach ...
 384   if (( uptr->flags & UNIT_ATT ) != 0)
 385     {
 386       detach_unit(uptr);
 387     }
 388 
 389   uptr->flags |= UNIT_ATT;
 390 
 391   return SCPE_OK;
 392 }
 393 
 394 // Detach (connect) ...
 395 static t_stat
 396 mgpDetach(UNIT *uptr)
     /* [previous][next][first][last][top][bottom][index][help] */
 397 {
 398   if (( uptr->flags & UNIT_ATT ) == 0)
 399     {
 400       return SCPE_OK;
 401     }
 402 
 403   uptr->flags &= ~(unsigned int)UNIT_ATT;
 404 
 405   return SCPE_OK;
 406 }
 407 
 408 DEVICE mgp_dev = {
 409   "MGP",       /* Name                */
 410   mgp_unit,    /* Units               */
 411   NULL,        /* Registers           */
 412   mgp_mod,     /* Modifiers           */
 413   N_MGP_UNITS, /* #units              */
 414   10,          /* Address radix       */
 415   24,          /* Address width       */
 416   1,           /* Address increment   */
 417   8,           /* Data radix          */
 418   36,          /* Data width          */
 419   NULL,        /* Examine             */
 420   NULL,        /* Deposit             */
 421   mgp_reset,   /* Reset               */
 422   NULL,        /* Boot                */
 423   mgpAttach,   /* Attach              */
 424   mgpDetach,   /* Detach              */
 425   NULL,        /* Context             */
 426   DEV_DEBUG,   /* Flags               */
 427   0,           /* Debug control flags */
 428   mgp_dt,      /* Debug flag names    */
 429   NULL,        /* Memory size change  */
 430   NULL,        /* Logical name        */
 431   NULL,        /* Help                */
 432   NULL,        /* Attach help         */
 433   NULL,        /* Attach context      */
 434   NULL,        /* Description         */
 435   NULL         /* End                 */
 436 };
 437 
 438 /*
 439  * mgp_init()
 440  */
 441 
 442 // Once-only initialization
 443 
 444 void
 445 mgp_init(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 446 {
 447   (void)memset(mgp_state, 0, sizeof ( mgp_state ));
 448   mgp_init_dev_state();
 449 }
 450 
 451 static void
 452 mgp_init_dev_state(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 453 {
 454   (void)memset(&mgp_dev_state, 0, sizeof ( mgp_dev_state ));
 455   mgp_dev_state.pipe = NULL;
 456   mgp_dev_state.in_buffer_len = 0;
 457 }
 458 
 459 static void ncp_close_cb(uv_handle_t *handle) {
     /* [previous][next][first][last][top][bottom][index][help] */
 460   free(handle);
 461 }
 462 
 463 static void ncp_alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
     /* [previous][next][first][last][top][bottom][index][help] */
 464   (void)handle;
 465   buf->base = malloc(suggested_size);
 466   buf->len  = buf->base ? suggested_size : 0;
 467 }
 468 
 469 static void ncp_read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
     /* [previous][next][first][last][top][bottom][index][help] */
 470   if (nread < 0) {
 471     if (nread != UV_EOF)
 472       (void)sir_error("ncp_read_cb: %s", uv_strerror(nread));
 473     else
 474       (void)sir_info("%s:%d: NCP connection closed", __func__, __LINE__);
 475     uv_close((uv_handle_t *)stream, ncp_close_cb);
 476     mgp_dev_state.pipe = NULL;
 477     if (buf && buf->base)
 478       free(buf->base);
 479     return;
 480   }
 481   if (nread > 0 && buf && buf->base) {
 482     if (mgp_dev_state.in_buffer_len + (int)nread
 483         <= (int)sizeof(mgp_dev_state.in_buffer)) {
 484       memcpy(mgp_dev_state.in_buffer + mgp_dev_state.in_buffer_len,
 485              buf->base, nread);
 486       mgp_dev_state.in_buffer_len += (int)nread;
 487     } else {
 488       (void)sir_error("ncp_read_cb: receive buffer overflow");
 489       uv_close((uv_handle_t *)stream, ncp_close_cb);
 490       mgp_dev_state.pipe = NULL;
 491       mgp_dev_state.in_buffer_len = 0;
 492     }
 493   }
 494   if (buf && buf->base)
 495     free(buf->base);
 496 }
 497 
 498 static void ncp_connect_cb(uv_connect_t *req, int status) {
     /* [previous][next][first][last][top][bottom][index][help] */
 499   if (status < 0) {
 500     (void)sir_notice("%s: connect error: %s", __func__, uv_strerror(status));
 501     if (mgp_dev_state.pipe) {
 502       uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
 503       mgp_dev_state.pipe = NULL;
 504     }
 505   } else {
 506     (void)sir_info("%s:%d: connected to NCP at %s",
 507                    __func__, __LINE__, ncp_socket_path);
 508     uv_read_start((uv_stream_t *)mgp_dev_state.pipe,
 509                   ncp_alloc_cb, ncp_read_cb);
 510   }
 511   free(req);
 512 }
 513 
 514 /*
 515  * Connect to the NCP process via Unix domain socket (async).
 516  * Returns 0 if a connect has been initiated (result arrives via callback),
 517  * -1 if still in PTW backoff or allocation failed.
 518  */
 519 static int
 520 ncp_connect(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 521 {
 522   if (mgp_dev_state.pipe) return 0; /* already connected or pending */
 523 
 524   /* PTW-failure backoff */
 525   if (mgp_dev_state.ptw_failed_at > 0)
 526     {
 527       if (time(NULL) - mgp_dev_state.ptw_failed_at < PTW_BACKOFF_SECS)
 528         return -1;
 529       mgp_dev_state.ptw_failed_at = 0;
 530     }
 531 
 532   mgp_dev_state.pipe = malloc(sizeof(uv_pipe_t));
 533   if (!mgp_dev_state.pipe)
 534     return -1;
 535   uv_pipe_init(uv_default_loop(), mgp_dev_state.pipe, 0);
 536 
 537   uv_connect_t *req = malloc(sizeof(uv_connect_t));
 538   if (!req) {
 539     uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
 540     mgp_dev_state.pipe = NULL;
 541     return -1;
 542   }
 543 
 544   mgp_dev_state.in_buffer_len = 0;
 545   uv_pipe_connect(req, mgp_dev_state.pipe, ncp_socket_path, ncp_connect_cb);
 546   return 0;
 547 }
 548 
 549 typedef struct {
 550   uv_write_t req;
 551   uv_buf_t   buf;
 552 } ncp_write_req_t;
 553 
 554 static void ncp_write_cb(uv_write_t *req, int status) {
     /* [previous][next][first][last][top][bottom][index][help] */
 555   ncp_write_req_t *wr = (ncp_write_req_t *)req;
 556   if (status < 0)
 557     (void)sir_error("ncp_write_cb: %s", uv_strerror(status));
 558   free(wr->buf.base);
 559   free(wr);
 560 }
 561 
 562 /*
 563  * Send a length-prefixed 8-bit MGP packet to the NCP process (async).
 564  * Returns 0 on success, -1 on failure.
 565  */
 566 static int
 567 ncp_send_packet(u_char *pkt8, int pktlen)
     /* [previous][next][first][last][top][bottom][index][help] */
 568 {
 569 # if defined(THREADZ) || defined(LOCKLESS)
 570   lock_libuv();
 571 # endif
 572   if (!mgp_dev_state.pipe) {
 573     if (ncp_connect() < 0) {
 574 # if defined(THREADZ) || defined(LOCKLESS)
 575       unlock_libuv();
 576 # endif
 577       return -1;
 578     }
 579   }
 580 
 581   ncp_write_req_t *wr = malloc(sizeof(ncp_write_req_t));
 582   if (!wr) {
 583 # if defined(THREADZ) || defined(LOCKLESS)
 584     unlock_libuv();
 585 # endif
 586     return -1;
 587   }
 588   wr->buf.len  = NCP_FRAME_HEADER_SIZE + pktlen;
 589   wr->buf.base = malloc(wr->buf.len);
 590   if (!wr->buf.base) {
 591     free(wr);
 592 # if defined(THREADZ) || defined(LOCKLESS)
 593     unlock_libuv();
 594 # endif
 595     return -1;
 596   }
 597   wr->buf.base[0] = (u_char)((pktlen >> 8) & 0xFF);
 598   wr->buf.base[1] = (u_char)(pktlen & 0xFF);
 599   memcpy(wr->buf.base + NCP_FRAME_HEADER_SIZE, pkt8, pktlen);
 600 
 601   int rc = uv_write(&wr->req, (uv_stream_t *)mgp_dev_state.pipe,
 602                     &wr->buf, 1, ncp_write_cb);
 603   if (rc < 0) {
 604     (void)sir_error("ncp_send_packet: uv_write: %s", uv_strerror(rc));
 605     free(wr->buf.base);
 606     free(wr);
 607 # if defined(THREADZ) || defined(LOCKLESS)
 608     unlock_libuv();
 609 # endif
 610     return -1;
 611   }
 612 # if defined(THREADZ) || defined(LOCKLESS)
 613   unlock_libuv();
 614 # endif
 615   return 0;
 616 }
 617 
 618 /*
 619  * Consume one length-prefixed packet from the libuv receive buffer.
 620  * Returns byte count >0, 0 if not enough data yet, and -1 on error.
 621  * Data is deposited into in_buffer by ncp_read_cb via libuv async.
 622  */
 623 static int
 624 ncp_recv_packet(u_char *pkt8, int maxlen)
     /* [previous][next][first][last][top][bottom][index][help] */
 625 {
 626   if (!mgp_dev_state.pipe) {
 627     //if (ncp_connect() < 0) return 0;
 628     return 0; /* connect pending; data arrives via ncp_read_cb */
 629   }
 630 
 631   if (mgp_dev_state.in_buffer_len < NCP_FRAME_HEADER_SIZE) return 0;
 632 
 633   int pktlen = ((unsigned char)mgp_dev_state.in_buffer[0] << 8)
 634              |  (unsigned char)mgp_dev_state.in_buffer[1];
 635 
 636   if (pktlen <= 0 || pktlen > maxlen) {
 637     (void)sir_error("%s:%d: bad packet length %d -- resetting NCP connection",
 638                     __func__, __LINE__, pktlen);
 639     if (mgp_dev_state.pipe) {
 640       uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
 641       mgp_dev_state.pipe = NULL;
 642     }
 643     mgp_dev_state.in_buffer_len = 0;
 644     return -1;
 645   }
 646 
 647   if (mgp_dev_state.in_buffer_len < NCP_FRAME_HEADER_SIZE + pktlen) return 0;
 648 
 649   memcpy(pkt8, mgp_dev_state.in_buffer + NCP_FRAME_HEADER_SIZE, pktlen);
 650   int consumed = NCP_FRAME_HEADER_SIZE + pktlen;
 651   mgp_dev_state.in_buffer_len -= consumed;
 652   if (mgp_dev_state.in_buffer_len > 0)
 653     memmove(mgp_dev_state.in_buffer,
 654             mgp_dev_state.in_buffer + consumed,
 655             mgp_dev_state.in_buffer_len);
 656 
 657   return pktlen;
 658 }
 659 
 660 /*
 661  * Convert an 8-bit MGP packet to 36-bit IOM words.
 662  * The 16-byte header occupies 4 words (4 x 9-bit bytes per word).
 663  * Data bytes follow in 9-bit format (upper bit zero).
 664  */
 665 static void
 666 pkt8_to_word36(u_char *pkt8, int pktlen, word36 *buf, uint maxwords)
     /* [previous][next][first][last][top][bottom][index][help] */
 667 {
 668   uint j;
 669   (void)memset(buf, 0, maxwords * sizeof(word36));
 670 
 671   for (j = 0; j < (uint)pktlen && j / 4 < maxwords; j++)
 672     {
 673       putbits36_9(&buf[j / 4], (j % 4) * 9, pkt8[j]);
 674     }
 675 }
 676 
 677 /*
 678  * Convert 36-bit IOM words to an 8-bit MGP packet.
 679  * Returns the total packet length in bytes.
 680  */
 681 static int
 682 word36_to_pkt8(word36 *buf, uint words, u_char *pkt8, int maxlen)
     /* [previous][next][first][last][top][bottom][index][help] */
 683 {
 684   int j;
 685   int total = (int)(words * 4);
 686   if (total > maxlen)
 687     {
 688       total = maxlen;
 689     }
 690 
 691   for (j = 0; j < total; j++)
 692     {
 693       pkt8[j] = getbits36_9(buf[j / 4], (j % 4) * 9);
 694     }
 695 
 696   /* Determine actual packet length from byte_count in header */
 697   if (total >= 16)
 698     {
 699       int byte_count = (pkt8[8] & 0xFF) | ((pkt8[9] & 0xFF) << 8);
 700       int real_len = 16 + byte_count;
 701       if (real_len < total)
 702         {
 703           total = real_len;
 704         }
 705     }
 706 
 707   return total;
 708 }
 709 
 710 static iom_cmd_rc_t
 711 get_ddcw(iom_chan_data_t *p, uint iom_unit_idx, uint chan, bool *ptro,
     /* [previous][next][first][last][top][bottom][index][help] */
 712          uint expected_tally, uint *tally)
 713 {
 714 # if defined(TESTING)
 715   cpu_state_t * cpup = _cpup;
 716 # endif
 717   bool  send, uff;
 718   int   rc = iom_list_service(iom_unit_idx, chan, ptro, &send, &uff);
 719 
 720   if (rc < 0)
 721     {
 722       p->stati = 05001;
 723       (void)sir_warn("%s:%d list service failed", __func__, __LINE__);
 724 
 725       return IOM_CMD_ERROR;
 726     }
 727 
 728   if (uff)
 729     {
 730       (void)sir_warn("%s:%d ignoring uff", __func__, __LINE__);
 731     }
 732 
 733   if (!send)
 734     {
 735       (void)sir_warn("%s:%d nothing to send", __func__, __LINE__);
 736       p->stati = 05001;
 737 
 738       return IOM_CMD_ERROR;
 739     }
 740 
 741   if (IS_IDCW(p) || IS_TDCW(p))
 742     {
 743       (void)sir_warn("%s:%d expected DDCW", __func__, __LINE__);
 744       p->stati = 05001;
 745 
 746       return IOM_CMD_ERROR;
 747     }
 748 
 749   *tally = p->DDCW_TALLY;
 750 
 751   if (*tally == 0)
 752     {
 753       sim_debug(DBG_DEBUG, &mgp_dev,
 754                 "%s:%d: Tally of zero interpreted as 010000(4096)\r\n", __func__, __LINE__);
 755       *tally = 4096;
 756     }
 757 
 758   sim_debug(DBG_DEBUG, &mgp_dev,
 759             "%s:%d: Tally %d (%o)\r\n", __func__, __LINE__, *tally, *tally);
 760 
 761   if (expected_tally && *tally != expected_tally)
 762     {
 763       (void)sir_warn("mgp_dev call expected tally of %d; got %d",
 764                      expected_tally, *tally);
 765       p->stati = 05001;
 766 
 767       return IOM_CMD_ERROR;
 768     }
 769 
 770   return IOM_CMD_PROCEED;
 771 }
 772 
 773 static char *
 774 cmd_name(int code)
     /* [previous][next][first][last][top][bottom][index][help] */
 775 {
 776   switch (code)
 777     {
 778     case 000:
 779       return "Request status";
 780 
 781     case 001:
 782       return "Read";
 783 
 784     case 011:
 785       return "Write";
 786 
 787     case 020:
 788       return "Host switch down";
 789 
 790     case 040:
 791       return "Reset status";
 792 
 793     case 042:
 794       return "Disable Bus Back";
 795 
 796     case 043:
 797       return "Enable Bus Back";
 798 
 799     case 060:
 800       return "Host switch up";
 801 
 802     default:
 803       return "Unknown";
 804     }
 805 }
 806 
 807 # if defined(TESTING)
 808 /*
 809  * dumppkt: Debug dump of a 36-bit MGP packet.
 810  * Kept from the original for debugging purposes.
 811  */
 812 static void
 813 dumppkt(char *hdr, word36 *buf, uint words)
     /* [previous][next][first][last][top][bottom][index][help] */
 814 {
 815   int i;
 816   if (words < MGP_PACKET_HEADER_SIZE)
 817     {
 818       (void)sir_notice("%s: packet too small (%d words)", hdr, words);
 819       return;
 820     }
 821 
 822   int checksum    = getbits36_9(buf[0],  0);
 823   int id          = getbits36_9(buf[0],  9);
 824   int pktype      = getbits36_9(buf[0], 18);
 825   int flags       = getbits36_9(buf[0], 27);
 826   int framenr     = getbits36_9(buf[1],  0);
 827   int rcpt        = getbits36_9(buf[1],  9);
 828   int pknr        = getbits36_9(buf[1], 18);
 829   int acknr       = getbits36_9(buf[1], 27);
 830   int bytecount   = (getbits36_9(buf[2], 0) & 0xff)
 831                   | ((getbits36_9(buf[2], 9) & 0xff) << 8);
 832   int srcprc      = (getbits36_9(buf[2], 18) & 0xff)
 833                   | ((getbits36_9(buf[2], 27) & 0xff) << 8);
 834   int dstprc      = (getbits36_9(buf[3], 0) & 0xff)
 835                   | ((getbits36_9(buf[3], 9) & 0xff) << 8);
 836   int chopcode    = getbits36_9(buf[3], 18);
 837 
 838   (void)sir_notice("%s packet (%d words)",
 839                    hdr, words);
 840   (void)sir_notice("cks %#x, id %#x, type %#x, flags %#x",
 841                    checksum, id, pktype, flags);
 842   (void)sir_notice("frame %#x, rcpt %#x, pknr %#x, acknr %#x",
 843                    framenr, rcpt, pknr, acknr);
 844   (void)sir_notice("bytecount %d, src %#x, dst %#x, chopcode %#o",
 845                    bytecount, srcprc, dstprc, chopcode);
 846 
 847   int pklen = MGP_PACKET_HEADER_SIZE + (bytecount / 4)
 848             + (bytecount % 4 ? 1 : 0);
 849   if (pklen > (int)words)
 850     {
 851       pklen = (int)words;
 852     }
 853 
 854   for (i = 0; i < pklen; i++)
 855     {
 856       int lh = getbits36_18(buf[i],  0);
 857       int rh = getbits36_18(buf[i], 18);
 858       int b0 = getbits36_9 (buf[i],  0);
 859       int b1 = getbits36_9 (buf[i],  9);
 860       int b2 = getbits36_9 (buf[i], 18);
 861       int b3 = getbits36_9 (buf[i], 27);
 862       (void)sir_notice(" %d: %06o,,%06o = 0x%02x %02x %02x %02x",
 863                        i, lh, rh, b0, b1, b2, b3);
 864     }
 865 
 866   (void)sir_notice("EOP");
 867 }
 868 # endif
 869 
 870 /* Forward declarations - defined later in this file */
 871 static int mgp_check_dma_ptw(uint iom_unit_idx, uint chan, uint max_words);
 872 static void mgp_validate_dcw_state(uint iom_unit_idx, uint chan, int expected_tally,
 873                                    const char *caller);
 874 
 875 /*
 876  * mgp_validate_dcw_state() - Diagnostic: validate DCW list state vs Multics memory.
 877  *
 878  * Called when an anomalous DDCW_TALLY or DDCW_ADDR is detected.  Logs:
 879  *   1. Current iom_chan_data fields (cached values the IOM is using).
 880  *   2. The raw DCW list entries from Multics memory (workspace page 0),
 881  *      so we can see whether the corruption is in the cache or in memory.
 882  *
 883  * The workspace structure (from mgp_read_dcm_.pl1 declare read_workspace):
 884  *   offset  0-11: dcw_list[0..5] - 6 x (idcw + ddcw) = 12 words
 885  *   offset    12: tdcw            - 1 word (transfer/wrap-around DCW)
 886  *   offset    13: reset_idcw      - 1 word
 887  *   offset 14-xx: status_queue[0..3] - 4 x istat (size varies)
 888  *   offset   ~22: buffer[0..5]   - 6 x data_size words (129 read, 128 write)
 889  *
 890  * Expected DDCW_ADDR values (for the 6 buffer slots) should be within
 891  * [0, 01777] octal (the workspace fits in one IOM page = 1024 words).
 892  * Expected DDCW_TALLY is 129 (read channel) or 128 (write channel).
 893  *
 894  * If DDCW_ADDR or DDCW_TALLY is outside those bounds, DCW corruption has
 895  * occurred - most likely from a prior PTW-failure DMA write to address ~= 0
 896  * that overwrote the IOM mailbox and caused iom_list_service to follow a
 897  * bad LPW pointer into arbitrary memory.
 898  */
 899 static void
 900 mgp_validate_dcw_state(uint iom_unit_idx, uint chan, int expected_tally,
     /* [previous][next][first][last][top][bottom][index][help] */
 901                         const char *caller)
 902   {
 903     iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
 904 
 905     /* 1. Dump the cached iom_chan_data state */
 906     (void)sir_notice("DCW_VALIDATE [%s] chan=%d:",
 907                      caller, chan);
 908     (void)sir_notice("  cached: DDCW_ADDR=0%o DDCW_TALLY=%d (expected ~%d)",
 909                      p->DDCW_ADDR, (int)p->DDCW_TALLY, expected_tally);
 910     (void)sir_notice("  DDCW_22_23_TYPE=%d",
 911                      (int)p->DDCW_22_23_TYPE);
 912     (void)sir_notice("  LPW_DCW_PTR=0%o LPW_TALLY=%d",
 913                      p->LPW_DCW_PTR, (int)p->LPW_TALLY);
 914     (void)sir_notice("  PCW_PAGE_TABLE_PTR=0%o PCW_63_PTP=%d PCW_64_PGE=%d SEG=%d",
 915                      p->PCW_PAGE_TABLE_PTR, (int)p->PCW_63_PTP, (int)p->PCW_64_PGE, (int)p->SEG);
 916     (void)sir_notice("  in_use=%d masked=%d",
 917                      (int)p->in_use, (int)p->masked);
 918 
 919     /* 2. Sanity-check DDCW_ADDR range (workspace = one IOM page = 1024 words) */
 920     if (p->DDCW_ADDR > 01777)
 921       {
 922         (void)sir_warn("DCW_VALIDATE: DDCW_ADDR=0%o is OUTSIDE workspace range"
 923                        " [0, 01777] - LPW likely corrupted", p->DDCW_ADDR);
 924       }
 925 
 926     /* 3. Read DCW list from Multics memory in paged mode */
 927     if (!p->PCW_63_PTP || !p->PCW_64_PGE)
 928       {
 929         (void)sir_warn("DCW_VALIDATE: not in paged mode"
 930                        " (PTP=%d PGE=%d) - skipping memory read",
 931                        (int)p->PCW_63_PTP, (int)p->PCW_64_PGE);
 932         return;
 933       }
 934 
 935     /* Look up the workspace page 0 PTW */
 936     word24 pgte0 = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
 937                  + (((word24)(p->SEG & 1)) << 8)
 938                  + 0u; /* page 0 */
 939 
 940     word36 ptw0 = 0;
 941     iom_core_read(iom_unit_idx, pgte0, &ptw0, __func__);
 942 
 943     int ptw0_valid = ((ptw0 & 0740000777747llu) == 04llu);
 944     (void)sir_warn("DCW_VALIDATE: workspace page 0 PTW at pgte=0%o:"
 945                    " 0%012llo (%s)",
 946                    pgte0, (unsigned long long)ptw0,
 947                    ptw0_valid ? "valid" : "INVALID");
 948 
 949     if (!ptw0_valid)
 950       {
 951         (void)sir_warn("DCW_VALIDATE: page 0 PTW invalid -"
 952                        " cannot read DCW list from Multics memory");
 953         return;
 954       }
 955 
 956     /* Physical base address of workspace (bits 4-17 of PTW, shifted left 10) */
 957     word24 phys_base = ((word24)((ptw0 >> 18) & MASK14)) << 10;
 958     (void)sir_warn("DCW_VALIDATE: workspace physical base = 0%o", phys_base);
 959 
 960     /* 4. Read and validate each of the 6 IDCW+DDCW pairs.
 961      * The dcw_list occupies offsets 0-11 in the workspace:
 962      *   slot i: IDCW at offset 2*i, DDCW at offset 2*i+1           */
 963     (void)sir_warn("DCW_VALIDATE: dcw_list entries from memory"
 964                    " (expected TALLY=%d, ADDR in [0,01777]):", expected_tally);
 965     for (int i = 0; i < 6; i++)
 966       {
 967         word24 idcw_phys = phys_base + (word24)(2 * i);
 968         word24 ddcw_phys = phys_base + (word24)(2 * i + 1);
 969 
 970         word36 idcw_word = 0, ddcw_word = 0;
 971         iom_core_read(iom_unit_idx, idcw_phys, &idcw_word, __func__);
 972         iom_core_read(iom_unit_idx, ddcw_phys, &ddcw_word, __func__);
 973 
 974         /* DDCW layout: bits 0-17 = address, bits 24-35 = tally */
 975         uint ddcw_addr  = (uint)((ddcw_word >> 18) & MASK18);
 976         uint ddcw_tally = (uint)(ddcw_word & 0xFFFu);
 977 
 978         int anomalous = (ddcw_tally == 0
 979                       || (int)ddcw_tally > (expected_tally + 2)
 980                       || ddcw_addr > 01777u);
 981 
 982         (void)sir_warn("  slot[%d]: IDCW=0%012llo DDCW=0%012llo"
 983                        " addr=0%o tally=%d%s",
 984                        i,
 985                        (unsigned long long)idcw_word,
 986                        (unsigned long long)ddcw_word,
 987                        ddcw_addr, ddcw_tally,
 988                        anomalous ? " *** ANOMALOUS" : "");
 989       }
 990 
 991     /* 5. Read the word at LPW_DCW_PTR to see what iom_list_service last fetched */
 992     word18 lpw_ptr = p->LPW_DCW_PTR;
 993     if (lpw_ptr <= 01777u)
 994       {
 995         word36 lpw_word = 0;
 996         iom_core_read(iom_unit_idx, phys_base + (word24)lpw_ptr, &lpw_word, __func__);
 997         (void)sir_warn("DCW_VALIDATE: mem[LPW_DCW_PTR=0%o] = 0%012llo",
 998                        lpw_ptr, (unsigned long long)lpw_word);
 999       }
1000     else
1001       {
1002         (void)sir_warn("DCW_VALIDATE: LPW_DCW_PTR=0%o is OUTSIDE workspace"
1003                        " [0, 01777] - LPW pointer corrupted", lpw_ptr);
1004       }
1005   }
1006 
1007 static iom_cmd_rc_t
1008 mgp_cmd(uint iom_unit_idx, uint chan)
     /* [previous][next][first][last][top][bottom][index][help] */
1009 {
1010 # if defined(TESTING)
1011   cpu_state_t * cpup = _cpup;
1012 # endif
1013   iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1014 
1015   sim_debug(DBG_TRACE, &mgp_dev,
1016             "mgp_cmd CHAN_CMD %o DEV_CODE %o DEV_CMD %o COUNT %o\r\n",
1017             p->IDCW_CHAN_CMD, p->IDCW_DEV_CODE, p->IDCW_DEV_CMD, p->IDCW_COUNT);
1018 
1019   // Not IDCW?
1020   if (IS_NOT_IDCW(p))
1021     {
1022       (void)sir_warn("%s:%d: Unexpected IOTx", __func__, __LINE__);
1023 
1024       return IOM_CMD_ERROR;
1025     }
1026 
1027   bool ptro;
1028 
1029   sim_debug(DBG_DEBUG, &mgp_dev, "mgp_cmd %#o (%s)\r\n",
1030             p->IDCW_DEV_CMD, cmd_name(p->IDCW_DEV_CMD));
1031 
1032   switch (p->IDCW_DEV_CMD)
1033     {
1034     case 000: // CMD 00 Request status
1035     {
1036       p->stati = 04000;
1037       sim_debug(DBG_DEBUG, &mgp_dev, "mgp request status\r\n");
1038     }
1039     break;
1040 
1041     case 001: // CMD 01 Read
1042     {
1043       sim_debug(DBG_DEBUG, &mgp_dev, "%s:%d: mgp_dev_$read\r\n", __func__, __LINE__);
1044 
1045       const uint    expected_tally = 0;
1046       uint          tally;
1047       iom_cmd_rc_t  rc
1048         = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1049       if (rc)
1050         {
1051           return rc;
1052         }
1053 
1054       /* Validate the DMA PTW before any IOM buffer access.
1055        * If Multics's memory manager has paged out the IOM buffer (e.g. after
1056        * extended idle), iom_indirect_data_service() would compute physical
1057        * address ~= 0 from the zero PTW and either read garbage or corrupt the
1058        * IOM mailbox area (addresses 0-0777).  Return IOM_CMD_DISCONNECT so the
1059        * IOM sends a terminate interrupt; Multics can then re-establish the
1060        * channel with properly pinned memory.
1061        *
1062        * max_words=256: NCP packets are at most MAX_PKT_BYTES=504 bytes =
1063        * ~126 36-bit words, which fits within the first IOM page (1024 words).
1064        * Using max_words=0 (full DDCW_TALLY=4096 words = 4 pages) would cause
1065        * false positives when Multics's memory manager pages out unused pages
1066        * 1-3 of the large DCW buffer overnight - those pages are never touched
1067        * by the DMA since the actual payload is only ~126 words.              */
1068       if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1069         {
1070           (void)sir_warn("%s:%d: CMD 001 invalid DMA PTW on chan %d - "
1071                          "skipping buffer access, sending terminate interrupt",
1072                          __func__, __LINE__, chan);
1073           mgp_dev_state.ptw_failed_at = time(NULL);
1074           p->stati = 04000;
1075           return IOM_CMD_DISCONNECT;
1076         }
1077 
1078       /* Sanity-check DDCW_TALLY.
1079        *
1080        * Legitimate MGP read DDCWs have TALLY set by initialize_workspace
1081        * in mgp_read_dcm_.pl1: data_size=129 words.  TALLY=0 (IOM convention
1082        * for 4096) or an implausibly large value means the DCW entry was
1083        * corrupted.  The primary corruption path (TDCW wrap -> DDCW_ADDR=0 ->
1084        * IDS overwrites DCW list) is now blocked by the DDCW_ADDR range check
1085        * in mgp_process_event; this check is retained as defense-in-depth.  */
1086       if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > MGP_MAX_TALLY)
1087         {
1088           (void)sir_warn("%s:%d: CMD 001 implausible DDCW_TALLY=%d on chan %d"
1089                          " (expected ~129, max %d - DCW corruption?)"
1090                          " sending terminate interrupt",
1091                          __func__, __LINE__, p->DDCW_TALLY, chan, MGP_MAX_TALLY);
1092           mgp_validate_dcw_state(iom_unit_idx, chan, 129, "CMD001-TALLY");
1093           p->stati = 04000;
1094           return IOM_CMD_DISCONNECT;
1095         }
1096 
1097       /* Read current buffer and complete DDCW processing */
1098       word36  buffer[256];
1099       uint    words_processed;
1100       iom_indirect_data_service(
1101         iom_unit_idx, chan, buffer, &words_processed, false);
1102 
1103       sim_debug(DBG_DEBUG, &mgp_dev,
1104                 "%s:%d: Read unit %#x chan %#x (%d), %d words\r\n",
1105                 __func__, __LINE__, iom_unit_idx, chan, chan, words_processed);
1106 
1107       /*
1108        * Mark that Multics wants to read. The actual data delivery happens
1109        * in mgp_process_event() when the NCP sends us a packet.
1110        */
1111       mgp_dev_state.want_to_read           = 1;
1112       mgp_dev_state.want_to_read_since     = time(NULL);
1113       mgp_dev_state.read_unit_idx          = iom_unit_idx;
1114       mgp_dev_state.read_unit_chan         = chan;
1115 
1116       /* Write back to IOM to complete the DDCW processing.
1117        * This is required before returning IOM_CMD_PENDING -
1118        * without it the IOM channel state is inconsistent and
1119        * Multics will timeout and mask the channel.
1120        * (cf. new_dps8m_chaos_code/dps8_mgp.c.new lines 525-526)
1121        */
1122       iom_indirect_data_service(
1123         iom_unit_idx, chan, buffer, &words_processed, true);
1124 
1125       p->stati = 04000;
1126       /* Signal mgp_process_event that this delivery attempt succeeded
1127        * (iom_continue_channel successfully called mgp_cmd). */
1128       mgp_dev_state.delivery_succeeded = 1;
1129       return IOM_CMD_PENDING;
1130     }
1131     /*NOTREACHED*/ /* unreachable */
1132     break;
1133 
1134     case 011: // CMD 11 Write
1135     {
1136       sim_debug(DBG_DEBUG, &mgp_dev, "%s:%d: mgp_dev_$write\r\n", __func__, __LINE__);
1137 
1138       const uint    expected_tally = 0;
1139       uint          tally;
1140       iom_cmd_rc_t  rc
1141         = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1142 
1143       /* Check get_ddcw result (same as CMD 001 - missing this check was a
1144        * bug: a failed get_ddcw left p->DDCW_ADDR stale, causing the PTW
1145        * check below to validate the wrong page).
1146        *
1147        * NOTE: get_ddcw() returns IOM_CMD_PROCEED (0) on success or
1148        * IOM_CMD_ERROR (-1) on failure - never IOM_CMD_PENDING.
1149        * Returning IOM_CMD_ERROR here sends a TERMINATE interrupt (not marker)
1150        * via the rc<0 path in doPayloadChannel/iom_continue_channel.  Multics
1151        * sees the terminate and re-issues the write; the packet content is
1152        * lost (silent frame drop -> "Unordered frame" in NCP log).
1153        * Logged as WARNING so we can correlate with NCP-side drops.        */
1154       if (rc)
1155         {
1156           (void)sir_warn("%s:%d: CMD 011 get_ddcw failed rc=%d on chan %d"
1157                          " - packet will be silently dropped (frame loss)",
1158                          __func__, __LINE__, rc, chan);
1159           return rc;
1160         }
1161 
1162       /* Validate PTW before DMA access (prevents mailbox corruption if
1163        * Multics has paged out the WRITE channel buffer).
1164        * max_words=256: same rationale as CMD 001 - the write payload is at
1165        * most ~126 36-bit words (MAX_PKT_BYTES / 4), which fits within the
1166        * current IOM page.  Checking the full DDCW_TALLY (4096 words = 4
1167        * pages) would cause false positives when pages beyond the payload
1168        * are paged out, leading to tight IOM_CMD_DISCONNECT loops.         */
1169       if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1170         {
1171           (void)sir_warn("%s:%d: CMD 011 invalid DMA PTW on chan %d - "
1172                          "skipping buffer access, sending terminate interrupt",
1173                          __func__, __LINE__, chan);
1174           mgp_dev_state.ptw_failed_at = time(NULL);
1175           if (mgp_dev_state.pipe)
1176             {
1177               uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1178               mgp_dev_state.pipe = NULL;
1179             } /* discard queued NOOPs */
1180           p->stati = 04000;
1181           return IOM_CMD_DISCONNECT;
1182         }
1183 
1184       /* Sanity-check DDCW_TALLY (same rationale as CMD 001).
1185        * The write channel uses data_size=128 (mgp_write_dcm_.pl1).
1186        * iom_indirect_data_service's READ path (first call below, reading
1187        * the outgoing packet from Multics memory) ignores cnt and walks
1188        * all p->DDCW_TALLY words; TALLY=0 -> 4096-word walk -> same console-
1189        * flooding cascade as CMD 001.                                    */
1190       if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > MGP_MAX_TALLY)
1191         {
1192           (void)sir_warn("%s:%d CMD 011 implausible DDCW_TALLY=%d on chan %d"
1193                          " (expected ~128, max %d - DCW corruption?)"
1194                          " sending terminate interrupt",
1195                          __func__, __LINE__, p->DDCW_TALLY, chan, MGP_MAX_TALLY);
1196           mgp_validate_dcw_state(iom_unit_idx, chan, 128, "CMD011-TALLY");
1197           mgp_dev_state.ptw_failed_at = time(NULL);
1198           if (mgp_dev_state.pipe)
1199             {
1200               uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1201               mgp_dev_state.pipe = NULL;
1202             }
1203           p->stati = 04000;
1204           return IOM_CMD_DISCONNECT;
1205         }
1206 
1207       word36  buffer[256];
1208       uint    words_processed;
1209       iom_indirect_data_service(
1210         iom_unit_idx, chan, buffer, &words_processed, false);
1211 
1212       sim_debug(DBG_DEBUG, &mgp_dev,
1213                 "%s:%d: Write unit %#x chan %#x (%d), %d words\r\n",
1214                 __func__, __LINE__, iom_unit_idx, chan, chan, words_processed);
1215 # if defined(TESTING)
1216       if (sim_deb && (mgp_dev.dctrl & DBG_DEBUG))
1217         {
1218           dumppkt("Write", buffer, words_processed);
1219         }
1220 # endif
1221       /* Convert 36-bit words to 8-bit bytes */
1222       u_char pkt8[MAX_PKT_BYTES];
1223       int pktlen = word36_to_pkt8(buffer, words_processed, pkt8, MAX_PKT_BYTES);
1224 
1225       /* Send to NCP */
1226       int v = ncp_send_packet(pkt8, pktlen);
1227       if (v < 0)
1228         {
1229           (void)sir_warn("%s:%d: ncp_send_packet failed", __func__, __LINE__);
1230         }
1231 
1232       /* Return value depends on IDCW control field:
1233        *
1234        * mgp_write_dcm_ uses a double-buffer (half_buffer_size=3 slots per
1235        * half).  When fill>=2, update_workspace_and_start_io patches the first
1236        * IDCW to CHAN_CTRL_PROCEED (=2, "no-terminate") and sets the last to
1237        * CHAN_CTRL_TERMINATE (=0).  start_io calls ioi_$connect and resets
1238        * fill=0.  doPayloadChannel's do-while loop then processes each IDCW:
1239        *
1240        *   - IOM_CMD_PROCEED (0): loop continues -> iom_list_service advances
1241        *     to the next IDCW -> mgp_cmd(011) called again for the next packet.
1242        *   - IOM_CMD_DISCONNECT (2): sets terminate=true -> loop exits after
1243        *     this iteration -> terminate interrupt sent.
1244        *
1245        * Without this check, we always returned IOM_CMD_DISCONNECT for every
1246        * IDCW, causing the loop to exit after the FIRST packet in a batch.
1247        * The second packet was orphaned (start_io had already reset fill=0, so
1248        * check_status_queue on the next write call skipped start_io entirely).
1249        * Result: frame N sent, frame N+1 silently dropped -> "Unordered frame"
1250        * in NCP log and truncated file transfer.                             */
1251       rc = (p->IDCW_CHAN_CTRL == CHAN_CTRL_TERMINATE)
1252              ? IOM_CMD_DISCONNECT   /* terminate IDCW: send terminate interrupt */
1253              : IOM_CMD_PROCEED;     /* no-terminate IDCW: continue DCW list loop */
1254       p->stati  = 04000;
1255 
1256       /* Write-back: re-validates PTW before writing back to the Multics
1257        * write DMA buffer.  ncp_connect() inside ncp_send_packet() can
1258        * block briefly (Unix connect syscall), during which CPU A (Multics)
1259        * may page out the DMA buffer.  If the page is now gone, skip the
1260        * write-back (avoids fetch_IDSPTW warnings and address-0 corruption),
1261        * close the socket to flush queued NOOPs, and let the terminate
1262        * interrupt trigger Multics channel recovery.
1263        * max_words=256: same rationale as initial PTW check above - check
1264        * only the payload area, not unused pages beyond it.                */
1265       if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1266         {
1267           (void)sir_warn("%s:%d: CMD 011 PTW invalid after ncp_send_packet on chan %d"
1268                          " - skipping write-back", __func__, __LINE__, chan);
1269           mgp_dev_state.ptw_failed_at = time(NULL);
1270           if (mgp_dev_state.pipe)
1271             {
1272               uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1273               mgp_dev_state.pipe = NULL;
1274             }
1275           return rc; /* IOM_CMD_DISCONNECT - sends terminate interrupt */
1276         }
1277 
1278       iom_indirect_data_service(
1279         iom_unit_idx, chan, buffer, &words_processed, true);
1280 
1281       return rc;
1282     }
1283     /*NOTREACHED*/ /* unreachable */
1284     break;
1285 
1286     case 006: // CMD 06 (seen during channel restart; acknowledge gracefully)
1287     {
1288       p->stati = 04000;
1289       sim_debug(DBG_DEBUG, &mgp_dev, "mgp cmd 006 (handled)\r\n");
1290     }
1291     break;
1292 
1293     case 020: // CMD 20 Host switch down
1294     {
1295       p->stati = 04000;
1296       sim_debug(DBG_DEBUG, &mgp_dev, "mgp host switch down\r\n");
1297     }
1298     break;
1299 
1300     case 040: // CMD 40 Reset status
1301     {
1302       p->stati = 04000;
1303     }
1304     break;
1305 
1306     case 042: // CMD 42 Disable Bus Back
1307     {
1308       p->stati = 04000;
1309       sim_debug(DBG_DEBUG, &mgp_dev, "mgp disable bus back\r\n");
1310     }
1311     break;
1312 
1313     case 043: // CMD 43 Enable Bus Back
1314     {
1315       p->stati = 04000;
1316       sim_debug(DBG_DEBUG, &mgp_dev, "mgp enable bus back\r\n");
1317     }
1318     break;
1319 
1320     case 060: // CMD 60 Host switch up
1321     {
1322       p->stati = 04000;
1323       sim_debug(DBG_DEBUG, &mgp_dev, "mgp host switch up\r\n");
1324     }
1325     break;
1326 
1327     default:
1328     {
1329       if (p->IDCW_DEV_CMD != 051) // ignore bootload console probe
1330         {
1331           (void)sir_warn("%s:%d: MGP unrecognized device command  %02o",
1332                          __func__, __LINE__, p->IDCW_DEV_CMD);
1333         }
1334 
1335       p->stati       = 04501; // cmd reject, invalid opcode
1336       p->chanStatus  = chanStatIncorrectDCW;
1337     }
1338       return IOM_CMD_ERROR;
1339     }
1340 
1341   if (p->IDCW_CHAN_CMD == 0)
1342     {
1343       return IOM_CMD_DISCONNECT; // don't do DCW list
1344     }
1345 
1346   return IOM_CMD_PROCEED;
1347 }
1348 
1349 iom_cmd_rc_t
1350 mgp_iom_cmd(uint iom_unit_idx, uint chan)
     /* [previous][next][first][last][top][bottom][index][help] */
1351 {
1352   iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1353 
1354   // Is it an IDCW?
1355   if (IS_IDCW(p))
1356     {
1357       return mgp_cmd(iom_unit_idx, chan);
1358     }
1359 
1360   (void)sir_notice("%s:%d: expected IDCW", __func__, __LINE__);
1361 
1362   return IOM_CMD_ERROR;
1363 }
1364 
1365 /*
1366  * mgp_check_dma_ptw() - Validate that ALL pages of the IOM DMA buffer for
1367  * channel `chan` have valid page table words (PTWs) before calling
1368  * iom_indirect_data_service().
1369  *
1370  * ioi_$workspace pins all workspace pages (DCW list and data buffers) in
1371  * physical memory for the duration of active I/O (while in_use=true).
1372  * Page eviction cannot occur.  However, if the DCW list becomes corrupted
1373  * (e.g., circular-buffer wrap during an RCP force-detach/reattach cycle),
1374  * DDCW_ADDR may point outside the wired workspace, where no PTW exists.
1375  * Calling iom_indirect_data_service() with PTW=0 computes physical address
1376  * ~= 0, overwriting the IOM mailbox area (addresses 0-0777).  That corruption
1377  * cascades: Multics's IOM interrupt handler reads garbage vectors ->
1378  * fault/interrupt storm -> 100% CPU and system hang.
1379  *
1380  * The buffer spans from DDCW_ADDR through DDCW_ADDR+tally-1, potentially
1381  * crossing page boundaries.  Only checking the first page misses unmapped
1382  * pages later in the buffer (symptom: fetch_IDSPTW warnings at addr 0o02000+
1383  * after a force-detach/reattach cycle).  This function validates ALL pages
1384  * in the range so that any zero PTW is caught before the DMA starts.
1385  *
1386  * This function replicates the PTW lookup from fetch_IDSPTW /
1387  * build_IDSPTW_address (both static in dps8_iom.c) so dps8_mgp.c can
1388  * pre-check validity without touching the IOM code.
1389  *
1390  * Returns: 1 if all PTWs in the buffer range are valid (safe to proceed),
1391  *          0 if any PTW is zero or otherwise invalid (skip the DMA).
1392  */
1393 static int
1394 mgp_check_dma_ptw(uint iom_unit_idx, uint chan, uint max_words)
     /* [previous][next][first][last][top][bottom][index][help] */
1395   {
1396     iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1397 
1398     /* If the channel is not in paged mode, no PTW to validate. */
1399     if (!p->PCW_63_PTP || !p->PCW_64_PGE)
1400       return 1;
1401 
1402     /* Determine the range of IOM pages the buffer spans.
1403      * tally=0 is interpreted as 4096 by get_ddcw / iom_indirect_data_service.
1404      * page numbers are 8-bit (IOM page table has at most 256 entries).
1405      *
1406      * max_words: when non-zero, caps the effective tally used for page range
1407      * calculation.  Use this when the caller knows it will only write a small
1408      * payload (e.g. mgp_process_event delivers NCP packets of at most ~128
1409      * 36-bit words) so that pages beyond the actual payload are not validated
1410      * unnecessarily.  Pass 0 to use the full DDCW_TALLY (or 4096).          */
1411     uint   raw_tally  = p->DDCW_TALLY ? p->DDCW_TALLY : 4096;
1412     uint   tally      = (max_words && max_words < raw_tally) ? max_words : raw_tally;
1413     word18 start_page = (p->DDCW_ADDR         >> 10) & MASK8;
1414     word18 end_page   = ((p->DDCW_ADDR + tally - 1)  >> 10) & MASK8;
1415 
1416     /* Replicate build_IDSPTW_address() from dps8_iom.c for each page:
1417      *   pgte = ((PCW_PAGE_TABLE_PTR & MASK18) << 6)
1418      *        + ((SEG & 1)             <<    8)
1419      *        + (pageNumber            & MASK8)                               */
1420     for (word18 page = start_page; page <= end_page; page++)
1421       {
1422         word24 pgte = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
1423                     + (((word24)(p->SEG & 1))                     <<  8)
1424                     + (page                                       & MASK8);
1425 
1426         word36 ptw;
1427         iom_core_read(iom_unit_idx, pgte, &ptw, __func__);
1428 
1429         /* Valid PTW has specific bits set; zero PTW means page not present. */
1430         if ((ptw & 0740000777747llu) != 04llu)
1431           {
1432             (void)sir_warn ("%s:%d: chan %d DDCW_ADDR 0%o page %u/%u: invalid PTW"
1433                             " 0%012llo at pgte 0%o"
1434                             " (PCW_PAGE_TABLE_PTR=0%o SEG=%d tally=%u)",
1435                             __func__, __LINE__, chan, p->DDCW_ADDR,
1436                             (unsigned)(page - start_page + 1),
1437                             (unsigned)(end_page - start_page + 1),
1438                             (unsigned long long)ptw, pgte,
1439                             p->PCW_PAGE_TABLE_PTR, (int)p->SEG, tally);
1440             return 0;
1441           }
1442       }
1443     return 1;
1444   }
1445 
1446 /*
1447  * mgp_process_event() - Called periodically from the emulator's event loop.
1448  *
1449  * If Multics has a pending read (want_to_read), poll the NCP socket for
1450  * incoming data.  If data is available, read it, convert from 8-bit to
1451  * 36-bit words, write to the current IOM workspace buffer[N] via
1452  * iom_indirect_data_service, then call iom_continue_channel() to:
1453  *
1454  *   1. Advance DDCW_ADDR to workspace buffer[N+1] (via one loop iteration
1455  *      of doPayloadChannel: fetch IDCW[N+1] -> call mgp_cmd(read) ->
1456  *      get_ddcw() sets DDCW_ADDR = buffer[N+1]).
1457  *
1458  *   2. Send a marker interrupt so mgp_read_dcm_'s wakeup handler fires.
1459  *      The interrupt's stat.offset causes stop_buffer_number = N+1, which
1460  *      is one ahead of the DCM's buffer_number (N), triggering the
1461  *      processing loop to consume buffer[N].
1462  *
1463  * Because the interrupt is a MARKER (not terminate), start_io in the DCM
1464  * sees running=true and does NOT call ioi_$connect.  The channel remains
1465  * pending with DDCW_ADDR pointing to buffer[N+1], ready for the next
1466  * incoming packet.  want_to_read stays set (mgp_cmd restores it inside
1467  * iom_continue_channel).
1468  *
1469  * TDCW wrap-around is handled transparently by iom_list_service inside
1470  * iom_continue_channel.
1471  */
1472 void
1473 mgp_process_event(void)
     /* [previous][next][first][last][top][bottom][index][help] */
1474 {
1475   /* Unconditional no-progress watchdog.  Runs regardless of want_to_read so
1476    * it cannot be starved by the masked/unmask flicker described above.
1477    * Lazily initializes last_progress_time on first observation so a fresh
1478    * connection (or one that just recovered) gets a full timeout window
1479    * before this can fire.  Gated on want_to_read_since > 0: read_unit_idx/
1480    * read_unit_chan are only meaningfully set once Multics has issued at
1481    * least one real read (mgp_cmd CMD 001); before that they are still
1482    * zero-initialized, and firing the watchdog would send a terminate
1483    * interrupt to the wrong (unit 0, chan 0) channel.                       */
1484 # if defined(TESTING)
1485   cpu_state_t * cpup = _cpup;
1486 # endif
1487   if (mgp_dev_state.pipe != NULL && mgp_dev_state.want_to_read_since > 0)
1488     {
1489       time_t now_np = time(NULL);
1490       if (mgp_dev_state.last_progress_time == 0)
1491         {
1492           mgp_dev_state.last_progress_time = now_np;
1493         }
1494       else if (now_np - mgp_dev_state.last_progress_time >= NO_PROGRESS_TIMEOUT_SECS)
1495         {
1496           (void)sir_warn("%s:%d: no packet delivered to Multics in %d+ seconds despite "
1497                          "NCP connection; forcing socket reset and terminate "
1498                          "interrupt to break a possible masked/unmask timeout loop",
1499                          __func__, __LINE__, NO_PROGRESS_TIMEOUT_SECS);
1500           if (mgp_dev_state.pipe)
1501             {
1502               uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1503               mgp_dev_state.pipe = NULL;
1504             }
1505           mgp_dev_state.last_progress_time = 0;
1506           mgp_dev_state.masked_since = 0;
1507           mgp_dev_state.want_to_read = 0;
1508           send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1509                                    mgp_dev_state.read_unit_chan);
1510           return;
1511         }
1512     }
1513 
1514   if (!mgp_dev_state.want_to_read)
1515     {
1516       return;
1517     }
1518 
1519   uint iom_unit_idx = mgp_dev_state.read_unit_idx;
1520   uint chan          = mgp_dev_state.read_unit_chan;
1521   iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1522 
1523   /* If the channel has been masked (Multics sent a PCW with MSK=1), stop
1524    * trying to deliver packets until Multics re-enables it with a fresh
1525    * Connect PCW (MSK=0).  want_to_read is restored by mgp_cmd() when
1526    * Multics issues the new read command inside doPayloadChannel.
1527    *
1528    * NOTE: We do NOT check !in_use here.  The IOM sets in_use=false after
1529    * a terminate interrupt (e.g. DCW fault), but iom_continue_channel()
1530    * needs to run in that case so it can advance the DCW list and generate
1531    * the terminate interrupt that tells Multics to re-issue the read command.
1532    * Blocking on !in_use (without masked) prevents that signalling and
1533    * causes Multics to stall for ~30 seconds until its d102 timer fires. */
1534   {
1535     if (p->masked)
1536       {
1537         mgp_dev_state.want_to_read = 0;
1538 
1539         /* If the NCP socket is connected, track how long the channel has been
1540          * stuck masked.  After MASKED_STUCK_TIMEOUT_SECS, close the socket and
1541          * send a terminate interrupt.  This breaks the deadlock:
1542          *   - masked channel -> no delivery -> NCP inflight=30 -> no more NOOPs
1543          *   - ioi_masked$timer fires but "masked while in use" prevents recovery
1544          * The terminate interrupt tells Multics the I/O failed so it re-issues
1545          * READ with a fresh channel state.  The NCP-absent path then handles
1546          * reconnection cleanly.                                               */
1547         if (mgp_dev_state.pipe != NULL)
1548           {
1549             time_t now = time(NULL);
1550             if (mgp_dev_state.masked_since == 0)
1551               {
1552                 mgp_dev_state.masked_since = now;
1553                 sim_debug(DBG_DEBUG, &mgp_dev,
1554                           "%s:%d: channel %d masked while NCP connected; "
1555                           "starting stuck timer\r\n", __func__, __LINE__, chan);
1556               }
1557             else if (now - mgp_dev_state.masked_since >= MASKED_STUCK_TIMEOUT_SECS)
1558               {
1559                 (void)sir_warn("%s:%d: channel %d masked+stuck for %d+ seconds; "
1560                                "closing NCP socket and sending terminate interrupt "
1561                                "to force recovery",
1562                                __func__, __LINE__, chan, MASKED_STUCK_TIMEOUT_SECS);
1563                 if (mgp_dev_state.pipe)
1564                   {
1565                     uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1566                     mgp_dev_state.pipe = NULL;
1567                   }
1568                 mgp_dev_state.masked_since = 0;
1569                 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1570                                          mgp_dev_state.read_unit_chan);
1571               }
1572           }
1573         else
1574           {
1575             /* NCP not connected.  Reset the masked-stuck timer (it's only
1576              * meaningful when the NCP is connected), but also run the
1577              * NCP-absent timeout so the IOM channel's in_use=true state is
1578              * eventually released.
1579              *
1580              * Without this: masked=true causes us to return here every 10ms,
1581              * bypassing the NCP-absent timeout check below.  If the channel
1582              * is masked AND the NCP is absent, in_use never clears, and
1583              * ioi_masked$timer fires every ~4 minutes finding "chan N masked
1584              * while in use" - a permanent stuck deadlock.
1585              *
1586              * With this: after NCP_ABSENT_TIMEOUT_SECS (30 s) we send a
1587              * terminate interrupt.  in_use becomes false.  The next
1588              * ioi_masked$timer invocation successfully reconnects the masked
1589              * channel (no longer masked+in_use), and normal operation
1590              * resumes once the NCP connects again.                          */
1591             mgp_dev_state.masked_since = 0;
1592             time_t now_ma = time(NULL);
1593             if (mgp_dev_state.want_to_read_since > 0 &&
1594                 now_ma - mgp_dev_state.want_to_read_since >= NCP_ABSENT_TIMEOUT_SECS)
1595               {
1596                 sim_debug(DBG_DEBUG, &mgp_dev,
1597                           "%s:%d: NCP absent + channel %d masked for %d+ s; "
1598                           "sending terminate interrupt to release in_use\r\n",
1599                           __func__, __LINE__, chan, NCP_ABSENT_TIMEOUT_SECS);
1600                 mgp_dev_state.want_to_read = 0;
1601                 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1602                                          mgp_dev_state.read_unit_chan);
1603               }
1604           }
1605         return;
1606       }
1607 
1608     /* Channel is not masked - clear the stuck timer */
1609     mgp_dev_state.masked_since = 0;
1610   }
1611 
1612   /* Periodic diagnostic: log socket state every 60 seconds at debug level */
1613   {
1614     static time_t last_diag = 0;
1615     time_t now = time(NULL);
1616     if (now - last_diag >= 60)
1617       {
1618         sim_debug(DBG_DEBUG, &mgp_dev,
1619                   "MGP diag: want_to_read=%d pipe=%p unit=%d chan=%d\r\n",
1620                   mgp_dev_state.want_to_read,
1621                   mgp_dev_state.pipe,
1622                   mgp_dev_state.read_unit_idx,
1623                   mgp_dev_state.read_unit_chan);
1624         last_diag = now;
1625       }
1626   }
1627 
1628   /* If the NCP is not connected, try to connect first.  If still not
1629    * connected after NCP_ABSENT_TIMEOUT_SECS, release the IOM channel via
1630    * a terminate interrupt.  Without this release, the channel stays in
1631    * IOM_CMD_PENDING indefinitely; Multics's ioi_masked$timer eventually
1632    * fires and sends a mask PCW while the channel is still "in use",
1633    * producing spurious "doConnectChan: chan N masked while in use" and
1634    * "ioi_masked$timer: Timeout on channel" console messages.
1635    * After the terminate interrupt, Multics re-issues the READ command and
1636    * want_to_read_since is reset, so the cycle repeats quietly every
1637    * NCP_ABSENT_TIMEOUT_SECS seconds until the NCP connects. */
1638   if (mgp_dev_state.pipe == NULL)
1639     {
1640       /* ncp_connect() handles PTW backoff and rate-limiting internally. */
1641       ncp_connect();
1642       if (mgp_dev_state.pipe == NULL)
1643         {
1644           /* Still not connected.  Check whether we have been waiting too long. */
1645           time_t now2 = time(NULL);
1646           if (now2 - mgp_dev_state.want_to_read_since >= NCP_ABSENT_TIMEOUT_SECS)
1647             {
1648               sim_debug(DBG_DEBUG, &mgp_dev,
1649                         "%s:%d: NCP absent for %d+ seconds, releasing IOM channel %d "
1650                         "via terminate interrupt\r\n",
1651                         __func__, __LINE__, NCP_ABSENT_TIMEOUT_SECS,
1652                         mgp_dev_state.read_unit_chan);
1653               mgp_dev_state.want_to_read = 0;
1654               send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1655                                        mgp_dev_state.read_unit_chan);
1656             }
1657           return;
1658         }
1659       /* NCP just connected.  Reset the timestamp so we don't immediately
1660        * time out on the next invocation. */
1661       mgp_dev_state.want_to_read_since = time(NULL);
1662     }
1663 
1664   /* REVERTED (2026-07-24) -- the advance_retry_pending mechanism that used to
1665    * live here (added 2026-07-23 alongside this same commit's read_full()/
1666    * resync-on-error improvements below, which are kept) turned out to cause
1667    * a worse bug than the one it fixed. On failure it retried by calling
1668    * iom_continue_channel() again on the next tick -- but iom_continue_channel()
1669    * unconditionally calls iom_list_service() as its first step, which fetches
1670    * the next DCW and advances LPW_DCW_PTR regardless of what happens
1671    * afterward. Each retry therefore advanced the DCW list an extra step with
1672    * no corresponding fresh packet write, silently racing the channel's
1673    * reported progress (stat.offset / stop_buffer_number on the Multics side)
1674    * ahead of how many buffer slots actually held fresh data. Confirmed via
1675    * mgp_daemon.log tracing on system-a/system-b: this caused Multics's
1676    * mgp_read_dcm_ catch-up loop to sweep past slots last written up to
1677    * buffer_size (16) delivery cycles ago and silently reprocess the stale
1678    * packet still sitting there, producing a steady-state "NAK from Multics"
1679    * storm recurring every ~3 seconds indefinitely, including at idle, and a
1680    * failed CFTP binary transfer. Reverting this mechanism (this branch) fixed
1681    * both directions of CFTP with no NAK storm, and reverified that fast-typing
1682    * character loss over chtn (the bug this was meant to fix) does NOT
1683    * resurface -- that fix already landed independently the day before
1684    * (2026-07-22) in mgp_queue_manager_.pl1's find_packet_connection and
1685    * mgp_read_dcm_.pl1's do-while loop, so this mechanism was redundant as well
1686    * as buggy. A defense-in-depth fix was also added on the Multics side
1687    * (mgp_read_dcm_.pl1 zeroes buffer_data(1) after a successful read) to
1688    * harmlessly skip any future stale-slot revisit regardless of cause, and is
1689    * kept independent of this revert.
1690    */
1691 
1692   /* Try to receive a packet from the NCP */
1693   u_char pkt8[MAX_PKT_BYTES];
1694   int pktlen = ncp_recv_packet(pkt8, MAX_PKT_BYTES);
1695 
1696   if (pktlen <= 0)
1697     {
1698       return; /* nothing available or error */
1699     }
1700 
1701   /* pkt8 is the 8-bit wire format documented in multics_ncp/src/mgp.rs:
1702    * byte 2 = packet_type, byte 4 = frame_number, byte 16 = first data byte. */
1703   sim_debug (DBG_DEBUG, &mgp_dev,
1704              "%s:%d: recv pktlen=%d type=%d frame=%d data0=0x%02x('%c')\r\n",
1705              __func__, __LINE__, pktlen,
1706              pktlen > 2  ? pkt8[2]  : -1,
1707              pktlen > 4  ? pkt8[4]  : -1,
1708              pktlen > 16 ? pkt8[16] : 0,
1709              (pktlen > 16 && pkt8[16] >= 32 && pkt8[16] < 127) ? pkt8[16] : '.');
1710 
1711   sim_debug(DBG_DEBUG, &mgp_dev,
1712             "%s:%d: received %d bytes from NCP for unit %d chan %d\r\n",
1713             __func__, __LINE__, pktlen, iom_unit_idx, chan);
1714 
1715   /* Guard: only deliver if the IOM channel has an active, pinned I/O
1716    * operation.  ioi_$workspace wires all workspace pages (DCW list, IDCWs,
1717    * DDCWs, and data buffers) while in_use=true.  When in_use=false the
1718    * channel has no active I/O: DDCW_ADDR and DDCW_TALLY may be stale or
1719    * corrupted (e.g., left over from the previous iom_continue_channel call),
1720    * and the workspace pin is not guaranteed.  Attempting delivery in this
1721    * state risks writing to a bad address.
1722    *
1723    * want_to_read=1 with in_use=false is the pathological state that produces
1724    * the overnight "fetch_IDSPTW: addr 07766 ptw 000000000000" cascade: mgp_cmd
1725    * was called via iom_continue_channel and stored a stale DDCW_ADDR, then
1726    * send_terminate_interrupt set in_use=false without clearing want_to_read.
1727    * The fix: if in_use is false, discard the packet, clear want_to_read, and
1728    * wait for Multics to re-issue ioi_$connect (which will set in_use=true and
1729    * establish a fresh, valid DDCW for us).                                   */
1730 
1731   if (! p->in_use)
1732     {
1733       (void)sir_warn("%s:%d: chan %d not in active I/O (in_use=false) but want_to_read=1"
1734                      " - discarding packet, clearing want_to_read",
1735                      __func__, __LINE__, chan);
1736       mgp_validate_dcw_state(iom_unit_idx, chan, 129, "process_event-in_use=0");
1737       mgp_dev_state.want_to_read = 0;
1738       return;
1739     }
1740 
1741   /* Convert 8-bit packet to 36-bit words */
1742   word36 buffer[128]; /* 4 header + up to 122 data words */
1743   uint words_processed = 128;
1744   (void)memset(buffer, 0, sizeof(buffer));
1745 
1746   pkt8_to_word36(pkt8, pktlen, buffer, 128);
1747 
1748   sim_debug(DBG_DEBUG, &mgp_dev,
1749             "%s:%d: received %d bytes from NCP for unit %d chan %d\r\n",
1750             __func__, __LINE__, pktlen, iom_unit_idx, chan);
1751 
1752 # if defined(TESTING)
1753   if (sim_deb && (mgp_dev.dctrl & DBG_DEBUG))
1754     {
1755       dumppkt("NCP-Read", buffer, words_processed);
1756     }
1757 # endif
1758 
1759   /* Validate the IOM DMA target PTW before writing packet data.
1760    *
1761    * This is a belt-and-suspenders check that runs AFTER the in_use guard
1762    * above.  If in_use=true, the workspace IS pinned and DDCW_ADDR should
1763    * be valid - but if the DCW list became corrupted (e.g., circular-buffer
1764    * wrap producing a bad DDCW), DDCW_ADDR might point outside the workspace.
1765    * The workspace has only 1 IOM page (820 words for buffer_size=6,
1766    * data_size=129), so any DDCW_ADDR >= 01400 octal (page 1+) has no PTW
1767    * entry and PTW=0.  The stale value "07766" seen in overnight cascades is
1768    * page 3 - far beyond the workspace - and is caught here.
1769    *
1770    * Calling iom_indirect_data_service() with PTW=0 would compute physical
1771    * address ~= 0 and overwrite the IOM mailbox area (addresses 0-0777),
1772    * causing an IOM interrupt storm -> Multics CPU at 100%.
1773    *
1774    * If invalid: close the NCP socket (discarding all queued NOOPs from the
1775    * kernel receive buffer), send one clean terminate interrupt so Multics
1776    * can re-establish the channel, and return without doing the DMA.
1777    *
1778    * max_words=256: cap the PTW range check at 256 words (generous upper
1779    * bound for any NCP packet: header 16 bytes + max data 488 bytes = 504
1780    * bytes = 126 36-bit words, rounded up).  This prevents false positives
1781    * when DDCW_TALLY=0 (IOM interprets as 4096 words, spanning pages 0-3)
1782    * which can occur if iom_continue_channel left the channel in a
1783    * transitional "uff or nothing to send" state.  With max_words=256 and
1784    * DDCW_ADDR=0: end_page=(0+255)/1024=0 -> only page 0 checked -> valid.
1785    * Real corruption (DDCW_ADDR=07766 = page 3) is still caught because
1786    * start_page=3 and that page has no PTW entry.                          */
1787   if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1788     {
1789       (void)sir_warn("%s:%d: invalid DMA PTW on chan %d - closing NCP socket and "
1790                      "sending terminate interrupt to allow Multics channel recovery",
1791                      __func__, __LINE__, chan);
1792       mgp_dev_state.want_to_read = 0;
1793       mgp_dev_state.ptw_failed_at = time(NULL); /* start reconnect backoff */
1794       /* Close the socket.  From the RECEIVER side, close() discards all
1795        * unread data in the kernel receive buffer.  This eliminates the
1796        * remaining queued NCP packets (typically 30 keepalive NOOPs) that
1797        * would otherwise continue triggering failed delivery attempts and
1798        * rapid terminate-interrupt storms after Multics re-issues the READ.
1799        * The NCP detects the closed connection and reconnects; NAK recovery
1800        * then re-synchronizes the MGP frame counters.                      */
1801       if (mgp_dev_state.pipe != NULL)
1802         {
1803           if (mgp_dev_state.pipe) //-V547
1804             {
1805               uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1806               mgp_dev_state.pipe = NULL;
1807             }
1808         }
1809       send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1810                                mgp_dev_state.read_unit_chan);
1811       return;
1812     }
1813 
1814   /* Validate DDCW_ADDR is within the read channel's data buffer area.
1815    *
1816    * ROOT CAUSE FIX for the DDCW_ADDR=0 / DCW-list corruption bug:
1817    *
1818    * After all 6 buffers are delivered, iom_continue_channel reaches the TDCW
1819    * at DCW list offset 12.  iom_list_service calls unpack_DCW for the TDCW
1820    * word; since the TDCW has DATA_ADDRESS=0 (all zero bits, as initialized
1821    * by mgp_read_dcm_.pl1 initialize_workspace), unpack_DCW stores
1822    * p->DDCW_ADDR = 0.  LPW_TALLY then decrements to 0, setting uff=true.
1823    * iom_continue_channel sees uff=true, logs "uff or nothing to send", and
1824    * returns WITHOUT calling mgp_cmd(001) and WITHOUT sending a terminate
1825    * interrupt.  want_to_read=1 persists with p->DDCW_ADDR=0.
1826    *
1827    * The PTW check above passes for DDCW_ADDR=0 with max_words=256 because
1828    * workspace page 0 IS a valid mapped page - the workspace is only 1 IOM
1829    * page.  Without this range check, iom_indirect_data_service(write=true)
1830    * would write 128 words of NCP packet data to workspace offset 0,
1831    * overwriting the entire DCW list (IDCW/DDCW pairs at offsets 0-11).
1832    * The corrupted DCW list then causes downstream DDCW_TALLY=0 readings
1833    * in iom_list_service, triggering the 4096-word IDS walk -> thousands of
1834    * fetch_IDSPTW sir_warn calls.
1835    *
1836    * Fix: reject any DDCW_ADDR below the first valid data buffer offset.
1837    * The minimum valid address is MGP_FIRST_BUFFER_OFFSET (22), which is the
1838    * start of buffer(0) in the workspace.  Send a terminate interrupt so
1839    * Multics re-issues ioi_$connect; the fresh mgp_cmd(001) / get_ddcw() call
1840    * will advance through the TDCW wrap back to IDCW[0]/DDCW[0] and set
1841    * DDCW_ADDR = 22 as expected.                                            */
1842 
1843   if ((int)p->DDCW_ADDR < MGP_FIRST_BUFFER_OFFSET)
1844     {
1845       (void)sir_warn("%s:%d: DDCW_ADDR=%d on chan %d is below first buffer offset %d"
1846                      " (stale DDCW_ADDR; TAL fix prevents TDCW wrap case)"
1847                      " - sending terminate interrupt; NCP socket stays connected",
1848                      __func__, __LINE__, p->DDCW_ADDR, chan,
1849                      MGP_FIRST_BUFFER_OFFSET);
1850       (void)sir_warn("%s:%d: DCW=%llo, IS_IDCW=%d, IS_TDCW=%d, IS_IOTD=%d, IS_IONTP=%d.",
1851                      __func__, __LINE__, p->DCW, IS_IDCW(p), IS_TDCW(p), IS_IOTD(p), IS_IONTP(p));
1852 
1853       /* Do NOT close the NCP socket.  The TDCW wrap is a normal, periodic
1854        * IOM event (happens after every 6th buffer delivery).  The socket
1855        * is healthy - only the IOM channel state needs resetting.  Closing
1856        * the socket would cause unnecessary NCP reconnect cycles (~every 6s)
1857        * which accumulate Multics error counts and eventually trigger channel
1858        * masking.  A terminate interrupt is sufficient: Multics re-issues
1859        * ioi_$connect, the fresh mgp_cmd(001)/get_ddcw() sets a valid
1860        * DDCW_ADDR (>= 22), and delivery resumes on the next NCP packet.   */
1861       mgp_dev_state.want_to_read = 0;
1862       send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1863                                mgp_dev_state.read_unit_chan);
1864       return;
1865     }
1866 
1867   /* Write the packet to the current IOM workspace buffer[N]. */
1868   iom_indirect_data_service(
1869       iom_unit_idx, chan, buffer, &words_processed, true);
1870 
1871   /* Real forward progress: feed the no-progress watchdog above. */
1872   mgp_dev_state.last_progress_time = time(NULL);
1873 
1874   sim_debug(DBG_DEBUG, &mgp_dev,
1875             "%s:%d: wrote %d words to IOM buffer, advancing channel and sending marker interrupt\r\n",
1876             __func__, __LINE__, words_processed);
1877 
1878   /*
1879    * Advance the channel to workspace buffer[N+1] and send a marker
1880    * interrupt.  iom_continue_channel() does the following in order:
1881    *
1882    *   1. Calls iom_list_service() to fetch IDCW[N+1] from the DCW list
1883    *      (LPW_DCW_PTR advances from IDCW[N+1] to DDCW[N+1]).
1884    *   2. Calls d->iom_cmd() for IDCW[N+1]:
1885    *        mgp_cmd(read) -> get_ddcw() -> iom_list_service() reads DDCW[N+1]
1886    *        -> DDCW_ADDR = buffer[N+1], LPW_DCW_PTR = IDCW[N+2].
1887    *        mgp_cmd sets want_to_read=1 and returns IOM_CMD_PENDING.
1888    *   3. Calls send_marker_interrupt():
1889    *        stat.offset = LPW_offset(IDCW[N+2]) - 1 = 2*(N+2) - 1
1890    *        stop_buffer = divide(stat.offset, 2) = N+1
1891    *      mgp_read_dcm_'s loop fires (buffer_number=N != stop_buffer=N+1),
1892    *      calls mgp_read_dim_(N), then increments buffer_number to N+1.
1893    *
1894    * start_io sees running=true (marker) and does NOT reconnect; the
1895    * channel stays pending with DDCW_ADDR pointing to buffer[N+1].
1896    * want_to_read remains 1 (set inside mgp_cmd via iom_continue_channel).
1897    *
1898    * On failure (e.g. DCW list corrupt, "expected IDCW"): iom_continue_channel
1899    * just returns without calling mgp_cmd and without sending any interrupt.
1900    * We detect this via delivery_succeeded: mgp_cmd(READ) sets it to 1 on
1901    * success; we clear it just before calling iom_continue_channel.  After
1902    * 3 consecutive failures we reset want_to_read, close the NCP socket,
1903    * and send a terminate interrupt so Multics can re-establish the channel.
1904    */
1905   mgp_dev_state.delivery_succeeded = 0;
1906   int rc = iom_continue_channel(iom_unit_idx, chan);
1907 
1908   sim_debug (DBG_DEBUG, &mgp_dev,
1909              "%s:%d iom_continue_channel rc=%d delivery_succeeded=%d\r\n",
1910              __func__, __LINE__, rc, mgp_dev_state.delivery_succeeded);
1911 
1912   /* Detect and break "expected IDCW" cascade. */
1913   {
1914     static int consecutive_iom_failures = 0;
1915     if (mgp_dev_state.delivery_succeeded)
1916       {
1917         consecutive_iom_failures = 0;
1918       }
1919     else
1920       {
1921         if (++consecutive_iom_failures >= 3)
1922           {
1923             (void)sir_warn("%s:%d: %d consecutive IOM delivery failures on chan %d; "
1924                            "resetting want_to_read, closing NCP socket, and "
1925                            "sending terminate interrupt",
1926                            __func__, __LINE__, consecutive_iom_failures,
1927                            mgp_dev_state.read_unit_chan);
1928             consecutive_iom_failures = 0;
1929             mgp_dev_state.want_to_read = 0;
1930             /* Close socket to discard remaining queued NCP packets;
1931              * see the comment in the PTW-check block above.           */
1932             if (mgp_dev_state.pipe != NULL)
1933               {
1934                 if (mgp_dev_state.pipe) //-V547
1935                   {
1936                     uv_close((uv_handle_t *)mgp_dev_state.pipe, ncp_close_cb);
1937                     mgp_dev_state.pipe = NULL;
1938                   }
1939               }
1940             if (rc == 0) /* we handle the rc != 0 case below */
1941               send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1942                                        mgp_dev_state.read_unit_chan);
1943           }
1944       }
1945   }
1946 
1947   /* if iom_continue_channel returned a fatal error, terminate the I/O and let the DCM
1948      restart it. */
1949   if (rc != 0)
1950     {
1951       send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1952                                mgp_dev_state.read_unit_chan);
1953     }
1954 }
1955 
1956 #endif /* if defined(WITH_MGP_DEV) */

/* [previous][next][first][last][top][bottom][index][help] */