root/src/dps8/dps8_mgp.c

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

DEFINITIONS

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

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

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