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

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